mirror of
https://github.com/4gray/iptvnator.git
synced 2026-10-15 05:36:17 -08:00
fix(xtream): guard conflicted favorites and explicit replacements
This commit is contained in:
1 parent
8148f334e6
commit
4f0884da66
12 files changed
+361
-37
No files matched your search
@@ -720,6 +720,10 @@ test.describe('Electron Favorites', () => {
|
||||
name: sourcePortalTitle,
|
||||
});
|
||||
await waitForXtreamWorkspaceReady(app.mainWindow);
|
||||
const sourcePath = new URL(app.mainWindow.url()).pathname;
|
||||
const sourceId = decodeURIComponent(
|
||||
sourcePath.split('/xtreams/')[1].split('/')[0]
|
||||
);
|
||||
|
||||
await app.mainWindow
|
||||
.getByRole('link', { name: 'Movies', exact: true })
|
||||
@@ -807,6 +811,41 @@ test.describe('Electron Favorites', () => {
|
||||
);
|
||||
await switchUnifiedCollectionContent(app.mainWindow, 'Series');
|
||||
await expectVisibleContentCardTitle(app.mainWindow, seriesTitle);
|
||||
// Missing favorites must remain visible even while Host is active.
|
||||
await app.mainWindow.evaluate(async (playlistId) => {
|
||||
for (const type of ['movie', 'series'] as const) {
|
||||
await window.electron.dbClearXtreamImportCache(
|
||||
playlistId,
|
||||
type
|
||||
);
|
||||
await window.electron.dbSetAppState(
|
||||
`xtream-import-status:${playlistId}:${type}`,
|
||||
'completed'
|
||||
);
|
||||
}
|
||||
}, sourceId);
|
||||
for (const title of [movieTitle, seriesTitle]) {
|
||||
await goToDashboard(app.mainWindow);
|
||||
await app.mainWindow
|
||||
.getByTestId('dashboard-favorite-vod-rail-card')
|
||||
.filter({ hasText: title })
|
||||
.first()
|
||||
.click();
|
||||
await expectPathname(
|
||||
app.mainWindow,
|
||||
/\/workspace\/global-favorites$/
|
||||
);
|
||||
await expectVisibleContentCardTitle(app.mainWindow, title);
|
||||
await expect(
|
||||
app.mainWindow.locator('app-content-hero')
|
||||
).toHaveCount(0);
|
||||
await expect(
|
||||
playlistSwitcherTitle(app.mainWindow)
|
||||
).toContainText(hostPortalTitle);
|
||||
await expect(
|
||||
contentCardByTitle(app.mainWindow, title).first()
|
||||
).toContainText('Not in the current catalog');
|
||||
}
|
||||
} finally {
|
||||
await closeElectronApp(app);
|
||||
}
|
||||
|
||||
@@ -28,6 +28,14 @@ for (const identity of ['missing', 'conflicting'] as const) {
|
||||
});
|
||||
expect(response.ok()).toBeTruthy();
|
||||
const movie = (await response.json())[0];
|
||||
let detailRequests = 0;
|
||||
page.on('request', (request) => {
|
||||
if (
|
||||
new URL(request.url()).searchParams.get('action') ===
|
||||
'get_vod_info'
|
||||
)
|
||||
detailRequests++;
|
||||
});
|
||||
await page.route('**/localhost:3000/xtream**', async (route) => {
|
||||
const original = new URL(route.request().url());
|
||||
if (original.searchParams.get('action') !== 'get_vod_streams') {
|
||||
@@ -130,6 +138,18 @@ for (const identity of ['missing', 'conflicting'] as const) {
|
||||
added_at: favorite.addedAt,
|
||||
position: 3,
|
||||
});
|
||||
if (identity === 'conflicting') {
|
||||
const card = page.getByRole('button', {
|
||||
name: favorite.snapshot.title,
|
||||
exact: true,
|
||||
});
|
||||
await expect(card).toHaveAttribute('aria-disabled', 'true');
|
||||
await card.dispatchEvent('click');
|
||||
await card.focus();
|
||||
await page.keyboard.press('Enter');
|
||||
await expect(page.locator('app-content-hero')).toHaveCount(0);
|
||||
expect(detailRequests).toBe(0);
|
||||
}
|
||||
await page.goto('/workspace/settings/backup');
|
||||
const downloadPromise = page.waitForEvent('download');
|
||||
await page.getByRole('button', { name: 'Export', exact: true }).click();
|
||||
@@ -142,6 +162,28 @@ for (const identity of ['missing', 'conflicting'] as const) {
|
||||
entry.exportedId === playlistId
|
||||
).userState.favorites
|
||||
).toEqual([favorite]);
|
||||
if (identity === 'conflicting') {
|
||||
await page.goto('/workspace/global-favorites');
|
||||
const card = page
|
||||
.locator('app-content-card')
|
||||
.filter({ hasText: favorite.snapshot.title });
|
||||
await card
|
||||
.getByRole('button', {
|
||||
name: 'Remove from favorites',
|
||||
exact: true,
|
||||
})
|
||||
.click();
|
||||
await expect(card).toHaveCount(0);
|
||||
expect(
|
||||
await page.evaluate(
|
||||
(id) =>
|
||||
JSON.parse(
|
||||
localStorage.getItem('xtream-favorites') ?? '{}'
|
||||
)[id],
|
||||
playlistId
|
||||
)
|
||||
).toEqual([]);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -94,7 +94,9 @@ handler as the header Back.
|
||||
so the correct detail host still opens without switching playlist context.
|
||||
- Dashboard `Global Favorites` and `Recently Watched` widgets hand off Xtream and
|
||||
Stalker movies/series into the matching global collection route with detail
|
||||
pre-opened.
|
||||
pre-opened. Unavailable favorites instead open the all-playlists collection
|
||||
filtered to that content type, with no detail handoff, so an active different
|
||||
playlist cannot hide the saved entry.
|
||||
- The dashboard hero CTA and the Continue Watching cards' explicit "Resume
|
||||
episode" ⋮ action for Xtream and Stalker series carry a one-shot
|
||||
season/episode resume target. The collection-owned detail consumes it after
|
||||
|
||||
@@ -110,6 +110,16 @@ Favorites and Recent reads/writes share this resolution. Recent/detail backdrop
|
||||
writes check the actual catalog evidence, never the resolved collection row
|
||||
which already carries saved identity. Browsing, reload and re-export therefore
|
||||
cannot replace an imported snapshot merely because a provider reused its ID.
|
||||
A known browser identity conflict marks the retained snapshot unavailable, so
|
||||
collection and dashboard detail actions cannot open the replacement under the
|
||||
saved title. This survives reload and clears only when the saved identity is
|
||||
confirmed again. Explicit removal remains available. After removal, deliberately
|
||||
favoriting the current item may replace the old snapshot (including legacy key
|
||||
aliases); that replacement must persist before the new reference is saved, so
|
||||
storage failure cannot silently resurrect the old title. Ordinary optional
|
||||
snapshot-cache writes retain their existing quota behavior. The explicit browser
|
||||
favorite writer loads on demand; after loading, reference and snapshot writes
|
||||
run synchronously against fresh storage so concurrent additions are not lost.
|
||||
Initial browser imports and deferred retries share `PwaXtreamBackupStorage`;
|
||||
`PwaXtreamBackupStateService` supplies its injected storage for the importer.
|
||||
Both write the lists first and require the nested favorite snapshots to persist
|
||||
|
||||
@@ -145,12 +145,14 @@ export function findCollectionItems(
|
||||
/** Catalog evidence must confirm every saved identity field before replacing metadata. */
|
||||
export function collectionSnapshotMatchesContent(
|
||||
saved: XtreamContentItem,
|
||||
current: XtreamContentItem | undefined
|
||||
current: XtreamContentItem | undefined,
|
||||
allowMissing = false
|
||||
): boolean {
|
||||
return (
|
||||
(saved.tmdb_id == null || saved.tmdb_id === current?.tmdb_id) &&
|
||||
(saved.release_year == null ||
|
||||
saved.release_year === current?.release_year)
|
||||
return (['tmdb_id', 'release_year'] as const).every(
|
||||
(key) =>
|
||||
saved[key] == null ||
|
||||
saved[key] === current?.[key] ||
|
||||
(allowMissing && current?.[key] == null)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -160,10 +162,14 @@ function mergeCollectionItem(
|
||||
current: XtreamContentItem
|
||||
): XtreamContentItem {
|
||||
if (!saved) return current;
|
||||
if (!collectionSnapshotMatchesContent(saved, current, true)) {
|
||||
return { ...saved, catalog_status: 'missing' };
|
||||
}
|
||||
if (!collectionSnapshotMatchesContent(saved, current)) return saved;
|
||||
return {
|
||||
...saved,
|
||||
...current,
|
||||
catalog_status: 'present',
|
||||
title: current.title || saved.title,
|
||||
poster_url: current.poster_url || saved.poster_url,
|
||||
backdrop_url: current.backdrop_url || saved.backdrop_url,
|
||||
|
||||
@@ -13,12 +13,6 @@ import {
|
||||
} from '@iptvnator/services';
|
||||
import { XtreamContentItem } from './xtream-data-source.interface';
|
||||
|
||||
const STORAGE_KEYS = {
|
||||
COLLECTION_ITEMS: XTREAM_USER_DATA_KEYS.collectionItems,
|
||||
FAVORITES: XTREAM_USER_DATA_KEYS.favorites,
|
||||
RECENT_ITEMS: XTREAM_USER_DATA_KEYS.recentItems,
|
||||
};
|
||||
|
||||
/** Synchronous persistence and migration using only snapshots and cached identity evidence. */
|
||||
export class PwaCollectionStorage {
|
||||
constructor(
|
||||
@@ -57,7 +51,7 @@ export class PwaCollectionStorage {
|
||||
|
||||
getFavoritesFromStorage(): Record<string, CollectionKey[]> {
|
||||
try {
|
||||
const data = this.storage.getItem(STORAGE_KEYS.FAVORITES);
|
||||
const data = this.storage.getItem(XTREAM_USER_DATA_KEYS.favorites);
|
||||
return normalizeFavoriteStorage(data ? JSON.parse(data) : {});
|
||||
} catch {
|
||||
return {};
|
||||
@@ -65,7 +59,10 @@ export class PwaCollectionStorage {
|
||||
}
|
||||
|
||||
saveFavoritesToStorage(favorites: Record<string, CollectionKey[]>): void {
|
||||
this.storage.setItem(STORAGE_KEYS.FAVORITES, JSON.stringify(favorites));
|
||||
this.storage.setItem(
|
||||
XTREAM_USER_DATA_KEYS.favorites,
|
||||
JSON.stringify(favorites)
|
||||
);
|
||||
}
|
||||
|
||||
clearFavoritesForPlaylist(playlistId: string): void {
|
||||
@@ -131,7 +128,9 @@ export class PwaCollectionStorage {
|
||||
|
||||
getRecentItemsFromStorage(): Record<string, StoredRecentItem[]> {
|
||||
try {
|
||||
const data = this.storage.getItem(STORAGE_KEYS.RECENT_ITEMS);
|
||||
const data = this.storage.getItem(
|
||||
XTREAM_USER_DATA_KEYS.recentItems
|
||||
);
|
||||
return normalizeRecentStorage(data ? JSON.parse(data) : {});
|
||||
} catch {
|
||||
return {};
|
||||
@@ -142,7 +141,7 @@ export class PwaCollectionStorage {
|
||||
recentItems: Record<string, StoredRecentItem[]>
|
||||
): void {
|
||||
this.storage.setItem(
|
||||
STORAGE_KEYS.RECENT_ITEMS,
|
||||
XTREAM_USER_DATA_KEYS.recentItems,
|
||||
JSON.stringify(recentItems)
|
||||
);
|
||||
}
|
||||
@@ -158,7 +157,9 @@ export class PwaCollectionStorage {
|
||||
Record<string, XtreamContentItem>
|
||||
> {
|
||||
try {
|
||||
const data = this.storage.getItem(STORAGE_KEYS.COLLECTION_ITEMS);
|
||||
const data = this.storage.getItem(
|
||||
XTREAM_USER_DATA_KEYS.collectionItems
|
||||
);
|
||||
const parsed = data ? JSON.parse(data) : {};
|
||||
if (!parsed || typeof parsed !== 'object') {
|
||||
return {};
|
||||
@@ -169,11 +170,11 @@ export class PwaCollectionStorage {
|
||||
}
|
||||
}
|
||||
|
||||
private saveCollectionItemsToStorage(
|
||||
saveCollectionItemsToStorage(
|
||||
items: Record<string, Record<string, XtreamContentItem>>
|
||||
): void {
|
||||
this.storage.setItem(
|
||||
STORAGE_KEYS.COLLECTION_ITEMS,
|
||||
XTREAM_USER_DATA_KEYS.collectionItems,
|
||||
JSON.stringify(items)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
import {
|
||||
collectionSnapshotMatchesContent,
|
||||
itemCollectionKey,
|
||||
type CollectionKey,
|
||||
} from './pwa-collection-identity';
|
||||
import type { PwaCollectionStorage } from './pwa-collection-storage';
|
||||
import type { XtreamContentItem } from './xtream-data-source.interface';
|
||||
|
||||
/** Loaded only for an explicit favorite action; all storage writes remain synchronous. */
|
||||
export function addPwaFavorite(
|
||||
storage: PwaCollectionStorage,
|
||||
playlistId: string,
|
||||
key: CollectionKey,
|
||||
current: XtreamContentItem | undefined,
|
||||
matchingKeysFor: (ids: CollectionKey[]) => Set<CollectionKey>,
|
||||
backdropUrl?: string
|
||||
): void {
|
||||
const allFavorites = storage.getFavoritesFromStorage();
|
||||
const ids = (allFavorites[playlistId] ??= []);
|
||||
const matchingKeys = matchingKeysFor(ids);
|
||||
if (!ids.some((id) => matchingKeys.has(id))) {
|
||||
prepareReplacement(storage, playlistId, key, current, backdropUrl);
|
||||
ids.push(key);
|
||||
}
|
||||
storage.saveFavoritesToStorage(allFavorites);
|
||||
storage.getLocalFavorites(playlistId);
|
||||
storage.saveCollectionItemSnapshot(playlistId, key, backdropUrl);
|
||||
}
|
||||
|
||||
/** Only a new explicit favorite can replace the identity of a removed favorite. */
|
||||
function prepareReplacement(
|
||||
storage: PwaCollectionStorage,
|
||||
playlistId: string,
|
||||
key: CollectionKey,
|
||||
current: XtreamContentItem | undefined,
|
||||
backdropUrl?: string
|
||||
): void {
|
||||
if (!current) return;
|
||||
const allItems = storage.getCollectionItemsFromStorage();
|
||||
const snapshots = allItems[playlistId] ?? {};
|
||||
const keys = Object.keys(snapshots).filter(
|
||||
(storedKey) => itemCollectionKey(snapshots[storedKey]) === key
|
||||
);
|
||||
if (
|
||||
keys.every((storedKey) =>
|
||||
collectionSnapshotMatchesContent(snapshots[storedKey], current)
|
||||
)
|
||||
)
|
||||
return;
|
||||
for (const storedKey of keys) {
|
||||
snapshots[storedKey] = {
|
||||
...current,
|
||||
backdrop_url: current.backdrop_url || backdropUrl?.trim(),
|
||||
};
|
||||
}
|
||||
// Required before adding the reference: quota must not revive the old identity.
|
||||
storage.saveCollectionItemsToStorage({
|
||||
...allItems,
|
||||
[playlistId]: snapshots,
|
||||
});
|
||||
}
|
||||
+153
@@ -10,6 +10,7 @@ import { XtreamPlaylistBackupEntry } from '@iptvnator/shared/interfaces';
|
||||
import { of } from 'rxjs';
|
||||
import { XtreamApiService } from '../services/xtream-api.service';
|
||||
import { PwaXtreamDataSource } from './pwa-xtream-data-source';
|
||||
import { XtreamContentItem } from './xtream-data-source.interface';
|
||||
|
||||
const credentials = {
|
||||
serverUrl: 'http://localhost:3211',
|
||||
@@ -596,6 +597,11 @@ describe('PWA Xtream collection identity', () => {
|
||||
);
|
||||
}
|
||||
await source.getRecentItems('p1');
|
||||
if (identity.includes('conflict')) {
|
||||
for (const item of await source.getFavorites('p1')) {
|
||||
expect(item).toMatchObject({ catalog_status: 'missing' });
|
||||
}
|
||||
}
|
||||
const expected = favorites.map((favorite) =>
|
||||
identity === 'confirmed'
|
||||
? {
|
||||
@@ -626,6 +632,153 @@ describe('PWA Xtream collection identity', () => {
|
||||
}
|
||||
);
|
||||
|
||||
it.each([false, true])(
|
||||
'replaces an explicitly removed favorite with current catalog data (legacy=%s)',
|
||||
async (legacy) => {
|
||||
await source.restoreUserData('p1', {
|
||||
favorites: [
|
||||
{
|
||||
contentType: 'movie',
|
||||
xtreamId: 42,
|
||||
snapshot: { title: 'Original', tmdbId: 100 },
|
||||
},
|
||||
],
|
||||
hiddenCategories: [],
|
||||
recentlyViewed: [],
|
||||
playbackPositions: [],
|
||||
});
|
||||
if (legacy) {
|
||||
const snapshots = read('xtream-collection-items');
|
||||
snapshots.p1[42] = snapshots.p1['movie:42'];
|
||||
localStorage.setItem(
|
||||
'xtream-collection-items',
|
||||
JSON.stringify(snapshots)
|
||||
);
|
||||
}
|
||||
const replacement = {
|
||||
id: 42,
|
||||
xtream_id: 42,
|
||||
type: 'movie',
|
||||
title: 'Replacement',
|
||||
tmdb_id: 200,
|
||||
poster_url: 'new.jpg',
|
||||
} as XtreamContentItem;
|
||||
await source.saveContent('p1', [replacement], 'movie');
|
||||
await source.getFavorites('p1');
|
||||
// Re-adding while retained must not replace the original saved identity.
|
||||
await source.addFavorite({ id: 42, type: 'movie' }, 'p1');
|
||||
expect((await source.getFavorites('p1'))[0].title).toBe('Original');
|
||||
await source.removeFavorite({ id: 42, type: 'movie' }, 'p1');
|
||||
const setItem = Storage.prototype.setItem;
|
||||
const quota = jest
|
||||
.spyOn(Storage.prototype, 'setItem')
|
||||
.mockImplementation(function (this: Storage, key, value) {
|
||||
if (key === 'xtream-collection-items')
|
||||
throw new DOMException('full', 'QuotaExceededError');
|
||||
setItem.call(this, key, value);
|
||||
});
|
||||
await expect(
|
||||
source.addFavorite({ id: 42, type: 'movie' }, 'p1')
|
||||
).rejects.toMatchObject({ name: 'QuotaExceededError' });
|
||||
expect(await source.getFavorites('p1')).toEqual([]);
|
||||
quota.mockRestore();
|
||||
await source.addFavorite({ id: 42, type: 'movie' }, 'p1');
|
||||
reload();
|
||||
expect(await source.getFavorites('p1')).toEqual([
|
||||
expect.objectContaining({
|
||||
title: 'Replacement',
|
||||
tmdb_id: 200,
|
||||
poster_url: 'new.jpg',
|
||||
}),
|
||||
]);
|
||||
}
|
||||
);
|
||||
|
||||
it('retains concurrent explicit additions after loading the favorite writer', async () => {
|
||||
await loadAll();
|
||||
await Promise.all(
|
||||
types.map((type) => source.addFavorite({ id: 42, type }, 'p1'))
|
||||
);
|
||||
expect(
|
||||
(await source.getFavorites('p1')).map((item) => item.type)
|
||||
).toEqual(types);
|
||||
});
|
||||
|
||||
it('keeps a new favorite when a compatible Recent snapshot cannot be refreshed', async () => {
|
||||
await loadAll();
|
||||
await source.addRecentItem({ id: 42, type: 'movie' }, 'p1');
|
||||
const setItem = Storage.prototype.setItem;
|
||||
jest.spyOn(Storage.prototype, 'setItem').mockImplementation(function (
|
||||
this: Storage,
|
||||
key,
|
||||
value
|
||||
) {
|
||||
if (key === 'xtream-collection-items')
|
||||
throw new DOMException('full', 'QuotaExceededError');
|
||||
setItem.call(this, key, value);
|
||||
});
|
||||
await expect(
|
||||
source.addFavorite({ id: 42, type: 'movie' }, 'p1')
|
||||
).resolves.toBeUndefined();
|
||||
expect(
|
||||
(await source.getFavorites('p1')).map((item) => item.type)
|
||||
).toEqual(['movie']);
|
||||
});
|
||||
|
||||
it('keeps known conflicts unavailable through reload until the saved identity returns', async () => {
|
||||
await source.restoreUserData('p1', {
|
||||
favorites: [
|
||||
{
|
||||
contentType: 'movie',
|
||||
xtreamId: 42,
|
||||
snapshot: {
|
||||
title: 'Original',
|
||||
tmdbId: 100,
|
||||
releaseYear: 1995,
|
||||
},
|
||||
},
|
||||
],
|
||||
hiddenCategories: [],
|
||||
recentlyViewed: [],
|
||||
playbackPositions: [],
|
||||
});
|
||||
const returning = {
|
||||
id: 42,
|
||||
xtream_id: 42,
|
||||
type: 'movie',
|
||||
title: 'Returning',
|
||||
tmdb_id: 200,
|
||||
release_year: 1995,
|
||||
} as XtreamContentItem;
|
||||
await source.saveContent('p1', [returning], 'movie');
|
||||
expect((await source.getFavorites('p1'))[0]).toMatchObject({
|
||||
catalog_status: 'missing',
|
||||
title: 'Original',
|
||||
});
|
||||
reload();
|
||||
expect((await source.getFavorites('p1'))[0]).toMatchObject({
|
||||
catalog_status: 'missing',
|
||||
});
|
||||
await source.saveContent(
|
||||
'p1',
|
||||
[{ ...returning, tmdb_id: undefined }],
|
||||
'movie'
|
||||
);
|
||||
expect((await source.getFavorites('p1'))[0]).toMatchObject({
|
||||
catalog_status: 'missing',
|
||||
title: 'Original',
|
||||
});
|
||||
await source.saveContent(
|
||||
'p1',
|
||||
[{ ...returning, tmdb_id: 100 }],
|
||||
'movie'
|
||||
);
|
||||
expect((await source.getFavorites('p1'))[0]).toMatchObject({
|
||||
catalog_status: 'present',
|
||||
title: 'Returning',
|
||||
});
|
||||
});
|
||||
|
||||
it('keeps the artwork saved with each view through clear/restore', async () => {
|
||||
await source.restoreUserData('p1', {
|
||||
hiddenCategories: [],
|
||||
|
||||
@@ -716,23 +716,22 @@ export class PwaXtreamDataSource implements IXtreamDataSource {
|
||||
return;
|
||||
}
|
||||
|
||||
const allFavorites = this.collectionStorage.getFavoritesFromStorage();
|
||||
if (!allFavorites[playlistId]) {
|
||||
allFavorites[playlistId] = [];
|
||||
}
|
||||
const matchingKeys = this.getMatchingCollectionKeys(
|
||||
playlistId,
|
||||
allFavorites[playlistId],
|
||||
normalizedContentId
|
||||
);
|
||||
if (!allFavorites[playlistId].some((id) => matchingKeys.has(id))) {
|
||||
allFavorites[playlistId].push(normalizedContentId);
|
||||
}
|
||||
this.collectionStorage.saveFavoritesToStorage(allFavorites);
|
||||
this.collectionStorage.getLocalFavorites(playlistId);
|
||||
this.collectionStorage.saveCollectionItemSnapshot(
|
||||
const { addPwaFavorite } = await import('./pwa-favorite-write');
|
||||
addPwaFavorite(
|
||||
this.collectionStorage,
|
||||
playlistId,
|
||||
normalizedContentId,
|
||||
this.getCollectionItemsById(
|
||||
playlistId,
|
||||
[normalizedContentId],
|
||||
true
|
||||
).get(normalizedContentId),
|
||||
(ids) =>
|
||||
this.getMatchingCollectionKeys(
|
||||
playlistId,
|
||||
ids,
|
||||
normalizedContentId
|
||||
),
|
||||
backdropUrl
|
||||
);
|
||||
}
|
||||
|
||||
@@ -77,6 +77,8 @@ export interface XtreamContentItem {
|
||||
added_at?: string;
|
||||
viewed_at?: string;
|
||||
position?: number | null;
|
||||
/** Last confirmed availability of a persisted collection snapshot. */
|
||||
catalog_status?: XtreamFavoriteCatalogStatus;
|
||||
|
||||
// XtreamItem compatibility fields (optional for search/navigation)
|
||||
num?: number;
|
||||
@@ -100,7 +102,6 @@ export interface XtreamFavoriteItem extends Omit<
|
||||
id: number | null;
|
||||
category_id: number | string | null;
|
||||
favorite_id?: number;
|
||||
catalog_status?: XtreamFavoriteCatalogStatus;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -664,7 +664,9 @@ describe('DashboardDataService', () => {
|
||||
expect(service.getGlobalFavoriteLink(item)).toEqual([
|
||||
'/workspace/global-favorites',
|
||||
]);
|
||||
expect(service.getGlobalFavoriteNavigationState(item)).toBeUndefined();
|
||||
expect(service.getGlobalFavoriteNavigationState(item)).toEqual({
|
||||
collectionViewState: { scope: 'all', selectedContentType: 'movie' },
|
||||
});
|
||||
await service.removeGlobalFavorite(item);
|
||||
expect(dbServiceMock.removeFromFavorites).toHaveBeenCalledWith(
|
||||
{ favoriteId: 71 },
|
||||
|
||||
@@ -49,6 +49,7 @@ import {
|
||||
toTimestamp,
|
||||
} from './dashboard-mappers';
|
||||
import {
|
||||
COLLECTION_VIEW_STATE_KEY,
|
||||
PORTAL_PLAYBACK_POSITIONS,
|
||||
WorkspaceNavigationTarget,
|
||||
} from '@iptvnator/portal/shared/util';
|
||||
@@ -1006,7 +1007,14 @@ export class DashboardDataService {
|
||||
getGlobalFavoriteNavigationState(
|
||||
item: DashboardFavoriteItem
|
||||
): WorkspaceNavigationTarget['state'] {
|
||||
if (isCatalogUnavailable(item)) return undefined;
|
||||
if (isCatalogUnavailable(item)) {
|
||||
return {
|
||||
[COLLECTION_VIEW_STATE_KEY]: {
|
||||
scope: 'all',
|
||||
selectedContentType: item.type,
|
||||
},
|
||||
};
|
||||
}
|
||||
return getGlobalFavoriteNavigationStateUtil(item);
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user