Files
iptvnator/docs/architecture/stalker-epg.md
T
4grayandClaude Fable 5 c707af1334 feat(epg): manual EPG mapping for Stalker portals (#1173)
* feat(epg): manual EPG mapping for Stalker portals

Extends the manual EPG-to-channel mapping (PR #1165) to Stalker:

- shared key helper buildStalkerEpgMappingKey — playlist-scoped
  stalker:{playlistId}:{channelId} keys, mirroring the Xtream scheme
- ITV sidebar: right-click context menu with "Map EPG channel"; after
  the dialog closes with a change, the bulk EPG cache is rebuilt so the
  panel and row previews reflect the new mapping immediately
- withStalkerEpg().applyMappedItvEpg(): batch-resolves mappings for
  rendered channels (one getEpgMappingsBatch IPC per new id set) and
  overlays uploaded-XMLTV programs onto bulkItvEpgByChannel; overrides
  survive ensureBulkItvEpg reloads
- stream-resolver: mapping check in loadStalkerEpgItems (detail) and
  batched prefetch in loadStalkerEpgBatch (previews);
  mappingCandidateKeys/prefetchEpgMappings generalized beyond Xtream
- global favorites: stalker branch for the Map-EPG menu entry (item id
  extracted from the stalker::{playlistId}::{id} uid)
- docs: manual-mapping section in docs/architecture/stalker-epg.md and
  a CLAUDE.md EPG bullet covering the whole mapping feature
- tests: applyMappedItvEpg suite, stalker preview mapping in the
  stream-resolver spec, key-builder specs

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(epg): harden Stalker mapping edges found in adversarial review

- stream-resolver: stalker items resolve only the playlist-scoped
  mapping key — bare tvgId/name candidates (tvgId mirrors the raw
  provider id) could only produce false matches against unrelated M3U
  mappings, and previews would disagree with the detail path
- applyMappedItvEpg: staleness guard after every await so an in-flight
  call cannot write portal A's mapped EPG into portal B's state after
  a playlist switch (the store is a root singleton)
- applyMappedItvEpg: ids are marked checked only after a successful
  lookup — a transient IPC failure no longer suppresses the mapping
  for the rest of the session
- live layout: post-dialog refresh re-applies overrides for the
  unfiltered channel list, so an active search cannot drop the playing
  channel's mapping
- global favorites: new epgMappingChanged output emitted when the
  dialog actually changed a mapping; unified live tab reloads its EPG
  previews in response

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-19 18:29:23 +02:00

13 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_info as a bulk endpoint, fetches a 7-day window once per playlist session, caches programs by channel id, and renders the selected channel through the shared app-epg-timeline component.
  • Channel rows no longer send preview EPG requests during initial category load. They stay empty until bulk EPG has been fetched once, then derive their current program and progress bar from the cached bulk map.
  • 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[]
  • The list-preview path uses this directly
  • The active-panel fallback maps the result into controlled EpgProgram[]

get_epg_info (bulk 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 / end are converted to ISO strings
  • startTimestamp / stopTimestamp are always populated
  • Programs are sorted by start time per channel
  • selectedItvId is 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:

  • selectedItvEpgPrograms
  • clearBulkItvEpgCache()

Bulk state is keyed by playlist so cached results do not leak between Stalker playlists.

Active panel flow

  1. User activates a live channel
  2. The component ensures playback link resolution as before
  3. The component calls ensureBulkItvEpg(168) on first use for the playlist
  4. selectedItvEpgPrograms() feeds app-epg-timeline
  5. 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

Before the first live-channel playback, channel rows do not fetch EPG at all.

After bulk EPG has been loaded once for the playlist, visible row previews are derived 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_mappings table under the playlist-scoped key stalker:{playlistId}:{channelId} (buildStalkerEpgMappingKey in libs/shared/interfaces/src/lib/epg-mapping-key.util.ts).
  • withStalkerEpg().applyMappedItvEpg(channelIds) batch-resolves mappings (one getEpgMappingsBatch IPC per new id set) and overlays the mapped XMLTV programs onto bulkItvEpgByChannel, so both the active panel and the row previews pick them up with no template changes. Overrides are re-merged whenever ensureBulkItvEpg replaces 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 (loadStalkerEpgItems for the detail panel, loadStalkerEpgBatch + prefetchEpgMappings for 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-timeline once the playback flow exists
  • optionally add category-level prefetch timing metrics for bulk EPG