docs(epg): document the programme guide and its release note

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
4grayandClaude Fable 5.1 committed 2026-09-06 13:26:08 +02:00
1 parent 5c2cf3f345
commit e5688ccd28
6 files changed
+107 -18

No files matched your search

+13
View File
@@ -0,0 +1,13 @@
---
type: feature
area: epg
issues: [171]
highlight: Programme guide, rebuilt
---
The programme guide now shows your playlist's own channels in their order,
with the current group or favorites one click away. Click a channel to
switch playback while the player stays on screen above the grid;
double-click switches and closes. Open it from the new Guide button, the
header, the command palette, or the G key. Hide channels without EPG and
pick comfortable or compact rows.
+3 -2
View File
@@ -362,7 +362,7 @@ This is an Nx monorepo with the following structure:
- **shared/marketing-fixtures** - Provider-neutral fictional movie metadata, live channel list and the generated channel-logo SVG renderer shared by the Xtream and Stalker marketing mocks (both serve `/assets/marketing/logo/<slug>.svg`)
- **shared/testing** - Shared test helpers
- **ui/components** - Reusable UI components (incl. channel list)
- **ui/epg** - EPG UI (timeline ribbon, multi-EPG, progress panel, program dialogs)
- **ui/epg** - EPG UI (timeline ribbon, programme guide grid via `EPG_GUIDE_SOURCE`, progress panel, program dialogs)
- **ui/playback** - Player UI (video/audio players)
- **ui/pipes** - Angular pipes
- **ui/remote-control** - Remote-control UI pieces
@@ -1471,7 +1471,8 @@ stream_id`); it drops `series_id`/`movie_id`, so the builder pins the
- XMLTV format support
- Background parsing in worker thread
- Stored in database for quick lookup
- Global display-time offset (`Settings.epgOffsetMinutes`, Settings → EPG, ±720 min, Electron only): display-only, provider data is never rewritten. Two equivalent forms in `libs/shared/interfaces/src/lib/epg-display-offset.util.ts` — `epgDisplayTimeMs` (shift the programme; `ui/epg` rendering via the `offsetMinutes` input, channel rows, dashboard/recording labels; the programme dialog and multi-EPG overlay read the store themselves) and `epgProviderClockMs` (shift "now"; every "currently airing" decision: the `GET_CURRENT_PROGRAMS_BATCH` lookup takes an explicit `nowMs` and `EpgService` tags its cache with the offset, Xtream/Stalker/M3U current-programme selection and previews, the unified collection resolver, dashboard progress, recording overlap). A consumer applies exactly one form per comparison. Contract: `docs/architecture/m3u-playlist-module.md` ("EPG display offset")
- Global display-time offset (`Settings.epgOffsetMinutes`, Settings → EPG, ±720 min, Electron only): display-only, provider data is never rewritten. Two equivalent forms in `libs/shared/interfaces/src/lib/epg-display-offset.util.ts` — `epgDisplayTimeMs` (shift the programme; `ui/epg` rendering via the `offsetMinutes` input, channel rows, dashboard/recording labels; the programme dialog and the programme guide read the store themselves) and `epgProviderClockMs` (shift "now"; every "currently airing" decision: the `GET_CURRENT_PROGRAMS_BATCH` lookup takes an explicit `nowMs` and `EpgService` tags its cache with the offset, Xtream/Stalker/M3U current-programme selection and previews, the unified collection resolver, dashboard progress, recording overlap). A consumer applies exactly one form per comparison. Contract: `docs/architecture/m3u-playlist-module.md` ("EPG display offset")
- Programme guide (Electron, M3U): `app-epg-guide` in `libs/ui/epg` fed by the host-provided `EPG_GUIDE_SOURCE`; the M3U host switches into guide mode (docked player strip, no sidebar/timeline, no remount) from the header action, the palette, the timeline's Guide button or `G`. Data: `EPG_GET_PROGRAMS_FOR_CHANNELS` / `EPG_GET_PROGRAM_COVERAGE` (keys resolved in main; manual mappings honoured). Contract: `docs/architecture/m3u-playlist-module.md` ("Programme guide").
- Manual EPG mapping (Electron only): right-click a channel in any list (M3U views, Xtream portal list, Stalker ITV sidebar, global favorites) → "Map EPG channel" attaches it to an uploaded-XMLTV channel; stored in `epg_channel_mappings` keyed by the M3U lookup key or a playlist-scoped portal key (`xtream:{playlistId}:{id}` / `stalker:{playlistId}:{id}`, helpers in `libs/shared/interfaces/src/lib/epg-mapping-key.util.ts`); resolved on every EPG path (single + batch IPC lookups, portal detail views, preview queues); dialog: `libs/ui/components/src/lib/channel-list-container/epg-mapping-dialog/`
**TMDB Metadata Enrichment** (opt-in):
+60 -2
View File
@@ -703,7 +703,7 @@ comparison; applying both doubles the shift.
header's range and progress) receives the offset as the `offsetMinutes`
input from each host; `app-channel-list-item`, the dashboard time range and
the recording detail shift their labels the same way. The programme dialog
and the multi-EPG overlay are opened imperatively, so they read
and the programme guide are opened imperatively, so they read
`SettingsStore.resolvedEpgOffsetMinutes` themselves.
- **Clock form** — every "currently airing" decision, so the row picked as
"now" is the one the UI renders as "now": the batched
@@ -1068,7 +1068,7 @@ class EpgService {
- EPG sidebar
- Collapsible inline EPG panel for internal players, persisted through the
shared `live-epg-panel-state` preference
- Multi-EPG modal view
- Programme guide (multi-channel grid) in guide mode — see "Programme guide" below
- Channel info overlay
- External player support (MPV, VLC) in Electron
- M3U archive/catch-up playback for supported replay schemes
@@ -1110,6 +1110,64 @@ class EpgService {
for a programme the user clicked, both hosts surface a
`EPG.TIMELINE.CATCHUP_FAILED` snackbar instead of doing nothing.
### Programme guide
`app-epg-guide` (`libs/ui/epg/src/lib/epg-guide/`) is a host-agnostic
multi-channel grid. It reads everything through the `EPG_GUIDE_SOURCE`
injection token (`epg-guide-source.ts`): the scope-resolved channel list
(`EpgGuideChannel { id, number, name, logoUrl, epgKey }`), the available scopes
(all / group / favorites), `loadPrograms(window)` and `loadCoverage(window)`
for a provider-clock time window, the active channel and `activate(id)`. An
optional `searchPrograms(query)` returns `EpgGuideSearchHit { channelId,
program }`, where `channelId` is `null` when the host cannot say which row a
hit belongs to. The M3U adapter is `M3uEpgGuideSourceService`
(`libs/playlist/m3u/feature-player/src/lib/epg-guide/`): channels come from
NgRx, `epgKey` uses the `tvg.id → tvg.name → name` chain, and the two bridge
reads `EPG_GET_PROGRAMS_FOR_CHANNELS` / `EPG_GET_PROGRAM_COVERAGE` resolve keys
in the main process (manual mappings first, then the metadata lookup shared
with the sidebar) and return programmes keyed by the requested key. Queries
are unscoped, like the timeline.
Guide mode is host layout, not an overlay: `VideoPlayerComponent.guideOpen`
hides the sidebar and the timeline, renders the guide, and CSS reflows the
untouched `app-web-player-view` into a 128 px docked strip
(`.content-container.is-guide`) beside `app-epg-guide-now-playing`; the strip
collapses to one line (`epg-guide:dock-collapsed`). Nothing remounts, so
playback and native-view Embedded MPV bounds survive. Entry points: the
workspace header action (`m3u-epg-guide`), the command palette, the Guide
button in the timeline toolbar (`EpgTimelineComponent.openGuide`) and the `G`
key on the player page. Player fullscreen, radio, recognised movies and the
PWA close or withhold it. Inside the guide: single click on a row or an
"on now" card switches the channel and keeps the guide open, double-click or
Enter switches and closes, other cards open the programme dialog; ↑/↓ ←/→
navigate, I details, N now, PgUp/PgDn day, Esc close — the keyboard controller
ignores keys on any element matching its interactive-target selector
(inputs, buttons, menu/option items, links, editable content) unless that
element carries `data-epg-guide-grid`, which the guide's own channel cells and
programme cards set so their `role="button"` does not shadow the grid's own
keys; a real control nested inside one (the catch-up button) still wins.
Density (`epg-guide:density`, comfortable 60 px / compact 44 px), zoom
(`epg-guide:zoom`, 120–480 px per hour, default 240) and the "Only with EPG"
toggle (`epg-guide:only-with-epg`) persist in localStorage; coverage is loaded
for the whole scope so the toggle never hides rows as they scroll in. The
guide reads the EPG display offset itself: the day axis is display time, the
request window is converted with `epgProviderClockMs`.
**Backend contract**: `EpgGuideQueryService`
(`apps/electron-backend/src/app/events/epg-guide-query.service.ts`) answers
both IPCs from one `guideWindowCondition()` predicate — when the request
carries `sourceUrls` (portal hosts only; the M3U host never does), a row
qualifies if it belongs to one of those sources OR carries no source at all
(legacy pre-per-source-tracking data), never if it belongs to a *different*
source. Both reads cap the requested channel-key batch
(`EPG_GUIDE_MAX_CHANNELS_PER_REQUEST` = 100 for programmes,
`EPG_GUIDE_MAX_COVERAGE_KEYS_PER_REQUEST` = 2000 for the cheaper coverage
probe) and respond with exactly the trimmed, de-duplicated, cap-respecting
requested keys: a key cut by the cap is absent from the answer, never present
with an empty list, so a caller can tell "queried, nothing found" apart from
"not queried at all". An invalid window (bad instants, `fromMs >= toMs`, no
usable keys) returns `{}`/`[]` rather than throwing.
### External Player Request Headers
The built-in web players inherit the playlist-level custom
+2 -2
View File
@@ -113,8 +113,8 @@ The self-hosted build is the browser PWA, not the Electron desktop app. Keep
these limitations explicit in UI, troubleshooting, and release notes:
- EPG/XMLTV is not supported in the PWA yet. Do not render live EPG panels,
multi-EPG shortcuts, or EPG-fetching flows in browser/PWA mode, and do not use
EPG as the readiness signal for Docker.
programme-guide entry points, or EPG-fetching flows in browser/PWA mode, and
do not use EPG as the readiness signal for Docker.
- The PWA does not use the Electron SQLite database or DB worker. Playlist
metadata uses `PlaylistsService` with IndexedDB; Xtream favorites, recently
viewed items, playback positions, and cached collection snapshots use
+3 -4
View File
@@ -266,8 +266,7 @@ The Electron window hides the native title bar on all desktop platforms
The controls are mounted once in `app-root` (not inside the workspace
header) as a `position: fixed` top-right overlay so they stay clickable
above full-window content such as the multi-EPG cdk overlay and Material
dialog backdrops — the same behavior as the macOS traffic lights. Because
above full-window content such as Material dialog backdrops — the same behavior as the macOS traffic lights. Because
CDK overlays render as popovers in the browser top layer (above any
z-index), the component host is itself a `popover="manual"` element: it
enters the top layer on init and re-enters it (hide + show) whenever
@@ -402,8 +401,8 @@ Layout integration:
`AppComponent`, same mechanism as `dark-theme`) — body-level so rules
also reach cdk-overlay content rendered outside `app-root`.
2. `apps/web/src/styles.scss` reserves `padding-right: 150px` in
top-aligned drag regions (`.workspace-header`, multi-EPG
`#epg-navigation`) for the 3 × 46px button strip.
the top-aligned drag region (`.workspace-header`) for the 3 × 46px
button strip.
3. Button colors follow the theme via CSS variables (`--app-on-surface`,
`--app-hover-overlay`); the close button uses the Windows-style red
hover (`#e81123`). No theme IPC is involved.
@@ -164,8 +164,14 @@ Location: `libs/ui/epg/src/lib/epg-guide/`. Every production file stays under
per-channel status `idle | loading | loaded`, coverage per day, reset on
scope or source change. Batches are capped at 100 channels per IPC.
- `epg-guide-keyboard.controller` — ↑/↓ channel, ←/→ programme, Enter play,
I details, N now, PgUp/PgDn day, Esc close. Ignores events from inputs and
while a dialog is open.
I details, N now, PgUp/PgDn day, Esc close. Ignores events whose target
matches its interactive-element selector (inputs, buttons, menu/option
items, links, editable content) and while a dialog is open. The guide's own
channel cells and programme cards carry `role="button"` for assistive tech,
which would otherwise match that selector too — they opt back in with
`data-epg-guide-grid` so the controller still owns their keys; a real
control nested inside one (the catch-up button) is the closest match and
still wins over the grid attribute.
- `epg-guide-now-playing.component` — the right part of the docked player
strip: channel, programme title, description, progress, Close (Esc) and
Collapse. Rendered by the host beside the player, because the strip itself
@@ -198,12 +204,12 @@ token set; card radius is 8 px (timeline uses 11 px on 96 px cards).
initial scope follows `activeView()` and the sidebar's selected group.
`epgKey` uses the `stream-resolver` chain `tvg.id → tvg.name → name`; radio
and movie entries are excluded. `loadPrograms`/`loadCoverage` call the new
bridge methods with the playlist's `sourceUrls`, as
`EpgService.getCurrentProgramsForChannels` does, so playlist-scoped EPG wins.
`activate` dispatches `ChannelActions.setActiveChannel`; `activeChannelId`
mirrors `activeChannel()`. `searchPrograms` uses `EPG_SEARCH_PROGRAMS` and
filters results to the scope's resolved channel ids. `catchUp` is not wired
in this sub-project.
bridge methods unscoped (every imported EPG source), exactly like the
timeline's `getChannelPrograms`; `sourceUrls` stays in the IPC contract for
portal hosts. `activate` dispatches `ChannelActions.setActiveChannel`;
`activeChannelId` mirrors `activeChannel()`. `searchPrograms` uses
`EPG_SEARCH_PROGRAMS` unfiltered (result click opens the programme dialog,
as the previous overlay did). `catchUp` is not wired in this sub-project.
- Entry points, all toggling the same signal:
1. Workspace header action (`m3u-epg-guide`, icon `grid_view`, label
"Programme guide"), highlighted while open.
@@ -236,6 +242,18 @@ lines).
`string[]` of requested ids with at least one programme in the window. Same
resolution, then `SELECT DISTINCT channel_id … WHERE start < to AND stop >
from`. 2000 keys cost one resolution and one query, not 2000.
- Both responses key exactly the trimmed, de-duplicated, cap-respecting
requested ids — never the raw request. An id cut by the per-request cap
(100 for programmes, 2000 for coverage) is absent from the answer, never
present with an empty list, so a caller can tell "queried, nothing found"
apart from "not queried at all"; an invalid window (bad instants,
`fromMs >= toMs`, no usable ids) returns `{}`/`[]`.
- When a request carries `sourceUrls`, `guideWindowCondition()` folds the
source scoping into the same query instead of a second scoped/unscoped
pass: a row qualifies if it belongs to one of the requested sources OR
carries no source at all (legacy rows predating per-source EPG tracking),
never if it belongs to a *different* source. The M3U host never sets
`sourceUrls`, so this only matters for portal hosts.
- Bridge: `getEpgProgramsForChannels` and `getEpgProgramCoverage` in
`ElectronBridgeApi`, `main.preload.ts`, `EpgRuntimeBridgeService`;
capability `supportsEpgGuide` in `RuntimeCapabilitiesService` replaces