From ab8468be22101854cf9172e5ca18e373933350ae Mon Sep 17 00:00:00 2001 From: 4gray Date: Mon, 20 Jul 2026 21:31:32 +0200 Subject: [PATCH] fix(stalker): preserve series episode metadata --- .../stalker-series-quick-start.ts | 2 + .../stalker-series-view.component.html | 5 +- .../stalker-series-view.component.spec.ts | 88 ++++++++++++++++++- .../stalker-series-view.component.ts | 26 +++++- 4 files changed, 117 insertions(+), 4 deletions(-) diff --git a/libs/portal/stalker/feature/src/lib/stalker-series-view/stalker-series-quick-start.ts b/libs/portal/stalker/feature/src/lib/stalker-series-view/stalker-series-quick-start.ts index a94388d7c..1d61c63e1 100644 --- a/libs/portal/stalker/feature/src/lib/stalker-series-view/stalker-series-quick-start.ts +++ b/libs/portal/stalker/feature/src/lib/stalker-series-view/stalker-series-quick-start.ts @@ -15,6 +15,7 @@ import type { export interface StalkerQuickStartButton { labelKey: string; + labelParams?: Record; episodeLabel: string | null; icon: string; disabled: boolean; @@ -72,6 +73,7 @@ export function getStalkerSeriesQuickStartButton( return { labelKey: action.labelKey, + labelParams: action.labelParams, episodeLabel: action.episodeLabel, icon: action.icon, disabled: action.disabled, 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 3b90c38b0..861be1769 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 @@ -104,7 +104,10 @@ - {{ action.labelKey | translate }} + {{ + action.labelKey + | translate: action.labelParams + }} @if (action.episodeLabel) { 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 77e9599b5..81281c105 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 @@ -241,7 +241,17 @@ describe('StalkerSeriesViewComponent', () => { StubSeasonContainerComponent, MockPipe( TranslatePipe, - (value: string | null | undefined) => value ?? '' + ( + value: string | null | undefined, + params?: Record + ) => { + if (value === 'XTREAM.PLAY_EPISODE') { + return `Play episode ${ + params?.['episode'] ?? '{{episode}}' + }`; + } + return value ?? ''; + } ), ], }, @@ -290,6 +300,65 @@ describe('StalkerSeriesViewComponent', () => { ); }); + it('interpolates the episode number for a recently started VOD is_series episode', async () => { + selectedContentType.set('vod'); + selectedItem.set({ + id: '50001', + is_series: true, + info: { + name: 'VOD Flagged Series', + description: 'Lazy seasons', + movie_image: 'vod-series.jpg', + }, + }); + serialSeasonsResource.set([]); + vodSeriesSeasonsResource.set([]); + + fixture.detectChanges(); + await fixture.whenStable(); + fixture.componentInstance.vodSeriesSeasons.set([ + { + id: 'season-1', + video_id: '50001', + season_number: '1', + name: 'Season 1', + episodes: [ + { + id: 'episode-1', + series_number: 1, + name: 'Pilot', + }, + ], + isLoading: false, + isExpanded: false, + }, + ]); + const episode = fixture.componentInstance.mappedSeasons()['1'][0]; + fixture.componentInstance.episodePlaybackPositions.set( + new Map([ + [ + Number(episode.id), + { + contentXtreamId: Number(episode.id), + contentType: 'episode', + seriesXtreamId: 50001, + positionSeconds: 5, + durationSeconds: 100, + }, + ], + ]) + ); + fixture.detectChanges(); + + const button: HTMLButtonElement | null = + fixture.nativeElement.querySelector( + '[data-testid="series-quick-start"]' + ); + + expect(button?.textContent).toContain('Play episode 1'); + expect(button?.textContent).not.toContain('{{episode}}'); + }); + it('loads the first VOD-series season and starts its first episode from quick start', async () => { selectedContentType.set('vod'); selectedItem.set({ @@ -350,6 +419,15 @@ describe('StalkerSeriesViewComponent', () => { expect.any(Number), undefined ); + expect(openResolvedPlayback).toHaveBeenCalledWith( + expect.objectContaining({ + contentInfo: expect.objectContaining({ + seasonNumber: 1, + episodeNumber: 1, + }), + }), + true + ); }); it('loads an earlier unloaded VOD-series season before showing completed', async () => { @@ -541,6 +619,14 @@ describe('StalkerSeriesViewComponent', () => { By.directive(StubPortalInlinePlayerComponent) ).componentInstance as StubPortalInlinePlayerComponent; + expect(inlinePlayer.playback()).toEqual( + expect.objectContaining({ + contentInfo: expect.objectContaining({ + seasonNumber: 1, + episodeNumber: 1, + }), + }) + ); expect(inlinePlayer.episodeMetadata()).toEqual({ label: 'S01E01', title: 'Pilot', 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 5e7cb5da8..55316a158 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 @@ -687,15 +687,37 @@ export class StalkerSeriesViewComponent implements OnDestroy { episodeId, startTime ); + const episodeState = + episodeId === undefined + ? null + : resolveSeriesPlaybackEpisodeState({ + episodesBySeason: this.mappedSeasons(), + currentEpisodeId: episodeId, + fallbackEpisodeNumber: episodeNum, + }); + const resolvedPlayback = + episodeState && playback.contentInfo?.contentType === 'episode' + ? { + ...playback, + contentInfo: { + ...playback.contentInfo, + seasonNumber: episodeState.seasonNumber, + episodeNumber: episodeState.episodeNumber, + }, + } + : playback; this.lastSaveTime = 0; if (this.portalPlayer.isEmbeddedPlayer()) { - this.inlinePlayback.set(playback); + this.inlinePlayback.set(resolvedPlayback); return; } this.closeInlinePlayer(); - void this.portalPlayer.openResolvedPlayback(playback, true); + void this.portalPlayer.openResolvedPlayback( + resolvedPlayback, + true + ); } catch (error) { this.logger.error('Failed to start inline series playback', error); const errorMessage =