Files
iptvnator/docs/architecture/playlist-backup-restore.md
T
4grayandClaude Opus 5 5e4f2ca3dd docs(stalker): reconcile the Stalker docs after the API-compatibility series (#1375)
Nine PRs landed between 2026-08-01 and 2026-08-04 in parallel worktrees, each
editing its own section of docs/architecture/stalker-portal.md and CLAUDE.md.
Sections that were correct when written disagreed with each other, or with
master, afterwards. Every claim here was verified against the code.

Corrected in stalker-portal.md: routes listed without the /workspace prefix;
"simple portals carry only the mac= cookie" (every request goes through the
shared identity builder — but the direct branch forwards no serial, so no
SN/__cfduid either, while playback headers are NOT mode-gated); a facade
introduced as "three modules" above a list of five; the pre-#1370 "blank
fields are not generated" opening; an ambiguous stalker-identity.utils.ts
citation (two files share the name); two of the three surfaces that apply the
scoped header override; a bare {status: 1} now being a refusal; and the
session-state fields #1354 added to the backup exclusion list (mirrored in
playlist-backup-restore.md).

CLAUDE.md had no entry at all for portal mode / endpoint discovery / lazy
repair — the largest change of the series; added one. Its session-facade list
was missing two modules and status 1 still read as plain "blocked".

Mock server: documented the /stalker, /stream/gated and marketing-poster
routes and the HOST variable; replaced the global POST /reset guidance with
the real per-MAC isolation contract (OWNED_MACS, the sibling 00:1A:79:5F:*
range, mode: 'serial'); added get_main_info; refreshed the project tree; fixed
a broken anchor; and corrected MOCK_PORT, which moves the client side only —
nothing maps it to the server's PORT.

The repo skill's "keep Stalker request rules in Stalker data access" no longer
holds: the wire-format, identity, portal-mode and auth-failure contracts live
in shared/interfaces because the Electron main process cannot import renderer
libs.

Also fixes four stale code comments carrying the same claims, including
"Single choke point for Stalker API calls" — four callers deliberately go
direct, and only fetchViaProfile() wires repair itself.

Docs and comments only; no executable change. No release note (no user-visible
behavior); no-release-note label applied for the libs/** paths.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-06 17:49:49 +02:00

7.4 KiB

Playlist Backup/Restore Architecture

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

Entry Points

  • UI: /Users/4gray/Code/iptvnator/apps/web/src/app/settings/settings-backup-section.component.ts (embedded in settings.component.html), with the file read/handoff in /Users/4gray/Code/iptvnator/apps/web/src/app/settings/settings-backup.facade.ts
  • Backup service: /Users/4gray/Code/iptvnator/libs/services/src/lib/playlist-backup.service.ts
  • Manifest types: /Users/4gray/Code/iptvnator/libs/shared/interfaces/src/lib/playlist-backup.interface.ts
  • Xtream pending restore storage: /Users/4gray/Code/iptvnator/libs/services/src/lib/xtream-pending-restore.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 }
    • playback positions as PlaybackPositionData[]

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.

Clearing the playlist's existing pins goes through a dedicated delete-by-playlist operation, not the keyed clear: that one caps its key list to bound an IN clause, so a playlist with more pinned movies than the cap kept the surplus while still reporting success. A failure now fails the entry rather than leaving the union of old and archived pins.

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 cleared and re-applied from backup state.
  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.

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