fix(backup): export and restore Xtream user state in the PWA (#1905)

* fix(backup): export and restore Xtream user state in the PWA

Without an Electron bridge the backup wrote empty Xtream favorites,
history and positions, and import only parked the state until the
playlist was next opened.

PwaXtreamBackupStateService reads and writes the four PWA maps through
USER_DATA_STORAGE, so self-hosted server storage is covered as well.
Export converts typed keys, resolves legacy ids only through their
snapshot, and fails on an unreadable map instead of writing []. Import
applies at once through applyAndConsume, remapped to the imported
playlist id; a failed write keeps the snapshot parked for the store.
Title snapshots travel in a new optional collectionItems field that
older builds ignore. The storage key names are shared with the portal
data source as XTREAM_USER_DATA_KEYS.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(backup): write PWA title snapshots before the Xtream lists

A browser restore that failed after its first list write kept the
pending state parked for the Xtream store, but that replay carries no
title snapshots, so they were lost. Merge them first; a damaged cache
is replaced, while a storage that cannot be read still fails the entry.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(backup): keep the artwork of PWA recently viewed items

The browser build saves a backdrop with each view and shows it ahead of
the title snapshot's, but the export dropped it, so a restore showed
older artwork. Carry it as an optional backdropUrl on the archive's
recently viewed items; the desktop build ignores it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(xtream): keep view artwork in the PWA pending restore state

A failed browser restore is replayed by PwaXtreamDataSource.restoreUserData,
which rewrote recently viewed items without their backdropUrl; a refresh
dropped it the same way in clearPlaylistContent. Both now carry it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(backup): restore PWA Xtream lists before the snapshot cache

Writing the optional title snapshots first could take the storage space
the favorites, history or positions needed, failing a restore that fits
without them. The lists now go first, as the snapshot-storage contract
asks; after a failed list write the snapshots are still merged for the
store's replay, unless storage is full.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(backup): pick the Xtream backup path by the SQLite data source

The backup chose its Xtream path by the mere presence of window.electron,
while XTREAM_DATA_SOURCE falls back to the browser storage whenever the
bridge lacks the Xtream SQLite API. On such a partial or stale preload the
export read the wrong store and the restore took the SQLite path. Both now
follow RuntimeCapabilitiesService.supportsXtreamSqliteDataSource.

Also casts the test helper's prototype instance through unknown, which the
spec typecheck requires.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(xtream): restore PWA playback positions under the target playlist

A PWA restore that is replayed by the Xtream store kept each position's
exported playlistId. When the backup was imported under another id, the
VOD details page rejected those positions as belonging to a different
playlist. restoreUserData now stamps the playlist it restores into.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
4grayandClaude Opus 5.5 authored and GitHub committed 2026-10-13 09:07:49 +02:00
1 parent ebb1ab3467
commit 8bf92f9d65
13 files changed
+1223 -81

No files matched your search

+47 -1
View File
@@ -12,6 +12,8 @@ settings screen.
- Manifest types: `libs/shared/interfaces/src/lib/playlist-backup.interface.ts`
- Xtream pending restore storage:
`libs/services/src/lib/xtream-pending-restore.service.ts`
- Browser (PWA) Xtream user state:
`libs/services/src/lib/pwa-xtream-backup-state.service.ts`
## Manifest Contract
@@ -61,9 +63,12 @@ Xtream backups export only connection metadata plus portable user state.
- User state:
- hidden categories by `{ categoryType, xtreamId }`
- favorites by `{ contentType, xtreamId, addedAt?, position? }`
- recently viewed by `{ contentType, xtreamId, viewedAt }`
- recently viewed by `{ contentType, xtreamId, viewedAt, backdropUrl? }`
(`backdropUrl` is written and read by the browser build only)
- playback positions as `PlaybackPositionData[]`
- optional VOD source pins by `{ matchKey, contentId, updatedAt? }`
- optional title snapshots `collectionItems` (browser build only; see
[Browser runtime](#browser-runtime))
An imported Xtream archive can wait for its catalog to download. Export reads
that pending snapshot before consulting the catalog: its hidden categories,
@@ -225,6 +230,47 @@ categories/favorites/history can
remain after a later failure, but the snapshot remains available to retry the
complete restore. It is consumed only after every restore step succeeds.
### Browser Runtime
Whenever `XTREAM_DATA_SOURCE` resolves to `PwaXtreamDataSource` — the
browser, or a desktop bridge without the Xtream SQLite API
(`RuntimeCapabilitiesService.supportsXtreamSqliteDataSource`) — Xtream
favorites, recently viewed items, playback positions and title snapshots live
in the `USER_DATA_STORAGE` maps owned by `PwaXtreamDataSource`
(`XTREAM_USER_DATA_KEYS`; see
[PWA portal user data](./pwa-self-hosted.md#pwa-portal-user-data)).
`PwaXtreamBackupStateService` reads and writes them through that token, so the
self-hosted server storage is covered too.
- Export converts each playlist's entries to the archive shape. Typed keys
(`movie:42`) carry their identity; a legacy bare id resolves only through
the snapshot stored under it, and is left out when none identifies it.
Recently viewed keeps the newest view per title, with the artwork saved
for that view as `backdropUrl`. `collectionItems` carries
the snapshots of exported favorites and history, and is omitted when there
are none. Hidden categories stay `[]` and `sourcePins` stays absent: the
browser tracks neither.
- An unreadable map (storage error, malformed JSON, a playlist entry that is
not a list) fails the export instead of being written as an empty list.
- A pending restore snapshot still wins over the stored maps, as on desktop.
- Import parks the restore state like the desktop does, then applies it at
once through `XtreamPendingRestoreService.applyAndConsume`: the browser has
no catalog cache to wait for. Favorites, history and positions of the
imported playlist id are replaced (positions get that id); other playlists'
entries are untouched. `collectionItems` merge into the playlist's
snapshots, keyed by typed identity; an archive without them leaves the
snapshots alone, malformed items are skipped, a damaged cache is replaced,
and a quota error on this cache does not fail the restore.
- The lists are written before the optional title snapshots, so the cache
never takes storage space they need (as for additions; see the
[snapshot-storage contract](./xtream-portal-compatibility.md)). A failed
write fails the entry and keeps the snapshot parked, so the Xtream store
replays the complete state when the playlist next opens. That replay
carries no title snapshots, so they are still merged after a failed list
write, unless storage is full and they would take room the replay needs.
Older builds ignore `collectionItems`; the manifest version stays `1`.
## Current UX
The settings page now exports/imports “playlist backups” instead of the old
+5 -1
View File
@@ -272,7 +272,11 @@ and sidecar state in localStorage:
`PwaXtreamDataSource` and `PwaCollectionStorage` reach these keys through the
`USER_DATA_STORAGE` token from `@iptvnator/services` (default:
`localStorage`), never through `localStorage` directly, so a deployment can
substitute a server-synced storage.
substitute a server-synced storage. The key names are shared as
`XTREAM_USER_DATA_KEYS` with the playlist backup, which exports and restores
the favorites, recent items, positions and snapshots through the same token
(`PwaXtreamBackupStateService`; see
[backup/restore](./playlist-backup-restore.md#browser-runtime)).
Entries should include a content snapshot when the item is added. Global
collection routes and the dashboard can then restore titles, posters, content