mirror of
https://github.com/4gray/iptvnator.git
synced 2026-10-09 17:36:15 -08:00
docs(portals): describe the Discover gates the code actually implements
Three rounds of review fixes moved the contracts out from under the prose. The year chip no longer gates on a merge-written numeric tmdb_id (that gate was wrong: the field is number | string, so a provider-sent number passed it with enrichment never having run) but on the navigation target, which requires a playlist and enabled enrichment. Discover loads are guarded by recency, not by facet key, because A→B→A leaves two in-flight requests sharing one key. Availability additionally waits for catalog readiness. Both canonical entries say so now. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
7dcc93b880
commit
9b8174f761
2 files changed
+35
-14
No files matched your search
@@ -1366,7 +1366,7 @@ stream_id`); it drops `series_id`/`movie_id`, so the builder pins the
|
||||
- Dashboard: opt-in "Trending this week" rail (weekly TMDB trending matched against imported Xtream playlists via one batched `DB_MATCH_TITLES` request; Electron-only, `dashboardRails.tmdbTrending` toggle), a "Because you watched" recommendations rail (`dashboardRails.tmdbRecommendations` toggle; TMDB has no account-free "for you" endpoint, so `DashboardRecommendationsService` seeds per-title `recommendations` — already riding in every cached details payload — from up to 3 recently watched movies/series via the shared `dashboard-tmdb-lookup.util.ts` attempt builder, interleaves them, dedupes by TMDB id (title collisions are resolved after matching, by the catalog row, so same-titled remakes both reach the matcher), drops watched/favorited titles through a year-gated exclusion index built by the same lookup-attempt builder (so a Stalker embedded-VOD series indexes under `series:` despite routing as `movie`, and its stored `o_name` alias counts too; only the PRIMARY attempt is indexed, or a watched film would swallow the same-named show) on two title tiers (exact normalized title plus a year-gated base tier so a stored "Inception 2010" excludes TMDB's "Inception" while "Blade Runner 2049" does not swallow the 1982 film), keeps only year-compatible `DB_MATCH_TITLES` matches — matching/exclusion run through both the localized title and the TMDB original-title alias, and a year-incompatible first alias falls through to the other — and hides the rail below 5 cards while resetting the latch; successful loads are keyed by TMDB language + seed set + watched/favorited exclusion set + imported-playlist ids, an emptied history clears the rail, a mid-flight load request is queued, and a no-seed-resolved load retries instead of latching) and hero TMDB extras (backdrop fallback, rating + genre badges, memoized per lookup identity; series heroes show the tracked S/E badge from playback positions) — `DashboardTrendingService` in `libs/workspace/dashboard/data-access`, `DashboardHeroTmdbService` in `libs/workspace/dashboard/feature`; both load async after first paint. The hero lookup must carry the same identity the detail view used, not just the display title — `extractStalkerItemTmdbHints` (`libs/shared/interfaces`) reads title/original title/year/tmdb id off a stored Stalker entry; an unconfirmed Stalker `movie` verdict retries as `tv` without the id (the default answer earns a retry, and an id is valid only for its own media type), while a `tv` verdict — reached only on positive series evidence — gets no retry back to `movie`; a confirmed `movie` gets none either, and is confirmed by an Xtream `source` (that catalog files movies and series apart) or by a stored Stalker `info.tmdb_id` (never a provider claim, only a match this app already gated). The lookup key is the WHOLE attempt sequence, since two rows can share title/year/id yet differ in whether a `tv` fallback follows, and callers memoize by it. Stalker items never reach the `content` table, so their backdrop rides in the stored entry (`info.tmdb_backdrop`) rather than `content.backdrop_url`, and the activity mappers surface it as `backdrop_url`. Xtream rows carry the same identity on the `content` row: the detail views back-fill `tmdb_id`/`release_year`/`original_title` next to `backdrop_url` (`xtreamDetailContentMetadata` → `XtreamStore.backfillContentMetadata` → `DB_SET_CONTENT_METADATA_IF_MISSING` → `persistContentMetadataIfMissing`), the activity SELECTs project them onto `PortalActivityItem`, and `buildDashboardTmdbAttempts` reads them back. Writes are per-column and never overwrite (enrichment supplies the pieces at different times, so a row-level guard would let the first arrival block every later one); `release_year` is the year the PROVIDER stated, never one read out of the title (readers still apply that fallback themselves, so an absent column means "no provider date" — and "2001: A Space Odyssey" can never be frozen in as a 2001 film), which holds only because the TMDB merge marks the dates it substitutes itself with `tmdb_supplied_release_date` and the extractor skips those — the merge's other `tmdb_*` fields are conditional on having content, so they cannot serve as an "enrichment ran" signal; the id is stored unvetted because every consumer re-gates it through `assessProviderId`; and there is no media-type column, since for Xtream `content.type` already is the media type. Both sides validate through `normalizeContentMetadataPatch` (`libs/shared/interfaces`), so legacy rows, never-opened rows and provider junk all collapse to the title-only fallback — as does the PWA, whose catalog cache is rebuilt from the API on every load
|
||||
- Series detail views show a TMDB production-status chip (`tmdb_status`, e.g. Ended / Returning) — TMDB sends `status` in English regardless of request language, so it is normalized to a token by `normalizeSeriesStatus` and rendered via `seriesStatusLabelKey` translations; person pages show `deathday` alongside `birthday`
|
||||
- Actor pages: cast avatar chips are clickable (TMDB person id) and open `actor/:personId` inside the current portal — TMDB person bio + full filmography (acting + directing credits merged; acting wins the per-title dedup); director/creator chips (`tmdb_directors` via `enrichedDirectors`/`enrichedCreators` in `tmdb-credits.ts`) are clickable the same way and open the same person page; Xtream matches titles against the loaded catalog (direct navigation), unmatched titles and all Stalker titles open the portal search prefilled (`?q=`); the in-portal search page shows a Back button (`SearchLayoutComponent.showBackButton` → `Location.back()`) so users can return to the actor page; shared UI in `libs/ui/shared-portals` (`ActorViewComponent`, whose grid is the extracted `TitleResultsComponent` shared with the Discover pages)
|
||||
- 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 gates on a merge-written numeric `tmdb_id`, 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`; facet changes on the same route instance are staleness-guarded by `discoverFacetKey()`. See "Discover Pages" in `docs/architecture/tmdb-metadata-enrichment.md`
|
||||
- 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`)
|
||||
|
||||
@@ -367,12 +367,21 @@ payloads already carry `genres`/`production_countries`
|
||||
rows produce facets without a refetch. Like person chips, facet chips are
|
||||
clickable ONLY with TMDB backing: genre/country chips render per-entry
|
||||
from the structured arrays (falling back to today's static joined-string
|
||||
chip without them), and the year chip is clickable only when
|
||||
`tmdb_id` is a merge-written number — provider payloads ship `tmdb_id`
|
||||
as untrusted strings, so `typeof === 'number'` is the gate.
|
||||
chip without them), while the year chip renders from provider data and
|
||||
so needs a gate of its own. That gate is the navigation TARGET, not the
|
||||
item's identity: `createDiscoverFacetNavigation()` offers a facet only
|
||||
when its click can land somewhere, and the four hosts return `null` from
|
||||
their target unless a playlist resolves AND
|
||||
`TmdbEnrichmentService.isEnabled()` — Discover reads its results from
|
||||
TMDB, so a chip must never promise a page enrichment cannot fill. An
|
||||
earlier version gated on `typeof tmdb_id === 'number'` instead; that is
|
||||
wrong, because `XtreamVodInfo.tmdb_id` is `number | string` and a
|
||||
provider sending a JSON number satisfied it with enrichment never having
|
||||
run. Discover-by-year does not use the item's TMDB id at all.
|
||||
|
||||
**Navigation.** Chip clicks navigate (via each render site's own
|
||||
`openDiscover*` methods, mirroring `openActor`) to the portal-scoped
|
||||
**Navigation.** Chip clicks navigate (via
|
||||
`createDiscoverFacetNavigation()`, shared by all four render sites) to
|
||||
the portal-scoped
|
||||
`discover` route: `/workspace/{xtreams|stalker}/:id/discover?type=
|
||||
movie|tv&year=&genre=<id>&genreLabel=&country=<iso>&countryLabel=`. The
|
||||
query-param assembly is centralized in `discoverLink()`
|
||||
@@ -397,14 +406,26 @@ with enrichment disabled.
|
||||
`StalkerDiscoverRouteComponent` clone the actor route containers: same
|
||||
scope toggle, same portal/global matching (Xtream in-memory index by the
|
||||
facet's media type; Stalker search-prefill only, global scope matching
|
||||
Xtream playlists), same navigation on click. Because facets change via
|
||||
query params on the SAME route instance, async results are
|
||||
staleness-guarded by `discoverFacetKey()` rather than by instance —
|
||||
but the in-flight INDICATOR is owned by `createLatestRequestGuard()`
|
||||
(`libs/portal/shared/util`), because a request whose subject changed
|
||||
must not clear a spinner a replacement request now owns, and a request
|
||||
with no replacement (the user left the scope) must still clear it. The
|
||||
actor pages use the same guard for the same reason. Both
|
||||
Xtream playlists), same navigation on click.
|
||||
|
||||
Facets change via query params on the SAME route instance, so staleness
|
||||
cannot be decided by instance — and, for the discover load, not by facet
|
||||
either: A→B→A leaves two in-flight requests whose `discoverFacetKey()`
|
||||
is identical, so an older one failing after the newer succeeded would
|
||||
replace valid results with an empty page. Recency decides instead, via
|
||||
`createLatestRequestGuard()` (`libs/portal/shared/util`). The catalog
|
||||
match keeps both: the guard owns the in-flight INDICATOR (a request whose
|
||||
subject changed must not clear a spinner its replacement now owns, and a
|
||||
request with no replacement must still clear it) while the facet key
|
||||
decides whether the RESULT is still wanted. The actor pages use the same
|
||||
guard for the same reason.
|
||||
|
||||
Availability also waits for the catalog, not just for TMDB: the content
|
||||
gate renders the route while a cold import runs, and TMDB usually answers
|
||||
first, so publishing then would state that owned titles are missing.
|
||||
Readiness is keyed on the in-flight flags rather than
|
||||
`isContentInitialized`, so a failed import settles the page instead of
|
||||
spinning forever. Both
|
||||
render the shared `DiscoverViewComponent`, whose grid is the
|
||||
`TitleResultsComponent` extracted from `ActorViewComponent`
|
||||
(`libs/ui/shared-portals/src/lib/title-results/`) — one grid, filter
|
||||
|
||||
Reference in new issue
Block a user