Commit Graph
2550 Commits
Author SHA1 Message Date
4grayandClaude Opus 4.8 5aa44d19d4 feat(tmdb): clickable director/creator chips and directing credits on person pages (#1227)
* feat(tmdb): clickable director/creator chips and directing credits on person pages

Directors were plain merged text — no photos, no navigation — while the
data was already sitting in the cached TMDB payloads (credits.crew and
created_by both carry id + profile_path; they just were not typed or
parsed).

- tmdb-merge: enrichedDirectors (crew, job === 'Director', deduped by
  person id) and enrichedCreators (created_by) produce the same chip
  shape as the cast (TmdbEnrichedCastMember) into a new tmdb_directors
  field on all three merges (Xtream VOD, Xtream series, Stalker); types
  widened (crew id/profile_path, created_by id/profile_path).
- Detail views (shared VodDetailsComponent, Xtream vod/serial routes,
  Stalker series view) render the Director row as clickable avatar chips
  when tmdb_directors is present — same markup and openActor handler as
  the cast strip — falling back to the plain text otherwise. Stalker
  re-normalization allowlist preserves the new field.
- Person pages: mapPersonFilmography now merges combined_credits.crew
  (jobs Director/Creator) into the filmography — acting wins the
  per-title dedup, directing-only titles show the job in the character
  slot. Everything else (library matching, All-portals scope, filters,
  search fallback, back button) works unchanged because the person page
  is role-agnostic. Existing caches work as-is: crew/created_by were
  always part of the stored payloads.

Tests: merge spec (director/creator chips + crew-row dedup ×3 merges),
person spec (crew credits, Producer excluded, acting-wins dedup),
stalker-vod.utils passthrough. Docs updated (CLAUDE.md + architecture).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix(tmdb): address director-pages review — split oversized spec, stable track keys, translated crew roles

- tmdb-merge.spec.ts grew past the 400-line lint ceiling — the Stalker
  merge suite moved to tmdb-merge-stalker.spec.ts (fixes the CI Lint job).
- All cast/director chip loops now track by TMDB person id with an
  index fallback ('p<id>' / 'i<index>') instead of member.name — distinct
  people can share a name and creator payloads carry no dedup (greptile).
- Directing-only filmography credits carry the role in a new crewJob
  field ('Director' | 'Creator') instead of stuffing TMDB's raw English
  job into character; ActorViewComponent renders it through translated
  labels (XTREAM.CREW_JOB_DIRECTOR/CREATOR, added to all 18 locales via
  the i18n patch workflow, matching each locale's existing glossary —
  pt "Diretor", de "Regisseur") (Codex).

Tests: person spec asserts character/crewJob separation; merge suites
green after the split (15 + stalker file).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-24 08:42:41 +02:00
4grayandClaude Fable 5 59c15493a7 docs: sync CLAUDE.md, AGENTS.md and architecture docs with actual code
Full audit of CLAUDE.md, AGENTS.md, README.md and docs/architecture/
against the codebase; every fix is backed by current code:

- remove documented-but-unimplemented IPTVNATOR_DISABLE_HARDWARE_ACCELERATION
  flag (no reads anywhere in apps/, libs/, tools/)
- CLAUDE.md: add epg_channel_mappings to the schema table list
- m3u-playlist-module: *-tab dirs -> *-view (+recent-view), selectActivePlaylist,
  real PlaylistState shape, ChannelEpgMetadata instead of removed EnrichedChannel,
  actual /workspace/playlists routes, per-view outputs, live-epg-panel-state key
- workspace-dashboard: per-rail Settings.dashboardRails toggles, three missing
  rails in the diagram, split live-favorites/recent-live rails,
  welcome-dashboard empty-state type, RECENTLY_WATCHED_LIVE_TV title key
- stalker-portal: CategoryContentViewComponent for vod/series, collection-route
  components for favorites/recent, corrected series-view/favorites-button paths,
  actor/:personId route, epg panel selectors
- category-management: reloadCategories lives in with-content.feature.ts,
  workspace-context-panel owns the dialog, XtreamPendingRestoreService flow
- stalker-mock-server (+app README): scenario-seeded faker, resetAll() clears
  content cache too, ordinal season episode ids, handlers/ dir location
- sqlite-db-worker: cancellation shipped (drop from out-of-scope), full
  operations module list
- portal-detail-navigation: replace three removed component paths
- tmdb-metadata-enrichment: details cache keys are id:<tmdbId>|v2
- electron-security: CSP frame-src youtube-nocookie exception,
  sandbox: !frameCopyExperiment nuance
- download-manager: libs/portal/xtream instead of xtream-electron folder,
  data-driven downloads nav, drop removed app-search-result-item note
- playlist-backup-restore: settings-backup facade owns the import handoff
- workspace-shell: functional workspaceEntryRedirect, playlists route children
- iptvnator-ui-guidelines: EPG card radius 11px, detail-view mixin is `base`
- embedded-mpv-native, player-controls-contract: minor precision fixes

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-24 08:16:33 +02:00
4grayandClaude Opus 4.8 db70b07093 feat(playback): theater stage and opt-in ambient fill for the inline portal player (#1223)
* 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>
2026-07-24 08:02:11 +02:00
4gray b3e130aa65 feat(stalker): full Live TV channel list for complete search, count badges, and all-channels grid (#1209)
Load the complete ITV channel list once per portal (Ministra get_all_channels with a paged crawl fallback) and use it for: full local search across every channel, per-genre category count badges, an all-channels paginated grid on Live TV entry, and eager bulk EPG so row previews appear without playing a channel. Censored (adult) genres absent from get_all_channels fall back to the legacy paged flow and show no badge. Includes mock-server support, unit + E2E coverage, and architecture docs.
2026-07-23 23:31:49 +02:00
Salemand4gray e53adcbeaf fix(catchup): parse negative sub-hour XMLTV offsets correctly (#1216)
* fix(catchup): parse negative sub-hour XMLTV offsets correctly

XMLTV timestamps with offsets like "-0030" were treated as UTC because the
hour part "-00" numerically evaluates to -0 and Math.sign(-0) dropped the
minutes' sign. Derive the sign from the offset string instead, so
sub-hour negative offsets shift the catch-up start time correctly.

* test(catchup): cover positive sub-hour XMLTV offsets

---------

Co-authored-by: 4gray <serega05@gmail.com>
2026-07-21 09:08:37 +02:00
4gray d308749e2c fix(stalker): preserve is_series episode metadata (#1218) 2026-07-21 07:51:37 +02:00
4gray 48ff4b9cc5 fix(portal): remove redundant catalog type badges (#1217)
Remove redundant LIVE, VOD, and SERIES badges from homogeneous Xtream and Stalker catalog grids while preserving mixed-content badges and type-driven grid behavior.
2026-07-20 21:52:00 +02:00
4grayandClaude Fable 5 bc6e7018e0 fix(epg): harden three latent edges from the #1165 manual-mapping review (#1214)
* fix(epg): harden three latent edges from the #1165 manual-mapping review

Follow-up to #1165 (manual EPG-to-channel mapping). Three minor but real
issues flagged by the bot reviews on #1173, all in already-merged #1165
code rather than the Stalker delta:

1. EPG program dedup ignored source_url. The unique index and upsert key
   (channel_id, start, title) collapsed programmes imported from different
   XMLTV sources that shared those columns, and the upsert reassigned
   source_url to the last importer — so source-scoped queries could miss a
   programme and source-scoped deletes could drop another source's row.
   The key and index now include source_url (migrated via a _v2 index that
   drops the old source-blind one); the upsert no longer overwrites
   source_url.

2. Xtream getMapping fallback capped candidate streams at an unordered
   first five, so a mapping saved under a later stream sharing the
   provider epg_channel_id was silently ignored. Replaced the two-step
   fetch-then-lookup with a single content⋈categories⋈mappings join that
   finds a mapping under any matching stream, with no arbitrary cap.

3. The Xtream mapping dialog did not refresh after closing, unlike the
   Stalker path, so a remapped visible/selected channel kept its stale
   preview until the 5-minute TTL or a rescroll. Added
   EpgQueueService.invalidate(streamId) and a before/after mapping compare
   in the channel-list dialog flow that invalidates the cache and refetches
   the current viewport when the mapping actually changed.

Tests: source-aware dedup index/upsert assertions; join-based getMapping
resolution regardless of stream position; EpgQueueService.invalidate.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(epg): address review feedback on the #1165 follow-up

- portal-channels-list: forward the already-validated playlistId into the
  mapping dialog instead of re-reading currentPlaylist() after the async
  getEpgMapping roundtrip (which could return undefined on navigation)
- EpgQueueService.invalidate(): bump a per-stream invalidation epoch and
  clear inFlight so a request already running when the mapping changes has
  its (pre-change) result discarded via an epoch check in fetchEpg, and the
  immediate re-enqueue can schedule a fresh mapping-aware fetch
- getMapping Xtream fallback: order the join deterministically before
  limit(1) so the resolved mapping is stable; documented that this backend
  layer has no caller playlist context and is a best-effort net behind the
  renderer's playlist-scoped resolution

Tests: in-flight staleness discard for invalidate().

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* test(epg): split EpgQueueService invalidation specs under the max-lines limit

The added invalidate() tests pushed epg-queue.service.spec.ts to 415 lines,
over the 400-line ESLint cap (and the baseline must not grow). Moved them to
a focused epg-queue-invalidation.spec.ts; both files are now under the limit.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(epg): resolve second-order review findings on the mapping follow-up

Two P2 issues Codex raised on the previous fixes:

- Unscoped program lookups could return the same programme twice now that
  the dedup index preserves per-source rows: the M3U timeline calls
  getChannelPrograms without sourceUrls, so two sources sharing
  channel/start/title both surfaced. Added toEpgProgams(), which collapses
  duplicate channel|start|title slots after mapping/validation, applied at
  every getChannelPrograms return.
- EpgQueueService.fetchEpg unconditionally cleared the in-flight marker in
  its finally, which could drop a marker a re-enqueued request took over
  after invalidate(). It now only releases the marker when the completing
  request still owns it (epoch unchanged), preserving per-stream dedup and
  concurrency accounting.

Tests: unscoped duplicate-slot collapse; stale request preserving a fresh
in-flight marker.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(epg): deduplicate program rows in SQL before applying row limits

Codex follow-up: the JS-level slot dedup ran after the SQL LIMIT, so
duplicate cross-source rows consumed the cap and truncated real data.
Moved the dedup into SQL with GROUP BY, applied before the limits:

- selectChannelPrograms / selectLegacyChannelPrograms: GROUP BY
  (channel_id, start, title) before ORDER BY start LIMIT 500, so the
  timeline cap counts distinct programmes rather than duplicate rows
- selectCurrentProgramsForChannelIds: GROUP BY channel_id before
  LIMIT channelIds.length, so duplicate cross-source current slots can't
  starve other channels of their current-programme preview

The JS toEpgPrograms() dedup stays as a safety net (e.g. legacy NULL-source
rows the unique index treats as distinct). Test query-chain mocks updated
for the new groupBy link.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-19 20:21:40 +02:00
4grayandClaude Fable 5 402382421c feat(matching): strip appended language/quality tags in title normalization (#1211)
* feat(matching): strip appended language/quality tags in title normalization

Real-world portal catalogs duplicate one show under dozens of tagged
variants ("|ALB| Fallout", "4K-DE - The Pitt (2025) (US)",
"Breaking Bad-eng", "Fallout_esp", "The Last of Us (2023) AF"). A third
of them normalized to polluted keys, silently skipping TMDB enrichment
and staying invisible to cross-portal title matching.

normalizeTitleKeys() now handles, conservatively:
- wrapped pipe tags:      "|ALB| X", "|MULTI| X"
- longer/compound leads:  "EXYU| X", "4K-DE - X", "AR-SUBS - X",
                          "4K-OSN+ - X" (dash/pipe only; colon stays
                          2-3 chars so "NCIS: LA" is untouched)
- underscore suffixes:    "X_eng", "(US)_msub" (single-underscore only,
                          "The_Last_of_Us" stays intact)
- double-dash suffixes:   "X--esp"
- joined dash tags:       "X-DE", "X-eng" (vocabulary-gated and
                          case-uniform only; "Spider-Man", "Kick-It",
                          "Peut-être" are untouched)
- bare trailing tags:     "X (2025) DE", "Breaking Bad ES" (UPPERCASE
                          vocabulary only, skipped for ALL-CAPS titles;
                          "Rocky II", "Made in USA", "Making It" are
                          untouched)

Every leading-tag segment must contain a letter, so numeric titles
("1917 - ...") are never treated as tags. The display-side
stripCountryPrefix() learns the same compound/plus-sign prefixes and the
numeric guard.

buildSearchLookupKey() gets a |v2 suffix so cached negative TMDB match
resolutions keyed on old polluted titles are invalidated.

Measured on 248 real catalog names from four shows (The Pitt, Fallout,
The Last of Us, Breaking Bad): clean matching keys 65% -> 99%, display
strip 91% -> 100%. The corpora are committed as spec fixtures.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(matching): use ES2015-safe trailing trim in title normalization

String.prototype.trimEnd is ES2019; the shared-interfaces lib compiles
against an older lib target (TS2550 in typecheck:web). Replace with a
regex-based trimRight helper. Jest uses its own tsconfig, so this only
surfaced in the CI typecheck, not local unit runs.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(matching): guard tag stripping against real-title false positives

Address code-review findings on the tag-stripping rules:

- underscore suffix is now vocabulary-gated, so "Mr_Robot",
  "Cowboy_Bebop", "Mrs_Davis" keep their second word
- leading single-segment tags before a spaced dash stay 2-3 chars
  (only hyphen-compounds like "4K-DE" and pipe-tags like "EXYU|" may be
  wider), so "DUNE - Part Two" and "ALIEN - Covenant" are left intact
- "IN" is excluded from the weak joined-dash/underscore paths so
  "drive-in" and "Plug-in" are not truncated (India still strips via
  the strong "IN| " / "IN - " forms)

The display-side stripCountryPrefix() mirrors the narrowed dash rule.
Corpus coverage is unchanged at 99% (245/248); new counter-example
tests lock in the guards.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-19 19:42:20 +02:00
4grayandClaude Fable 5 aa1941cf2c fix(embedded-mpv): stop native-view video jump by moving control menus into the dock (#1207)
* fix(embedded-mpv): stop native-view video jump by moving menus into the dock

Opening any control popover in the native-view embedded MPV dock used to
shrink the MPV view by a 300 px bottom cutout so the popover DOM stayed
clickable, which made mpv re-letterbox the video on every menu open/close.

All five menus now render horizontally inside the fixed-height controls
strip, so menu state never changes the native view bounds:

- volume expands as an inline horizontal slider next to the mute button
- audio/subtitle/speed/aspect morph the dock row into a back button, a
  panel title, and a scrollable chip ribbon (app-embedded-mpv-dock-panel)
  with wheel-to-horizontal-scroll mapping, edge fades, active-chip
  reveal/focus, roving arrow-key navigation, ellipsis + tooltips, and
  RTL-aware scrolling
- boundsProvider loses the menus.anyOpen() cutout branch and the
  MENU_OPEN_BOTTOM_CUTOUT_PX constant is removed; HIDDEN_BOUNDS for modal
  overlays is unchanged
- global arrow shortcuts (seek/volume) are suspended while a chip panel
  is open so arrows walk the chips; Esc, click-outside, and close-on-select
  semantics are preserved
- new EMBEDDED_MPV.PLAYER.BACK i18n key in all 18 languages

Regression coverage: the new dock-panels spec asserts the bounds provider
returns full host bounds while every menu is open (fails against the old
cutout behavior), plus panel morph/a11y/selection specs and a dedicated
dock-panel component spec (keyboard, wheel, tabindex, emits).

Frame-copy shared controls (app-player-controls) are untouched.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(embedded-mpv): address native-view dock review feedback

Resolves the actionable P2 review comments on the dock rework:

- Inline volume no longer clips the dock actions. At sidebar-constrained
  player widths (~480-660px, viewport wider than the 720px breakpoint) the
  new in-flow volume slider widened the non-shrinking actions column and,
  under overflow:hidden, clipped the fullscreen button. Add min-width:0 to
  .embedded-mpv-player__actions and __volume-group so the inline volume (a
  scroll container) compresses its own slider instead of pushing neighbors
  off-edge. Verified in Chromium: fullscreen stays visible down to 480px.
- Space now selects a focused chip. onPanelKeydown stops Space/Enter from
  bubbling to the global shortcut handler (whose Space case preventDefault'd
  the button's native activation and toggled playback) without calling
  preventDefault itself, so the menuitemradio chip activates and emits
  chipSelected. Matches the WAI-ARIA menu activation-key expectation.
- Simplify dock-panel opener tracking: always remember the toggled kind so
  focus restoration is correct if in-panel switching ever becomes reachable
  (currently unreachable — the toggle buttons are removed from the DOM while
  a panel is open); restoreOpenerFocus still no-ops unless focus fell to body.

Not changed: the "closePanels no-ops when unavailable" comment — verified
unreachable (chip selection closes via menus.close() directly, not through
closePopovers; isAvailable() is engine-bound and the native dock only renders
while it is true, with engine handoff calling menus.closeAll()).

Regression test added for Space/Enter chip activation. The volume-overflow
fix is CSS layout (no jsdom layout engine) and was validated in a real
browser.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-19 18:39:59 +02:00
4grayandClaude Fable 5 c707af1334 feat(epg): manual EPG mapping for Stalker portals (#1173)
* feat(epg): manual EPG mapping for Stalker portals

Extends the manual EPG-to-channel mapping (PR #1165) to Stalker:

- shared key helper buildStalkerEpgMappingKey — playlist-scoped
  stalker:{playlistId}:{channelId} keys, mirroring the Xtream scheme
- ITV sidebar: right-click context menu with "Map EPG channel"; after
  the dialog closes with a change, the bulk EPG cache is rebuilt so the
  panel and row previews reflect the new mapping immediately
- withStalkerEpg().applyMappedItvEpg(): batch-resolves mappings for
  rendered channels (one getEpgMappingsBatch IPC per new id set) and
  overlays uploaded-XMLTV programs onto bulkItvEpgByChannel; overrides
  survive ensureBulkItvEpg reloads
- stream-resolver: mapping check in loadStalkerEpgItems (detail) and
  batched prefetch in loadStalkerEpgBatch (previews);
  mappingCandidateKeys/prefetchEpgMappings generalized beyond Xtream
- global favorites: stalker branch for the Map-EPG menu entry (item id
  extracted from the stalker::{playlistId}::{id} uid)
- docs: manual-mapping section in docs/architecture/stalker-epg.md and
  a CLAUDE.md EPG bullet covering the whole mapping feature
- tests: applyMappedItvEpg suite, stalker preview mapping in the
  stream-resolver spec, key-builder specs

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(epg): harden Stalker mapping edges found in adversarial review

- stream-resolver: stalker items resolve only the playlist-scoped
  mapping key — bare tvgId/name candidates (tvgId mirrors the raw
  provider id) could only produce false matches against unrelated M3U
  mappings, and previews would disagree with the detail path
- applyMappedItvEpg: staleness guard after every await so an in-flight
  call cannot write portal A's mapped EPG into portal B's state after
  a playlist switch (the store is a root singleton)
- applyMappedItvEpg: ids are marked checked only after a successful
  lookup — a transient IPC failure no longer suppresses the mapping
  for the rest of the session
- live layout: post-dialog refresh re-applies overrides for the
  unfiltered channel list, so an active search cannot drop the playing
  channel's mapping
- global favorites: new epgMappingChanged output emitted when the
  dialog actually changed a mapping; unified live tab reloads its EPG
  previews in response

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-19 18:29:23 +02:00
4gray b1fc23abbb fix(playback): route MKV sources as Matroska (#1210)
* docs(playback): design native MKV source routing

* fix(playback): route MKV sources as Matroska

* chore(playback): address MKV review feedback
2026-07-19 15:13:19 +02:00
Salem 61fb563fbd refactor(database): split EPG mapping schema out of oversized schema.ts (#1213)
schema.ts grew past the 400-line lint budget with the manual
EPG-to-channel mapping table (#1165), which breaks lint for every PR.
Move the mapping table and its types into epg-mapping.schema.ts and
re-export them from schema.ts so the schema namespace and all existing
imports stay unchanged.
2026-07-19 12:15:15 +02:00
4grayandClaude Fable 5 ec09778f36 feat(about): show build commit next to the app version (#1208)
* feat(about): show build commit next to the app version

Settings > About now renders "<version> (<short-sha>)" with the full
SHA in the tooltip, so bug reports from test and nightly builds
identify the exact commit. The commit is injected at CI build time into
apps/web/src/environments/build-commit.ts (same placeholder pattern as
the TMDB key inject); PR builds use the real head SHA instead of the
ephemeral merge commit. Local/dev builds keep the plain version.

The semver version itself deliberately stays untouched: a "-sha"
suffix would flip electron-updater into prerelease mode and leak into
installer/artifact version fields.

Requested by WolfganP in #1202.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* style(settings): keep relative import after monorepo alias imports

Addresses Greptile feedback on #1208.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* feat(docker): inject build commit into published PWA images

The Docker/PWA build path bypassed the Electron workflow's inject step,
so published images showed the plain version in About. Pass the commit
as a build arg and run the inject script before the PWA build; the
script no-ops when BUILD_COMMIT is empty, leaving local docker builds
unchanged.

Addresses Codex feedback on #1208.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-19 09:44:08 +02:00
ec6fc403a3 feat: manual EPG-to-channel mapping with mapping fallback (#1165)
* feat(epg): manual EPG-to-channel mapping with mapping fallback in all EPG paths

* fix: epg mapping in live tv list

* fix(epg): harden manual EPG mapping — upgrade safety, perf, playlist-scoped keys

Follow-up fixes on top of the manual EPG-to-channel mapping feature:

- epg-database: dedupe existing epg_programs rows before creating the
  unique (channel_id, start, title) index — a plain CREATE UNIQUE INDEX
  crashed the EPG worker on upgrade when historical duplicates exist;
  replace INSERT OR REPLACE with ON CONFLICT DO UPDATE so the
  epg_programs_fts delete trigger is not bypassed (REPLACE skips delete
  triggers unless recursive_triggers is on), with a plain-INSERT
  fallback when the index cannot be created
- db: add idx_content_epg_channel — the mapping fallback scanned the
  whole content table on every single-channel EPG lookup
- keys: scope Xtream mapping keys per playlist via shared
  buildXtreamEpgMappingKey (xtream:{playlistId}:{id}) — bare stream ids
  collide across portals; the backend fallback now joins categories to
  resolve the playlist id
- pwa: hide "Map EPG channel" entries behind the supportsEpgMapping
  capability — the menu item was a dead end in the PWA
- parser: parse the XMLTV offset sign from the string — Math.sign(0)
  dropped the minutes of ±00:xx offsets
- cleanup: typed window.electron access instead of ad-hoc casts, drop
  unused resolveChannelId and dialog data field, shared
  EpgMappingDialogComponent.open() for all seven call sites
- dialog UX: minimum-characters search hint, save/remove snackbars,
  current mapping shows the EPG channel display name,
  takeUntilDestroyed on the search stream
- i18n: fill the new keys in all 17 locales
- tests: cover mapping CRUD/search escaping, the dedup-index guard,
  offset parsing and playlist-scoped keys; update stale stream-resolver
  specs for the new 50-item limit and 10s timeout

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(epg): escape backslashes in EPG channel search LIKE pattern

CodeQL js/incomplete-sanitization: a lone trailing backslash in the
search term paired with the closing wildcard under the ESCAPE clause
and corrupted the pattern.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* perf(epg): batch mapping lookups in the viewport preview queue

Expose the existing getEpgMappingsBatch operation over a new
EPG_MAPPING_GET_BATCH IPC channel and use it in resolveManualMappings —
the per-entry lookup issued one IPC round-trip per visible channel on
every scroll event.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* perf(epg): batch mapping prefetch in the collection preview loader

Resolve all candidate mapping keys for an Xtream preview batch with a
single getEpgMappingsBatch IPC call instead of per-channel lookups.
Also fix a worker early-exit: a channel without tvgId/name returned out
of the shared-iterator loop and silently killed one of the three
concurrent preview workers.

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>
2026-07-19 09:00:14 +02:00
4grayandClaude Fable 5 45b6d8a041 fix(m3u): parse playlists with URLs longer than 2084 characters (#1204)
* fix(m3u): parse playlists with URLs longer than 2084 characters

Pluto TV style playlists (issue #1189) embed a session JWT in every
stream URL (~2200 chars). validator.isURL inside iptv-playlist-parser
rejected anything over its IE-era 2084-char default, and the parser's
stalled item index then collapsed the whole playlist into a single
channel.

Sync the 4gray/iptv-playlist-parser fork with upstream v0.15.2, which
removes URL validation entirely and adds an explicit branch so '#'
comments and unknown directives are never treated as URLs. Two fork
deltas are preserved on top: the radio attribute (radio player
detection) and pipe stripping (item.url is cut at the first '|' while
|User-Agent=/|Referer= params still land in item.http). The now-dead
validator/is-valid-path dependencies are dropped from the fork.

- pin iptv-playlist-parser to the fork commit SHA
- add a parser contract spec guarding long URLs, comment handling,
  radio, pipe stripping, and header EPG attrs
- document the parser fork contract in the M3U architecture doc

Fixes #1189

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* chore(m3u): bump parser to optimized fork build

Pulls the fork's optimized parse() rewrite (2.5-3x faster: 100k
channels ~780ms -> ~285ms, 10k ~79ms -> ~25ms) and the README
documenting fork deltas. Output is differential-verified byte-identical
to the previous build; all parser-contract, unit, and import E2E suites
rerun green against the new pin.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* chore(m3u): bump parser for input robustness and library hygiene

Pulls the fork's real-world input tolerance: UTF-8 BOM, blank lines and
whitespace before the header, and case-insensitive #EXTM3U no longer
reject the playlist (all VLC-accepted forms); Node Buffers are decoded
as UTF-8 and other non-string input throws a clear TypeError. Also
brings truthful types (url?: string), fork metadata, an enforced 100%
coverage gate, and the fork CHANGELOG.

Extends the contract spec with a BOM regression test.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* chore(m3u): pin parser to the tagged fork release v0.15.2-iptvnator.1

Same commit as before (33f5e9c) — the readable tag replaces the raw
SHA in package.json while pnpm-lock still records the immutable
codeload tarball by commit. Fork release:
https://github.com/4gray/iptv-playlist-parser/releases/tag/v0.15.2-iptvnator.1

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(types): make ParsedPlaylistItem.url optional to match runtime

The parser fork's d.ts now truthfully declares url?: string (a trailing
#EXTINF without a stream URL yields url === undefined at runtime, and
always has). The local ParsedPlaylistItem mirrored the old type lie and
made the production typecheck reject the parser's Playlist type.
createChannel and createPlaylistObject already tolerate the absent url.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-19 08:56:26 +02:00
4gray 5cae310430 fix(logging): redact sensitive portal and Electron diagnostics (#1182)
* fix: redact sensitive log data

* fix(ci): keep logging preload self-contained

* fix(logging): preserve shared diagnostics

* fix(logging): close trace redaction gaps

* fix(logging): redact Xtream path credentials

* fix(logging): close credential redaction gaps

* fix(logging): suppress external player arguments

* fix(logging): harden URL and date redaction

* fix(logging): redact map keys and URL fragments

* fix(logging): redact sensitive map values

* fix(logging): redact credentials in diagnostic text

* fix(logging): close remaining credential leaks
2026-07-19 08:30:21 +02:00
4grayandClaude Fable 5 29e624b0dd ci(release): make test draft releases traceable and self-cleaning (#1202)
* ci(release): make test draft releases traceable and self-cleaning

Every PR and master build created a draft named "Release v<version>"
with tag test-<github.sha>, so 70+ identical drafts piled up and PR
drafts were untraceable (for pull_request events github.sha is the
ephemeral merge-commit SHA that resolves to nothing in the repo).

- Title test drafts as "v<ver> — PR #<n> @ <sha> [test]" /
  "v<ver> — master @ <sha> [test]"; tag releases keep "Release v<ver>"
- Prepend a context header (PR, head commit, workflow run links) to the
  auto-generated release notes
- Use the PR head SHA and pass target_commitish so generated notes
  actually cover the PR commits
- Use stable tags (test-pr-<n>, test-master) so action-gh-release
  updates one rolling draft in place instead of creating a new one per
  push
- Mark all non-tag drafts as prerelease
- Cancel superseded in-progress PR builds via a concurrency group
- Delete a PR's rolling draft when the PR closes (new workflow)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* ci(release): close review-bot race windows in draft release flow

- Move concurrency from workflow level to job level: cancelling a whole
  run could interrupt action-gh-release mid-asset-replacement and leave
  the rolling draft incomplete. Build slots still cancel superseded PR
  work (matrix-aware groups); the release job gets its own serializing,
  never-cancelling group.
- Re-check the live PR state in the release job right before touching
  the draft, so a build that outlives its PR cannot recreate the draft
  after cleanup deleted it.
- In the cleanup workflow, cancel still-running builds of the closed PR
  (dead work anyway) and wait for them to settle before deleting.
- Emit an explicit empty `body=` output for tag builds instead of a
  blank-line heredoc.

Addresses Codex and Greptile review feedback on #1202.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* ci(release): grant actions:write so PR-close cleanup can cancel builds

gh run cancel needs the actions scope; with only contents: write the
cancellation 403s silently and the settle-poll burns its full window.
Also skip the cleanup job for fork PRs entirely: they never get a
draft and their token is read-only regardless of the permissions block.

Addresses Greptile P1 / Codex P2 follow-up on #1202.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* ci(release): re-assert rolling draft title after asset upload

action-gh-release@v2 updates name/body/target_commitish on the normal
draft-reuse path, but in a rare race (release listing transiently
missing the draft) it uploads assets to the canonical oldest draft
without refreshing its metadata. PATCH the title and commitish on the
release id the action actually used, so the draft title always names
the current head SHA; the body is left alone to preserve generated
notes.

Addresses Codex round-2 feedback on #1202.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* ci(release): prune stale assets before updating a rolling draft

The release action only replaces same-name assets, so a PR that bumps
the app version would leave old-version installers beside the new set
in its rolling draft. Delete all existing assets of the matched draft
before the upload; the action re-uploads the full current set right
after. Published releases are never touched.

Addresses Codex round-3 feedback on #1202.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* ci(release): rebuild full draft metadata after asset upload

Extend the post-upload metadata step to also rebuild the body (context
header + notes from the same generate-notes API the action uses), not
just title/commitish. The rolling draft now ends up with correct
metadata regardless of which internal action-gh-release path ran,
including the rare canonicalize-duplicate fallback. If notes
generation fails, the body is left as the action set it.

Addresses Codex round-4 feedback on #1202.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* ci(release): only cancel pull_request runs when cleaning up a closed PR

A manually dispatched build on the same head branch is not the PR's
work; filter the cancellation list by event so PR-close cleanup cannot
abort it.

Addresses Codex round-5 feedback on #1202.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* ci(release): guard PR-close cleanup against close-reopen races

Re-check the live PR state at the start of the cleanup job and again
right before deleting the draft, so a PR that is reopened while the
cleanup is queued or waiting keeps its rolling draft and its fresh
reopened-run builds are not cancelled.

Addresses Codex round-6 feedback on #1202.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* ci(release): keep tag_name when patching rolling draft metadata

PATCHing a draft release without tag_name makes GitHub drop the
pending tag (the draft turns into untagged-<hash>), so the next run
cannot find the rolling draft by tag and creates a duplicate — observed
live on this PR's own drafts. Include tag_name in both PATCH payloads
of the metadata step.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-19 07:59:53 +02:00
4grayandClaude Fable 5 b719ed23cd fix(playback): position embedded MPV native view correctly on scaled displays (#1206)
* fix(playback): position embedded MPV native view correctly on scaled displays

Renderer bounds are measured in CSS pixels, but the native-view engines
position OS windows: SetWindowPos (win32) and XMoveResizeWindow (linux)
expect physical pixels, NSView setFrame (macOS) expects points. The raw
values landed the video toward the window's top-left corner at 1/scale of
its size on any display scale or page zoom other than 100%, windowed and
fullscreen alike.

The main process now converts native-view bounds (x page zoom everywhere,
x display scale factor on win32/linux) with edge-based rounding; frame-copy
bounds stay unscaled because the adapter owns its render scale. The session
controller re-syncs bounds when devicePixelRatio changes, covering moves to
a display with a different scale that keep the CSS layout identical.

Closes #1145

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(playback): keep CSS bounds unrounded until native scaling

Review feedback on #1206: measureBounds() rounded the CSS edges in the
renderer, before the main-process CSS-to-native conversion, so fractional
layout positions could drift by a pixel per scale factor (a 10.49px edge
at 200% must land on 21 physical px, not 20). The renderer now sends raw
getBoundingClientRect() edges and rounding happens exactly once, after
scaling. Also pins process.platform explicitly in the macOS wiring test
instead of relying on the suite default.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-19 07:48:44 +02:00
StefanandClaude 2f8aee72df feat(xtream): add catch-up playback to favorites and recent tabs (#1166)
Enables Xtream catch-up/timeshift from the Favorites and Recent surfaces (per-playlist and global), not just Live TV, and adds start-over replay of the currently-airing programme. Carries tv_archive/tv_archive_duration through the favorites and recently-viewed DB projections and maps them onto UnifiedCollectionItem; tv_archive_duration is interpreted as days, matching live-stream-layout.controlledArchiveDays.

Closes #1138.

Co-authored-by: Claude <noreply@anthropic.com>
2026-07-19 06:38:19 +02:00
4gray c266eaa680 fix(packaging): preserve Flatpak Electron ELF for Zypak (#1205)
Fixes the launcher/Zypak regression reported in #1203. The additional GPU/video.js behavior remains tracked separately in that issue.
2026-07-18 22:42:45 +02:00
4gray 8fdac824fd feat(packaging): ship Linux embedded MPV frame-copy runtime (#1200)
* docs: design Linux frame-copy packaging

* docs: plan Linux frame-copy packaging

* feat(packaging): define Linux frame-copy profiles

* fix(packaging): reject inherited profile names

* feat(embedded-mpv): validate staged Linux runtime

* fix(embedded-mpv): require Linux source packages

* fix(embedded-mpv): harden Linux runtime staging

* feat(embedded-mpv): build LGPL Linux runtime

* fix(embedded-mpv): pin Linux runtime inputs

* feat(embedded-mpv): build relocatable Linux helper

* fix(embedded-mpv): require bundled Linux runtime

* fix(embedded-mpv): make Linux runtime portable

* feat(packaging): ship Linux frame-copy artifacts

* fix(embedded-mpv): verify Linux helper linkage

* fix(packaging): enforce Linux frame-copy isolation

* fix(embedded-mpv): pin Linux display data

* docs(embedded-mpv): document Linux frame-copy packaging

* feat(embedded-mpv): probe Linux frame-copy runtime

* test(embedded-mpv): smoke packaged Linux frame-copy

* docs(embedded-mpv): clarify Linux system runtime baseline

* fix(embedded-mpv): harden Linux runtime capability gate

* ci: verify Linux frame-copy packages

* test(embedded-mpv): harden packaged Linux smoke

* test(embedded-mpv): preserve packaged GL mode

* test(packaging): harden Linux package probes

* fix(embedded-mpv): enable private Snap shared memory

* fix(embedded-mpv): sanitize Linux helper environment

* fix(packaging): enforce private Snap memory semantics

* fix(packaging): reject ambiguous Snap memory metadata

* fix(embedded-mpv): prioritize trusted Snap GL

* fix(packaging): reject advanced Snap YAML semantics

* fix(packaging): reject arbitrary Snap YAML aliases

* feat(packaging): ship Linux runtime license notices

* docs(embedded-mpv): document Linux runtime distribution

* fix(packaging): parse Snap trailing comments safely

* fix(release): gate Snap publish on public source release

* fix(packaging): strip VCS metadata from source bundle

* docs(packaging): clarify Linux source release gate

* test(embedded-mpv): smoke missing bundled libmpv

* style(embedded-mpv): format final validation inputs

* fix(e2e): satisfy fixture index signature typing

* fix(ci): declare fontconfig gperf generator

* fix(embedded-mpv): hash runtime cache identities

* fix(packaging): harden Linux frame-copy delivery

* fix(packaging): tighten runtime delivery gates

* fix(ci): decouple Linux runtime matrix

* fix(packaging): harden Linux frame-copy delivery

* fix(packaging): validate Linux frame-copy runtimes

* fix(packaging): scope Snap Electron library checks

* feat(packaging): ship Linux frame-copy runtimes

* fix(packaging): improve Linux runtime smoke diagnostics

* fix(packaging): expose bounded helper probe details

* test(packaging): trace Snap EGL probe failures

* fix(packaging): prefer core22 ABI in Snap helper

* fix(packaging): bound helper probe capture

* fix(packaging): harden Linux frame-copy releases

* fix(packaging): canonicalize libplacebo submodule identity

* fix(packaging): make source archive inspection portable

* fix(packaging): harden Snap release verification
2026-07-18 17:28:22 +02:00
MahdiHrmandClaude Fable 5 643dee1be3 feat(xtream): resume the latest series episode (#1187)
Dashboard Continue Watching now carries the exact saved season/episode into
Xtream series details and starts it at the persisted offset. Successful
external MPV/VLC launches persist the launched episode and retarget the
series CTA to "Play episode N". Recent-history rows keyed by an episode id
resolve their parent series before navigation.

Includes maintainer follow-ups: no zero-offset resume on failed position
loads, seriesXtreamId-gated resume targets for legacy rows, and patch
coverage raised from 76.7% to 93.9%.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-18 15:49:22 +02:00
d61fd5db19 feat: add strip country prefix setting (#1162)
* 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>
2026-07-18 15:43:37 +02:00
4gray c6ed504723 feat(playback): add shared picture-in-picture controls (#1199)
* docs(playback): design shared web picture-in-picture

* docs(playback): keep picture-in-picture exit available

* docs(playback): plan shared web picture-in-picture

* feat(playback): add picture-in-picture controls contract

* feat(playback): add shared web picture-in-picture

* feat(playback): expose shared picture-in-picture action

* docs(playback): document shared web picture-in-picture

* test(playback): cover shared picture-in-picture flow

* test(playback): wait for PiP video in Electron E2E

* refactor(playback): extract picture-in-picture controller
2026-07-17 22:11:29 +02:00
4gray beb62db314 feat(settings): add shared web player controls toggle (#1198)
* docs(playback): design shared controls setting

* docs(playback): plan shared controls setting

* feat(settings): persist shared web controls preference

* test(settings): harden shared controls normalization coverage

* feat(settings): expose shared web controls toggle

* fix(settings): label shared controls toggle

* feat(playback): resolve shared controls from settings

* test(playback): cover shared controls setting

* docs(playback): document shared controls preference

* fix(playback): await settings before host creation

* fix(settings): normalize shared controls updates
2026-07-17 13:38:06 +02:00
4grayandLars Emig 48e3736460 feat(artplayer): add feature-flagged shared controls (#1196)
* feat(artplayer): add feature-flagged shared controls

Co-authored-by: Lars Emig <lars.emig@pickware.de>

* fix(artplayer): align native type and signal inputs

---------

Co-authored-by: Lars Emig <lars.emig@pickware.de>
2026-07-17 00:12:49 +02:00
4grayandLars Emig 4e572c60ca feat(videojs): add feature-flagged shared controls (#1195)
* feat(videojs): add feature-flagged shared controls

Rebuild the Video.js shared-controls integration on the current player lifecycle with Tech rebinds, source-scoped tracks, reset ordering, volume preservation, diagnostics gating, and default-off compatibility.

Credits and supersedes the stacked implementation proposed in #1153.

Co-authored-by: Lars Emig <lars.emig@pickware.de>

* fix(videojs): harden MPEG-TS reset lifecycle

---------

Co-authored-by: Lars Emig <lars.emig@pickware.de>
2026-07-16 23:08:18 +02:00
4gray c49ea2f6a8 feat(html-player): add feature-flagged shared controls (#1194)
* feat(html-player): bridge engine state to shared controls

* fix(html-player): avoid HLS subtitle event reentry

* fix(html-player): restore delayed HLS default subtitles

* refactor(html-player): split controls bridge collaborators

* feat(html-player): add feature-flagged shared controls

* test(html-player): cover shared-controls source ownership

* feat(html-player): pass shared-controls playback metadata

* docs(player-controls): describe HTML5 shared-controls bridge

* docs(player-controls): clarify HTML5 rollout effect

* fix(html-player): reveal diagnostics from fullscreen

* fix(html-player): defer HLS event resolution

* refactor(html-player): isolate video element session
2026-07-16 21:30:41 +02:00
4gray f611ea3d7c feat(embedded-mpv): use shared controls for frame-copy (#1193)
* docs(embedded-mpv): plan frame-copy shared controls

* feat(embedded-mpv): adapt frame-copy sessions to shared controls

* fix(embedded-mpv): correlate recording control updates

* fix(embedded-mpv): accept recording ack before command resolve

* fix(embedded-mpv): serialize delayed recording commands

* fix(embedded-mpv): latch buffered recording outcomes

* feat(embedded-mpv): use shared controls for frame-copy

* fix(embedded-mpv): isolate recording ticks by engine

* fix(embedded-mpv): reset controls on engine handoff

* docs(embedded-mpv): document frame-copy shared controls

* docs(embedded-mpv): normalize shared-controls plans

* fix(embedded-mpv): isolate legacy feedback on handoff

* fix(player-controls): block toggles while stalled

* refactor(embedded-mpv): isolate controls timing

* fix(player-controls): reset recording feedback on handoff

* fix(embedded-mpv): preserve newer session snapshots
2026-07-16 18:47:32 +02:00
8f597b44cf refactor(embedded-mpv): split session controller into focused collaborators [2/7] (#1149)
* refactor(embedded-mpv): split session controller into focused collaborators

Mechanical decomposition of the embedded-MPV session controller into
focused collaborators under embedded-mpv-player/:

- embedded-mpv-command-runner.ts: transport/track/recording IPC
  delegators with guarded snapshot reconciliation
- embedded-mpv-session-factory.ts: pure placeholder-session factories
  (loading/attaching/error) and the startup-paint wait
- embedded-mpv-stalled-tracker.ts: loading-stall timer and stalled flag
- embedded-mpv-compositor.ts: host bounds measurement (measureBounds),
  re-exported from embedded-mpv-format.utils for existing imports

No behavior change. The existing embedded-mpv-player component is kept
untouched and keeps working against the controller's unchanged public
API (commands are now bound fields delegating to the runner).

Test coverage extended per Codecov patch report.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* refactor(embedded-mpv): drop superseded overlay hooks

* fix(embedded-mpv): guard async session races

* docs(embedded-mpv): document renderer collaborators

* fix(embedded-mpv): abort stale recording startup

* docs(embedded-mpv): clarify renderer safety details

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Co-authored-by: 4gray <serega05@gmail.com>
2026-07-16 14:13:13 +02:00
aa6ee85d3f feat(player-controls): add shared engine-agnostic controls layer (#1148)
* feat(player-controls): shared engine-agnostic controls layer (flag off)

Introduce a shared, engine-agnostic player-controls layer in libs/ui/playback
as pure additive library code with no consumers yet.

- Contract (player-controls.model.ts): PlayerControlsCapabilities,
  PlayerControlsState, and PlayerControlsCommands make up the
  PlayerController interface every engine adapter implements.
- Single presentation component (app-player-controls): one controls UI
  binding purely to a PlayerController, with focused helpers for
  visibility auto-hide, volume, fullscreen (built-in DOM path), menus,
  keyboard shortcuts, seek/volume feedback, and the controls surface.
- Web-video adapter (web-video-controls.adapter.ts + host directive):
  drives the contract from an HTMLVideoElement, including optional
  HLS.js quality/audio-track integration and series episode navigation.
- Feature flag WEB_PLAYER_SHARED_CONTROLS (web-player-controls.flag.ts)
  defaults to OFF; no player component consumes the new layer yet, so
  runtime behavior is unchanged.
- docs/architecture/player-controls-contract.md documents the target
  architecture (later PRs add the embedded-MPV adapter, immersive
  overlay, and host-supplied fullscreen delegate).

Review-driven hardening: the web-video adapter now holds the host
series-navigation signal reactively (updates after setContext are
reflected); the shared controls template is fully localized via
ngx-translate (reusing the EMBEDDED_MPV.PLAYER.* keys); single click on
the viewport toggles play/pause deferred so a double-click still
fullscreens; keyboard shortcuts are shadow-DOM-safe (composedPath) and
guard against duplicate-instance double-execution (defaultPrevented);
and hidden controls now also disable shortcuts.

Test coverage extended per Codecov patch report.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(player-controls): harden shared controls foundation

* fix(player-controls): restore detached adapter state

* fix(player-controls): harden keyboard and adapter state

* fix(player-controls): refresh readiness and hide timers

* fix(player-controls): keep controls root inside surface

* fix(player-controls): hide seek controls for live streams

* fix(player-controls): harden multi-engine control state

* fix(player-controls): respect runtime interaction availability

* fix(player-controls): gate loading toggles and localize mute

* fix(player-controls): harden surface and error states

* fix(player-controls): preserve volume and cursor state

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Co-authored-by: 4gray <serega05@gmail.com>
2026-07-16 12:56:30 +02:00
4gray 59e08fd2d6 feat(embedded-mpv): add Windows frame-copy support (#1175)
Port the embedded mpv frame-copy pipeline to Windows with WGL rendering and named shared memory. Includes packaging validation, platform gates, tests, and architecture documentation.
2026-07-15 21:27:56 +02:00
4grayandClaude Fable 5 7d75d989e8 feat(embedded-mpv): Linux port of the frame-copy rendering engine (headless EGL) (#1171)
* feat(embedded-mpv): Linux frame-copy helper via headless EGL

Port the frame-copy engine's native layer to Linux (PORTING.md items 1-4):

- frame_helper_gl.h: platform GlContext abstraction. macOS keeps the CGL
  path (moved verbatim); Linux acquires an EGL display in order
  surfaceless-Mesa -> default display -> GBM render node, binds a 3.2 core
  desktop-GL context surfaceless (1x1 pbuffer fallback), and hands mpv
  eglGetProcAddress. The helper's own GL calls link against glvnd
  libOpenGL, so no display server is required.
- frame_shm.h: portable frame_shm_now_ns() (CLOCK_MONOTONIC) shared by the
  helper and the reader addon, replacing the macOS-only
  clock_gettime_nsec_np(CLOCK_MONOTONIC_RAW); producer and consumer stay on
  the same clock.
- embedded_mpv_frame_reader.c: real implementation now also on __linux__
  (the code was already POSIX apart from the clock call).
- binding.gyp: OS==linux executable branch for iptvnator_mpv_helper linking
  system libmpv (-lmpv) + EGL/OpenGL/gbm, with rpaths for $ORIGIN/lib and
  the build-time library dir. The in-process addon still does not link
  libmpv - the ban only binds in-process, the helper is out of process.
- build-embedded-mpv.js: system-dev fallback on Linux (LIBMPV_INCLUDE_DIR
  or /usr/include) so a distro libmpv-dev install builds without staging a
  vendored runtime; a pre-set LINUX_NATIVE_LIBRARY_DIR now wins over the
  vendored lib dir.

Verified on Ubuntu 25.04 / i7-1165G7 (Iris Xe): lavfi smoke per PORTING.md
(idle->loading->playing snapshots at 4 Hz, aspect-fit generation bump
g1 1280x720 -> g2 960x720 for a 4:3 source), reader probe 60 fps at
1080p60 with 0 torn reads, clean quit with no leaked processes or shm.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* feat(embedded-mpv): enable the frame-copy engine gates on Linux

Flip the TypeScript side of the Linux port (PORTING.md item 5). A shared
dependency-free predicate, isFrameCopyPlatformSupported() (linux any-arch,
darwin arm64-only), now backs all four gates so they cannot drift:

- main.ts: the persisted Settings toggle promotes to the env flag on Linux
  too (this runs before window creation and controls the sandbox relax).
- EmbeddedMpvNativeService.isFrameCopyEngineActive/isFrameCopyAvailable.
- EmbeddedMpvFrameCopyAdapter.isSupported.

getSupport() ordering: the frame-copy branch moves above the Linux-only
native-engine prerequisites - the X11/Xwayland display-server check and
the system-mpv-on-PATH probe only bind the --wid native engine, while the
frame-copy helper renders offscreen (headless EGL) and links libmpv
itself. createSession() also skips resolving the native window handle for
frame-copy sessions, which the adapter ignores anyway, so native-Wayland
sessions no longer trip the window-handle assertion.

Settings copy: the i18n frame-copy description now says macOS (Apple
Silicon) and Linux in all 18 languages; stale macOS-only doc comments in
the settings/support interfaces updated alongside.

Tests: platform-gate matrix for the adapter (darwin arm64/x64, linux
x64/arm64, win32) and service specs covering Linux activation under
native Wayland, macOS arm64 staying active, macOS x64 staying native, and
the skipped window handle for frame-copy sessions.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* chore(packaging): CI + package guards for the Linux frame-copy helper

- build-and-make.yaml: install libegl-dev/libgl-dev/libgbm-dev on the
  Linux runner (the helper's EGL backend needs them now that the helper
  target builds on Linux), and verify the built helper exists and DOES
  link libmpv - the inverse of the addon's no-libmpv rule, which still
  holds and stays validated.
- electron-after-pack.cjs: strip iptvnator_mpv_helper from packaged Linux
  apps. It links the build host's system libmpv, which end-user systems
  cannot be assumed to have; the support probe treats the missing helper
  as frame-copy-unavailable (dev-build-only engine until the
  bundled-runtime staging milestone).
- frame_helper_gl.h: log the chosen EGL display tier to stderr (the
  adapter mirrors helper stderr), so bring-up problems on exotic setups
  are diagnosable.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* docs(embedded-mpv): document the Linux frame-copy port

- architecture doc: frame-copy section covers Linux (EGL display tiers,
  build deps, package strip), Linux support matrix notes the frame-copy
  exception to the X11 + system-mpv requirements, Linux measured baseline.
- RESULTS.md: Ubuntu 25.04 / i7-1165G7 (Iris Xe) measurement rows via the
  production helper + reader probe; viewport-size claim reproduced.
- PORTING.md: Linux marked done with pointers to what changed; Windows
  remains the open port and its perf gate the open decision.
- CLAUDE.md + tools/embedded-mpv/README.md: platform scope, Linux dev
  build requirements, system-headers fallback, helper strip.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* test(embedded-mpv): commit the Linux frame-copy measurement probe

linux-frame-probe.mjs reproduces the RESULTS.md Linux rows: spawns the
production helper, attaches the frame-reader addon to the announced shm
generation, and reports new-frame fps, copy wall time, produce->copy age,
torn reads and pixel spread. Usage documented in RESULTS.md.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(embedded-mpv): address multi-agent review findings on the Linux port

Confirmed findings (each verified by 3 adversarial reviewers):

- CI would fail to link the helper: -lOpenGL needs the unversioned glvnd
  libOpenGL.so, shipped only by libopengl-dev, which neither the runner
  images nor the previous apt line provide. Added to the workflow and to
  every documented Linux build-dep list.
- The new 'test -x' dist guard could never pass: webpack's dist asset
  copy drops file modes (helper arrives as 0644). The guard is now
  'test -f'; electron-after-pack.cjs restores the execute bit on packaged
  helpers (also fixes packaged-macOS spawns); the support probe now
  requires X_OK, so a mode-stripped helper reads as frame-copy-unavailable
  and falls back to native instead of failing spawn with EACCES.
- The Settings frame-copy toggle was unreachable in exactly the Linux
  states the port targets: the native-Wayland and missing-system-mpv
  unsupported payloads omitted frameCopyAvailable, and toggle visibility
  derives solely from it. Both returns now advertise availability.

Also from review:

- build-embedded-mpv.js keeps the old graceful-skip contract when the new
  system-dev fallback finds libmpv-dev but the GL/EGL/gbm dev stack is
  missing (previously such machines skipped; a hard electron-build
  failure was a regression).
- createSession derives the window-handle skip from the dispatched addon
  instead of re-evaluating the engine gate, so the two cannot disagree.
- The render thread logs the GL renderer string (surfaceless Mesa can
  silently pick llvmpipe on non-Mesa-primary systems; now diagnosable —
  verified 'Mesa Intel Iris Xe' on this machine).
- Specs pin the new semantics: frameCopyAvailable advertised while native
  is unsupported (Wayland / no mpv), frame-copy supported without a
  system mpv, and the handle-skip test disposes its session through the
  owning adapter.
- Docs: PORTING.md file map reflects the frame_helper_gl.h seam for the
  Windows porter; helper-strip removal correctly gated on milestone 4
  (bundled libmpv), not milestone 3; RESULTS.md preamble notes the
  RAW->MONOTONIC clock change; stale '(macOS)' scope comments updated.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(embedded-mpv): address Greptile/Codex review comments

- Sandbox gate requires a usable helper (Greptile P1, security): the
  main.ts env promotion now also probes for an executable
  iptvnator_mpv_helper before relaxing the window sandbox — a stale
  opt-in on packaged Linux (helper deliberately stripped) or after a
  cleaned native build no longer costs a sandboxless launch for an
  engine that cannot activate. Helper discovery (addon candidate paths +
  X_OK probe) moved into embedded-mpv-frame-copy-platform.util.ts,
  shared by main.ts and the service; the service keeps thin instance
  wrappers so tests can stub per scenario. New util spec pins the
  platform matrix, candidate resolution, and the execute-bit semantics.
- Stale frame-copy artifacts on skipped builds (Codex P2): cleanOutput()
  now also removes iptvnator_mpv_helper and
  embedded_mpv_frame_reader.node, so a failed/skipped rebuild cannot
  leave a previous helper advertising frame-copy support against a
  runtime the build just declared unavailable.
- Multiarch default lib dir (Greptile P1, partially refuted): -l
  resolution never depended on our -L (the compiler's built-in search
  paths include the Debian/Ubuntu multiarch dir — proven by the green CI
  run linking with a nonexistent -L dir), but the system-dev fallback
  now defaults to /usr/lib/<multiarch-triple> when present so the -L
  flag and the helper's baked rpath point somewhere real.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(embedded-mpv): harden Linux frame-copy port

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-15 20:42:50 +02:00
4grayandClaude Fable 5 26271fc076 feat(embedded-mpv): frame-copy rendering engine (experimental, macOS Apple Silicon) (#1169)
* 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>
2026-07-15 19:48:07 +02:00
4gray eb1bfaa474 chore(package): update version to 0.23.0 2026-07-13 21:35:48 +02:00
4grayandClaude Opus 4.8 b6e1c1db16 fix(e2e): stop saveSettings hanging on a spurious form-submit navigation wait (#1180)
The shared saveSettings helper clicks the settings save control, which is a
native `<button type="submit">` inside `<form (ngSubmit)="onSubmit()">`.
Clicking it makes Chromium register a form-submission navigation that
Angular's ngSubmit handler immediately cancels via preventDefault(), so no
real navigation ever happens. Playwright's default post-click "wait for
signals" barrier still observes that requested-then-cancelled navigation and
waits for it to settle; on slow/loaded CI runners that wait can stall for the
full timeout ("waiting for scheduled navigations to finish").

When the click times out, the test is aborted and its finally block closes
the Electron app; the still-pending click then rejects late, after the test
has finished, so Playwright reports it at the worker level as "1 error was
not a part of any test" and exits non-zero even though the retry passed
(e.g. 78 passed / 1 flaky yet job fails).

Opt out of the barrier with { noWaitAfter: true } and keep asserting the
deterministic post-save state (the button disables once the settings write
resolves and the form is marked pristine), which is a stronger, race-free
confirmation that the save committed. Fixes the flaky radio-playback remote
control test and hardens every saveSettings caller in the suite.

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-12 21:36:08 +02:00
4grayandClaude Fable 5 5dbf205935 fix(lint): quote eslint globs in web-backend and database lint targets (#1177)
The unquoted globs in these two nx:run-commands lint targets are expanded
by the POSIX shell on Linux CI, which matches only a shallow subset of
files, while on Windows the literal pattern reaches ESLint and the full
tree is linted. Quoting the glob (as tools/packaging already does) makes
both platforms lint the same complete file set.

Full-tree lint of both projects passes on Windows, so the widened Linux
coverage introduces no new errors.

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-12 20:50:35 +02:00
4grayandClaude Fable 5 a51c537bb2 fix(theme): make scrollbars follow the app theme instead of the OS color scheme (#1179)
On Windows with a light OS theme, scrollbars rendered light even when the
app was switched to dark. Two combined causes:

- The page never declared `color-scheme`, so Chromium colored native
  scrollbars from the OS preference. Declare `color-scheme: light` on html
  and flip it to `dark` via `html:has(> body.dark-theme)` plus the
  `.dark-theme` block itself.
- Scrollbar styling referenced `--mat-sys-*` tokens, which are never
  emitted by the current Material theme setup (mat.define-theme +
  all-component-themes does not produce system tokens). Those
  `scrollbar-color` declarations computed to `auto`, falling back to the
  native (light) scrollbar. Switch scrollbar styling to the `--app-muted-color`
  design token (defined for both themes), replace hardcoded white
  `rgba(255,255,255,.08)` thumbs, and add an explicit `scrollbar-color`
  where only `scrollbar-width: thin` was set.

Verified live in Electron via CDP in both themes: scrollbar-color resolves
and scrollbars render dark in dark theme regardless of the OS setting.

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-12 17:24:49 +02:00
4grayandClaude Fable 5 54265755ee fix(favorites): persist custom drag-and-drop order for Xtream favorites (#1143)
* fix(favorites): persist custom drag-and-drop order for Xtream favorites

Prepared-statement writes dispatched via drizzle's `.execute()` on the
better-sqlite3 driver return a promise and defer the write to a microtask.
Inside a synchronous `db.transaction(() => ...)` callback (which cannot
await), the transaction commits before that promise settles, so the write
is a silent no-op — no error, no rows changed.

This bit `reorderGlobalFavorites`: the custom favorites order never
persisted for the per-playlist ("This playlist") Xtream scope, which relies
solely on the `favorites.position` column. The global ("All playlists")
scope masked the bug because it also persists an order to the `appState`
`global-favorites-channel-order-v1` key and re-applies it on read.
`removeRecentItemsBatch` had the same latent bug — batch "clear recent
items" silently did nothing.

Switch both writers to synchronous `.run()`. Add regression coverage that
asserts `.run()` (not `.execute()`) is used and would fail on the old
behavior, and document the gotcha in the DB worker architecture doc.

Verified over CDP against a live Electron instance: reorder writes
positions 0..N, and the order survives navigation and a full reload.

Fixes #1137

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix(favorites): scope reorder position writes by playlist

The global favorites reorder wrote the new position filtering only by
content_id, so two Xtream playlists holding a favorite with the same
content_id would clobber each other's persisted order (greptile P1).

Thread playlist_id through the whole reorder path — the renderer builder
(UnifiedCollectionItem already carries playlistId), the IPC contract
(ElectronBridgeFavoriteReorderUpdate + inline payload types), the worker
op — and scope the prepared UPDATE by (contentId, playlistId), matching
the favorites composite unique index.

Tests: favorites.operations.spec asserts the playlistId placeholder and
per-row playlistId payload; preload contract fixture updated.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix(favorites): include playlist_id in workspace global favorites reorder payload

The workspace global-favorites reorder path still sent updates with only
content_id and position. Since the backend UPDATE is now scoped by
(contentId, playlistId), that payload binds an undefined playlist id and
matches no rows — the DB write silently no-ops (flagged by Greptile P1).

Also scope the prepared-statement example in the sqlite-db-worker gotcha
doc by (contentId, playlistId) so it no longer documents the
cross-playlist rewrite this PR fixes (flagged by Codex P3).

Regression spec asserts the reorder payload carries playlist_id per item
(fails on the old payload shape) and that the appState uid order is
still persisted for non-Xtream items.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-12 13:24:58 +02:00
f856226dc3 fix(xtream): send player-style User-Agent to avoid WAF challenge blocking connections (#1170)
Some Xtream panels sit behind Cloudflare/WAF rules that challenge generic
browser-looking User-Agents while allowlisting known IPTV player clients.
The previous hardcoded, truncated browser-style User-Agent in
xtream.events.ts (XTREAM_REQUEST and XTREAM_PROBE_URL) was being served a
Cloudflare challenge page (HTTP 403 HTML) instead of the real API response,
so "Test Connection" always failed with "Could not connect to the portal"
even though the same portal worked fine via curl (with a player-style UA)
and Safari. Switching to a shared VLC-style User-Agent constant across all
three request sites resolves it, verified against a live panel.

Co-authored-by: Sergio Herencias Redondo <sherencr@MacBook-Pro-de-diverzy.local>
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-11 20:49:12 +02:00
4grayandClaude Fable 5 09764e24d4 feat(portals): back button on the in-portal search page (#1142)
* feat(portals): back button on the in-portal search page

Opening a "grey" (unmatched) film from an actor page navigates to the
portal search prefilled with ?q=, but the search page had no way back —
users had to re-navigate through the sidebar to reach the actor's
filmography again.

SearchLayoutComponent gains a showBackButton input + backClick output;
both the Xtream in-portal search (SearchResultsComponent) and the Stalker
search wire it to Location.back(). Shown only on the nested in-portal
search — the top-level global search (sidebar destination) and the dialog
mode (own close button) are excluded. location.back() unwinds the full
history, so search → actor → movie detail → … all chain correctly.

Tests: search-layout spec covers the conditional button + backClick.
Docs: CLAUDE.md actor-pages note updated.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(portals): address search back-button review (dialog guard, RTL-safe spacing)

- showBackButton now also checks !dialogRef, so a hypothetical dialog
  opened with isGlobalSearch:false from a workspace route can't show a
  back arrow that navigates the page behind the dialog (greptile).
- Replaced the fragile negative margin with a header-title-group flex
  wrapper (gap: 4px) that keeps the arrow and title together regardless
  of the outer gap or Material padding, and is RTL-safe (greptile).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-07 08:20:34 +02:00
4grayandClaude Fable 5 64839e3495 fix(tmdb): fall back to original language for missing trailers too (#1136)
* fix(tmdb): fall back to original language for missing trailers too

TMDB language-filters videos the same way it filters overviews: a
Russian-only title has its trailer tagged iso_639_1=ru, so an en-US
request returns no trailer — the trailer only appeared when the user
switched the app language. The original-language fallback previously
triggered on an empty overview only, so titles that had a translated
overview but a language-only trailer never got one.

detailsFallbackLanguage now also triggers when the payload has no usable
YouTube trailer, and fillDetailsFromFallback fills the overview and the
trailer independently — a present app-language overview is kept while the
trailer is pulled from the original-language payload. The fallback fetch
stays best-effort and cached per language.

Tests: extended tmdb-language-fallback.spec.ts (trailer-missing trigger,
independent overview/trailer fill, YouTube-only guard). Docs updated.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* test(tmdb): cover combined overview+trailer fallback fill

Adds the missing case where the primary payload lacks both overview and
trailer and the fallback supplies both — the two fill branches are
independent (greptile).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-06 19:33:12 +02:00
4grayandClaude Opus 4.8 df01b738c0 feat(website): use full app screenshot for hero, drop mock window chrome
Swap the hero image to screenshots/screenshot-player.webp (channel list,
live player, EPG). The screenshot already includes the app's own window
controls, so remove the decorative title bar (traffic lights + label);
keep the rounded frame, glow, and scanline. Delete the now-unused
hero-dark.png.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-05 23:13:06 +02:00
4grayandClaude Opus 4.8 baad440602 docs(readme): keep Codecov badge in for-the-badge style (shields), pin master
Revert to the shields Codecov badge for visual consistency with the
other for-the-badge badges (native badge has no matching style). Pin
branch=master, add the Codecov logo, and keep the canonical
app.codecov.io link. Note: the shields Codecov proxy is occasionally
slow and may briefly render 'unknown'.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-05 23:05:59 +02:00
4grayandClaude Opus 4.8 c7683479a2 docs(readme): use native Codecov badge and canonical link
The shields.io Codecov proxy intermittently rendered 'unknown' (extra
API hop, cached by GitHub's camo). Switch to Codecov's native master
branch badge and point the link at app.codecov.io directly.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-05 22:57:35 +02:00
4grayandClaude Opus 4.8 0acb29d244 docs(readme): add Ko-fi to the donations buttons, fix Sponsors label
The single donation button used a 'Buy Me A Coffee' image that actually
linked to GitHub Sponsors. Split it into two correctly labeled badges:
GitHub Sponsors and Ko-fi (ko-fi.com/4gray, same as the blog).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-05 22:53:33 +02:00
4grayandClaude Opus 4.8 f7ba68dc13 docs(readme): fix stale CI badge (build-and-test.yaml -> ci.yml)
The workflow status badge pointed at a non-existent build-and-test.yaml.
Point it at the actual CI workflow, link it to the workflow page, and
label it CI.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-05 22:50:16 +02:00
4grayandClaude Opus 4.8 e75aad44c8 docs(readme): regroup features by theme, add TMDB/embedded MPV/downloads/global search
Replace the flat feature list with seven themed groups, drop granular
release-note-level items, and surface recent capabilities (TMDB
enrichment, embedded MPV, download manager, global search, dashboard,
auto-updater, remote control) with desktop-only markers.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-05 22:48:53 +02:00
4grayandClaude Opus 4.8 73b7905668 docs(website): warn about unofficial IPTVnator sites; tidy README languages
- Add blog post warning about scam sites misusing the IPTVnator name to
  sell IPTV services or push suspicious downloads (official sources + safety)
- Add "official sources only" callout to README linking to the post
- Replace enumerated language list with a count (18) linking to i18n files

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-05 22:36:47 +02:00