mirror of
https://github.com/4gray/iptvnator.git
synced 2026-10-16 22:26:16 -08:00
521 lines
28 KiB
Markdown
521 lines
28 KiB
Markdown
# Stalker Portal Architecture
|
|
|
|
This document describes the Stalker portal implementation in IPTVnator and where each feature is integrated.
|
|
|
|
## Related Docs
|
|
|
|
- [Stalker Portal EPG Architecture](./stalker-epg.md)
|
|
- [Playlist Backup/Restore Architecture](./playlist-backup-restore.md)
|
|
- [Portal Detail Navigation](./portal-detail-navigation.md)
|
|
- [Embedded Inline Playback](./embedded-inline-playback.md)
|
|
- [Remote Control Architecture](./remote-control.md)
|
|
- [Download Manager](./download-manager.md)
|
|
- [Category Management](./category-management.md)
|
|
- [Stalker Store API Baseline](./stalker-store-api-baseline.md)
|
|
|
|
## Scope
|
|
|
|
Stalker support covers:
|
|
|
|
- Live TV (`itv`)
|
|
- Radio (`radio`)
|
|
- VOD (`vod`)
|
|
- Series (`series`)
|
|
- VOD-as-series flows (`is_series=1` and embedded `series[]`)
|
|
- Favorites and recently viewed collections
|
|
- Search
|
|
- External player playback (shared Xtream player infrastructure)
|
|
- Remote control for live ITV navigation
|
|
|
|
## Routing Structure
|
|
|
|
Primary route tree lives in
|
|
`libs/portal/stalker/feature/src/lib/stalker-feature.routes.ts`.
|
|
|
|
- `/stalker/:id/vod` (plus `vod/:categoryId` child)
|
|
- `/stalker/:id/series` (plus `series/:categoryId` child)
|
|
- `/stalker/:id/itv`
|
|
- `/stalker/:id/radio`
|
|
- `/stalker/:id/favorites`
|
|
- `/stalker/:id/recent`
|
|
- `/stalker/:id/search`
|
|
- `/stalker/:id/actor/:personId`
|
|
- `/stalker/:id/downloads` (shared `DownloadsComponent` from `@iptvnator/portal/downloads/feature`)
|
|
|
|
## Runtime Architecture
|
|
|
|
1. Angular Stalker screens call methods/resources in `StalkerStore`.
|
|
2. `StalkerStore` builds request params based on selected content type and current view state.
|
|
3. Requests go through `DataService.sendIpcEvent(STALKER_REQUEST, ...)` or `StalkerSessionService` (full portal auth).
|
|
4. Electron main process handles `STALKER_REQUEST` in
|
|
`apps/electron-backend/src/app/events/stalker.events.ts`.
|
|
5. Axios calls Stalker `load.php` API with required headers/cookies and returns the raw `response.data` to the renderer; normalization happens in the store feature slices.
|
|
|
|
## Main UI Components
|
|
|
|
- `CategoryContentViewComponent` from `@iptvnator/portal/catalog/feature` (`libs/portal/catalog/feature`)
|
|
- Shared category + content layout used by the `vod` and `series` routes (wired in `stalker-feature.routes.ts` via `loadCategoryContentViewComponent`)
|
|
- `libs/portal/stalker/feature/src/lib/stalker-live-stream-layout/stalker-live-stream-layout.component.ts`
|
|
- ITV live playback, radio playback, channel/station navigation, EPG panel integration
|
|
- `libs/ui/playback/src/lib/audio-player/audio-player.component.ts`
|
|
- Shared inline audio player used by M3U radio channels and Stalker radio stations
|
|
- `libs/portal/stalker/feature/src/lib/stalker-series-view/stalker-series-view.component.ts`
|
|
- Season/episode UI for all Stalker series modes
|
|
- `libs/portal/stalker/feature/src/lib/stalker-collection-route.component.ts`
|
|
- Favorites and recently-viewed collection views (`mode = 'favorites' | 'recent'` route data), rendering `stalker-collection-detail.component.ts`
|
|
- `libs/portal/stalker/feature/src/lib/stalker-search/stalker-search.component.ts`
|
|
|
|
## Store and Data Flow
|
|
|
|
Stalker store is now feature-composed:
|
|
|
|
- Facade: `libs/portal/stalker/data-access/src/lib/stalker.store.ts`
|
|
- Feature slices: `libs/portal/stalker/data-access/src/lib/stores/features/*`
|
|
- Shared helpers: `libs/portal/stalker/data-access/src/lib/*`
|
|
|
|
Important store responsibilities:
|
|
|
|
- Selected content/category/item state
|
|
- Category and paginated content resources
|
|
- ITV channel list + pagination (full-list session cache when the portal
|
|
supports it, legacy 14-per-page lazy loading otherwise)
|
|
- Radio category/station list + pagination
|
|
- Regular series seasons resource
|
|
- VOD-series (`is_series=1`) seasons + episodes resources
|
|
- Playback link creation (`create_link` flow)
|
|
- Favorites and recently viewed persistence helpers
|
|
|
|
Internal structure to preserve:
|
|
|
|
- `stalker.store.ts` stays as the thin facade that composes feature slices.
|
|
- Cross-slice contracts live in `stores/stalker-store.contracts.ts` so
|
|
feature dependencies are declared instead of repeated `unknown` casts.
|
|
- Request execution is centralized in `stores/utils/stalker-request.utils.ts`
|
|
for both authenticated full-portal calls and simple IPC-backed requests.
|
|
- Playback link resolution and Stalker collection persistence live in
|
|
dedicated `stores/utils/` helpers so player/favorites/recent slices stay
|
|
focused on orchestration.
|
|
- Category/content resources stay internal to the store slices. Feature
|
|
consumers should read `getCategoryResource()` and `getPaginatedContent()`,
|
|
which now always return arrays, and pair them with
|
|
`isCategoryResourceFailed()` / `isPaginatedContentFailed()` for explicit
|
|
error handling.
|
|
|
|
Failure-handling rule:
|
|
|
|
- Failed category or content requests must degrade into empty/error UI state,
|
|
not `undefined` collections or renderer exceptions. The workspace Stalker
|
|
context panel and live layout rely on this guarantee.
|
|
|
|
## Stalker Identity Policy
|
|
|
|
Full Stalker/Ministra portal authentication defaults to MAC-only identity. The
|
|
import UI can capture optional serial number, device IDs, and signatures, but
|
|
blank fields are not generated or forwarded to `get_profile`.
|
|
|
|
- User-provided `sn`, `device_id`, `device_id2`, `signature`, and `signature2`
|
|
values are trimmed, persisted under the canonical `stalker*` playlist fields,
|
|
and reused for initial auth, token refresh, retry auth, normal API requests,
|
|
and same-origin playback headers.
|
|
- Empty optional identity fields remain absent. IPTVnator must not generate a
|
|
device ID from the MAC address or duplicate `device_id2` from `device_id1`.
|
|
- The legacy default serial value `BEDACD4569BAF` is treated as absent at
|
|
runtime so older blank imports do not keep sending a synthetic serial number.
|
|
- Playback headers use the same serial normalization, so the legacy default is
|
|
not sent as `SN` or as a serial-derived `__cfduid`. MAC-only API and
|
|
playback requests do not synthesize `__cfduid`; when a real serial is
|
|
present, same-origin playback uses a canonical 32-character `__cfduid`
|
|
protocol cookie.
|
|
- Generated MAG-like identity remains a future explicit setting. It must not be
|
|
the default because strict portals can bind accounts to the first device
|
|
fingerprint they receive.
|
|
- Stalker workspace routes must initialize `StalkerStore` from a playlist object
|
|
with an explicit `isFullStalkerPortal` mode. If the active route metadata is a
|
|
lightweight playlist record without that field, the route session must load the
|
|
full playlist by id before category/content resources run. Stalker auth
|
|
metadata is independent from M3U playlist EPG metadata and must not depend on
|
|
M3U-specific EPG fields.
|
|
|
|
## Live TV and Radio
|
|
|
|
The Stalker live route and radio route intentionally share
|
|
`StalkerLiveStreamLayoutComponent`:
|
|
|
|
- `itv` uses `type=itv&action=get_ordered_list`, stores results in
|
|
`itvChannels`, resolves playback through `resolveItvPlayback(...)`, and keeps
|
|
the EPG panel visible.
|
|
- ITV additionally loads the COMPLETE channel list once per portal session (see
|
|
"Full ITV channel list cache" below), so category views and search are not
|
|
limited to the lazily loaded 14-item pages.
|
|
- `radio` uses `type=radio&action=get_ordered_list`, stores results in
|
|
`radioChannels`, resolves playback through `resolveRadioPlayback(...)`, and
|
|
renders `AudioPlayerComponent` instead of a video player.
|
|
- Radio hides the EPG panel and must not call Stalker EPG endpoints because
|
|
radio stations do not have EPG data.
|
|
- Radio always uses the inline audio player. External player settings are
|
|
ignored for Stalker radio, matching M3U radio behavior.
|
|
- Radio stations opened from favorites or recently viewed remain live
|
|
collection items with `radio: 'true'`; the shared collection resolver uses
|
|
`create_link` with `type=radio`, skips EPG loading, and renders the same
|
|
`AudioPlayerComponent` layout instead of the Stalker VOD detail layout.
|
|
- Some Stalker portals do not expose radio categories. Radio category loading
|
|
falls back to a synthetic `PORTALS.ALL_RADIO` category with
|
|
`category_id: '*'` so the station list can still be loaded.
|
|
|
|
## Full ITV Channel List Cache
|
|
|
|
Stalker portals paginate `get_ordered_list` with a server-side page size
|
|
(typically 14 items), so lazy loading alone can never power a complete local
|
|
search — this used to limit ITV search to whatever pages the user had scrolled
|
|
through. `StalkerItvCacheService`
|
|
(`libs/portal/stalker/data-access/src/lib/stalker-itv-cache.service.ts`) fixes
|
|
this with a per-portal, in-memory session cache of the complete live channel
|
|
list:
|
|
|
|
- Load strategy: first try the Ministra `get_all_channels` action (`type=itv`,
|
|
returns ALL channels in one response — the same call STB clients use); if
|
|
the portal does not implement it, crawl `get_ordered_list` pages
|
|
(`category=*`, `genre=*`, concurrency 4, one retry per page, early stop on
|
|
an empty page **or a page that adds no new channel ids** — some portals
|
|
ignore `p` and repeat — 30k-channel hard cap) with progress reporting. The
|
|
assembled list is de-duplicated by channel id (both strategies) so it never
|
|
collides with the template's `track item.id`. The loading strategy itself is
|
|
a stateless helper (`stalker-itv-channel-loader.ts`); the service owns state.
|
|
- Outcomes: a well-formed but unusable response marks the portal
|
|
`unsupported` for the session (legacy paged flow stays in charge); a
|
|
transient failure (network, or a page that failed both attempts) is retried
|
|
later but throttled by a per-portal cooldown (`ERROR_COOLDOWN_MS`, 30s) so a
|
|
deterministically-failing page can't trigger an unbounded re-crawl loop.
|
|
- Per-portal reactivity: the "cache ready / refreshed" trigger is a
|
|
**per-portal** version signal (`versionFor(playlist)`), not one global
|
|
counter, and the content resource reads it **only for ITV**. This is
|
|
load-bearing: a global counter re-fired the resource for whatever was on
|
|
screen (radio, another portal), and the legacy paged branch appends at
|
|
`pageIndex > 1`, so an unrelated load completing duplicated the visible page
|
|
(colliding `track item.id` → NG0955). The `isCurrentRequest` guard is scoped
|
|
the same way.
|
|
- Integration: the `getContentResource` loader in
|
|
`with-stalker-content.feature.ts` serves ITV categories from the cache when
|
|
ready (local `tv_genre_id` filtering via `filterItvChannelsByGenre`,
|
|
`hasMoreChannels=false`), and otherwise runs the legacy paged fetch while
|
|
`ensureLoaded()` fills the cache in the background; the resource re-fires
|
|
via the `cacheVersion` signal once the full list arrives.
|
|
- UI: `StalkerLiveStreamLayoutComponent` windows the rendered list
|
|
(100-item chunks extended by the existing scroll handler) so multi-thousand
|
|
channel lists do not blow up the DOM; the header count and search cover the
|
|
whole category; a refresh button re-loads the list in place; a progress line
|
|
shows crawl status.
|
|
- Loading state contract (important — regressions here strand the sidebar on a
|
|
skeleton): in full-list mode the content loader serves the filtered list
|
|
**synchronously** from the cache. The category-change reset effect therefore
|
|
must NOT `setItvChannels([])` while `itvFullListActive()` is true — it runs
|
|
after the store resource and would clobber the freshly served list, leaving
|
|
every category after the first stuck on a skeleton. The initial-loading
|
|
skeleton (`isInitialChannelsLoading`) must key off an actual in-flight load
|
|
(`itvFullListLoading()` or `isPaginatedContentLoading()`), not merely an empty
|
|
channel list; an empty result once loading has settled is an empty category
|
|
and renders `PORTALS.NO_CHANNELS_IN_CATEGORY`, not a spinner.
|
|
- Search: with the cache active, the header search spans the ENTIRE portal
|
|
(all genres) — filtering the store's `itvFullChannelList`, not just the
|
|
selected category — so searching "CNN" while a "Sports" genre is selected
|
|
still finds it; clearing the term returns to the selected category. The
|
|
workspace shell drops the `degraded-loaded-only` / "loaded only" status for
|
|
Stalker ITV once `itvFullListActive`; radio (no full-list cache) always keeps
|
|
the loaded-only hint (`workspace-shell-search.service.ts`).
|
|
- Windowed selection: remote channel-up/down and numeric select operate over
|
|
the full filtered category, so the render window (`renderLimit`) grows to
|
|
include a selection beyond it (`ensureChannelWithinRenderWindow`) instead of
|
|
drifting off-screen.
|
|
- Category count badges: the context panel shows per-genre channel counts on
|
|
Stalker **Live TV** categories (like Xtream/M3U), fed by the store computed
|
|
`itvCategoryItemCounts` (the full list grouped by numeric `tv_genre_id`; the
|
|
`'*'` "All" row's total is stored under the `NaN` key that
|
|
`Number('*')` produces). Badges are ITV-only — VOD/series/radio still page
|
|
lazily so their per-category totals are unknown — and show a loading shimmer
|
|
while the full list is still loading (`workspace-context-panel` →
|
|
`stalkerShowCounts` / `stalkerCountDisplayMode`).
|
|
- Censored (adult) genres: portals typically EXCLUDE these channels from
|
|
`get_all_channels` (sometimes without even flagging the genre `censored` in
|
|
`get_genres`), so the cache legitimately has zero channels for them. The
|
|
content loader therefore serves a genre from the cache only when the
|
|
genre-filtered result is non-empty; otherwise it falls back to the legacy
|
|
paged `get_ordered_list` fetch, which still returns those channels. The
|
|
store computed `itvSelectedCategoryFromCache` is the single source of truth
|
|
for this mode — the live layout keys windowing/infinite-scroll/`loadMore`
|
|
and the category-change reset off it, NOT off `itvFullListActive`. Count
|
|
badges: genres with no cached channels get NO map entry and the category
|
|
view omits their badge (`omitMissingCounts`) instead of showing a
|
|
misleading "0". The mock server ships a censored `For adults` ITV category
|
|
(id 1099) to exercise this path.
|
|
- Eager preload + all-channels view (Xtream parity): entering the Live TV
|
|
section immediately starts the full-list load (`preloadItvChannels()`, fired
|
|
from an effect in `StalkerLiveStreamLayoutComponent` — not from the first
|
|
category click), so the count badges and the all-channels view are available
|
|
right away. Before a category is selected, the main area shows
|
|
`StalkerItvAllItemsComponent` — a paginated card grid of every channel in
|
|
the portal (client-side pagination only; it must never touch the store's
|
|
legacy `page` state, which would re-fire portal requests). Clicking a card
|
|
runs the same `playChannel` flow as the sidebar. Portals without a usable
|
|
full list keep the "select a category" placeholder.
|
|
- Scope: ITV only. VOD/series keep server-side search; radio keeps legacy
|
|
paging (station lists are small).
|
|
- The stalker-mock-server implements `get_all_channels` and provides the
|
|
`legacy-pagination` scenario MAC (`00:1A:79:00:00:06`) to exercise the
|
|
crawl fallback.
|
|
|
|
## VOD/Series Modes
|
|
|
|
Stalker has multiple real-world data shapes. The current implementation supports all three:
|
|
|
|
Within Stalker portal data access and feature code,
|
|
`isStalkerSeriesFlag()` is the canonical predicate for `is_series`.
|
|
`normalizeStalkerSeriesFlag()` delegates to it and produces the normalized
|
|
positive marker `true` or `undefined`. The activity normalizer in
|
|
`libs/shared/interfaces` keeps its dependency-neutral equivalent for dashboard
|
|
records. Both accept the same closed set: boolean `true`, numeric `1`, or string
|
|
`'1'`. Unsupported values do not by themselves classify a VOD item as a series.
|
|
|
|
1. Regular Series (`/series`):
|
|
|
|
- Seasons come from API resource (`serialSeasonsResource`).
|
|
- Episodes are derived from season payload.
|
|
- This is the only mode that sets `selectedSerialId`, which is what drives
|
|
`serialSeasonsResource`. It is set purely from `selectedContentType ===
|
|
'series'` — the `series` detail branch renders `<app-stalker-series-view />`
|
|
with no `vodWithSeries` input, so the API resource is its only episode
|
|
source and the fetch must never be gated on item shape.
|
|
- Modes 2 and 3 below are always opened under the `vod` content type, which
|
|
leaves the id unset — otherwise every VOD detail open would fire a
|
|
`get_ordered_list&type=series` request whose result is discarded.
|
|
|
|
2. VOD with Embedded `series[]`:
|
|
|
|
- Item is opened under VOD, but already contains episodes.
|
|
- `StalkerSeriesViewComponent` creates a pseudo-season and renders episodes directly.
|
|
|
|
3. VOD with `is_series=1` (Ministra plugin behavior):
|
|
|
|
- Treated as series flow from VOD context.
|
|
- Seasons are fetched lazily.
|
|
- Episodes are fetched on season select.
|
|
- The series quick-start CTA can load the first unloaded VOD-series season
|
|
before playback. Unloaded seasons are considered unplayed in full season
|
|
order, so an earlier unloaded season is not skipped just because a later
|
|
season was loaded manually. If all currently loaded episodes are watched and
|
|
more season metadata exists, quick start loads the next unloaded season
|
|
instead of showing the completed state. After a lazy load, quick start is
|
|
recomputed from the mapped episodes before playback so provider episode
|
|
ordering cannot start the wrong episode.
|
|
- For unloaded VOD-series seasons, the CTA target label is derived from season
|
|
metadata and rendered as `SxxE01` until episode details are loaded.
|
|
- Lazy VOD-series episodes use scoped tracking IDs derived from the parent
|
|
series ID, provider episode ID, season key, and episode number. The season
|
|
key follows the mapping fallback (`season_number`, then name, then ID).
|
|
- The previous season/episode hash remains available only as a compatibility
|
|
alias in `legacyTrackingId`. The scoped ID is the in-memory episode key, and
|
|
new playback positions always use it.
|
|
- Quick-start actions preserve both their translation key and interpolation
|
|
parameters when adapted for the Stalker CTA. Dropping `labelParams` exposes
|
|
the raw `{{episode}}` placeholder.
|
|
|
|
Series inline playback behavior is shared across all three modes:
|
|
|
|
- `StalkerSeriesViewComponent` maps every mode into `mappedSeasons()` and derives the currently playing episode from `inlinePlayback.contentInfo.contentXtreamId`.
|
|
- The inline player header shows the current episode metadata below the title, for example `S01E03 - Episode title`.
|
|
- Embedded players receive previous/next episode state for the current season only.
|
|
- Inline series autoplay is enabled by default. On player EOF (`ended`), Stalker starts the next episode only when it already exists in the current season's mapped episode list.
|
|
- Autoplay and Next stop at the last episode of the current season. They do not jump to the next season and do not lazy-load an unloaded `is_series=1` season. Quick start remains the only flow that may load another VOD-series season before playback.
|
|
- Previous is disabled on the first episode of the current season and otherwise switches directly to the previous episode.
|
|
- Before either inline or external playback starts, the resolved content info
|
|
includes the parent `seriesXtreamId` and the mapped `seasonNumber` /
|
|
`episodeNumber`. Future playback-position rows therefore carry enough
|
|
metadata for workspace surfaces to render an episode badge. Existing rows
|
|
without those fields are intentionally not migrated and remain badge-less
|
|
until the episode is played again.
|
|
- Ministra payloads may omit `season_number`. Episode mapping and lazy
|
|
quick-start labels share the same naturally ordered season fallback so later
|
|
seasons are not persisted as season 1.
|
|
|
|
### Playback Position Identity and Compatibility
|
|
|
|
Legacy playback positions are reconciled lazily when the current parent
|
|
series' positions and mapped episodes are available:
|
|
|
|
- The lookup is scoped to the current parent series. A legacy row is eligible
|
|
only when its stored season and episode metadata, when present, match the
|
|
mapped episode.
|
|
- An exact scoped tracking-ID row always wins. The old tracking ID may supply
|
|
an in-memory compatibility alias only when no exact row exists.
|
|
- On the next position write, IPTVnator persists the scoped row through the
|
|
strict, failure-propagating persistence boundary before removing a confirmed
|
|
legacy row. If the scoped write fails, the legacy row remains intact.
|
|
- This is an on-read/on-write compatibility path, not a database schema
|
|
migration or a bulk rewrite of saved positions.
|
|
|
|
The VOD-series contract is cross-surface:
|
|
|
|
- Favorites and recently viewed records preserve the raw `is_series` flag and
|
|
VOD origin so reopening still uses the lazy Ministra resources.
|
|
- `extractStalkerItemType()` normalizes those activity records to dashboard
|
|
type `series`.
|
|
- The dashboard resolves episode progress by the parent `seriesXtreamId` and
|
|
renders the saved season/episode metadata. It does not infer episode numbers
|
|
from provider payloads.
|
|
|
|
Core decision logic and normalization are centralized in:
|
|
|
|
- `libs/portal/stalker/data-access/src/lib/stalker-vod.utils.ts`
|
|
- `libs/portal/stalker/data-access/src/lib/models/*.ts`
|
|
|
|
## Favorites and Recently Viewed
|
|
|
|
Current implementation is shared via Stalker-specific helpers:
|
|
|
|
- `createPortalCollectionResource(...)` generic collection loader
|
|
- `createPortalFavoritesResource(...)` favorites wrapper
|
|
- `createStalkerDetailViewState(...)` unified "open detail" decision
|
|
- `toggleStalkerVodFavorite(...)` shared add/remove behavior
|
|
- `normalizeStalkerEntityId(...)` and `normalizeStalkerEntityIdAsNumber(...)` for stable ID matching
|
|
- `matchesFavoriteById(...)` for cross-shape favorite matching
|
|
|
|
Where this is used:
|
|
|
|
- `libs/portal/stalker/feature/src/lib/stalker-collection-detail.component.ts` (favorites + recently viewed)
|
|
- `libs/portal/stalker/feature/src/lib/stalker-search/stalker-search.component.ts`
|
|
- `libs/portal/stalker/feature/src/lib/stalker-catalog-detail/stalker-catalog-detail.component.ts`
|
|
- `libs/portal/stalker/feature/src/lib/stalker-favorites-button/stalker-favorites-button.component.ts`
|
|
|
|
Navigation rule to preserve:
|
|
|
|
- Stalker favorites, recently viewed, and search stay in their current screen and open inline detail state.
|
|
- They should not redirect into a canonical content/category/item route because Stalker detail rendering is currently store-state/inline driven, not route driven.
|
|
- Stalker radio favorites/recent items are the exception to VOD/series inline
|
|
detail opening: they are normalized as live items and must open through the
|
|
shared live collection audio-player path.
|
|
- VOD-backed series favorites can be displayed in series collections, but detail
|
|
opening must preserve their VOD origin: `is_series=1` favorites set the
|
|
selected content type to `vod` so the lazy Ministra season/episode resources
|
|
run, and embedded `series[]` favorites render through the embedded VOD-series
|
|
branch.
|
|
- See [Portal Detail Navigation](./portal-detail-navigation.md).
|
|
|
|
### Embedded-series snapshot refresh
|
|
|
|
Favorites and recently-viewed rows store the whole Stalker item as a JSON
|
|
snapshot, so an embedded `series[]` episode list (and its playback `cmd`)
|
|
freezes at the moment the row was written — newly released episodes would
|
|
never appear when the item is reopened from favorites, recents, or the
|
|
dashboard rails. `withStalkerSnapshotRefresh()`
|
|
(`stores/features/with-stalker-snapshot-refresh.feature.ts`) fixes this with a
|
|
snapshot-first + background re-fetch contract:
|
|
|
|
- The stored snapshot renders immediately; the store method
|
|
`refreshEmbeddedSeriesSelection()` then re-fetches the item via a portal
|
|
title search (`get_ordered_list&type=vod&search=<title>`, item matched by
|
|
id, paginated up to 5 pages, wildcard-category retry) in the background.
|
|
- When the episode list or `cmd` changed, the selection is patched in place.
|
|
The guard requires both the item id **and** the active playlist id to be
|
|
unchanged, because Stalker ids are only unique per portal.
|
|
- Only the in-memory selection is patched — the stored snapshot row is
|
|
deliberately left alone. Every entry path into the detail view runs this
|
|
refresh, so a stale stored episode list is never rendered for longer than
|
|
one background request, and writing it back would add an uncontrolled
|
|
background writer to the whole-playlist read-modify-write that every
|
|
favorite/recent mutation performs (lost-update risk).
|
|
- Triggers: `stalker-collection-detail.component.ts` (favorites/recent tabs,
|
|
global collections, dashboard handoffs) and the optional catalog-facade hook
|
|
`refreshSnapshotSelection()` for snapshot-injected browse detail
|
|
(`openStalkerItem` navigation state).
|
|
- Regular `type=series` and Ministra `is_series` favorites are unaffected —
|
|
their seasons/episodes are always fetched fresh on open.
|
|
|
|
## Backup and Restore
|
|
|
|
Versioned playlist backups include Stalker connection metadata plus playlist-
|
|
scoped favorites/recent snapshots.
|
|
|
|
Exported fields:
|
|
|
|
- `portalUrl`
|
|
- `macAddress`
|
|
- `isFullStalkerPortal`
|
|
- optional `username` / `password`
|
|
- optional request headers (`userAgent`, `referrer`, `origin`)
|
|
- full-portal serial/device/signature fields when present
|
|
- favorites and recently viewed collections
|
|
|
|
Excluded fields:
|
|
|
|
- `stalkerToken`
|
|
- `stalkerAccountInfo`
|
|
- playback positions in backup v1
|
|
|
|
Import rule:
|
|
|
|
- backups restore the saved portal definition and replace the stored
|
|
favorites/recent state for the matched playlist
|
|
- a fresh handshake must happen after import for full-portal sessions; imported
|
|
backups never trust a serialized token
|
|
|
|
## Remote Control Integration
|
|
|
|
Stalker live remote control is implemented in:
|
|
|
|
- `libs/portal/stalker/feature/src/lib/stalker-live-stream-layout/stalker-live-stream-layout.component.ts`
|
|
|
|
Supported today:
|
|
|
|
- Channel up/down
|
|
- Numeric channel selection (list-position based)
|
|
- Status publish for remote UI (portal/channel/current program)
|
|
|
|
See full backend and web-remote flow in [Remote Control Architecture](./remote-control.md).
|
|
|
|
## EPG Integration
|
|
|
|
Stalker ITV now splits EPG usage:
|
|
|
|
- active channel panel: bulk `get_epg_info` cached once per playlist and rendered
|
|
through the shared EPG panel (`app-epg-timeline`, or `app-epg-list-view` in list mode)
|
|
- channel row preview: once ITV channels render, a post-reset effect eagerly
|
|
starts the de-duplicated bulk `get_epg_info` load; rows issue no per-row
|
|
request and derive previews from that cache
|
|
- active panel fallback: `get_short_epg` when bulk EPG is missing or unsupported
|
|
|
|
Full details are documented in [Stalker Portal EPG Architecture](./stalker-epg.md).
|
|
|
|
## Shared/Reusable Infrastructure
|
|
|
|
Stalker reuses some Xtream UI infrastructure deliberately:
|
|
|
|
- Category content rendering route uses Xtream category content component
|
|
- Season container for episodes uses shared Xtream season UI component
|
|
- Playback position handling for series episodes reuses Xtream store position mechanisms
|
|
- Downloads route reuses shared downloads feature
|
|
|
|
This reduces duplicate UI logic across portal types and keeps compatibility behavior aligned.
|
|
|
|
## Regression Coverage
|
|
|
|
The compatibility helper and focused regression coverage for Stalker VOD mode
|
|
branching and the cross-surface series contract live in:
|
|
|
|
- `libs/portal/stalker/data-access/src/lib/stalker-vod.utils.spec.ts`
|
|
- `libs/portal/stalker/feature/src/lib/stalker-series-view/stalker-series-position-compatibility.ts`
|
|
- `libs/portal/stalker/feature/src/lib/stalker-series-view/stalker-series-position-compatibility.spec.ts`
|
|
- `libs/portal/stalker/feature/src/lib/stalker-series-view/stalker-series-view.position-compatibility.spec.ts`
|
|
- `libs/portal/stalker/feature/src/lib/stalker-series-view/stalker-series-view.component.spec.ts`
|
|
- `libs/workspace/dashboard/data-access/src/lib/dashboard-data.service.spec.ts`
|
|
|
|
Covered scenarios include:
|
|
|
|
- Embedded `series[]` opens series view state
|
|
- `is_series=1` opens lazy series state
|
|
- VOD-backed series favorites keep VOD-series loading semantics when opened from
|
|
favorites/global favorites
|
|
- Favorite toggle helper path invokes the expected add/remove flow
|
|
- Quick-start episode labels interpolate their episode number
|
|
- Inline and external episode handoffs carry resolved season/episode metadata
|
|
- Dashboard activity classifies `is_series` VOD as series and resolves its
|
|
saved episode position
|