* feat(settings): grouped navigation, cards and switches from the settings concept
Implements the Claude Design "Settings concept" handoff.
Navigation: the sidebar is a 20px "Settings" title over App, Library, Devices
and Data groups, with About pinned to the footer beside the installed version
and an update badge. The active item is a soft fill without a border. Esc
leaves settings like the header Back. Reset is no longer a page: its one
destructive action is the last card of "Backup & data".
Pages: a title and one-line subtitle, then rows grouped into titled cards
with one right-aligned control column. Selects are compact without floating
labels, checkboxes are switches, segmented controls are pills, and
descriptions are capped at 56ch. "Show subtitles" moves to Playback, the
Embedded MPV note becomes a callout under the player select (only while it
is selected), and the TMDB attribution becomes the About footer.
EPG: Add and Refresh all sit in the Sources header, the empty state has its
own Add, the format examples are one line, Clear EPG data is its own row and
the offset is a stepper. About: version, update state and channel share one
card; the nightly warning is a callout shown only while Nightly is selected,
with a shortcut to the backup page; the support buttons are neutral with
coloured icons.
Save model: the save bar stays (design option B) and now floats over the
content column, counts the staged changes, answers Cmd/Ctrl+S, and marks
pages with staged edits in the sidebar. A saved change that waits for a
restart shows a dismissible notice.
Rows stack under 600px of pane width (the page is a size container, so the
persistent sidebar is accounted for). Page-specific styles moved into the
About, EPG, Backup and Remote control section stylesheets to stay within the
component style budget. New SETTINGS keys are translated in all 19 locales
and NAV_RESET is removed; brand names and loanwords that stay English are in
the identical-value baseline.
E2E: Material slide toggles report aria-checked after the next render, so
both suites toggle through a setSwitch helper; the settings nav is a
navigation landmark, so the Dashboard rail link is scoped to the rail.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
* fix(settings): compare restart notices against the running app, fix CI fallout
The restart notice now lists only the restart controls whose saved value
differs from what the running app uses: the stored values at first load,
or what the embedded engine reports it actually runs for the frame-copy
opt-in. Saving the launch value back withdraws the notice instead of
leaving it up with no chip to explain it. The refresh reads the current
list untracked and writes only a changed one, because the engine probe
effect calls it.
CI: the zoneless checklist dropped the deleted Reset section component, and
the theme-tokens Electron E2E floats the recording folder label on the
Playback page now that the settings selects carry no floating label.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
* fix(settings): keep a dismissed restart reminder away until the setting changes
"Later" now records the saved value it dismissed, for the rest of the app
run, so an unrelated save (or reopening Settings) does not bring the same
reminder back. It returns once a restart setting is saved with another
value.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
---------
Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
* build(test): make spec tsconfigs resolve what Jest resolves
Lib spec tsconfigs used module: commonjs with node10 resolution, which cannot
see Angular's exports-only secondary entry points, and dropped global.d.ts, so
tsc reported thousands of resolution errors and no window.electron typing.
Switch them to module: preserve with bundler resolution (ts-jest still forces
CommonJS emit outside ESM mode), add global.d.ts to every spec program, type
jest.unstable_mockModule for the ESM workspace, include the ui-epg and
ui-playback specs that jest.web-esm.workspace.ts runs under the web spec
config, and drop the snack-bar stub that shadowed the real Material types.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
* ci(test): gate spec type-checking with typecheck:spec
Add tools/typecheck/spec-typecheck.mjs, which runs tsc --noEmit over every
tsconfig.spec.json with a small pool and fails on any diagnostic, wire it into
the unit-and-typecheck job after typecheck:ci, and document the gate and the
spec tsconfig conventions in the validation map. Also bring the non-Tier-A
spec configs (remote-control-web, ui-remote-control, stalker-mock-server) to
the same conventions so the gate covers the whole workspace.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
* test: fix the spec type errors surfaced by typecheck:spec
With the spec programs resolving modules and ambient typings correctly,
tsc reported 432 genuine errors across the Tier A projects: read-only
capability flags assigned on Partial<> doubles, signal-store values used as
types, fixtures missing required fields, index-signature property access,
partial bridge doubles cast through incompatible shapes, and deferred
resolvers narrowed to never. Type the doubles instead of casting to any:
writable mapped types for capability flags, InstanceType<typeof StalkerStore>,
typed jest.fn signatures, protectedState: false on test signal stores, and
completed fixtures. Production changes are limited to bracket access for
index-signature properties under the libs' noPropertyAccessFromIndexSignature
setting and two narrowing guards in the global favorites loader.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
* test(playback): use the ESM setup's jest global in the controls fixtures
The fixture imported jest from @jest/globals, which is not a direct
dependency. Jest provides that module at runtime, so tests passed, but on a
clean pnpm install tsc cannot resolve it and typecheck:spec failed in CI.
The ESM test setup already installs import.meta.jest as the global, typed
by @types/jest, as the other ESM specs use it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* test: type the parental lock doubles merged since the gate was written
The parental lock feature (#1601) and the Stalker actor route landed on master
with spec doubles declared as zero-argument jest.fn()s that the tests then
drive with the real arguments, plus a copy of the ResizableDirective override
imported from a library that does not export it. Give the doubles the lock
service's real signatures, drop the dead override as in the sibling layout
specs, use bracket access for the actor route's personId param, and keep the
Stalker layout spec within the 1200-line limit.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: 4gray <fourgray@proton.me>
Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
* chore(deps): upgrade Angular to 22.1 and Nx to 23.2
* fix(deps): complete Angular migrations after rebasing on master
* fix(ci): use the Node pin for Windows runtime refresh
* docs(deps): synchronize the workspace-shell Node requirements
* 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>
settings.component.spec.ts was 1516 lines and the last settings file in the
max-lines baseline. The behaviour that moved into facades now has its own
specs, driven directly instead of through the rendered page.
- settings-app-update.facade.spec.ts: status polling/retry, bridge actions,
release notes dialog, version messaging, dispose
- settings-epg.facade.spec.ts: refresh, clear flow, post-save re-fetch
- settings-playlist-reset.facade.spec.ts: summary, dialog, Electron progress,
browser fallback, failure snackbar
- settings-backup.facade.spec.ts: desktop export, browser download fallback
- settings.component.spec.ts keeps the page shell, the facade lifecycle seam
and runtime capabilities; settings.component.form.spec.ts takes hydration,
section outputs, dashboard controls and submit
- settings-section-scroll.directive.spec.ts gives the directive its first spec
- shared TestBed fixtures live in settings/test-stubs/, kept out of both the
app build (.stub.ts) and the coverage ratchet (test-stubs/)
105 settings tests, up from 94; every file is under the 400-line limit, so
settings.component.spec.ts leaves the baseline.
settings.component.ts had grown to 819 lines — past the CLAUDE.md target (<300)
and hard maximum, passing lint only because it sat in the max-lines baseline.
The behaviour moves into facades the template binds to directly, following the
precedent already in this folder: new app-update (218), form (197), epg (123),
embedded-mpv (74) and remote-control (37) facades, with playlist-reset extended
to 143 and settings-options to 200. The component is now a 259-line coordinator
holding capability flags, section nav, players() and the cross-facade flows.
settings.component.ts is removed from the max-lines baseline.
No behaviour change. One ordering detail: applyChangedSettings now applies
language/theme before kicking off the EPG re-fetch; changeTheme only touches DOM
theme sync and translate.use does not touch the form, so the two are
independent.
* feat(playback): Up Next episode rail for the inline series player
On wide windows the inline series player now docks left and fills the
leftover stage column with a Netflix-style "Up Next" rail: the rest of the
current season plus next-season spillover, the playing episode highlighted,
and watch-progress bars from playback positions. Clicking an episode plays
it inline through the host's existing episode flow (Xtream serial-details
and Stalker series view).
- New app-up-next-rail component + buildUpNextRailItems() util in
ui/playback; entries carry the host's raw episode object so selection
needs no id lookup.
- PortalInlinePlayerComponent measures the theater stage with a
ResizeObserver and docks the rail only when the leftover beside the 16:9
player is >= 320px; narrower stages keep the centered theater/ambient
behavior from #1223. Movies and live never show the rail.
- New playerUpNextRail setting (Settings > Playback, default on, built-in
web players only), mirroring playerAmbientMode; enforced at runtime for
non-web engines.
- i18n: SETTINGS.PLAYER_UP_NEXT_RAIL(+_DESCRIPTION) and PORTALS.UP_NEXT in
all 18 locales.
- The rail renders as an opaque panel on top of the stage, so the ambient
fill stays behind it.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(playback): address Greptile review on the Up Next rail
- Stage overflow: `.player-shell__viewport` had no border-box sizing (the repo
has no global reset), so the docked-rail modifier's 12px padding widened the
stage past its container and the right edge was clipped.
- Width gate: compute the width the rail actually receives (stage minus the
docked layout's padding, the height-driven 16:9 player, and the flex gap)
instead of raw stage slack, and observe the stage's border box so the
modifier's own padding cannot feed back into the measurement.
- Stalker lazy seasons: Ministra VOD-series seasons hold no episodes until
opened, so the rail's next-season spillover stopped at the current season.
Prefetch the following season while an episode plays inline.
Adds regression coverage for the gate boundary, gate stability across the
padding toggle, and the lazy-season prefetch.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* fix(stalker): stop the rail spillover prefetch from retrying forever
A failed or genuinely empty Ministra season resets isLoading while leaving
episodes empty, so the prefetch effect re-requested the same season on every
emission for as long as inline playback continued. Remember which seasons this
view already requested and ask at most once each.
Regression test asserts the empty-response case fetches exactly once and does
not retrigger on further playback in the same season.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* fix(stalker): let a failed spillover prefetch recover on the next episode
The previous guard was permanent, so a transient network or authorization
failure disabled the rail's next-season prefetch for the component's lifetime.
Distinguish the two outcomes instead:
- Answered (even with zero episodes) — a real answer, never asked again.
- Failed — the claim is released, but pinned to the episode that triggered it,
so the retry waits for the next playback change. Retrying immediately would
loop, since the failure itself flips isLoading and re-runs the effect.
The claim is taken synchronously; awaiting first let the isLoading flip re-run
the effect and fire a duplicate request before the answer arrived.
`loadEpisodesForSeason` now reports whether the portal answered; existing
callers ignore the result and are unaffected.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
* fix(tmdb): purge obsolete search cache rows
* feat(playback): theater stage and opt-in ambient fill for the inline portal player
On wide-short windows the VOD/series inline player left a strip of app
surface next to the video: with `width: auto`, the viewport's `max-height`
transferred through `aspect-ratio` into a max-width (CSS transferred size
constraints), re-clamping the stage to 16:9 and leaving the leftover
outside it.
- Theater stage: give `.player-shell__viewport` a definite `width: 100%`
so it always fills the content row; the player renders as the largest
16:9 box that fits the stage height, centered — the leftover is always
the stage's black background, never app surface (YouTube-style
letterbox). Applies to every inline engine.
- Ambient fill: new `playerAmbientMode` setting (default off, Settings >
Playback, web players only) renders a blurred, dimmed copy of the
poster behind the player, filling the letterbox margins. Enforced at
runtime too: Embedded MPV never gets the extra DOM layer. Live channels
and non-http(s) poster URLs are excluded.
Verified live via CDP at 1720x760 (stage 1362x532, player 946x532 with
symmetric 208px margins) and 1280x950 (stage exactly 16:9, no bars).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix(i18n): add ambient-mode setting keys to all remaining locales
The i18n drift gate requires SETTINGS.PLAYER_AMBIENT_MODE and its
description in every locale; the feature commit only covered en and ru.
Translated via the i18n-fill workflow (per-locale patch + mechanical
merge, glossary-matched against each existing file).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* test(settings): include playerAmbientMode in expected default settings
settings.component.spec asserts the persisted settings object with
toEqual; the new default-off field has to be part of the fixture.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 <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>
* spike(embedded-mpv): frame-copy pipeline prototype (helper + shm ring + Electron viewer)
Standalone macOS spike for the frame-copy unification direction from the
2026-07-10 analysis: a helper process renders mpv offscreen into a GL FBO,
reads frames back through an async PBO ring, and publishes BGRA frames into
a 3-slot POSIX shm seqlock ring; a minimal Electron viewer copies the newest
frame via a plain-C N-API addon and uploads it to a WebGL canvas per rAF.
First numbers on M1 Pro (see spike README): 4K60 HEVC hwdec sustained at
60 fps end to end, ~1.2 ms shm copy + ~3.5 ms texture upload, ~10 ms
produce-to-upload age, zero torn frames. Remaining gates: weak hardware,
long-run pacing, HDR, latency flash test.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* spike(embedded-mpv): add RESULTS.md measurement log with M1 Pro baseline
Structured per-machine table with repro commands so the pending Intel Mac
and Windows iGPU runs can be appended and compared one-to-one.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* spike(embedded-mpv): pacing/judder instrumentation + 50/25 fps and HDR gate results
Viewer now measures inter-frame intervals on both clocks (present side and
producer side): stddev/p99/max, late-frame counters vs the producer's median
interval, and a cumulative LONGRUN summary every 30 s. The addon exposes the
producer timestamp (produceMs) for this.
Measured on M1 Pro: 50 fps and 25 fps cadences are clean (late frames only
at startup; residual jitter is 120 Hz rAF grid quantization, bounded by one
display tick), and 4K25 HDR10 PQ/BT.2020 is tonemapped to SDR by mpv before
readback at full rate with unchanged copy costs. RESULTS.md carries the
tables and HDR-clip repro commands.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* spike(embedded-mpv): record 10-minute 4K60 HEVC long-run results
Zero dropped frames and zero torn reads after the first-minute warmup over
~8.5 minutes; steady-state late frames (~0.4%) track the 12 s test clip's
--loop restarts, not the copy pipeline.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* spike(embedded-mpv): viewport-scaling measurement + integration design draft
Confirms the render-at-viewport-size claim (4K source in a 720p FBO costs
720p: 0.17 ms readback / 0.16 ms copy / 0.17 ms upload at 60 fps) and adds
DESIGN.md — the draft integration architecture: per-session helper process
linking bundled libmpv on all platforms (finally full-featured + Wayland-
agnostic Linux), JSON-over-stdio control evolving the Linux wid protocol,
unchanged EmbeddedMpvSession renderer contract, shm generations for resize,
packaging via the existing vendored-runtime tooling, rollout behind its own
flag with the docked path as default.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* spike(embedded-mpv): auto-detect Homebrew prefix and Node headers for Intel Macs
BREW_PREFIX was hardcoded to /opt/homebrew (Apple Silicon) and NODE_INC to
one nvm version; both now resolve via brew --prefix and the PATH node's
execPath, so the pending Intel Mac run needs no Makefile edits. README gets
a fresh-machine checklist.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* spike(embedded-mpv): self-contained measurement bundle for machines without Node/pnpm
make-bundle.sh assembles a tarball with the spike sources, vendored N-API
headers (Makefile prefers them when present, so no Node install is needed),
pre-generated 4K HEVC/HDR10 test clips, and an official Electron dist
download for the target arch. collect-results.sh builds and runs the full
RESULTS.md scenario suite automatically (plus an optional --long 10-minute
run) and writes one results-<host>-<date>.txt to send back. Target-machine
prerequisites shrink to Xcode CLT + brew mpv — built for the pending Intel
Mac baseline run.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* spike(embedded-mpv): support MacPorts libmpv and legacy-macOS bundles
Makefile and collect-results.sh now detect libmpv in the Homebrew prefix or
MacPorts /opt/local (Homebrew is unsupported on legacy macOS like High
Sierra; 'sudo port install mpv +libmpv' provides libmpv there). make-bundle
takes ELECTRON_VERSION/BUNDLE_SUFFIX overrides — Electron 27+ needs macOS
10.15, so High Sierra bundles ship Electron 26.6.10 (LSMinimumSystemVersion
10.13).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* spike(embedded-mpv): scope macOS frame-copy engine to Apple Silicon only
Owner decision 2026-07-10: skip Intel Mac measurements and gate the future
frame-copy engine on arm64. Intel Macs able to run the app at all are a
shrinking 2015-2020 cohort and keep the docked/external/web player paths;
the macOS hardware gate closes with the M1 Pro numbers, and remaining
hardware risk moves to the Windows/Linux ports.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* feat(embedded-mpv): frame-copy helper process and shm frame reader (native layer)
iptvnator_mpv_helper: one-process-per-session libmpv host that renders
offscreen at viewport size (headless CGL + async PBO ring, validated in
spikes/mpv-frame-copy), publishes BGRA frames into a seqlock shm ring with
resize generations, plays audio directly, and speaks a stdio protocol —
tab-separated commands in, JSON events out. The snapshot event mirrors
NativeEmbeddedMpvSessionSnapshot; status semantics (END_FILE reasons,
eof-reached with keep-open, pause gated on loaded path, fatal-only status
flips) are ported from embedded_mpv.mm.
embedded_mpv_frame_reader.node: plain-C N-API reader the preload script
uses to memcpy the newest complete frame into a V8 ArrayBuffer (Electron's
memory cage forbids zero-copy). Stub exports off macOS.
Both build as extra binding.gyp targets through build-embedded-mpv.js; the
helper gets the same libmpv dependency-path rewrite + ad-hoc re-sign as the
addon and is validated by the forbidden-link check.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* feat(embedded-mpv): frame-copy engine wiring in main process and preload
EmbeddedMpvFrameCopyAdapter implements the NativeEmbeddedMpvAddon surface
over a per-session helper process (spawn, stdio protocol, snapshot cache,
graceful quit->SIGTERM->SIGKILL teardown), so EmbeddedMpvNativeService
reuses its polling/diff/power-blocker/recording logic unchanged. The
IPTVNATOR_ENABLE_EMBEDDED_MPV_FRAME_COPY flag (darwin/arm64 only) routes
getAddon() to the adapter and reports engine: 'frame-copy' in support.
The preload frame pump loads the shm reader addon, copies the newest frame
once per rAF into a reused buffer, and uploads it to WebGL2 on the
renderer's canvas — no frame data crosses the contextBridge; the bridge
only gains attachEmbeddedMpvFrameView/detachEmbeddedMpvFrameView. The
experiment flag relaxes the window sandbox for that native require;
contextIsolation and nodeIntegration:false stay on.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* feat(embedded-mpv): frame-copy canvas mode in the player component + docs
EmbeddedMpvPlayerComponent renders <canvas data-embedded-mpv-frame> when
support reports engine 'frame-copy' and the session controller starts/stops
the preload frame pump around the session lifecycle. The bounds provider
skips HIDDEN_BOUNDS and the popover cutout for this engine — the canvas is
ordinary DOM, dialogs and popovers stack above it natively; bounds sync
still drives the helper's render size. Adapter unit tests cover spawn args,
snapshot caching, shm generations, protocol encoding, unexpected-exit
mapping, and dispose escalation. Architecture doc and CLAUDE.md describe
the engine, its flag, and the sandbox trade-off.
Verified end to end in the built app (M1 Pro): engine detection, helper
spawn, lavfi playback onto the canvas via CDP-injected smoke — including an
orientation fix (helper FLIP_Y already yields texture-order rows; the pump
shader must not flip uv again).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(embedded-mpv): close helper stdin on dispose + lifecycle logging
Live testing surfaced a stray idle helper that survived a session switch;
until the root cause is pinned down, dispose now also closes the child's
stdin (the helper exits on EOF) as a second kill path besides quit ->
SIGTERM -> SIGKILL, and spawn/dispose/exit are logged with the session id
so leaks are attributable.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(embedded-mpv): reap sessions when the renderer reloads or crashes
Root cause of the stray idle helper found during live testing: session
teardown lives in the renderer's Angular lifecycle, which never runs on a
renderer crash or hard reload — the main process kept the session (and its
frame-copy helper process / native mpv handle) alive until app shutdown.
EmbeddedMpvNativeService now watches the main window's webContents for
render-process-gone and did-navigate (full reloads only; in-app Angular
routing emits did-navigate-in-page) and disposes every session. Applies to
both engines. Verified live: location.reload() during frame-copy playback
logs 'Disposing 1 session(s): renderer reloaded' and the helper exits
cleanly. Regression test drives both events against the service.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* feat(embedded-mpv): Settings toggle for the frame-copy engine
New 'Embedded MPV: frame-copy engine' checkbox in Settings > Playback,
shown only when the machine can run it (macOS arm64 with the helper binary
present — support now reports frameCopyAvailable). The choice persists to
the main-process config store because the engine relaxes the window sandbox
for the preload frame pump, which is fixed at window creation: main.ts
reads the store before creating the window and sets the engine env var; an
explicitly set env var (including '0') always wins, and the UI shows a
restart hint while the saved choice differs from the active engine.
Localized in all 18 locales.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* feat(embedded-mpv): aspect-fit rendering in the frame-copy helper
The helper now observes dwidth/dheight and renders its FBO at the
aspect-fit size of the video inside the requested viewport, bumping a shm
generation on change — letterbox bars are never baked into frames (the VOD
watch shell's ~2:1 box no longer shows black side bars; the canvas
background is transparent so the sides show the app surface, while
fullscreen keeps its black backdrop). Frames also get smaller than the
viewport when aspects differ, trimming copy cost. Aspect override changes
refit automatically. Snapshots now carry videoWidth/videoHeight, and the
adapter forwards IPTVNATOR_EMBEDDED_MPV_AUDIO_DELAY to mpv's audio-delay
for lip-sync tuning until proper calibration lands.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* feat(embedded-mpv): require frame-copy artifacts in macOS package validation + docs
macOS packages that ship embedded_mpv.node must also ship the
iptvnator_mpv_helper binary and the embedded_mpv_frame_reader.node addon —
they come out of the same binding.gyp run, and a package missing them would
silently lose the frame-copy engine. Covered in the package-identity test.
Architecture doc and CLAUDE.md document the Settings toggle, aspect-fit
rendering, audio-delay passthrough, and the renderer-reload session reaping.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(electron): inline TS helpers so the sandboxed preload keeps working
The frame pump's async/await (target es2015 + importHelpers) made webpack
externalize tslib in main.preload.js. Sandboxed preloads can only require
Electron's built-in module whitelist, so the entire preload script failed
to load and window.electron disappeared for every run without the
frame-copy flag. importHelpers:false for electron-backend keeps the preload
bundle self-contained — and future async code in preload can no longer
silently reintroduce the breakage. Verified live: sandboxed run now has the
bridge, reports engine 'native' and frameCopyAvailable true.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* docs(embedded-mpv): commit the frame-copy analysis handoff + source inventory
The 2026-07-10 analysis that led to this branch now lives next to the spike
(spikes/mpv-frame-copy/ANALYSIS.md), and the architecture doc's What To
Commit section lists the frame-copy engine sources.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(embedded-mpv): address review findings on the frame-copy engine
- Stale pump attach can no longer win over a newer session: attach/detach
bump a shared epoch and async attach waits re-check it after every await,
so an attach for a replaced session aborts instead of installing itself
(greptile P1).
- A failed frame-view attach (no canvas, no WebGL2, reader missing) now
disposes the session and surfaces the error UI instead of leaving audio
playing behind a black canvas (codex P2).
- A stale frame-copy opt-in without the helper binary falls back to the
native engine instead of reporting embedded MPV unsupported, and the
Settings checkbox stays visible while a saved opt-in exists so it can
always be cleared (codex P2). Regression test covers the fallback.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(packaging): make the darwin frame-copy packaging test host-agnostic
On non-macOS CI hosts validatePackagedEmbeddedMpv also reports that macOS
link validation needs a macOS host, so the success-path assertion now
checks only the frame-copy artifact requirement instead of expecting an
empty error list.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* docs(embedded-mpv): Windows/Linux porting handoff for the frame-copy engine
Self-contained entry point for porting sessions on other machines: current
state and coordination constraints, per-OS task lists (Linux EGL first,
then Windows WGL + named shm — the decisive iGPU perf gate), the
hard-won gotchas from the macOS integration (preload/tslib sandbox
breakage, V8 memory cage, frame orientation, stale-attach epoch, dispose
escalation, node-gyp naming, snapshot protocol semantics), testing
recipes, and the suggested milestone order.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* docs(embedded-mpv): branching and merge strategy in the porting handoff
Port work goes to stacked branches off the frame-copy branch (PR base =
frame-copy branch, sequential merges, stack depth one), never into the
frozen PR #1169 branch itself.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* docs(embedded-mpv): drop stale uncommitted note from porting handoff
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(embedded-mpv): harden frame-copy helper startup
---------
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
* feat(dashboard): TMDB trending rail and hero enrichment (backdrop, badges, S/E)
Two dashboard additions, both async after first paint so the page renders
exactly as fast as before:
Trending rail ("Trending this week", dashboardRails.tmdbTrending toggle,
default on, rendered only when TMDB is opted in AND the Electron DB
worker is available):
- TmdbTrendingService fetches /trending/{movie,tv}/week (one request
each, cached one day per language in tmdb_metadata under
trending:week), merges by popularity; exposed via the enrichment
facade (getTrendingWeek)
- DashboardTrendingService matches the titles against imported Xtream
playlists with ONE batched DB_MATCH_TITLES request, applying the same
two-tier + year-compatibility rule as actor pages; matched cards show
the playlist name and navigate straight to the detail view, unmatched
cards open the global search prefilled (?q=)
- The load fires only after the dashboard's own recent/favorites data
is in (never competes for the worker at startup) and once per session
- DashboardRailCard gained optional queryParams for the search links
Hero enrichment:
- DashboardHeroTmdbService patches the hero with a TMDB backdrop (only
when the item has none), a rating badge and up to two genre chips —
via the enrichment facade, so previously opened items resolve from
the SQLite cache without network; memoized per title per session,
staleness-guarded against hero changes in flight
- Series heroes show the tracked "S{n}·E{n}" badge from the playback
position; the watch-progress bar no longer applies to live heroes
Settings: new dashboardRails.tmdbTrending toggle in Settings > Dashboard.
i18n: 3 new keys translated into all 17 locales via tools/i18n patches.
Tests: dashboard-trending.service.spec.ts (gating, matching, year guard,
single-flight); settings fixtures updated. Docs updated
(tmdb-metadata-enrichment.md Dashboard Integration section, CLAUDE.md).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(dashboard): resolve hero TMDB extras for Stalker embedded-series items
Stalker vclub items carry type 'movie' in activity rows but are TV shows
on TMDB, so the hero's movie lookup found no confident match and the
backdrop/badges never appeared — while the detail view (which resolves
via is_series) showed them. When a movie-typed hero item has no movie
match, retry the lookup as TV: the detail view has usually already
cached that resolution, and misses are negative-cached.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* feat(tmdb): cross-portal "Similar" rail — Stalker gets it, Xtream gains other-portal matches
The Similar rail only existed on Xtream because it matched against the
locally loaded catalog; Stalker catalogs are server-paginated, so its
detail views had no rail despite tmdb_recommendations being cached.
New CrossPortalSimilarService (libs/services) matches recommendations
against ALL imported Xtream playlists with one batched DB_MATCH_TITLES
worker request — the same two-tier normalized-title + year-compatibility
rule as actor pages and the trending rail. Electron-only; resolves to []
in the PWA.
- Stalker: the shared VodDetailsComponent (movies; covers catalog and
inline detail hosts) and stalker-series-view (series) now render a
"Similar" rail from cross-portal matches, each card badged with the
source playlist and navigating into that portal's detail view.
- Xtream: vod/serial detail rails keep instant local-catalog matches and
append cross-portal matches (current playlist excluded, deduplicated
against local hits by normalized exact title), also playlist-badged.
- Loads async after the detail view renders, staleness-guarded; the
section only appears when there is something to show.
Tests: cross-portal-similar.service.spec.ts (PWA gate, navigation
targets, playlist exclusion, type/year guards). Docs updated.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(tmdb): drop TestBed from cross-portal similar spec
The services Jest target has no @angular/core/testing (same CI failure
as the cache spec earlier) — construct the service via Injector.create +
runInInjectionContext instead.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(dashboard): address PR review — reactive opt-out gates, retry after empty trending load
- Trending rail and hero TMDB extras now vanish immediately when the
TMDB opt-in is switched off mid-session: the render computeds read the
settings signal through isAvailable/isEnabled instead of trusting data
loaded earlier (Codex P2 ×2).
- loadedOnce latches only after a successful non-empty load, so a
transient TMDB outage on first visit no longer suppresses the rail for
the whole session — the next dashboard visit retries (greptile P2).
- Unified the duplicated heroTmdbExtras() read in the hero computed
(greptile P2).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
* feat(tmdb): opt-in TMDB metadata enrichment for Xtream and Stalker portals
Adds an opt-in TMDB integration (Settings > Metadata) that enriches
detail views with a field-level merge — the provider stays authoritative
for stream data, TMDB fills editorial fields when the match is confident.
Enrichment:
- Movie/series details: plot, cast (avatar chips), director, genres,
rating, poster/backdrop, official YouTube trailers
- Confidence-gated matching: provider tmdb_id trusted; otherwise
normalized-title search with year gate (±1; series accept earlier
premieres), season-suffix stripping, Cyrillic search-language override,
and language-prefix fallback variants
- Lazy season/episode enrichment: real episode names, overviews, stills
- "Similar" rail (Xtream): TMDB recommendations matched to the catalog
- Actor pages per portal with full filmography, availability filter and
an Electron-only "All portals" scope backed by a batched DB_MATCH_TITLES
worker op over the trigram FTS index
Infrastructure:
- SQLite cache table tmdb_metadata (details, search verdicts, seasons,
persons; per-language, TTL-guarded), in-memory fallback for the PWA
- Settings: enable toggle, own-API-key override with a live "check key"
button; TMDB attribution in Settings and About
- Embedded key stays an empty placeholder; CI injects TMDB_API_KEY via
tools/tmdb/inject-tmdb-key.mjs when the secret is configured
- normalizeTitle shared between renderer and DB worker
- CSP: allow YouTube embeds (frame-src was 'none'; trailers never worked)
Fixes and refactors along the way:
- fix(stalker): Advanced Search sent bare get_ordered_list requests and
skipped the auth handshake when isFullStalkerPortal was missing on the
active-playlist meta — full portals answered "Authorization failed."
and search looked empty; now mirrors the catalog request shape and
routes through makeAuthenticatedRequest with URL-based detection
- fix(stalker): TMDB fields survive info re-normalization; detail views
prefer the store copy patched by async enrichment over stale snapshots
- refactor(xtream): split oversized vod/serial detail components into
component-scoped playback services; detail routes re-initialize on
route param changes (router reuses them for detail-to-detail nav)
- i18n: all new keys translated across the 18 locales
Docs: docs/architecture/tmdb-metadata-enrichment.md + CLAUDE.md updates.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(tmdb): provide route params observable to inline collection details, linearize regexes
The global-collection inline detail host builds a fake ActivatedRoute for
VodDetailsRouteComponent/SerialDetailsComponent with only snapshot.params.
Since the detail components now read route.params via toSignal() (detail->
detail re-init), the missing observable crashed component construction and
the content hero never rendered — broke dashboard-activation, favorites and
recent Electron E2E on all platforms. Provide the params observable
alongside the snapshot and assert it in the component spec.
Also resolves both CodeQL js/polynomial-redos alerts: bracket-stripping in
normalizeTitle now excludes opening delimiters inside the classes, and
youtubeEmbedUrl extracts watch?v= ids with a linear two-pass match instead
of "watch\?.*v=". Combining-diacritics range rewritten as explicit \u
escapes (greptile note).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(tmdb): surface TMDB-only VOD score in the rating badge, drop youtube.com from CSP
Review follow-ups on PR #1123: the Xtream VOD detail badge renders
rating_imdb, but the merge wrote the TMDB score only into `rating`, so a
TMDB-only score was never displayed (Codex P2) — fill rating_imdb when the
provider left it empty, mirroring the Stalker merge. All trailer iframes
are normalized to youtube-nocookie.com, so the extra youtube.com frame-src
allowance was dead surface (greptile) — removed.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(tmdb): resolve confirmed review findings — matching correctness, race guards, cache schema
Fixes the confirmed findings from the PR #1123 code review:
- Stalker search: setSelectedContentType now runs BEFORE setSelectedItem,
so the TMDB enrichment gate in the selection hook no longer sees the
content type of the previously open tab (wrong/no enrichment after
ITV -> search -> movie).
- Title normalization is now two-tier (normalizeTitleKeys): the exact
normalized form keeps a trailing year, the base form strips it and
remembers the tag. Year stripping is anchored to the end of the title
("2001: A Space Odyssey" keeps its year) and language-prefix stripping
is UPPERCASE-only ("It: Chapter Two" is no longer amputated).
- All catalog matching (similar rail, actor pages, DB worker
DB_MATCH_TITLES) compares exact forms first and only accepts
year-stripped matches when the stripped tag is year-compatible (+-1)
with the TMDB year — "Blade Runner" (1982) can no longer claim a
catalog "Blade Runner 2049". CatalogTitleMatch carries the stripped
trailingYear so the renderer can apply the guard to worker matches.
- mergedBackdrops tolerates a plain-string backdrop_path; enrichment
merge+patch blocks are wrapped in try/catch so a malformed provider
payload can no longer become an unhandled rejection.
- loadGlobalMatches (both actor routes) guards against actor->actor
navigation races — a slow match for the previous person no longer
overwrites the current one's results.
- tmdb_metadata media_type CHECK widened to ('movie','tv','person') and
person rows now use the honest 'person' type (TmdbCacheMediaType).
Pre-release dev DBs with the narrow CHECK are rebuilt in place — the
table is a pure cache, so the migration is a self-healing
drop-and-recreate keyed off sqlite_master.
Docs updated (tmdb-metadata-enrichment.md, CLAUDE.md). New regression
coverage: title-normalization.util.spec.ts, two-tier cases in
tmdb-similar.util.spec.ts and title-match.operations.spec.ts.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.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>
* fix(settings): show current release notes
* fix(updater): use app version for local release notes
* fix(settings): constrain release note images
* fix(settings): prefer current notes without updates
Adds experimental embedded MPV support for Windows and Linux, expands packaging/runtime validation, and updates CI coverage for the cross-platform native addon.
Live TV channels in Xtream playlists go blank whenever the provider's
get_short_epg returns nothing, even when the user has working XMLTV
URLs in Settings. The XMLTV pipeline already populates epg_programs
but only the M3U module was reading it.
Wire those uploads up as a fallback for Xtream too: when the provider
returns nothing for a channel with an epg_channel_id, look it up in
the local table. A settings toggle flips the priority for users whose
curated XMLTV is better than the provider's auto guide.
EpgQueueService.enqueue() became async to batch the XMLTV lookup once
per viewport change. To keep the queue consistent under fast scroll,
all shared-state mutations now happen behind a generation counter so
only the latest call commits. Per-method bridge gating in the new
XtreamXmltvFallbackService keeps each path working when the preload
exposes only one of the two endpoints.
VLC was unconditionally spawned per click — VLC's own single-instance
preference fails because the per-launch RC args defeat its D-Bus
forwarder. Mirror the existing MPV reuse pattern so users can opt in to
driving one tracked VLC via its RC interface (clear + add) instead of
opening a new window every stream.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Entire-Checkpoint: f957cd9849e0
- 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
- Updated confirmation dialog messages to provide detailed consequences of deleting playlists.
- Added backup hints and progress messages for better user experience during playlist deletion.
- Ensured consistency in language across all supported translations (de, el, en, es, fr, it, ja, ko, nl, pl, pt, ru, tr, zh, zhtw).
Entire-Checkpoint: c6e522b4276c