Files
iptvnator/libs/shared/interfaces/src/lib/vod-details-item.interface.ts
T
4grayandClaude Fable 5 b7e717d127 feat(tmdb): cross-portal "Similar" rail — Stalker gets it, Xtream gains other-portal matches
The Similar rail only existed on Xtream because it matched against the
locally loaded catalog; Stalker catalogs are server-paginated, so its
detail views had no rail despite tmdb_recommendations being cached.

New CrossPortalSimilarService (libs/services) matches recommendations
against ALL imported Xtream playlists with one batched DB_MATCH_TITLES
worker request — the same two-tier normalized-title + year-compatibility
rule as actor pages and the trending rail. Electron-only; resolves to []
in the PWA.

- Stalker: the shared VodDetailsComponent (movies; covers catalog and
  inline detail hosts) and stalker-series-view (series) now render a
  "Similar" rail from cross-portal matches, each card badged with the
  source playlist and navigating into that portal's detail view.
- Xtream: vod/serial detail rails keep instant local-catalog matches and
  append cross-portal matches (current playlist excluded, deduplicated
  against local hits by normalized exact title), also playlist-badged.
- Loads async after the detail view renders, staleness-guarded; the
  section only appears when there is something to show.

Tests: cross-portal-similar.service.spec.ts (PWA gate, navigation
targets, playlist exclusion, type/year guards). Docs updated.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-05 11:39:53 +02:00

104 lines
2.9 KiB
TypeScript

import { XtreamVodDetails } from './xtream-vod-details.interface';
import { StalkerVodDetails } from './stalker-vod-details.interface';
import { TmdbEnrichedCastMember,
TmdbRecommendation,
} from './tmdb.interface';
/**
* Discriminated union for VOD details across portal types.
* Enables type-safe handling without runtime type checks.
*
* @example
* ```typescript
* function handleVod(item: VodDetailsItem) {
* if (item.type === 'xtream') {
* // TypeScript knows item.data is XtreamVodDetails
* console.log(item.data.movie_data.stream_id);
* } else {
* // TypeScript knows item.data is StalkerVodDetails
* console.log(item.data.cmd);
* }
* }
* ```
*/
export type VodDetailsItem = XtreamVodItem | StalkerVodItem;
/**
* Xtream Codes VOD item wrapper with context.
*/
export interface XtreamVodItem {
/** Discriminator for Xtream Codes portals */
readonly type: 'xtream';
/** Full VOD details from Xtream API */
data: XtreamVodDetails;
/** Playlist/portal ID for favorites and downloads */
playlistId: string;
/** Stream ID from Xtream API */
vodId: number;
}
/**
* Stalker portal VOD item wrapper with context.
*/
export interface StalkerVodItem {
/** Discriminator for Stalker portals */
readonly type: 'stalker';
/** Full VOD details from Stalker API */
data: StalkerVodDetails;
/** Playlist/portal ID for favorites and downloads */
playlistId: string;
/** Command string for stream playback */
cmd: string;
}
/**
* Normalized metadata for display in templates.
* Provides a consistent interface regardless of portal type.
*/
export interface NormalizedVodMeta {
/** Movie title */
title: string;
/** Plot description */
description?: string;
/** Poster/cover image URL */
posterUrl?: string;
/** Backdrop image URL (Xtream only) */
backdropUrl?: string;
/** Release year (4 digits) */
year?: string;
/** Genre(s) */
genre?: string;
/** Duration string (e.g., "1h 30m") */
duration?: string;
/** Country of origin (Xtream only) */
country?: string;
/** Director name */
director?: string;
/** Actors/cast (comma-separated) */
actors?: string;
/** IMDB rating */
ratingImdb?: string;
/** Kinopoisk rating */
ratingKinopoisk?: string;
/** YouTube trailer ID (Xtream only) */
youtubeTrailer?: string;
/** Cast with profile photos, populated by TMDB enrichment */
tmdbCast?: TmdbEnrichedCastMember[];
/** TMDB recommendations (drives the cross-portal "Similar" rail) */
tmdbRecommendations?: TmdbRecommendation[];
}
/**
* Type guard to check if item is Xtream VOD.
*/
export function isXtreamVod(item: VodDetailsItem): item is XtreamVodItem {
return item.type === 'xtream';
}
/**
* Type guard to check if item is Stalker VOD.
*/
export function isStalkerVod(item: VodDetailsItem): item is StalkerVodItem {
return item.type === 'stalker';
}