Files
iptvnator/docs/architecture/playlist-backup-restore.md
T
4grayandClaude Opus 5.5 8bf92f9d65 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>
2026-10-13 09:07:49 +02:00

12 KiB

Playlist Backup/Restore Architecture

This document describes the versioned playlist backup/restore flow used by the settings screen.

Entry Points

  • UI: apps/web/src/app/settings/settings-backup-section.component.ts (embedded in settings.component.html), with the file read/handoff in apps/web/src/app/settings/settings-backup.facade.ts
  • Backup service: libs/services/src/lib/playlist-backup.service.ts
  • 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

Backups are versioned JSON manifests, not raw Playlist[] dumps and not SQLite database snapshots.

Top-level shape:

  • kind: "iptvnator-playlist-backup"
  • version: 1
  • exportedAt
  • includeSecrets
  • settings?.epgUrls
  • playlists[]

The manifest is portable across machines because it stores playlist definitions and portable user state, while excluding cache-only database content.

Export Scope

M3U

M3U backups are self-contained.

  • Always export canonical rawM3u from PlaylistsService.getRawPlaylistById()
  • Preserve source metadata when available:
    • original source kind: url, file, or text
    • original URL
    • userAgent, referrer, origin
    • filePathHint for provenance only
  • Export playlist-scoped user state:
    • favorites by channel URL
    • recently viewed M3U items
    • hidden group titles

The embedded raw text is the canonical restore artifact. The internal parsed playlist object graph is not the backup format.

Xtream

Xtream backups export only connection metadata plus portable user state.

  • Connection metadata:
    • serverUrl
    • username
    • password
  • User state:
    • hidden categories by { categoryType, xtreamId }
    • favorites by { contentType, xtreamId, addedAt?, position? }
    • 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)

An imported Xtream archive can wait for its catalog to download. Export reads that pending snapshot before consulting the catalog: its hidden categories, favorites, history, and positions are authoritative, including empty lists. Pending pins are authoritative when present; an absent sourcePins field keeps the existing store's pins when that store is available, or stays absent when it is unavailable. Re-export neither applies nor consumes the pending snapshot, so it also works before the portal has been opened and in browser runtimes. Parental category locks still come from the current lock store.

A failed pending-storage or playback-position read fails export. An unreadable collection must never be serialized as a successful empty collection.

Explicitly excluded:

  • cached categories/content rows
  • import-status flags and other app-state cache markers
  • downloads

Stalker

Stalker backups export connection metadata plus playlist-scoped favorites/recent state.

  • Exported connection fields:
    • portalUrl
    • macAddress
    • isFullStalkerPortal
    • username
    • password
    • userAgent
    • referrer
    • origin
    • serial/device/signature fields when present
  • Exported user state:
    • favorites snapshots
    • recently viewed snapshots

Explicitly excluded — session state, as opposed to the connection definition:

  • stalkerToken
  • stalkerSessionIdentity (the fingerprint the token was negotiated for)
  • stalkerWatchdogTimeout / stalkerTimeslot (the profile-advertised cadence)
  • stalkerAccountInfo
  • playback positions in v1

App Settings

Only EPG source URLs are backed up at the app-settings level.

  • Exported: settings.epgUrls
  • Excluded: cached EPG database content

Import Flow

The settings backup facade (settings-backup.facade.ts, driven by settings-backup-section.component.ts) reads the file (file.text()) and hands its contents to PlaylistBackupService.importBackup().

The service:

  1. Validates the manifest kind/version before any writes.
  2. Rejects legacy raw Playlist[] JSON blobs.
  3. Builds stable source fingerprints for merge-vs-create decisions.
  4. Upserts playlists into app playlist storage.
  5. Restores provider-specific user state.

Fingerprint rules:

  • M3U URL playlists: normalized URL
  • M3U without URL: hash of canonical rawM3u
  • Xtream: normalized serverUrl + username
  • Stalker: normalized portalUrl + macAddress

If a fingerprint matches an existing playlist:

  • keep the existing playlist ID
  • update mutable metadata from the backup
  • replace playlist-scoped state with the backup payload

If no fingerprint matches:

  • create a new playlist
  • reuse exportedId only when it is unused
  • otherwise generate a new UUID

Xtream Restore Contract

Xtream restore is type-aware end to end. The app no longer stores plain xtream_id[] arrays for refresh/import restore because IDs can collide across live, movie, and series.

Runtime contract:

  • shared shape: XtreamPendingRestoreState
  • persisted in local storage by playlist ID
  • consumed by:
    • Xtream refresh actions
    • settings backup import
    • Xtream content initialization

Restore state originates from untrusted sources (user-supplied backup files, stale localStorage entries), so every read and write goes through normalizeXtreamPendingRestoreState (libs/shared/interfaces). Entries in hiddenCategories, favorites, and recentlyViewed without a usable numeric xtreamId are dropped rather than restored: backups exported by builds affected by issue #1017 contain ID-less hidden-category entries, and matching them against category rows would otherwise degrade to a type-only comparison that hides every category of that type. Category rows themselves cross the DB worker IPC boundary in the snake_case wire shape declared by XCategoryFromDb/XtreamCategoryFromDb; the category operations project their Drizzle rows explicitly to keep that contract true.

Replacing the playlist's pins deletes by playlist and writes the replacements in one transaction. It does not use the capped keyed-clear operation, which could leave surplus pins behind. Any replacement failure fails the entry and retains the pending snapshot for retry.

sourcePins (VOD multi-source) is the one optional collection, and the normalizer preserves that: an absent field stays absent rather than becoming [], because restore treats a PRESENT collection as authoritative and clears the playlist's existing pins before applying it. Materializing an empty array would turn "this archive predates pins" into "this archive says there are none": archives written before multi-source existed simply do not have it, so its absence is age rather than damage and only a wrong type is rejected. A pin is carried under the playlist it points AT — exporting it anywhere else would restore a preference for a portal the archive never contained. Its matchKey identifies the film rather than the portal, so it survives untouched; only the playlist id is remapped to the imported copy. Pins whose match key or content id is unusable are dropped, since writing one would occupy the unique key of a film it does not describe.

Electron restore behavior:

  1. Category import reads pending hidden-category state while saving categories.
  2. After content import, favorites/recent state is restored by typed { contentType, xtreamId } matching.
  3. Playback positions are replaced for the destination playlist in one SQLite transaction through DB_REPLACE_ALL_PLAYBACK_POSITIONS. Empty means clear; a failed insert rolls back the deletion and all earlier inserts.
  4. VOD source pins are re-applied against the IMPORTED playlist id.

For existing Xtream playlists with a fully populated offline cache, backup import applies the restore immediately. Otherwise the typed restore payload is left pending until the next Xtream initialization/import. Both paths use strict playback-position replacement: unavailable storage, rejected IPC, or an unsuccessful response fail the restore and leave the pending snapshot intact. Atomic restore support is checked separately from the ordinary Xtream SQLite capability gate. An older preload missing the replacement method keeps using SQLite for playback and backup export. Import parks the snapshot first, then rejects unsupported atomic restore before checking catalog completeness or writing category visibility, collections, positions, or pins. Deferred restore performs the same preflight before applying user state. The retained snapshot can be retried once atomic replacement becomes available. Ordinary playback keeps its existing capability checks and best-effort persistence API. The whole restore is not one database transaction; already applied 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). 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). 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 raw JSON application dump.

  • Export filename: iptvnator-playlist-backup-YYYY-MM-DD.json
  • Import summary reports:
    • imported
    • merged
    • skipped
    • failed