feat(tmdb): clickable director/creator chips and directing credits on person pages (#1227)

* feat(tmdb): clickable director/creator chips and directing credits on person pages

Directors were plain merged text — no photos, no navigation — while the
data was already sitting in the cached TMDB payloads (credits.crew and
created_by both carry id + profile_path; they just were not typed or
parsed).

- tmdb-merge: enrichedDirectors (crew, job === 'Director', deduped by
  person id) and enrichedCreators (created_by) produce the same chip
  shape as the cast (TmdbEnrichedCastMember) into a new tmdb_directors
  field on all three merges (Xtream VOD, Xtream series, Stalker); types
  widened (crew id/profile_path, created_by id/profile_path).
- Detail views (shared VodDetailsComponent, Xtream vod/serial routes,
  Stalker series view) render the Director row as clickable avatar chips
  when tmdb_directors is present — same markup and openActor handler as
  the cast strip — falling back to the plain text otherwise. Stalker
  re-normalization allowlist preserves the new field.
- Person pages: mapPersonFilmography now merges combined_credits.crew
  (jobs Director/Creator) into the filmography — acting wins the
  per-title dedup, directing-only titles show the job in the character
  slot. Everything else (library matching, All-portals scope, filters,
  search fallback, back button) works unchanged because the person page
  is role-agnostic. Existing caches work as-is: crew/created_by were
  always part of the stored payloads.

Tests: merge spec (director/creator chips + crew-row dedup ×3 merges),
person spec (crew credits, Producer excluded, acting-wins dedup),
stalker-vod.utils passthrough. Docs updated (CLAUDE.md + architecture).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix(tmdb): address director-pages review — split oversized spec, stable track keys, translated crew roles

- tmdb-merge.spec.ts grew past the 400-line lint ceiling — the Stalker
  merge suite moved to tmdb-merge-stalker.spec.ts (fixes the CI Lint job).
- All cast/director chip loops now track by TMDB person id with an
  index fallback ('p<id>' / 'i<index>') instead of member.name — distinct
  people can share a name and creator payloads carry no dedup (greptile).
- Directing-only filmography credits carry the role in a new crewJob
  field ('Director' | 'Creator') instead of stuffing TMDB's raw English
  job into character; ActorViewComponent renders it through translated
  labels (XTREAM.CREW_JOB_DIRECTOR/CREATOR, added to all 18 locales via
  the i18n patch workflow, matching each locale's existing glossary —
  pt "Diretor", de "Regisseur") (Codex).

Tests: person spec asserts character/crewJob separation; merge suites
green after the split (15 + stalker file).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
4grayandClaude Opus 4.8 authored and GitHub committed 2026-07-24 08:42:41 +02:00
1 parent 59c15493a7
commit 5aa44d19d4
40 files changed
+528 -154

No files matched your search

@@ -39,6 +39,8 @@ export interface StalkerVodInfo {
rating_kinopoisk: string;
/** Populated by TMDB enrichment; absent in raw portal responses */
tmdb_cast?: TmdbEnrichedCastMember[];
/** Directors (movies) / creators (series) as clickable person chips */
tmdb_directors?: TmdbEnrichedCastMember[];
/** TMDB backdrop URL — Stalker portals never provide one themselves */
tmdb_backdrop?: string;
/** YouTube trailer key from TMDB — Stalker portals provide no trailers */
@@ -30,6 +30,7 @@ export function normalizeXtreamVod(item: XtreamVodDetails): NormalizedVodMeta {
ratingKinopoisk: info?.rating_kinopoisk,
youtubeTrailer: info?.youtube_trailer,
tmdbCast: info?.tmdb_cast,
tmdbDirectors: info?.tmdb_directors,
tmdbRecommendations: info?.tmdb_recommendations,
};
}
@@ -59,6 +60,7 @@ export function normalizeStalkerVod(item: StalkerVodDetails): NormalizedVodMeta
// Stalker portals provide no trailers; TMDB enrichment can
youtubeTrailer: info?.tmdb_trailer,
tmdbCast: info?.tmdb_cast,
tmdbDirectors: info?.tmdb_directors,
tmdbRecommendations: info?.tmdb_recommendations,
};
}
@@ -84,6 +84,8 @@ export interface NormalizedVodMeta {
youtubeTrailer?: string;
/** Cast with profile photos, populated by TMDB enrichment */
tmdbCast?: TmdbEnrichedCastMember[];
/** Directors (movies) / creators (series) as clickable person chips */
tmdbDirectors?: TmdbEnrichedCastMember[];
/** TMDB recommendations (drives the cross-portal "Similar" rail) */
tmdbRecommendations?: TmdbRecommendation[];
}
@@ -26,6 +26,8 @@ export interface XtreamSerieInfo {
category_id: string;
/** Populated by TMDB enrichment; absent in raw provider responses */
tmdb_cast?: TmdbEnrichedCastMember[];
/** Directors (movies) / creators (series) as clickable person chips */
tmdb_directors?: TmdbEnrichedCastMember[];
/** Populated by TMDB enrichment; matched against the catalog in views */
tmdb_recommendations?: TmdbRecommendation[];
/** Matched TMDB show id — enables lazy season/episode enrichment */
@@ -36,6 +36,8 @@ export interface XtreamVodInfo {
rating_imdb?: string;
/** Populated by TMDB enrichment; absent in raw provider responses */
tmdb_cast?: TmdbEnrichedCastMember[];
/** Directors (movies) / creators (series) as clickable person chips */
tmdb_directors?: TmdbEnrichedCastMember[];
/** Populated by TMDB enrichment; matched against the catalog in views */
tmdb_recommendations?: TmdbRecommendation[];
}