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

@@ -231,6 +231,15 @@ describe('DashboardHeroSlidesPresenter', () => {
jest.useRealTimers();
});
it('leaves unavailable saved films in the collection rather than promoting them to a playable hero', () => {
favorites.set([{ ...favoriteFilm, catalogStatus: 'missing' }]);
expect(
create()
.slides()
.some((slide) => slide.kind === 'favorite')
).toBe(false);
});
it('builds the rotation from resume, live, favourite and import slides', () => {
const slides = create().slides();
@@ -1,3 +1,4 @@
import { isCatalogUnavailable } from '@iptvnator/shared/interfaces';
import {
computed,
DestroyRef,
@@ -128,7 +129,9 @@ export class DashboardHeroSlidesPresenter {
favorites: this.data
.globalFavoriteItems()
.filter(
(item) => item.type === 'movie' || item.type === 'series'
(item) =>
!isCatalogUnavailable(item) &&
(item.type === 'movie' || item.type === 'series')
),
recentlyAdded: this.data.xtreamRecentlyAddedItems(),
// Until the resume candidates are known the newest row may be one
@@ -1,6 +1,9 @@
import type { EpgProgram } from '@iptvnator/shared/interfaces';
import {
buildDashboardLiveEpgDetails,
buildLiveEpgLookupGroups,
getLiveEpgProgramForCard,
liveEpgProgramKey,
calcEpgProgress,
formatEpgTimeRange,
sameLiveEpgAnswers,
@@ -25,6 +28,23 @@ function program(overrides: Partial<EpgProgram> = {}): EpgProgram {
}
describe('dashboard-live-epg.utils', () => {
it.each(['missing', 'unknown'] as const)(
'does not query or enrich unavailable favorites (%s)',
(catalogStatus) => {
const card = {
id: 'saved',
title: 'ABC',
icon: 'live_tv',
link: [],
catalogStatus,
};
expect(buildLiveEpgLookupGroups([card], () => [])).toEqual([]);
const answers = new Map([
[liveEpgProgramKey('scope', 'ABC'), program()],
]);
expect(getLiveEpgProgramForCard(card, answers, 'scope')).toBeNull();
}
);
describe('timestamps in unix seconds', () => {
// `EpgProgram.startTimestamp`/`stopTimestamp` are unix SECONDS
// everywhere else in the app; treating them as milliseconds put
@@ -1,3 +1,4 @@
import { isCatalogUnavailable } from '@iptvnator/shared/interfaces';
import {
epgDisplayTimeMs,
epgProviderClockMs,
@@ -120,7 +121,9 @@ export function buildDashboardLiveEpgDetails(
}
function liveEpgLookupKeyForCard(card: DashboardRailCard): string {
return card.epgLookupKey?.trim() || card.title.trim();
return isCatalogUnavailable(card)
? ''
: card.epgLookupKey?.trim() || card.title.trim();
}
/** One XMLTV lookup: the scope it is answered in and the keys asked for. */
@@ -244,6 +247,7 @@ export function getLiveEpgProgramForCard(
epgMap: ReadonlyMap<string, EpgProgram | null>,
scopeKey: string
): EpgProgram | null {
if (isCatalogUnavailable(card)) return null;
const key = liveEpgLookupKeyForCard(card);
const program = epgMap.get(liveEpgProgramKey(scopeKey, key));
if (program) {
@@ -1,3 +1,4 @@
import type { XtreamFavoriteCatalogStatus } from '@iptvnator/shared/interfaces';
import {
AfterViewInit,
ChangeDetectionStrategy,
@@ -36,6 +37,7 @@ export interface DashboardRailCard {
id: string;
title: string;
subtitle?: string;
catalogStatus?: XtreamFavoriteCatalogStatus;
imageUrl?: string;
icon: string;
contentType?: 'live' | 'movie' | 'series';
@@ -513,6 +513,15 @@ export class WorkspaceDashboardRailsComponent {
private toFavoriteCard(item: DashboardFavoriteItem): DashboardRailCard {
return {
id: `fav-${item.id}-${item.playlist_id}-${item.added_at}`,
catalogStatus: item.catalogStatus,
subtitle:
item.catalogStatus && item.catalogStatus !== 'present'
? this.translate.instant(
item.catalogStatus === 'missing'
? 'WORKSPACE.GLOBAL_FAVORITES.CATALOG_MISSING'
: 'WORKSPACE.GLOBAL_FAVORITES.CATALOG_UNKNOWN'
)
: undefined,
title: item.title,
imageUrl: item.poster_url,
icon: this.typeIcon(item.type),