mirror of
https://github.com/4gray/iptvnator.git
synced 2026-10-15 13:46:16 -08:00
fix(xtream): protect favorite snapshots and collection navigation
This commit is contained in:
1 parent
8ccfcb68cc
commit
e5a5d0581d
16 files changed
+427
-50
No files matched your search
@@ -86,6 +86,64 @@ describe('DatabaseService browser guards', () => {
|
||||
);
|
||||
});
|
||||
|
||||
it.each([
|
||||
'getGlobalFavorites',
|
||||
'getAllGlobalFavorites',
|
||||
'getFavorites',
|
||||
] as const)(
|
||||
'%s still reads healthy favorites when a pending conversion fails',
|
||||
async (method) => {
|
||||
const pending = JSON.stringify({
|
||||
favorites: [{ contentType: 'movie', xtreamId: 7.5 }],
|
||||
});
|
||||
localStorage.setItem('xtream-restore-broken', pending);
|
||||
localStorage.setItem(
|
||||
'xtream-restore-healthy',
|
||||
JSON.stringify({
|
||||
favorites: [{ contentType: 'movie', xtreamId: 9 }],
|
||||
})
|
||||
);
|
||||
const rows = [{ favorite_id: 1, title: 'Saved favorite' }];
|
||||
const read = jest.fn().mockResolvedValue(rows);
|
||||
const restore = jest.fn(async (id: string) => {
|
||||
if (id === 'broken')
|
||||
throw new Error('Invalid favorite identity');
|
||||
return { success: true };
|
||||
});
|
||||
Object.defineProperty(window, 'electron', {
|
||||
configurable: true,
|
||||
value: {
|
||||
dbGetPlaylist: jest
|
||||
.fn()
|
||||
.mockResolvedValue({ type: 'xtream' }),
|
||||
dbRestoreXtreamFavorites: restore,
|
||||
dbGetGlobalFavorites: read,
|
||||
dbGetAllGlobalFavorites: read,
|
||||
dbGetFavorites: read,
|
||||
dbGetFavoritesForBackup: jest.fn(),
|
||||
},
|
||||
});
|
||||
await expect(service[method]('broken')).resolves.toEqual(rows);
|
||||
expect(read).toHaveBeenCalledTimes(1);
|
||||
if (method !== 'getFavorites') {
|
||||
expect(restore).toHaveBeenCalledWith(
|
||||
'healthy',
|
||||
[{ contentType: 'movie', xtreamId: 9 }],
|
||||
undefined
|
||||
);
|
||||
}
|
||||
expect(localStorage.getItem('xtream-restore-broken')).toBe(pending);
|
||||
restore.mockResolvedValueOnce({ success: false });
|
||||
await expect(service.getFavorites('broken')).resolves.toEqual(rows);
|
||||
await expect(service.getFavoritesOrThrow('broken')).rejects.toThrow(
|
||||
'Invalid favorite identity'
|
||||
);
|
||||
expect(
|
||||
window.electron.dbGetFavoritesForBackup
|
||||
).not.toHaveBeenCalled();
|
||||
}
|
||||
);
|
||||
|
||||
it('ignores orphan legacy state but never treats failed conversion as an empty backup', async () => {
|
||||
localStorage.setItem(
|
||||
'xtream-restore-orphan',
|
||||
|
||||
@@ -741,7 +741,11 @@ export class DatabaseService {
|
||||
}
|
||||
|
||||
try {
|
||||
await materializePendingXtreamFavorites(window.electron);
|
||||
await materializePendingXtreamFavorites(
|
||||
window.electron,
|
||||
undefined,
|
||||
'continue'
|
||||
);
|
||||
const items = await window.electron.dbGetGlobalFavorites();
|
||||
return items || [];
|
||||
} catch (error) {
|
||||
@@ -759,7 +763,11 @@ export class DatabaseService {
|
||||
}
|
||||
|
||||
try {
|
||||
await materializePendingXtreamFavorites(window.electron);
|
||||
await materializePendingXtreamFavorites(
|
||||
window.electron,
|
||||
undefined,
|
||||
'continue'
|
||||
);
|
||||
const items = await window.electron.dbGetAllGlobalFavorites();
|
||||
return items || [];
|
||||
} catch (error) {
|
||||
@@ -901,7 +909,8 @@ export class DatabaseService {
|
||||
try {
|
||||
await materializePendingXtreamFavorites(
|
||||
window.electron,
|
||||
playlistId
|
||||
playlistId,
|
||||
'continue'
|
||||
);
|
||||
return await window.electron.dbGetFavorites(playlistId);
|
||||
} catch (error) {
|
||||
|
||||
@@ -10,7 +10,8 @@ export async function materializePendingXtreamFavorites(
|
||||
ElectronBridgeApi,
|
||||
'dbGetPlaylist' | 'dbRestoreXtreamFavorites'
|
||||
>,
|
||||
playlistId?: string
|
||||
playlistId?: string,
|
||||
failureMode: 'throw' | 'continue' = 'throw'
|
||||
): Promise<void> {
|
||||
const prefix = getXtreamPendingRestoreStorageKey('');
|
||||
const ids = playlistId
|
||||
@@ -35,14 +36,20 @@ export async function materializePendingXtreamFavorites(
|
||||
if (!state.favorites.length) continue;
|
||||
// Old localStorage can outlive deletion of its owning SQLite playlist.
|
||||
// Never recreate a playlist or let an orphan block other saved entries.
|
||||
const playlist = await bridge.dbGetPlaylist(id);
|
||||
if (!playlist || playlist.type !== 'xtream') continue;
|
||||
const result = await bridge.dbRestoreXtreamFavorites(
|
||||
id,
|
||||
state.favorites,
|
||||
state.favoritesRestoreId
|
||||
);
|
||||
if (!result.success)
|
||||
throw new Error('Recovering saved favorites failed.');
|
||||
try {
|
||||
const playlist = await bridge.dbGetPlaylist(id);
|
||||
if (!playlist || playlist.type !== 'xtream') continue;
|
||||
const result = await bridge.dbRestoreXtreamFavorites(
|
||||
id,
|
||||
state.favorites,
|
||||
state.favoritesRestoreId
|
||||
);
|
||||
if (!result.success)
|
||||
throw new Error('Recovering saved favorites failed.');
|
||||
} catch (error) {
|
||||
if (failureMode === 'throw') throw error;
|
||||
// Keep pending state for retry while ordinary reads show healthy data.
|
||||
console.warn('A pending favorites conversion could not complete.');
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user