diff --git a/libs/services/src/lib/server-storage/server-migration.service.spec.ts b/libs/services/src/lib/server-storage/server-migration.service.spec.ts index daeb261c6..fe0e84a47 100644 --- a/libs/services/src/lib/server-storage/server-migration.service.spec.ts +++ b/libs/services/src/lib/server-storage/server-migration.service.spec.ts @@ -418,4 +418,34 @@ describe('ServerMigrationService', () => { expect.objectContaining({ _id: 's1', isFullStalkerPortal: true }) ); }); + + it("does not take an inherited name for a playlist id's data", async () => { + TestBed.overrideProvider(NgxIndexedDBService, { + useValue: { + getAll: () => + of([{ _id: 'toString', title: 'Odd id', url: 'http://x' }]), + }, + }); + localStorage.setItem( + 'xtream-favorites', + JSON.stringify({ toString: ['live:1'] }) + ); + // The server has the namespace, but no entry for that id. + session.api.getJson.mockResolvedValue({ + entries: { 'xtream-favorites': {} }, + tags: { 'xtream-favorites': {} }, + }); + + const result = await TestBed.inject( + ServerMigrationService + ).moveBrowserData(); + + expect(result.failed).toBe(0); + expect(session.api.sendJson).toHaveBeenCalledWith( + 'PUT', + '/user-state/xtream-favorites/toString', + { value: ['live:1'] }, + { 'If-None-Match': '*' } + ); + }); }); diff --git a/libs/services/src/lib/server-storage/server-migration.service.ts b/libs/services/src/lib/server-storage/server-migration.service.ts index f0f4193ec..f74a2ab76 100644 --- a/libs/services/src/lib/server-storage/server-migration.service.ts +++ b/libs/services/src/lib/server-storage/server-migration.service.ts @@ -16,7 +16,8 @@ import { unionItems, } from './server-migration-merge'; import { planServerMigration } from './server-migration-plan'; -import { preconditionHeaders } from './server-user-data-entries'; +import { withoutDeleted } from './server-user-data-edit'; +import { idMap, preconditionHeaders } from './server-user-data-entries'; import { typeLegacyXtreamRefs, type XtreamUserData, @@ -205,10 +206,14 @@ export class ServerMigrationService { if (targets.size === 0) { return 0; } - const { entries, tags } = + // Maps keyed by playlist id without a prototype: an id such as + // `toString` is not mistaken for an entry. + const { entries, tags } = withoutDeleted( await this.session.api.getJson( '/user-state' - ); + ), + [] + ); // A map this browser has but cannot read fails the move, so it // stays on offer instead of being marked done without it. let unreadable = 0; @@ -369,12 +374,12 @@ function readLegacyMap(key: string): Record | null { return null; } if (value === null) { - return {}; + return idMap(); } try { const parsed: unknown = JSON.parse(value); return parsed && typeof parsed === 'object' && !Array.isArray(parsed) - ? (parsed as Record) + ? idMap(parsed as Record) : null; } catch { return null;