* docs(skills): design implementation synchronization * docs(skills): plan implementation synchronization * fix(release): filter internal notes from public body * docs(release): synchronize release workflow guidance * fix(stalker): normalize catalog series flags * fix(stalker): preserve progress with scoped episode IDs * fix(playback): expose strict position persistence * docs(stalker): record series position compatibility * test(skills): validate repository skill contracts * fix(database): keep SQL trace values private * docs(skills): refresh Nx and SQLite ownership * docs(skills): align provider and UI guidance * docs(skills): tighten validated guidance * docs(release): require exact release pushes * style(electron): remove trailing blank line * fix(ci): classify repository skills coverage
14 KiB
Stalker Portal EPG Architecture
This document describes the current EPG implementation for Stalker/Ministra ITV channels in IPTVnator.
Related architecture docs:
Overview
Stalker now uses two EPG paths with different purposes:
- The active channel EPG panel uses
get_epg_infoas a bulk endpoint, fetches a 7-day window once per playlist session, caches programs by channel id, and renders the selected channel through the sharedapp-epg-timelinecomponent. - Channel rows never send per-row EPG requests. The bulk EPG load is triggered
eagerly when a category's channels first render (a constructor effect in
StalkerLiveStreamLayoutComponentcallsensureBulkItvEpg(168)once ITV channels are present) — not only after the first channel is played — so the row "now playing" previews and the EPG panel populate immediately. Rows derive their current program and progress bar from the cached bulk map.- Effect ordering matters: the eager-EPG effect is registered after the
playlist-change effect that calls
clearBulkItvEpgCache(). On a portal switch the cache is cleared first and then refilled; if the order is reversed the clear clobbers the just-loaded bulk EPG on initial render. ensureBulkItvEpgde-duplicates (viaisLoadingBulkItvEpg/bulkItvEpgLoaded+ matching playlist/period), so the eager trigger and the play-timeloadEpgForChannelpath never double-fetch.
- Effect ordering matters: the eager-EPG effect is registered after the
playlist-change effect that calls
- If a portal does not return usable bulk data for the selected channel, the
active panel falls back to
get_short_epg.
This keeps the live list cheap while giving the active panel the same date-navigator UI used in the M3U/Xtream flows.
Architecture
┌────────────────────────────────────────────────────────────────────────────┐
│ StalkerLiveStreamLayoutComponent │
│ libs/portal/stalker/feature/src/lib/stalker-live-stream-layout/ │
│ │
│ sidebar rows active channel panel │
│ ──────────── ─────────────────── │
│ row preview map playChannel() │
│ from bulk cache │ │
│ │ ▼ │
│ │ ensureBulkItvEpg(168) │
│ │ selectedItvEpgPrograms() │
│ ▼ │ │
│ current program preview ├── bulk hit → app-epg-timeline │
│ after first bulk load └── empty/unsupported → short fallback │
└────────────────────────────────────────────────────────────────────────────┘
│ │
▼ ▼
┌────────────────────────────────────────────────────────────────────────────┐
│ with-stalker-epg.feature │
│ │
│ bulkItvEpgByChannel: Record<string, EpgProgram[]> │
│ bulkItvEpgPlaylistId / bulkItvEpgPeriodHours / bulkItvEpgLoaded │
│ ensureBulkItvEpg() selectedItvEpgPrograms() │
└────────────────────────────────────────────────────────────────────────────┘
│ │
▼ ▼
┌────────────────────────────────────────────────────────────────────────────┐
│ Stalker Portal API │
│ │
│ action=create_link action=get_short_epg action=get_epg_info │
└────────────────────────────────────────────────────────────────────────────┘
Stalker EPG API
get_short_epg (active-panel fallback)
Request
GET load.php?type=itv&action=get_short_epg&ch_id={channel_id}&size={n}&JsHttpRequest=1-xml
Current usage
- Active panel fallback path:
size=10
Response
{
"js": {
"data": [
{
"id": "123",
"ch_id": "45",
"name": "Program Title",
"descr": "Program description",
"time": "2025-01-15 14:00:00",
"time_to": "2025-01-15 14:30:00",
"duration": "1800",
"start_timestamp": "1736949600",
"stop_timestamp": "1736951400"
}
]
}
}
Notes
- The response is normalized into shared
EpgItem[] - Only the active-panel fallback uses this path and maps the result into
controlled
EpgProgram[]
get_epg_info (bulk row-preview and active-panel source)
Request
GET load.php?type=itv&action=get_epg_info&period={hours}&JsHttpRequest=1-xml
Current usage
- Fetched once with
period=168 - Scoped to the current playlist session
- Not refetched on active-channel change
Expected response
{
"js": {
"data": {
"45": [
{
"id": "1",
"name": "Program Title",
"descr": "Program description",
"time": "2025-01-15 14:00:00",
"time_to": "2025-01-15 16:00:00",
"start_timestamp": "1736949600",
"stop_timestamp": "1736956800"
}
]
}
}
}
Notes
- The store supports the channel-keyed bulk shape above as the primary contract
- For weak or mock-style portals that still return array-style data, the store treats the result as compatibility input and leaves the short-EPG fallback path available
Data Mapping
Fallback data (get_short_epg) → EpgItem
The short EPG path now exists only for the active-panel fallback flow.
Key mapped fields:
| Stalker field | EpgItem field |
|---|---|
id |
id |
ch_id |
channel_id |
name |
title |
descr |
description |
time |
start |
time_to |
end, stop |
start_timestamp |
start_timestamp |
stop_timestamp |
stop_timestamp |
Active panel data (get_epg_info / fallback) → EpgProgram
The active panel uses controlled EpgProgram[] because app-epg-timeline filters
and groups by day.
Normalization rules:
start/endare converted to ISO stringsstartTimestamp/stopTimestampare always populated- Programs are sorted by start time per channel
selectedItvIdis used to project cached bulk data to the active channel
Implementation Details
Key files
| File | Purpose |
|---|---|
libs/portal/stalker/data-access/src/lib/stores/features/with-stalker-epg.feature.ts |
bulk cache and fallback handling |
libs/portal/stalker/feature/src/lib/stalker-live-stream-layout/stalker-live-stream-layout.component.ts |
active-channel EPG loading and controlled app-epg-timeline wiring |
libs/portal/stalker/feature/src/lib/stalker-live-stream-layout/stalker-live-stream-layout.component.html |
active panel template |
libs/ui/epg/src/lib/epg-timeline/epg-timeline.component.ts |
shared controlled EPG timeline with date navigator |
Store API
The Stalker EPG feature exposes one bulk method plus the short-EPG fallback:
fetchChannelEpg(channelId: number | string, size?: number): Promise<EpgItem[]>
ensureBulkItvEpg(periodHours = 168): Promise<void>
It also exposes:
selectedItvEpgProgramsclearBulkItvEpgCache()
Bulk state is keyed by playlist so cached results do not leak between Stalker playlists.
Active panel flow
- User activates a live channel
- The component ensures playback link resolution as before
- The component ensures
ensureBulkItvEpg(168)has run; the eager row effect normally started the same de-duplicated request before playback selectedItvEpgPrograms()feedsapp-epg-timeline- If the selected channel has no bulk programs, the component falls back to
get_short_epg
The active panel no longer uses local EPG pagination or a "Load more" button.
When Stalker live TV is playing through an internal player, the active panel is
wrapped in the shared collapsible live EPG panel. The collapsed/expanded state
uses the shared live-epg-panel-state preference and is only applied after a
stream URL has been resolved; external playback keeps the full EPG-only panel.
Channel row preview flow
Once non-radio ITV channels render, the post-reset component effect calls
ensureBulkItvEpg(168). It starts eagerly before playback and is de-duplicated
against the active-channel path. Individual rows never issue per-row requests.
As soon as the bulk request completes, visible row previews derive locally
from bulkItvEpgByChannel:
- pick the current program for the channel, if one exists
- compute progress from the cached program timestamps
- leave the row in its existing placeholder state when no current program exists
Cache Lifecycle
- Bulk EPG is fetched once per playlist session
- Channel switches only read from
bulkItvEpgByChannel - The cache is cleared when the Stalker playlist changes
- This implementation does not add TTL-based refresh or background polling
Authentication
EPG requests follow the standard Stalker request path:
| Portal type | Auth path |
|---|---|
| Full Stalker portal | StalkerSessionService.makeAuthenticatedRequest() |
| Simple Stalker portal | generic IPC request path via Electron |
No EPG-specific backend transport was needed; the Electron Stalker request handler forwards portal params directly.
Fallback Behavior
Some providers do not implement get_epg_info consistently. The active panel
therefore falls back to get_short_epg when:
- the bulk request fails
- the bulk response is empty
- the selected channel has no programs in the cached bulk map
This keeps the panel usable even on limited portals, while still taking advantage of the richer bulk API when it is available. Row previews do not fallback to per-channel requests in this mode; they remain empty until bulk EPG is available.
Manual EPG Mapping
Stalker channels carry no XMLTV identifier, so when the portal's own EPG is missing or wrong the only uploaded-EPG entry point is a manual mapping: right-click a channel in the ITV sidebar (or in global favorites) and pick "Map EPG channel" to attach it to a channel from an uploaded XMLTV guide.
- Mappings are stored in the shared
epg_channel_mappingstable under the playlist-scoped keystalker:{playlistId}:{channelId}(buildStalkerEpgMappingKeyinlibs/shared/interfaces/src/lib/epg-mapping-key.util.ts). withStalkerEpg().applyMappedItvEpg(channelIds)batch-resolves mappings (onegetEpgMappingsBatchIPC per new id set) and overlays the mapped XMLTV programs ontobulkItvEpgByChannel, so both the active panel and the row previews pick them up with no template changes. Overrides are re-merged wheneverensureBulkItvEpgreplaces the bulk record and are re-checked after the mapping dialog closes with a change.- The collection views (global favorites/recent) resolve the same keys in
StreamResolverService(loadStalkerEpgItemsfor the detail panel,loadStalkerEpgBatch+prefetchEpgMappingsfor row previews). - Everything is gated behind
supportsEpgMapping, so the PWA never shows the menu entry.
Future Enhancements
- add cache refresh / invalidation for long-running live sessions
- add Stalker catch-up support to
app-epg-timelineonce the playback flow exists - optionally add category-level prefetch timing metrics for bulk EPG