Files
iptvnator/docs/architecture/xtream-portal-compatibility.md
T
4gray faad8fd8fd docs(agents): compact root guidance and preserve task-specific knowledge (#1645)
* 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
2026-09-21 18:07:14 +02:00

24 KiB
Raw Blame History

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, SQLite DB Worker, Portal Detail Navigation, and VOD Multi-Source.

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 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.