docs(tmdb): clarify user-provided API key policy (#1581)

This commit is contained in:
4gray authored and GitHub committed 2026-09-13 21:25:42 +02:00
1 parent d438f5c655
commit 40092f6efc
4 files changed
+31 -20

No files matched your search

+1 -1
View File
@@ -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:<id>` 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.
+1 -1
View File
@@ -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**
+28 -17
View File
@@ -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
+1 -1
View File
@@ -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.