diff --git a/.github/workflows/docker-publish.yml b/.github/workflows/docker-publish.yml index a65dbe9..9c54d1e 100644 --- a/.github/workflows/docker-publish.yml +++ b/.github/workflows/docker-publish.yml @@ -21,18 +21,18 @@ env: jobs: test: name: Lint, Typecheck & Test - runs-on: ubuntu-latest + runs-on: ubuntu-24.04 steps: - name: Checkout repository - uses: actions/checkout@v4 + uses: actions/checkout@v5 - name: Setup pnpm - uses: pnpm/action-setup@v4 + uses: pnpm/action-setup@v5 - name: Setup Node.js - uses: actions/setup-node@v4 + uses: actions/setup-node@v5 with: - node-version: '20' + node-version: '22' cache: 'pnpm' - name: Install dependencies @@ -51,7 +51,7 @@ jobs: run: pnpm test:coverage - name: Upload coverage reports - uses: codecov/codecov-action@v4 + uses: codecov/codecov-action@v5 with: token: ${{ secrets.CODECOV_TOKEN }} files: ./apps/backend/coverage/coverage-final.json,./apps/frontend/coverage/coverage-final.json @@ -60,22 +60,22 @@ jobs: # E2E tests disabled temporarily - need to investigate timeout issues # e2e: # name: E2E Tests - # runs-on: ubuntu-latest + # runs-on: ubuntu-24.04 # needs: test # env: # NEXT_PUBLIC_API_URL: http://localhost:4000/api # NEXT_PUBLIC_WS_URL: http://localhost:4000 # steps: # - name: Checkout repository - # uses: actions/checkout@v4 + # uses: actions/checkout@v5 # # - name: Setup pnpm - # uses: pnpm/action-setup@v4 + # uses: pnpm/action-setup@v5 # # - name: Setup Node.js - # uses: actions/setup-node@v4 + # uses: actions/setup-node@v5 # with: - # node-version: '20' + # node-version: '22' # cache: 'pnpm' # # - name: Install dependencies @@ -91,7 +91,7 @@ jobs: # run: pnpm test:e2e --project=chromium # # - name: Upload Playwright report - # uses: actions/upload-artifact@v4 + # uses: actions/upload-artifact@v6 # if: always() # with: # name: playwright-report @@ -100,7 +100,7 @@ jobs: build-and-push: name: Build & Push Docker Images - runs-on: ubuntu-latest + runs-on: ubuntu-24.04 needs: test if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository permissions: @@ -121,14 +121,14 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@v4 + uses: actions/checkout@v5 - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v3 + uses: docker/setup-buildx-action@v4 - name: Log in to GitHub Container Registry if: github.event_name != 'pull_request' - uses: docker/login-action@v3 + uses: docker/login-action@v4 with: registry: ${{ env.REGISTRY }} username: ${{ github.actor }} @@ -136,14 +136,14 @@ jobs: - name: Log in to Docker Hub if: github.event_name != 'pull_request' - uses: docker/login-action@v3 + uses: docker/login-action@v4 with: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }} - name: Extract metadata (GHCR) id: meta-ghcr - uses: docker/metadata-action@v5 + uses: docker/metadata-action@v6 with: images: ${{ matrix.image_ghcr }} tags: | @@ -156,7 +156,7 @@ jobs: - name: Extract metadata (Docker Hub) id: meta-dockerhub - uses: docker/metadata-action@v5 + uses: docker/metadata-action@v6 with: images: ${{ matrix.image_dockerhub }} tags: | @@ -168,7 +168,7 @@ jobs: type=raw,value=latest,enable={{is_default_branch}} - name: Build and push to GHCR - uses: docker/build-push-action@v5 + uses: docker/build-push-action@v7 with: context: ${{ matrix.context }} file: ${{ matrix.dockerfile }} @@ -180,7 +180,7 @@ jobs: - name: Build and push to Docker Hub if: github.event_name != 'pull_request' - uses: docker/build-push-action@v5 + uses: docker/build-push-action@v7 with: context: ${{ matrix.context }} file: ${{ matrix.dockerfile }} diff --git a/packages/provider-emby/src/emby.provider.ts b/packages/provider-emby/src/emby.provider.ts index 8ff81fa..861d883 100644 --- a/packages/provider-emby/src/emby.provider.ts +++ b/packages/provider-emby/src/emby.provider.ts @@ -199,10 +199,11 @@ export class EmbyProvider implements MediaServerProvider { const hasNowPlaying = nowPlayingItem !== undefined && playState !== undefined; // Build thumbnail URL for now playing item + const primaryTag = nowPlayingItem?.ImageTags?.['Primary']; let thumbnailUrl: string | undefined; - if (hasNowPlaying && this.config !== null && nowPlayingItem.ImageTags?.['Primary']) { + if (hasNowPlaying && this.config !== null && primaryTag !== undefined && primaryTag !== '') { const baseUrl = this.config.url.replace(/\/$/, ''); - thumbnailUrl = `${baseUrl}/Items/${nowPlayingItem.Id}/Images/Primary?maxWidth=300&tag=${nowPlayingItem.ImageTags['Primary']}&api_key=${this.config.apiKey}`; + thumbnailUrl = `${baseUrl}/Items/${nowPlayingItem.Id}/Images/Primary?maxWidth=300&tag=${primaryTag}&api_key=${this.config.apiKey}`; } return { @@ -245,7 +246,7 @@ export class EmbyProvider implements MediaServerProvider { * Build display name for a now playing item */ private buildItemName(item: { Name: string; SeriesName?: string; Type: string }): string { - if (item.Type === 'Episode' && item.SeriesName) { + if (item.Type === 'Episode' && item.SeriesName !== undefined && item.SeriesName !== '') { return `${item.SeriesName} - ${item.Name}`; } return item.Name; diff --git a/packages/provider-plex/src/plex.client.ts b/packages/provider-plex/src/plex.client.ts index 047c41c..432dde8 100644 --- a/packages/provider-plex/src/plex.client.ts +++ b/packages/provider-plex/src/plex.client.ts @@ -258,10 +258,6 @@ export class PlexClient extends EventEmitter { case 'playing': if (container.PlaySessionStateNotification) { for (const notification of container.PlaySessionStateNotification) { - // Log for debugging - Plex typically sends these every ~10 seconds - console.log( - `[Plex WS] Playing notification: state=${notification.state}, offset=${notification.viewOffset}ms` - ); this.emit('playing', notification); } } diff --git a/packages/provider-seerr/src/seerr.client.spec.ts b/packages/provider-seerr/src/seerr.client.spec.ts index ed5994d..c86ce79 100644 --- a/packages/provider-seerr/src/seerr.client.spec.ts +++ b/packages/provider-seerr/src/seerr.client.spec.ts @@ -4,12 +4,29 @@ import { SeerrClient } from './seerr.client.js'; const mockFetch = vi.fn(); -const ok = (json: unknown) => ({ ok: true, status: 200, json: async () => json }); -const fail = (status: number, text = 'err') => ({ +interface JsonResponse { + ok: true; + status: number; + json: () => Promise; +} + +interface TextResponse { + ok: false; + status: number; + statusText: string; + text: () => Promise; +} + +const ok = (json: unknown): JsonResponse => ({ + ok: true, + status: 200, + json: (): Promise => Promise.resolve(json), +}); +const fail = (status: number, text = 'err'): TextResponse => ({ ok: false, status, statusText: 'x', - text: async () => text, + text: (): Promise => Promise.resolve(text), }); function calledUrl(index = 0): URL { diff --git a/packages/provider-seerr/src/seerr.parser.spec.ts b/packages/provider-seerr/src/seerr.parser.spec.ts index 22df024..3a69d88 100644 --- a/packages/provider-seerr/src/seerr.parser.spec.ts +++ b/packages/provider-seerr/src/seerr.parser.spec.ts @@ -269,7 +269,7 @@ describe('SEERR_CORRELATION_PATTERNS', () => { }); it('extracts ids', () => { - const find = (name: string, text: string) => + const find = (name: string, text: string): string | undefined => text.match(SEERR_CORRELATION_PATTERNS.find((p) => p.name === name)?.pattern as RegExp)?.[1]; expect(find('requestId', 'Request #12 approved')).toBe('12'); expect(find('tmdbId', 'lookup tmdbId=603 failed')).toBe('603'); @@ -277,7 +277,7 @@ describe('SEERR_CORRELATION_PATTERNS', () => { }); it('extracts ids from JSON-style metadata', () => { - const find = (name: string, text: string) => + const find = (name: string, text: string): string | undefined => text.match(SEERR_CORRELATION_PATTERNS.find((p) => p.name === name)?.pattern as RegExp)?.[1]; expect(find('tmdbId', '{"tmdbId":1396,"mediaType":"tv"}')).toBe('1396'); expect(find('userId', '{"userId":42}')).toBe('42'); diff --git a/packages/provider-seerr/src/seerr.provider.spec.ts b/packages/provider-seerr/src/seerr.provider.spec.ts index 90f4050..4147007 100644 --- a/packages/provider-seerr/src/seerr.provider.spec.ts +++ b/packages/provider-seerr/src/seerr.provider.spec.ts @@ -3,21 +3,45 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; import { SEERR_LOG_FILE_CONFIG } from './seerr.parser.js'; import { SeerrProvider } from './seerr.provider.js'; +import type { NormalizedActivity } from '@logarr/core'; + const mockFetch = vi.fn(); -const ok = (json: unknown) => ({ ok: true, status: 200, json: async () => json }); -const fail = (status: number, text = 'err') => ({ +interface JsonResponse { + ok: true; + status: number; + json: () => Promise; +} + +interface TextResponse { + ok: false; + status: number; + statusText: string; + text: () => Promise; +} + +interface SeerrPage { + pageInfo: { pages: number; pageSize: number; results: number; page: number }; + results: unknown[]; +} + +const ok = (json: unknown): JsonResponse => ({ + ok: true, + status: 200, + json: (): Promise => Promise.resolve(json), +}); +const fail = (status: number, text = 'err'): TextResponse => ({ ok: false, status, statusText: 'x', - text: async () => text, + text: (): Promise => Promise.resolve(text), }); -type Routes = Record unknown>; +type Routes = Record JsonResponse | TextResponse>; /** Route-based fetch mock keyed on pathname (after /api/v1) */ function route(routes: Routes): void { - mockFetch.mockImplementation(async (url: string) => { + mockFetch.mockImplementation((url: string): JsonResponse | TextResponse => { const path = new URL(url).pathname.replace('/api/v1', ''); const handler = routes[path]; return handler === undefined ? fail(404, 'not found') : handler(); @@ -69,7 +93,7 @@ const issues = [ { id: 6, issueType: 1, status: 2, createdAt: '2026-10-10T12:04:00.000Z' }, ]; -const paged = (results: unknown[]) => ({ +const paged = (results: unknown[]): SeerrPage => ({ pageInfo: { pages: 1, pageSize: 50, results: results.length, page: 1 }, results, }); @@ -349,7 +373,10 @@ describe('SeerrProvider', () => { it('pages while items are newer than since and takes only one page without since', async () => { const p = await connected(); - const page = (start: number, iso: string) => + const page = ( + start: number, + iso: string + ): Array<{ id: number; status: number; updatedAt: string }> => Array.from({ length: 50 }, (_, i) => ({ id: start + i, status: 2, @@ -436,13 +463,13 @@ describe('SeerrProvider', () => { }); describe('edge cases', () => { - const one = async (request: unknown) => { + const one = async (request: unknown): Promise => { const p = await connected(); route({ '/request': () => ok(paged([request])), '/issue': () => ok(paged([])) }); const [act] = await p.getActivity(); return act; }; - const oneIssue = async (issue: unknown) => { + const oneIssue = async (issue: unknown): Promise => { const p = await connected(); route({ '/request': () => ok(paged([])), '/issue': () => ok(paged([issue])) }); const [act] = await p.getActivity();