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

@@ -77,6 +77,7 @@ describe('XtreamRefreshFlowService', () => {
let confirmPromise: Promise<void> | undefined;
let order: string[];
let databaseService: {
restoreXtreamFavoriteSnapshots: jest.Mock;
createOperationId: jest.Mock;
deleteXtreamPlaylistContent: jest.Mock;
updateXtreamPlaylistDetails: jest.Mock;
@@ -94,6 +95,10 @@ describe('XtreamRefreshFlowService', () => {
order = [];
databaseService = {
restoreXtreamFavoriteSnapshots: jest.fn(() => {
order.push('save-favorites');
return Promise.resolve();
}),
createOperationId: jest.fn((prefix: string) => `${prefix}-op`),
deleteXtreamPlaylistContent: jest.fn(() => {
order.push('delete');
@@ -154,6 +159,53 @@ describe('XtreamRefreshFlowService', () => {
localStorage.clear();
});
it('converts legacy pending favorites before deleting or replacing the old catalog', async () => {
localStorage.setItem(
'xtream-restore-playlist-1',
JSON.stringify({
favorites: [{ contentType: 'movie', xtreamId: 7 }],
hiddenCategories: [],
recentlyViewed: [],
playbackPositions: [],
})
);
service.confirmAndRefresh(
createPlaylistMeta(),
createReporter().reporter
);
await confirmPromise;
expect(
databaseService.restoreXtreamFavoriteSnapshots
).toHaveBeenCalledWith(
'playlist-1',
[{ contentType: 'movie', xtreamId: 7 }],
undefined
);
expect(order.indexOf('save-favorites')).toBeLessThan(
order.indexOf('delete')
);
});
it('preserves legacy pending state and the catalog when durable conversion fails', async () => {
const pending = JSON.stringify({
favorites: [{ contentType: 'movie', xtreamId: 7 }],
});
localStorage.setItem('xtream-restore-playlist-1', pending);
databaseService.restoreXtreamFavoriteSnapshots.mockRejectedValue(
new Error('SQLite unavailable')
);
jest.spyOn(console, 'error').mockImplementation(() => undefined);
service.confirmAndRefresh(
createPlaylistMeta(),
createReporter().reporter
);
await confirmPromise;
expect(
databaseService.deleteXtreamPlaylistContent
).not.toHaveBeenCalled();
expect(localStorage.getItem('xtream-restore-playlist-1')).toBe(pending);
});
it('ignores a confirmation that arrives while the playlist is already busy', async () => {
const { reporter, calls } = createReporter({ isBusy: () => true });
@@ -152,6 +152,17 @@ export class XtreamRefreshFlowService {
);
await reporter.waitForVisibleProgress?.();
// Migrate an older parked favorite list before replacing its state.
// A durable receipt prevents replay after an explicit user removal.
const pending = this.pendingRestoreService.getOrThrow(item._id);
if (pending) {
await this.databaseService.restoreXtreamFavoriteSnapshots(
item._id,
pending.favorites,
pending.favoritesRestoreId
);
}
// Delete content/categories and update the timestamp in parallel —
// all three operations are fully independent.
const updateDate = Date.now();