From 13ab41b99df2c6138fd7c546ab2c6627795e21c2 Mon Sep 17 00:00:00 2001 From: 4gray Date: Fri, 24 Jul 2026 08:42:17 +0200 Subject: [PATCH] =?UTF-8?q?fix(tmdb):=20address=20director-pages=20review?= =?UTF-8?q?=20=E2=80=94=20split=20oversized=20spec,=20stable=20track=20key?= =?UTF-8?q?s,=20translated=20crew=20roles?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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' / 'i') 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 --- apps/web/src/assets/i18n/ar.json | 2 + apps/web/src/assets/i18n/ary.json | 2 + apps/web/src/assets/i18n/by.json | 2 + apps/web/src/assets/i18n/de.json | 2 + apps/web/src/assets/i18n/el.json | 2 + apps/web/src/assets/i18n/en.json | 2 + apps/web/src/assets/i18n/es.json | 2 + apps/web/src/assets/i18n/fr.json | 2 + apps/web/src/assets/i18n/it.json | 2 + apps/web/src/assets/i18n/ja.json | 2 + apps/web/src/assets/i18n/ko.json | 2 + apps/web/src/assets/i18n/nl.json | 2 + apps/web/src/assets/i18n/pl.json | 2 + apps/web/src/assets/i18n/pt.json | 2 + apps/web/src/assets/i18n/ru.json | 2 + apps/web/src/assets/i18n/tr.json | 2 + apps/web/src/assets/i18n/zh.json | 2 + apps/web/src/assets/i18n/zhtw.json | 2 + .../stalker-series-view.component.html | 4 +- .../serial-details.component.html | 4 +- .../vod-details-route.component.html | 4 +- .../src/lib/tmdb/tmdb-merge-stalker.spec.ts | 113 ++++++++++++++++ libs/services/src/lib/tmdb/tmdb-merge.spec.ts | 122 +----------------- .../services/src/lib/tmdb/tmdb-person.spec.ts | 8 +- libs/services/src/lib/tmdb/tmdb-person.ts | 30 +++-- .../vod-details/vod-details.component.html | 4 +- .../lib/actor-view/actor-view.component.html | 2 + .../lib/actor-view/actor-view.component.ts | 7 + 28 files changed, 196 insertions(+), 138 deletions(-) create mode 100644 libs/services/src/lib/tmdb/tmdb-merge-stalker.spec.ts diff --git a/apps/web/src/assets/i18n/ar.json b/apps/web/src/assets/i18n/ar.json index 833bd2075..6010740a3 100644 --- a/apps/web/src/assets/i18n/ar.json +++ b/apps/web/src/assets/i18n/ar.json @@ -783,6 +783,8 @@ "ACTORS": "الممثلون", "DIRECTOR": "المخرج", "SIMILAR": "مشابه", + "CREW_JOB_DIRECTOR": "المخرج", + "CREW_JOB_CREATOR": "المبتكر", "ACTOR_FILMOGRAPHY": "الأعمال الفنية", "ACTOR_FILTER_ALL": "الكل", "ACTOR_SCOPE_THIS_PORTAL": "هذه البوابة", diff --git a/apps/web/src/assets/i18n/ary.json b/apps/web/src/assets/i18n/ary.json index 27188b4e8..ee374f10d 100644 --- a/apps/web/src/assets/i18n/ary.json +++ b/apps/web/src/assets/i18n/ary.json @@ -783,6 +783,8 @@ "ACTORS": "الممثلون", "DIRECTOR": "المخرج", "SIMILAR": "بحال هادو", + "CREW_JOB_DIRECTOR": "المخرج", + "CREW_JOB_CREATOR": "المبتكر", "ACTOR_FILMOGRAPHY": "الأفلام ديالو", "ACTOR_FILTER_ALL": "كلشي", "ACTOR_SCOPE_THIS_PORTAL": "هاد البوابة", diff --git a/apps/web/src/assets/i18n/by.json b/apps/web/src/assets/i18n/by.json index ac6cd8081..38b110ac9 100644 --- a/apps/web/src/assets/i18n/by.json +++ b/apps/web/src/assets/i18n/by.json @@ -783,6 +783,8 @@ "ACTORS": "Акцёры", "DIRECTOR": "Рэжысёр", "SIMILAR": "Падобнае", + "CREW_JOB_DIRECTOR": "Рэжысёр", + "CREW_JOB_CREATOR": "Стваральнік", "ACTOR_FILMOGRAPHY": "Фільмаграфія", "ACTOR_FILTER_ALL": "Усе", "ACTOR_SCOPE_THIS_PORTAL": "Гэты партал", diff --git a/apps/web/src/assets/i18n/de.json b/apps/web/src/assets/i18n/de.json index b9a3c2d9f..d9cfaa05f 100644 --- a/apps/web/src/assets/i18n/de.json +++ b/apps/web/src/assets/i18n/de.json @@ -783,6 +783,8 @@ "ACTORS": "Schauspieler", "DIRECTOR": "Regisseur", "SIMILAR": "Ähnliche Titel", + "CREW_JOB_DIRECTOR": "Regisseur", + "CREW_JOB_CREATOR": "Schöpfer", "ACTOR_FILMOGRAPHY": "Filmografie", "ACTOR_FILTER_ALL": "Alle", "ACTOR_SCOPE_THIS_PORTAL": "Dieses Portal", diff --git a/apps/web/src/assets/i18n/el.json b/apps/web/src/assets/i18n/el.json index 335cc47bb..25d8a343f 100644 --- a/apps/web/src/assets/i18n/el.json +++ b/apps/web/src/assets/i18n/el.json @@ -783,6 +783,8 @@ "ACTORS": "Ηθοποιοί", "DIRECTOR": "Σκηνοθέτης", "SIMILAR": "Παρόμοια", + "CREW_JOB_DIRECTOR": "Σκηνοθέτης", + "CREW_JOB_CREATOR": "Δημιουργός", "ACTOR_FILMOGRAPHY": "Φιλμογραφία", "ACTOR_FILTER_ALL": "Όλα", "ACTOR_SCOPE_THIS_PORTAL": "Αυτή η πύλη", diff --git a/apps/web/src/assets/i18n/en.json b/apps/web/src/assets/i18n/en.json index 92d396baa..0912214c2 100644 --- a/apps/web/src/assets/i18n/en.json +++ b/apps/web/src/assets/i18n/en.json @@ -783,6 +783,8 @@ "ACTORS": "Actors", "DIRECTOR": "Director", "SIMILAR": "Similar", + "CREW_JOB_DIRECTOR": "Director", + "CREW_JOB_CREATOR": "Creator", "ACTOR_FILMOGRAPHY": "Filmography", "ACTOR_FILTER_ALL": "All", "ACTOR_SCOPE_THIS_PORTAL": "This portal", diff --git a/apps/web/src/assets/i18n/es.json b/apps/web/src/assets/i18n/es.json index c7cef836c..f8bd90d41 100644 --- a/apps/web/src/assets/i18n/es.json +++ b/apps/web/src/assets/i18n/es.json @@ -783,6 +783,8 @@ "ACTORS": "Actores", "DIRECTOR": "Director", "SIMILAR": "Similares", + "CREW_JOB_DIRECTOR": "Director", + "CREW_JOB_CREATOR": "Creador", "ACTOR_FILMOGRAPHY": "Filmografía", "ACTOR_FILTER_ALL": "Todas", "ACTOR_SCOPE_THIS_PORTAL": "Este portal", diff --git a/apps/web/src/assets/i18n/fr.json b/apps/web/src/assets/i18n/fr.json index 79e523fff..ba869a727 100644 --- a/apps/web/src/assets/i18n/fr.json +++ b/apps/web/src/assets/i18n/fr.json @@ -783,6 +783,8 @@ "ACTORS": "Acteurs", "DIRECTOR": "Réalisateur", "SIMILAR": "Similaires", + "CREW_JOB_DIRECTOR": "Réalisateur", + "CREW_JOB_CREATOR": "Créateur", "ACTOR_FILMOGRAPHY": "Filmographie", "ACTOR_FILTER_ALL": "Tout", "ACTOR_SCOPE_THIS_PORTAL": "Ce portail", diff --git a/apps/web/src/assets/i18n/it.json b/apps/web/src/assets/i18n/it.json index 0421f61d8..bd9aefbf1 100644 --- a/apps/web/src/assets/i18n/it.json +++ b/apps/web/src/assets/i18n/it.json @@ -783,6 +783,8 @@ "ACTORS": "Attori", "DIRECTOR": "Regista", "SIMILAR": "Simili", + "CREW_JOB_DIRECTOR": "Regista", + "CREW_JOB_CREATOR": "Ideatore", "ACTOR_FILMOGRAPHY": "Filmografia", "ACTOR_FILTER_ALL": "Tutti", "ACTOR_SCOPE_THIS_PORTAL": "Questo portale", diff --git a/apps/web/src/assets/i18n/ja.json b/apps/web/src/assets/i18n/ja.json index 73ea0138e..7361326f3 100644 --- a/apps/web/src/assets/i18n/ja.json +++ b/apps/web/src/assets/i18n/ja.json @@ -783,6 +783,8 @@ "ACTORS": "出演者", "DIRECTOR": "監督", "SIMILAR": "類似作品", + "CREW_JOB_DIRECTOR": "監督", + "CREW_JOB_CREATOR": "クリエイター", "ACTOR_FILMOGRAPHY": "出演作品", "ACTOR_FILTER_ALL": "すべて", "ACTOR_SCOPE_THIS_PORTAL": "このポータル", diff --git a/apps/web/src/assets/i18n/ko.json b/apps/web/src/assets/i18n/ko.json index e43ddc6b3..d77cca393 100644 --- a/apps/web/src/assets/i18n/ko.json +++ b/apps/web/src/assets/i18n/ko.json @@ -783,6 +783,8 @@ "ACTORS": "배우", "DIRECTOR": "감독", "SIMILAR": "비슷한 콘텐츠", + "CREW_JOB_DIRECTOR": "감독", + "CREW_JOB_CREATOR": "크리에이터", "ACTOR_FILMOGRAPHY": "출연 작품", "ACTOR_FILTER_ALL": "전체", "ACTOR_SCOPE_THIS_PORTAL": "현재 포털", diff --git a/apps/web/src/assets/i18n/nl.json b/apps/web/src/assets/i18n/nl.json index 6d05aab79..90db26c23 100644 --- a/apps/web/src/assets/i18n/nl.json +++ b/apps/web/src/assets/i18n/nl.json @@ -783,6 +783,8 @@ "ACTORS": "Acteurs", "DIRECTOR": "Regisseur", "SIMILAR": "Vergelijkbaar", + "CREW_JOB_DIRECTOR": "Regisseur", + "CREW_JOB_CREATOR": "Bedenker", "ACTOR_FILMOGRAPHY": "Filmografie", "ACTOR_FILTER_ALL": "Alles", "ACTOR_SCOPE_THIS_PORTAL": "Dit portaal", diff --git a/apps/web/src/assets/i18n/pl.json b/apps/web/src/assets/i18n/pl.json index 14758b632..14851a2cb 100644 --- a/apps/web/src/assets/i18n/pl.json +++ b/apps/web/src/assets/i18n/pl.json @@ -783,6 +783,8 @@ "ACTORS": "Aktorzy", "DIRECTOR": "Reżyser", "SIMILAR": "Podobne", + "CREW_JOB_DIRECTOR": "Reżyser", + "CREW_JOB_CREATOR": "Twórca", "ACTOR_FILMOGRAPHY": "Filmografia", "ACTOR_FILTER_ALL": "Wszystkie", "ACTOR_SCOPE_THIS_PORTAL": "Ten portal", diff --git a/apps/web/src/assets/i18n/pt.json b/apps/web/src/assets/i18n/pt.json index 89f58e2ec..541edfdb4 100644 --- a/apps/web/src/assets/i18n/pt.json +++ b/apps/web/src/assets/i18n/pt.json @@ -783,6 +783,8 @@ "ACTORS": "Atores", "DIRECTOR": "Diretor", "SIMILAR": "Semelhantes", + "CREW_JOB_DIRECTOR": "Diretor", + "CREW_JOB_CREATOR": "Criador", "ACTOR_FILMOGRAPHY": "Filmografia", "ACTOR_FILTER_ALL": "Todos", "ACTOR_SCOPE_THIS_PORTAL": "Este portal", diff --git a/apps/web/src/assets/i18n/ru.json b/apps/web/src/assets/i18n/ru.json index f0c007b9d..1cdc23312 100644 --- a/apps/web/src/assets/i18n/ru.json +++ b/apps/web/src/assets/i18n/ru.json @@ -783,6 +783,8 @@ "ACTORS": "Актеры", "DIRECTOR": "Режиссёр", "SIMILAR": "Похожее", + "CREW_JOB_DIRECTOR": "Режиссёр", + "CREW_JOB_CREATOR": "Создатель", "ACTOR_FILMOGRAPHY": "Фильмография", "ACTOR_FILTER_ALL": "Все", "ACTOR_SCOPE_THIS_PORTAL": "Этот портал", diff --git a/apps/web/src/assets/i18n/tr.json b/apps/web/src/assets/i18n/tr.json index d89dbad45..8f2f81762 100644 --- a/apps/web/src/assets/i18n/tr.json +++ b/apps/web/src/assets/i18n/tr.json @@ -783,6 +783,8 @@ "ACTORS": "Oyuncular", "DIRECTOR": "Yönetmen", "SIMILAR": "Benzerler", + "CREW_JOB_DIRECTOR": "Yönetmen", + "CREW_JOB_CREATOR": "Yaratıcı", "ACTOR_FILMOGRAPHY": "Filmografi", "ACTOR_FILTER_ALL": "Tümü", "ACTOR_SCOPE_THIS_PORTAL": "Bu portal", diff --git a/apps/web/src/assets/i18n/zh.json b/apps/web/src/assets/i18n/zh.json index 17ccb0b36..26afe5757 100644 --- a/apps/web/src/assets/i18n/zh.json +++ b/apps/web/src/assets/i18n/zh.json @@ -783,6 +783,8 @@ "ACTORS": "演员", "DIRECTOR": "导演", "SIMILAR": "类似影片", + "CREW_JOB_DIRECTOR": "导演", + "CREW_JOB_CREATOR": "主创", "ACTOR_FILMOGRAPHY": "参演作品", "ACTOR_FILTER_ALL": "全部", "ACTOR_SCOPE_THIS_PORTAL": "当前门户", diff --git a/apps/web/src/assets/i18n/zhtw.json b/apps/web/src/assets/i18n/zhtw.json index 87d93aa43..4490ed506 100644 --- a/apps/web/src/assets/i18n/zhtw.json +++ b/apps/web/src/assets/i18n/zhtw.json @@ -783,6 +783,8 @@ "ACTORS": "演員", "DIRECTOR": "導演", "SIMILAR": "類似作品", + "CREW_JOB_DIRECTOR": "導演", + "CREW_JOB_CREATOR": "主創", "ACTOR_FILMOGRAPHY": "參演作品", "ACTOR_FILTER_ALL": "全部", "ACTOR_SCOPE_THIS_PORTAL": "目前入口", diff --git a/libs/portal/stalker/feature/src/lib/stalker-series-view/stalker-series-view.component.html b/libs/portal/stalker/feature/src/lib/stalker-series-view/stalker-series-view.component.html index 95ff1df99..af79c3b2f 100644 --- a/libs/portal/stalker/feature/src/lib/stalker-series-view/stalker-series-view.component.html +++ b/libs/portal/stalker/feature/src/lib/stalker-series-view/stalker-series-view.component.html @@ -37,7 +37,7 @@
@for ( member of serial.info.tmdb_cast; - track member.name + track member.tmdbPersonId ? 'p' + member.tmdbPersonId : 'i' + $index ) { {{ 'XTREAM.DIRECTOR' | translate }} @if (serial.info.tmdb_directors?.length) {
- @for (member of serial.info.tmdb_directors; track member.name) { + @for (member of serial.info.tmdb_directors; track member.tmdbPersonId ? 'p' + member.tmdbPersonId : 'i' + $index) { {{ 'XTREAM.CAST' | translate }} @if (info.tmdb_cast?.length) {
- @for (member of info.tmdb_cast; track member.name) { + @for (member of info.tmdb_cast; track member.tmdbPersonId ? 'p' + member.tmdbPersonId : 'i' + $index) { {{ 'XTREAM.DIRECTOR' | translate }} @if (info.tmdb_directors?.length) {
- @for (member of info.tmdb_directors; track member.name) { + @for (member of info.tmdb_directors; track member.tmdbPersonId ? 'p' + member.tmdbPersonId : 'i' + $index) { {{ 'XTREAM.ACTORS' | translate }} @if (info.tmdb_cast?.length) {
- @for (member of info.tmdb_cast; track member.name) { + @for (member of info.tmdb_cast; track member.tmdbPersonId ? 'p' + member.tmdbPersonId : 'i' + $index) { {{ 'XTREAM.DIRECTOR' | translate }} @if (info.tmdb_directors?.length) {
- @for (member of info.tmdb_directors; track member.name) { + @for (member of info.tmdb_directors; track member.tmdbPersonId ? 'p' + member.tmdbPersonId : 'i' + $index) { ([ + 'Director', + 'Creator', +] satisfies ActorCrewJob[]); export function mapPersonProfile(person: TmdbPersonDetails): ActorProfile { return { @@ -48,7 +59,7 @@ export function mapPersonProfile(person: TmdbPersonDetails): ActorProfile { * first (undated entries last), capped at {@link MAX_FILMOGRAPHY_CREDITS}. * Acting credits win the dedup so "Actor — Character" survives when a * person both starred in and directed the same title; directing-only - * titles show the job ("Director") in the character slot. + * titles carry the crew role in `crewJob` for translated rendering. */ export function mapPersonFilmography( person: TmdbPersonDetails @@ -56,12 +67,14 @@ export function mapPersonFilmography( const seen = new Set(); const credits: ActorFilmographyCredit[] = []; - const castCredits = person.combined_credits?.cast ?? []; - const crewCredits = (person.combined_credits?.crew ?? []).filter( - (credit) => credit.job && FILMOGRAPHY_CREW_JOBS.has(credit.job) + const castCredits = (person.combined_credits?.cast ?? []).map( + (credit) => ({ credit, crewJob: null as ActorCrewJob | null }) ); + const crewCredits = (person.combined_credits?.crew ?? []) + .filter((credit) => credit.job && FILMOGRAPHY_CREW_JOBS.has(credit.job)) + .map((credit) => ({ credit, crewJob: credit.job as ActorCrewJob })); - for (const credit of [...castCredits, ...crewCredits]) { + for (const { credit, crewJob } of [...castCredits, ...crewCredits]) { const mediaType = credit.media_type === 'movie' || credit.media_type === 'tv' ? credit.media_type @@ -79,7 +92,8 @@ export function mapPersonFilmography( title, year: extractYear(credit.release_date ?? credit.first_air_date), posterUrl: tmdbPosterUrl(credit.poster_path), - character: credit.character?.trim() || credit.job?.trim() || null, + character: credit.character?.trim() || null, + crewJob, }); } diff --git a/libs/ui/playback/src/lib/vod-details/vod-details.component.html b/libs/ui/playback/src/lib/vod-details/vod-details.component.html index 588f5a350..7e578926c 100644 --- a/libs/ui/playback/src/lib/vod-details/vod-details.component.html +++ b/libs/ui/playback/src/lib/vod-details/vod-details.component.html @@ -37,7 +37,7 @@ {{ 'XTREAM.ACTORS' | translate }} @if (meta.tmdbCast?.length) {
- @for (member of meta.tmdbCast; track member.name) { + @for (member of meta.tmdbCast; track member.tmdbPersonId ? 'p' + member.tmdbPersonId : 'i' + $index) { {{ 'XTREAM.DIRECTOR' | translate }} @if (meta.tmdbDirectors?.length) {
- @for (member of meta.tmdbDirectors; track member.name) { + @for (member of meta.tmdbDirectors; track member.tmdbPersonId ? 'p' + member.tmdbPersonId : 'i' + $index) { this.showAvailabilityFilter() && this.filterMode() === 'available' ? this.items().filter((item) => item.available)