mirror of
https://github.com/4gray/iptvnator.git
synced 2026-10-11 11:06:16 -08:00
Favorites and recently-viewed rows store Stalker items as full JSON snapshots, so a vclub-style embedded series[] episode list froze at the moment the row was written: a series favorited when only episode 1 was out kept showing one episode forever when opened from favorites, recents, Continue Watching, or any dashboard rail. New withStalkerSnapshotRefresh() store feature renders the stored snapshot immediately and re-fetches the item from the portal in the background via a title search (get_ordered_list&type=vod&search=..., matched by id, paginated up to 5 pages, wildcard-category retry), patching fresh episodes and cmd into the active selection. The patch is guarded on both the item id and the active playlist id, since Stalker ids are only unique per portal. Only the in-memory selection is patched — the stored snapshot row is deliberately left alone, because every entry path into the detail view runs this refresh and writing it back would add an uncontrolled background writer to the whole-playlist read-modify-write that every favorite/recent mutation performs. Also fixes the stalker-mock-server embedded-series scenario, which generated series[] as objects the app's vclub adapters filter out instead of the episode-number arrays real portals send. Regular type=series and Ministra is_series items are unaffected; Xtream is unaffected (get_series_info is never cached). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
488 lines
26 KiB
Markdown
488 lines
26 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:
|
|
|
|
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.
|
|
- Uses unique generated tracking IDs for episode playback position compatibility.
|
|
- 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.
|
|
|
|
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: no pre-playback network requests; previews are derived
|
|
from cached bulk EPG only after the first active-channel fetch succeeds
|
|
- 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
|
|
|
|
Focused regression tests 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-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
|