From 522e18e13d2588c6292d6b781852166956ccb29f Mon Sep 17 00:00:00 2001 From: 4gray Date: Fri, 30 Jan 2026 20:38:26 +0100 Subject: [PATCH] refactor: add Stalker VOD and serial details interfaces with normalization functions --- libs/shared/interfaces/src/index.ts | 8 + .../lib/stalker-serial-details.interface.ts | 28 ++++ .../src/lib/stalker-vod-details.interface.ts | 33 ++++ .../src/lib/vod-details-adapters.ts | 158 ++++++++++++++++++ .../src/lib/vod-details-item.interface.ts | 96 +++++++++++ 5 files changed, 323 insertions(+) create mode 100644 libs/shared/interfaces/src/lib/stalker-serial-details.interface.ts create mode 100644 libs/shared/interfaces/src/lib/stalker-vod-details.interface.ts create mode 100644 libs/shared/interfaces/src/lib/vod-details-adapters.ts create mode 100644 libs/shared/interfaces/src/lib/vod-details-item.interface.ts diff --git a/libs/shared/interfaces/src/index.ts b/libs/shared/interfaces/src/index.ts index 4b1c63f02..6a9dc7c19 100644 --- a/libs/shared/interfaces/src/index.ts +++ b/libs/shared/interfaces/src/index.ts @@ -27,3 +27,11 @@ export * from './lib/xtream-serie-details.interface'; export * from './lib/xtream-serie-item.interface'; export * from './lib/xtream-vod-details.interface'; export * from './lib/xtream-vod-stream.interface'; + +// Stalker interfaces +export * from './lib/stalker-vod-details.interface'; +export * from './lib/stalker-serial-details.interface'; + +// Unified VOD details (discriminated union + adapters) +export * from './lib/vod-details-item.interface'; +export * from './lib/vod-details-adapters'; diff --git a/libs/shared/interfaces/src/lib/stalker-serial-details.interface.ts b/libs/shared/interfaces/src/lib/stalker-serial-details.interface.ts new file mode 100644 index 000000000..76ed1300c --- /dev/null +++ b/libs/shared/interfaces/src/lib/stalker-serial-details.interface.ts @@ -0,0 +1,28 @@ +/** + * Stalker portal series/serial details response structure. + * Contains series metadata from Stalker/Ministra portals. + */ +export interface StalkerSerialDetails { + /** Series title */ + name: string; + /** Plot description */ + description: string; + /** Director name */ + director: string; + /** Comma-separated list of actors */ + actors: string; + /** Release year */ + year: string; + /** Comma-separated genres */ + genres_str: string; + /** Age rating */ + age: string; + /** IMDB rating as string */ + rating_imdb: string; + /** Kinopoisk rating as string */ + rating_kinopoisk: string; + /** Screenshot/poster URL */ + screenshot_uri: string; + /** Date added to portal */ + added: string; +} diff --git a/libs/shared/interfaces/src/lib/stalker-vod-details.interface.ts b/libs/shared/interfaces/src/lib/stalker-vod-details.interface.ts new file mode 100644 index 000000000..bb048d939 --- /dev/null +++ b/libs/shared/interfaces/src/lib/stalker-vod-details.interface.ts @@ -0,0 +1,33 @@ +/** + * Stalker portal VOD details response structure. + * Contains movie metadata from Stalker/Ministra portals. + */ +export interface StalkerVodDetails { + /** Unique identifier for the VOD item */ + id: string; + /** Command string used to play the stream (e.g., "/media/file_123.mpg") */ + cmd: string; + /** VOD metadata */ + info: StalkerVodInfo; +} + +export interface StalkerVodInfo { + /** Poster/cover image URL */ + movie_image: string; + /** Plot description */ + description: string; + /** Movie title */ + name: string; + /** Comma-separated list of actors */ + actors: string; + /** Director name */ + director: string; + /** Release date (format varies) */ + releasedate: string; + /** Genre(s) */ + genre: string; + /** IMDB rating as string */ + rating_imdb: string; + /** Kinopoisk rating as string */ + rating_kinopoisk: string; +} diff --git a/libs/shared/interfaces/src/lib/vod-details-adapters.ts b/libs/shared/interfaces/src/lib/vod-details-adapters.ts new file mode 100644 index 000000000..8a0a9f2d2 --- /dev/null +++ b/libs/shared/interfaces/src/lib/vod-details-adapters.ts @@ -0,0 +1,158 @@ +import { XtreamVodDetails } from './xtream-vod-details.interface'; +import { StalkerVodDetails } from './stalker-vod-details.interface'; +import { + NormalizedVodMeta, + VodDetailsItem, + XtreamVodItem, + StalkerVodItem, +} from './vod-details-item.interface'; + +/** + * Normalizes Xtream VOD details into a display-friendly format. + * Handles the various field name variations in Xtream API responses. + */ +export function normalizeXtreamVod(item: XtreamVodDetails): NormalizedVodMeta { + const info = item?.info; + const movieData = item?.movie_data; + + return { + title: info?.name || movieData?.name || 'Unknown', + description: info?.description || info?.plot, + posterUrl: info?.movie_image || info?.cover_big, + backdropUrl: info?.backdrop_path?.[0], + year: extractYear(info?.releasedate), + genre: info?.genre, + duration: info?.duration || formatDuration(info?.duration_secs), + country: info?.country, + director: info?.director, + actors: info?.actors || info?.cast, + ratingImdb: info?.rating_imdb, + ratingKinopoisk: info?.rating_kinopoisk, + youtubeTrailer: info?.youtube_trailer, + }; +} + +/** + * Normalizes Stalker VOD details into a display-friendly format. + * Stalker has fewer metadata fields than Xtream. + */ +export function normalizeStalkerVod(item: StalkerVodDetails): NormalizedVodMeta { + const info = item?.info; + + return { + title: info?.name || 'Unknown', + description: info?.description, + posterUrl: info?.movie_image, + backdropUrl: undefined, // Stalker doesn't provide backdrop + year: extractYear(info?.releasedate), + genre: info?.genre, + duration: undefined, // Stalker doesn't provide duration + country: undefined, // Stalker doesn't provide country + director: info?.director, + actors: info?.actors, + ratingImdb: info?.rating_imdb, + ratingKinopoisk: info?.rating_kinopoisk, + youtubeTrailer: undefined, // Stalker doesn't provide trailers + }; +} + +/** + * Normalizes any VodDetailsItem to display-friendly format. + * Uses discriminated union for type-safe handling. + */ +export function normalizeVodDetails(item: VodDetailsItem): NormalizedVodMeta { + if (item.type === 'xtream') { + return normalizeXtreamVod(item.data); + } + return normalizeStalkerVod(item.data); +} + +/** + * Creates an XtreamVodItem from raw data and context. + */ +export function createXtreamVodItem( + data: XtreamVodDetails, + playlistId: string, + vodId?: number +): XtreamVodItem { + return { + type: 'xtream', + data, + playlistId, + vodId: vodId ?? data.movie_data?.stream_id ?? 0, + }; +} + +/** + * Creates a StalkerVodItem from raw data and context. + */ +export function createStalkerVodItem( + data: StalkerVodDetails, + playlistId: string +): StalkerVodItem { + return { + type: 'stalker', + data, + playlistId, + cmd: data.cmd, + }; +} + +/** + * Gets the unique identifier for a VOD item. + * Returns vodId for Xtream, id for Stalker. + */ +export function getVodId(item: VodDetailsItem): number | string { + if (item.type === 'xtream') { + return item.vodId; + } + return item.data.id; +} + +/** + * Gets the numeric ID for download/favorite tracking. + */ +export function getVodNumericId(item: VodDetailsItem): number { + if (item.type === 'xtream') { + return item.vodId; + } + return Number(item.data.id) || 0; +} + +// ============ Helper Functions ============ + +/** + * Extracts 4-digit year from various date formats. + */ +function extractYear(dateString?: string): string | undefined { + if (!dateString) return undefined; + + // Try to extract 4-digit year from beginning + const yearMatch = dateString.match(/^(\d{4})/); + if (yearMatch) { + return yearMatch[1]; + } + + // Try to parse as date and extract year + const date = new Date(dateString); + if (!isNaN(date.getTime())) { + return date.getFullYear().toString(); + } + + return dateString.slice(0, 4); +} + +/** + * Formats duration from seconds to human-readable string. + */ +function formatDuration(seconds?: number): string | undefined { + if (!seconds || seconds <= 0) return undefined; + + const hours = Math.floor(seconds / 3600); + const minutes = Math.floor((seconds % 3600) / 60); + + if (hours > 0) { + return `${hours}h ${minutes}m`; + } + return `${minutes}m`; +} diff --git a/libs/shared/interfaces/src/lib/vod-details-item.interface.ts b/libs/shared/interfaces/src/lib/vod-details-item.interface.ts new file mode 100644 index 000000000..3e6b88373 --- /dev/null +++ b/libs/shared/interfaces/src/lib/vod-details-item.interface.ts @@ -0,0 +1,96 @@ +import { XtreamVodDetails } from './xtream-vod-details.interface'; +import { StalkerVodDetails } from './stalker-vod-details.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; +} + +/** + * 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'; +}