E5 S5: enriched Plex detail page (ratings + facts + keywords + crew)
Consumes the TMDB block the S2 engine already persists + serves (movie/show detail's `tmdb`), turning the movie info page into a Seerr-style enriched detail (page variant only — the player overlay stays lean): - Multi-source ratings row: IMDb (links out) · TMDb (with vote count) · Plex audience — whichever are present. Only the primary score (IMDb, else audience) carries the ratingMin filter, mirroring the backend rating column. - Facts panel: release · runtime · cert · budget · revenue · studios · countries. - Keyword chips → tap to filter the library. Backend: a new `keywords` filter on LibraryFilters + meta_filter_conds (`keywords @> [kw]`, both models), wired through PlexFilters (appendPlexFilters / plexFilterCount / active-chip / clear). - Full crew (TMDB key-crew) → tap a name to open their person page. Tests: test_plex_keyword_filter.py (3) + PlexInfo.dom.test.tsx (5, incl. the overlay-stays-lean guard). New i18n plex.info.facts.* / keywords / crew / audience (en+hu). Gate: vitest 337, backend lane 396.
This commit is contained in:
@@ -54,6 +54,28 @@ export interface PlexCard {
|
||||
show_id?: string | null; // episode: the show's rating_key (for grouping a playlist by show/season)
|
||||
summary?: string | null; // movie/show hover-overview synopsis (trimmed) + episode summary
|
||||
}
|
||||
// TMDB enrichment block (E5 S2), served from OUR DB (never a live TMDB call) on a movie/show detail.
|
||||
// null until the row has been enriched. Powers the S5 enriched detail page: multi-source ratings row,
|
||||
// facts sidebar, keyword chips (→ filter), and the full crew.
|
||||
export interface PlexTmdbCrew {
|
||||
name: string;
|
||||
job: string;
|
||||
tmdb_id?: number | null;
|
||||
profile_path?: string | null; // raw TMDB path — we don't proxy crew headshots, so name+job only
|
||||
}
|
||||
export interface PlexTmdb {
|
||||
tmdb_id?: number | null;
|
||||
rating?: number | null; // TMDb user score (0–10)
|
||||
vote_count?: number | null;
|
||||
budget?: number | null; // USD, movies only
|
||||
revenue?: number | null; // USD, movies only
|
||||
keywords: string[];
|
||||
crew: PlexTmdbCrew[];
|
||||
studios: string[];
|
||||
production_countries: string[];
|
||||
recommendations: { tmdb_id?: number | null; title?: string | null; in_library: boolean }[];
|
||||
backdrops: string[];
|
||||
}
|
||||
// Unified cross-library browse (movies + shows mixed). `episodes` is populated only on a search that
|
||||
// also matches episodes (the grouped "Episodes" section).
|
||||
export interface PlexUnifiedResult {
|
||||
@@ -98,6 +120,7 @@ export interface PlexShowDetail {
|
||||
first?: PlexCard | null;
|
||||
episode_count: number;
|
||||
collection_keys: string[];
|
||||
tmdb?: PlexTmdb | null;
|
||||
};
|
||||
seasons: PlexSeasonDetail[];
|
||||
related: PlexCard[];
|
||||
@@ -149,6 +172,7 @@ export interface PlexItemDetail {
|
||||
// `source` buckets the strip so each type can be shown/hidden independently on the info page:
|
||||
// "collection" (genuine franchise), "imdb"/"tmdb"/"tvdb"/"trakt" (external auto-lists), "smart".
|
||||
collections?: { id: string; title: string; source: string; items: PlexCard[] }[];
|
||||
tmdb?: PlexTmdb | null; // movies only (episodes aren't enriched)
|
||||
}
|
||||
export interface PlexCollection {
|
||||
id: string;
|
||||
@@ -205,6 +229,7 @@ export interface PlexFilters {
|
||||
actors: string[]; // OR (any) by default, or AND (all) via actorMode
|
||||
actorMode?: "any" | "all"; // how multiple actors combine (default any = OR)
|
||||
studios: string[]; // OR — from any selected studio
|
||||
keywords: string[]; // AND — TMDB keyword chips from the detail page (E5 S5)
|
||||
collection?: string | null; // a single Plex collection (rating_key)
|
||||
collectionTitle?: string | null; // its title, for the active-filter chip
|
||||
}
|
||||
@@ -214,6 +239,7 @@ export const EMPTY_PLEX_FILTERS: PlexFilters = {
|
||||
directors: [],
|
||||
actors: [],
|
||||
studios: [],
|
||||
keywords: [],
|
||||
};
|
||||
export function plexFilterCount(f: PlexFilters): number {
|
||||
return (
|
||||
@@ -226,6 +252,7 @@ export function plexFilterCount(f: PlexFilters): number {
|
||||
f.directors.length +
|
||||
f.actors.length +
|
||||
f.studios.length +
|
||||
f.keywords.length +
|
||||
(f.collection ? 1 : 0)
|
||||
);
|
||||
}
|
||||
@@ -246,6 +273,7 @@ function appendPlexFilters(u: URLSearchParams, f: PlexFilters): void {
|
||||
if (f.actors?.length) u.set("actors", f.actors.join(","));
|
||||
if (f.actorMode === "all") u.set("actor_mode", "all");
|
||||
if (f.studios?.length) u.set("studios", f.studios.join(","));
|
||||
if (f.keywords?.length) u.set("keywords", f.keywords.join(","));
|
||||
if (f.collection) u.set("collection", f.collection);
|
||||
}
|
||||
export interface PlexFacets {
|
||||
|
||||
Reference in New Issue
Block a user