# 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 `