mirror of
https://github.com/4gray/iptvnator.git
synced 2026-10-08 09:01:03 -08:00
* docs(agents): compact root guidance and preserve task-specific knowledge * fix(agents): parse guidance navigation with Markdown tokens * fix(agents): validate generic literal repository paths * fix(agents): distinguish code symbols and shortcut images * fix(agents): recognize SCSS filename literals * fix(agents): handle fenced imports and encoded paths * fix(agents): parse prose and rendered HTML anchors * fix(agents): validate rendered HTML navigation * fix(agents): use GitHub-compatible heading slugs * fix(agents): require standalone top-level Claude import * fix(agents): exclude HTML-contained guidance imports * fix(agents): handle image fragments and quoted imports * fix(agents): validate visible HTML and image source sets * fix(agents): recognize package scopes and route source work * fix(agents): parse JSONC and constrain package exemptions * fix(agents): decode link entities and allow package subpaths * fix(agents): route source work and check extensionless files * fix(agents): support package versions and source fragments * fix(agents): accept qualified package prose * fix(agents): retain rendered context for Markdown references * fix(agents): validate visible headings and spaced paths * fix(agents): validate media and hyphenated literal paths * fix(agents): decode full HTML entities and media assets * fix(agents): recognize possessive package mentions * fix(agents): validate extensionless imports and version comparators * fix(agents): retain visible backticks and explicit path punctuation * fix(agents): validate image-map navigation targets * fix(agents): count all Markdown line endings in budgets * fix(agents): delimit package prose at Unicode punctuation * fix(agents): normalize punctuation for extensionless imports * fix(agents): preserve filenames across prose punctuation * fix(agents): validate iframe document references * fix(agents): inspect document suffix before URL fragments * fix(agents): unify Markdown suffix and encoded import guards * fix(agents): handle wildcard versions and alternate documents * fix(agents): validate document formats and trim HTML URLs * fix(agents): cover document families and guidance basenames * fix(agents): require files for media references * fix(agents): preserve block boundaries and validate embeds * fix(agents): normalize internal HTML URL whitespace * fix(agents): reject empty media and ignore URL at-signs * fix(agents): validate srcdoc references and empty srcset * fix(agents): honor HTML bases and preserve adjacent imports * fix(agents): convert base file URLs to native paths * fix(agents): preserve imports after bare URL punctuation * fix(agents): exclude opaque URI prose from import scans * fix(agents): keep import tokens outside URI scheme matches * fix(agents): restrict opaque URI exemptions to parsed links * fix(agents): handle opening prose delimiters * fix(agents): scan nested imports and share document suffixes * fix(agents): reject pathless media and direct file URLs * fix(agents): reject file bases and preserve quoted URL boundaries * fix(agents): distinguish URL quotes and cover guidance variants * fix(agents): validate SVG images and conventional guides * fix(agents): handle declared package names handles and SVG use * fix(agents): normalize closing punctuation on federated handles * fix(agents): normalize Unicode punctuation on handles * fix(agents): normalize possessive federated handles * fix(agents): separate parenthetical prose from handles * fix(agents): exclude www autolinks from import scanning * ci: allow manual CodeQL validation of PR branches * fix(agents): reject nonportable Windows drive links
404 lines
24 KiB
Markdown
404 lines
24 KiB
Markdown
# Xtream Portal Compatibility
|
||
|
||
This document captures the Xtream Codes compatibility rules shared by the
|
||
Electron and PWA paths.
|
||
|
||
## Runtime Selection And Ownership
|
||
|
||
`provideXtreamDataSource()` selects `ElectronXtreamDataSource` only when
|
||
`RuntimeCapabilitiesService.supportsXtreamSqliteDataSource` proves that the
|
||
complete SQLite-backed Xtream bridge is available. Otherwise it selects
|
||
`PwaXtreamDataSource`. A generic Electron or `window.electron` check is not the
|
||
data-source capability contract. Older favorites/recent branches that still
|
||
probe `window.electron` directly are migration debt, not an alternate runtime
|
||
selection rule; changes in those paths should follow the selected data source
|
||
and explicit capabilities.
|
||
|
||
Ownership follows the workspace boundaries:
|
||
|
||
- routed screens and screen-session orchestration:
|
||
`libs/portal/xtream/feature`
|
||
- Xtream API, cache, Signal Store, and data sources:
|
||
`libs/portal/xtream/data-access`
|
||
- provider-neutral collection services and reusable multi-source
|
||
discovery/resolution: `libs/portal/shared/data-access`
|
||
- reusable presentation: `libs/portal/shared/ui`
|
||
- pure provider-neutral contracts/helpers: `libs/portal/shared/util`
|
||
|
||
Persisted Xtream identity is playlist-scoped and content-type-aware:
|
||
`playlist_id + content.type + xtream_id`. Mixed collection keys likewise
|
||
include type plus provider ID because live, movie, and series IDs can collide.
|
||
Do not confuse the normalized SQLite row ID with provider `xtream_id`,
|
||
`stream_id`, or `series_id`, especially when recovering a hidden provider
|
||
category for detail playback.
|
||
|
||
See [Nx Workspace Boundaries](./nx-workspace-boundaries.md),
|
||
[SQLite DB Worker](./sqlite-db-worker.md),
|
||
[Portal Detail Navigation](./portal-detail-navigation.md), and
|
||
[VOD Multi-Source](./vod-multi-source.md).
|
||
|
||
## Connection Input
|
||
|
||
Xtream server URLs are normalized through
|
||
`normalizeXtreamServerUrl` from `@iptvnator/shared/interfaces`.
|
||
|
||
Rules:
|
||
|
||
1. Only `http` and `https` URLs are accepted.
|
||
2. URL username/password credentials are rejected.
|
||
3. Leading and trailing whitespace is ignored.
|
||
4. Trailing slashes are removed.
|
||
5. Full API or playlist URLs ending in `/player_api.php`, `/panel_api.php`, or `/get.php` are
|
||
reduced to the portal base URL.
|
||
6. Provider subpaths are preserved. For example,
|
||
`https://example.test/panel/player_api.php?...` becomes
|
||
`https://example.test/panel`.
|
||
|
||
The Xtream import form may extract `username` and `password` from full
|
||
`get.php` or `player_api.php` URLs, but stored playlist metadata should keep
|
||
the normalized `serverUrl` plus trimmed credentials.
|
||
|
||
### Explicit protocol discovery
|
||
|
||
Add and Edit source share `XtreamConnectionTestService` and form-owned
|
||
`createXtreamConnectionTestState` (`@iptvnator/services`). The explicit
|
||
**Test HTTPS and HTTP** button has a visible, accessible pre-request notice
|
||
that credentials may be sent over unencrypted HTTP. Clicking that action supplies
|
||
`allowHttpFallback`; the service defaults it to false, so an ordinary programmatic
|
||
test cannot authorize plaintext credentials. No modal or persistent opt-in is
|
||
needed. The test first probes the entered base, including the existing account-action variants.
|
||
Only an initial `ECONNREFUSED` or TLS wrong-version failure permits one HTTP
|
||
candidate on the same hostname/path. Default HTTPS port 443 becomes HTTP 80;
|
||
nonstandard explicit ports are preserved, never scanned. DNS, timeout, reset,
|
||
certificate, HTTP authorization, and redirected-destination failures cannot
|
||
trigger a downgrade. Aggregate and nested cause errors require positive
|
||
evidence from every address; mixed failures, cycles and truncated error trees
|
||
fail closed. TLS verification diagnostics include incomplete certificate chains.
|
||
A response from an earlier account-action variant also
|
||
prevents downgrade. HTTP failures (including panels returning 500 for unsupported actions)
|
||
still try the remaining account actions on the same candidate.
|
||
|
||
An active account on HTTP replaces only the form's `serverUrl`, with localized
|
||
copy explaining that HTTP is unencrypted. Add/Save persists through the existing
|
||
metadata path; Test never writes storage. Edits, reset, destruction and newer
|
||
tests invalidate pending results and prevent a stale fallback request. Add/Save
|
||
is disabled while that form's test is running. Empty or whitespace-only
|
||
credentials produce a localized validation message without a network request. Passive status checks, startup,
|
||
refresh and playback never perform protocol discovery.
|
||
Every completed current test refreshes `PortalStatusService` for the exact
|
||
connection: account responses publish status and expiration; terminal failures
|
||
publish unavailable with no expiration, without another network request.
|
||
Older passive checks cannot overwrite this explicit evidence; stale form
|
||
results do not publish it.
|
||
|
||
Both transports return an optional, credential-free `connectionFailure` envelope
|
||
only for `connectionTest` requests. Electron returns it rather than throwing
|
||
through IPC (which loses custom error fields); the PWA proxy strips the control
|
||
parameter before contacting the provider and preserves validated redirect-chain
|
||
evidence. PWA URL/DNS-policy refusals and Electron URL/redirect-policy errors are local
|
||
connection failures, never reported as provider HTTP statuses or used to
|
||
authorize HTTP. Older backends without the envelope cannot authorize HTTP discovery.
|
||
Provider JSON remains nested in `payload` and cannot provide this evidence.
|
||
|
||
The saved base drives catalog refresh, provider EPG, live/VOD/series/catch-up URL
|
||
construction and fresh Favorites/Recent resolution. The routed Xtream session
|
||
observes metadata connection changes and bootstraps the new connection. Separate
|
||
XMLTV source URLs, provider-supplied absolute URLs and already-issued download
|
||
or playback sessions retain their own URLs; they are not rewritten. No database
|
||
schema change or migration is required.
|
||
|
||
## Account Status
|
||
|
||
Account status handling uses `resolveXtreamPortalStatus`.
|
||
|
||
Compatibility rules:
|
||
|
||
1. Status text is case-insensitive, so `Active`, `active`, and `ACTIVE` are
|
||
treated the same.
|
||
2. `auth` values `1`, `'1'`, and `true` can mark a response as active when no
|
||
status text is present.
|
||
3. `auth` values `0`, `'0'`, and `false` mark the response inactive.
|
||
4. `exp_date` values `0`, negative numbers, missing values, or invalid values
|
||
are treated as no expiry.
|
||
5. A past positive `exp_date` marks the account expired even when status is
|
||
active.
|
||
|
||
Status probes try account-info-compatible Xtream variants in this order:
|
||
|
||
1. `action=get_account_info`
|
||
2. no `action`
|
||
3. `action=get_profile`
|
||
|
||
This fallback exists because real panels differ even when they advertise
|
||
Xtream Codes compatibility.
|
||
|
||
## Request Construction
|
||
|
||
Electron IPC and the PWA backend both construct API requests by appending
|
||
`/player_api.php` to the normalized portal base URL. They must not append
|
||
`player_api.php` to an already full `player_api.php` or `get.php` URL.
|
||
|
||
Credentials sent to the API are trimmed before serialization.
|
||
|
||
The PWA backend only proxies Xtream requests through registered provider
|
||
targets. Those targets are validated when registered and revalidated before the
|
||
`/xtream` proxy request, including protocol, URL credentials, DNS resolution,
|
||
and private-network checks.
|
||
|
||
## User-Agent
|
||
|
||
Electron's `XTREAM_REQUEST` and stream-probe handlers plus fresh Xtream movie
|
||
and series-episode download requests share the exported
|
||
`XTREAM_CLIENT_USER_AGENT` fallback. A playlist's explicit User-Agent,
|
||
Referer, and Origin are propagated to either download kind; the explicit
|
||
User-Agent still wins over the fallback. Legacy
|
||
download rows without a stored User-Agent receive the fallback when retrying,
|
||
resuming, or recovering a missing completed file. Download rows intentionally
|
||
survive individual source deletion; when the playlist row is already gone and
|
||
its type can no longer be recovered, a headerless legacy download receives the
|
||
same IPTV-player fallback, while a still-identifiable Stalker row remains
|
||
unchanged. Some Xtream
|
||
panels sit behind a WAF (e.g. Cloudflare) configured to challenge
|
||
generic/incomplete browser-looking User-Agents while allowlisting known IPTV
|
||
player clients; a player-style User-Agent (currently a VLC signature) avoids
|
||
that challenge page, whereas a browser-looking but non-browser TLS/HTTP client
|
||
(axios/curl with a Chrome or empty User-Agent) can be blocked even though a
|
||
real browser or a VLC-style client passes. Keep all three request sites using
|
||
the shared constant instead of inlining the string again.
|
||
|
||
## Playback URL Formats
|
||
|
||
When account info includes `user_info.allowed_output_formats`, the current
|
||
Xtream playlist keeps those formats for the active session. The default
|
||
application format is `auto`: live stream URL construction chooses `m3u8` when
|
||
the provider allows HLS, falls back to `ts` when MPEG-TS is the only known
|
||
standard format, and otherwise uses the first provider-advertised format. If
|
||
the provider does not advertise output formats, `auto` falls back to `m3u8`.
|
||
Manual `ts` and `m3u8` settings remain supported; when a manual setting is not
|
||
allowed by the portal, URL construction falls back to the first
|
||
provider-allowed format.
|
||
|
||
### Initial Auto HLS failure
|
||
|
||
The routed Xtream Live TV host attaches `ResolvedPortalPlayback.liveAutoTsUrl`
|
||
only when the current user preference is Auto and the current account explicitly
|
||
advertises both HLS and TS. `XtreamUrlService.constructAutoLiveTsUrl` uses the
|
||
same credential encoding and provider-subpath URL builder as the initial URL;
|
||
no URL string replacement, manifest preflight, stream download, or API probe is
|
||
introduced. Unknown/empty formats and manual HLS/TS have no alternative.
|
||
|
||
`WebPlayerLiveAutoFormat` may consume that alternative once per mounted logical
|
||
live session, in the same selected web player, after an owned terminal network
|
||
diagnostic with HTTP 4xx/5xx, before the native video element reports `playing`.
|
||
A successful manifest is not playback. HLS key/media/unknown-stage failures,
|
||
DRM payloads, content-info VOD/episodes, and catch-up are excluded. Generic
|
||
network failures, status zero, cancellation, timeouts without an HTTP response,
|
||
and decode errors keep the normal explicit recovery surface.
|
||
|
||
The old application is first removed from the render tree. The next render
|
||
callback starts TS only after the web engine's synchronous loader teardown and
|
||
only while source, logical session and user intent still match. The TS payload
|
||
keeps the original headers, title and playback metadata; Electron applies them
|
||
through its existing scoped header handoff. HTTP-media requests retain the
|
||
existing transport/trust rules and do not feed new evidence or admission into
|
||
the separate portal-API host-connectivity guard. Recovery recommendations still
|
||
require a user action and never auto-switch engines. No settings or playlist
|
||
cache is changed. A TS failure displays normal recovery actions; Retry repeats
|
||
TS without rearming the automatic attempt. Changing the actual logical channel
|
||
or playlist, or mounting a new host, creates a new session; same-session metadata
|
||
or provider-source refresh does not rearm it. Same-channel replay and metadata
|
||
refresh preserve the chosen TS while the candidate URLs, headers and mode stay
|
||
the same; an actual transport/provider change discards that selection. This deliberately avoids a sticky
|
||
playlist preference and reevaluates advertised formats on the next channel.
|
||
|
||
| Player/path | Initial Auto failure support |
|
||
| ---------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||
| HTML5 / ArtPlayer, hls.js | One advertised TS attempt after terminal HLS manifest/level/segment HTTP failure; waits for the engine's own bounded retries |
|
||
| Video.js / VHS | Only an observable terminal HTTP diagnostic can trigger an attempt. A sole HLS rendition's failing segment can remain in VHS retry/exclusion cycles without such a diagnostic; use manual TS for that case |
|
||
| External MPV / VLC | Manual TS only. Process launch/exit and best-effort playing telemetry do not reliably identify the initial HLS media HTTP failure |
|
||
| Embedded MPV | Manual TS only; no equivalent structured HTTP diagnostic |
|
||
| Unified Favorites/Recent live resolution | Manual TS; it does not own fresh session account-format evidence |
|
||
|
||
For manual recovery, stop the current player, choose **Settings > Playback >
|
||
Stream Format > ts**, save, and reopen the channel. This keeps the chosen player
|
||
and avoids a second uncorrelated external process. The settings description
|
||
explains this route. This is partial playback coverage of #1513, not a claim
|
||
that all external-player and VHS scenarios automatically recover.
|
||
|
||
Synthetic regression media lives in the mock's `live-fallback:live-fallback`
|
||
account: HLS manifest 200 plus segment 403, manifest 403, a playable local
|
||
H.264/AAC TS alternative, a failing TS alternative, and a delayed HLS segment.
|
||
Tests assert actual video progress and the number of concrete TS requests.
|
||
|
||
If stored Xtream playback credentials contain an invalid server URL or blank
|
||
username/password, stream URL construction returns an empty URL instead of
|
||
throwing during playback.
|
||
|
||
VOD playback uses the standard
|
||
`/movie/{username}/{password}/{streamId}.{containerExtension}` URL after
|
||
resolving the stream id and container extension as one source. Fields from
|
||
`movie_data` take priority, with the top-level catalog fields used as a
|
||
fallback when extended metadata is absent or incomplete. A source requires a
|
||
positive integer id and a non-empty container extension; otherwise URL
|
||
construction returns an empty string.
|
||
|
||
The Electron catalog cache does not persist the container extension. If the
|
||
merged detail response and cached row still cannot resolve a source, the detail
|
||
loader requests the exact VOD from its provider category and merges that raw
|
||
catalog row in memory. An Electron route category uses the SQLite row id, so
|
||
the loader maps it through the complete persisted category set, including
|
||
hidden categories, before sending the provider `category_id`. Cross-portal
|
||
Similar links already carry that provider id, so recovery accepts either the
|
||
SQLite `id` or `xtream_id` representation while preserving local-id lookup
|
||
precedence. If the numeric representations collide, candidate provider ids are
|
||
deduplicated and tried in that order until the exact VOD is found. PWA falls
|
||
back to its API-backed visible categories, and an unresolved database id is
|
||
never sent as though it were a provider id. This recovery request is skipped
|
||
when the detail response or owner-valid cached catalog fields are already
|
||
sufficient. Recovery is best-effort and never gates the detail page: the initial
|
||
sparse selection is published and the loading shell ends before the category
|
||
lookup completes, then a successful result upgrades that same fallback
|
||
reactively with playback actions. Detail, recovered, and cached
|
||
playback fields remain separate candidates: the first complete pair wins, so
|
||
two incomplete rows can never synthesize a source. A failed lookup leaves the
|
||
already-rendered item safely unplayable. Detail requests are generation- and
|
||
playlist-guarded, and detail teardown invalidates the active generation, so a
|
||
late response cannot replace a newer selection or repopulate a closed detail.
|
||
In-memory VOD category and stream arrays record the playlist that populated
|
||
them. Cross-portal Favorites/Recent details ignore arrays owned by another
|
||
playlist, so colliding Xtream ids cannot suppress recovery or contribute a
|
||
foreign playback extension, title, poster, category, or recommendation.
|
||
|
||
Metadata availability and VOD playability are independent. An empty or sparse
|
||
`get_vod_info` response keeps the curated fallback detail page, but that page
|
||
offers the same Play/Resume, Favorite, and Download actions when the source
|
||
resolves. An unresolved source exposes no actions. Playback and download
|
||
descriptors also fall back to the catalog title and poster after `info` and
|
||
`movie_data`.
|
||
|
||
## Catch-Up Playback URLs
|
||
|
||
Xtream-compatible portals differ on archive playback URL shape. IPTVnator
|
||
supports these catch-up variants:
|
||
|
||
1. REST-style `/timeshift/{username}/{password}/{duration}/{start}/{streamId}.ts`
|
||
and `.m3u8`.
|
||
2. Legacy `/streaming/timeshift.php?username=...&password=...&stream=...&start=...&duration=...`
|
||
with optional `extension=ts` or `extension=m3u8`.
|
||
|
||
Electron probes concrete catch-up variants before caching a playlist-level
|
||
choice. The cache key includes the playlist id and the normalized
|
||
`allowed_output_formats` advertised by the provider, so a catch-up variant
|
||
detected before account capabilities are known cannot force stale MPEG-TS URLs
|
||
after the portal later reports HLS-only playback. The probe uses a short range
|
||
`GET`, follows only validated redirects, and accepts only `200` or `206` as
|
||
playable. MPEG-TS is preferred before HLS when the provider allows it because
|
||
some portals return a valid HLS manifest while the first media segment fails in
|
||
Chromium/video.js. PWA fallback keeps the REST MPEG-TS URL when no Electron
|
||
probe API is available.
|
||
|
||
Catch-up is offered from the Xtream Live TV tab and from the unified
|
||
collection surfaces (per-playlist and global Favorites and Recent). The
|
||
`tv_archive` / `tv_archive_duration` columns are carried through the
|
||
favorites and recently-viewed DB projections and mapped onto
|
||
`UnifiedCollectionItem.tvArchive` / `tvArchiveDuration` so the shared live
|
||
tab can gate the timeline's archive window. `tv_archive_duration` is
|
||
interpreted as **days** everywhere, matching
|
||
`live-stream-layout.controlledArchiveDays` (issue #1138).
|
||
|
||
Programme details in the Live TV and Favorites/Recent EPG timeline/list also
|
||
offer **Copy archive URL**. This uses the same resolver and persisted server
|
||
timezone without changing playback. See the M3U module's "Copy archive URL"
|
||
contract for clipboard feedback and credential/header handling.
|
||
|
||
### Start time is the panel's clock, not the viewer's
|
||
|
||
The `{start}` segment (`Y-m-d:H-M`) is read by the panel with `strtotime()`
|
||
in ITS OWN timezone — the one it reports as `server_info.timezone` in the
|
||
account-info response — never the viewer's local clock (issue #1562). The
|
||
timezone is learned by `withPortal.checkPortalStatus()` and normalized by
|
||
`resolveXtreamServerTimezone()` (`libs/shared/interfaces/src/lib/xtream-server-timezone.util.ts`):
|
||
|
||
- a timezone name the runtime's ICU resolves (`Europe/London`) is kept as is;
|
||
- otherwise (`UTC+3`, a typo, an unknown alias) the offset is derived from
|
||
the clock pair the same response carries — `time_now` read as a naive UTC
|
||
wall clock minus `timestamp_now`, snapped to 15 minutes — and stored as
|
||
`UTC±HH:MM`. This is a snapshot without DST rules: for such a panel,
|
||
programmes on the far side of a DST switch are off by an hour until the
|
||
next account-info check refreshes the offset. Xtream Codes reports PHP
|
||
timezone identifiers (IANA names), so the snapshot only serves
|
||
non-standard servers, where the alternative was the viewer's clock;
|
||
- with neither, nothing is stored and the URL falls back to the viewer's
|
||
clock, the only remaining guess.
|
||
|
||
The value is persisted on the playlist row (`Playlist.serverTimezone`)
|
||
because the two catch-up entry points read different sources: the Live TV
|
||
layout uses the store's `currentPlaylist`, while the Favorites / Recent
|
||
resolver (`StreamResolverService.resolveXtreamCatchupUrl`) reads the stored
|
||
row through `dbGetAppPlaylist` / IndexedDB. The write goes through
|
||
`IXtreamDataSource.rememberServerTimezone` and is atomic against the row's
|
||
CURRENT connection in both runtimes — Electron: one conditional UPDATE
|
||
(`DB_SET_PLAYLIST_SERVER_TIMEZONE` → `setPlaylistServerTimezone`,
|
||
`json_set(payload, '$.serverTimezone', …)` only while `serverUrl`/`username`/
|
||
`password` still match the request and the payload does not already carry
|
||
the value; a malformed payload is never rewritten); PWA:
|
||
`PlaylistsService.transformPlaylistMeta`, whose read and write share one
|
||
IndexedDB readwrite cursor transaction. No read precedes the write, because
|
||
the database worker interleaves requests and the Xtream edit dialog saves
|
||
through `DB_UPDATE_PLAYLIST` outside `PlaylistsService`'s queue: a
|
||
read-modify-write could hand a concurrent upsert's newer payload back to the
|
||
past or undo an edit that landed in between. The reverse ordering is covered
|
||
on the upsert side: `DB_UPSERT_APP_PLAYLIST(S)` (`playlistConflictUpdate`)
|
||
carries the STORED clock into a snapshot that has none while the row still
|
||
points at the same connection, so a favorites, recent-items or metadata write
|
||
built from a pre-clock snapshot cannot strip it; a snapshot carrying its own
|
||
clock, or moving the source, wins as is. The store offers the resolved
|
||
value on every check (a transient write failure is retried by the next one),
|
||
patches its own state only while the selected playlist is still the panel the
|
||
answer came from (`answersFor`: id + connection), and returns the store's
|
||
verdict about the current selection when it is not. An update that moves
|
||
`serverUrl` (`mergePlaylistMeta`, `DB_UPDATE_PLAYLIST`) drops the clock until
|
||
the next account-info check. Electron's `DB_GET_PLAYLIST` projects the
|
||
persisted value from the row payload so the store is seeded with it before,
|
||
or without, the account-info check. The same timezone lets
|
||
`XtreamApiService` read timestamp-less EPG `start`/`end` strings in the
|
||
clock the panel wrote them in (`parseXtreamServerLocalDateTime`);
|
||
`start_timestamp` still wins whenever it is present. Formatting uses
|
||
`hourCycle: 'h23'`, so server midnight renders as `00`, never `24`.
|
||
|
||
### Downloading completed catch-up programmes
|
||
|
||
Desktop Xtream Live TV exposes a TS-only download action in programme details.
|
||
It reuses the canonical timeshift resolver and original timestamps, preserves
|
||
playback headers and does not change playback. See
|
||
[Download Manager](download-manager.md#xtream-archive-downloads) for identity,
|
||
restart, expiry and transport-completion limits.
|
||
|
||
## Store composition and catalog windowing
|
||
|
||
`XtreamStore` is the public facade built with `signalStore()`, composing
|
||
`signalStoreFeature()` features for portal, content, selection, search, EPG,
|
||
player, favorites, recent and playback positions. Most features live under
|
||
`libs/portal/xtream/data-access/src/lib/stores/features/`; favorites and recent
|
||
items live directly under the data-access library’s `src/lib/`.
|
||
Routed components consume that facade; features delegate persistence/networking
|
||
to `IXtreamDataSource`, selected through `provideXtreamDataSource()`. Complete
|
||
SQLite capability uses database-first cache reads and API fill; the PWA source
|
||
uses API requests and session memory. This does not move screen orchestration
|
||
into shared utility projects.
|
||
|
||
Catalog lazy loading: catalog grids scroll infinitely instead of paging.
|
||
`withSelection` keeps a `visibleCount` render window over the in-memory
|
||
catalog plus bounded per-selection scroll snapshots for detail/tab
|
||
round-trips; the shared `InfiniteScrollDirective`
|
||
(`libs/portal/shared/ui`) measures container overflow to auto-fill tall
|
||
viewports (terminating on lack of container growth, not on a load count)
|
||
and fires `loadMore` near the bottom. The search layout routes its results
|
||
container through the same directive (`nearEnd*` inputs). Stalker feeds the
|
||
same contract from server-paged appends: portal pages accumulate into one
|
||
deduplicated list, `hasMoreContent` derives from accumulated length vs
|
||
`total_items`, a failed append keeps loaded pages and offers a tail retry,
|
||
and the facade maps page 0 to the skeleton and later pages to the tail
|
||
spinner. These catalog/search surfaces use incremental loading instead of
|
||
page buttons.
|