diff --git a/libs/portal/stalker/feature/src/lib/stalker-catalog-detail/stalker-catalog-detail.component.ts b/libs/portal/stalker/feature/src/lib/stalker-catalog-detail/stalker-catalog-detail.component.ts index 06373fb35..ded5cb3c3 100644 --- a/libs/portal/stalker/feature/src/lib/stalker-catalog-detail/stalker-catalog-detail.component.ts +++ b/libs/portal/stalker/feature/src/lib/stalker-catalog-detail/stalker-catalog-detail.component.ts @@ -264,6 +264,10 @@ export class StalkerCatalogDetailComponent implements OnDestroy { fetchMovieFileId: (id) => this.catalog.fetchMovieFileId(id), fetchLinkToPlay: (portalUrl, macAddress, cmd) => this.catalog.fetchLinkToPlay(portalUrl, macAddress, cmd), + language: + this.translateService.currentLang || + this.translateService.defaultLang || + 'en', }); } diff --git a/libs/portal/stalker/feature/src/lib/stalker-catalog-detail/stalker-vod-download.spec.ts b/libs/portal/stalker/feature/src/lib/stalker-catalog-detail/stalker-vod-download.spec.ts new file mode 100644 index 000000000..cf134e036 --- /dev/null +++ b/libs/portal/stalker/feature/src/lib/stalker-catalog-detail/stalker-vod-download.spec.ts @@ -0,0 +1,144 @@ +import type { VodDetailsItem } from '@iptvnator/shared/interfaces'; +import { startStalkerVodDownload } from './stalker-vod-download'; + +describe('startStalkerVodDownload', () => { + it('captures the rendered Stalker movie metadata without changing provider identity', async () => { + const startDownload = jest.fn().mockResolvedValue({ success: true }); + const deps = { + playlist: { + id: 'stalker-1', + portalUrl: 'https://stalker.example.test', + macAddress: '00:1A:79:12:34:56', + title: 'Living Room Portal', + }, + downloadsService: { startDownload }, + fetchMovieFileId: jest.fn(), + fetchLinkToPlay: jest + .fn() + .mockResolvedValue('https://cdn.example.test/movie.mpg'), + language: 'de', + }; + const item = { + type: 'stalker', + playlistId: 'stalker-1', + cmd: '/media/file_42.mpg', + data: { + id: '42', + cmd: '/media/file_42.mpg', + category_id: '7', + info: { + name: 'Metadata Movie', + o_name: 'Original Metadata Movie', + description: 'A captured Stalker plot', + movie_image: + 'https://images.example.test/posters/stalker-movie.jpg', + tmdb_backdrop: + 'https://images.example.test/backdrops/stalker-movie.jpg', + releasedate: '2024-11-03', + genre: 'Drama, Thriller', + rating_imdb: '7.7', + tmdb_id: 541, + tmdb_cast: [ + { + name: 'Stella Star', + character: 'Mira', + profileUrl: null, + tmdbPersonId: 77, + }, + ], + tmdb_directors: [ + { + name: 'Dorian Vale', + profileUrl: null, + tmdbPersonId: 78, + }, + ], + }, + }, + } as unknown as VodDetailsItem; + + await startStalkerVodDownload(item, deps); + + expect(startDownload).toHaveBeenCalledWith( + expect.objectContaining({ + playlistId: 'stalker-1', + playlistName: 'Living Room Portal', + playlistType: 'stalker', + portalUrl: 'https://stalker.example.test', + xtreamId: 42, + metadataSnapshot: expect.objectContaining({ + version: 1, + language: 'de', + mediaKind: 'movie', + title: 'Metadata Movie', + originalTitle: 'Original Metadata Movie', + plot: 'A captured Stalker plot', + posterUrl: + 'https://images.example.test/posters/stalker-movie.jpg', + backdropUrl: + 'https://images.example.test/backdrops/stalker-movie.jpg', + providerCategoryId: '7', + genres: ['Drama', 'Thriller'], + tmdbId: 541, + cast: [ + expect.objectContaining({ + name: 'Stella Star', + role: 'Mira', + tmdbPersonId: 77, + }), + ], + creators: [ + expect.objectContaining({ + name: 'Dorian Vale', + tmdbPersonId: 78, + }), + ], + }), + }) + ); + }); + + it('still captures a useful sparse Stalker snapshot', async () => { + const startDownload = jest.fn().mockResolvedValue({ success: true }); + const item = { + type: 'stalker', + playlistId: 'stalker-1', + cmd: '/media/file_9.mpg', + data: { + id: '9', + cmd: '/media/file_9.mpg', + title: 'Sparse title', + info: { + name: ' ', + rating_imdb: ' ', + tmdb_id: ' ', + }, + }, + } as unknown as VodDetailsItem; + + await startStalkerVodDownload(item, { + playlist: { + id: 'stalker-1', + portalUrl: 'https://stalker.example.test', + macAddress: '00:1A:79:12:34:56', + }, + downloadsService: { startDownload }, + fetchMovieFileId: jest.fn(), + fetchLinkToPlay: jest + .fn() + .mockResolvedValue('https://cdn.example.test/movie.mpg'), + }); + + expect(startDownload).toHaveBeenCalledWith( + expect.objectContaining({ + metadataSnapshot: expect.objectContaining({ + mediaKind: 'movie', + title: 'Sparse title', + }), + }) + ); + const snapshot = startDownload.mock.calls[0]?.[0]?.metadataSnapshot; + expect(snapshot).not.toHaveProperty('rating'); + expect(snapshot).not.toHaveProperty('tmdbId'); + }); +}); diff --git a/libs/portal/stalker/feature/src/lib/stalker-catalog-detail/stalker-vod-download.ts b/libs/portal/stalker/feature/src/lib/stalker-catalog-detail/stalker-vod-download.ts index 06ee93e7e..fac20a50f 100644 --- a/libs/portal/stalker/feature/src/lib/stalker-catalog-detail/stalker-vod-download.ts +++ b/libs/portal/stalker/feature/src/lib/stalker-catalog-detail/stalker-vod-download.ts @@ -1,5 +1,9 @@ import type { DownloadsService } from '@iptvnator/services'; -import type { VodDetailsItem } from '@iptvnator/shared/interfaces'; +import { createMovieDownloadSnapshot } from '@iptvnator/portal/shared/util'; +import type { + TmdbEnrichedCastMember, + VodDetailsItem, +} from '@iptvnator/shared/interfaces'; /** The Stalker branch of the item union — the only one carrying `cmd`. */ type StalkerVodDetailsItem = Extract; @@ -22,7 +26,22 @@ export interface DownloadVodData { id?: string | number; has_files?: unknown; title?: string; - info?: { name?: string; movie_image?: string }; + category_id?: string | number; + info?: { + name?: string; + o_name?: string; + description?: string; + movie_image?: string; + tmdb_backdrop?: string; + releasedate?: string; + genre?: string; + rating_imdb?: string | number; + tmdb_id?: string | number; + tmdb_cast?: TmdbEnrichedCastMember[]; + tmdb_directors?: TmdbEnrichedCastMember[]; + actors?: string; + director?: string; + }; } export interface StalkerVodDownloadPlaylist { @@ -44,6 +63,46 @@ export interface StalkerVodDownloadDeps { macAddress: string, cmd: string ) => Promise; + language?: string; +} + +function textList(value: string | undefined): string[] | undefined { + const entries = value + ?.split(',') + .map((entry) => entry.trim()) + .filter(Boolean); + return entries?.length ? entries : undefined; +} + +function number(value: string | number | undefined): number | undefined { + if (value === undefined || String(value).trim() === '') { + return undefined; + } + const parsed = Number(value); + return Number.isFinite(parsed) ? parsed : undefined; +} + +function year(value: string | undefined): number | undefined { + const match = value?.match(/(?:^|\D)((?:19|20)\d{2})(?:\D|$)/); + return match ? Number(match[1]) : undefined; +} + +function firstText(...values: (string | undefined)[]): string | undefined { + return values.map((value) => value?.trim()).find(Boolean); +} + +function people( + enriched: TmdbEnrichedCastMember[] | undefined, + fallback: string | undefined +) { + return ( + enriched?.map((person) => ({ + name: person.name, + role: person.character, + profileUrl: person.profileUrl ?? undefined, + tmdbPersonId: person.tmdbPersonId, + })) ?? textList(fallback)?.map((name) => ({ name })) + ); } export async function startStalkerVodDownload( @@ -60,6 +119,7 @@ export async function startStalkerVodDownload( } const itemData = item.data as DownloadVodData; + const title = firstText(itemData?.info?.name, itemData?.title) ?? 'Unknown'; const cmdToUse = await resolveDownloadCmd(item, itemData, deps); const url = await deps.fetchLinkToPlay( @@ -75,9 +135,31 @@ export async function startStalkerVodDownload( playlistId: playlist.id, xtreamId: normalizeStalkerEntityIdAsNumber(itemData?.id) ?? 0, contentType: 'vod', - title: itemData?.info?.name || itemData?.title || 'Unknown', + title, url, posterUrl: itemData?.info?.movie_image, + metadataSnapshot: createMovieDownloadSnapshot({ + language: deps.language?.trim() || 'en', + title, + originalTitle: itemData.info?.o_name, + plot: itemData.info?.description, + releaseDate: itemData.info?.releasedate, + year: year(itemData.info?.releasedate), + genres: textList(itemData.info?.genre), + rating: number(itemData.info?.rating_imdb), + posterUrl: itemData.info?.movie_image, + backdropUrl: itemData.info?.tmdb_backdrop, + tmdbId: number(itemData.info?.tmdb_id), + providerCategoryId: + itemData.category_id === undefined + ? undefined + : String(itemData.category_id), + cast: people(itemData.info?.tmdb_cast, itemData.info?.actors), + creators: people( + itemData.info?.tmdb_directors, + itemData.info?.director + ), + }), headers: { userAgent: playlist.userAgent, referer: playlist.referer, diff --git a/libs/portal/stalker/feature/src/lib/stalker-series-view/stalker-series-view.component.spec.ts b/libs/portal/stalker/feature/src/lib/stalker-series-view/stalker-series-view.component.spec.ts index 80a9ffaa1..e8e036802 100644 --- a/libs/portal/stalker/feature/src/lib/stalker-series-view/stalker-series-view.component.spec.ts +++ b/libs/portal/stalker/feature/src/lib/stalker-series-view/stalker-series-view.component.spec.ts @@ -90,6 +90,13 @@ describe('StalkerSeriesViewComponent', () => { const openResolvedPlayback = jest.fn(); const isEmbeddedPlayer = jest.fn(); const tmdbGetSeason = jest.fn(); + const startDownload = jest.fn(); + const fetchLinkToPlay = jest.fn(); + const currentPlaylist = signal({ + _id: 'stalker-1', + portalUrl: 'https://stalker.example.test', + macAddress: '00:1A:79:12:34:56', + }); beforeEach(async () => { selectedContentType.set('series'); @@ -148,6 +155,15 @@ describe('StalkerSeriesViewComponent', () => { overview: 'Season overview from TMDB', episodes: [], }); + startDownload.mockReset().mockResolvedValue({ success: true }); + fetchLinkToPlay + .mockReset() + .mockResolvedValue('https://cdn.example.test/episode.mpg'); + currentPlaylist.set({ + _id: 'stalker-1', + portalUrl: 'https://stalker.example.test', + macAddress: '00:1A:79:12:34:56', + }); await TestBed.configureTestingModule({ imports: [StalkerSeriesViewComponent], @@ -157,7 +173,7 @@ describe('StalkerSeriesViewComponent', () => { useValue: { selectedItem, selectedContentType, - currentPlaylist: signal({ _id: 'stalker-1' }), + currentPlaylist, getSerialSeasonsResource: () => serialSeasonsResource(), getVodSeriesSeasonsResource: () => vodSeriesSeasonsResource(), @@ -165,7 +181,7 @@ describe('StalkerSeriesViewComponent', () => { isSerialSeasonsLoading, fetchVodSeriesEpisodes, resolveVodPlayback, - fetchLinkToPlay: jest.fn(), + fetchLinkToPlay, clearSelectedItem: jest.fn(), }, }, @@ -199,7 +215,7 @@ describe('StalkerSeriesViewComponent', () => { { provide: DownloadsService, useValue: { - startDownload: jest.fn(), + startDownload, }, }, { @@ -222,6 +238,8 @@ describe('StalkerSeriesViewComponent', () => { instant: (key: string) => key, get: (key: string) => of(key), stream: (key: string) => of(key), + currentLang: 'en', + defaultLang: 'en', // EMPTY (not of(null)): the real TranslatePipe inside // the detail shell reads `event.lang` from emissions. onLangChange: EMPTY, @@ -328,6 +346,70 @@ describe('StalkerSeriesViewComponent', () => { ).not.toBeNull(); }); + it('captures enriched parent and episode metadata when an episode download starts', async () => { + selectedContentType.set('vod'); + selectedItem.set({ + id: '50001', + is_series: true, + category_id: '18', + info: { + name: 'Signal House', + description: 'Parent series plot', + movie_image: + 'https://images.example.test/posters/signal-house.jpg', + }, + }); + + fixture.detectChanges(); + await fixture.whenStable(); + + await fixture.componentInstance.downloadEpisode({ + id: 'episode-tracking-id', + episode_num: 2, + title: 'The Call', + container_extension: 'mpg', + info: { + plot: 'Episode-specific plot', + movie_image: 'https://images.example.test/stills/the-call.jpg', + duration_secs: 3120, + rating: 8.6, + }, + custom_sid: 'vod-series', + added: '', + season: 1, + direct_source: '', + originalId: '502', + } as never); + + expect(startDownload).toHaveBeenCalledWith( + expect.objectContaining({ + playlistId: 'stalker-1', + playlistType: 'stalker', + seriesXtreamId: 50001, + seasonNumber: 1, + episodeNumber: 2, + metadataSnapshot: expect.objectContaining({ + version: 1, + language: 'en', + mediaKind: 'series', + title: 'Signal House', + plot: 'Parent series plot', + posterUrl: + 'https://images.example.test/posters/signal-house.jpg', + providerCategoryId: '18', + episode: { + seasonNumber: 1, + episodeNumber: 2, + title: 'The Call', + plot: 'Episode-specific plot', + stillUrl: + 'https://images.example.test/stills/the-call.jpg', + }, + }), + }) + ); + }); + it('interpolates the episode number for a recently started VOD is_series episode', async () => { selectedContentType.set('vod'); selectedItem.set({ 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 950e0b651..ace0a9c58 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 @@ -29,6 +29,7 @@ import { seriesStatusLabelKey, TmdbEnrichedCastMember, XtreamSerieEpisode, + type XtreamSerieEpisodeInfo, youtubeEmbedUrl, } from '@iptvnator/shared/interfaces'; import { SafePipe } from '@iptvnator/pipes'; @@ -36,6 +37,7 @@ import { PORTAL_EXTERNAL_PLAYBACK, PORTAL_PLAYBACK_POSITIONS, PORTAL_PLAYER, + createSeriesEpisodeDownloadSnapshot, createLogger, getStalkerReturnToState, } from '@iptvnator/portal/shared/util'; @@ -90,6 +92,48 @@ interface SeriesPositionContext { readonly mutationKey: string; } +function snapshotTextList(value: string | undefined): string[] | undefined { + const entries = value + ?.split(',') + .map((entry) => entry.trim()) + .filter(Boolean); + return entries?.length ? entries : undefined; +} + +function snapshotNumber( + value: string | number | undefined +): number | undefined { + if (value === undefined || String(value).trim() === '') { + return undefined; + } + const parsed = Number(value); + return Number.isFinite(parsed) ? parsed : undefined; +} + +function snapshotYear(value: string | undefined): number | undefined { + const match = value?.match(/(?:^|\D)((?:19|20)\d{2})(?:\D|$)/); + return match ? Number(match[1]) : undefined; +} + +function snapshotPeople( + enriched: TmdbEnrichedCastMember[] | undefined, + fallback: string | undefined +) { + return ( + enriched?.map((person) => ({ + name: person.name, + role: person.character, + profileUrl: person.profileUrl ?? undefined, + tmdbPersonId: person.tmdbPersonId, + })) ?? snapshotTextList(fallback)?.map((name) => ({ name })) + ); +} + +function positiveCoordinate(value: number | string | undefined): number { + const parsed = Number(value); + return Number.isSafeInteger(parsed) && parsed > 0 ? parsed : 1; +} + /** * Component for displaying series/episodes for Stalker portal content. * Supports three modes: @@ -1061,10 +1105,12 @@ export class StalkerSeriesViewComponent implements OnDestroy { const episodeInfo = this.getEpisodeInfo(episode); const posterUrl = episodeInfo?.movie_image; - const seasonNum = Number(episode.season || 1); - const episodeNum = episode.episode_num || 1; + const seasonNum = positiveCoordinate(episode.season); + const episodeNum = positiveCoordinate(episode.episode_num); const seriesTitle = - item.info?.name || this.displayItem()?.info?.name || 'Series'; + item.info?.name?.trim() || + this.displayItem()?.info?.name?.trim() || + 'Series'; const episodeTitle = `${seriesTitle} - S${String(seasonNum).padStart( 2, '0' @@ -1077,6 +1123,36 @@ export class StalkerSeriesViewComponent implements OnDestroy { title: episodeTitle, url, posterUrl, + metadataSnapshot: createSeriesEpisodeDownloadSnapshot({ + language: + this.translateService.currentLang || + this.translateService.defaultLang || + 'en', + title: seriesTitle, + originalTitle: item.info?.o_name, + plot: item.info?.description, + releaseDate: item.info?.releasedate, + year: snapshotYear(item.info?.releasedate), + genres: snapshotTextList(item.info?.genre), + rating: snapshotNumber(item.info?.rating_imdb), + status: item.info?.tmdb_status, + posterUrl: item.info?.movie_image, + backdropUrl: item.info?.tmdb_backdrop, + tmdbId: item.info?.tmdb_id, + providerCategoryId: item.category_id, + cast: snapshotPeople(item.info?.tmdb_cast, item.info?.actors), + creators: snapshotPeople( + item.info?.tmdb_directors, + item.info?.director + ), + episode: { + seasonNumber: seasonNum, + episodeNumber: episodeNum, + title: episode.title, + plot: episodeInfo?.plot, + stillUrl: episodeInfo?.movie_image, + }, + }), seriesXtreamId: this.toSeriesId(item.id), seasonNumber: seasonNum, episodeNumber: episodeNum, @@ -1456,11 +1532,11 @@ export class StalkerSeriesViewComponent implements OnDestroy { private getEpisodeInfo( episode: XtreamSerieEpisode - ): { movie_image?: string } | undefined { + ): XtreamSerieEpisodeInfo | undefined { if (!episode.info || Array.isArray(episode.info)) { return undefined; } - return episode.info as { movie_image?: string }; + return episode.info; } private getEpisodeDownloadId(episode: XtreamSerieEpisode): number { diff --git a/libs/portal/xtream/feature/src/lib/vod-details/vod-details-downloads.service.ts b/libs/portal/xtream/feature/src/lib/vod-details/vod-details-downloads.service.ts index f16e46121..ba3491597 100644 --- a/libs/portal/xtream/feature/src/lib/vod-details/vod-details-downloads.service.ts +++ b/libs/portal/xtream/feature/src/lib/vod-details/vod-details-downloads.service.ts @@ -1,10 +1,46 @@ import { Injectable, Signal, computed, inject, signal } from '@angular/core'; import { XtreamStore } from '@iptvnator/portal/xtream/data-access'; +import { createMovieDownloadSnapshot } from '@iptvnator/portal/shared/util'; import { DownloadsService } from '@iptvnator/services'; import { resolveXtreamVodPlaybackSource } from '@iptvnator/portal/xtream/data-access'; -import { XtreamVodDetails } from '@iptvnator/shared/interfaces'; +import { + getXtreamVodInfo, + XtreamVodDetails, + type XtreamVodInfo, +} from '@iptvnator/shared/interfaces'; +import { TranslateService } from '@ngx-translate/core'; import { resolveXtreamVodPlaybackPresentation } from './vod-details-playback-presentation'; +function textList(value: string | undefined): string[] | undefined { + const entries = value + ?.split(',') + .map((entry) => entry.trim()) + .filter(Boolean); + return entries?.length ? entries : undefined; +} + +function number(value: number | string | undefined): number | undefined { + if (value === undefined || String(value).trim() === '') { + return undefined; + } + const parsed = Number(value); + return Number.isFinite(parsed) ? parsed : undefined; +} + +function year(value: string | undefined): number | undefined { + const match = value?.match(/(?:^|\D)((?:19|20)\d{2})(?:\D|$)/); + return match ? Number(match[1]) : undefined; +} + +function durationMinutes(info: XtreamVodInfo | null): number | undefined { + const seconds = number(info?.duration_secs); + if (seconds && seconds > 0) { + return Math.round(seconds / 60); + } + const minutes = number(info?.episode_run_time); + return minutes && minutes > 0 ? Math.round(minutes) : undefined; +} + /** * Offline downloads for the movie on screen. * @@ -16,6 +52,7 @@ import { resolveXtreamVodPlaybackPresentation } from './vod-details-playback-pre export class VodDetailsDownloadsService { private readonly downloadsService = inject(DownloadsService); private readonly xtreamStore = inject(XtreamStore); + private readonly translateService = inject(TranslateService); private routeContentId: Signal = signal(NaN); @@ -90,6 +127,7 @@ export class VodDetailsDownloadsService { } const presentation = resolveXtreamVodPlaybackPresentation(vodItem); + const info = getXtreamVodInfo(vodItem); const routeVodId = this.routeContentId(); const id = Number.isSafeInteger(routeVodId) && routeVodId > 0 @@ -103,6 +141,42 @@ export class VodDetailsDownloadsService { title: presentation.title, url: this.xtreamStore.constructVodStreamUrl(vodItem), posterUrl: presentation.posterUrl, + metadataSnapshot: createMovieDownloadSnapshot({ + language: + this.translateService.currentLang || + this.translateService.defaultLang || + 'en', + title: presentation.title, + originalTitle: info?.o_name, + plot: info?.plot || info?.description, + releaseDate: info?.releasedate, + year: year(info?.releasedate), + durationMinutes: durationMinutes(info), + genres: textList(info?.genre), + rating: number(info?.rating), + status: info?.status, + posterUrl: presentation.posterUrl, + backdropUrl: info?.backdrop_path?.[0], + tmdbId: number(info?.tmdb_id), + providerCategoryId: vodItem.movie_data?.category_id, + cast: + info?.tmdb_cast?.map((person) => ({ + name: person.name, + role: person.character, + profileUrl: person.profileUrl ?? undefined, + tmdbPersonId: person.tmdbPersonId, + })) ?? + textList(info?.actors || info?.cast)?.map((name) => ({ + name, + })), + creators: + info?.tmdb_directors?.map((person) => ({ + name: person.name, + role: person.character, + profileUrl: person.profileUrl ?? undefined, + tmdbPersonId: person.tmdbPersonId, + })) ?? textList(info?.director)?.map((name) => ({ name })), + }), headers: { userAgent: playlist.userAgent, referer: playlist.referrer, diff --git a/libs/portal/xtream/feature/src/lib/vod-details/vod-details-route-playback.spec.ts b/libs/portal/xtream/feature/src/lib/vod-details/vod-details-route-playback.spec.ts index 786c15957..d04c545eb 100644 --- a/libs/portal/xtream/feature/src/lib/vod-details/vod-details-route-playback.spec.ts +++ b/libs/portal/xtream/feature/src/lib/vod-details/vod-details-route-playback.spec.ts @@ -426,11 +426,42 @@ describe('VodDetailsRouteComponent — playback actions', () => { fixture.detectChanges(); await fixture.componentInstance.downloadVod({ + info: { + name: 'Metadata Movie', + description: 'Provider or TMDB description', + movie_image: + 'https://images.example.test/posters/metadata-movie.jpg', + backdrop_path: [ + 'https://images.example.test/backdrops/metadata-movie.jpg', + ], + releasedate: '2025-03-14', + duration_secs: 7200, + genre: 'Drama, Mystery', + rating: '8.4', + status: 'Released', + tmdb_id: 12345, + tmdb_cast: [ + { + name: 'Ada Actor', + character: 'The Lead', + profileUrl: 'https://image.tmdb.org/t/p/w185/ada.jpg', + tmdbPersonId: 91, + }, + ], + tmdb_directors: [ + { + name: 'Dana Director', + profileUrl: null, + tmdbPersonId: 92, + }, + ], + }, // A DIFFERENT id in the payload: the route's id must win. movie_data: { stream_id: 111, name: 'Example', container_extension: 'mp4', + category_id: '235', }, } as never); @@ -439,7 +470,37 @@ describe('VodDetailsRouteComponent — playback actions', () => { // (the Similar rail), and the snapshot still names the film the user // came from — so the download would fetch the wrong movie. expect(stubs.startDownload).toHaveBeenCalledWith( - expect.objectContaining({ xtreamId: 650020 }) + expect.objectContaining({ + playlistId: 'playlist-1', + xtreamId: 650020, + metadataSnapshot: expect.objectContaining({ + version: 1, + language: 'en', + mediaKind: 'movie', + title: 'Metadata Movie', + plot: 'Provider or TMDB description', + posterUrl: + 'https://images.example.test/posters/metadata-movie.jpg', + backdropUrl: + 'https://images.example.test/backdrops/metadata-movie.jpg', + providerCategoryId: '235', + tmdbId: 12345, + genres: ['Drama', 'Mystery'], + cast: [ + expect.objectContaining({ + name: 'Ada Actor', + role: 'The Lead', + tmdbPersonId: 91, + }), + ], + creators: [ + expect.objectContaining({ + name: 'Dana Director', + tmdbPersonId: 92, + }), + ], + }), + }) ); });