* feat(settings): split settings into per-section pages with an unsaved-changes bar
Replace the single scrolling settings page with routed section pages
(/workspace/settings/:section): the context-panel rail links each section,
only the active section renders, and unknown or capability-gated sections
redirect to General. The shared form lives on the parent component, so
staged edits survive section switches; a floating unsaved-changes bar
(Save/Discard) replaces the always-visible footer Save button. Rail links
navigate with replaceUrl so Back still leaves settings in one step.
Along the way:
- delete the unreachable settings dialog mode and the dead
AppPortalNavigationActionsService with both of its never-injected DI
tokens (PORTAL_NAVIGATION_ACTIONS, PLAYLIST_PLAYER_ACTIONS)
- delete the scroll-spy directive and pendingScrollTarget plumbing
- revive the EPG panel's "Open EPG settings" empty-state button as a deep
link to /workspace/settings/epg; the M3U player now reports
m3u-needs-setup only when the channel has no programmes and no EPG
source exists in settings or on the playlist itself
- load TMDB cache stats when the Metadata page opens (the section
component now only exists while its page is open)
- add SETTINGS.UNSAVED_CHANGES / SETTINGS.DISCARD_CHANGES to all 19 locales
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* feat(settings): confirm before leaving with unsaved changes
Add settingsUnsavedChangesGuard (canDeactivate on the :section route) with
a three-action dialog: save and leave, leave without saving, keep editing.
The guard only intercepts leaving the settings AREA — section switches
share the one settings form and pass unconditionally, so the dialog can
never nag while moving between pages. A failed save cancels the navigation
instead of silently dropping the edits it promised to keep; leaving
without saving also reverts the live theme preview. Save-and-leave is
disabled while the form is invalid, with a hint explaining why.
New SETTINGS.UNSAVED_DIALOG_* keys in all 19 locales.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(settings): stage cover size and EPG view mode; adapt e2e to section pages
Cover size and EPG view mode were the only two controls that persisted
eagerly on click, which made Discard (and leave-without-saving) unable to
revert them: hydrateFromStore() faithfully reloaded the just-persisted
edit. They now stage in the form like every other setting and reach the
store on Save. Review finding by Greptile (P1) and Codex.
E2E suites that walk through settings are updated for one-section-page
rendering (epg, backup-roundtrip, xtream-epg, remote-control) and for the
staged cover size (downloads asserts the dataset after Save); the EPG icon
fallback test saves before leaving settings so the new unsaved-changes
dialog does not block its navigation.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
* fix(build): include shared UI stylesheets in Nx cache inputs
`libs/ui/styles` held shared SCSS partials but had no `project.json`, so its
files belonged to no Nx project and were absent from every task hash. Editing
a partial and running `pnpm nx build web` reported 4 of 4 tasks cached and
shipped the previous CSS — a silent wrong build rather than a failure.
Nx derives its project graph from TypeScript imports only, so a relative Sass
`@use` that crosses a project root creates no edge. Verified directly: after
adding the project but before declaring anything, `ui-styles` still had zero
dependents in the graph.
Make it the `ui-styles` project (no targets — it exists to be hashed) and
declare `implicitDependencies` on the 8 consumers. Chosen over adding the path
to `sharedGlobals`, which would put shared styles into every project's hash and
make a one-line SCSS tweak mark the whole workspace affected. A styles edit now
marks 15 projects affected and leaves electron-backend, website, the mock
servers and the shared libs alone.
`libs/ui/styles` was the only projectless directory holding files under `libs/`
or `apps/`.
Add `pnpm run styles:inputs:validate` to keep it closed: it resolves every
relative stylesheet import against Nx's real project graph and fails when one
escapes the input closure of a build that compiles it, naming the project to
declare. It exits 1 with 21 diagnostics on the pre-fix tree. Imports of
`apps/web/src/nav-list.scss` are deliberately accepted — `web` already hashes
that file, and a lib -> app edge would make the graph cyclic.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* fix(build): spawn git without a shell in the stylesheet check
`execSync("git ls-files '*.scss'")` runs through `cmd.exe` on Windows, where
single quotes are literal characters rather than quoting. Git received the
pathspec with the quotes intact, matched nothing and exited 0, so
`styles:inputs:validate` reported success after checking zero stylesheets —
silently disabling the check for Windows developers while staying green.
Spawn with `execFileSync` so no shell is involved and git expands its own
pathspec; verified to return the identical 133 files.
Both this and the eslint glob trap next to it in the docs report success while
covering nothing, so also make an empty scan fail rather than pass: the
workspace always contains SCSS, and a listing that returns none means the scan
broke.
Reported by Codex review on #1360.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* refactor(styles): move nav-list partial into ui-styles (#1361)
* fix(build): count every target of a comma-separated Sass @import
`@import` is the only rule that takes a list, and the scan read just its
first target. A later entry crossing an Nx project boundary escaped the
cache key while the check still reported success — the same silent-pass
failure the tool exists to prevent.
Parse every target of an `@import` list. The obvious "read all quoted
strings" fix trades one silent gap for a phantom one, so the rule decides:
`@use`/`@forward` load exactly one module and a quoted string after it is
`with (...)` configuration, and `url(...)` stays a plain CSS import the
browser resolves at runtime. Neither is a module Sass compiles.
The workspace has no relative `@import` at all today, so the scan still
finds the same 42 imports across 133 files; this closes the gap before
someone writes one.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
* feat(ui): turn the phone context panel into an off-canvas drawer
On ≤640px viewports the workspace context panel (categories, filters,
settings sections, collection filters) no longer stacks above the route
content capped at 30vh — it is a hidden-by-default drawer that slides in
from the left over a backdrop, opened via a new header toggle
(phone-only, CSS-gated) and closed by selection, backdrop tap, Escape,
or any navigation.
State lives in the new WorkspaceShellContextDrawerService provided by
the shell component; panels close it explicitly after selections that
do not navigate (Stalker ITV/radio categories, settings sections,
sources filters, collection filters), since NavigationEnd alone cannot
cover those. Desktop behavior is untouched, including the
ResizableDirective inline width.
Closes the drawer follow-up deferred from #1100 / PR #1326.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(ui): make the phone context drawer modal for keyboard users
Addresses Greptile P1 and Codex P2 review feedback on #1332:
- CdkTrapFocus on the sidebar captures focus into the drawer on open and
contains it while the drawer is modal; the shell restores focus to the
header toggle on close, since the closed drawer is visibility: hidden
and focus left inside it would silently drop to <body>.
- The drawer service closes the drawer when the viewport leaves the
phone breakpoint (matchMedia), so the trap can never hold the in-flow
desktop sidebar after a resize.
- The toggle's tooltip and aria-label are now variant-aware — categories
on portal routes, filters on sources/collection routes, settings
sections on the settings route — instead of a fixed 'Categories &
filters' that misdescribed two of the three; the two generic i18n keys
are replaced by six variant keys across all 19 locales.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(ui): remove background content from the a11y tree while the drawer is open
Round-2 review feedback on #1332 (Greptile P1, Codex P2):
- The rail, header, route content and playback footer are marked inert
while the phone drawer is open — CdkTrapFocus constrains Tab focus,
but a screen reader's virtual cursor could still reach and activate
the visually obscured controls behind the backdrop.
- The drawer panel itself is the trap's initial focus target
(tabindex=-1 + cdkFocusInitial), so focus capture still works when a
category list is loading, empty, or failed and renders no focusable
rows.
- Focus restore on close is deferred one tick: the toggle lives in the
inert header, and focus() on a still-inert element is silently
ignored.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(playback): gate global shortcuts and Escape behind the open phone drawer
Round-3 review feedback on #1332 (Codex P2s):
- The shell consumes Escape while the drawer is open: downstream Escape
consumers (the portal detail shell's inline player close, the shared
controls shortcuts) check defaultPrevented, so one keypress no longer
closes both the drawer and the obscured playback surface.
- inert does not silence document-level keydown listeners, so players
opt out themselves while inside an inert region: ControlsShortcuts
gains an optional hostElement handler and ignores every shortcut
(including Escape) when that host has an inert ancestor, and the radio
audio player applies the same check to its volume/mute keys.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(ui): close control, Cmd+F gate, and Embedded MPV inert guard for the drawer
Round-4 review feedback on #1332 (Greptile P1, Codex P2s):
- The drawer carries its own phone-only close button: touch
screen-reader users have no hardware Escape and cannot reach the inert
header toggle or the aria-hidden backdrop, so the trapped surface must
offer dismissal itself — even when a category list is loading or
empty and renders no actionable entries.
- Ctrl/Cmd+F no longer opens global search while the drawer is modal;
the shortcut would have navigated and focused an input inside the
inert header.
- EmbeddedMpvShortcuts (native-view legacy dock) gains the same
hostElement/inert-ancestor guard as the shared controls shortcuts, so
the obscured player cannot react to Space/arrows/M/Escape behind the
drawer.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(ui): round-5 drawer feedback + update phone-layout e2e for the drawer
Merges master (#1333 landed mobile-layout.e2e.ts pinning the #1326
stacked-panel behavior this PR replaces) and updates that spec to pin
the drawer contract instead: panel hidden by default with full-width
content, header toggle opens it over a backdrop, category selection and
backdrop tap close it. Verified locally on Chromium, Firefox and WebKit
(12/12). The spec's getByTestId calls needed plain [data-test-id=...]
locators — the web-e2e Playwright config never mapped testIdAttribute.
Also addresses Codex round-5 P2s:
- Focus restore now reports whether the toggle received focus; when a
drawer selection navigated to a route without a context panel (toggle
gone), focus falls back to the route content instead of dropping to
<body>.
- The Xtream and Stalker live layouts' Ctrl/Cmd+B sidebar shortcut opts
out while their host sits inside an inert region, matching the other
document-level listeners.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(ui): suppress command palette and shortcuts dialog behind the open drawer
Greptile round-6 finding on #1332: the document-level Ctrl/Cmd+K
handler in WorkspaceShellFacade and the '?' help-key handler in
WorkspaceKeyboardShortcutsService still opened their dialogs while the
phone context drawer was modal, stacking a second focus-trapped surface
on top of it. Both now check the drawer service (injected optionally,
same shell-component providers) and stay quiet while it is open, like
the Ctrl/Cmd+F global-search gate.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(ui): round-7 drawer feedback — Cmd+R gate and native MPV surface hiding
Addresses the two Codex round-7 P2s on #1332:
- WorkspaceShellContextDrawerService moves to @iptvnator/workspace/shell/util
and becomes root-provided, so AppComponent's document-level Ctrl/Cmd+R
global-recent shortcut can observe the modal drawer without pulling the
lazy shell chunk into the eager bundle. Cmd+R is now suppressed while
the drawer is open, like Cmd+F/Cmd+K/'?'.
- The shell registers the open drawer with a new
EmbeddedMpvOverlayVisibilityService.acquireExternalModalSurface() API:
the native-view video surface is composited outside DOM stacking and
would paint straight over the drawer regardless of z-index. The service
treats registered external modal surfaces exactly like open Material
dialogs.
- The service's recompute no longer reads overlayActive back before
setting it: signals already skip notification on equal values, and that
hidden read registered overlayActive as a dependency of any reactive
context calling into the service — the shell's acquire/release effect
looped forever on exactly that (caught by a live browser probe; the
unit suite mocked the service). The effect also wraps the acquire in
untracked() for caller-side hygiene.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(ui): expose the phone drawer as a named modal dialog
Round-8 review feedback on #1332 (Codex P2s):
- While open, the drawer carries role=dialog, aria-modal=true, and a
variant-appropriate accessible name (categories / filters / settings
sections) — assistive technology now hears that a named modal surface
opened instead of an unnamed complementary landmark. Closed (and the
always-visible desktop sidebar) stays a plain landmark.
- The UI-guidelines drawer section no longer claims the drawer service
is component-provided; it is root-provided from workspace/shell/util
since the round-7 move, and the stale claim could have led a future
change to re-scope it and silently break the AppComponent shortcut
gate and the Embedded MPV overlay observer. Matching code comments
updated everywhere.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(ui): round-9 — gate M3U player keys behind the drawer, raise drawer stacking
Greptile round-9 P1 + Codex round-9 P2 on #1332:
- The M3U video player's document-level digit-key channel switching and
Ctrl/Cmd+B sidebar toggle now apply the same inert-ancestor guard as
every other routed-content key listener. A codebase sweep confirms
this closes the class: every document-level key listener on routed
content is now either gated by the shell (Escape, Cmd+F/K/R, '?') or
opts out via closest('[inert]'); the guidelines now require the guard
for any new listener.
- The drawer moves from z-index 99/98 to 951/950: above the settings
action bar (100) and the root EPG/update panels (900/901), which
inert removes from interaction but not from paint order — below the
CDK overlay container (1000), since dialogs opened from inside the
drawer (Manage categories) must stack on top of it.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
* fix(ui): make the workspace usable on phone-sized screens
The shell was half-adapted below 640px: the rail flipped to a horizontal
bar but the link lists inside it kept stacking downwards, so the navigation
was drawn outside the bar and over the header (#1100).
Three resizable rails — the shell context panel, the live-layout channel
sidebar and the M3U channel drawer — kept their persisted desktop width,
which left the content around 50px on a 375px screen. They now span the
full width and stack above the content. The inline width written by
ResizableDirective is why these rules need `!important`.
Found while walking the rest of the UI at 375px and 768px:
- The detail hero kept poster and details side by side, squeezing the
action row below its own labels until "Play" was clipped to its icon.
- The settings section list did not scroll and painted over the footer,
which also affected short desktop windows.
- Hiding the M3U channel list on a phone was one-way: the restore handle
was hidden and only Cmd/Ctrl+B could bring it back.
- The live header drew the channel count and the paginator on top of each
other up to tablet width, because the paginator does not shrink and the
meta collapsed to zero width and overflowed its box.
- The search scope checkbox was pushed off the right edge.
Live TV states a floor for the player instead of a ceiling for the lists,
so the video keeps a usable share of the screen under the categories panel
and the channel list.
Closes#1100
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(ui): address review — keep the palette reachable and the video visible
Two findings from the Codex review on #1326.
Hiding the command-palette trigger on phones removed the only pointer-driven
way to open it: the rail renders route links plus Settings and emits nothing,
so `commandPaletteRequested` had exactly one source. The button stays and its
keyboard-shortcut label is swapped for an icon instead. Doing that exposed a
latent flex trap in the same row — an <input> keeps an intrinsic min-width
from its `size`, and `min-width: auto` honours it, so the field refused to
shrink and pushed the trigger out onto the buttons beside it.
The M3U drawer released the shared player floor, which on a short landscape
phone (600-640px wide) left the content container at half the shell body.
The inline guide inside it is `flex: 0 0 <basis>` and took its full 180px out
of a container that no longer had it, so the video could reach zero height.
The floor is restored and now yields on short viewports, the video states its
own minimum, and the guide is what gives way.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(ui): let the channel list keep its height on a landscape phone
Follow-up to the review: the player floor added in the previous commit was
measured against the viewport, not against what the shell had left. On a
640x360 landscape phone the stacked categories panel already takes 30vh, so
claiming another 50vh here drove the channel sidebar to zero height while it
was still marked expanded — no way to pick another channel — and pushed the
layout past the viewport.
The floor now applies only where the screen can afford it (`min-height:
600px`), the sidebar states a floor of its own so it cannot be squeezed out,
and the collapsed rule clears that floor so hiding the list still works.
Below that height the two panes simply share what is left.
Portrait is unchanged: categories 244px, channel list 220px, player 240px on
a 375x812 screen.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(ui): address review — settings nav on landscape, poster dead space
Two more findings from the Codex review.
The stacked settings context panel capped itself at 30vh, which on a 360px
screen is 108px — less than the panel's own title and footer, so the seven
section rows collapsed to nothing behind an overlapping footer. On short
screens the caption gives way (the rail already labels the page), the footer
sheds its tall-screen padding, and the settings variant gets a slightly
larger cap: unlike the live routes there is no player below competing for
height, only a scrollable form.
The poster kept a 330px minimum from the skeleton fallback at the bottom of
the file — sized for the 220px desktop poster — while the stacked phone hero
renders it 140px wide with a ~210px aspect-ratio height. Every loaded detail
page carried ~120px of empty space between the poster and the title. The
override sits after that rule because it wins on source order, not
specificity.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(ui): let the playlist switcher yield to the search field on narrow phones
Codex review of d6133da9: on a 320px header a route that contributes its
shortcut button left the search field less than its own chrome needs (~74px
of icon, palette trigger, gaps and padding), so the field's contents spilled
onto the buttons beside it.
The switcher is the one header region whose content can ellipsize, so it is
what shrinks — down to an 88px floor — while the field states its chrome as
a minimum. The field's basis moves from auto to zero so the input's intrinsic
size stops counting as content: with basis auto the field claimed its
intrinsic width even when room was ample and squeezed the switcher to ~115px
on a 375px screen that could fit all 140.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(ui): fit the switcher's own chrome inside its phone floor
Follow-up to the Codex note that the trigger's fixed chrome (type icon,
refresh, chevron, gaps, padding) exceeds the 88px floor the shell now allows
the switcher to shrink to. The flagged scenario itself cannot occur — the
Multi-EPG shortcut needs Electron bridge methods the PWA lacks, and Electron
enforces a 900px minimum window width so it never sees the phone breakpoint —
but the floor should hold on its own terms rather than by accident of which
buttons happen to render. Dropping the decorative type icon on phones brings
the fixed chrome under the floor, and the name gets the space instead.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
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.
The persisted subtitle preference only ever had an owner behind the default-off
shared web-controls flag, so with the shipping controls it did nothing: Video.js
never read it, ArtPlayer declared the input but never used it, and the HTML5
player only ran a one-shot pass after play() resolved — before hls.js had added
its text tracks. No portal host bound the input at all, so it never reached
Xtream or Stalker pages either.
Extract the source-local track controllers into the adapter-free
WebVideoSourceTracks and have WebVideoSourceControlsBridge wrap it, so both
controls modes apply the preference through the same code. The preference-off
players bind it directly (VjsLegacyTracks for Video.js), and
WebPlayerViewComponent reads the preference from SettingsStore instead of an
input so every host inherits it.
The preference means different things depending on who renders the caption UI:
shared controls stay authoritative for the session, while vendor chrome is
source-default — the preference seeds each new source and is released once the
media reports playing, so the engine own caption menu keeps working. Mode
selection is an optional playbackStarted probe passed to the HLS, native and
Shaka helpers; in that mode the HLS helper deselects the track rather than
hiding it, since subtitleDisplay would silently override the vendor menu.
Closes#1155
* feat(m3u): extract ClearKey DRM from #KODIPROP playlist lines
Adds the typed ChannelDrm model (shared interfaces) and a KODIPROP
post-processing step in createPlaylistObject() — the single funnel for all
four playlist import paths. Parses inputstream.adaptive.license_type,
license_key and drm_legacy; ClearKey keys accepted as kid:key hex pairs,
W3C ClearKey license JSON, or a plain kid→key JSON map. Unsupported license
types (Widevine/PlayReady/license URLs) are preserved with supported=false
so playback can surface a DRM diagnostic instead of failing silently.
Also adds isDashStreamUrl/isDashChannel helpers for DASH routing.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* feat(playback): add Shaka DASH source engine with ClearKey support
Introduces ShakaVideoSession (libs/ui/playback/src/lib/shaka-engine/): a
lazily imported shaka-player engine (separate lazy chunk, ~217 KB transfer)
owning attach/configure/load with an operation queue and generation guard
against channel-switch races. Channel ClearKey config maps to
drm.clearKeys; channels with an unsupported license type emit a
DrmOrEncryption diagnostic without starting an engine. Shaka errors are
classified into the existing playback diagnostics
(PlaybackDiagnosticSource.Shaka).
Wires the engine into both built-in players like hls.js/mpegts.js:
- HTML5: extension === 'mpd' branch in playChannel(); hls/mpegts/native
glue extracted to helpers to keep the component within the size budget
- ArtPlayer: customType 'mpd' in ArtPlayerSourceSession (+ getDrm seam)
- Shared controls: WebVideoControlsSource kind 'shaka' +
WebVideoShakaControls using the Shaka 5 text model (selectTextTrack(null)
hides subtitles; Player.setTextTrackVisibility no longer exists)
Adds a CJS shaka-player jest stub (video.js precedent) for web specs.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* feat(m3u): route DASH channels to the inline Shaka-capable player
DASH (.mpd) channels always play in a built-in web engine (radio
precedent): external MPV/VLC cannot receive KODIPROP ClearKey
configuration (VLC upstream #29465) and Video.js has no DASH bridge yet.
- shouldShowInlinePlayer() bypasses the external-player setting for DASH
- new shouldAutoLaunchExternalPlayer() guard consolidates the MPV/VLC
auto-launch conditions in the m3u-state effects (incl. catch-up path)
- the M3U page overrides the player for DASH channels: ArtPlayer stays
ArtPlayer, everything else falls back to the HTML5 player
- ChannelDrm is passed through ResolvedPortalPlayback into the synthetic
player-view channel
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* test(e2e): add offline DASH ClearKey fixtures and e2e coverage
Fixtures (apps/web-e2e/src/fixtures/dash/): ~4s VP9+Opus DASH, clear and
CENC-encrypted variants with fixed synthetic ClearKey credentials.
Content synthesized by ffmpeg; encryption done by Shaka Packager because
ffmpeg's mp4 muxer writes senc-only metadata (Chromium needs saiz/saio)
and cannot produce the subsample encryption the VP9 CENC binding
requires. Generation script + README document regeneration.
web-e2e (Chromium): import an M3U with KODIPROP ClearKey via raw text,
verify encrypted and clear DASH actually play (currentTime advances, no
diagnostic banner) and that an unsupported license type (Widevine)
surfaces the DRM diagnostic. Fixtures are served through Playwright route
interception with HTTP Range support; the Angular service worker is
blocked since SW-routed requests bypass interception.
electron-backend-e2e: the same happy path + negative against a local
Range-aware fixture server — the automated proof that ClearKey EME works
in the real Electron runtime (file:// secure context).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* docs: document DASH + ClearKey playback architecture
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(pwa): extract KODIPROP DRM on the web-backend /parse import path
The web-backend keeps its own playlist builder for the PWA URL-import
path, so the shared createPlaylistObject() DRM hook never ran there and
encrypted DASH channels imported by URL reached Shaka without keys.
Apply extractDrmFromRaw() in that builder too and cover the path with a
regression test.
Addresses Codex review on PR #1225.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(playback): interrupt stalled Shaka loads and destroy failed engines
Two review findings on the ShakaVideoSession lifecycle:
- stop()/start() now tear the current player down immediately instead of
queueing the destroy behind the in-flight operation. Shaka's destroy()
interrupts a pending load() (LOAD_INTERRUPTED), so a stalled manifest
fetch can no longer wedge the operation chain and block the next
channel start (Codex P1).
- A rejected attach()/load() now destroys the failed player after
emitting the diagnostic, so a non-functional engine never stays
attached to the media element or exposed to the shared-controls
bridge (Greptile P1).
Regression tests cover both paths. The Shaka fakes are consolidated into
a shared jest-free test double that mirrors the destroy-interrupts-load
semantic, and the ArtPlayer source-session spec is split (fixtures +
DASH cases) to stay within the max-lines lint budget.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(m3u): unify DASH URL detection with playback extension normalization
isDashStreamUrl() used the simpler getStreamExtensionFromUrl(), so URLs
the player engines classify as DASH (stream.MPD, ?ext=mpd, ?format=mpd)
were not routed to the Shaka-capable inline player and lost their
ClearKey metadata with Video.js or external players configured
(Codex P2). The normalized getPlaybackMediaExtensionFromUrl() now lives
in @iptvnator/shared/m3u-utils (re-exported unchanged from the playback
lib) and both routing and engine selection share it.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* chore(lint): satisfy CI lint and CodeQL in DASH support files
- replace shell-built tar/npm commands with execFileSync arg arrays in
the fixture generator (CodeQL: uncontrolled shell command)
- give jest stub methods explicit bodies (no-empty-function)
- compact the diagnostic label switches in WebPlayerViewComponent to
stay under the max-lines budget
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(playback): tear down the Shaka engine on critical error events too
A non-recoverable Shaka error emitted after a successful load left the
dead engine attached to the media element and exposed to the
shared-controls bridge (Greptile P1, round 2). Critical error events now
destroy the player right after the diagnostic is emitted, matching the
load-failure path.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(m3u): honor DASH catch-up URLs and drop unusable DRM fallbacks
Two Codex round-2 findings:
- The inline-playback DASH gate only examined the channel URL, while the
external-player guard checks the resolved catch-up URL — a replay that
resolves to an .mpd manifest with MPV/VLC configured ended up with no
player at all. The gate now uses the effective playback URL
(activePlaybackUrl ?? channel.url).
- The unsupported-DRM diagnostic advertised MPV/VLC fallback actions,
but external players cannot receive the KODIPROP license config either
— the diagnostic no longer recommends them.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(playback): suppress unusable external fallback for ClearKey DRM failures
Runtime DRM errors on channels that carry KODIPROP ClearKey config (wrong
or rotated keys) advertised MPV/VLC fallback actions, but external
players never receive the license config — the fallback could only fail
differently. DRM-classified diagnostics from such channels no longer
recommend external players; clear channels keep the hint.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(m3u): symmetric DASH inline gate and lazy DRM for pre-upgrade playlists
- The inline DASH gate is now true when either the channel or the
resolved catch-up URL is DASH, mirroring the external-player guard —
a .mpd channel whose catch-up resolves to .m3u8 no longer ends up
with no player at all.
- Playlists imported before the DRM feature carry no drm field, but the
raw KODIPROP block survived in the stored items; the M3U page now
falls back to extractDrmFromRaw(channel.raw) at playback time, so
encrypted channels work without a re-import (Channel gains raw?).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* docs: sync the DASH/Shaka contract across agent docs
Mirrors the DASH/Shaka source-engine contract into AGENTS.md and adds
Shaka to the shared web-video bridge descriptions in CLAUDE.md and the
player-controls contract; documents the lazy raw-KODIPROP DRM fallback
for pre-upgrade playlists in the M3U architecture doc.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(playback): reset the media element for rejected DRM and widen ClearKey fallback suppression
- Switching from a playing stream to an unsupported-DRM DASH channel
loads no new source, but play() still ran and the un-loaded element
could resume the previous stream underneath the diagnostic banner.
The HTML5 player now resets the element instead of playing.
- Any inline failure on a KODIPROP ClearKey channel (manifest, codec,
media, network — not just DRM-category errors) is unsolvable in
MPV/VLC, which never receive the license config; the external
fallback hint is now suppressed for all diagnostics of such channels.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(playback): restore suppressed DASH captions when the preference re-enables
The Shaka bridge dropped the auto-selected text track with
selectTextTrack(null) when showCaptions was off, but did not remember it
— re-enabling the preference mid-session left captions permanently off
(HLS/native bridges already restore). The session now remembers the
suppressed track id and reselects it via the bridge's caption-state pass;
suppression is also skipped when no track is active. Covered by session
and new WebVideoShakaControls specs.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* chore: retrigger CI
GitHub Actions created no check suites for the last three pushes to this
branch (third-party apps received the webhooks); an empty commit re-fires
the push and pull_request events.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* refactor(playback): split oversized Shaka session and HTML5 spec files
CI lint enforces max-lines 400: extract ShakaTextTrackSuppression and the
shaka-error helpers out of ShakaVideoSession, and move the DASH-specific
HTML5 player test into its own spec. No behavior change.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* ci: allow manual dispatch of the cross-platform E2E workflow
GitHub stopped delivering push/pull_request events for this branch;
workflow_dispatch provides a manual escape hatch (CI and build-and-make
already have one).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
* feat: add strip country prefix setting
* feat: scope country-prefix stripping to live content and cover missing surfaces
- narrow the heuristic: pipes always strip, dash/colon separators only
when the prefix is a short uppercase tag ("UK - BBC One" strips,
"Sky - Sports F1" and "Mission: Impossible - Fallout" stay intact)
- fall back to the original name when stripping would leave nothing
- scope stripping to live content only: grid type (live/itv/radio),
playback isLive, external sessions without contentInfo, dashboard
cards with contentType 'live'
- cover previously missed surfaces: M3U player EPG timeline header,
M3U inline player title, radio player, dashboard live rails
- replace hardcoded settings strings with translate keys and add
SETTINGS.STRIP_COUNTRY_PREFIX(_DESCRIPTION) to all 18 locales
- add unit specs for the utility plus regression specs for
channel-list-item and external-playback-dock
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* test: cover strip-country-prefix call sites for codecov
- dashboard-rail: new spec for cardTitle live/movie/series scoping
- grid-list: strip enabled/disabled, VOD passthrough, 'No name' fallback
- portal-inline-player: live strip vs VOD passthrough
- unified-live-tab: timeline channel name strip + M3U name precedence
- video-player: timeline/radio/inline titles with the setting on and off
- settings-store: default false + persisted true round-trip
- settings-form.utils: new spec for form default and ?? false fallback
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
---------
Co-authored-by: 4gray <serega05@gmail.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
* feat(epg): add vertical list view for the live EPG panel
Add an EPG list view — a vertical, single-day programme list — as an
alternative rendering of the live EPG panel, selectable via a new
Settings → EPG → "Guide view" toggle (epgViewMode: 'timeline' | 'list',
default 'timeline' so existing users see no change).
- New EpgListViewComponent (app-epg-list-view) mirrors
EpgTimelineComponent's input/output contract 1:1, so all four live
hosts (M3U player, unified live tab, Xtream, Stalker) swap the panel
with a plain @if and identical bindings.
- Reuses the shared view-agnostic EPG modules (classifyTimelineWhen,
hasProgramsForDateKey, epg-archive.util catch-up gating,
epg-summary.util collapsed-summary maths, epg-date helpers,
EpgProgrammeDialogService, app-epg-timeline-empty-state) — no
duplicated logic.
- Rows show time range, title, optional description, live progress on
the on-air row, catch-up "Watch" on past rows when archive playback
is available, and a details dialog; keyboard activation guards
nested buttons (target === currentTarget).
- Auto-focuses the on-air row on channel select, restores it across
collapse/expand remounts, and shows a sticky in-flow "On now" strip
(never overlaying rows) when the current programme is scrolled away;
all scroll maths is rect-based relative to the scroller.
- List mode raises only the inline panel height via an epg--list
modifier (--epg-inline-height clamp); timeline and collapsed heights
are unchanged.
- Setting flows end-to-end (Settings interface → DEFAULT_SETTINGS →
SettingsStore/StorageMap → segmented control in the EPG section);
Electron-only UI, PWA stays on the timeline default. i18n keys added
to all 18 locales.
- Tests: new component/row/utils/scroll-controller specs, settings
persistence spec, swap tests in all four host specs, and Electron
E2E for the settings round-trip and the rendered list view. Docs
updated (m3u-playlist-module.md).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix(epg): address list-view review findings from Codex and Greptile
- Reset the list view to today when a new channel's programme set
arrives while the user is parked on another day (timeline parity):
the scroll controller now keys by the full programme-set identity
(programsFocusKey) and commits today before focusing, instead of
silently stranding the new channel on the stale day. (Codex P2)
- Centralise the 'timeline' fallback as a resolvedEpgViewMode computed
on SettingsStore; the four live hosts consume the derived signal
instead of duplicating the `?? 'timeline'` expression. (Greptile P2)
- Extract the component's reactive plumbing into
registerEpgListViewEffects(), bringing the component back under the
300-line guideline (290). (Greptile P2)
- Controller spec rewritten around programme-set fixtures with new
coverage: return-to-today on channel switch, day navigation left
alone, no-takeover when today has no data, empty-set no-op.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix(epg): drop malformed programmes from the list-view day filter
Reject programmes whose stop is not after their start in
buildEpgListRows — same as the timeline's buildTimelineBlocks. Bad
provider data would otherwise render impossible time ranges and could
even be offered as catch-up playable. (Codex P2 on e6fd0d08)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Replaces the vertical EPG list with a shared horizontal `app-epg-timeline`
ribbon across all live surfaces (M3U player, unified live tab, Xtream, Stalker):
zoom, day navigation, short-programme grouping, catch-up/timeshift, and
per-state empty views. Backend gains timezone-aware `datetime()` comparisons,
unscoped source fallback, non-ASCII candidate matching, and chunked candidate
queries.
Timeline split into reusable, view-agnostic modules (archive/summary/dialog
service/render util/scroll controller) for the future EPG list view.
Fixes landed during review:
- honor the controlled `selectedDate` input (seed via linkedSignal)
- restore ribbon position across collapse/expand
- keep the ribbon mounted when scrolling across a gap day
- don't trigger block playback on Enter from nested watch/info buttons
- don't reset timeshift playback on the 30s now-tick during EPG gaps
Greptile 5/5 (safe to merge); Codex clean; CI green.
fix(m3u): highlight active archive epg program
* Store the active archive EPG program with the resolved playback URL so the clicked M3U catch-up row stays highlighted during archive playback.
* Show archive playback state and return-to-live controls in the inline player panel.
* Hide duplicate EPG program rows that share the same time slot while keeping the richer provider entry.
- merge origin/master into PR #964 and keep embedded MPV test on the isolated playback sub-entrypoint
- centralize EPG capability through DataService.supportsEpg and update PWA web-e2e expectations
- split BrowserAccessError copy between Electron and PWA diagnostics
- Introduced tooling for building and staging the macOS `libmpv` runtime for IPTVnator's embedded MPV player.
- Added `build-macos-runtime.mjs` for building an LGPL-compatible runtime from source.
- Created `stage-macos-runtime.mjs` for staging the built runtime artifacts.
- Implemented validation for the packaged embedded MPV runtime in `electron-after-pack.cjs` and `embedded-mpv-macos.cjs`.
- Updated packaging scripts to ensure the embedded MPV runtime is correctly integrated and validated during the build process.
- Added README files to document the expected layout and usage for the embedded MPV runtime artifacts.
Entire-Checkpoint: c6e522b4276c
- Added WorkspaceViewCommandService to UnifiedCollectionPageComponent for workspace layout commands.
- Implemented clear current view command for favorites and recent items.
- Updated unified favorites data service to use buildXtreamCollectionUid for UID generation.
- Enhanced unified recent data service to normalize timestamps and handle backdrop URLs.
- Introduced withRecentItems feature to manage recent item additions with backdrop support.
- Updated VodDetailsRouteComponent and SerialDetailsComponent to backfill content backdrops.
- Modified IXtreamDataSource interface to include backdrop URL handling for favorites and recent items.
- Added tests for recent items feature to ensure correct functionality and backdrop handling.
- Implemented a loading state for channel lists in various components, enhancing user experience during data fetching.
- Added `ChannelListLoadingStateComponent` to display loading skeletons for channels and groups.
- Updated `M3uWorkspaceRouteSession` to manage loading states more effectively when fetching playlists.
- Modified `VideoPlayerComponent`, `SidebarComponent`, and `ChannelListContainerComponent` to utilize the new loading state.
- Enhanced tests to cover new loading behaviors and ensure proper dispatching of loading actions.
- Updated video player component to conditionally render audio player for radio channels.
- Added radio-specific styles to video player and unified live tab components.
- Introduced new layout classes for radio and external players.
- Enhanced audio player component with improved UI and volume controls.
- Implemented logic to preserve radio flag in M3U playback details.
- Updated tests to cover new radio functionality and ensure proper rendering.
- Updated PlaylistErrorViewComponent to use PlaylistContextFacade for accessing the current playlist.
- Refactored StalkerSearchComponent to utilize PlaylistContextFacade for retrieving the active playlist.
- Modified StalkerWorkspaceRouteSession to sync route context using PlaylistContextFacade.
- Adjusted XtreamStore to remove NgRx store dependencies and use PlaylistContextFacade for current playlist access.
- Updated ChannelListContainerComponent to resolve playlist ID using PlaylistContextFacade.
- Refactored SidebarComponent and ToolbarComponent to eliminate NgRx store usage for playlist-related data.
- Enhanced unit tests in with-content.feature to ensure proper loading and cancellation of content imports.
- Added `GlobalFavoritesListComponent` to display and manage favorite channels.
- Integrated drag-and-drop functionality for reordering favorite channels.
- Created HTML and SCSS files for the global favorites list layout and styling.
- Implemented logic to handle empty states and search functionality within the favorites list.
feat: Enhance unified collection page with live and grid tabs
- Developed `UnifiedCollectionPageComponent` to manage different content types (live, movies, series).
- Added `UnifiedGridTabComponent` and `UnifiedLiveTabComponent` for displaying content in grid and live formats.
- Implemented loading states and empty states for better user experience.
- Integrated EPG (Electronic Program Guide) functionality for live channels.
style: Refactor styles for unified collection components
- Updated SCSS styles for `UnifiedGridTabComponent` and `UnifiedLiveTabComponent` to improve layout and responsiveness.
- Added skeleton loading states for better visual feedback during data fetching.
fix: Improve scrolling behavior in category view component
- Adjusted scrolling logic in `CategoryViewComponent` to ensure selected categories are centered in the view.
Entire-Checkpoint: 0e4488af6c76
- Introduced M3uRecentlyViewedItem interface to define structure for recently viewed items.
- Implemented utility functions to handle mapping and building of recently viewed items from playlists.
- Updated Playlist interface to use PlaylistRecentlyViewedItem type for recently viewed items.
- Enhanced PortalActivityItem interface to include 'm3u' as a possible source.
- Added UI components for displaying recently viewed channels, including clear functionality.
- Integrated recent view functionality into the channel list container and sidebar components.
- Updated dashboard data service to manage M3U favorites and recently viewed items.
- Added tests to ensure correct behavior of recently viewed items in the dashboard data service.