mirror of
https://github.com/4gray/iptvnator.git
synced 2026-10-08 17:06:15 -08:00
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>
212 lines
7.4 KiB
Markdown
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
|