mirror of
https://github.com/4gray/iptvnator.git
synced 2026-10-14 21:26:15 -08:00
fix(xtream): validate backup capabilities before favorite writes
This commit is contained in:
1 parent
2a8009fe6c
commit
c0edfef616
5 files changed
+62
-4
No files matched your search
@@ -90,7 +90,10 @@ an online catalog. Existing history, hidden-category and playback-position
|
||||
restore contracts remain separate.
|
||||
|
||||
Both Xtream data-source and portal-activity capabilities require the pending
|
||||
favorite conversion bridge; activity reads also require playlist lookup.
|
||||
favorite conversion bridge; activity reads also require playlist lookup. The
|
||||
SQLite capability also requires the unfiltered favorite backup reader. An
|
||||
unsupported atomic restore performs no favorite writes: a fresh archive stays
|
||||
parked, while an older pending restore is retained without being overwritten.
|
||||
|
||||
Records already deleted by an older build cannot be recovered without a
|
||||
surviving pending state or backup. Opening an Xtream section may initialize
|
||||
|
||||
@@ -413,6 +413,10 @@ describe('PlaylistBackupService export → import round-trip', () => {
|
||||
collaborators.databaseService,
|
||||
'restoreXtreamUserData'
|
||||
);
|
||||
const favorites = jest.spyOn(
|
||||
collaborators.databaseService,
|
||||
'restoreXtreamFavoriteSnapshots'
|
||||
);
|
||||
const positions = jest.spyOn(
|
||||
collaborators.playbackPositionService,
|
||||
'replaceAllPlaybackPositions'
|
||||
@@ -426,6 +430,7 @@ describe('PlaylistBackupService export → import round-trip', () => {
|
||||
expect(failed.failed).toBe(1);
|
||||
expect(visibility).not.toHaveBeenCalled();
|
||||
expect(collections).not.toHaveBeenCalled();
|
||||
expect(favorites).not.toHaveBeenCalled();
|
||||
expect(positions).not.toHaveBeenCalled();
|
||||
expect(pins).not.toHaveBeenCalled();
|
||||
expect(
|
||||
@@ -448,6 +453,37 @@ describe('PlaylistBackupService export → import round-trip', () => {
|
||||
}
|
||||
);
|
||||
|
||||
it('retains an older pending restore without writing favorites when atomic restore is unsupported', async () => {
|
||||
const collaborators = createStatefulBackupCollaborators(seedState());
|
||||
const service = createPlaylistBackupService(collaborators);
|
||||
const archive = await service.exportBackup();
|
||||
collaborators.pendingRestoreService.set('xtream-1', {
|
||||
favorites: [{ contentType: 'movie', xtreamId: 9999 }],
|
||||
hiddenCategories: [],
|
||||
recentlyViewed: [],
|
||||
playbackPositions: [],
|
||||
});
|
||||
const previous =
|
||||
collaborators.pendingRestoreService.getOrThrow('xtream-1');
|
||||
collaborators.playbackPositionService.assertSupportsAtomicReplacement.mockImplementation(
|
||||
() => {
|
||||
throw new Error(
|
||||
'Atomic playback position replacement unavailable'
|
||||
);
|
||||
}
|
||||
);
|
||||
const favorites = jest.spyOn(
|
||||
collaborators.databaseService,
|
||||
'restoreXtreamFavoriteSnapshots'
|
||||
);
|
||||
const result = await service.importBackup(archive.json);
|
||||
expect(result.failed).toBe(1);
|
||||
expect(favorites).not.toHaveBeenCalled();
|
||||
expect(
|
||||
collaborators.pendingRestoreService.getOrThrow('xtream-1')
|
||||
).toEqual(previous);
|
||||
});
|
||||
|
||||
it('retains failed position restores for retry and consumes only after success', async () => {
|
||||
const state = seedState();
|
||||
const collaborators = createStatefulBackupCollaborators(state);
|
||||
|
||||
@@ -940,8 +940,11 @@ export class PlaylistBackupService {
|
||||
|
||||
// Preserve an older unfinished import before parking the incoming backup.
|
||||
// Canonical favorites are additive; legacy-only ones need the same protection.
|
||||
const sqlite = this.runtime.supportsXtreamSqliteDataSource;
|
||||
const previous = this.pendingRestoreService.getOrThrow(playlistId);
|
||||
if (previous && this.runtime.supportsXtreamSqliteDataSource) {
|
||||
if (previous && sqlite) {
|
||||
// An unsupported restore must leave the older pending state intact.
|
||||
this.playbackPositionService.assertSupportsAtomicReplacement();
|
||||
await this.databaseService.restoreXtreamFavoriteSnapshots(
|
||||
playlistId,
|
||||
previous.favorites,
|
||||
@@ -959,14 +962,16 @@ export class PlaylistBackupService {
|
||||
}
|
||||
|
||||
// Browser storage has no catalog cache to wait for: apply at once.
|
||||
const sqlite = this.runtime.supportsXtreamSqliteDataSource;
|
||||
if (sqlite) {
|
||||
// With no older pending state, park the archive before failing preflight.
|
||||
if (!previous) {
|
||||
this.playbackPositionService.assertSupportsAtomicReplacement();
|
||||
}
|
||||
await this.databaseService.restoreXtreamFavoriteSnapshots(
|
||||
playlistId,
|
||||
restoreState.favorites,
|
||||
restoreState.favoritesRestoreId
|
||||
);
|
||||
this.playbackPositionService.assertSupportsAtomicReplacement();
|
||||
if (!(await this.hasCompletedOfflineCache(playlistId))) return;
|
||||
}
|
||||
|
||||
|
||||
@@ -87,6 +87,7 @@ describe('RuntimeCapabilitiesService', () => {
|
||||
dbRemoveRecentItem: jest.fn(),
|
||||
dbRemoveFavorite: jest.fn(),
|
||||
dbGetFavorites: jest.fn(),
|
||||
dbGetFavoritesForBackup: jest.fn(),
|
||||
dbReorderGlobalFavorites: jest.fn(),
|
||||
dbGetRecentItems: jest.fn(),
|
||||
dbAddRecentItem: jest.fn(),
|
||||
@@ -603,6 +604,17 @@ describe('RuntimeCapabilitiesService', () => {
|
||||
expect(service.supportsXtreamSectionNavigation).toBe(true);
|
||||
});
|
||||
|
||||
it('requires the durable favorite backup reader in the Xtream SQLite contract', () => {
|
||||
const bridge = createXtreamSqliteBridge();
|
||||
testWindow.electron = bridge;
|
||||
const service = new RuntimeCapabilitiesService();
|
||||
expect(service.supportsXtreamSqliteDataSource).toBe(true);
|
||||
delete bridge['dbGetFavoritesForBackup'];
|
||||
expect(service.supportsXtreamSqliteDataSource).toBe(false);
|
||||
bridge['dbGetFavoritesForBackup'] = jest.fn();
|
||||
expect(service.supportsXtreamSqliteDataSource).toBe(true);
|
||||
});
|
||||
|
||||
it('keeps Xtream SQLite and ordinary position storage when atomic restore is unavailable', () => {
|
||||
const bridge = createXtreamSqliteBridge();
|
||||
testWindow.electron = bridge;
|
||||
@@ -658,6 +670,7 @@ function createXtreamSqliteBridge(): Record<string, jest.Mock> {
|
||||
dbGetContent: jest.fn(),
|
||||
dbGetContentByXtreamId: jest.fn(),
|
||||
dbGetFavorites: jest.fn(),
|
||||
dbGetFavoritesForBackup: jest.fn(),
|
||||
dbGetPlaylist: jest.fn(),
|
||||
dbGetPlaybackPosition: jest.fn(),
|
||||
dbGetRecentItems: jest.fn(),
|
||||
|
||||
@@ -212,6 +212,7 @@ export class RuntimeCapabilitiesService {
|
||||
'dbSetAppState',
|
||||
'dbSearchContent',
|
||||
'dbGetFavorites',
|
||||
'dbGetFavoritesForBackup',
|
||||
'dbAddFavorite',
|
||||
'dbRemoveFavorite',
|
||||
'dbIsFavorite',
|
||||
|
||||
Reference in new issue
Block a user