Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,9 @@ WHISPARR_API_KEY=
# Lidarr
LIDARR_URL=
LIDARR_API_KEY=
# Seerr (also Overseerr / Jellyseerr) - use the ADMIN API key (Settings > General)
SEERR_URL=
SEERR_API_KEY=

# =============================================================================
# SECURITY & AUTHENTICATION
Expand Down Expand Up @@ -156,6 +159,11 @@ WHISPARR_LOGS_PATH=
# Linux: ~/.config/Lidarr/logs
# Docker: /path/to/lidarr/config/logs
LIDARR_LOGS_PATH=
# Seerr / Overseerr / Jellyseerr examples (container path: /seerr-logs):
# Host path to the <config>/logs directory (files seerr-YYYY-MM-DD.log;
# legacy overseerr-/jellyseerr- prefixes are supported)
# Docker: /path/to/seerr/config/logs
SEERR_LOGS_PATH=

# =============================================================================
# MULTIPLE INSTANCES (optional - for multiple servers of the same type)
Expand Down Expand Up @@ -205,3 +213,6 @@ WHISPARR_LOGS_PATH_3=
LIDARR_LOGS_PATH_1=
LIDARR_LOGS_PATH_2=
LIDARR_LOGS_PATH_3=
SEERR_LOGS_PATH_1=
SEERR_LOGS_PATH_2=
SEERR_LOGS_PATH_3=
6 changes: 6 additions & 0 deletions .env.tpl
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,11 @@ WHISPARR_API_KEY={{ op://Development/Logarr-Dev/WHISPARR_API_KEY }}
LIDARR_URL={{ op://Development/Logarr-Dev/LIDARR_URL }}
LIDARR_API_KEY={{ op://Development/Logarr-Dev/LIDARR_API_KEY }}

# Seerr (also Overseerr / Jellyseerr) - not in the vault yet; add the items, then
# replace these with op:// references
# SEERR_URL=
# SEERR_API_KEY=

# =============================================================================
# SECURITY & AUTHENTICATION
# =============================================================================
Expand All @@ -111,3 +116,4 @@ RADARR_LOGS_PATH={{ op://Development/Logarr-Dev/RADARR_LOGS_PATH }}
PROWLARR_LOGS_PATH={{ op://Development/Logarr-Dev/PROWLARR_LOGS_PATH }}
WHISPARR_LOGS_PATH={{ op://Development/Logarr-Dev/WHISPARR_LOGS_PATH }}
LIDARR_LOGS_PATH={{ op://Development/Logarr-Dev/LIDARR_LOGS_PATH }}
# SEERR_LOGS_PATH=
2 changes: 1 addition & 1 deletion .github/agents/my-agent.agent.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ This is a pnpm + Turborepo TypeScript monorepo.
- `apps/backend`: NestJS 11 API with Drizzle/Postgres, Redis, Socket.IO, Swagger, auth/RBAC, audit logging, retention, ingestion, settings, and proxying.
- `apps/frontend`: Next.js 16 App Router UI with React 19, Tailwind 4, Radix UI, React Query, and realtime socket hooks.
- `packages/core`: shared provider interfaces and normalized cross-service types.
- `packages/provider-*`: provider adapters for Plex, Jellyfin, Emby, Sonarr, Radarr, Prowlarr, Whisparr, Lidarr, plus shared `provider-arr` base logic.
- `packages/provider-*`: provider adapters for Plex, Jellyfin, Emby, Sonarr, Radarr, Prowlarr, Whisparr, Lidarr, Seerr, plus shared `provider-arr` base logic.
- `e2e`: Playwright coverage for key UI flows.
- `.github/workflows/docker-publish.yml`: CI gates and container publishing.

Expand Down
16 changes: 13 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
[![Docker](https://img.shields.io/badge/Docker-Ready-2496ED?logo=docker&logoColor=white)](https://www.docker.com/)
[![License](https://img.shields.io/badge/License-MIT-green.svg)](LICENSE)

Unified logging, intelligent issue detection, and AI-powered analysis for Plex, Jellyfin, Emby, Sonarr, Radarr, Prowlarr, Whisparr, Lidarr, and more.
Unified logging, intelligent issue detection, and AI-powered analysis for Plex, Jellyfin, Emby, Sonarr, Radarr, Prowlarr, Whisparr, Lidarr, Seerr, and more.

[Quick Start](#quick-start) · [Features](#features) · [Screenshots](#screenshots) · [Documentation](#documentation)

Expand Down Expand Up @@ -101,7 +101,9 @@ _Configure multiple AI providers for issue analysis (Anthropic, OpenAI, Google,
| **Sonarr** | ✅ Supported | **Emby** | ✅ Supported |
| **Radarr** | ✅ Supported | **Whisparr** | ✅ Supported |
| **Prowlarr** | ✅ Supported | **Lidarr** | ✅ Supported |
| **Kodi** | 🚧 Planned | | |
| **Seerr** | ✅ Supported | **Kodi** | 🚧 Planned |

> **Seerr** also covers Overseerr and Jellyseerr (same API). Use the **admin** API key (Settings → General). Activity comes from media requests and user-reported issues; application logs come from file ingestion of `seerr-YYYY-MM-DD.log` (legacy `overseerr-`/`jellyseerr-` files are supported).

> Logarr uses a provider architecture—adding support for new servers is straightforward. [Contributions welcome!](#contributing)

Expand Down Expand Up @@ -179,13 +181,14 @@ flowchart TB
PR[Prowlarr]
WH[Whisparr]
LI[Lidarr]
SE[Seerr]
end

FE <--> API
FE <--> WS
API <--> PG
API <--> RD
API <--> PX & JF & EM & SO & RA & PR & WH & LI
API <--> PX & JF & EM & SO & RA & PR & WH & LI & SE
```

### How It Works
Expand Down Expand Up @@ -274,6 +277,9 @@ RADARR_LOGS_PATH=/path/to/radarr/config/logs
PROWLARR_LOGS_PATH=/path/to/prowlarr/config/logs
WHISPARR_LOGS_PATH=/path/to/whisparr/config/logs
LIDARR_LOGS_PATH=/path/to/lidarr/config/logs

# Request Managers
SEERR_LOGS_PATH=/path/to/seerr/config/logs
```

2. **Restart Logarr** — The docker-compose mounts these paths automatically.
Expand All @@ -290,6 +296,7 @@ LIDARR_LOGS_PATH=/path/to/lidarr/config/logs
| Prowlarr | `/prowlarr-logs` |
| Whisparr | `/whisp-logs` |
| Lidarr | `/lidarr-logs` |
| Seerr | `/seerr-logs` |

#### Multiple Instances (e.g., Movies vs Shorts, 1080p vs 4K)

Expand Down Expand Up @@ -335,6 +342,7 @@ Simply add multiple servers in the UI using the same server type but different U
| Prowlarr | `/config/logs` | `~/.config/Prowlarr/logs` | `C:\ProgramData\Prowlarr\logs` |
| Whisparr | `/config/logs` | `~/.config/Whisparr/logs` | `C:\ProgramData\Whisparr\logs` |
| Lidarr | `/config/logs` | `~/.config/Lidarr/logs` | `C:\ProgramData\Lidarr\logs` |
| Seerr | `/app/config/logs` | `~/.config/seerr/logs` | `C:\ProgramData\seerr\logs` |

---

Expand Down Expand Up @@ -377,6 +385,7 @@ logarr/
│ ├── provider-prowlarr/ # Prowlarr integration
│ ├── provider-whisparr/ # Whisparr integration
│ ├── provider-lidarr/ # Lidarr integration
│ ├── provider-seerr/ # Seerr integration (Overseerr/Jellyseerr compatible)
│ └── provider-arr/ # Base *arr provider
└── docker-compose.yml
```
Expand Down Expand Up @@ -490,6 +499,7 @@ pnpm test:e2e # Run Playwright E2E tests
| `@logarr/provider-emby` | Emby log parser |
| `@logarr/provider-whisparr` | Whisparr provider |
| `@logarr/provider-lidarr` | Lidarr provider |
| `@logarr/provider-seerr` | Seerr provider and log parser |

> Coverage thresholds are enforced at 80% for statements, branches, functions, and lines.

Expand Down
2 changes: 2 additions & 0 deletions apps/backend/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,8 @@ COPY --from=builder --chown=nestjs:nodejs /app/packages/provider-whisparr/packag
COPY --from=builder --chown=nestjs:nodejs /app/packages/provider-whisparr/dist ./packages/provider-whisparr/dist
COPY --from=builder --chown=nestjs:nodejs /app/packages/provider-lidarr/package.json ./packages/provider-lidarr/
COPY --from=builder --chown=nestjs:nodejs /app/packages/provider-lidarr/dist ./packages/provider-lidarr/dist
COPY --from=builder --chown=nestjs:nodejs /app/packages/provider-seerr/package.json ./packages/provider-seerr/
COPY --from=builder --chown=nestjs:nodejs /app/packages/provider-seerr/dist ./packages/provider-seerr/dist

# Copy backend
COPY --from=builder --chown=nestjs:nodejs /app/apps/backend/package.json ./apps/backend/
Expand Down
2 changes: 1 addition & 1 deletion apps/backend/Dockerfile.dev
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ COPY apps/backend/ ./apps/backend/
RUN pnpm install

# Build only the packages (not backend itself - that will be done in watch mode)
RUN pnpm build --filter @logarr/core --filter @logarr/provider-arr --filter @logarr/provider-jellyfin --filter @logarr/provider-sonarr --filter @logarr/provider-radarr --filter @logarr/provider-prowlarr --filter @logarr/provider-plex --filter @logarr/provider-emby --filter @logarr/provider-whisparr --filter @logarr/provider-lidarr
RUN pnpm build --filter @logarr/core --filter @logarr/provider-arr --filter @logarr/provider-jellyfin --filter @logarr/provider-sonarr --filter @logarr/provider-radarr --filter @logarr/provider-prowlarr --filter @logarr/provider-plex --filter @logarr/provider-emby --filter @logarr/provider-whisparr --filter @logarr/provider-lidarr --filter @logarr/provider-seerr

EXPOSE 4002

Expand Down
1 change: 1 addition & 0 deletions apps/backend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
"@logarr/provider-plex": "workspace:*",
"@logarr/provider-prowlarr": "workspace:*",
"@logarr/provider-radarr": "workspace:*",
"@logarr/provider-seerr": "workspace:*",
"@logarr/provider-sonarr": "workspace:*",
"@logarr/provider-whisparr": "workspace:*",
"@nestjs/axios": "^4.0.1",
Expand Down
3 changes: 3 additions & 0 deletions apps/backend/src/modules/ingestion/ingestion.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { LidarrProvider } from '@logarr/provider-lidarr';
import { PlexProvider, PlexClient } from '@logarr/provider-plex';
import { ProwlarrProvider } from '@logarr/provider-prowlarr';
import { RadarrProvider } from '@logarr/provider-radarr';
import { SeerrProvider } from '@logarr/provider-seerr';
import { SonarrProvider } from '@logarr/provider-sonarr';
import { WhisparrProvider } from '@logarr/provider-whisparr';
import { Injectable, Inject, Logger, OnModuleInit, OnModuleDestroy } from '@nestjs/common';
Expand Down Expand Up @@ -85,6 +86,8 @@ export class IngestionService implements OnModuleInit, OnModuleDestroy {

const lidarrProvider = new LidarrProvider();
this.providers.set(lidarrProvider.id, lidarrProvider);
const seerrProvider = new SeerrProvider();
this.providers.set(seerrProvider.id, seerrProvider);
}

async onModuleInit() {
Expand Down
1 change: 1 addition & 0 deletions apps/backend/src/modules/proxy/proxy.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ export class ProxyService {
lidarr: '/api/v1',
readarr: '/api/v1',
whisparr: '/api/v1',
seerr: '/api/v1',
qbittorrent: '/api/v2',
sabnzbd: '/api',
};
Expand Down
7 changes: 7 additions & 0 deletions apps/backend/src/modules/servers/server-seed.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,13 @@ const ENV_SERVER_CONFIGS: EnvServerConfig[] = [
name: 'Lidarr',
containerLogPath: '/lidarr-logs',
},
{
urlEnvKey: 'SEERR_URL',
apiKeyEnvKey: 'SEERR_API_KEY',
providerId: 'seerr',
name: 'Seerr',
containerLogPath: '/seerr-logs',
},
];

@Injectable()
Expand Down
3 changes: 3 additions & 0 deletions apps/backend/src/modules/servers/servers.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { LidarrProvider } from '@logarr/provider-lidarr';
import { PlexProvider } from '@logarr/provider-plex';
import { ProwlarrProvider } from '@logarr/provider-prowlarr';
import { RadarrProvider } from '@logarr/provider-radarr';
import { SeerrProvider } from '@logarr/provider-seerr';
import { SonarrProvider } from '@logarr/provider-sonarr';
import { WhisparrProvider } from '@logarr/provider-whisparr';
import { Injectable, Inject, NotFoundException, forwardRef, Logger } from '@nestjs/common';
Expand Down Expand Up @@ -55,6 +56,8 @@ export class ServersService {

const lidarrProvider = new LidarrProvider();
this.providers.set(lidarrProvider.id, lidarrProvider);
const seerrProvider = new SeerrProvider();
this.providers.set(seerrProvider.id, seerrProvider);
}

async findAll() {
Expand Down
3 changes: 3 additions & 0 deletions apps/frontend/src/app/(dashboard)/logs/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -160,6 +160,9 @@ function LogDetailModal({
case 'lidarr':
// *arr apps: system/events shows searchable event log
return `${baseUrl}/system/events`;
case 'seerr':
// Seerr/Overseerr/Jellyseerr: Settings > Logs
return `${baseUrl}/settings/logs`;
default:
return null;
}
Expand Down
25 changes: 25 additions & 0 deletions apps/frontend/src/components/provider-icon.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,31 @@ describe('providerMeta', () => {
});
});

describe('Seerr icon', () => {
it('renders the dashboard-icons image instead of the default icon', () => {
const { container } = render(<ProviderIcon providerId="seerr" className="custom-class" />);
const img = container.querySelector('img');
expect(img).not.toBeNull();
expect(img?.getAttribute('src')).toBe(
'https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/svg/seerr.svg'
);
expect(img?.classList.contains('custom-class')).toBe(true);
expect(container.querySelector('svg')).toBeNull();
});
});

describe('Seerr metadata', () => {
it('should have Seerr entry', () => {
expect(providerMeta.seerr.name).toBe('Seerr');
expect(providerMeta.seerr.color).toBe('#8B5CF6');
expect(providerMeta.seerr.bgColor).toBe('bg-violet-500/10');
});

it('should resolve case-insensitively via getProviderMeta', () => {
expect(getProviderMeta('SEERR').name).toBe('Seerr');
});
});

describe('getProviderMeta', () => {
it('should return Whisparr metadata', () => {
const meta = getProviderMeta('whisparr');
Expand Down
13 changes: 13 additions & 0 deletions apps/frontend/src/components/provider-icon.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -568,6 +568,17 @@ function EmbyIcon({ className }: { className?: string }) {
);
}

// Seerr logo - Uses external CDN icon
function SeerrIcon({ className }: { className?: string }) {
return (
<img
src="https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/svg/seerr.svg"
alt="Seerr"
className={className}
/>
);
}

// Official Docker logo from dashboard-icons
function DockerIcon({ className }: { className?: string }) {
return (
Expand Down Expand Up @@ -618,6 +629,7 @@ const providerIcons: Record<string, React.ComponentType<{ className?: string }>>
whisparr: WhisparrIcon,
lidarr: LidarrIcon,
readarr: ReadarrIcon,
seerr: SeerrIcon,
sqlite: SQLiteIcon,
openai: OpenAIIcon,
anthropic: AnthropicIcon,
Expand Down Expand Up @@ -645,6 +657,7 @@ export const providerMeta: Record<string, { name: string; color: string; bgColor
whisparr: { name: 'Whisparr', color: '#FF69B4', bgColor: 'bg-pink-500/10' },
lidarr: { name: 'Lidarr', color: '#00C853', bgColor: 'bg-green-500/10' },
readarr: { name: 'Readarr', color: '#8B0000', bgColor: 'bg-red-500/10' },
seerr: { name: 'Seerr', color: '#8B5CF6', bgColor: 'bg-violet-500/10' },
sqlite: { name: 'SQLite', color: '#003B57', bgColor: 'bg-slate-500/10' },
openai: { name: 'OpenAI', color: '#10A37F', bgColor: 'bg-emerald-500/10' },
anthropic: { name: 'Anthropic', color: '#D4A574', bgColor: 'bg-orange-500/10' },
Expand Down
51 changes: 51 additions & 0 deletions apps/frontend/src/lib/integrations.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,57 @@ describe('Integrations Registry', () => {
});
});

describe('Seerr integration', () => {
it('should be registered with correct metadata', () => {
const seerr = getIntegrationById('seerr');
expect(seerr).toBeDefined();
expect(seerr?.name).toBe('Seerr');
expect(seerr?.description).toContain('Overseerr');
expect(seerr?.description).toContain('Jellyseerr');
expect(seerr?.category).toBe('media_requests');
expect(seerr?.status).toBe('available');
expect(seerr?.connectionType).toBe('api');
expect(seerr?.defaultPort).toBe(5055);
});

it('should use a dashboard-icons slug', () => {
const seerr = getIntegrationById('seerr');
expect(seerr?.iconType).toBe('dashboard-icons');
expect(seerr?.icon).toBe('seerr');
});

it('should have name, url, admin apiKey and logPath fields', () => {
const seerr = getIntegrationById('seerr');
expect(seerr?.configFields.map((f) => f.name)).toEqual(['name', 'url', 'apiKey', 'logPath']);
expect(seerr?.configFields.find((f) => f.name === 'apiKey')?.label).toBe('Admin API Key');
});

it('should have correct capabilities', () => {
expect(getIntegrationById('seerr')?.capabilities).toEqual({
realTimeLogs: true,
activityLog: true,
sessions: false,
webhooks: false,
metrics: false,
});
});

it('should be available and replace the separate Overseerr/Jellyseerr entries', () => {
expect(getAvailableIntegrations().map((i) => i.id)).toContain('seerr');
expect(getIntegrationById('overseerr')).toBeUndefined();
expect(getIntegrationById('jellyseerr')).toBeUndefined();
const available = getIntegrationsByCategory('media_requests').filter(
(i) => i.status !== 'coming_soon'
);
expect(available.map((i) => i.id)).toEqual(['seerr']);
});

it('should be searchable by Overseerr and Jellyseerr', () => {
expect(searchIntegrations('overseerr').map((i) => i.id)).toContain('seerr');
expect(searchIntegrations('jellyseerr').map((i) => i.id)).toContain('seerr');
});
});

describe('getIntegrationsByCategory', () => {
it('should include Whisparr in arr_stack category', () => {
const arrIntegrations = getIntegrationsByCategory('arr_stack');
Expand Down
Loading