diff --git a/libs/workspace/dashboard/data-access/src/lib/dashboard-data.service.spec.ts b/libs/workspace/dashboard/data-access/src/lib/dashboard-data.service.spec.ts index d94547821..9410ed0db 100644 --- a/libs/workspace/dashboard/data-access/src/lib/dashboard-data.service.spec.ts +++ b/libs/workspace/dashboard/data-access/src/lib/dashboard-data.service.spec.ts @@ -20,6 +20,20 @@ describe('DashboardDataService', () => { let service: DashboardDataService; const playlistsLoadedSignal = signal(true); + const createPendingItems = () => { + let resolvePending: (items: T[]) => void = () => { + throw new Error('Pending item promise resolved before init'); + }; + const promise = new Promise((resolve) => { + resolvePending = resolve; + }); + + return { + promise, + resolve: resolvePending, + }; + }; + const createDefaultPlaylists = (): PlaylistMeta[] => [ { _id: 'm3u-1', @@ -388,6 +402,74 @@ describe('DashboardDataService', () => { ); }); + it('loads PWA Xtream favorites for multiple playlists in parallel', async () => { + Object.defineProperty(window, 'electron', { + value: undefined, + configurable: true, + }); + playlistsSignal.set([ + { + _id: 'xtream-1', + title: 'Xtream One', + serverUrl: 'https://one.example.com', + }, + { + _id: 'xtream-2', + title: 'Xtream Two', + serverUrl: 'https://two.example.com', + }, + ]); + const firstFavorites = createPendingItems(); + xtreamDataSourceMock.getFavorites.mockImplementation((playlistId) => + playlistId === 'xtream-1' + ? firstFavorites.promise + : Promise.resolve([]) + ); + + const reload = service.reloadGlobalFavorites(); + await Promise.resolve(); + + expect(xtreamDataSourceMock.getFavorites).toHaveBeenCalledWith( + 'xtream-1' + ); + expect(xtreamDataSourceMock.getFavorites).toHaveBeenCalledWith( + 'xtream-2' + ); + + firstFavorites.resolve([]); + await reload; + }); + + it('uses the epoch fallback for PWA Xtream favorites with empty added dates', async () => { + Object.defineProperty(window, 'electron', { + value: undefined, + configurable: true, + }); + xtreamDataSourceMock.getFavorites.mockResolvedValue([ + { + id: 52, + category_id: 12, + title: 'Undated Movie', + rating: '8.0', + added: '', + poster_url: 'https://example.com/movie.png', + xtream_id: 5002, + type: 'movie', + }, + ]); + + await service.reloadGlobalFavorites(); + + expect(service.globalFavoriteItems()).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + id: 52, + added_at: new Date(0).toISOString(), + }), + ]) + ); + }); + it('does not load Stalker playlists through the PWA Xtream favorites path', async () => { Object.defineProperty(window, 'electron', { value: undefined, @@ -687,6 +769,44 @@ describe('DashboardDataService', () => { ); }); + it('loads PWA Xtream recent items for multiple playlists in parallel', async () => { + Object.defineProperty(window, 'electron', { + value: undefined, + configurable: true, + }); + playlistsSignal.set([ + { + _id: 'xtream-1', + title: 'Xtream One', + serverUrl: 'https://one.example.com', + }, + { + _id: 'xtream-2', + title: 'Xtream Two', + serverUrl: 'https://two.example.com', + }, + ]); + const firstRecentItems = createPendingItems(); + xtreamDataSourceMock.getRecentItems.mockImplementation((playlistId) => + playlistId === 'xtream-1' + ? firstRecentItems.promise + : Promise.resolve([]) + ); + + const reload = service.reloadGlobalRecentItems(); + await Promise.resolve(); + + expect(xtreamDataSourceMock.getRecentItems).toHaveBeenCalledWith( + 'xtream-1' + ); + expect(xtreamDataSourceMock.getRecentItems).toHaveBeenCalledWith( + 'xtream-2' + ); + + firstRecentItems.resolve([]); + await reload; + }); + it('does not load Stalker playlists through the PWA Xtream recent path', async () => { Object.defineProperty(window, 'electron', { value: undefined, diff --git a/libs/workspace/dashboard/data-access/src/lib/dashboard-data.service.ts b/libs/workspace/dashboard/data-access/src/lib/dashboard-data.service.ts index 4555f6346..5fac114de 100644 --- a/libs/workspace/dashboard/data-access/src/lib/dashboard-data.service.ts +++ b/libs/workspace/dashboard/data-access/src/lib/dashboard-data.service.ts @@ -434,33 +434,39 @@ export class DashboardDataService { private async loadPwaXtreamGlobalRecentItems(): Promise< GlobalRecentItem[] > { - const results: GlobalRecentItem[] = []; - for (const playlist of this.getXtreamPlaylists()) { - const rows = await this.xtreamDataSource.getRecentItems( - playlist._id - ); - results.push( - ...rows.map((item) => + const nested = await Promise.all( + this.getXtreamPlaylists().map(async (playlist) => { + const rows = await this.xtreamDataSource.getRecentItems( + playlist._id + ); + return rows.map((item) => this.mapPwaXtreamRecentItem(item, playlist) - ) - ); - } - return results; + ); + }) + ); + return nested.reduce( + (items, playlistItems) => items.concat(playlistItems), + [] + ); } private async loadPwaXtreamGlobalFavorites(): Promise< DashboardFavoriteItem[] > { - const results: DashboardFavoriteItem[] = []; - for (const playlist of this.getXtreamPlaylists()) { - const rows = await this.xtreamDataSource.getFavorites(playlist._id); - results.push( - ...rows.map((item) => + const nested = await Promise.all( + this.getXtreamPlaylists().map(async (playlist) => { + const rows = await this.xtreamDataSource.getFavorites( + playlist._id + ); + return rows.map((item) => this.mapPwaXtreamFavoriteItem(item, playlist) - ) - ); - } - return results; + ); + }) + ); + return nested.reduce( + (items, playlistItems) => items.concat(playlistItems), + [] + ); } private getXtreamPlaylists(): PlaylistMeta[] { @@ -498,7 +504,7 @@ export class DashboardDataService { type: this.normalizeXtreamActivityType(item.type), playlist_id: playlist._id, playlist_name: playlist.title || 'Xtream', - added_at: item.added_at ?? item.added ?? new Date(0).toISOString(), + added_at: item.added_at || item.added || new Date(0).toISOString(), category_id: item.category_id, xtream_id: item.xtream_id, poster_url: item.poster_url,