From 3eda25ac3ca54f653fecbaf98a6c41d522465032 Mon Sep 17 00:00:00 2001 From: 4gray Date: Thu, 30 Jul 2026 09:29:08 +0200 Subject: [PATCH] fix(downloads): open Stalker items in provider details --- ...ownload-library-navigation.service.spec.ts | 93 +++++++++++++++++-- .../download-library-navigation.service.ts | 40 ++++++-- 2 files changed, 117 insertions(+), 16 deletions(-) diff --git a/libs/portal/downloads/feature/src/lib/download-library-navigation.service.spec.ts b/libs/portal/downloads/feature/src/lib/download-library-navigation.service.spec.ts index 97a73bb84..1a2908d59 100644 --- a/libs/portal/downloads/feature/src/lib/download-library-navigation.service.spec.ts +++ b/libs/portal/downloads/feature/src/lib/download-library-navigation.service.spec.ts @@ -279,6 +279,73 @@ describe('DownloadLibraryNavigationService', () => { expect(router.navigate).not.toHaveBeenCalled(); }); + it('opens a Stalker VOD at its canonical provider detail route', async () => { + playlists.getPlaylistById.mockReturnValue(of(STALKER_PLAYLIST)); + const recent = { + movie_id: '41', + category_id: 'vod', + title: 'Recent title', + name: 'Recent name', + } satisfies StalkerPortalItem; + playlists.getPortalRecentlyViewed.mockReturnValue(of([recent])); + + await expect(navigation.open(download())).resolves.toBe(true); + + expect(router.navigate).toHaveBeenCalledWith( + ['/workspace', 'stalker', PLAYLIST_ID, 'vod', 'vod'], + { + state: { + openStalkerItem: { + ...recent, + id: '41', + category_id: 'vod', + }, + }, + } + ); + }); + + it.each([ + ['is_series flag', { is_series: '1' }], + ['embedded episode list', { series: [1, 2] }], + ] as const)( + 'opens a recovered Stalker episode with an %s through the VOD-series detail route', + async (_label, vodSeriesMetadata) => { + playlists.getPlaylistById.mockReturnValue(of(STALKER_PLAYLIST)); + const recent = { + movie_id: '72', + category_id: 'series', + title: 'Recovered VOD series', + name: 'Recovered VOD series', + ...vodSeriesMetadata, + } satisfies StalkerPortalItem; + playlists.getPortalRecentlyViewed.mockReturnValue(of([recent])); + + await expect( + navigation.open( + download({ + contentType: 'episode', + xtreamId: 501, + seriesXtreamId: 72, + }) + ) + ).resolves.toBe(true); + + expect(router.navigate).toHaveBeenCalledWith( + ['/workspace', 'stalker', PLAYLIST_ID, 'vod', 'vod'], + { + state: { + openStalkerItem: { + ...recent, + id: '72', + name: 'Recovered VOD series', + }, + }, + } + ); + } + ); + it.each(['id', 'movie_id', 'series_id', 'stream_id'] as const)( 'matches a Stalker recent item by normalized %s', async (idKey) => { @@ -296,10 +363,10 @@ describe('DownloadLibraryNavigationService', () => { const expectedId = idKey === 'stream_id' ? '41' : recent[idKey]; expect(router.navigate).toHaveBeenCalledWith( - ['/workspace', 'stalker', PLAYLIST_ID, 'recent'], + ['/workspace', 'stalker', PLAYLIST_ID, 'vod', 'series'], { state: { - openRecentItem: { + openStalkerItem: { ...recent, id: expectedId, category_id: 'series', @@ -327,10 +394,10 @@ describe('DownloadLibraryNavigationService', () => { await expect(navigation.open(download())).resolves.toBe(true); expect(router.navigate).toHaveBeenCalledWith( - ['/workspace', 'stalker', PLAYLIST_ID, 'recent'], + ['/workspace', 'stalker', PLAYLIST_ID, 'vod', 'vod'], { state: { - openRecentItem: { + openStalkerItem: { ...recent, id: '41:episode', category_id: 'vod', @@ -364,10 +431,16 @@ describe('DownloadLibraryNavigationService', () => { await navigation.open(download({ contentType })); expect(router.navigate).toHaveBeenCalledWith( - ['/workspace', 'stalker', PLAYLIST_ID, 'recent'], + [ + '/workspace', + 'stalker', + PLAYLIST_ID, + contentType === 'episode' ? 'series' : 'vod', + expected, + ], { state: { - openRecentItem: expect.objectContaining({ + openStalkerItem: expect.objectContaining({ category_id: expected, }), }, @@ -385,10 +458,10 @@ describe('DownloadLibraryNavigationService', () => { await expect(navigation.open(download())).resolves.toBe(true); expect(router.navigate).toHaveBeenCalledWith( - ['/workspace', 'stalker', PLAYLIST_ID, 'recent'], + ['/workspace', 'stalker', PLAYLIST_ID, 'vod', 'vod'], { state: { - openRecentItem: { + openStalkerItem: { id: '41', category_id: 'vod', title: 'Downloaded title', @@ -416,10 +489,10 @@ describe('DownloadLibraryNavigationService', () => { await expect(navigation.open(item)).resolves.toBe(true); expect(router.navigate).toHaveBeenCalledWith( - ['/workspace', 'stalker', PLAYLIST_ID, 'recent'], + ['/workspace', 'stalker', PLAYLIST_ID, 'series', 'series'], { state: { - openRecentItem: { + openStalkerItem: { id: '72', category_id: 'series', title: 'Downloaded title', diff --git a/libs/portal/downloads/feature/src/lib/download-library-navigation.service.ts b/libs/portal/downloads/feature/src/lib/download-library-navigation.service.ts index d8f03a30d..3efb17977 100644 --- a/libs/portal/downloads/feature/src/lib/download-library-navigation.service.ts +++ b/libs/portal/downloads/feature/src/lib/download-library-navigation.service.ts @@ -6,11 +6,15 @@ import { type DownloadItem, PlaylistsService, } from '@iptvnator/services'; +import { buildStalkerDetailNavigationTarget } from '@iptvnator/portal/shared/util'; import type { StalkerPortalItem } from '@iptvnator/shared/interfaces'; type PortalSource = 'xtream' | 'stalker'; type StalkerCategory = 'vod' | 'series' | 'itv'; type StalkerFallbackCategory = Exclude; +type StalkerOpenItem = Record & { + category_id: StalkerCategory; +}; @Injectable() export class DownloadLibraryNavigationService { @@ -144,11 +148,23 @@ export class DownloadLibraryNavigationService { ); } + private isStalkerVodSeries(item: Record): boolean { + const seriesFlag = item['is_series']; + const embeddedSeries = item['series']; + + return ( + seriesFlag === true || + seriesFlag === 1 || + seriesFlag === '1' || + (Array.isArray(embeddedSeries) && embeddedSeries.length > 0) + ); + } + private async stalkerOpenState( item: DownloadItem, targetId: number, fallback: StalkerFallbackCategory - ): Promise> { + ): Promise { try { const recent = await firstValueFrom( this.playlists.getPortalRecentlyViewed(item.playlistId) @@ -194,15 +210,27 @@ export class DownloadLibraryNavigationService { targetId: number ): Promise { const fallback = item.contentType === 'episode' ? 'series' : 'vod'; - const openRecentItem = await this.stalkerOpenState( + const openStalkerItem = await this.stalkerOpenState( item, targetId, fallback ); + const isVodSeries = + item.contentType === 'episode' && + this.isStalkerVodSeries(openStalkerItem); + const target = buildStalkerDetailNavigationTarget({ + playlistId: item.playlistId, + type: + item.contentType === 'episode' && !isVodSeries + ? 'series' + : 'movie', + categoryId: + isVodSeries && openStalkerItem.category_id === 'series' + ? 'vod' + : openStalkerItem.category_id, + item: openStalkerItem, + }); - return this.router.navigate( - this.route('stalker', item.playlistId, ['recent']), - { state: { openRecentItem } } - ); + return this.router.navigate(target.link, { state: target.state }); } }