mirror of
https://github.com/4gray/iptvnator.git
synced 2026-10-11 19:46:16 -08:00
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:
1 parent
5c2cf3f345
commit
e5688ccd28
6 files changed
+107
-18
No files matched your search
@@ -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.
|
||||
@@ -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):
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in new issue
Block a user