mirror of
https://github.com/4gray/iptvnator.git
synced 2026-10-11 19:46:16 -08:00
Year, genre and country chips on movie and series detail pages now open a Discover page inside the portal: popular TMDB titles for that facet, matched against the user's own catalog. Generalizes the existing actor-page pattern (TMDB list -> what's in my library -> else portal search) to metadata facets. - All three TMDB merges emit structured `tmdb_genres`/`tmdb_countries` (+ `tmdb_media_type` on Stalker, whose embedded-VOD series route as movies). Cached details payloads already carry both, so existing rows need no refetch. - Chips are clickable only with TMDB backing, like person chips today; the year chip gates on a merge-written numeric `tmdb_id`, since provider payloads ship junk string ids. - `TmdbDiscoverService` fetches up to 5 `/discover` pages by popularity and caches them in memory only — popularity rankings are volatile and must not reach the persisted `tmdb_metadata` table. - New `discover` route in both portals; containers clone the actor route, staleness-guarded by a facet key because facets change via query params on the same route instance. - The grid, filter chips and badges move out of `ActorViewComponent` into a shared `TitleResultsComponent` used by both pages. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
147 lines
4.4 KiB
TypeScript
147 lines
4.4 KiB
TypeScript
/**
|
|
* Shared contracts for the TMDB metadata enrichment subsystem.
|
|
*
|
|
* The renderer talks to the TMDB API directly (it supports CORS); these
|
|
* types describe the user-facing settings and the cache rows persisted in
|
|
* SQLite (Electron) or kept in memory (PWA).
|
|
*/
|
|
|
|
export type TmdbMediaType = 'movie' | 'tv';
|
|
|
|
/**
|
|
* Row namespace in the shared metadata cache table. Besides movie/tv
|
|
* details it also stores person payloads (`person:<id>` lookup keys).
|
|
*/
|
|
export type TmdbCacheMediaType = TmdbMediaType | 'person';
|
|
|
|
/**
|
|
* Opt-in TMDB enrichment settings. Enrichment sends movie/series titles to
|
|
* TMDB, so it is disabled by default for privacy.
|
|
*/
|
|
export interface TmdbSettings {
|
|
enabled: boolean;
|
|
/** Optional user-provided API key overriding the embedded default */
|
|
apiKey?: string;
|
|
}
|
|
|
|
export const DEFAULT_TMDB_SETTINGS: TmdbSettings = {
|
|
enabled: false,
|
|
apiKey: '',
|
|
};
|
|
|
|
/**
|
|
* Cast member attached to provider detail objects after TMDB enrichment.
|
|
* Rendered as avatar chips in the VOD/series detail views.
|
|
*/
|
|
/**
|
|
* Production status of a series, as a stable token. TMDB returns this
|
|
* field as an ENGLISH string regardless of the request language, so the
|
|
* raw value must never reach the UI — it is normalized here and rendered
|
|
* through translated labels.
|
|
*/
|
|
export type TmdbSeriesStatus =
|
|
| 'returning'
|
|
| 'planned'
|
|
| 'in-production'
|
|
| 'ended'
|
|
| 'canceled'
|
|
| 'pilot';
|
|
|
|
const SERIES_STATUS_TOKENS: Record<string, TmdbSeriesStatus> = {
|
|
'returning series': 'returning',
|
|
planned: 'planned',
|
|
'in production': 'in-production',
|
|
ended: 'ended',
|
|
canceled: 'canceled',
|
|
cancelled: 'canceled',
|
|
pilot: 'pilot',
|
|
};
|
|
|
|
/** `null` for unknown values — an unmapped status is simply not shown */
|
|
export function normalizeSeriesStatus(
|
|
status: string | null | undefined
|
|
): TmdbSeriesStatus | null {
|
|
const key = status?.trim().toLowerCase();
|
|
return (key && SERIES_STATUS_TOKENS[key]) || null;
|
|
}
|
|
|
|
const SERIES_STATUS_LABEL_KEYS: Record<TmdbSeriesStatus, string> = {
|
|
returning: 'XTREAM.SERIES_STATUS_RETURNING',
|
|
planned: 'XTREAM.SERIES_STATUS_PLANNED',
|
|
'in-production': 'XTREAM.SERIES_STATUS_IN_PRODUCTION',
|
|
ended: 'XTREAM.SERIES_STATUS_ENDED',
|
|
canceled: 'XTREAM.SERIES_STATUS_CANCELED',
|
|
pilot: 'XTREAM.SERIES_STATUS_PILOT',
|
|
};
|
|
|
|
export function seriesStatusLabelKey(status: TmdbSeriesStatus): string {
|
|
return SERIES_STATUS_LABEL_KEYS[status];
|
|
}
|
|
|
|
export interface TmdbEnrichedCastMember {
|
|
name: string;
|
|
character?: string;
|
|
/** Full TMDB profile image URL, `null` when the actor has no photo */
|
|
profileUrl: string | null;
|
|
/** TMDB person id — makes the cast chip clickable (actor page) */
|
|
tmdbPersonId?: number;
|
|
}
|
|
|
|
/**
|
|
* A recommended/similar title attached to detail objects after TMDB
|
|
* enrichment. Detail views match these against the provider catalog and
|
|
* render a "Similar" rail from the hits.
|
|
*/
|
|
export interface TmdbRecommendation {
|
|
tmdbId: number;
|
|
title: string;
|
|
year: number | null;
|
|
posterUrl: string | null;
|
|
}
|
|
|
|
/**
|
|
* Genre as stated by TMDB for one title. The id drives the Discover-page
|
|
* filter (`with_genres`), so it must never be mixed across media types —
|
|
* movie and TV genre id spaces differ.
|
|
*/
|
|
export interface TmdbGenreFacet {
|
|
id: number;
|
|
name: string;
|
|
}
|
|
|
|
/**
|
|
* Production country as stated by TMDB. `code` is ISO 3166-1 alpha-2 and
|
|
* drives the Discover-page filter (`with_origin_country`); `name` is the
|
|
* TMDB display name (English regardless of request language).
|
|
*/
|
|
export interface TmdbCountryFacet {
|
|
code: string;
|
|
name: string;
|
|
}
|
|
|
|
/**
|
|
* One cached TMDB lookup. Two kinds of rows share the table, discriminated
|
|
* by the `lookupKey` prefix:
|
|
* - `id:<tmdbId>` — full details payload for a TMDB id
|
|
* - `title:<normalized>|year:<year>` — search resolution result; a `null`
|
|
* `tmdbId` is a cached "no confident match" verdict (negative cache)
|
|
*/
|
|
/** Size of the persisted TMDB cache, shown in the settings panel */
|
|
export interface TmdbCacheStats {
|
|
entries: number;
|
|
/** Total payload size in bytes */
|
|
bytes: number;
|
|
}
|
|
|
|
export interface TmdbCacheEntry {
|
|
mediaType: TmdbCacheMediaType;
|
|
lookupKey: string;
|
|
/** TMDB language code the payload was fetched with, e.g. `en-US` */
|
|
language: string;
|
|
tmdbId: number | null;
|
|
/** Raw JSON of the TMDB response, `null` for negative match rows */
|
|
payload: string | null;
|
|
/** ISO timestamp set by the persistence layer */
|
|
fetchedAt?: string;
|
|
}
|