From 1ea6914f981e98391ddc1e656286c90daf349a1b Mon Sep 17 00:00:00 2001 From: 4gray Date: Thu, 21 May 2026 19:27:40 +0300 Subject: [PATCH] fix(pwa): harden xtream identity normalization --- .../pwa-xtream-data-source.spec.ts | 71 +++++++++++++++++++ .../data-sources/pwa-xtream-data-source.ts | 8 ++- 2 files changed, 76 insertions(+), 3 deletions(-) diff --git a/libs/portal/xtream/data-access/src/lib/data-sources/pwa-xtream-data-source.spec.ts b/libs/portal/xtream/data-access/src/lib/data-sources/pwa-xtream-data-source.spec.ts index 0b12daf62..2b7bcc5f0 100644 --- a/libs/portal/xtream/data-access/src/lib/data-sources/pwa-xtream-data-source.spec.ts +++ b/libs/portal/xtream/data-access/src/lib/data-sources/pwa-xtream-data-source.spec.ts @@ -48,6 +48,7 @@ describe('PwaXtreamDataSource', () => { case 'live': return Promise.resolve([ { + id: '0', stream_id: 101, name: 'News Live', stream_icon: 'live.png', @@ -58,6 +59,7 @@ describe('PwaXtreamDataSource', () => { case 'movie': return Promise.resolve([ { + id: 0, stream_id: 202, name: 'Movie One', stream_icon: 'movie.png', @@ -184,4 +186,73 @@ describe('PwaXtreamDataSource', () => { }), ]); }); + + it('does not persist zero or invalid Xtream identities as user collections', async () => { + apiService.getStreams.mockResolvedValue([ + { + id: 0, + name: 'Headless Zero', + category_id: '20', + }, + { + id: 'not-a-number', + name: 'Headless Invalid', + category_id: '20', + }, + ]); + + const content = (await dataSource.getContent( + 'playlist-1', + credentials, + 'movie' + )) as Array>; + + expect(content).toEqual([ + expect.objectContaining({ + id: -1, + xtream_id: -1, + }), + expect.objectContaining({ + id: -1, + xtream_id: -1, + }), + ]); + + await dataSource.addFavorite(-1, 'playlist-1'); + await dataSource.addRecentItem(-1, 'playlist-1'); + + expect(localStorage.getItem('xtream-favorites')).toBeNull(); + expect(localStorage.getItem('xtream-recent-items')).toBeNull(); + + localStorage.setItem( + 'xtream-favorites', + JSON.stringify({ 'playlist-1': [-1, 0, 'not-a-number'] }) + ); + localStorage.setItem( + 'xtream-recent-items', + JSON.stringify({ + 'playlist-1': [ + { + id: -1, + viewedAt: '2026-05-21T12:00:00.000Z', + }, + { + id: 0, + viewedAt: '2026-05-21T12:00:00.000Z', + }, + { + id: 'not-a-number', + viewedAt: '2026-05-21T12:00:00.000Z', + }, + ], + }) + ); + + await expect(dataSource.getFavorites('playlist-1')).resolves.toEqual( + [] + ); + await expect(dataSource.getRecentItems('playlist-1')).resolves.toEqual( + [] + ); + }); }); diff --git a/libs/portal/xtream/data-access/src/lib/data-sources/pwa-xtream-data-source.ts b/libs/portal/xtream/data-access/src/lib/data-sources/pwa-xtream-data-source.ts index 8ffbdba90..102cb6fbd 100644 --- a/libs/portal/xtream/data-access/src/lib/data-sources/pwa-xtream-data-source.ts +++ b/libs/portal/xtream/data-access/src/lib/data-sources/pwa-xtream-data-source.ts @@ -330,7 +330,7 @@ export class PwaXtreamDataSource implements IXtreamDataSource { ...item, added: item.added ?? item.last_modified ?? '', category_id: item.category_id ?? '', - id: Number.isFinite(id) ? id : xtreamId, + id: Number.isFinite(id) && id > 0 ? id : xtreamId, name: item.name ?? title, poster_url: posterUrl, rating: String(item.rating ?? ''), @@ -351,7 +351,7 @@ export class PwaXtreamDataSource implements IXtreamDataSource { item.series_id ?? item.id; const numericId = Number(preferredId); - return Number.isFinite(numericId) ? numericId : 0; + return Number.isFinite(numericId) && numericId > 0 ? numericId : -1; } // ========================================================================= @@ -724,7 +724,9 @@ export class PwaXtreamDataSource implements IXtreamDataSource { private normalizeStoredId(value: unknown): number | null { const numericValue = Number(value); - return Number.isFinite(numericValue) ? numericValue : null; + return Number.isFinite(numericValue) && numericValue > 0 + ? numericValue + : null; } private normalizeFavoriteStorage(value: unknown): Record {