diff --git a/CLAUDE.md b/CLAUDE.md index 580c7e82e..a4b2938d8 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -820,6 +820,7 @@ engine` (restart required) or - "Similar" rail in ALL detail views: TMDB recommendations matched against the provider catalog by normalized title, two-tier — exact form first, year-stripped fallback gated on year compatibility (`libs/portal/xtream/feature/src/lib/tmdb-similar.util.ts`, `normalizeTitleKeys`); cross-portal matches from other imported Xtream playlists supplement the Xtream rail and fully power the Stalker rail (`CrossPortalSimilarService` in `libs/services`, batched `DB_MATCH_TITLES`, Electron only); detail components re-initialize on route param changes since the router reuses them for detail→detail navigation - Season/episode enrichment: opening a season lazily fetches `/tv/{id}/season/{n}` and overlays real episode names, overviews and stills via `mergeEpisodesWithTmdb` (Xtream: `XtreamStore.enrichSelectedSerialSeason`; Stalker: overlay in the series view's `mappedSeasons`); for single-season provider slices whose title carries an explicit season marker ("The Mandalorian (2 season)", "s02", "2 сезон"), the marker overrides the provider's renumbered season (`resolveEnrichmentSeasonNumber` in `libs/shared/interfaces/src/lib/season-marker.util.ts`) - 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) and hero TMDB extras (backdrop fallback, rating + genre badges, memoized per session; 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 +- 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-merge.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`) - 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` - Opt-in via `Settings > Metadata (TMDB)` (sends titles to TMDB); 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`) diff --git a/apps/web/src/assets/i18n/ar.json b/apps/web/src/assets/i18n/ar.json index 7608a7e62..b7681d351 100644 --- a/apps/web/src/assets/i18n/ar.json +++ b/apps/web/src/assets/i18n/ar.json @@ -802,6 +802,13 @@ "ACTOR_IN_LIBRARY": "متوفر في مكتبتك", "ACTOR_SEARCH_IN_PORTAL": "البحث في البوابة", "ACTOR_BORN": "تاريخ الميلاد:", + "ACTOR_DIED": "توفي", + "SERIES_STATUS_RETURNING": "مستمر", + "SERIES_STATUS_PLANNED": "مخطط", + "SERIES_STATUS_IN_PRODUCTION": "قيد الإنتاج", + "SERIES_STATUS_ENDED": "منتهي", + "SERIES_STATUS_CANCELED": "ملغى", + "SERIES_STATUS_PILOT": "حلقة تجريبية", "DURATION": "المدة", "CAST": "طاقم العمل", "EPISODE_RUN_TIME": "مدة الحلقة", diff --git a/apps/web/src/assets/i18n/ary.json b/apps/web/src/assets/i18n/ary.json index 60c4714c5..1034eafee 100644 --- a/apps/web/src/assets/i18n/ary.json +++ b/apps/web/src/assets/i18n/ary.json @@ -802,6 +802,13 @@ "ACTOR_IN_LIBRARY": "كاين فالمكتبة ديالك", "ACTOR_SEARCH_IN_PORTAL": "قلب فالبوابة", "ACTOR_BORN": "مزداد:", + "ACTOR_DIED": "توفا", + "SERIES_STATUS_RETURNING": "مستمر", + "SERIES_STATUS_PLANNED": "مخطط", + "SERIES_STATUS_IN_PRODUCTION": "كيتصور", + "SERIES_STATUS_ENDED": "سالا", + "SERIES_STATUS_CANCELED": "تلغا", + "SERIES_STATUS_PILOT": "حلقة تجريبية", "DURATION": "المدة", "CAST": "طاقم التمثيل", "EPISODE_RUN_TIME": "مدة تشغيل الحلقة", diff --git a/apps/web/src/assets/i18n/by.json b/apps/web/src/assets/i18n/by.json index 601d3cb44..5485a3871 100644 --- a/apps/web/src/assets/i18n/by.json +++ b/apps/web/src/assets/i18n/by.json @@ -802,6 +802,13 @@ "ACTOR_IN_LIBRARY": "Ёсць у вашай бібліятэцы", "ACTOR_SEARCH_IN_PORTAL": "Пошук на партале", "ACTOR_BORN": "Нарадзіўся:", + "ACTOR_DIED": "Памёр", + "SERIES_STATUS_RETURNING": "Працягваецца", + "SERIES_STATUS_PLANNED": "Запланаваны", + "SERIES_STATUS_IN_PRODUCTION": "У вытворчасці", + "SERIES_STATUS_ENDED": "Завершаны", + "SERIES_STATUS_CANCELED": "Скасаваны", + "SERIES_STATUS_PILOT": "Пілот", "DURATION": "Працягласць", "CAST": "Акцёрскі склад", "EPISODE_RUN_TIME": "Працягласць эпізоду", diff --git a/apps/web/src/assets/i18n/de.json b/apps/web/src/assets/i18n/de.json index 0a2af0752..6f55673e3 100644 --- a/apps/web/src/assets/i18n/de.json +++ b/apps/web/src/assets/i18n/de.json @@ -802,6 +802,13 @@ "ACTOR_IN_LIBRARY": "In Ihrer Bibliothek", "ACTOR_SEARCH_IN_PORTAL": "Im Portal suchen", "ACTOR_BORN": "Geboren:", + "ACTOR_DIED": "Gestorben", + "SERIES_STATUS_RETURNING": "Laufend", + "SERIES_STATUS_PLANNED": "Geplant", + "SERIES_STATUS_IN_PRODUCTION": "In Produktion", + "SERIES_STATUS_ENDED": "Abgeschlossen", + "SERIES_STATUS_CANCELED": "Abgesetzt", + "SERIES_STATUS_PILOT": "Pilot", "DURATION": "Dauer", "CAST": "Gießen", "EPISODE_RUN_TIME": "Laufzeit der Episode", diff --git a/apps/web/src/assets/i18n/el.json b/apps/web/src/assets/i18n/el.json index eafaa6404..f0ef6ff16 100644 --- a/apps/web/src/assets/i18n/el.json +++ b/apps/web/src/assets/i18n/el.json @@ -802,6 +802,13 @@ "ACTOR_IN_LIBRARY": "Στη βιβλιοθήκη σας", "ACTOR_SEARCH_IN_PORTAL": "Αναζήτηση στην πύλη", "ACTOR_BORN": "Γεννήθηκε:", + "ACTOR_DIED": "Απεβίωσε", + "SERIES_STATUS_RETURNING": "Σε εξέλιξη", + "SERIES_STATUS_PLANNED": "Προγραμματισμένο", + "SERIES_STATUS_IN_PRODUCTION": "Σε παραγωγή", + "SERIES_STATUS_ENDED": "Ολοκληρώθηκε", + "SERIES_STATUS_CANCELED": "Ακυρώθηκε", + "SERIES_STATUS_PILOT": "Πιλότος", "DURATION": "Διάρκεια", "CAST": "Πρόσωπα", "EPISODE_RUN_TIME": "Διάρκεια επεισοδίου", diff --git a/apps/web/src/assets/i18n/en.json b/apps/web/src/assets/i18n/en.json index c7d090fb6..ded4da1d5 100644 --- a/apps/web/src/assets/i18n/en.json +++ b/apps/web/src/assets/i18n/en.json @@ -802,6 +802,13 @@ "ACTOR_IN_LIBRARY": "In your library", "ACTOR_SEARCH_IN_PORTAL": "Search in portal", "ACTOR_BORN": "Born", + "ACTOR_DIED": "Died", + "SERIES_STATUS_RETURNING": "Returning", + "SERIES_STATUS_PLANNED": "Planned", + "SERIES_STATUS_IN_PRODUCTION": "In production", + "SERIES_STATUS_ENDED": "Ended", + "SERIES_STATUS_CANCELED": "Canceled", + "SERIES_STATUS_PILOT": "Pilot", "DURATION": "Duration", "CAST": "Cast", "EPISODE_RUN_TIME": "Episode run time", diff --git a/apps/web/src/assets/i18n/es.json b/apps/web/src/assets/i18n/es.json index 441a2b579..8dc779349 100644 --- a/apps/web/src/assets/i18n/es.json +++ b/apps/web/src/assets/i18n/es.json @@ -802,6 +802,13 @@ "ACTOR_IN_LIBRARY": "En tu biblioteca", "ACTOR_SEARCH_IN_PORTAL": "Buscar en el portal", "ACTOR_BORN": "Nacimiento:", + "ACTOR_DIED": "Fallecido", + "SERIES_STATUS_RETURNING": "En emisión", + "SERIES_STATUS_PLANNED": "Planificada", + "SERIES_STATUS_IN_PRODUCTION": "En producción", + "SERIES_STATUS_ENDED": "Finalizada", + "SERIES_STATUS_CANCELED": "Cancelada", + "SERIES_STATUS_PILOT": "Piloto", "DURATION": "Duración", "CAST": "Elenco", "EPISODE_RUN_TIME": "Tiempo de ejecución del episodio", diff --git a/apps/web/src/assets/i18n/fr.json b/apps/web/src/assets/i18n/fr.json index 37024977b..d1a2cbe55 100644 --- a/apps/web/src/assets/i18n/fr.json +++ b/apps/web/src/assets/i18n/fr.json @@ -802,6 +802,13 @@ "ACTOR_IN_LIBRARY": "Dans votre bibliothèque", "ACTOR_SEARCH_IN_PORTAL": "Rechercher dans le portail", "ACTOR_BORN": "Naissance :", + "ACTOR_DIED": "Décédé", + "SERIES_STATUS_RETURNING": "En cours", + "SERIES_STATUS_PLANNED": "Planifiée", + "SERIES_STATUS_IN_PRODUCTION": "En production", + "SERIES_STATUS_ENDED": "Terminée", + "SERIES_STATUS_CANCELED": "Annulée", + "SERIES_STATUS_PILOT": "Pilote", "DURATION": "Durée", "CAST": "Distribution", "EPISODE_RUN_TIME": "Durée de l'épisode", diff --git a/apps/web/src/assets/i18n/hu.json b/apps/web/src/assets/i18n/hu.json index 48c3f8f0a..929b2936d 100644 --- a/apps/web/src/assets/i18n/hu.json +++ b/apps/web/src/assets/i18n/hu.json @@ -802,6 +802,13 @@ "ACTOR_IN_LIBRARY": "A médiatárban", "ACTOR_SEARCH_IN_PORTAL": "Keresés a portálon", "ACTOR_BORN": "Születési dátum", + "ACTOR_DIED": "Elhunyt", + "SERIES_STATUS_RETURNING": "Folyamatban", + "SERIES_STATUS_PLANNED": "Tervezett", + "SERIES_STATUS_IN_PRODUCTION": "Gyártás alatt", + "SERIES_STATUS_ENDED": "Befejezett", + "SERIES_STATUS_CANCELED": "Törölt", + "SERIES_STATUS_PILOT": "Pilot", "DURATION": "Időtartam", "CAST": "Szereplők", "EPISODE_RUN_TIME": "Epizód játékideje", diff --git a/apps/web/src/assets/i18n/it.json b/apps/web/src/assets/i18n/it.json index 242958c72..809ac023a 100644 --- a/apps/web/src/assets/i18n/it.json +++ b/apps/web/src/assets/i18n/it.json @@ -802,6 +802,13 @@ "ACTOR_IN_LIBRARY": "Nella tua libreria", "ACTOR_SEARCH_IN_PORTAL": "Cerca nel portale", "ACTOR_BORN": "Nato:", + "ACTOR_DIED": "Deceduto", + "SERIES_STATUS_RETURNING": "In corso", + "SERIES_STATUS_PLANNED": "Pianificata", + "SERIES_STATUS_IN_PRODUCTION": "In produzione", + "SERIES_STATUS_ENDED": "Conclusa", + "SERIES_STATUS_CANCELED": "Cancellata", + "SERIES_STATUS_PILOT": "Pilota", "DURATION": "Durata", "CAST": "Cast", "EPISODE_RUN_TIME": "Tempo esecuzione episodio", diff --git a/apps/web/src/assets/i18n/ja.json b/apps/web/src/assets/i18n/ja.json index 51ef44cec..31fbfd08f 100644 --- a/apps/web/src/assets/i18n/ja.json +++ b/apps/web/src/assets/i18n/ja.json @@ -802,6 +802,13 @@ "ACTOR_IN_LIBRARY": "ライブラリにあり", "ACTOR_SEARCH_IN_PORTAL": "ポータルで検索", "ACTOR_BORN": "生年月日:", + "ACTOR_DIED": "死去", + "SERIES_STATUS_RETURNING": "放送中", + "SERIES_STATUS_PLANNED": "企画中", + "SERIES_STATUS_IN_PRODUCTION": "製作中", + "SERIES_STATUS_ENDED": "完結", + "SERIES_STATUS_CANCELED": "打ち切り", + "SERIES_STATUS_PILOT": "パイロット", "DURATION": "時間", "CAST": "キャスト", "EPISODE_RUN_TIME": "エピソード再生時間", diff --git a/apps/web/src/assets/i18n/ko.json b/apps/web/src/assets/i18n/ko.json index a7b0ed26c..ba89e55b2 100644 --- a/apps/web/src/assets/i18n/ko.json +++ b/apps/web/src/assets/i18n/ko.json @@ -802,6 +802,13 @@ "ACTOR_IN_LIBRARY": "내 라이브러리에 있음", "ACTOR_SEARCH_IN_PORTAL": "포털에서 검색", "ACTOR_BORN": "출생:", + "ACTOR_DIED": "사망", + "SERIES_STATUS_RETURNING": "방영 중", + "SERIES_STATUS_PLANNED": "기획 중", + "SERIES_STATUS_IN_PRODUCTION": "제작 중", + "SERIES_STATUS_ENDED": "완결", + "SERIES_STATUS_CANCELED": "취소됨", + "SERIES_STATUS_PILOT": "파일럿", "DURATION": "지속", "CAST": "깁스", "EPISODE_RUN_TIME": "에피소드 실행 시간", diff --git a/apps/web/src/assets/i18n/nl.json b/apps/web/src/assets/i18n/nl.json index 3723ffd8e..8743e9b09 100644 --- a/apps/web/src/assets/i18n/nl.json +++ b/apps/web/src/assets/i18n/nl.json @@ -802,6 +802,13 @@ "ACTOR_IN_LIBRARY": "In je bibliotheek", "ACTOR_SEARCH_IN_PORTAL": "Zoeken in portal", "ACTOR_BORN": "Geboren:", + "ACTOR_DIED": "Overleden", + "SERIES_STATUS_RETURNING": "Lopend", + "SERIES_STATUS_PLANNED": "Gepland", + "SERIES_STATUS_IN_PRODUCTION": "In productie", + "SERIES_STATUS_ENDED": "Afgelopen", + "SERIES_STATUS_CANCELED": "Geannuleerd", + "SERIES_STATUS_PILOT": "Pilot", "DURATION": "Lengte", "CAST": "Cast", "EPISODE_RUN_TIME": "Aflevering lengte", diff --git a/apps/web/src/assets/i18n/pl.json b/apps/web/src/assets/i18n/pl.json index f0bb39c07..9919dc02d 100644 --- a/apps/web/src/assets/i18n/pl.json +++ b/apps/web/src/assets/i18n/pl.json @@ -802,6 +802,13 @@ "ACTOR_IN_LIBRARY": "W twojej bibliotece", "ACTOR_SEARCH_IN_PORTAL": "Szukaj w portalu", "ACTOR_BORN": "Data urodzenia:", + "ACTOR_DIED": "Zmarł", + "SERIES_STATUS_RETURNING": "W trakcie", + "SERIES_STATUS_PLANNED": "Zaplanowany", + "SERIES_STATUS_IN_PRODUCTION": "W produkcji", + "SERIES_STATUS_ENDED": "Zakończony", + "SERIES_STATUS_CANCELED": "Anulowany", + "SERIES_STATUS_PILOT": "Pilot", "DURATION": "Czas trwania", "CAST": "Obsada", "EPISODE_RUN_TIME": "Czas trwania odcinka", diff --git a/apps/web/src/assets/i18n/pt.json b/apps/web/src/assets/i18n/pt.json index 3e56a1d62..7f87a7027 100644 --- a/apps/web/src/assets/i18n/pt.json +++ b/apps/web/src/assets/i18n/pt.json @@ -802,6 +802,13 @@ "ACTOR_IN_LIBRARY": "Na sua biblioteca", "ACTOR_SEARCH_IN_PORTAL": "Buscar no portal", "ACTOR_BORN": "Nascimento:", + "ACTOR_DIED": "Falecido", + "SERIES_STATUS_RETURNING": "Em exibição", + "SERIES_STATUS_PLANNED": "Planeada", + "SERIES_STATUS_IN_PRODUCTION": "Em produção", + "SERIES_STATUS_ENDED": "Terminada", + "SERIES_STATUS_CANCELED": "Cancelada", + "SERIES_STATUS_PILOT": "Piloto", "DURATION": "Duração", "CAST": "Elenco", "EPISODE_RUN_TIME": "Tempo de execução do episódio", diff --git a/apps/web/src/assets/i18n/ru.json b/apps/web/src/assets/i18n/ru.json index 2098932a9..7b43ff48b 100644 --- a/apps/web/src/assets/i18n/ru.json +++ b/apps/web/src/assets/i18n/ru.json @@ -802,6 +802,13 @@ "ACTOR_IN_LIBRARY": "Есть в вашей библиотеке", "ACTOR_SEARCH_IN_PORTAL": "Искать на портале", "ACTOR_BORN": "Родился:", + "ACTOR_DIED": "Умер", + "SERIES_STATUS_RETURNING": "Продолжается", + "SERIES_STATUS_PLANNED": "Запланирован", + "SERIES_STATUS_IN_PRODUCTION": "В производстве", + "SERIES_STATUS_ENDED": "Завершён", + "SERIES_STATUS_CANCELED": "Закрыт", + "SERIES_STATUS_PILOT": "Пилот", "DURATION": "Продолжительность", "CAST": "Бросать", "EPISODE_RUN_TIME": "Длительность эпизода", diff --git a/apps/web/src/assets/i18n/tr.json b/apps/web/src/assets/i18n/tr.json index 6131836af..7f03f8c15 100644 --- a/apps/web/src/assets/i18n/tr.json +++ b/apps/web/src/assets/i18n/tr.json @@ -802,6 +802,13 @@ "ACTOR_IN_LIBRARY": "Kitaplığınızda mevcut", "ACTOR_SEARCH_IN_PORTAL": "Portalda ara", "ACTOR_BORN": "Doğum:", + "ACTOR_DIED": "Vefat", + "SERIES_STATUS_RETURNING": "Devam ediyor", + "SERIES_STATUS_PLANNED": "Planlandı", + "SERIES_STATUS_IN_PRODUCTION": "Yapım aşamasında", + "SERIES_STATUS_ENDED": "Tamamlandı", + "SERIES_STATUS_CANCELED": "İptal edildi", + "SERIES_STATUS_PILOT": "Pilot", "DURATION": "Süre", "CAST": "Oyuncular", "EPISODE_RUN_TIME": "Bölüm süresi", diff --git a/apps/web/src/assets/i18n/zh.json b/apps/web/src/assets/i18n/zh.json index f50e930c8..6ecf42801 100644 --- a/apps/web/src/assets/i18n/zh.json +++ b/apps/web/src/assets/i18n/zh.json @@ -802,6 +802,13 @@ "ACTOR_IN_LIBRARY": "已在您的媒体库中", "ACTOR_SEARCH_IN_PORTAL": "在门户中搜索", "ACTOR_BORN": "出生:", + "ACTOR_DIED": "逝世", + "SERIES_STATUS_RETURNING": "连载中", + "SERIES_STATUS_PLANNED": "筹备中", + "SERIES_STATUS_IN_PRODUCTION": "制作中", + "SERIES_STATUS_ENDED": "已完结", + "SERIES_STATUS_CANCELED": "已取消", + "SERIES_STATUS_PILOT": "试播", "DURATION": "期间", "CAST": "投掷", "EPISODE_RUN_TIME": "剧集运行时间", diff --git a/apps/web/src/assets/i18n/zhtw.json b/apps/web/src/assets/i18n/zhtw.json index 2ceeb8071..619d60d72 100644 --- a/apps/web/src/assets/i18n/zhtw.json +++ b/apps/web/src/assets/i18n/zhtw.json @@ -802,6 +802,13 @@ "ACTOR_IN_LIBRARY": "已在您的媒體庫中", "ACTOR_SEARCH_IN_PORTAL": "在入口網站搜尋", "ACTOR_BORN": "出生:", + "ACTOR_DIED": "逝世", + "SERIES_STATUS_RETURNING": "連載中", + "SERIES_STATUS_PLANNED": "籌備中", + "SERIES_STATUS_IN_PRODUCTION": "製作中", + "SERIES_STATUS_ENDED": "已完結", + "SERIES_STATUS_CANCELED": "已取消", + "SERIES_STATUS_PILOT": "試播", "DURATION": "片長", "CAST": "演員陣容", "EPISODE_RUN_TIME": "集數時長", diff --git a/docs/architecture/tmdb-metadata-enrichment.md b/docs/architecture/tmdb-metadata-enrichment.md index 5c8b322fb..e810ec965 100644 --- a/docs/architecture/tmdb-metadata-enrichment.md +++ b/docs/architecture/tmdb-metadata-enrichment.md @@ -68,7 +68,12 @@ item lands. Enriched cast (`tmdb_cast` with profile photos) renders as avatar chips in the detail views, and so do directors/creators (`tmdb_directors`: movie directors from `credits.crew` with `job === 'Director'`, series creators from `created_by`) — both chip kinds -carry `tmdbPersonId` and open the same person page. A "check key" button +carry `tmdbPersonId` and open the same person page. Series detail views +also show a production-status chip (`tmdb_status`): TMDB returns `status` +as an ENGLISH string regardless of the request language, so it is +normalized to a token (`normalizeSeriesStatus`) and rendered through +translated labels (`seriesStatusLabelKey`) — the raw value never reaches +the UI. Unknown values are dropped rather than displayed. A "check key" button in the settings section validates the API key against `/configuration`. ## Match Confidence diff --git a/libs/portal/stalker/data-access/src/lib/stalker-vod.utils.ts b/libs/portal/stalker/data-access/src/lib/stalker-vod.utils.ts index 44c7c4d26..b70c28e65 100644 --- a/libs/portal/stalker/data-access/src/lib/stalker-vod.utils.ts +++ b/libs/portal/stalker/data-access/src/lib/stalker-vod.utils.ts @@ -161,6 +161,7 @@ export function createStalkerInfo(item: StalkerVodSource): StalkerVodInfo { ...(info.tmdb_directors ? { tmdb_directors: info.tmdb_directors } : {}), + ...(info.tmdb_status ? { tmdb_status: info.tmdb_status } : {}), ...(info.tmdb_backdrop ? { tmdb_backdrop: info.tmdb_backdrop } : {}), ...(info.tmdb_trailer ? { tmdb_trailer: info.tmdb_trailer } : {}), ...(info.tmdb_recommendations 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 8754f10b7..5f7615f50 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 @@ -10,6 +10,11 @@ > + @if (serial.info.tmdb_status; as status) { + {{ + seriesStatusLabelKey(status) | translate + }} + } @if (serial.info.releasedate) { {{ serial.info.releasedate }} } diff --git a/libs/portal/stalker/feature/src/lib/stalker-series-view/stalker-series-view.component.ts b/libs/portal/stalker/feature/src/lib/stalker-series-view/stalker-series-view.component.ts index 2038c58cd..88d6d48a1 100644 --- a/libs/portal/stalker/feature/src/lib/stalker-series-view/stalker-series-view.component.ts +++ b/libs/portal/stalker/feature/src/lib/stalker-series-view/stalker-series-view.component.ts @@ -23,11 +23,12 @@ import { SeasonContainerPlaybackToggleRequest, } from '@iptvnator/ui/components'; import { + pickSeasonMarkedTitle, PlaybackPositionData, ResolvedPortalPlayback, + seriesStatusLabelKey, TmdbEnrichedCastMember, XtreamSerieEpisode, - pickSeasonMarkedTitle, youtubeEmbedUrl, } from '@iptvnator/shared/interfaces'; import { SafePipe } from '@iptvnator/pipes'; @@ -135,6 +136,9 @@ export class StalkerSeriesViewComponent implements OnDestroy { private readonly tmdbSeasons = inject(StalkerSeriesTmdbSeasonsService); private readonly crossPortalSimilar = inject(CrossPortalSimilarService); + /** Maps the status token to its translated label key */ + readonly seriesStatusLabelKey = seriesStatusLabelKey; + /** * TMDB recommendations found in the user's Xtream portals (batched DB * match, Electron only) — Stalker catalogs are server-paginated, so diff --git a/libs/portal/xtream/feature/src/lib/serial-details/serial-details.component.html b/libs/portal/xtream/feature/src/lib/serial-details/serial-details.component.html index 05a2df569..3069de5d8 100644 --- a/libs/portal/xtream/feature/src/lib/serial-details/serial-details.component.html +++ b/libs/portal/xtream/feature/src/lib/serial-details/serial-details.component.html @@ -11,6 +11,11 @@ (closePlayerRequested)="closeInlinePlayer()" > + @if (info.tmdb_status; as status) { + {{ + seriesStatusLabelKey(status) | translate + }} + } @if (info.releaseDate) { {{ info.releaseDate | slice: 0 : 4 diff --git a/libs/portal/xtream/feature/src/lib/serial-details/serial-details.component.ts b/libs/portal/xtream/feature/src/lib/serial-details/serial-details.component.ts index 86d4dc37e..edb4362bf 100644 --- a/libs/portal/xtream/feature/src/lib/serial-details/serial-details.component.ts +++ b/libs/portal/xtream/feature/src/lib/serial-details/serial-details.component.ts @@ -31,11 +31,12 @@ import { type UpNextRailItem, } from '@iptvnator/ui/playback'; import { - TmdbEnrichedCastMember, - XtreamSerieInfo, - XtreamSerieEpisode, - XtreamSerieDetails, normalizeTitleKeys, + seriesStatusLabelKey, + TmdbEnrichedCastMember, + XtreamSerieDetails, + XtreamSerieEpisode, + XtreamSerieInfo, } from '@iptvnator/shared/interfaces'; import { CrossPortalSimilarItem, @@ -84,6 +85,9 @@ export class SerialDetailsComponent implements OnInit, OnDestroy { private readonly route = inject(ActivatedRoute); private readonly router = inject(Router); private readonly crossPortalSimilar = inject(CrossPortalSimilarService); + + /** Maps the status token to its translated label key */ + readonly seriesStatusLabelKey = seriesStatusLabelKey; private readonly xtreamStore = inject(XtreamStore); private readonly playback = inject(SerialDetailsPlaybackService); private readonly snackBar = inject(MatSnackBar); diff --git a/libs/services/src/lib/tmdb/tmdb-merge.spec.ts b/libs/services/src/lib/tmdb/tmdb-merge.spec.ts index 050d13775..084ff13df 100644 --- a/libs/services/src/lib/tmdb/tmdb-merge.spec.ts +++ b/libs/services/src/lib/tmdb/tmdb-merge.spec.ts @@ -303,3 +303,52 @@ describe('mergeSerieInfoWithTmdb', () => { expect(merged.cover).toBe('http://provider/cover.jpg'); }); }); + +describe('series status', () => { + const info: XtreamSerieInfo = { + name: 'The Boys', + cover: '', + plot: '', + cast: '', + director: '', + genre: '', + releaseDate: '', + last_modified: '', + rating: '', + rating_5based: 0, + backdrop_path: [], + youtube_trailer: '', + episode_run_time: '', + category_id: '1', + }; + const base: TmdbTvDetails = { id: 76479, name: 'The Boys' }; + + it('maps TMDB status strings to stable tokens', () => { + expect( + mergeSerieInfoWithTmdb(info, { + ...base, + status: 'Returning Series', + }).tmdb_status + ).toBe('returning'); + expect( + mergeSerieInfoWithTmdb(info, { ...base, status: 'Ended' }) + .tmdb_status + ).toBe('ended'); + }); + + it('is case-insensitive and accepts the British spelling', () => { + expect( + mergeSerieInfoWithTmdb(info, { ...base, status: 'cancelled' }) + .tmdb_status + ).toBe('canceled'); + }); + + it('omits the field for unknown or missing statuses', () => { + // Never surface a raw English string TMDB might add later + expect( + mergeSerieInfoWithTmdb(info, { ...base, status: 'Something New' }) + .tmdb_status + ).toBeUndefined(); + expect(mergeSerieInfoWithTmdb(info, base).tmdb_status).toBeUndefined(); + }); +}); diff --git a/libs/services/src/lib/tmdb/tmdb-merge.ts b/libs/services/src/lib/tmdb/tmdb-merge.ts index c4e0a2cca..4f309605d 100644 --- a/libs/services/src/lib/tmdb/tmdb-merge.ts +++ b/libs/services/src/lib/tmdb/tmdb-merge.ts @@ -1,4 +1,5 @@ import { + normalizeSeriesStatus, StalkerVodInfo, TmdbEnrichedCastMember, TmdbMediaType, @@ -218,6 +219,7 @@ export function mergeSerieInfoWithTmdb( ): XtreamSerieInfo { const tmdbCast = enrichedCast(details.credits); const tmdbDirectors = enrichedCreators(details); + const status = normalizeSeriesStatus(details.status); const trailer = pickTrailerKey(details); const recommendations = recommendationList(details); const cast = castNames(details.credits); @@ -240,6 +242,7 @@ export function mergeSerieInfoWithTmdb( backdrop_path: mergedBackdrops(details, info.backdrop_path), youtube_trailer: prefer(trailer, info.youtube_trailer), tmdb_id: details.id, + ...(status ? { tmdb_status: status } : {}), ...(tmdbDirectors.length > 0 ? { tmdb_directors: tmdbDirectors } : {}), ...(tmdbCast.length > 0 ? { tmdb_cast: tmdbCast } : {}), ...(recommendations.length > 0 @@ -264,6 +267,10 @@ export function mergeStalkerInfoWithTmdb( mediaType === 'movie' ? enrichedDirectors(details.credits) : enrichedCreators(details as TmdbTvDetails); + const status = + mediaType === 'tv' + ? normalizeSeriesStatus((details as TmdbTvDetails).status) + : null; const trailer = pickTrailerKey(details); const recommendations = recommendationList(details); const cast = castNames(details.credits); @@ -291,6 +298,7 @@ export function mergeStalkerInfoWithTmdb( rating_imdb: info.rating_imdb || (rating !== null ? String(rating) : ''), tmdb_id: details.id, + ...(status ? { tmdb_status: status } : {}), ...(backdrop ? { tmdb_backdrop: backdrop } : {}), ...(trailer ? { tmdb_trailer: trailer } : {}), ...(tmdbDirectors.length > 0 ? { tmdb_directors: tmdbDirectors } : {}), diff --git a/libs/services/src/lib/tmdb/tmdb.types.ts b/libs/services/src/lib/tmdb/tmdb.types.ts index f36ee6c06..c6ec2b093 100644 --- a/libs/services/src/lib/tmdb/tmdb.types.ts +++ b/libs/services/src/lib/tmdb/tmdb.types.ts @@ -98,6 +98,8 @@ export interface TmdbTvDetails extends TmdbDetailsBase { name?: string; original_name?: string; first_air_date?: string; + /** English production status ("Ended", "Returning Series", ...) */ + status?: string; episode_run_time?: number[]; created_by?: { id?: number; diff --git a/libs/shared/interfaces/src/lib/stalker-vod-details.interface.ts b/libs/shared/interfaces/src/lib/stalker-vod-details.interface.ts index 98e2d1458..247911423 100644 --- a/libs/shared/interfaces/src/lib/stalker-vod-details.interface.ts +++ b/libs/shared/interfaces/src/lib/stalker-vod-details.interface.ts @@ -1,6 +1,7 @@ import { TmdbEnrichedCastMember, TmdbRecommendation, + TmdbSeriesStatus, } from './tmdb.interface'; /** @@ -41,6 +42,8 @@ export interface StalkerVodInfo { tmdb_cast?: TmdbEnrichedCastMember[]; /** Directors (movies) / creators (series) as clickable person chips */ tmdb_directors?: TmdbEnrichedCastMember[]; + /** Series production status (token, never a raw TMDB string) */ + tmdb_status?: TmdbSeriesStatus; /** TMDB backdrop URL — Stalker portals never provide one themselves */ tmdb_backdrop?: string; /** YouTube trailer key from TMDB — Stalker portals provide no trailers */ diff --git a/libs/shared/interfaces/src/lib/tmdb.interface.ts b/libs/shared/interfaces/src/lib/tmdb.interface.ts index ce0e4bd9d..0174b3265 100644 --- a/libs/shared/interfaces/src/lib/tmdb.interface.ts +++ b/libs/shared/interfaces/src/lib/tmdb.interface.ts @@ -33,6 +33,51 @@ export const DEFAULT_TMDB_SETTINGS: TmdbSettings = { * 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 = { + '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 = { + 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; diff --git a/libs/shared/interfaces/src/lib/xtream-serie-details.interface.ts b/libs/shared/interfaces/src/lib/xtream-serie-details.interface.ts index b5a047c7c..681137182 100644 --- a/libs/shared/interfaces/src/lib/xtream-serie-details.interface.ts +++ b/libs/shared/interfaces/src/lib/xtream-serie-details.interface.ts @@ -1,6 +1,7 @@ import { TmdbEnrichedCastMember, TmdbRecommendation, + TmdbSeriesStatus, } from './tmdb.interface'; export interface XtreamSerieDetails { @@ -28,6 +29,8 @@ export interface XtreamSerieInfo { tmdb_cast?: TmdbEnrichedCastMember[]; /** Directors (movies) / creators (series) as clickable person chips */ tmdb_directors?: TmdbEnrichedCastMember[]; + /** Series production status (token, never a raw TMDB string) */ + tmdb_status?: TmdbSeriesStatus; /** Populated by TMDB enrichment; matched against the catalog in views */ tmdb_recommendations?: TmdbRecommendation[]; /** Matched TMDB show id — enables lazy season/episode enrichment */ diff --git a/libs/ui/shared-portals/src/lib/actor-view/actor-view.component.html b/libs/ui/shared-portals/src/lib/actor-view/actor-view.component.html index e9aeecc34..92bed9a46 100644 --- a/libs/ui/shared-portals/src/lib/actor-view/actor-view.component.html +++ b/libs/ui/shared-portals/src/lib/actor-view/actor-view.component.html @@ -27,10 +27,19 @@ }

{{ person.name }}

- @if (person.birthday) { + @if (person.birthday || person.deathday) {

- {{ 'XTREAM.ACTOR_BORN' | translate }} - {{ person.birthday }} + @if (person.birthday) { + {{ 'XTREAM.ACTOR_BORN' | translate }} + {{ person.birthday }} + } + @if (person.deathday) { + @if (person.birthday) { + · + } + {{ 'XTREAM.ACTOR_DIED' | translate }} + {{ person.deathday }} + } @if (person.placeOfBirth) { · {{ person.placeOfBirth }} } diff --git a/libs/ui/styles/_detail-view.scss b/libs/ui/styles/_detail-view.scss index e07d954cc..891b2af8a 100644 --- a/libs/ui/styles/_detail-view.scss +++ b/libs/ui/styles/_detail-view.scss @@ -135,6 +135,14 @@ color: #ffd700; border-color: rgba(255, 215, 0, 0.3); } + + // Series production status — a state, not a fact about the + // title, so it reads quieter than the other tags + &--status { + text-transform: uppercase; + font-size: 0.6875rem; + letter-spacing: 0.06em; + } } &__meta-item {