diff --git a/CLAUDE.md b/CLAUDE.md index abd136b93..39e53cd24 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -1602,7 +1602,7 @@ stream_id`); it drops `series_id`/`movie_id`, so the builder pins the - Discover pages (clickable metadata chips, issue #1449): year, genre, and country chips on all four detail views (Xtream VOD/series, shared Stalker detail, Stalker series) are clickable when TMDB matched the item — the merges emit structured `tmdb_genres`/`tmdb_countries` (+ `tmdb_media_type` on Stalker), the year chip renders from provider data so it is gated on the navigation TARGET instead of the item's identity — `createDiscoverFacetNavigation()` offers a facet only when a playlist resolves AND `TmdbEnrichmentService.isEnabled()`, since Discover reads its results from TMDB (gating on `typeof tmdb_id === 'number'` is WRONG: the field is `number | string` and a provider-sent number satisfied it with enrichment never having run) — and clicks navigate via `discoverLink()` (`libs/portal/shared/util`) to the portal-scoped `discover` route (`?type&year&genre&genreLabel&country&countryLabel`). The route containers (`XtreamDiscoverRouteComponent`, `StalkerDiscoverRouteComponent`) clone the actor-page pattern: TMDB `/discover` top-5-pages by popularity via `TmdbDiscoverService` (session-only Map cache, never persisted to `tmdb_metadata`), matched against the catalog (Xtream in-memory index / all-portals `DB_MATCH_TITLES`; Stalker search-prefill), rendered by `DiscoverViewComponent`. Facets change via query params on the same route instance, so the discover load is guarded by recency (`createLatestRequestGuard()`) — A→B→A leaves two in-flight requests sharing one `discoverFacetKey()` — while the catalog match uses the guard for its spinner and the facet key for its results; availability also waits for catalog readiness (in-flight flags, not `isContentInitialized`, so a failed import still settles). See "Discover Pages" in `docs/architecture/tmdb-metadata-enrichment.md` - Actor page "All portals" scope (Electron only): batched `DB_MATCH_TITLES` worker op (trigram FTS over all imported Xtream playlists, `apps/electron-backend/src/app/database/operations/title-match.operations.ts`); `normalizeTitle` is shared renderer/worker via `libs/shared/interfaces/src/lib/title-normalization.util.ts` - All `DB_MATCH_TITLES` consumers (Trending rail, "Because you watched" recommendations rail, cross-portal Similar rail, actor "All portals" scope) resolve the worker's flat result list through the shared `groupTitleMatchesByKey()` + `pickTitleMatch()` in `libs/services/src/lib/catalog-title-match.service.ts`. The grouping keeps EVERY row per `type:exactNormalizedTitle` on purpose — the year that separates same-titled rows belongs to the lookup, which the grouping cannot see, so collapsing first made a catalog holding both "Dune 1984" and "Dune 2021" drop whichever copy the user actually owns. `pickTitleMatch` then ranks year-compatible rows by evidence (exact year → untagged → any compatible) across all title aliases at once; only the recommendations rail passes an alias (TMDB `original_title`, via `candidateLookup()`). Multi-source VOD discovery deliberately stays off these helpers: there every copy is a distinct selectable source, not one best answer -- Opt-in via `Settings > Metadata (TMDB)` (sends titles to TMDB); the section also has a "check key" button and a cache panel (row count + payload size, with a clear button); optional user API key overrides the embedded default (`DEFAULT_TMDB_API_KEY` in `libs/services/src/lib/tmdb/tmdb-config.ts` — an empty placeholder in the repo by design; the real key lives in the `TMDB_API_KEY` GitHub Actions secret and is injected at CI build time by `tools/tmdb/inject-tmdb-key.mjs`) +- Opt-in via `Settings > Metadata (TMDB)` (sends titles to TMDB); the section also has a "check key" button and a cache panel (row count + payload size, with a clear button). Distributed builds ship without a shared key; users supply their own. `DEFAULT_TMDB_API_KEY` in `libs/services/src/lib/tmdb/tmdb-config.ts` is empty by default; `tools/tmdb/inject-tmdb-key.mjs` still supports optional CI injection from `TMDB_API_KEY`, but is a no-op when it is unset. A user key takes precedence over any injected default; without either key, enrichment stays inactive even when enabled. Requiring a personal key is project policy, not a categorical TMDB terms restriction; see the canonical "Settings and API Key" section in `docs/architecture/tmdb-metadata-enrichment.md`. - Match confidence: a provider `tmdb_id` is a strong hint, not gospel — its payload is weighed against the item (`assessProviderId`: title or year agrees → use it; both years known and incompatible → the search may take over; title-only mismatch → keep it, since TMDB localizes titles). A 404 marks the id dead (`badProviderId:` row); transient failures never do. Without a usable id: normalized-title + year (±1) search with a strict gate — no confident match means no enrichment - Detail views render provider data immediately; enrichment patches the selection asynchronously (staleness-guarded) - Cached in SQLite `tmdb_metadata` (Electron, via DB worker ops `DB_GET/SET_TMDB_METADATA`, plus `DB_GET_TMDB_CACHE_STATS` / `DB_CLEAR_TMDB_METADATA` behind the settings cache panel) or in-memory (PWA); localized via the app language setting. Search-match lookup keys are versioned, and connection startup removes obsolete unversioned rows once through the `migration:tmdb-search-lookup-v2-cache-cleanup:v1` app-state marker. diff --git a/README.md b/README.md index 298ace49d..d593d19b6 100644 --- a/README.md +++ b/README.md @@ -53,7 +53,7 @@ The application is a cross-platform, open-source project built with Electron and **Discovery & metadata** - Global search across live TV, movies, and series _(desktop)_ -- TMDB enrichment (opt-in) — plots, cast & crew, trailers, ratings, artwork, a "Similar" rail, clickable actor pages, and a trending dashboard rail _(trending rail: desktop)_ +- TMDB enrichment (opt-in, requires your own TMDB API key) — plots, cast & crew, trailers, ratings, artwork, a "Similar" rail, clickable actor pages, and a trending dashboard rail _(trending rail: desktop)_ - Dashboard with recently watched & continue-watching **Organization** diff --git a/docs/architecture/tmdb-metadata-enrichment.md b/docs/architecture/tmdb-metadata-enrichment.md index 420b4679e..9bbf515b3 100644 --- a/docs/architecture/tmdb-metadata-enrichment.md +++ b/docs/architecture/tmdb-metadata-enrichment.md @@ -24,7 +24,8 @@ Related: "Movie Recognition (VOD Detail View)" in `docs/architecture/m3u-playlist-module.md`. - Enrichment is **opt-in** via `Settings > Metadata (TMDB)` because it sends - movie/series titles to a third-party API. Default: disabled. + movie/series titles to a third-party API. Default: disabled. Users supply + their own TMDB API key; distributed builds ship without a shared key. - The detail view renders provider data **immediately**; enrichment runs asynchronously and patches the selected item once TMDB responds. A staleness guard drops responses that arrive after the user navigated away. @@ -44,7 +45,7 @@ store imports): | File | Responsibility | | ---------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------- | -| `tmdb-config.ts` | API/image base URLs, embedded default API key, cache TTLs, app-language → TMDB-language mapping | +| `tmdb-config.ts` | API/image base URLs, empty default API key placeholder, cache TTLs, app-language → TMDB-language mapping | | `tmdb.types.ts` | TMDB v3 response shapes (search, details with credits) | | `tmdb-api.service.ts` | Thin `fetch`-based client (TMDB supports CORS; works in Electron renderer and PWA). Accepts v3 keys (`api_key` param) and v4 tokens (Bearer) | | `tmdb-matcher.ts` | Title normalization, year extraction, and the match-confidence gate (pure functions) | @@ -505,8 +506,7 @@ The panel's full path — settings button, preload bridge, DB worker, real SQLite — is covered by `@settings @electron @persistence sizes and clears the TMDB metadata cache` in `apps/electron-backend-e2e/src/settings.e2e.ts`. It seeds a row through `dbSetTmdbMetadata` rather than through enrichment, -which needs an API key that builds outside the release pipeline do not -carry. +so the test does not depend on a TMDB API key or external API access. The PWA uses a session-scoped in-memory map (acceptable for phase 1; TMDB supports CORS so the PWA calls the API directly). @@ -515,7 +515,7 @@ supports CORS so the PWA calls the API directly). `Settings.tmdb?: { enabled: boolean; apiKey?: string }` (`libs/shared/interfaces/src/lib/tmdb.interface.ts`). The settings page has -a "Metadata (TMDB)" section: enable toggle, optional API key override with a +a "Metadata (TMDB)" section: enable toggle, user-provided API key with a "check key" button (validates against `/configuration`), the M3U movie-recognition toggle (root-level `Settings.m3uVodDetails`, shown only while TMDB is enabled and bound via `[formControl]` because it is not part of @@ -525,19 +525,30 @@ lot. Sizing is a full table scan, so it runs only once that section is the active one, and a failed read or clear says so instead of showing an empty cache. -The embedded default key lives in `DEFAULT_TMDB_API_KEY` -(`libs/services/src/lib/tmdb/tmdb-config.ts`) and is an **empty placeholder -in the repository by design**: the real key is stored in the `TMDB_API_KEY` -GitHub Actions secret and injected at CI build time by -`tools/tmdb/inject-tmdb-key.mjs` (step "Inject TMDB API key" in -`build-and-make.yaml`, before the frontend build). Rationale: TMDB keys are -free and extractable from any client binary regardless, but keeping the key -out of the public repo prevents trivial scraping and fork propagation. Never -commit a real key; never reuse keys found in other repositories. +Distributed builds ship **without a shared TMDB API key**. To use +metadata enrichment, users enable it and enter their own key under +`Settings > Metadata (TMDB)`. Key registration is available through the +[TMDB account settings](https://www.themoviedb.org/settings/api); see the +[TMDB API FAQ](https://developer.themoviedb.org/docs/faq). -With no key available (empty default and no user override in settings), -enrichment stays inactive even when the toggle is on — fork PRs and local -dev builds fall into this mode automatically. +This is a project distribution policy, not a claim that TMDB's terms +categorically prohibit embedding an application key. A key shipped in a +client can be extracted and misused; revoking a shared key could interrupt +metadata access for everyone using it. TMDB also applies +[service rate limits](https://developer.themoviedb.org/docs/rate-limiting), +so a personal key does not remove the need to respect throttling. + +`DEFAULT_TMDB_API_KEY` in `libs/services/src/lib/tmdb/tmdb-config.ts` is +empty by default. The build tooling still supports optional injection: +`tools/tmdb/inject-tmdb-key.mjs` reads `TMDB_API_KEY` during the "Inject +TMDB API key" step in `.github/workflows/build-and-make.yaml`. With no +non-empty secret, that step is a no-op. Its presence does not mean release +builds include a key; leave the secret unset for the no-shared-key policy. +Never commit a real key or reuse keys found in other repositories. + +At runtime, a non-empty user key takes precedence over an injected default. +With neither available, enrichment stays inactive even when the toggle is +on. This applies to release, local development, and fork builds alike. ## Failure Behavior diff --git a/docs/architecture/tmdb-roadmap.md b/docs/architecture/tmdb-roadmap.md index dbe0344e2..67adfa664 100644 --- a/docs/architecture/tmdb-roadmap.md +++ b/docs/architecture/tmdb-roadmap.md @@ -118,7 +118,7 @@ Canonical titles in grids, rating/year badges on cards, genre/decade browse, fam **Blockers, all hard:** -- **The shared release key.** `.github/workflows/build-and-make.yaml:402-406` injects one `TMDB_API_KEY` into every installed copy. Bulk backfill on that key is exactly the traffic shape TMDB throttles ("upper limits to help mitigate needlessly high bulk scraping"). Must be **hard-gated to a user-supplied personal key**, not merely "explicit consent". +- **User-supplied key and throttling.** Distributed builds ship without a shared key; the workflow retains optional injection support (see [Settings and API Key](./tmdb-metadata-enrichment.md#settings-and-api-key)). Bulk backfill must require a user-supplied key even in a build with an injected default. A personal key does not exempt bulk traffic from [TMDB rate limits](https://developer.themoviedb.org/docs/rate-limiting); explicit consent alone is insufficient. - **No 429/backoff machinery exists** (Theme A5 is a prerequisite). - **Per-item IPC.** 40k warm writes = 40k+ worker round-trips. Batched cache ops are a prerequisite, not an optimization. - **6-month retention** must ship _with_ it, not after — bulk warming is precisely a mechanism for maximizing held TMDB data.