diff --git a/README.md b/README.md index bebe37717..b1e67834d 100644 --- a/README.md +++ b/README.md @@ -31,6 +31,7 @@ The application is a cross-platform, open-source project built with Electron and - EPG support (TV Guide) with detailed information - TV archive/catchup/timeshift functionality - Group-based channel list +- Read-only M3U channel details from the channel context menu - Favorite channels management - Global favorites aggregated from all playlists - HTML video player with HLS.js support or Video.js-based player diff --git a/apps/stalker-mock-server/README.md b/apps/stalker-mock-server/README.md index 3067afc5e..68a71617e 100644 --- a/apps/stalker-mock-server/README.md +++ b/apps/stalker-mock-server/README.md @@ -67,8 +67,8 @@ All endpoints are served at `GET /portal.php?action=&...` matching the r | `get_ordered_list` | Paginated content list; if `movie_id` is present → returns seasons | | `create_link` | Returns a real public HLS stream URL for playback | | `favorites` | Add / remove / get favorites (in-memory, resets on restart) | -| `get_short_epg` | EPG program list for a channel (`ch_id`) | -| `get_epg_info` | Alias for `get_short_epg` | +| `get_short_epg` | Current-and-upcoming EPG window for a channel (`ch_id`, `size`) | +| `get_epg_info` | Bulk EPG keyed by channel id for a requested `period` window | ## Cover Images @@ -99,6 +99,18 @@ nx e2e web-e2e --grep "@stalker" The test suite uses `00:1A:79:00:00:01` (default scenario) for most tests, and calls `POST /reset` in `beforeEach` to ensure a clean state between tests. +## EPG Behavior + +The mock server generates a 7-day EPG schedule for every ITV channel using +2-hour slots starting at the current UTC day boundary. + +- `get_short_epg` returns the current program and upcoming items from that + schedule, limited by `size` +- `get_epg_info` returns bulk data in the shape + `{ js: { data: Record } }` +- `get_epg_info` filters the bulk response from the current UTC day start through + `now + period` + ## Architecture See [`docs/architecture/stalker-mock-server.md`](../../docs/architecture/stalker-mock-server.md) for full implementation details. @@ -123,6 +135,7 @@ apps/stalker-mock-server/ │ ├── get-seasons.handler.ts │ ├── create-link.handler.ts │ ├── favorites.handler.ts +│ ├── get-epg-info.handler.ts │ ├── get-short-epg.handler.ts │ └── get-genres.handler.ts ├── project.json diff --git a/docs/architecture/stalker-epg.md b/docs/architecture/stalker-epg.md index 6b64298ee..26a6ab775 100644 --- a/docs/architecture/stalker-epg.md +++ b/docs/architecture/stalker-epg.md @@ -1,6 +1,7 @@ # Stalker Portal EPG Architecture -This document describes the EPG (Electronic Program Guide) implementation for Stalker/Ministra portal live TV (ITV) streams. +This document describes the current EPG implementation for Stalker/Ministra ITV +channels in IPTVnator. Related architecture docs: @@ -9,48 +10,72 @@ Related architecture docs: ## Overview -The Stalker ITV live stream layout displays EPG data in the right panel when a live channel is playing. EPG is fetched per-channel using the Stalker `get_short_epg` API action, which returns the current program plus the next ~5 upcoming programs. The response is mapped to the shared `EpgItem` interface and rendered by the reusable `EpgViewComponent`. +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-list` 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 │ │ Video Player │ │ EPG Panel │ │ -│ │ (channels) │ │ (WebPlayerView) │ │ (EpgViewComponent) │ │ -│ │ │ │ │ │ │ │ -│ │ click ──────┼────┼──► playChannel() ────┼────┼──► loadEpgFor...() │ │ -│ └──────────────┘ └──────────────────────┘ └─────────────────────┘ │ -│ │ │ │ -└──────────────────────────────┼────────────────────────────┼───────────────┘ - │ │ - ▼ ▼ - ┌──────────────────────┐ ┌────────────────────────┐ - │ StalkerStore │ │ StalkerStore │ - │ fetchLinkToPlay() │ │ fetchChannelEpg() │ - └──────────┬───────────┘ └────────────┬───────────┘ - │ │ - ▼ ▼ - ┌──────────────────────────────────────────────────────┐ - │ Stalker Portal API │ - │ action=create_link action=get_short_epg │ - └──────────────────────────────────────────────────────┘ +```text +┌────────────────────────────────────────────────────────────────────────────┐ +│ 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-list │ +│ after first bulk load └── empty/unsupported → short fallback │ +└────────────────────────────────────────────────────────────────────────────┘ + │ │ + ▼ ▼ +┌────────────────────────────────────────────────────────────────────────────┐ +│ with-stalker-epg.feature │ +│ │ +│ bulkItvEpgByChannel: Record │ +│ 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` (per-channel) +### `get_short_epg` (active-panel fallback) -**Request:** -``` -GET load.php?type=itv&action=get_short_epg&ch_id={channel_id}&JsHttpRequest=1-xml -``` -Uses standard Stalker auth headers (Bearer token + MAC cookie). +**Request** + +```text +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** -**Response:** ```json { "js": { @@ -64,149 +89,179 @@ Uses standard Stalker auth headers (Bearer token + MAC cookie). "time_to": "2025-01-15 14:30:00", "duration": "1800", "start_timestamp": "1736949600", - "stop_timestamp": "1736951400", - "t_time": "14:00", - "t_time_to": "14:30" + "stop_timestamp": "1736951400" } ] } } ``` -**Notes:** -- If the channel has no `xmltv_id` set on the server, returns an empty array -- Response normalization handles both `{ js: { data: [...] } }` and `{ js: [...] }` formats -- No backend (Electron IPC) changes needed — the generic `stalker.events.ts` handler forwards any `params` to the portal URL +**Notes** -### `get_epg_info` (bulk — reserved for future use) +- 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** + +```text GET load.php?type=itv&action=get_epg_info&period={hours}&JsHttpRequest=1-xml ``` -Returns EPG for all channels for a given time period. Currently unused but the enum value `StalkerPortalActions.GetEpgInfo` is defined for future bulk EPG features. +**Current usage** + +- Fetched once with `period=168` +- Scoped to the current playlist session +- Not refetched on active-channel change + +**Expected response** + +```json +{ + "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 -### Stalker EPG → `EpgItem` Interface +### Fallback data (`get_short_epg`) → `EpgItem` -| Stalker field | `EpgItem` field | Notes | -|--------------------|--------------------|-------| -| `id` | `id` | Converted to string | -| `ch_id` | `channel_id` | Falls back to passed `channelId` | -| `name` | `title` | | -| `descr` | `description` | | -| `time` | `start` | Full datetime string | -| `time_to` | `end`, `stop` | Both set to same value | -| `start_timestamp` | `start_timestamp` | Unix timestamp as string | -| `stop_timestamp` | `stop_timestamp` | Unix timestamp as string | -| _(n/a)_ | `epg_id` | Empty string | -| _(n/a)_ | `lang` | Empty string | +The short EPG path now exists only for the active-panel fallback flow. -### `EpgItem` Interface +Key mapped fields: -```typescript -// libs/shared/interfaces/src/lib/epg-item.interface.ts -interface EpgItem { - id: string; - epg_id: string; - title: string; - lang: string; - start: string; - end: string; - stop: string; - description: string; - channel_id: string; - start_timestamp: string; - stop_timestamp: string; -} -``` +| 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-list` 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 +### Key files | File | Purpose | -|------|---------| -| `libs/shared/interfaces/src/lib/stalker-portal-actions.enum.ts` | `GetShortEpg`, `GetEpgInfo` enum values | -| `libs/portal/stalker/data-access/src/lib/stalker.store.ts` | `fetchChannelEpg()` method | -| `libs/portal/stalker/feature/src/lib/stalker-live-stream-layout/stalker-live-stream-layout.component.ts` | EPG signals + `loadEpgForChannel()` | -| `libs/portal/stalker/feature/src/lib/stalker-live-stream-layout/stalker-live-stream-layout.component.html` | `` integration | -| `libs/ui/shared-portals/src/lib/epg-view/epg-view.component.ts` | Shared EPG display component | +| --- | --- | +| `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-list` wiring | +| `libs/portal/stalker/feature/src/lib/stalker-live-stream-layout/stalker-live-stream-layout.component.html` | active panel template | +| `libs/portal/stalker/feature/src/lib/stalker-collection-channels-list/stalker-collection-channels-list.component.ts` | row preview loading | +| `libs/ui/shared-portals/src/lib/epg-list/epg-list.component.ts` | shared controlled EPG list with date navigator | -### StalkerStore.fetchChannelEpg() +### Store API -Location: `libs/portal/stalker/data-access/src/lib/stalker.store.ts` (in `withMethods`) +The Stalker EPG feature exposes one bulk method plus the short-EPG fallback: -```typescript -async fetchChannelEpg(channelId: number | string): Promise +```ts +fetchChannelEpg(channelId: number | string, size?: number): Promise +ensureBulkItvEpg(periodHours = 168): Promise ``` -- Sends `get_short_epg` request with `ch_id` param -- Supports both full Stalker portals (authenticated via `StalkerSessionService`) and simple portals (direct IPC) -- Returns mapped `EpgItem[]` or empty array on failure -- No store state mutation — returns data directly to the component +It also exposes: -### StalkerLiveStreamLayoutComponent EPG Integration +- `selectedItvEpgPrograms` +- `clearBulkItvEpgCache()` -The component manages EPG state locally with signals: +Bulk state is keyed by playlist so cached results do not leak between Stalker +playlists. -```typescript -readonly epgItems = signal([]); -readonly isLoadingEpg = signal(false); -``` +### Active panel flow -**Flow:** -1. User clicks a channel in the sidebar → `playChannel(item)` -2. `fetchLinkToPlay()` gets the stream URL -3. If using embedded player, `streamUrl` is set and `loadEpgForChannel(item.id)` is called -4. `loadEpgForChannel()` sets loading state, calls `stalkerStore.fetchChannelEpg()`, updates `epgItems` -5. Template renders `` or a loading spinner +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-list` +5. If the selected channel has no bulk programs, the component falls back to + `get_short_epg` -### EpgViewComponent (shared) +The active panel no longer uses local EPG pagination or a "Load more" button. -Location: `libs/ui/shared-portals/src/lib/epg-view/` +### Channel row preview flow -Reusable component shared between Stalker and Xtream live stream layouts: -- **Input:** `epgItems: EpgItem[]` -- Displays program list with time, title, and info button -- Highlights current program with green progress bar -- Handles empty state (shows "EPG not available" message) -- Info button opens `EpgItemDescriptionComponent` dialog with title and description +Before the first live-channel playback, channel rows do not fetch EPG at all. -**Current program detection:** -```typescript -isCurrentProgram(item: EpgItem): boolean { - const now = new Date().getTime(); - const start = new Date(item.start).getTime(); - const stop = new Date(item.stop ?? item.end).getTime(); - return now >= start && now <= stop; -} -``` +After bulk EPG has been loaded once for the playlist, visible row previews are +derived locally from `bulkItvEpgByChannel`: -This works with Stalker's datetime format (`"2025-01-15 14:00:00"`) because `new Date()` parses it correctly. +- 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 same authentication pattern as all Stalker API calls: +EPG requests follow the standard Stalker request path: -| Portal Type | Auth Method | -|-------------|-------------| -| **Full Stalker** (`isFullStalkerPortal: true`) | `StalkerSessionService.makeAuthenticatedRequest()` — handles token refresh and retry on 401 | -| **Simple Stalker** | Direct IPC via `DataService.sendIpcEvent(STALKER_REQUEST, ...)` — no auth headers | +| 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. ## Future Enhancements -### Bulk EPG in Channel List Sidebar - -Use `get_epg_info` with `period=3` to pre-fetch current program titles for all channels when a category is selected: -- Call `get_epg_info` after category selection -- Build a `Map` -- Show current program name below channel title in the sidebar -- Display progress bar per channel item - -This is a separate task due to Stalker's lazy-loaded channel pagination model. - -### EPG Auto-Refresh - -Currently EPG is fetched once per channel selection. A future enhancement could add a timer to refresh EPG data periodically (e.g., every 5 minutes) to keep the "current program" indicator accurate during long viewing sessions. +- add cache refresh / invalidation for long-running live sessions +- add Stalker catch-up support to `app-epg-list` once the playback flow exists +- optionally add category-level prefetch timing metrics for bulk EPG diff --git a/docs/architecture/stalker-mock-server.md b/docs/architecture/stalker-mock-server.md index a2b59fee4..1205f7651 100644 --- a/docs/architecture/stalker-mock-server.md +++ b/docs/architecture/stalker-mock-server.md @@ -152,9 +152,9 @@ The stream URL is selected from a pool of 4 real public HLS test streams. The ch "id": "1", "name": "Channel Name: Program Title", "start": "2026-02-21T10:00:00.000Z", - "stop": "2026-02-21T10:30:00.000Z", + "stop": "2026-02-21T12:00:00.000Z", "start_timestamp": 1740128400, - "stop_timestamp": 1740130200, + "stop_timestamp": 1740135600, "descr": "...", "category": "News" } @@ -163,7 +163,37 @@ The stream URL is selected from a pool of 4 real public HLS test streams. The ch } ``` -EPG programs are generated as 30-minute slots spanning 3 hours past to 3 hours future relative to the time of generation. +`get_short_epg` returns the current program and upcoming items from the +generated schedule, limited by the requested `size`. + +### `get_epg_info` + +```json +{ + "js": { + "data": { + "10000": [ + { + "id": "1", + "name": "Channel Name: Program Title", + "start": "2026-02-21T10:00:00.000Z", + "stop": "2026-02-21T12:00:00.000Z", + "start_timestamp": 1740128400, + "stop_timestamp": 1740135600, + "descr": "...", + "category": "News" + } + ] + } + } +} +``` + +`get_epg_info` returns bulk EPG keyed by channel id and filters the generated +7-day schedule from the current UTC day start through `now + period`. + +EPG programs are generated as 2-hour slots across 7 days for each channel, +starting at the current UTC day boundary. ## Scenarios diff --git a/docs/architecture/stalker-portal.md b/docs/architecture/stalker-portal.md index a5e0ea1eb..e3ef1247b 100644 --- a/docs/architecture/stalker-portal.md +++ b/docs/architecture/stalker-portal.md @@ -138,7 +138,15 @@ See full backend and web-remote flow in [Remote Control Architecture](./remote-c ## EPG Integration -EPG in Stalker ITV uses `get_short_epg` and shared `EpgViewComponent`. Full details are documented in [Stalker Portal EPG Architecture](./stalker-epg.md). +Stalker ITV now splits EPG usage: + +- active channel panel: bulk `get_epg_info` cached once per playlist and rendered + through shared `app-epg-list` +- 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