fix(xtream): preserve favorites across catalog refreshes

This commit is contained in:
4gray committed 2026-10-13 23:31:25 +02:00
1 parent ec4b9286a7
commit 8ccfcb68cc
121 files changed
+4000 -398

No files matched your search

@@ -632,6 +632,44 @@ describe('DashboardDataService', () => {
).toHaveLength(1);
});
it('keeps an unresolved favorite readable and removable without opening its details', async () => {
dbServiceMock.getAllGlobalFavorites.mockResolvedValue([
{
id: null,
category_id: null,
favorite_id: 71,
catalog_status: 'missing',
title: 'Saved ABC',
xtream_id: 501,
type: 'movie',
playlist_id: 'xtream-1',
playlist_name: 'Xtream Playlist',
added_at: '2026-02-02T10:00:00.000Z',
poster_url: 'https://example.test/abc.jpg',
},
]);
await service.reloadGlobalFavorites();
const item = service
.globalFavoriteItems()
.find((row) => row.source === 'xtream');
if (!item) throw new Error('Expected the saved Xtream favorite');
expect(item).toMatchObject({
id: 'favorite:71',
title: 'Saved ABC',
catalogStatus: 'missing',
favorite_id: 71,
});
expect(service.getGlobalFavoriteLink(item)).toEqual([
'/workspace/global-favorites',
]);
expect(service.getGlobalFavoriteNavigationState(item)).toBeUndefined();
await service.removeGlobalFavorite(item);
expect(dbServiceMock.removeFromFavorites).toHaveBeenCalledWith(
{ favoriteId: 71 },
'xtream-1'
);
});
it('includes PWA Xtream favorites from the active data source', async () => {
Object.defineProperty(window, 'electron', {
value: undefined,
@@ -1139,7 +1177,10 @@ describe('DashboardDataService', () => {
// Hold every answer: asked one after another, the second playlist
// would not be asked until the first had answered, and a DB worker
// busy with a long request would delay the set once per playlist.
const pending = new Map<string, (rows: PlaybackPositionData[]) => void>();
const pending = new Map<
string,
(rows: PlaybackPositionData[]) => void
>();
playbackPositionsMock.getAllPlaybackPositions.mockImplementation(
(playlistId: string) =>
new Promise<PlaybackPositionData[]>((resolve) => {
@@ -1,3 +1,4 @@
import { isCatalogUnavailable } from '@iptvnator/shared/interfaces';
import {
Injectable,
NgZone,
@@ -699,17 +700,19 @@ export class DashboardDataService {
}
private mapPwaXtreamFavoriteItem(
item: XtreamContentItem,
item: import('@iptvnator/portal/xtream/data-access').XtreamFavoriteItem,
playlist: PlaylistMeta
): DashboardFavoriteItem {
return {
id: item.id,
id: item.id ?? `favorite:${item.favorite_id}`,
favorite_id: item.favorite_id,
catalogStatus: item.catalog_status,
title: item.title,
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(),
category_id: item.category_id,
category_id: item.category_id ?? '',
xtream_id: item.xtream_id,
poster_url: item.poster_url,
backdrop_url: item.backdrop_url ?? undefined,
@@ -996,12 +999,14 @@ export class DashboardDataService {
}
getGlobalFavoriteLink(item: DashboardFavoriteItem): string[] {
if (isCatalogUnavailable(item)) return ['/workspace/global-favorites'];
return getGlobalFavoriteLinkUtil(item);
}
getGlobalFavoriteNavigationState(
item: DashboardFavoriteItem
): WorkspaceNavigationTarget['state'] {
if (isCatalogUnavailable(item)) return undefined;
return getGlobalFavoriteNavigationStateUtil(item);
}
@@ -1019,7 +1024,9 @@ export class DashboardDataService {
if (item.source === 'xtream') {
if (this.hasPortalActivityStorage) {
await this.dbService.removeFromFavorites(
item.id as number,
item.favorite_id != null
? { favoriteId: item.favorite_id }
: (item.id as number),
item.playlist_id
);
} else {
@@ -60,13 +60,15 @@ export function mapDbFavoriteToItem(
item: DbGlobalFavoriteItem
): PortalFavoriteItem {
return {
id: item.id,
id: item.id ?? `favorite:${item.favorite_id}`,
favorite_id: item.favorite_id,
catalogStatus: item.catalog_status,
title: item.title,
type: normalizeActivityType(item.type),
playlist_id: item.playlist_id,
playlist_name: item.playlist_name,
added_at: normalizeStalkerDate(item.added_at),
category_id: item.category_id,
category_id: item.category_id ?? '',
xtream_id: item.xtream_id,
poster_url: item.poster_url,
backdrop_url: item.backdrop_url ?? undefined,
@@ -1,5 +1,6 @@
import {
isStalkerRadioItem,
isCatalogUnavailable,
type EpgProgram,
type PortalActivityItem,
} from '@iptvnator/shared/interfaces';
@@ -35,7 +36,7 @@ export function buildDashboardPortalLiveEpgKey(
export function buildDashboardPortalLiveEpgEntry(
item: PortalActivityItem
): DashboardPortalLiveEpgEntry | null {
if (item.type !== 'live') {
if (item.type !== 'live' || isCatalogUnavailable(item)) {
return null;
}
if (item.source === 'xtream') {