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

212 lines
7.4 KiB
Markdown

# 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