mirror of
https://github.com/4gray/iptvnator.git
synced 2026-10-08 17:06:15 -08:00
feat(tmdb): series production-status chip and person death dates (#1240)
Two fields TMDB already sends us and the merge threw away — no new API calls, no cache-key bump, they light up on existing cached payloads. Series detail views (Xtream and Stalker) gain a production-status chip: "Ended" tells you a show is finished before you commit to it, "Returning" that it is not. TMDB returns `status` as an ENGLISH string even under language=ru-RU, so it is normalized to a stable token (normalizeSeriesStatus) and rendered through translated labels (seriesStatusLabelKey). Unknown values are dropped rather than shown, so a status TMDB adds later can never leak raw English into 19 locales. Person pages render `deathday`, which mapPersonProfile has always parsed into ActorProfile and no template ever read. i18n: 7 keys across all 19 locales via the tools/i18n workflow. Tests: status normalization (token mapping, case-insensitivity, the British "cancelled" spelling, unknown/missing dropped). Docs: tmdb-metadata-enrichment.md, CLAUDE.md. Refs docs/architecture/tmdb-roadmap.md C1 and the zero-extra-call tier. Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
1 parent
cfa602d5b1
commit
8e1320cb34
34 files changed
+294
-9
No files matched your search
@@ -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`)
|
||||
|
||||
@@ -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": "مدة الحلقة",
|
||||
|
||||
@@ -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": "مدة تشغيل الحلقة",
|
||||
|
||||
@@ -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": "Працягласць эпізоду",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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": "Διάρκεια επεισοδίου",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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": "エピソード再生時間",
|
||||
|
||||
@@ -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": "에피소드 실행 시간",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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": "Длительность эпизода",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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": "剧集运行时间",
|
||||
|
||||
@@ -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": "集數時長",
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
+5
@@ -10,6 +10,11 @@
|
||||
>
|
||||
<!-- Tags: Year, Genre, Rating -->
|
||||
<ng-template appDetailTags>
|
||||
@if (serial.info.tmdb_status; as status) {
|
||||
<span class="details__tag details__tag--status">{{
|
||||
seriesStatusLabelKey(status) | translate
|
||||
}}</span>
|
||||
}
|
||||
@if (serial.info.releasedate) {
|
||||
<span class="details__tag">{{ serial.info.releasedate }}</span>
|
||||
}
|
||||
|
||||
+5
-1
@@ -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
|
||||
|
||||
@@ -11,6 +11,11 @@
|
||||
(closePlayerRequested)="closeInlinePlayer()"
|
||||
>
|
||||
<ng-template appDetailTags>
|
||||
@if (info.tmdb_status; as status) {
|
||||
<span class="details__tag details__tag--status">{{
|
||||
seriesStatusLabelKey(status) | translate
|
||||
}}</span>
|
||||
}
|
||||
@if (info.releaseDate) {
|
||||
<span class="details__tag">{{
|
||||
info.releaseDate | slice: 0 : 4
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
@@ -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 } : {}),
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 */
|
||||
|
||||
@@ -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<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;
|
||||
|
||||
@@ -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 */
|
||||
|
||||
@@ -27,10 +27,19 @@
|
||||
}
|
||||
<div class="actor-view__meta">
|
||||
<h1>{{ person.name }}</h1>
|
||||
@if (person.birthday) {
|
||||
@if (person.birthday || person.deathday) {
|
||||
<p class="actor-view__born">
|
||||
{{ '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 }}
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in new issue
Block a user