* docs(epg): add programme guide redesign spec for the M3U host
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
* docs(epg): add programme guide implementation plan
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
* feat(epg): add window-scoped guide programme queries
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
* fix(epg): harden guide query scoping, caps and row mapping
- Scoped guide programme/coverage queries now include legacy
(unsourced) rows via source_url IN (...) OR IS NULL OR '',
mirroring EpgQueryService's legacy fallback.
- getProgramsForChannels/getProgramCoverage build their result from
the normalized, capped window.channelIds instead of the raw
request, so a key cut by the cap is absent rather than [] — an
invalid window now returns {}. Truncation logs counts only.
- Split the 100-channel guide cap from a new 2000-key coverage cap,
and cap sourceUrls at 50; normalizeGuideWindow takes the cap as a
parameter and moved (with guideWindowOverlapSqlText) into
epg-guide-window.util.ts.
- Extracted shared row mapping (toEpgProgramFromRow/isValidEpgProgram)
into epg-program-row.util.ts, used by both EpgQueryService and
EpgGuideQueryService so invalid start/stop rows are dropped
identically in both.
- Added a real-SQLite-backed test for the overlap predicate's exact
text, plus per-key array copies in the response.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
* fix(epg): render the guide predicate in tests and document its scope
Correct the guide query's JSDoc: it runs one query accepting the union
of requested-source and unsourced legacy rows, unlike EpgQueryService's
two-query scoped-then-legacy fallback. Replace the hand-maintained
plain-SQL twin of the Drizzle overlap predicate with a rendered copy of
the real predicate (SQLiteSyncDialect().sqlToQuery) in the spec, add a
source-scoping case, and drop the now-redundant operator-sequence test.
warnIfTruncated reuses uniqueTrimmedStrings and names which read
(programme/coverage) was truncated. Rename epg-query.service.ts's local
EpgProgramRow to EpgProgramSelectRow so it isn't confused with the
shared EpgProgramRow type, and document getProgramCoverage like its
sibling.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
* feat(epg): expose guide programme and coverage reads over the bridge
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
* docs(epg): separate coverage chunk size in the guide plan
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
* feat(epg): add guide source contract, day layout maths and preferences
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
* fix(epg): key guide IPC answers by trimmed, present keys only
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
* docs(epg): guide search hits carry a row id
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
* fix(epg): make guide geometry DST-safe and tighten the contract
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
* feat(epg): cache guide programmes per day with batched loading
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
* feat(epg): add guide keyboard navigation controller
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
* fix(epg): make guide programme cache robust to first-run effects and coverage failures
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
* feat(epg): add the programme guide grid components
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
* feat(epg): add a Guide button to the timeline toolbar
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
* feat(m3u): adapt the playlist channel list to the guide contract
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
* fix(m3u): guard the guide's initial group scope and track language changes
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
* feat(m3u): open the programme guide in place with a docked player
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
* fix(epg): scope guide keys to the grid, clip the now-line and re-measure on resize
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
* refactor(epg): remove the multi-EPG overlay and the channel-range IPC
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
* docs(epg): document the programme guide and its release note
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
* i18n(epg): translate the programme guide
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
* fix(m3u): let the guide own the keyboard and gate its entry points
While the programme guide is open the docked player carries
`data-player-shortcuts-suspended`, which `ControlsShortcuts` now honours
alongside `[inert]` — the arrows moved the player's volume instead of the
guide's row focus. The external-player strip loses its Collapse toggle
(nothing to reveal, no preference to write), the header action and its
palette command report `disabled` when the guide cannot open, the docked
strip derives its programme from the active channel's own schedule instead
of the retained NgRx value, switching playlists closes the guide, and the
collapsed strip can reach 48 px on phones.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
* fix(m3u): keep the sidebar mounted while the guide is open
Guide mode wrapped the sidebar in `@if (!guideOpen())`, so opening the
guide destroyed `app-channel-list-container`, whose `ngOnDestroy`
dispatches `resetActiveChannel()`. That cleared the active channel, which
unmounted the block hosting `app-epg-guide` and tripped the
`!canOpenGuide()` effect into closing the guide again: the guide never
appeared and the page dropped to "Please select a channel".
The sidebar now stays mounted and is hidden with
`.sidebar--guide-hidden` plus `inert`, so it is neither focusable nor read
by assistive technology while the guide owns the layout. Hiding also
preserves the channel list's scroll position across guide toggles.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
* test(e2e): cover the programme guide flow
Imports a two-channel playlist with XMLTV, opens the guide from the
timeline toolbar and asserts the row list, the "Only with EPG" filter, a
channel switch that keeps the guide open, the hidden-but-mounted sidebar,
and that the player element survives both the mode and channel switches.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
* chore(epg): tidy guide docs, palette gating and the unbound output
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
* fix(epg): match guide favorites by channel URL and skip re-activating the playing row
Favorites are persisted by channel URL (FavoritesActions.updateFavorites),
so the Favorites scope compared the wrong key; the id stays as a legacy
fallback. A double-click arrives as click, click, dblclick and each
activate restarts playback, so the guide now leaves the already-playing row
alone and the commit path only closes.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
* perf(epg): let the guide window predicate use the programme time index
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
* fix(m3u): stabilise guide row identity, seed the sidebar group and provide translations in every player fixture
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
* refactor(epg): split the guide shell, add a roving focus model and offset-aware search times
The shell component now owns rows, focus and the viewport only: the day,
zoom, density, filters, clock and day geometry move to EpgGuideViewState,
and every programme-dialog entry point to EpgGuideDialogController.
Keyboard navigation is reachable by assistive technology: exactly one grid
cell carries tabindex="0" (the focused cell, else the playing row's channel
cell, else the first row's), the guide moves DOM focus with it after each
handled key, a click hands the roving index to the clicked cell, and the
viewport, rows and cells expose grid/row/gridcell roles.
Search results were formatting raw provider instants, so they ignored the
EPG display offset; they go through getProgramTimeMs like every other time
the guide renders.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
* fix(m3u): make guide row ids collision-proof and gate the G shortcut
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
* fix(epg): keep guide keys on the grid, reconcile focus with filtered rows and wrap the toolbar
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
* docs(epg): describe guide row ids as scope-local
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
* fix(epg): clear guide search on scope change, match the active duplicate by url, keep failed coverage unknown
Search hits carry scope-local row ids, so a scope change drops them.
Two playlist entries can share an id but not a stream, so the active row
is matched by id + url before falling back to the id. A failed coverage
query now rejects instead of answering an empty set, which the guide
already treats as "coverage unknown" (every row stays visible).
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
* fix(epg): tell duplicate guide rows apart by group, keep G out of dialogs, use prototype-safe answers
The store spreads the selected channel, so the active row is matched by
id, url, group and name before widening; G no longer closes the guide from
a dialog or menu; guide answers use null-prototype records so a key named
__proto__ stays an own property.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
* fix(epg): let coverage reject on lookup failures and compare whole entries for the active guide row
EpgQueryService.getChannelMetadata swallowed database errors into {}, so the
guide's coverage read could publish an empty set after a transient failure;
the guide now uses the strict resolveChannelMetadata (getChannelMetadata is
the fail-soft wrapper around it). The active guide row is matched on the
whole channel entry (all fields except the reducer-rewritten epgParams)
before widening to url and id, so copies that differ only in playback
headers or logo are told apart.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
* fix(epg): let the guide return catch-up to live and normalise programme-search rows
The guide source contract gains an optional livePlayback signal: while the
host plays a catch-up URL, the active row may be activated again, which is
how the M3U host returns to live. EPG_DB_SEARCH_PROGRAMS now maps the raw
snake_case rows to the EpgProgram shape the bridge promises (plus the joined
channel name), so search hits resolve their channel and keep descriptions.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
* fix(epg): name search hits, keep guide coverage strict on mapping failures
- Search results and the unresolved programme dialog show the channel's
display name (playlist row name, else the XMLTV display name the search
joined in) instead of the raw XMLTV id.
- The guide coverage read resolves manual mappings through a strict variant
that rejects on database failure, so a mapped channel can never be reported
as uncovered and hidden by "Only with EPG".
- Architecture doc describes the tiered active-row resolution.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
* fix(epg): offer the Guide action in the list view too
The EPG list view mirrors the timeline's input/output contract, but the Guide
action was bound only in the timeline branch, so Settings → EPG → Guide view =
List lost the in-panel entry point. The list toolbar now carries the same
icon-only Guide button behind `guideAvailable`/`openGuide`, and the M3U
host binds it in both branches.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
---------
Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
* fix(portals): preserve live channel navigation while browsing
* test(portals): await media source assertion in remote E2E
* fix(xtream): capture destination queue for live auto-open
Preserve current-program context and enabled actions in narrow channel rows while aligning loaded rows, skeletons, and virtual-scroll geometry across M3U, Xtream, Stalker, Favorites, and Recent views.
Moves the fictional movie catalog into `libs/shared/marketing-fixtures` so the
Xtream and Stalker mocks describe the same titles, and adds 20 rendered posters
plus the shared fixture types behind them.
Supporting changes made while getting it green:
- `shared-marketing-fixtures` is classified Tier B in the coverage policy. Not
Tier A: it is fictional fixture data, so a statement percentage over it means
nothing, and a Tier A entry would pull it into the merged coverage map and the
ratchet. Tier B still runs its spec in CI. `stalker-mock-server` needs no entry
of its own — it is already Tier C and the Tier B/C runner falls back to
`pnpm nx test <project>`, so its new `marketing-poster-url.spec.ts` runs.
- Two release-capture defects the catalog reorder introduced, both fixed in
`tools/release/capture-app-driver.ts`:
- VOD stream ids are `MARKETING_VOD_STREAM_ID_BASE + index` and the generator
now lists the showcase movies first, so 62000-62002 became Black Harbor, The
Paper Astronaut and Summer Static while the dashboard seeding still mapped
those ids to the previous titles' backdrops.
- the raw `tsx` spawn of the Xtream mock lacked `--tsconfig
tsconfig.base.json`, so the mock could not resolve
`@iptvnator/shared/marketing-fixtures` and the capture never started. Both
mock projects' own serve targets already passed the flag.
Adds contract-focused regression coverage for the Electron HTTP server,
remote-control events, settings events, and managed download paths, and makes
Tier A coverage fail closed when instrumentation fails or a runtime-owning
production file disappears from a project or from the merged Istanbul report.
The old `coverage:ci` exited 0 despite a `Failed to collect coverage`
diagnostic: libs/m3u-state/src/lib/effects.ts was simply absent from the merged
map. All 30 Tier A reports are now required, the merged map covers 710 files,
and effects.ts is reported as 0/159 instead of silently disappearing.
Also fixes remote static-file path containment for encoded, malformed, NUL,
POSIX and Win32-style traversal inputs, with behavior-preserving testability
seams.
Statements 69.27% -> 69.54%; http-server.ts 0% -> 90.21%,
remote-control.events.ts 0% -> 96.55%, settings.events.ts 59.25% -> 96.29%.
Remove redundant LIVE, VOD, and SERIES badges from homogeneous Xtream and Stalker catalog grids while preserving mixed-content badges and type-driven grid behavior.
* feat(videojs): add feature-flagged shared controls
Rebuild the Video.js shared-controls integration on the current player lifecycle with Tech rebinds, source-scoped tracks, reset ordering, volume preservation, diagnostics gating, and default-off compatibility.
Credits and supersedes the stacked implementation proposed in #1153.
Co-authored-by: Lars Emig <lars.emig@pickware.de>
* fix(videojs): harden MPEG-TS reset lifecycle
---------
Co-authored-by: Lars Emig <lars.emig@pickware.de>