mirror of
https://github.com/4gray/iptvnator.git
synced 2026-10-09 01:16:15 -08:00
e53adcbeaf0dfdcc6cdb1ac84bd8871fa3207730
2546
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
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> |
||
|
|
d308749e2c | fix(stalker): preserve is_series episode metadata (#1218) | ||
|
|
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. |
||
|
|
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> |
||
|
|
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>
|
||
|
|
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> |
||
|
|
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> |
||
|
|
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 |
||
|
|
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. |
||
|
|
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> |
||
|
|
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>
|
||
|
|
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> |
||
|
|
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 |
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
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. |
||
|
|
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 |
||
|
|
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> |
||
|
|
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>
|
||
|
|
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 |
||
|
|
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 |
||
|
|
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> |
||
|
|
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> |
||
|
|
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 |
||
|
|
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 |
||
|
|
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> |
||
|
|
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> |
||
|
|
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. |
||
|
|
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> |
||
|
|
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> |
||
|
|
eb1bfaa474 | chore(package): update version to 0.23.0 | ||
|
|
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>
|
||
|
|
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> |
||
|
|
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> |
||
|
|
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>
|
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
6a867667ca |
docs(website): note Linux embedded MPV package support in v0.22 post
Clarify that embedded MPV on Linux is x64-only, needs system mpv on PATH under X11/Xwayland, works on .deb/pacman/RPM/AppImage, and is not offered on Flatpak/Snap sandboxes. Links to the README support matrix. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
9e62d9163c |
docs(website): add v0.22 release notes blog post
Announce the v0.22 release: opt-in TMDB metadata enrichment, redesigned two-state VOD/series detail pages, embedded MPV on Windows and Linux, a built-in desktop auto-updater, global search, and the reworked live EPG timeline. Includes compressed hero/slider imagery and a disclaimer that IPTVnator ships no content and is a pure player. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
c55acfb1d9 |
fix(epg): wire catch-up Watch button in unified live tab, surface resolve failures (#1133)
* fix(epg): wire catch-up Watch button in unified live tab, surface resolve failures The favorites/recent live views (UnifiedLiveTabComponent) passed archivePlaybackAvailable/archiveDays to the EPG timeline and list view, so the "Watch" button and the dialog's "Watch from start" action rendered — but neither (programActivated) nor (returnToLive) was bound. Clicking Watch emitted an event nobody listened to: no error, no feedback, no playback change. - Handle programActivated in the unified live tab: resolve the catch-up URL via resolveM3uCatchupUrl, swap the inline player's playback target (or hand it to the configured external player), show the "Archive playback" summary label, "From archive" block tag and "Return to live"; clear the override on channel switch/close/return. - Never fail silently: both the unified tab and the m3u-state effect now show an EPG.TIMELINE.CATCHUP_FAILED snackbar when the replay URL can't be resolved. New i18n key translated into all 17 locales via the i18n-fill workflow. - Extract the effect's resolve branch into a pure resolveActiveEpgProgramAction util and the unified tab's EPG summary helpers into unified-live-epg-summary.util.ts (component shrinks 598 → 572 lines despite the new feature). - Regression coverage: 6 component tests for the catch-up flow, 3 unit tests for the resolver action; docs updated in m3u-playlist-module.md. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(epg): address PR review — untrack progress tick in archive summary, open live external without prior timeshift - liveEpgPanelSummary no longer tracks the 30s progressTick while an archived programme is shown (Greptile P2): the archive summary is frozen, so re-running the computed each tick only produced needless re-renders. - returnToLivePlayback no longer early-returns when no timeshift is active (Codex P2): with an external player configured and openStreamOnDoubleClick enabled, the programme dialog's "Watch live" action now launches the external player instead of being dropped. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com>v0.22.0 |
||
|
|
862b271eac |
feat(tmdb): dashboard trending + hero enrichment, cross-portal Similar rail (#1132)
* feat(dashboard): TMDB trending rail and hero enrichment (backdrop, badges, S/E)
Two dashboard additions, both async after first paint so the page renders
exactly as fast as before:
Trending rail ("Trending this week", dashboardRails.tmdbTrending toggle,
default on, rendered only when TMDB is opted in AND the Electron DB
worker is available):
- TmdbTrendingService fetches /trending/{movie,tv}/week (one request
each, cached one day per language in tmdb_metadata under
trending:week), merges by popularity; exposed via the enrichment
facade (getTrendingWeek)
- DashboardTrendingService matches the titles against imported Xtream
playlists with ONE batched DB_MATCH_TITLES request, applying the same
two-tier + year-compatibility rule as actor pages; matched cards show
the playlist name and navigate straight to the detail view, unmatched
cards open the global search prefilled (?q=)
- The load fires only after the dashboard's own recent/favorites data
is in (never competes for the worker at startup) and once per session
- DashboardRailCard gained optional queryParams for the search links
Hero enrichment:
- DashboardHeroTmdbService patches the hero with a TMDB backdrop (only
when the item has none), a rating badge and up to two genre chips —
via the enrichment facade, so previously opened items resolve from
the SQLite cache without network; memoized per title per session,
staleness-guarded against hero changes in flight
- Series heroes show the tracked "S{n}·E{n}" badge from the playback
position; the watch-progress bar no longer applies to live heroes
Settings: new dashboardRails.tmdbTrending toggle in Settings > Dashboard.
i18n: 3 new keys translated into all 17 locales via tools/i18n patches.
Tests: dashboard-trending.service.spec.ts (gating, matching, year guard,
single-flight); settings fixtures updated. Docs updated
(tmdb-metadata-enrichment.md Dashboard Integration section, CLAUDE.md).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(dashboard): resolve hero TMDB extras for Stalker embedded-series items
Stalker vclub items carry type 'movie' in activity rows but are TV shows
on TMDB, so the hero's movie lookup found no confident match and the
backdrop/badges never appeared — while the detail view (which resolves
via is_series) showed them. When a movie-typed hero item has no movie
match, retry the lookup as TV: the detail view has usually already
cached that resolution, and misses are negative-cached.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* feat(tmdb): cross-portal "Similar" rail — Stalker gets it, Xtream gains other-portal matches
The Similar rail only existed on Xtream because it matched against the
locally loaded catalog; Stalker catalogs are server-paginated, so its
detail views had no rail despite tmdb_recommendations being cached.
New CrossPortalSimilarService (libs/services) matches recommendations
against ALL imported Xtream playlists with one batched DB_MATCH_TITLES
worker request — the same two-tier normalized-title + year-compatibility
rule as actor pages and the trending rail. Electron-only; resolves to []
in the PWA.
- Stalker: the shared VodDetailsComponent (movies; covers catalog and
inline detail hosts) and stalker-series-view (series) now render a
"Similar" rail from cross-portal matches, each card badged with the
source playlist and navigating into that portal's detail view.
- Xtream: vod/serial detail rails keep instant local-catalog matches and
append cross-portal matches (current playlist excluded, deduplicated
against local hits by normalized exact title), also playlist-badged.
- Loads async after the detail view renders, staleness-guarded; the
section only appears when there is something to show.
Tests: cross-portal-similar.service.spec.ts (PWA gate, navigation
targets, playlist exclusion, type/year guards). Docs updated.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(tmdb): drop TestBed from cross-portal similar spec
The services Jest target has no @angular/core/testing (same CI failure
as the cache spec earlier) — construct the service via Injector.create +
runInInjectionContext instead.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(dashboard): address PR review — reactive opt-out gates, retry after empty trending load
- Trending rail and hero TMDB extras now vanish immediately when the
TMDB opt-in is switched off mid-session: the render computeds read the
settings signal through isAvailable/isEnabled instead of trusting data
loaded earlier (Codex P2 ×2).
- loadedOnce latches only after a successful non-empty load, so a
transient TMDB outage on first visit no longer suppresses the rail for
the whole session — the next dashboard visit retries (greptile P2).
- Unified the duplicated heroTmdbExtras() read in the hero computed
(greptile P2).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
|