mirror of
https://github.com/4gray/iptvnator.git
synced 2026-10-08 17:06:15 -08:00
9494643a8dacac2a5bb44a2cc3497e366bb71c16
2759
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
9494643a8d |
docs(agents): re-run pnpm install after the checkout moves (#1473)
A node_modules tree installed at an older commit keeps serving the old dependency versions after git pull/reset/rebase moves the checkout, because git rewrites pnpm-lock.yaml but never re-links node_modules. Document the trigger and the cmp-based staleness check in the Agent Bootstrap section of both CLAUDE.md and the mirrored AGENTS.md. Co-authored-by: 4gray <fourgray@proton.me> Co-authored-by: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
72727a5dfa |
feat(dashboard): detail-first Continue Watching cards with quick actions (#1469)
* feat(dashboard): detail-first continue watching cards with quick actions (#1441) Continue Watching cards now open the detail page on click like movie cards; resuming the saved episode, marking it watched, and removing the entry from history move into a per-card ⋮ menu. Series details land on the earliest season with unwatched episodes (or the latest once all are watched) instead of always season 1. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(dashboard): address review findings and season auto-select regressions - A session's own watched toggles no longer re-resolve the selected season when the positions map first fills — marking season 1 watched used to jump the view to season 2 (CI regression in the web and Electron season-watched-toggle E2Es). - The all-watched season fallback skips loaded-but-empty seasons and picks the latest season that has episodes (Greptile P1). - Mark as Watched uses the strict failure-propagating save boundary (Codex P2), and both card mutations surface persistence failures via a snackbar with the new WORKSPACE.DASHBOARD.ACTION_FAILED key in all 19 languages (Greptile P2). - Season E2Es now assert the intended post-reload behavior: the fresh mount lands on the earliest unwatched season while season 1 keeps its watched state behind its tab. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: 4gray <fourgray@proton.me> Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
5b2eb515d1 |
feat(downloads): track live-TV recordings in the download manager (#1452)
* feat(downloads): track live-TV recordings in the download manager Embedded MPV recordings were written to disk and forgotten: no list, no reveal/play, no missing-file handling, and the channel/EPG context was lost the moment the recording stopped. Recordings now live beside downloads: - New `recordings` table (no unique index, no playlist FK — recordings survive source deletion; playlist name stored via playlistDisplayLabel). - EmbeddedMpvRecordingTracker persists the lifecycle: start/stop hooks plus a session-snapshot observer for implicit stops (stream-replacement auto-stop, frame-copy helper crash, session error/close); startup repair turns rows a hard kill left behind into playable `interrupted` partials. - Channel/EPG metadata is captured at recording START in all four live hosts (M3U, Xtream, Stalker ITV, unified live tab); a clean stop triggers renderer-side enrichment with every program overlapping the recorded window, keyed by target path — covering recordings that span a program boundary. Provider EPG never reaches SQLite, so post-hoc lookup is impossible by design. - Own RECORDINGS_* IPC surface + RECORDINGS_UPDATE_EVENT ping and a separate supportsRecordings capability gate (the supportsDownloads allowlist is all-or-nothing and stays untouched). Reveal/play shell IPCs are gated on the recordings table, so the renderer-supplied recording directory stays a write-location preference, not a shell-access grant. - Manager UI: `recording` filter chip, "Recording now" queue section (REC pulse, elapsed, live file size — no percentage, the length is unknown), 16:9 channel-logo Recordings library, Needs attention with Remove only, focused detail at /workspace/downloads/recording/:recordingId. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(downloads): close the stop-enrichment race and repair player stubs Greptile spotted a real ordering bug: the stop IPC returns as soon as mpv acknowledges, while the recording row's terminal-state update is still queued in the tracker. The renderer answers that snapshot with stop enrichment, whose handler only accepts a terminal row — so the covered-program metadata could be silently dropped with "Recording not found". - EmbeddedMpvRecordingTracker.whenSettled() exposes the serialized write chain; RECORDINGS_UPDATE_PROGRAMS awaits it before the terminal-row lookup. Regression covered from both sides: the handler must not touch the database until the barrier resolves, and the barrier must imply a committed row. CI also caught spec stubs that had not learned the new player inputs (my local run-many had been an Nx cache hit, so the failures only surfaced in CI): - Teach the `app-web-player-view` and `app-embedded-mpv-player` stubs the `recordingMetadata` input and `recordingStopped` output across the m3u, Xtream, Stalker, unified-live-tab and web-player-view specs. - The races spec now asserts the metadata argument explicitly instead of matching a two-argument call. - Extract the Stalker and unified-live-tab spec stubs into sibling `*.spec-stubs.ts` files (the pattern ui/playback already uses) so both specs stay under the 1200-line test limit without shaving assertions. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * test(downloads): make the recordings events spec a module The spec deliberately has no static imports — every dependency is swapped through jest.doMock before the harness's dynamic import — which also made it a TS script rather than a module, so its top-level `registeredHandlers` landed in the global scope and collided with the same-named const in stream-probe.spec.ts (TS2451). Local per-project runs compile the specs separately and stayed green; only the Tier A coverage suite builds them into one program, so CI caught it. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(downloads): address Codex review on recording lifecycle Four findings from the Codex review, all real: - P1: `addon.stopRecording()` only dispatches — native-view uses `mpv_set_property_async`, frame-copy writes a helper command — so finalizing inside the stop hook could stat a file mpv had not flushed and even unlink bytes still being written. The tracker now treats the hook as a request and finalizes on the acknowledged inactive snapshot, with a 10 s bound so a lost acknowledgement cannot strand the row. Only a recording that never went active has its empty reservation removed. Stop enrichment follows through `whenFinalized(targetPath)` (bounded) instead of merely draining the write queue. - Live file size: `file_size_bytes` is written at finalization only, so the manager's 15 s refresh reported nothing while recording. Active rows are now decorated with a current `fs.stat` size. - Manager-initiated Stop bypassed both player stop paths, so recordings spanning program boundaries kept only the start-time program. `EmbeddedMpvPlayerComponent` now owns the active→inactive edge and emits `recordingStopped` for every trigger; the adapter and legacy toggle no longer emit it themselves. - Startup recovery could terminate a row another live instance was still writing under IPTVNATOR_ALLOW_MULTIPLE_INSTANCES. Rows carry `owner_pid` and recovery skips those whose owner process is alive. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(downloads): derive the enrichment wait from the stop fallback Greptile caught the seam my previous fix left: the enrichment barrier waited 5 s while the tracker's acknowledgement fallback only finalizes at 10 s, so a stop mpv never confirms let the terminal-row lookup expire early and drop the covered programs with no retry — precisely the case the fallback exists for. The wait is now derived from the acknowledgement bound (fallback + 1 s), with a regression test that fails if the two ever drift apart again. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(downloads): address the second Codex pass on recordings Four more findings, all real: - P1 (macOS native-view): `StopRecording` clears `recordingActive` *before* dispatching the async property set and restores it if the request is rejected, so the first inactive snapshot is optimistic, not an acknowledgement — the tracker could finalize (and stat) a file mpv was still writing, and a rejected stop would leave the row `completed` while recording continued. An inactive snapshot now has to survive a 1.5 s settle window (three poll cycles); a revived recording cancels the pending finalization. - Removing a failed row unlinked its path unconditionally, which takes the file of a newer recording that reused the freed name within the same timestamp second. The cleanup now runs only while no other row claims it. - The All chip and the header's active badge ignored recordings, so a manager holding only recordings read "All 0" and an active recording never showed up in the badge. - Switching channels auto-stops the recording, but by the time the host handled the stop its `activeChannel`/EPG already described the NEW channel, so the old recording was enriched with the wrong schedule (and an unrelated program could be promoted to its title). The stop event now carries the EPG key captured while the recording was active and every host compares it before enriching. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(downloads): close the persistence race and two recording UX gaps - Greptile P1: the enrichment deadline (fallback + 1 s) still raced the terminal write — if the tracker queue or the UPDATE took longer than the remaining margin, `whenFinalized` returned while the row was still `recording` and the one-shot enrichment was dropped. The deadline now bounds only the wait for mpv; `finalize()` removes the entry synchronously, so once it has started the wait follows the write itself. - Codex: `RECORDINGS_STOP` ignored `owner_pid`. Session ids restart per process, so under IPTVNATOR_ALLOW_MULTIPLE_INSTANCES stopping another instance's row could stop an unrelated local recording. Foreign rows are now refused. - Codex: the In progress chip counted active recordings while its filter deliberately hid them, so clicking it showed "no matches". Active recordings now belong to that filter — a chip whose count disagrees with its page is a lie. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * refactor(downloads): drop the enrichment barrier instead of tuning it Three review rounds circled the same class: synchronizing mpv's asynchronous stop acknowledgement with a one-shot program enrichment. Each fix moved the deadline (5 s → fallback+1 s → wait-on-the-write) without removing the reason a deadline existed at all — the handler insisted on a *terminal* row. It never needed one. `openSync('wx')` makes the reserved path exclusive while a recording owns it, so the newest row for that path IS the recording that was stopped, and `finalize()` writes only status/end time/size and never `programs_json`. Enrichment and finalization are therefore order-independent: - `RECORDINGS_UPDATE_PROGRAMS` matches the newest row for the path in any status and awaits only the tracker's write queue, which exists solely to guarantee the INSERT committed (a recording stopped milliseconds after it started). - `whenFinalized`, its deadline constant, and the per-entry finalized promise are gone; the tracker keeps only the settle window and fallback that make *finalization* itself correct. No behavior is lost and the whole timing class disappears with the code. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(downloads): bind recording finalization to its entry and shield live rows from startup repair Two races from the Codex review: - Tracker timers finalized by reusable session id, so a stop followed by an immediate restart on the same session let the old settle timer finalize the NEW row (marked completed while mpv kept writing) and strand the old row in 'recording'. Finalization is now bound to the exact open entry, and replacing a session's entry arms the old entry's settle timer so an unobserved stop still finalizes it. - reconcileStaleRecordings() runs after the renderer is interactive; a recording started during bootstrap has ownerPid === process.pid and was repaired to interrupted/failed mid-write. Recovery now skips rows the tracker reports as actively tracked (activeRowIds()). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(downloads): harden recording startup repair against recycled pids and stale renderer lists Second Codex pass on the recovery path: - A live ownerPid alone no longer shields a row: after a crash the OS can recycle the pid for an unrelated process, which would park the row in 'recording' with no instance able to finalize it. Recovery now also checks (best-effort, ps/tasklist) that the process looks like an IPTVnator/Electron instance; an unreadable name stays conservative and keeps the skip. - The renderer loads before the repair pass runs and may already hold the pre-repair list with a stale Stop affordance; recovery now broadcasts one RECORDINGS_UPDATE_EVENT after changing any rows. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(downloads): defer teardown finalization behind the flush window and bound the live-size stat Third Codex pass: - A synthetic error/closed snapshot from disposeSession() arrives while the frame-copy helper may still be flushing (0.5 s quit grace + 2 s SIGTERM grace before SIGKILL). Finalizing there statted a file mid-write — short captures became terminal 'failed', longer rows persisted a truncated size, and startup recovery could repair neither. The tracker now defers that finalization behind a 2.5 s flush window; the row stays 'recording' (repairable) meanwhile, and an already-acknowledged stop's settle timer keeps its 'completed' verdict instead of being relabelled 'interrupted'. - The active row's live file size used a bare await stat(): one stat hanging on a dead network filesystem wedged every RECORDINGS_GET_LIST. The probe now mirrors the availability probe's contract — in-flight coalescing plus a 1 s deadline degrading to no size. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(downloads): unmask recycled recording owners, guard the PWA recording route, and unblock file probes Fourth Codex pass: - Recycled-pid discrimination no longer stops at the process-name family check (any Electron app could shield the row): a live holder must also not provably have started after the recording did (ps -o etime= / PowerShell StartTime). A pid frees only when its previous owner dies, so a recycled pid's holder is always younger than the recording; unreadable evidence stays conservative. - /workspace/downloads/recording/:recordingId gets a supportsRecordings capability guard redirecting the PWA to the manager — RecordingsService never becomes authoritative there, so the detail rendered a permanently blank workspace. - Finalization and startup repair stat through a bounded async probe (3 s deadline, ENOENT/ENOTDIR as the only proof of absence) instead of main-thread statSync: a dead network mount no longer freezes the main thread or the tracker queue, repair leaves unjudgeable rows recoverable, and finalization keeps the requested status with an unknown size rather than branding a likely-good file failed. The 0-byte reservation unlink is fire-and-forget for the same reason. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(downloads): keep inconclusive recording probes out of Needs attention and bound repair batches Fifth Codex pass: - Recording list decoration now uses the bounded availability variant that preserves 'unknown': a timed-out or permission-errored probe is not proof of absence, so a good recording on a slow mount no longer lands in Needs attention with its Play/Reveal hidden. ElectronRecordingItem.fileAvailability widens accordingly; consumers already gate on === 'missing'. - Startup repair probes its whole batch concurrently, so main.ts awaits roughly one 3 s deadline instead of one per stale row. Cross-process ping propagation under IPTVNATOR_ALLOW_MULTIPLE_INSTANCES stays out of scope (debug-only flag, same single-window design as DOWNLOADS_UPDATE_EVENT) — rationale left on the review thread. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(downloads): fix duration rounding at hour boundaries and bound owner-process probes Sixth Codex pass: - The recording duration formatter rounded minutes after flooring hours, so 59:45 read '60 min' and 1:59:45 read '1 h 60 min'. One shared recordingDurationLabel() now rounds the total minutes before splitting (both the detail page and the library card used a duplicated copy). - Startup repair's synchronous ps/tasklist/PowerShell ownership probes get a 2 s spawn timeout and are memoized per unique pid, so a batch of rows from one crashed instance costs at most one name query and one start-time query, and a hung process query degrades to the conservative fallback instead of blocking the main thread. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(downloads): return to the manager through history from the recording detail Seventh Codex pass (single finding): with a validated returnUrl the manager is already the previous history entry, so Back now uses Location.back() instead of pushing a third entry that made the browser Back button reopen the detail; router navigation remains the fallback for direct links — matching the offline-detail navigation. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(downloads): bound removal cleanup and shell gates, date interrupted rows by file mtime Eighth Codex pass: - RECORDINGS_REMOVE no longer awaits an unbounded unlink of a failed row's leftover reservation: cleanup is raced against the 1 s deadline, so a hung network unlink cannot keep the Remove action busy — the row deletion is what matters. - Reveal/Play swap the synchronous lstat gate for the bounded async availability probe: a dead mount no longer blocks the main process, and only PROVEN absence refuses the action — an inconclusive probe lets the shell try and answer honestly. - Startup repair dates an interrupted row's endedAt from the captured file's mtime (mpv's last write) instead of the repair time, so an overnight shutdown no longer inflates a five-minute capture into an hours-long recording; the repair-time fallback remains when mtime is unreadable. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(downloads): keep recording-start program metadata fresh across EPG boundaries Ninth Codex pass (single finding): the unified live tab's recordingMetadata computed cached its Date.now() verdict — starting a recording after an EPG boundary snapshotted the previous show. It now tracks the existing 30 s progress tick. The Stalker live layout's currentProgram had the same memoization (feeding recording metadata, the EPG panel summary, and external-player metadata); it gains a 30 s clock tick with interval cleanup in ngOnDestroy. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(downloads): re-select the Xtream current program against the 30 s tick at recording start Tenth Codex pass (single finding): the Xtream live layout's recording snapshot read withEpg().currentEpgItem, a computed whose Date.now() verdict stays cached until epgItems changes — a recording started after an EPG boundary snapshotted the previous show. The selection logic is extracted as the pure findCurrentEpgItem(items, nowMs), the store computed delegates to it unchanged, and recordingMetadata re-selects with the layout's existing 30 s currentTimeMs tick. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(downloads): scope stop enrichment to the exact recorded list item Eleventh Codex pass (single finding): the stop-enrichment guard compared only the EPG key, which is not unique for M3U items — two list entries sharing a tvgId (or the display-name fallback) could hand the first item's recording the second item's schedule after a switch-triggered auto-stop. RecordingStartMetadata/RecordingStoppedEvent gain an opaque sourceItemKey (unified tab: item.uid; M3U player: channel.id), captured while the recording is active exactly like the EPG key, carried through the player's stop edge, and compared by the hosts before enriching. Xtream/Stalker keys are already playlist+id-scoped and need no extra key. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(downloads): derive the M3U start-snapshot program from the active channel's schedule Twelfth Codex pass (single finding): the M3U recording snapshot read the NgRx currentEpgProgram, which retains its last value across a channel switch and through EPG gaps (the mirror effect only dispatches when a program exists) — a recording started on a channel with no airing program could persist the previous channel's title, which stop enrichment deliberately never overwrites. The snapshot now derives the program from the active channel's own schedule against the existing 30 s clock, and an EPG gap snapshots no program. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(downloads): keep finalizing rows in the recovery ledger and guard the repair update Thirteenth Codex pass (single finding): finalize() removes an entry from the open map before its queued terminal update commits, so activeRowIds() briefly omitted a row still persisted as 'recording' — startup recovery overlapping a clean stop could relabel it interrupted, after which the tracker's status-guarded update could not restore 'completed'. Finalizing entries now stay in a dedicated ledger until the update settles, and the repair UPDATE itself is guarded on status='recording' as a second belt against a finalization that commits between recovery's SELECT and its write. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(downloads): register update listeners before the initial list load Fourteenth Codex pass (single finding): RecordingsService awaited its initial RECORDINGS_GET_LIST before subscribing to the update ping — a recording transition during that request pinged into the void while the response still reflected the pre-transition state, and recording pings are rare enough that nothing self-healed until the 15 s poll (armed only once an active row is visible). The listener now registers first so the load-state coalescing queues the trailing refresh. DownloadsService had the same latent window and gets the same reorder. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com> Co-authored-by: 4gray <fourgray@proton.me> |
||
|
|
50871e581f |
feat(playback): add quality selection to shared player controls (#1470)
* feat(playback): add quality selection to shared player controls Adds a per-session video quality menu (Auto + "1080p"-style levels) to the shared player-controls layer, mirroring the audio-track pattern: - Contract: qualityLevels capability, qualityLevels/qualityAutoEnabled state, setQualityLevel command with AUTO_QUALITY_LEVEL_ID (-1) restoring ABR. - hls.js (HTML5/ArtPlayer via the neutral source bridge): levels with list-index ids, smooth switching through nextLevel, selection read from manualLevel; refresh events extended with MANIFEST_PARSED, LEVELS_UPDATED, LEVEL_SWITCHED. - Shaka (DASH): variant tracks filtered to the active audio language, ABR disabled before selectVariantTrack; manual state keyed to the exact player instance so a session restart never shows a stale selection. - Video.js: new VjsQualityLevels over videojs-contrib-quality-levels (manual = exactly one enabled level, auto = all enabled, derived statelessly). - Embedded MPV and external players report the capability false. The capability derives from the manifest (advertised only for >1 video rendition), nothing persists to Settings, and the menu rides the default-off webPlayerSharedControls rollout gate. Labels come from one shared helper so all engines render the same vocabulary. QUALITY/QUALITY_AUTO keys added to all 19 i18n files. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(playback): pin DASH quality candidates to the active audio stream Review findings on #1470: - Shaka quality candidates now match the active variant's exact audioId (language fallback only when Shaka reports none), so a DASH manifest with same-language audio tracks (main vs. commentary, stereo vs. 5.1) can no longer switch the audio track or show duplicate levels when a quality is picked. Regression test added. - Mirror the quality-selection contract into AGENTS.md's Shared Player Controls section, which must stay in sync with CLAUDE.md. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(playback): track Video.js manual quality intent explicitly Codex re-review finding on #1470: VHS flips a rendition's `enabled` flag off itself when it temporarily excludes failing renditions, so inferring the manual/auto mode from the enabled count could report a manual selection the user never made once exclusions leave a single survivor. VjsQualityLevels now records the picked level object as explicit manual intent: error exclusions read as auto, a picked level that leaves the list reverts to auto, and the bridge resets the intent on every new source. Regression tests added. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(playback): re-enable surviving renditions when the picked level is removed Codex follow-up on #1470: dropping manual intent when the picked QualityLevel leaves the list reverted the UI to auto but left the surviving renditions disabled by the earlier manual pick, pinning VHS with no selectable rendition. Reverting to auto now re-enables every remaining level, both on the removal event and lazily from the state read. Regression tests added. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: 4gray <fourgray@proton.me> Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
242640e8c6 |
chore(deps): bump ngx-indexed-db from 21.0.0 to 22.0.0 (#1472)
Coordinated replacement for the Dependabot branch: the bot updated only the root package.json, leaving the ^21 specifier in libs/shared/interfaces, which failed the @nx/dependency-checks lint rule. Co-authored-by: 4gray <fourgray@proton.me> Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
96e235cd90 |
chore(deps-dev): bump @angular/cli from 21.2.19 to 21.2.21 (#1462)
Bumps [@angular/cli](https://github.com/angular/angular-cli) from 21.2.19 to 21.2.21. - [Release notes](https://github.com/angular/angular-cli/releases) - [Changelog](https://github.com/angular/angular-cli/blob/main/CHANGELOG.md) - [Commits](https://github.com/angular/angular-cli/compare/v21.2.19...v21.2.21) --- updated-dependencies: - dependency-name: "@angular/cli" dependency-version: 21.2.21 dependency-type: direct:development update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> |
||
|
|
c1ad4672c7 |
chore(deps-dev): bump @angular-devkit/schematics from 21.2.19 to 21.2.21 (#1460)
Bumps [@angular-devkit/schematics](https://github.com/angular/angular-cli) from 21.2.19 to 21.2.21. - [Release notes](https://github.com/angular/angular-cli/releases) - [Changelog](https://github.com/angular/angular-cli/blob/main/CHANGELOG.md) - [Commits](https://github.com/angular/angular-cli/compare/v21.2.19...v21.2.21) --- updated-dependencies: - dependency-name: "@angular-devkit/schematics" dependency-version: 21.2.21 dependency-type: direct:development update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> |
||
|
|
82486220ac |
chore(deps): bump github/codeql-action in the actions-minor-patch group (#1463)
Bumps the actions-minor-patch group with 1 update: [github/codeql-action](https://github.com/github/codeql-action). Updates `github/codeql-action` from 4.37.6 to 4.37.7 - [Release notes](https://github.com/github/codeql-action/releases) - [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md) - [Commits](https://github.com/github/codeql-action/compare/v4.37.6...v4.37.7) --- updated-dependencies: - dependency-name: github/codeql-action dependency-version: 4.37.7 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: actions-minor-patch ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> |
||
|
|
ca301d5399 |
chore(deps): bump the npm-minor-patch group with 8 updates (#1459)
Bumps the npm-minor-patch group with 8 updates: | Package | From | To | | --- | --- | --- | | [hls.js](https://github.com/video-dev/hls.js) | `1.6.17` | `1.7.0` | | [@typescript-eslint/eslint-plugin](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/eslint-plugin) | `8.66.0` | `8.67.0` | | [@typescript-eslint/parser](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/parser) | `8.66.0` | `8.67.0` | | [@typescript-eslint/utils](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/utils) | `8.66.0` | `8.67.0` | | [esbuild](https://github.com/evanw/esbuild) | `0.28.1` | `0.28.2` | | [ng-mocks](https://github.com/help-me-mom/ng-mocks) | `14.16.1` | `14.17.1` | | [tsx](https://github.com/privatenumber/tsx) | `4.23.11` | `4.23.12` | | [typescript-eslint](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/typescript-eslint) | `8.66.0` | `8.67.0` | Updates `hls.js` from 1.6.17 to 1.7.0 - [Release notes](https://github.com/video-dev/hls.js/releases) - [Changelog](https://github.com/video-dev/hls.js/blob/master/docs/release-process.md) - [Commits](https://github.com/video-dev/hls.js/compare/v1.6.17...v1.7.0) Updates `@typescript-eslint/eslint-plugin` from 8.66.0 to 8.67.0 - [Release notes](https://github.com/typescript-eslint/typescript-eslint/releases) - [Changelog](https://github.com/typescript-eslint/typescript-eslint/blob/main/packages/eslint-plugin/CHANGELOG.md) - [Commits](https://github.com/typescript-eslint/typescript-eslint/commits/v8.67.0/packages/eslint-plugin) Updates `@typescript-eslint/parser` from 8.66.0 to 8.67.0 - [Release notes](https://github.com/typescript-eslint/typescript-eslint/releases) - [Changelog](https://github.com/typescript-eslint/typescript-eslint/blob/main/packages/parser/CHANGELOG.md) - [Commits](https://github.com/typescript-eslint/typescript-eslint/commits/v8.67.0/packages/parser) Updates `@typescript-eslint/utils` from 8.66.0 to 8.67.0 - [Release notes](https://github.com/typescript-eslint/typescript-eslint/releases) - [Changelog](https://github.com/typescript-eslint/typescript-eslint/blob/main/packages/utils/CHANGELOG.md) - [Commits](https://github.com/typescript-eslint/typescript-eslint/commits/v8.67.0/packages/utils) Updates `esbuild` from 0.28.1 to 0.28.2 - [Release notes](https://github.com/evanw/esbuild/releases) - [Changelog](https://github.com/evanw/esbuild/blob/main/CHANGELOG.md) - [Commits](https://github.com/evanw/esbuild/compare/v0.28.1...v0.28.2) Updates `ng-mocks` from 14.16.1 to 14.17.1 - [Release notes](https://github.com/help-me-mom/ng-mocks/releases) - [Changelog](https://github.com/help-me-mom/ng-mocks/blob/main/CHANGELOG.md) - [Commits](https://github.com/help-me-mom/ng-mocks/compare/v14.16.1...v14.17.1) Updates `tsx` from 4.23.11 to 4.23.12 - [Release notes](https://github.com/privatenumber/tsx/releases) - [Changelog](https://github.com/privatenumber/tsx/blob/master/release.config.cjs) - [Commits](https://github.com/privatenumber/tsx/compare/v4.23.11...v4.23.12) Updates `typescript-eslint` from 8.66.0 to 8.67.0 - [Release notes](https://github.com/typescript-eslint/typescript-eslint/releases) - [Changelog](https://github.com/typescript-eslint/typescript-eslint/blob/main/packages/typescript-eslint/CHANGELOG.md) - [Commits](https://github.com/typescript-eslint/typescript-eslint/commits/v8.67.0/packages/typescript-eslint) --- updated-dependencies: - dependency-name: hls.js dependency-version: 1.7.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: npm-minor-patch - dependency-name: "@typescript-eslint/eslint-plugin" dependency-version: 8.67.0 dependency-type: direct:development update-type: version-update:semver-minor dependency-group: npm-minor-patch - dependency-name: "@typescript-eslint/parser" dependency-version: 8.67.0 dependency-type: direct:development update-type: version-update:semver-minor dependency-group: npm-minor-patch - dependency-name: "@typescript-eslint/utils" dependency-version: 8.67.0 dependency-type: direct:development update-type: version-update:semver-minor dependency-group: npm-minor-patch - dependency-name: esbuild dependency-version: 0.28.2 dependency-type: direct:development update-type: version-update:semver-patch dependency-group: npm-minor-patch - dependency-name: ng-mocks dependency-version: 14.17.1 dependency-type: direct:development update-type: version-update:semver-minor dependency-group: npm-minor-patch - dependency-name: tsx dependency-version: 4.23.12 dependency-type: direct:development update-type: version-update:semver-patch dependency-group: npm-minor-patch - dependency-name: typescript-eslint dependency-version: 8.67.0 dependency-type: direct:development update-type: version-update:semver-minor dependency-group: npm-minor-patch ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> |
||
|
|
cb37f628ed |
fix(pwa): drop the retired demo URL from backend CORS and og:url defaults
The public demo deployment is paused for good, so the production CLIENT_URL fallback allowed an origin nobody can reach — a manual (non-Docker) self-host failed every provider request with a CORS error. Default to the documented http://localhost:4333 instead, and point og:url at the project website. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
2d8e8d77cb |
fix(website): point web app CTAs at the Docker self-hosting docs
The hosted PWA at iptvnator.vercel.app is paused because the free plan's bandwidth quota is exhausted, so both "web app" CTAs led to a dead page. Link them to docker/README.md and reword them around self-hosting. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
df8962969e |
feat(portals): make year, genre and country metadata clickable (#1449) (#1453)
* feat(portals): make year, genre and country metadata clickable (#1449) Year, genre and country chips on movie and series detail pages now open a Discover page inside the portal: popular TMDB titles for that facet, matched against the user's own catalog. Generalizes the existing actor-page pattern (TMDB list -> what's in my library -> else portal search) to metadata facets. - All three TMDB merges emit structured `tmdb_genres`/`tmdb_countries` (+ `tmdb_media_type` on Stalker, whose embedded-VOD series route as movies). Cached details payloads already carry both, so existing rows need no refetch. - Chips are clickable only with TMDB backing, like person chips today; the year chip gates on a merge-written numeric `tmdb_id`, since provider payloads ship junk string ids. - `TmdbDiscoverService` fetches up to 5 `/discover` pages by popularity and caches them in memory only — popularity rankings are volatile and must not reach the persisted `tmdb_metadata` table. - New `discover` route in both portals; containers clone the actor route, staleness-guarded by a facet key because facets change via query params on the same route instance. - The grid, filter chips and badges move out of `ActorViewComponent` into a shared `TitleResultsComponent` used by both pages. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * refactor(portals): share the discover facet navigation across detail pages The four render sites each carried their own copy of the year/genre/country click handlers, which also pushed serial-details.component.ts past the 400-line limit. `createDiscoverFacetNavigation()` now owns the navigation, the numeric-tmdb_id gate and the year parsing. Year parsing moves from a fixed 4-char slice to the first four-digit run, so a day-first provider date resolves instead of producing NaN. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(portals): address review findings on the Discover pages - The matching indicator was keyed on the request's subject, so an obsolete response skipped clearing it while no replacement request ever ran — the results grid then sat under the spinner forever. `createLatestRequestGuard()` now owns the indicator: the newest request always clears it, and the subject check keeps deciding whether the RESULT is still wanted. The actor pages carried the same latent bug and use the same guard now. - Country chips came from `production_countries` while Discover filters by `with_origin_country`, so clicking a co-production partner returned titles originating there instead of titles it produced. Chips are now built from `origin_country` and labelled from `production_countries`; a code TMDB does not name is dropped rather than shown as a bare code. - A cold load of an `actor` or `discover` route never initialized the catalog, so every result claimed to be missing from a library that actually holds it. Both are import-driven sections now. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * refactor(portals): extract the series Similar rail into its own service Rebasing onto master pushed serial-details.component.ts back over the 400-line limit. The "Similar" rail moves into SerialDetailsSimilarService, mirroring VodDetailsSimilarService next to it: same two sources, same component-provided lifetime so a cross-portal lookup dies with the page. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(portals): make facet chips keyboard-operable and reject zero years - The chips were plain spans with a click handler, so the whole feature was mouse-only. Actionable chips are <button> now (focusable, Enter and Space activate); a year chip that cannot be discovered by stays an informational span rather than becoming a disabled button. The button chrome is neutralized so they render identically to the chips beside them, with a visible focus ring. - `0000-00-00`, the placeholder providers ship for "no date", read as a four-digit year: the chip offered it, and the request then dropped the filter because 0 is falsy, so the page answered with unfiltered popular titles. `isTmdbYearFacet()` now gates both the chip and the route params, so a deep link cannot reach a state the chips refuse to offer. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(portals): hydrate the offline catalog and hide results while matching - `toCachedContentScope` returned null for the actor and discover routes, so an expired, inactive or offline portal skipped hydration entirely and both pages answered "not in your library" from an empty catalog even though a full imported catalog sat in SQLite. Both map to the aggregate `search` scope now — neither reads a single content type. - The results grid stayed rendered under the matching spinner. Until the matches land every card reads as unavailable, so a click during the worker request opened the portal search for a title the next tick would have resolved in another playlist. The grid is hidden while matching. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(portals): gate facet chips on enrichment, keep portal results visible - `typeof tmdb_id === 'number'` was the wrong proof that enrichment ran: XtreamVodInfo.tmdb_id allows a provider-sent JSON number, so with TMDB disabled the year chip stayed clickable and opened a Discover page that cannot load anything. The target now answers the real question — can a facet click land anywhere — and returns null when enrichment is off, so the id argument is gone from the chip API entirely. - Hiding the grid on the raw matching flag blanked valid portal results when the user switched back to "This portal" mid-request; a stuck worker would have blanked them indefinitely. The spinner belongs to the global scope, so it only replaces the grid while that scope is active. - CLAUDE.md and docs/architecture/stalker-portal.md list the portal child routes explicitly; both now include `discover`. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(portals): label the year chip with the year it navigates to `facetYear()` reads the first four-digit run so a day-first provider date resolves, but the templates still sliced the first four characters — so `31-03-1999` rendered as `31-0` while the click opened 1999. The label now comes from the same parser as the destination (`yearLabel`), and the informational chip keeps its previous rendering only when no year parses. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(portals): match Discover results by original title, guard stale loads - `/discover` returns titles localized to the app language while the provider catalog stores whatever the panel named the file, usually the original. Discarding `original_title`/`original_name` marked owned titles unavailable and sent the click to a search for the wrong name. Results carry the alias now, and both local and cross-playlist matching pass it the way the recommendations rail already does. - A facet change to B and back to A leaves two in-flight loads with the SAME key, so the key could not tell them apart: an older request failing after the newer one succeeded replaced valid results with an empty page. Recency decides who may commit, via the same request guard the matching path uses. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(portals): wait for the catalog before stating Discover availability Triggering initializeContent() for the discover route was only half the fix: TMDB usually answers before a cold catalog finishes importing, and the content gate renders the route while that runs. The page dropped its spinner as soon as the TMDB request settled, so cards computed against an empty catalog claimed that titles the user owns are missing and their clicks opened a search instead of the detail page. Availability now waits for the catalog too. Readiness is keyed on what is in flight rather than on isContentInitialized, mirroring the recently-added route, so a failed import settles the page instead of spinning forever. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * test(portals): cover the Discover catalog-readiness gate Holds the TMDB request and the catalog flags independently so the cold-load regression cannot return: results settling first must keep the page loading, a finished catalog must publish them, a failed import must still settle the page, and a running import must keep it loading. Verified to fail on the pre-fix gate: reverting isLoading to the results signal alone turns two of the four cases red. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * docs(portals): describe the Discover gates the code actually implements Three rounds of review fixes moved the contracts out from under the prose. The year chip no longer gates on a merge-written numeric tmdb_id (that gate was wrong: the field is number | string, so a provider-sent number passed it with enrichment never having run) but on the navigation target, which requires a playlist and enabled enrichment. Discover loads are guarded by recency, not by facet key, because A→B→A leaves two in-flight requests sharing one key. Availability additionally waits for catalog readiness. Both canonical entries say so now. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * refactor(portals): drop the normalized tmdbId that proved nothing `NormalizedVodMeta.tmdbId` existed only to gate the year chip, and its comment claimed a numeric `tmdb_id` proved enrichment had run. That test was wrong — the provider field is `number | string` — so the gate moved to the navigation target and the field lost its last consumer. Removing it beats re-documenting it: a field that survives with a false guarantee in its doc comment is how the rejected gate gets reintroduced. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(portals): read the year with one rule everywhere The shared detail path fed the chip `meta.year`, which the adapter built with a fixed-prefix fallback: a day-first `31-03-1999` became `31-0`, so that path both displayed the wrong label and lost the facet, since the guard could not parse it back. `parseFacetYear()` moves to shared/interfaces and both callers delegate to it, so the adapters and the Discover chips cannot drift into disagreeing about what a date says. The adapter keeps its date-parse fallback for shapes stating no four-digit run, but no longer invents one by slicing. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
bee7df1e02 |
feat(playlist): auto-detect import method that parses pasted provider messages (#1445)
Adds an "Auto-detect" method to the Add playlist dialog: paste the message a provider sent — links, Xtream credentials, a MAC address with device identity — and a deterministic parser recognizes the source(s) and prefills the matching import form. - detectProviderImportCandidates (libs/shared/interfaces) extracts URLs, MAC addresses and labeled fields, classifies each finding as Xtream, Stalker or an M3U link/body, and returns ranked candidates. Pure and synchronous. - Built against a corpus of 19 real reseller handouts kept verbatim in the spec: Unicode "font" labels, arrow/dingbat separators, separator-less hex serials, dual device IDs, multi-MAC lists, bare three-line handouts, and a guard so a parental PIN is never read as the account password. - Detection only proposes: the target form's own validation and behavioral probes remain the sole path into the store, and no pasted text leaves the app. Passwords are masked on candidate cards, including query and HTTP Basic userinfo forms. - Covered by parser, component and dialog unit tests plus two web E2E specs for the paste → pick → prefilled form workflow; i18n for all 19 languages. |
||
|
|
03719ba51d | fix(i18n): fix translation with ChatGPT 5.6 Sol (#1390) | ||
|
|
00aa623b83 |
fix(downloads): reconnect interrupted transfers and resume without validators (#1446)
* fix(downloads): reconnect interrupted transfers and resume without validators Xtream panels commonly kill each VOD connection after a byte/time burst (~130-260 MB) and send no ETag/Last-Modified. The validator-only resume path then deleted the partial and surfaced a raw "aborted" failure, so every Retry restarted from zero and large files could never finish. - Resume without a validator through overlap verification: the Range request rewinds by 256 KiB and the replayed window must match the partial's tail byte-for-byte before anything is appended; a mismatch truncates the partial and restarts from scratch (download-overlap.ts). - Reconnect automatically on recoverable interruptions and clean short responses (download-reconnect.ts): progress >=64 KiB past the best attempt resets a 3-stall budget; request-phase failures during reconnects are converted into retained interruptions so automation can never delete a partial. - Extract pure response-header helpers into download-resume-validation.ts to keep download-transfer.ts within the file-size guideline. Verified against a real throttling portal: a 1.6 GB and a 3 GB movie completed through 15 and 23 connection resets respectively. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(downloads): address review findings on reconnect baseline and small partials - Judge reconnect progress against the previous attempt instead of a high-water mark, so a transfer that legitimately restarted from byte zero mid-loop (overlap mismatch, ignored Range) is measured by its rebuilt file; at most two such regressions are tolerated per transfer to keep the loop structurally bounded (Greptile P1). - Floor reported progress at the partial's retained size while appending, so a response that ends inside the overlap window can never move persisted progress backwards (Greptile P1). - Verify partials smaller than the overlap window in full from byte zero and append, instead of rewriting the .part in place — an early-dying reconnect can now only grow the file (Codex P2). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(downloads): keep tolerated regressions off the reconnect stall budget A tolerated restart regression consumed a regression credit AND counted as a stalled attempt (its negative delta is below the progress threshold), so a legitimately rebuilt file that grows in sub-64 KiB steps was failed one reconnect early. Regressions are now charged to their own bounded budget only; the stall budget stays reserved for attempts that genuinely fail to grow the file. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(downloads): gate success and validator promotion on complete overlap verification Round-3 review findings: - Success is rejected while the overlap verifier has not consumed its entire window: a complete 206 that ends inside the window proves the remote entity shrank, so the transfer truncates and restarts from scratch instead of finalizing the old suffix as a completed file; an early-dying stream stays an ordinary retained interruption (Codex P1). - A verify-append attempt promotes the response's ETag/Last-Modified only after the complete overlap matched — an unverified partial is never blessed with a validator the next resume would If-Range-append onto (Codex P1). - A tolerated restart regression resets stalls accumulated against the discarded representation, so the rebuilt file starts with the full stall budget (Greptile P1). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(downloads): carry the known total through total-less reconnect responses A resumed response without a usable total (chunked, or an unsatisfiable Content-Range) erased task.totalBytes, so a reset over that response could no longer classify as a retained interruption and generic cleanup deleted the verified partial. The previously learned total is now carried forward for appending attempts; fresh and restarted transfers still drop it, since it described a discarded file. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(downloads): never fabricate a total for an unverified retained partial A retained failure with an unknown total persisted totalBytes = bytesDownloaded, so after stalled reconnects over a chunked, validator-less response that kept ending inside the overlap window, Retry's completed-partial shortcut saw the .part size equal the fabricated total and finalized the unverified partial without a request. The fallback is now explicit per call site: only a finalization failure after a complete transfer records its byte count as the total; retained interruptions keep an unknown total unknown, forcing Retry to re-verify. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(downloads): treat an unsatisfiable resume range as a representation change A range-capable server whose entity shrank below the rewound overlap offset answers 416 before any response body exists, which rejected the request into the generic partial-deleting failure path. The 416 is now recognized as a representation change: the partial is truncated and the transfer restarts against the current entity from byte zero. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(downloads): treat an indeterminate Content-Range total as unknown `Content-Range: bytes 200-299/*` fell through to the Content-Length fallback, deriving a "total" equal to the end of the selected range — a resumed response ending there was declared complete and the truncated partial finalized. An indeterminate total now yields null, letting the previously known total carry forward and classify the short response as a retained truncation instead. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(downloads): signal restarts explicitly and keep carried totals informational Round-8 review findings, both fixed at the design level instead of another byte-comparison patch: - Restart epochs (Greptile P1): the transfer layer now reports every rewrite-from-zero via task.transferRestarts (overlap mismatch, shrunk entity, 416, ignored Range), and the reconnect loop opens a fresh progress epoch on that signal — clean stall budget, no baseline. Byte inference could not recognize a rebuild landing near the previous attempt's count; the explicit signal can. Two restarts are tolerated per transfer; an unsignalled regression is an ordinary stall. - Authoritative vs informational totals (Codex P1): completion and truncation decisions now use only the response's own total or its advertised indeterminate range end (`bytes X-Y/*` -> Y+1); a carried total is informational, is dropped once the bytes on disk falsify it, and can flag a short transfer but never authorize finalization. A mid-reset 206 retains the partial even with a falsified total — the response proved range capability — persisting the total as unknown. Also raises the resume spec's jest timeout and tightens its polling: the previous 5 s default flaked on starved CI runners. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(downloads): keep unproven state fully uncommitted across the resume model Round-9 review findings, closing the remaining commit-before-proof gaps: - The response's total now stays uncommitted (task and row) until the complete overlap matched, exactly like the validator: a persisted total equal to the unverified partial's size let the completed-partial shortcut finalize unproven bytes after a pause, crash, or retained failure (Codex P1). - Retained-interruption persistence syncs the live task with the row: a stale falsified total left in memory made the next reconnect's resume-offset guard reject the retained partial into generic cleanup (Codex P1). - An observed 206 is remembered as task.serverAcceptsRanges, so a request-phase failure (no response at all) can retain an unknown-length partial on that evidence instead of deleting it (Codex P2). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(downloads): treat a reset after the final ranged byte as completion A 206 that delivers every advertised byte but ends in ECONNRESET instead of a clean close was classified as an interruption with a falsified total; the reconnect then resumed at EOF, collected a 416, and truncated the complete file — an endpoint that always resets after its last byte could never finish. A retainable failure whose on-disk bytes exactly equal the response's own completion evidence (and the proven total, with the overlap verified) now completes the transfer. Strict equality keeps oversized partials on the generic-failure path. Also splits the transfer error classes and retention classification into download-transfer-errors.ts to stay under the max-lines rule. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(downloads): never let an indeterminate range end authorize completion Reaching Y of `Content-Range: bytes X-Y/*` proves the selected range was delivered, not that the entity ends there — a range-capping server resetting at its cap would have finalized a truncated movie as complete. The reset-after-final-byte completion now requires the response's authoritative total; indeterminate range ends keep flagging short delivery but such resets stay retained interruptions. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(downloads): retain any nonempty partial and keep indeterminate ranges incomplete Round-12 findings, resolved by removing the last evidence requirements: - Retention no longer needs a total, validator, or range proof: since overlap verification owns resume correctness, the next attempt can safely prove, resume, or restart over ANY retained partial — deleting bytes is the only unrecoverable outcome. This closes the whole family (refused reconnects, chunked responses, falsified totals, and reconstructed retry tasks losing the in-memory range flag) and removes task.serverAcceptsRanges entirely (Codex P2). - A clean EOF at the advertised end of an indeterminate range (`bytes X-Y/*`) stays incomplete, matching the reset path: reaching Y proves the range was delivered, not that the entity ends there, so a range-capping server can no longer finalize a truncated movie; the shrunk-entity restart likewise requires an authoritative total (Codex P1). Responses with no range and no total keep the clean-EOF completion contract of unknown-length HTTP. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(downloads): honor 416-confirmed completion and keep proven validators Round-13 findings plus a CI limit: - A 416 whose `Content-Range: bytes */N` equals the partial's size confirms the file IS the complete entity (under If-Range a validator mismatch yields 200, so the 416 also confirms identity): finalize it instead of truncating and redownloading forever (Codex P1). - A validator proven by a complete overlap match is now promoted on the error path too, and retained-failure/pause persistence write resume_validator from the task — later attempts resume via If-Range instead of replaying the 256 KiB window, which stalled out servers whose per-connection cap barely exceeds the window (Codex P1). - download-resume.spec.ts crossed the 1200-line test limit: the shared harness moves to download-resume.test-harness.ts and the overlap-family cases into download-overlap-resume.spec.ts; the runtime spec gets the same CI-load timeout headroom as the resume specs. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(downloads): settle falsified totals on clean exits and probe EOF after zero-growth replays Round-14 review findings plus the CI build break: - download-resume.test-harness.ts was outside the tsconfig test-helpers exclude glob and broke every app typecheck/build; renamed to download-resume.test-helpers.ts (the excluded pattern). - A clean indeterminate delivery that outgrows a stale carried total now settles that total to unknown on the row AND the live task before persisting or throwing — the reconnect's resume-offset guard would otherwise reject the retained partial into generic cleanup (Codex P1). - A verified overlap replay of an indeterminate range that appends nothing arms a one-shot EOF probe: the next attempt requests the byte after the partial so a compliant 416 (bytes */N) can confirm the file is the complete entity, instead of repeating the rewind until the stall budget fails a finished download; a probe answered with more data is retired unappended and rewound verification resumes (Codex P1). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(downloads): retain the partial on an inconclusive EOF-probe 416 A probe at the partial's exact end always collects a 416 when the entity ends there, and the confirming Content-Range length is optional — so a length-less 416 is equally consistent with a complete file, and the unconditional restart redownloaded a likely finished movie every cycle. The probe's 416 now restarts only when a stated total BELOW the partial proves the entity shrank; otherwise the partial is retained. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(downloads): treat any EOF-offset 416 as inconclusive, not just the probe A validator-backed resume at the partial's exact end IS an EOF request: when its 416 arrived without the optional Content-Range length, the probingEof gate saw false and restartFromScratch truncated the complete partial. The inconclusive-416 retention now keys on the request having started at the partial's end (covering the probe and every If-Range resume alike); a restart still requires a stated total below the partial. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(downloads): contradiction-proof carried totals and identity-gated 416 completion Round-17 review findings: - A carried total now falls the moment the response's advertised range end contradicts it, before any byte lands — waiting for the bytes to reach it left a pause/exit window where an N/N row let the completed-partial shortcut finalize a truncated file (Codex P1). - The 416 completion shortcut now requires identity proof: an exact-EOF request backed by If-Range, or the EOF probe that follows a fully verified overlap replay. A bare length match on a rewound request proves nothing about whose bytes are on disk; a contradictory 416 (stated total says the rewound range was satisfiable) retains instead of restarting (Codex P2). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * refactor(downloads): extract 416 classification to stay under max-lines The previous commit pushed download-transfer.ts to 404 effective lines; the 416 decision moves into classifyRangeNotSatisfiable() in download-transfer-errors.ts with identical semantics, and the unused re-export-only imports are dropped. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(downloads): promote the proven total on the error path and fix the 416 contract doc - The error-path promotion after a complete overlap match now updates task.totalBytes alongside the validator: a pause landing while the partial sits at a stale carried total otherwise persisted an N/N row that Resume's completed-partial shortcut would finalize (Codex P1). - The download-manager contract doc's rewound-416 paragraph now matches classifyRangeNotSatisfiable(): completion requires identity proof at exact EOF, restart requires a proven shrink, everything ambiguous retains (Codex P1 on the doc). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(downloads): correct both 416 classification boundaries Round-19 review findings, one in each direction: - A rewound 416 WITHOUT a stated length now retains: unsatisfiability alone never proves the entity shrank relative to the retained bytes, and the canonical contract reserves restart for stated proof (Codex P1). - A stated total EQUAL to a rewound request's first byte now restarts: the entity ending exactly at the rewound offset makes the 416 valid and proves the partial extends past the entity — equality was being misread as a contradiction, stranding the download in retain forever (Codex P1). classifyRangeNotSatisfiable() gains an exhaustive pure table spec covering every branch. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(downloads): restart on reset-ended responses that completed a shorter entity in-window The clean-EOF path already restarted when a response delivered its complete authoritative total inside the verification window, but a reset arriving right after that final byte took the retention path and stranded the oversized partial in a stall loop. The catch path now mirrors the shrink detection: authoritative total reached inside an unproven overlap restarts from scratch. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(downloads): request identity encoding for byte-exact transfers Axios's Node adapter transparently decodes gzip/brotli responses, which would put decoded bytes on disk while Content-Length, Content-Range, and every Range offset speak the encoded representation — desyncing resume offsets and overlap verification on origins that compress. Downloads now always send Accept-Encoding: identity. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(downloads): drop carried totals an indeterminate range can exactly reach `bytes 200-249/*` can deliver the partial exactly TO a carried total of 250; the <= guard kept that total, and a pause or exit anywhere in that window persisted a 250/250 row the completed-partial shortcut would finalize without EOF proof. The guard is now strict: a carried total survives only when the advertised indeterminate range cannot reach it, which also closes the mid-stream pause window. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(downloads): arm the EOF probe after a reset-ended verified zero-growth replay A verified overlap replay that reset right at the partial's end (zero growth, indeterminate range, no validator) retained without arming the EOF probe, so every retry replayed the same tail until the stall budget expired — the clean-EOF path's probe arming now has its reset-path mirror. Also splits to stay under max-lines: the classifyRangeNotSatisfiable table spec moves to download-transfer-errors.spec.ts and the DB persist helpers to download-transfer-persistence.ts. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
0a2fe263db |
feat(portals): mark a whole series as watched in one click (#1451)
* feat(portals): mark a whole series as watched in one click Adds a series-level watched toggle to the season header's new overflow menu on both Xtream and Stalker series detail pages (issue #1442 v2, building on the season-level toggle from #1447). - Shared: buildSeriesWatchToggleRequest flattens every loaded season with the season builder's mark/unmark semantics; the direction is always the one the label advertised, never re-inferred at persist time. Watch-toggle state math for both scopes moves into the new component-provided SeasonWatchPresenter (the container component sat at the max-lines cap). - Xtream: the series request reuses SerialDetailsSeasonWatchService through a scope-parameterized handle(), the same stillCurrent ownership guard, and the XtreamStore.loadAllPositions badge refresh. - Stalker: the season handler's core is extracted into runWatchToggleBatch (feedback keys per scope). Lazy Ministra VOD hydrates unloaded seasons sequentially first (zero writes on a failed fetch, silent abort on navigation), re-runs the position reconcile synchronously so newly hydrated episodes' legacy rows are cleaned, then rebuilds the request keeping the clicked direction; an all-watched outcome reports an honest count-0 snackbar. - Container: new hasUnloadedSeasons input blocks the fully-watched verdict and the count label while lazy seasons are unloaded, and the empty mark request contract lets the host hydrate-then-rebuild. Six new XTREAM i18n keys, synced to all 18 locales via the i18n-fill workflow. No new IPC: the existing playback-position batch channels are season-agnostic. Refs #1442 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(portals): treat an empty Stalker season answer as loaded, not pending A lazy season the portal ANSWERS for with zero episodes was still counted as unloaded (episodes.length === 0 heuristic): the series label stayed countless forever and every series toggle re-fetched the empty season, while a glitch-empty answer could silently skip a season and still report success as if nothing remained. VodSeriesSeasonVm gains an episodesLoaded flag set by every successful episode fetch — including an empty one — and the series toggle's pending predicate, hydration re-check, and hasUnloadedVodSeasons now key on it. A loaded-and-empty season unblocks the count label and the fully-watched verdict instead of re-fetching; a fresh detail mount still re-fetches, so a one-off glitch self-corrects next session. Addresses the Greptile P1 on PR #1451. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(portals): harden lazy season hydration against malformed and racing loads Two review findings on the series watched toggle: - fetchVodSeriesEpisodes now trusts an empty answer only when the envelope actually carried a well-formed array; a malformed envelope or an answer whose rows contain no recognizable episode rejects, so the load fails instead of the season being recorded loaded-and-empty and silently skipped by the series batch. - loadEpisodesForSeason is single-flight per season: a tab click, the spillover prefetch, the quick-start recursion, and the series-toggle hydration join one in-flight request instead of duplicating portal traffic — previously a second request's failure could abort a series toggle whose original request succeeded. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
7fc9380bff |
feat(portals): mark a full season as watched in one click (#1447)
* feat(portals): mark a full season as watched in one click Series detail pages on both Xtream and Stalker portals get a season-level watched toggle next to "Download season": marking writes full-progress rows for the unwatched episodes only (real durations survive), a fully watched season flips the action to unwatch-all. Persistence goes through new batch IPC channels (DB_SAVE/CLEAR_PLAYBACK_POSITIONS_BATCH, one SQLite transaction with onConflictDoUpdate().run(); the PWA data source rewrites its localStorage blob once). Stalker deliberately bypasses the batch IPC and loops the existing position-mutation queue so legacy-row reconciliation still runs and the queue coalesces to a single reload; partial failures surface a dedicated snackbar. Also removes the dead toggleEpisodeWatched store method, splits season-container/serial-details-playback under the max-lines cap (season-watch-toggle.util.ts, SerialDetailsSeasonWatchService), and classifies *.spec-data.ts fixtures under the test max-lines ceiling (baseline shrinks by main.preload.spec-data.ts). Closes #1442 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(portals): guard stale season batches and split partial-unwatch feedback Review follow-up (Codex on #1447): - A season batch completing after the user navigated to another series or playlist no longer writes the old series' rows into the freshly reset position state (episode ids can collide across playlists); the Xtream host captures the playlist/series identity before awaiting and skips the rendered-state mutation when it changed. The DB write is unaffected — it carries its own playlistId. - A partially failed "mark season as unwatched" on Stalker now reports a dedicated SEASON_MARKED_UNWATCHED_PARTIAL message instead of the watch-direction "marked" text; translated into all 18 locales. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(portals): exclude the playing episode from season marking and count partial saves Second review round (Codex on #1447): - The episode currently playing (inline or in an external session, or with a launch in flight) is excluded from a season's mark-watched batch: the player persists its live position every ~15 s and would immediately overwrite the just-written full-progress row. The button count reflects the exclusion and the action disables when nothing is markable. Unmarking still clears such an episode — the recreated in-progress row reflects live playback truthfully. - A Stalker StalkerSeriesPositionPartialSaveError (scoped watched row saved and published, only legacy cleanup failed) now counts as a watched success instead of feeding false total-failure feedback. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(portals): gate stale season-batch snackbars on the originating page Third review round (Codex on #1447): a batch resolving after the user navigated away no longer shows its contextless success/error snackbar on the newly opened detail page — the same ownership check that guards the state mutation now guards the feedback too. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(portals): sync catalog progress badges after toggles and gate Stalker feedback Fourth review round (Codex on #1447): - Any Xtream watched toggle (single episode or season batch) now refreshes XtreamStore.loadAllPositions after persisting — the catalog reads series-progress badges from the store, which otherwise loads positions once per playlist, so returning from the detail kept stale badges. Skipped when the playlist changed mid-flight (the store then belongs to the other playlist; its own init reloads positions). - Stalker's season snackbars are gated on the captured playlist/series identity, matching the Xtream ownership guard — a batch draining after navigation no longer reports on the newly opened page. - Stalker season-toggle specs moved to stalker-series-view.season-watch .spec.ts with their own harness; both prior spec files sat at the 1200-line test ceiling. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * docs: describe the season watched toggle in CLAUDE.md Fifth review round (Codex on #1447): the canonical Seasons entry in the VOD/Series detail section now covers the bulk toggle, its playing-episode exclusion, both persistence paths, catalog badge sync, and the stale-completion contract. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(portals): let only the latest positions load patch the Xtream store Sixth review round (Codex on #1447): loadAllPositions is now latest-load-wins — a fetch superseded while in flight (playlist switch before getAllPlaybackPositions resolves) no longer patches the singleton store with the previous playlist's position maps, which could leave the new catalog showing the old playlist's progress badges. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * docs: reflect the spec-data max-lines classification in CLAUDE.md and AGENTS.md Seventh review round (Codex on #1447): both canonical max-lines descriptions now list **/*.spec-data.ts among the test-ceiling globs so future agents neither treat these fixtures as production files nor remove the exemption unknowingly. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(portals): parse "N min" durations when marking episodes watched Eighth review round (Codex on #1447): Stalker VOD episodes report durations like "45 min", which parseDuration could not read — bulk (and single) mark-watched then persisted 1/1-second rows. The minute format now parses to seconds, matching what the removed legacy store method already handled. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(portals): parse compound hour durations and cover the toggle end-to-end Ninth review round (Codex on #1447): - parseDuration now reads the compound "1h 30min" form the Xtream fixtures emit (hour group optional, so "45 min" keeps working) — bulk-marked episodes no longer persist a minutes-only duration. - New Playwright coverage exercises the season toggle through the real UI on both portals: Xtream (category → series detail → mark → reload-persistence → unmark) and Stalker (embedded-series flow, mark → unmark with the item's actual episode count). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(portals): refresh Stalker catalog progress badges after watched toggles Tenth review round (Codex on #1447): the Stalker mirror of the Xtream catalog sync — StalkerCatalogFacadeService loads its position maps once per playlist and the runtime bridge only pushes external-player updates, so renderer-initiated toggles left grid badges stale. The series view now calls the facade's new ownership-checked refreshPositions after the season batch (including partial successes) and after single toggles; the reload is latest-load-wins like the Xtream store fix. Optional injection keeps collection-detail mounts outside the catalog working. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * test(portals): cover the season toggle batch IPC end-to-end in Electron Eleventh review round (Codex on #1447): the new Electron E2E marks a season through the real UI, asserts the eight SQLite rows written by DB_SAVE_PLAYBACK_POSITIONS_BATCH directly through the preload bridge, proves persistence with a full app relaunch (renderer and main process die, so state can only come from the database file), and clears again through DB_CLEAR_PLAYBACK_POSITIONS_BATCH back to zero rows. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(dashboard): keep watched rows out of the series resume target Twelfth review round (Codex on #1447): a watched position row — a natural finish or a manual/bulk "mark watched" marker — is a completion record, not resumable progress. Continue Watching no longer auto-plays such an episode at its end; the handoff stays detail-only and the series page's quick-start picks the first unwatched episode instead. Card progress bars and SxxEyy badges keep their current source. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(portals): fail closed on refresh reads and gate batch APIs by capability Thirteenth review round (Codex on #1447): - Position-cache refreshes now use a failure-propagating read (getAllPlaybackPositionsOrThrow through the Electron data source): a transient IPC failure rejects instead of masquerading as an empty list, so a populated store/facade cache stays stale-but-populated rather than being wiped. All load/refresh call sites handle the new rejection (init loads may retry on the next activation; post-toggle refreshes log and keep the snackbar flow). - The season-batch bridge methods joined playbackPositionStorageMethods, so a bridge lacking them degrades to the in-memory path wholesale instead of throwing mid-action. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
00000de798 | feat(downloads): align manager cards, queue rows, and headings with the rail design language (#1448) | ||
|
|
20857e34c2 |
refactor(playback): extract web player diagnostic/recovery controller (#1444)
* refactor(playback): extract web player diagnostic/recovery controller WebPlayerViewComponent sat at ~391 counted lines against the 400 max-lines cap. Move the diagnostic/recovery surface — the diagnostic ownership/intent tokens, visible-diagnostic computed, playback-issue handling, recommended-player switching, retry, and session sync — into a dedicated WebPlayerRecoveryController in the same directory. The component stays the template-facing facade with an unchanged public API, so all four spec files pass without modification. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * refactor(playback): move recommendations into the recovery controller The controller deps closed over the component's recommendations computed, which itself read the controller's visiblePlaybackDiagnostic — a reference cycle tsc rejects under noImplicitAny (TS7022/TS7023) even though Jest and ESLint accept it. Owning the recommendations computed in WebPlayerRecoveryController removes the cycle at its root and shrinks the facade further; the component re-exposes the signal unchanged for the template and specs. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
c723093890 |
test(playback): drive embedded MPV controller specs on a fake clock (#1434)
* test(playback): drive embedded MPV controller specs on a fake clock The `waitFor` helper in the three embedded-mpv-session-controller specs polled real timers against a `Date.now() + 1s` deadline. Under parallel Jest workers the controller's async startup chain (support load -> waitForStartupPaint -> session create -> update subscription) did not always settle inside that budget, so a different pair of tests failed on each run. Bound the drain loop by rounds on a fake clock instead of elapsed wall time, so machine load can no longer change the outcome. Each round advances 1ms rather than 0ms: `waitForStartupPaint` nests rAF inside rAF, and a zero-delay timer scheduled from inside a timer callback is clamped to the next millisecond, so a 0ms advance fires the outer hop and strands the inner one forever. Assertions and behavior under test are unchanged. Fake timers also stop the stalled tracker's 30s timeout leaking into the real timer queue, which removes the "worker process failed to exit gracefully" warning. Verified with 5x `nx test ui-playback --skip-nx-cache` and 5x the same suite at --maxWorkers=32 (the condition that reproduced the flake): 975/975 tests passing each time. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * docs(playback): scope the fake-clock claim in the MPV controller spec The comment said machine load "no longer changes the outcome", which overclaims. Removing the spec's own `Date.now()` deadline means the spec imposes no budget of its own, but Jest's per-test timeout still measures wall time — at extreme worker oversubscription these specs can still hit it, as every spec can. Comment only; no behavior change. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
7952e6444d |
test(playback): stub the inline player in the VOD details spec (#1440)
`VodDetailsComponent` specs imported the real component tree, so every `createComponent` pulled in `PortalInlinePlayerComponent` -> `WebPlayerViewComponent` -> ArtPlayer, video.js and the embedded-MPV bridge, and instantiated all of it. The suite's slowest test measured 2521ms idle on a 10-core machine -- a 2.0x margin against Jest's 5000ms default, thin enough that a loaded worker failed tests that had no defect behind them. Swap the inline player for a stand-in that mirrors the selector and the template's bindings; the specs only assert what the host hands the player. Worst test drops 2521ms -> 306ms and the summed test time 3427ms -> 632ms. Assertions are unchanged. Also raise testTimeout to 15s for the shared web-ESM config. The 5s default is thin for Angular component specs generally: TestBed compiles and instantiates a real tree per test, and CI runners are much slower per-core than a dev machine. This is headroom, not a mute -- a spec that genuinely hangs still fails, just later. Attribution, full suite at --maxWorkers=48 on 10 cores: - forced back to 5000ms: VodDetails no longer fails (it did before this change); HtmlVideoPlayer still does, which is what the timeout covers - shipped 15000ms: 2/2 runs, 975/975 passing All six projects sharing the config pass: 1656 tests. Co-authored-by: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
e94cc029eb |
fix(portals): time out and fast-fail PWA proxy requests to dead hosts (#1424)
* refactor(portals): hoist the connectivity guard into libs/shared/host-health The breaker was written dependency-free so both processes that talk to portals could share it. Move the part that has no Electron in it — the state machine, the failure classification, the redirect-attribution helpers and the fast-fail error — into `@iptvnator/shared/host-health` (`scope:shared` / `domain:shared-runtime` / `type:util`). What stays in `apps/electron-backend` is the genuinely main-process part: one guard for the whole process, so both portal IPC handlers see each other's evidence, and the console warning that announces it. Every call site is unchanged; the wrapper re-exports the two types they import. The spec splits the same way — the state machine moves with the class, the singleton and its redirect attribution stay with the wrapper. Register the new project in the coverage policy, which every project with a test target must declare a tier for. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix(portals): time out and fast-fail PWA proxy requests to dead hosts The web backend's proxy routes were bare `axios.get()` calls with no `timeout`, so a provider that accepted a connection and then went silent held the request until the OS gave up on the TCP connection — minutes, rather than the 15/30 s budget the Electron handlers use. Add the same per-route timeouts (Xtream 30 s, Stalker 15 s / 30 s for `create_link`, playlist and XMLTV 30 s). Those numbers are safe for large downloads: on axios' default transport `timeout` bounds the time to response headers and then continues as the socket's inactivity timeout, so a multi-megabyte XMLTV file that keeps delivering bytes is never cut off — only a stalled one is. With requests bounded, run `/xtream` and `/stalker` through the shared breaker, injected via `WebBackendAppOptions.hostGuard` so specs drive it with a clock they own. Playlist and XMLTV downloads keep the timeout but no breaker, matching Electron: a download is one request rather than a catalog fan-out, it is usually the direct result of the user asking for it, and it can outlive the half-open trial window. The breaker is checked before the Xtream URL revalidation, which resolves the hostname — a dead host is where DNS is slow too, and a request admitted and then abandoned by the URL policy hands its token back rather than holding the trial slot. `resetHostConnectivityGuard()` no longer no-ops in the PWA: the breaker lives in the backend process, so it travels to a new `POST /connectivity-guard/reset`, which reads only the origin and never logs the credential-bearing URL. `skipConnectionGuard` now survives the PWA transport too, so Stalker endpoint discovery keeps the exemption it has on the desktop instead of tripping the breaker with its own probes. A fast-fail keeps each route's HTTP 200 `{message, status}` envelope. The Stalker path needs one extra step: `forwardStalkerRequest` turns that envelope into `HTTP Error <code>: …` with a numeric `status`, and the renderer reads both as "the endpoint answered" — which would make discovery walk every candidate and fire lazy repair at a host just declared dead. A prior branch keyed on the shared `isHostConnectivityFastFailMessage` rethrows it bare instead. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix(portals): report exempt Stalker probe responses to the PWA breaker Flagged by the author of #1421 as one of the twelve fixes that landed there after this branch cherry-picked the pre-review commit: an exempt discovery probe must still REPORT, it just must not COUNT. The web backend was skipping the report entirely for a probe, which loses the case that matters. A failure carrying an HTTP response proves the endpoint answered, and this route sets no `validateStatus`, so axios rejects every non-2xx with `error.response` attached — a probe answered with 404 or 500 was therefore dropped instead of clearing the record. Two counted failures either side of it then read as consecutive and opened the breaker in the middle of discovery, which is exactly what the exemption exists to prevent. `reportProviderRequestFailure` now takes `countFailures`, matching the Electron reporter, and both routes always report. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix(portals): read the redirect hop from the transport that followed it `failedAfterRedirect` decided whether a failure belonged to a redirect destination by reading `error.config.url`. That is right for the Electron transport, which sets `maxRedirects: 0` and reissues every hop as its own request, so the hop IS the config URL. It is blind on the web backend, which uses axios' default transport: follow-redirects walks the chain inside one request and `config` is built once, so `config.url` stays the URL we asked for. Verified against the installed axios 1.19.0 with a live server that 302s to a dead port: asked for : http://127.0.0.1:63953/player_api.php config.url : http://127.0.0.1:63953/player_api.php request._currentUrl: http://127.0.0.1:1/dead So the comparison was original-vs-original, found no redirect, and charged two dead destinations to the provider that had answered both times with a 302 — then fast-failed it. Read `request._currentUrl` first and fall back to `config.url`, which covers both transports; Electron's native per-hop requests expose no `_currentUrl` and are unaffected. Also check redirect attribution BEFORE suppressing failure counting for an exempt probe. A 3xx from the guarded endpoint is an answer, so a probe that observed one must clear the record; otherwise a timeout, a probe redirected to a dead destination, and another timeout still read as two consecutive failures. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix(portals): stop axios query params reading as a redirect Codex found that the Xtream breaker never opened at all, and it was right. The web backend passes credentials and the action through axios' `params`, so axios sends `…/player_api.php?username=…&action=…` while the baseline handed to `failedAfterRedirect` is the query-less URL the route built. Verified against axios 1.19.0 with a plain ECONNREFUSED and no redirect anywhere in sight: baseline : http://127.0.0.1:1/player_api.php request._currentUrl: http://127.0.0.1:1/player_api.php?username=demo&… The two normalized URLs differ, so every ordinary failure looked like a post-redirect failure, credited the endpoint, and the breaker could never trip. Compare origin and path, not the whole URL. That keeps what the check is for — an endpoint that answered and sent us elsewhere, including the same-origin `/player_api.php` → `/slow/player_api.php` case — and gives up only a redirect that changes nothing but the query, which is then counted as an ordinary failure. Erring towards counting is the safe direction here. The reason 57 tests passed over a dead feature is the real lesson: `StubHttpClient` threw bare `Error`s, so the guard's redirect check saw neither `config.url` nor `request._currentUrl` and quietly did nothing. The stub now shapes its rejections like axios does, including the query axios appends. With that alone, four existing tests fail against the old comparison. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix(portals): count a hostname that stops resolving, and fix a stale docblock Two from review, one behavioural and one documentation. A name that will not resolve is the host failing to answer — the same evidence as the ENOTFOUND the transport would have raised a moment later. But the SSRF validation turns a lookup failure into a 400 "host could not be resolved", and the release path added earlier handed the token back as inconclusive, so the breaker could never open for a host whose DNS died and every request kept paying for the same dead lookup. `ProviderUrlError` now carries the underlying lookup error internally. A refusal that has one is counted; a genuine policy refusal — private address, bad scheme, credentials in the URL — still only releases the half-open slot, because that says nothing about reachability. The field is internal: `providerUrlErrorBody()` strips it at both call sites, so the client sees exactly the body it saw before, which the test asserts. The docblock on `resetHostConnectivityGuard` still said the PWA channel is unknown and the call no-ops. That stopped being true when this branch implemented `CONNECTIVITY_GUARD_RESET` over HTTP, and a stale contract there is how the next caller silently skips the PWA path. (The edit was in an earlier commit and was lost when the branch was rebuilt on master.) Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * docs(portals): record the transport-specific redirect contract The redirect section still described one transport: hop-by-hop requests, `error.config.url`, whole-URL comparison. Two of those three are now wrong for the web backend, and this document is the canonical contract — leaving it stale is how the attribution bugs fixed in the last two commits get reintroduced. Says what is actually true: which field holds the failed hop on each transport and why the helper reads both, and that the comparison is origin + path because the web backend's credentials ride in axios' `params` and a whole-URL comparison therefore reported a redirect for every ordinary failure. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * docs(portals): scope the guard summary to both processes The opening line still defined the breaker as an Electron main-process concern, which contradicted the ownership section below it and is the part a reader skims to decide whether the document applies to them. Names both processes, and records that the web backend had the worse version of the problem first — no request timeout at all — since that is why the timeouts and the breaker had to land there together. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix(portals): declare the shared-interfaces dependency of host-health The new library's manifest listed only `tslib`, but its emitted JavaScript does `require('@iptvnator/shared/interfaces')` — the guard builds its fast-fail message with `buildHostConnectivityFastFailMessage`. Anything resolving the built artifact from its own manifest would have failed with MODULE_NOT_FOUND. The manifest was copied from `shared/logging`, which imports nothing across libraries and therefore needs nothing beyond `tslib`. `shared/m3u-utils` is the right precedent: it imports the same library and declares `"@iptvnator/shared/interfaces": "0.0.1"`. Verified against the build output rather than by inspection — the emitted `host-connectivity-guard.js` requires the module, and the generated `dist/libs/shared/host-health/package.json` now declares it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix(portals): bound the PWA connectivity-guard reset The reset was a bare `fetch` with no timeout, which is the exact failure this change exists to remove, reintroduced one layer up. Every caller awaits the reset BEFORE issuing the request it is clearing the way for — `retryContentInitialization` awaits it first by design — so a backend or reverse proxy that accepts the POST and then goes quiet would leave Retry doing nothing at all, for as long as the socket stayed open. Bound it with an AbortController and a 5 s timer. The abort rejects, `resetHostConnectivityGuard` swallows it as it already does for any other failure, and the caller proceeds to its real request — which is what "best effort" was supposed to mean. The timer is cleared in a `finally`, and it covers the body read as well as the headers. Five seconds because this talks to the user's own backend rather than a provider: it should answer immediately, and a slow one must not hold up the retry that asked for it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
3103eba083 |
fix(playback): apply saved player changes to mounted web players (#1437)
* fix(playback): apply saved player changes to mounted web players WebPlayerViewComponent resolved the saved engine from a one-shot StorageMap snapshot taken at mount, so a player switch from the command palette or settings page confirmed via snackbar and persisted the setting while an already-mounted Xtream/Stalker player silently kept the previous engine. The same snapshot also made first play mount the default Video.js engine and swap to the saved one once the async read landed. Resolve the player (and recording folder) from the live SettingsStore signal instead and drop the snapshot entirely. Precedence is unchanged: temporary recovery override -> host playerOverride -> saved player -> Video.js. Hosts passing no override (Xtream/Stalker live layouts, the portal inline detail player) now track saved changes in place; first mount reads the already-loaded store, so the default engine no longer flashes. Regression coverage (all verified to fail with the fix reverted): three unit tests on the component and two Xtream live-route e2e tests — a palette switch reaching the mounted player without a layout remount, and a MutationObserver engines-ever-seen assertion that the saved engine mounts first time. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(playback): retain mounted engine when saved player becomes MPV/VLC With the saved player now live-tracked, a mid-session palette switch to managed MPV/VLC cleared the inline binding on hosts without a playerOverride and left a blank viewport — the web player view can neither render nor launch external players. resolveRenderableWebPlayer keeps the mounted engine in that case; the external choice applies when the host starts the next playback. Renderable players, including Embedded MPV, still apply live. Raised by Codex review (P2). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
61fca6f016 |
fix(dashboard): reuse the detail view's TMDB identity for activity rows (#1423)
An Xtream activity row is built from its `content` row, and the catalog endpoints that create those rows carry only a title and a poster. So the dashboard hero and the recommendations rail rebuilt their TMDB query from the display title alone, while the detail view had searched with the original title, the release date and often a TMDB id. Without a year `pickConfidentMatch` requires a globally unique exact title, which common titles never satisfy — "Inside Out" matches several films and resolves to nothing, every time. Three `content` columns close that gap next to the existing `backdrop_url`: `tmdb_id`, `release_year`, `original_title`. The detail views back-fill them from what is on screen, the activity SELECTs project them, and `buildDashboardTmdbAttempts` reads them back. Stalker keeps stating the same facts through its stored entry, and rows with neither keep the title-only fallback. Measured against a real profile before building: of 58 distinct Xtream movie/series activity rows, 16 (28%) produce a year-less key — the cohort where a miss is guaranteed rather than likely. Contracts worth preserving: - Per-column, never overwrite. Enrichment supplies the pieces at different times, so a row-level guard would let the first arrival block every later one forever. - `release_year` is the year the PROVIDER stated. The TMDB merge fills the date field when the provider left it empty, so it marks its own substitution with `tmdb_supplied_release_date` and the extractor skips those — making contamination structurally impossible rather than avoided. - The id is stored unvetted: every consumer re-gates it through `assessProviderId`, which re-decides per lookup where a write-time verdict would be permanent. - No media-type column — for Xtream the catalog files movies and series apart, so `content.type` already is the media type. Worker requests now await `getDatabase()` before dispatching. The renderer loads before `initDatabase()` and the worker opens the database file without running migrations, so a query issued during startup on an upgraded install could otherwise hit a schema whose new columns do not exist yet. Not covered: the PWA, whose catalog cache is rebuilt from the API on every load, so a stored id would never outlive the detail view that resolved it. |
||
|
|
78587f95ba |
fix(portals): return from a portal handoff without losing the collection view (#1435)
* fix(portals): return from a portal handoff without losing the collection view "View in portal" left `stalkerReturnTo` pointing at the collection URL, and the portal detail's back affordance re-navigated there with `navigateByUrl()`. That starts a stateless history entry, but the collection's active tab, scope and open inline detail live only in `window.history.state` — so back landed on the default `live` tab with the title closed, and the portal page stayed one browser Back away. The handoff now also sets `stalkerReturnByHistory`, and both Stalker back handlers step back a single history entry instead. That entry is the one the handoff itself pushed, so it restores the collection exactly as the user left it and adds nothing to the history stack. The flag is set only by this builder and only alongside `returnTo`, so the dashboard handoff and every other `stalkerReturnTo` caller keeps re-navigating. Reported by Codex on #1422 after it merged. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(portals): scope the history-return marker to its own handoff `openStalkerItem` is consumed on arrival, but `stalkerReturnTo` and the new `stalkerReturnByHistory` stay on the history entry, and a Stalker detail opens in place without pushing one. So after Back + browser Forward the same entry can host a different title, whose back affordance would follow the leftover marker out to the collection instead of just closing it. The marker now carries the handed-off item's identity instead of a bare `true`, and a marker that does not match the open title is treated as stale: it suppresses the whole return contract, so back simply closes the detail. Reported by Codex on #1435. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * refactor(portals): share the Stalker back-navigation decision Both back handlers duplicated the marker/`returnTo` precedence verbatim, and the catalog view kept its own copy of the identity normalization the marker binding mirrors — two places for one rule to drift. `resolveStalkerBackNavigation()` now owns the decision and both handlers just apply it, while `stalkerItemIdentity()` delegates to the shared `normalizeStalkerHandoffIdentity()`. Behaviour is unchanged; the precedence gains direct unit coverage instead of only being exercised through the two components. Follow-up to Greptile's review notes on #1435. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(portals): make the return marker one-shot and id-shape agnostic Two defects in the marker binding, both reported by Codex on #1435: The comparison identity read only `item.id`, but the marker is built from `extractStalkerItemId()`, which also accepts `stream_id`/`series_id`/ `movie_id`. A collection row carrying only `movie_id` therefore compared against an empty identity, and since a marker was present the handler returned without stepping back or re-navigating — the back affordance simply stopped working. It now follows the same field order. Binding also only fixed a *different* title reopened on the entry: selecting the original title again still matched. Honouring the marker now retires both return keys from the entry, so a browser Forward cannot replay the handoff. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(portals): bind the return marker to the id the detail can report The previous attempt widened the comparison to `extractStalkerItemId()`'s field set, but `buildStalkerSelectedVodItem()` — which every opened detail goes through — derives `id` from `id ?? stream_id` and drops `series_id`/`movie_id`. The wider lookup therefore ran after the lossy normalization and still could not match, leaving the back affordance doing nothing for those rows. The marker is now bound to the identity the detail will actually report, and a row whose id cannot survive normalization gets no marker at all: the handoff falls back to re-navigating via `stalkerReturnTo`, which works. The earlier regression tests hid this by injecting a `movie_id`-shaped selection directly, which normalization can never produce; they now build the selection through `buildStalkerSelectedVodItem()`. Reported by Codex on #1435. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(portals): retire the return contract after a native browser Back Leaving the handed-off portal entry with the browser's own Back runs no back affordance, so nothing consumed the marker. A Forward replay then reopened the catalog with the contract intact, and opening the same title again matched the identity — its Back exited to the collection instead of closing the freshly opened detail. `CategoryContentViewComponent` now retires the contract whenever it lands on the entry with no handoff item and no detail open: the handoff is over, so anything opened from the list afterwards is a fresh selection. The guard on an open detail keeps arrival itself from retiring a contract it still needs. Also documents, per Greptile, that a `none` decision still closes the detail and only suppresses the navigation. Reported by Codex on #1435. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(portals): keep alternate-id rows on the history return The previous revision let a row carrying only `movie_id`/`series_id` fall back to re-navigating, which reproduced the exact defect this PR exists to fix: the new entry has no `collectionViewState`, so the collection reopens on its default tab with the title closed. The builder now pins the resolved id onto the handoff state item when the raw row carries neither `id` nor `stream_id`, so `buildStalkerSelectedVodItem()` reports an identity the marker can bind to and those rows get the same history return as every other one. An existing id is never overwritten. Also scopes the arrival-side retirement to handoffs that actually set the marker, so a plain `stalkerReturnTo` caller such as the dashboard keeps its behaviour — I had made that unconditional, which contradicted the scope this PR claims. Reported by Codex on #1435. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * refactor(portals): drop a redundant guard in the identity normalizer `split(':')` always yields at least one element, and the workspace does not enable `noUncheckedIndexedAccess`, so the optional chain and `?? ''` fallback were unreachable rather than type-required. Behaviour is unchanged. Spotted by Greptile on #1435. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
0f7d529b40 |
refactor(portals): split the Stalker collection detail component (#1433)
StalkerCollectionDetailComponent was 529 non-blank lines, well over the 400-line production maximum, and passed lint only because it sat in tools/eslint/max-lines-baseline.mjs — a list CLAUDE.md says must only shrink. Extract four cohesive units into siblings in the same folder: - stalker-collection-playback.controller.ts — playback ownership, inline playback and the external-player fallback - stalker-collection-detail-mode.ts — pure item / detail-mode / category resolution (movie vs regular series vs embedded VOD series vs lazy is_series) - stalker-collection-favorites.controller.ts — favorites resource, sync and toggle - stalker-collection-store-snapshot.ts — store snapshot capture/restore Both controllers follow the existing StalkerVodPlaybackController model — plain classes taking a config object, constructed in a component field initializer — rather than DI services. The playback controller needs the component's `item` input signal and the favorites controller's rxResource needs an injection context; a field initializer supplies both for free, where a DI service would have needed an extra effect to feed the input across. Public template bindings and the component's public API are unchanged: the signals are re-exported by reference, so neither the template nor the spec sees the split. The spec is untouched and its 11 tests, plus the rest of the project suite (272), pass exactly as before. Regenerate the max-lines baseline so the file drops off it, and update the three doc references that named resolveDetailMode() and resolveSelectedCategory() as methods on the component. Co-authored-by: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
365cf35317 |
refactor(portals): extract the destructive Xtream refresh into one flow (#1431)
The header action and the Workspace sources page each had their own ~60-line implementation of the destructive Xtream refresh, which is why #1421 had to fix the same connectivity-guard bug twice. `XtreamRefreshFlowService` now owns the sequence once; the entry points supply only an `XtreamRefreshProgressReporter`, so neither can reach the guard reset and skip it. Extracting it surfaced a pre-existing race that neither entry point could have fixed alone: the two guards were independent, so the same playlist could be refreshed from both at once and the second run parked an already-emptied catalog over the first run's snapshot, losing favorites, history, hidden categories and playback positions. The shared flow now serializes runs per playlist, refusing a second one before the guard reset. Found by Greptile; the ordering is pinned by a test that fails when the check moves below the reset. |
||
|
|
f7bb3a13db |
feat(playlist): open recognized M3U movies in the VOD detail view (#1420)
M3U entries recognized as movie files now open in the portals' two-state VOD detail view, fed by TMDB metadata instead of the empty EPG zone. Watch-first: activation still plays immediately, with plot, cast, rating and artwork below the player; Escape reveals the Browse hero. Recognition is a synchronous URL-shape heuristic (movie container extension or an Xtream-style /movie/ path; radio, DASH, /series/ paths and episode-marker names keep today's live layout), gated on TMDB enrichment plus the new default-on Settings.m3uVodDetails toggle. Works in Electron and the PWA. Review follow-ups included: the playback payload no longer carries TMDB fields (its identity is the player's source-application key), the persisted volume reaches the player and survives Browse → Play, the enrichment guard keys on the full lookup identity, and the saved engine mounts first time instead of briefly falling back to Video.js. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
82a2948ffd |
fix(matching): keep abbreviation titles out of bare-year keys (#1426)
A leading 2-5 character uppercase token before a dash, pipe or colon was always read as a provider tag, so a film whose NAME is such a token lost it and normalized down to its release year alone. "AKA - 2023" became the key "2023", where it collided with BDE, BRO, OUT, WIL and IF — and they were offered to each other as alternative sources in VOD multi-source. "IT - 65 (2023)" (the Italian copy of the film "65") is structurally identical, so only the token's meaning can separate them. The leading token is now tested against a vocabulary — TRAILING_TAG_VOCABULARY plus a prefix-only list derived from the real catalog — but only when the strip would leave no real word behind. A compound is read by its head, so the open-ended "4K-<lang>" family keeps working while "INU-OH" and "PC-4L" are recognized as film names. An unknown token keeps its title: a refused strip costs one unmatched copy, a wrong one corrupts that film's identity in VOD multi-source, the TMDB Similar rail, DB_MATCH_TITLES and pin keys. "No real word" is decided by running the rest of the pipeline on the stripped form and looking at what comes out, never by re-implementing what later stages remove. Quality tags, trailing and underscore tags, double-dash suffixes and season markers each otherwise smuggle the strip through, and a stage added later is covered for free. Validated over the live catalog, movies and series: 83 keys fixed, 0 corrupted across 1,616,111 titles. Deriving the vocabulary from movies alone missed AMZ, D+ and P+ and broke the Paramount+/Disney+ copies of the numeric series 1923, 1883, 24 and 9-1-1. |
||
|
|
30d76a3a46 |
fix(search): keep in-flight typing when the page rewrites its query params (#1432)
* fix(search): keep in-flight typing when the page rewrites its query params `WorkspaceShellSearchSyncService` re-read `q` on every `NavigationEnd` and unconditionally called `setSearchState(...)`, which cancels the pending debounce and overwrites the search box. Any same-page navigation that carried no search intent — a downloads filter chip writing `?filter=…`, a refresh bump, or the router echoing back our own `q` — therefore ate whatever had been typed since the last applied term. While input is debouncing, ignore a navigation that stays on the same page and carries the term already applied. Real search intent (route change, back/forward, a different `q`) still syncs as before. This is the race behind the flaky `@downloads @electron keeps global and scoped libraries truthful …` e2e test: it clicks a filter chip and fills the searchbox with nothing awaited in between, so under CI load the chip's navigation lands after the keystroke, wipes the term, and `q` is never written. Reproduced deterministically by dispatching the chip click and the `input` event in the same page task; the searchbox value goes to `""` and `q` stays `null`. The new spec fails on the old code for the two regression cases and passes for the three guard cases on both. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix(search): keep history authoritative and retire the debounce on Enter Two follow-ups from review of the same-page navigation guard. Greptile: the guard could not tell an app-initiated `q` echo from back/forward landing on a history entry that carries the same term. Key the exemption on `Navigation.trigger === 'imperative'` instead, so browser history always wins over in-flight typing. `lastSuccessfulNavigation` is set immediately before `NavigationEnd` is emitted, so it describes the navigation being handled. Codex: with the guard in place, an Enter commit no longer had its queued debounce cancelled as a side effect of the resulting `NavigationEnd`. Typing "Beta " and pressing Enter before the debounce expired applied the trimmed term, then the stale timeout reapplied the untrimmed one — leaving the box and URL on "Beta" while the provider store searched "Beta ". `applySearchQuery()` now cancels the pending debounce itself, which is the correct owner of that rule rather than relying on a navigation side effect. Both new tests were mutation-checked: dropping either sub-fix fails exactly its own test and no other. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
0cba49f3e2 | fix(dashboard): keep every catalog row per title key when matching (#1425) | ||
|
|
e3f72f7dce | perf(portals): fast-fail requests to portal hosts that stopped answering (#1421) | ||
|
|
2d7811eb5f | feat(portals): add "View in portal" action to inline collection details (#1422) | ||
|
|
ea4214a0d8 |
ci(embedded-mpv): add pinned mirrors to the Linux runtime source download (#1427)
The "Build pinned Linux Embedded MPV runtime" job failed twice on 2026-08-11 because www.freedesktop.org answered GitHub runners with HTTP 418 for the fontconfig tarball. The Linux builder curled a single pinned URL with no fallback, so upstream rate-limiting reddened the build. Route downloadArchive() through the shared downloadPinnedSource() helper the macOS builder already uses, and pin a mirror for each single-host source: fontconfig and libdisplay-info (freedesktop-hosted) plus freetype, which the macOS builder already mirrors. Each mirror was downloaded and verified to hash to the existing pin. The curl hardening flags and assertArchiveMatchesPin are unchanged, and the helper verifies every candidate against the same SHA-256, so a mirror serving different bytes is rejected rather than used. Unlike macOS, the Linux manifest keeps sourceUrl at the canonical pinned value even when a mirror served the bytes: notice generation and the Snap publication boundary compare that field against the immutable pin. A used mirror is logged instead. build-linux-runtime.mjs now imports the downloader, so download-pinned-source.mjs joins the released source-archive tooling set (otherwise the archive would ship a build script it cannot run) and the Linux runtime cache key. Co-authored-by: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
5f4d219d3a |
refactor(portals): compact credential-free logging for failed Stalker requests (#1418)
* refactor(portals): log failed Stalker requests as compact credential-free summaries Failed STALKER_REQUEST handlers dumped the entire axios error object (config, request internals, agent state, stack) into the main-process console — ~100 lines per dead-portal request. Extract the existing compact Xtream error formatter into a shared portal-request-error util (action, host, pathname, code, status, message — query string never included) and use it from both handlers. The redundant pre-throw "[StalkerEvents] HTTP Error" line is dropped; HTTP >=400 already reaches the catch block and is now logged once, compactly. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(portals): never retain an unparseable request URL in error logs The URL guard in stalker.events.ts rejects credentialed portal URLs before the request URL is built, so a malformed row value such as "http://user:secret@" reaches the error formatter as-is. Its fallback copied that raw string into `pathname`, and redactSensitiveData only sanitizes userinfo of URLs it can PARSE — an unparseable one passes through verbatim, password included. Withhold the URL entirely in that branch. Verified: the added regression test fails on the old fallback and passes with the marker. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
36c2867d36 |
feat(xtream): recognize more language tags in VOD multi-source (#1417)
* feat(xtream): recognize more language tags in VOD multi-source
The sources popover's language filter and copy chips now read prefixes
with Unicode pipe lookalikes, brackets and spaced dashes, Cyrillic tags
and MULTI. When a stream title carries no tag, the language falls back
to what the stream's visible categories unambiguously state ("EN |
Netflix") — discovery aggregates category names per (playlist, stream)
in SQL, and category prefixes must pass a known-language gate because
everyday category words like new/top/hot are real ISO 639-3 codes.
Both signals stay parsed guesses: browse filter and chips only, never
ranking, failover or dub-warning inputs.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(xtream): address Codex review on multi-source language detection
Gate the new bracket and dash title forms through isKnownLanguageTag:
those positions carry quality/rip tags ([HD], [CAM], NEW -) whose
fabricated "language" would outrank and mask a real category-derived
one. The legacy pipe form stays permissive.
Overlay a late-arriving route category onto the existing route row in
the same-key refresh path — cold/direct routes load categories after
discovery, and the category is outside the movie key on purpose. The
mid-flight case is redelivered by the bind() effect re-running on the
controller's sources signal; that tracked read is now documented as
load-bearing and pinned by a session spec.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(xtream): pair brackets and strip the new tag forms when matching
Greptile: the bracket prefix chose its opening and closing delimiter
independently, so a malformed "[EN)" was read as a language tag.
Codex: recognizing a prefix is only half the job — normalizeTitleKeys
has to strip the same tag, or the tagged copy never matches the bare
one and multi-source cannot offer the film at all. Its leading-tag rule
now shares the pipe-lookalike set and, on the pipe branch only, takes
the same Latin+Cyrillic any-case alphabet with no required trailing
space. Dash and colon keep their uppercase-Latin spaced form: those are
ordinary punctuation, and loosening them would amputate "ОНО: Часть 2"
the way a case-insensitive rule amputates "It: Chapter Two".
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(xtream): keep normalization uppercase-only, measured on real catalogs
The previous commit widened the pipe branch of normalizeTitleKeys to any
case and to Cyrillic, on the theory that nothing but a tag precedes a
pipe. Checked against 1.27M real catalog titles that theory is wrong in
two ways at once: "Akira | 1988" and "Coco | 2017" put the film's name
before the pipe and the year after it, and Russian catalogs write
"Момо | Momo" — localized title, then original. The widening corrupted
349 keys and rescued none, so it is reverted.
What survives is what the data supports: the pipe-lookalike set (0
changed keys, and correct for panels that use them) and dropping the
required space after a pipe (35 changed keys, genuine welded tags like
"EN|Dark Shadows" and "|FR|VO|Le dernier empereur").
A leading-tag guard that refused to strip when no letter remained is
also dropped: it fixes "AKA | 2023" but breaks "IT - 65", so telling
those apart needs a tag vocabulary and belongs in its own change.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* docs(xtream): cite the measured evidence for the category language gate
The gate's rationale named hypothetical category shapes. On a real
catalog the four it actually turns away are VOD (5,245 movies), KIDS
(1,010), SHOW and WWE — without it the language select offers "VOD" and
"KIDS" as languages. Comments, doc and one spec case only.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* docs(xtream): restore the docblock currentSourceRow lost to an insertion
routeCategoryLanguage was added between currentSourceRow's docblock and
its signature, so the paragraph describing "the row standing for the
source the route is already playing" ended up introducing a function
that returns a language string. Moved below; no behavior change.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(xtream): stop grouping the scan tier, it can drop a matching source
content is unique per (category, type, stream), so one stream sitting in
several categories is several rows and nothing forces their titles to
agree. The GROUP BY added for group_concat let SQLite keep an arbitrary
row's title, and the normalized confirmation then rejected the whole
stream on a title a sibling row would have matched — the source vanished.
The FTS tier can afford that grouping because its window makes it
necessary; the scan tier takes no window at all, so it now returns a row
per category and their names are merged per stream in TypeScript, which
also keeps the rejected sibling's category in the language derivation.
Found by Codex. Latent rather than active on the catalog I measured (0
streams currently carry differing titles across categories), but the
schema permits it.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* docs(xtream): record why category names stay scoped to matched rows
Codex flagged that the FTS predicate runs before the aggregate, so a
sibling row under a localized title contributes no category. True, and
deliberate: the field is a guess feeding a chip and a browse filter, and
completing it costs measured latency — 0.74s to 2.0s for a correlated
subquery on a 3.9GB catalog, 19.7s for a second bounded lookup — to
correct a cosmetic guess in a shape that occurs 0 times in 2.7M rows.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
|
||
|
|
4bcd4bd390 |
feat(dashboard): add TMDB "Because you watched" recommendations rail (#1419)
* feat(dashboard): add TMDB "Because you watched" recommendations rail
TMDB has no account-free "for you" endpoint, so the rail seeds per-title
recommendations from up to 3 recently watched movies/series. Seeds resolve
through the enrichment facade via a shared lookup-attempt builder (extracted
from the hero service), and recommendations already ride in every cached
details payload, so watched seeds cost zero network. Per-seed lists are
interleaved round-robin, deduplicated by id and normalized title, stripped
of watched/favorited titles, and matched against imported libraries with one
batched DB_MATCH_TITLES request; only year-compatible matches render and
fewer than 5 cards hides the rail. Loads are keyed by the seed set, and a
load where no seed resolved retries instead of latching.
The header names the seed ("Because you watched X") when exactly one seed
contributed, else falls back to the generic "Recommended for you". New
dashboardRails.tmdbRecommendations toggle (default on) in Settings ->
Dashboard; 4 new i18n keys translated across all 19 locales.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(dashboard): harden recommendations rail reload semantics
Address Codex review findings: the load latch is now keyed by the seed
set PLUS the watched/favorited exclusion set, so favoriting a recommended
title re-filters the rail instead of being ignored by the seed-only memo;
an emptied watch history clears the root-provided service's items and
seed titles instead of leaving a stale rail; and a load requested while
one is in flight is queued and re-run afterwards, so a mid-flight history
change cannot commit results for an obsolete seed set. The dashboard
effect now also tracks favorites. Three regression tests added.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(dashboard): catalog-aware invalidation, no empty latch, original-title aliases
Address Codex round-2 findings: the load key now includes the
imported-playlist id set, so importing or deleting a playlist re-runs the
catalog matching instead of leaving dead links or hiding fresh matches; a
below-threshold (or transiently failed) match result hides the rail
WITHOUT latching, mirroring the trending rail's retry-on-empty semantics,
since matchTitles maps worker failures to an empty list; and matching plus
watched/favorited exclusion now work through both the localized TMDB title
and the original-title alias, so a catalog named in the original language
still matches while cards keep displaying the localized form. Regression
tests added for all three.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(dashboard): reset latch on hide, alias-year fallback, language-keyed loads
Address Codex round-3 findings: hiding the rail below the match threshold
now also resets the saved load key, so returning to a previously
successful input set (un-favoriting, restoring a playlist) reloads instead
of dying on the equality guard; alias matching picks the first alias whose
match is also year-compatible, so a same-named different-year row hit by
the localized title no longer vetoes the correct original-title match; and
the load key now includes the effective TMDB language (exposed on the
enrichment facade), so switching the app language re-localizes the cards
instead of keeping the previous language all session. Regression tests
added for all three.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(dashboard): two-tier watched-title exclusion, drop ES2019 flatMap
Address the Codex round-4 finding: a provider stores whatever the panel
named the file, so a watched "Inception 2010" never matched TMDB's
canonical "Inception" by exact key. Exclusion now runs on two tiers —
exact normalized title plus a year-gated base tier — so the year-suffixed
shape is caught while a stored "Blade Runner 2049" still cannot swallow
the 1982 film. An unknown year on either side counts as agreeing, since
re-recommending something already watched is the worse failure.
Also replaces the alias query builder's flatMap with a loop: the web app
compiles this lib against lib: es2018, where Array.prototype.flatMap does
not exist, which broke the web build and every job downstream of it.
Both exclusion tiers are pinned by mutation-verified regression tests.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(dashboard): index recommendation exclusions the way TMDB looks them up
Address Codex round-5 findings. The watched/favorited exclusion index is
now built through the same lookup-attempt builder the seeds and the hero
use, so an activity row is indexed under the media type the detail view
enriched with rather than its routing verdict — a Stalker embedded-VOD
series routes as 'movie' but is a show to TMDB, so its recommendation
looked up series: and sailed past a movie:-only entry — and under its
stored original-language title (info.o_name), which a translated
recommendation shares no key with. Only the builder's PRIMARY attempt is
indexed: the second is a fallback guess, and indexing it would let a
watched film exclude the same-named show.
Adds Electron E2E for the new setting: the toggle now appears in the
disabled-when-dashboard-off assertion (with the trending toggle, which
was also missing), plus a restart-persistence test. Rail rendering stays
unit-covered — it needs the TMDB opt-in, live TMDB data and catalog
matches, which would make an E2E network-dependent and flaky.
All three new unit tests are mutation-verified, including one that was
passing vacuously before this round.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(dashboard): keep every catalog row until the year gate has chosen
Address the Codex round-6 finding: buildTitleMatchIndex collapses to one
row per key before the candidate's year is known, so a catalog holding
both "Dune 1984" and "Dune 2021" keeps whichever the worker returned
first and a 2021 recommendation then fails the year check with the right
row already discarded. The rail now groups the rows per key itself and
lets the year gate pick, still preferring an exact-title match over a
year-stripped one so the shared helper's precedence is preserved.
Mutation-verified regression test.
The trending rail shares the same collapse-then-check shape and is
unaffected by this PR; flagged separately as a follow-up.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(dashboard): survive a failed refresh, document the new rail
Address Codex round-7 findings.
A refresh that cannot reach TMDB no longer leaves the rail untouched, but
it does not blank it either: a failed request is not a verdict that there
is nothing to recommend, and removing still-valid cards is the worse
answer for an offline user. What the failure cannot excuse is a card the
user has since watched or favorited, so the retained cards are re-filtered
against the fresh exclusion index and the rail hides if too few survive.
The key stays unlatched, so the next visit still retries.
Also documents the rail in the two canonical dashboard docs I missed:
the surface diagram and render rules in docs/architecture/workspace-dashboard.md
and the rail list in the feature README. Both had also never mentioned the
sibling trending rail, so that gap is closed in the same pass.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(dashboard): year-aware exclusions, remake-safe dedupe, key reset
Address Codex round-8 findings.
The exclusion index now records each row's release year (Stalker's
info.releasedate, else a year read off the title) with every key, and both
tiers gate on it, so a watched 1954 "Godzilla" no longer excludes the 2014
one. A row that states no year records null and keeps excluding
unconditionally, so the conservative behaviour survives where nothing is
known.
Candidate dedupe is by TMDB id only; title collisions are resolved after
matching, by the catalog row a candidate resolved to. Same-titled remakes
("Dune" 1984 and 2021) are different films and must both reach the
matcher — collapsing them beforehand let whichever arrived first fail the
year gate on behalf of the one the library actually holds — while two
candidates landing on one row would render as duplicate cards.
The offline re-filter now clears the saved load key, so restoring those
exact inputs (un-favoriting the title) rebuilds the rail instead of
hitting the equality guard.
Splits the pure helpers and data shapes into dashboard-recommendations.util.ts:
the service had crossed the 400-line production limit. All three fixes are
mutation-verified, including one test that only became real after the
mutation showed it passing on the wrong ordering.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(dashboard): do not latch a partially resolved seed set
Address the Codex round-9 finding: when several seeds load and only some
resolve, latching marked the whole set complete, so a seed that failed
transiently lost its recommendations for the rest of the session. The load
now latches only once every seed has answered.
A seed with no TMDB match never resolves either, so that user's rail
re-runs on each dashboard visit. That is bounded work — the enrichment
misses are cached and the catalog match is one batched worker call — and
it matches the rail's existing policy of not latching on uncertainty.
Mutation-verified regression test, plus one pinning that a fully resolved
set still latches.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(dashboard): trust only stated years on the exact exclusion tier
Address Codex round-10 findings.
The first is a regression I introduced last round: recording a
title-inferred year with the exact exclusion key meant a watched
"Blade Runner 2049" carried year 2049, disagreed with TMDB's actual 2017,
and stopped excluding the very film the user had just watched. The exact
tier now gates only on a year the row STATES in a metadata field
(Stalker's info.releasedate) — the rule releaseTagYear already documents:
on a whole-title match a trailing number belongs to the name and nothing
can settle it. The base tier keeps its stripped trailing year, which is a
suffix by construction, so the Godzilla 1954/2014 case still holds.
The offline re-filter also drops cards whose playlist has been deleted.
That path is the only one that can reach retained cards without the
catalog key rebuilding the rail, so those cards would otherwise navigate
to a dead route.
Both fixes are mutation-verified.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(dashboard): resolved media type replaces the routing one; prefer year-tagged rows
Address Codex round-11 findings.
The exclusion index no longer indexes an activity row under BOTH its
routing type and its resolved media type. A Stalker embedded-VOD series
routes as 'movie' on positive series evidence, so keeping that key made a
watched show exclude an unrelated film of the same name — and, with no
release date to gate on, unconditionally. The resolved type now replaces
the routing one; a row the builder cannot classify keeps its routing type,
which is then the only thing known.
Catalog matching now prefers a row whose stripped year IS the candidate's
over an untagged one: an untagged "Dune" row could be either cut, so
linking a 2021 recommendation to it while "Dune 2021" also exists throws
away the better evidence. Untagged rows stay next in precedence, which is
also the only tier reachable when the candidate's year is unknown.
Both mutation-verified.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(dashboard): no TV retry for catalog-classified Xtream rows
Address the Codex round-12 finding: the movie -> tv lookup retry exists
because a Stalker embedded-VOD series is stored as a 'movie' activity row,
but an Xtream row's type comes from a catalog that files movies and series
apart, so there 'movie' is evidence rather than a default. The retry let a
same-titled show answer for a film — the mirror of the existing rule that
a 'tv' verdict never retries as 'movie'.
The lookup item type had dropped the `source` field that distinguishes
them; restoring it is enough to gate the retry. This also tightens the
hero rail, which shares the builder. Mutation-verified.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(dashboard): confirmed movies skip the TV retry; key by the whole attempt chain
Address Codex round-13 findings, both consequences of last round's change.
A stored Stalker `info.tmdb_id` is never a provider claim — the contract
says its only source is a match this app already gated, under that very
media type — so such a row's 'movie' verdict is no longer the ambiguous
default the TV retry exists for. Retrying it let a same-titled show answer
for a film whenever the movie lookup transiently returned null. The retry
now runs only for rows nothing has confirmed.
The lookup key is now the whole attempt sequence rather than the primary
attempt alone: two rows can share title, year and id yet differ in whether
a TV fallback follows, and callers cache by this key — the hero's
root-level memo would otherwise serve a Stalker row's TV answer as an
Xtream movie's metadata, and selectSeeds() would collapse two seeds that
do not perform the same lookup.
Both mutation-verified. One existing hero test asserted the retry for a
fixture that carries a stored id; it now pins the confirmed-identity
behaviour instead, with a separate test for the id-less retry it used to
cover.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(dashboard): rank catalog matches by year evidence across aliases
Address the Codex round-14 finding: match selection returned as soon as
any alias had a compatible row, so an untagged row under the localized
title beat a row the original-title alias found carrying the candidate's
own year — the wrong remake when both cuts exist. Compatible rows from
every alias now form one pool ranked by evidence, with alias order kept
only as the tiebreaker inside a tier. The nested loop collapses into a
single pass in the process. Mutation-verified.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
|
||
|
|
dded17010d |
fix(playback): keep the display awake while built-in players play video (#1405)
* fix(playback): keep the display awake while built-in players play video Closes #1095. The renderer tracks every playing <video> through document-level capture listeners (element-level release listeners catch the detached-element pause on component teardown) and, while any video is playing and the document is visible, holds a display-sleep lock: a main-process powerSaveBlocker over IPC in Electron — reliable on Linux where Chromium's own video wake lock depends on DE D-Bus inhibitors — and the Screen Wake Lock API in the PWA. The vote is auto-cleared when the renderer reloads or dies. Radio's <audio> deliberately never blocks display sleep; embedded MPV and external MPV/VLC already manage their own inhibition. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(playback): withdraw the keep-awake vote when the renderer crashes A crash emits render-process-gone while the WebContents object stays alive, so the destroyed listener alone missed it: without a follow-up reload the display stayed pinned awake. Review finding by Codex. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(playback): keep the display lock for picture-in-picture playback Minimizing the window hides the document but leaves the PiP surface on screen, so the visibility gate was releasing the lock mid-watch. A tracked playing video that owns document.pictureInPictureElement now counts as visible playback, and PiP enter/leave events resynchronize the gate. Review finding by Codex. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(playback): re-evaluate the wake lock after a rejection masked a state change In the PWA path a hidden-visible round-trip (or pause/resume) while wakeLock.request() was pending got swallowed by the in-flight guard; if that request then rejected, only the flag was cleared and a continuously playing visible video sat without a wake lock until the next unrelated event. State changes arriving mid-flight now queue one re-evaluation on rejection; permanent denials still don't loop because nothing queues a retry without a fresh interleaved change. Review finding by Codex. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * docs(playback): mirror the display-sleep contract into AGENTS.md AGENTS.md carries its own playback sections (radio, shared controls, PiP), so the keep-awake contract belongs there too. Review finding by Codex. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
6041233f41 |
chore(deps-dev): bump electron from 41.10.3 to 43.3.0 (#1414)
* chore(deps-dev): bump electron from 41.10.3 to 43.3.0 Bumps [electron](https://github.com/electron/electron) from 41.10.3 to 43.3.0. - [Release notes](https://github.com/electron/electron/releases) - [Commits](https://github.com/electron/electron/compare/v41.10.3...v43.3.0) --- updated-dependencies: - dependency-name: electron dependency-version: 43.3.0 dependency-type: direct:development update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] <support@github.com> * fix(deps): prepare Electron 43 runtime policy --------- Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: 4gray <serega05@gmail.com> |
||
|
|
1e038657d6 |
chore(deps): bump better-sqlite3 from 12.9.0 to 13.0.3 (#1415)
* chore(deps): bump better-sqlite3 from 12.9.0 to 13.0.3 Bumps [better-sqlite3](https://github.com/WiseLibs/better-sqlite3) from 12.9.0 to 13.0.3. - [Release notes](https://github.com/WiseLibs/better-sqlite3/releases) - [Commits](https://github.com/WiseLibs/better-sqlite3/compare/v12.9.0...v13.0.3) --- updated-dependencies: - dependency-name: better-sqlite3 dependency-version: 13.0.3 dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] <support@github.com> * fix(deps): use better-sqlite3 prebuilt binaries * docs(deps): note SQLite worker stability fix --------- Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: 4gray <serega05@gmail.com> |
||
|
|
5c9411869a |
chore(deps): bump the npm-minor-patch group across 1 directory with 11 updates (#1416)
Bumps the npm-minor-patch group with 11 updates in the / directory: | Package | From | To | | --- | --- | --- | | [hls.js](https://github.com/video-dev/hls.js) | `1.6.16` | `1.6.17` | | [marked](https://github.com/markedjs/marked) | `18.0.7` | `18.0.9` | | [video.js](https://github.com/videojs/video.js) | `8.23.9` | `8.24.0` | | [@playwright/test](https://github.com/microsoft/playwright) | `1.62.0` | `1.62.1` | | [@swc/core](https://github.com/swc-project/swc/tree/HEAD/packages/core) | `1.15.46` | `1.15.47` | | [@typescript-eslint/eslint-plugin](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/eslint-plugin) | `8.65.0` | `8.66.0` | | [@typescript-eslint/parser](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/parser) | `8.65.0` | `8.66.0` | | [@typescript-eslint/utils](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/utils) | `8.65.0` | `8.66.0` | | [ng-mocks](https://github.com/help-me-mom/ng-mocks) | `14.15.3` | `14.16.1` | | [tsx](https://github.com/privatenumber/tsx) | `4.23.1` | `4.23.11` | | [typescript-eslint](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/typescript-eslint) | `8.65.0` | `8.66.0` | Updates `hls.js` from 1.6.16 to 1.6.17 - [Release notes](https://github.com/video-dev/hls.js/releases) - [Changelog](https://github.com/video-dev/hls.js/blob/master/docs/release-process.md) - [Commits](https://github.com/video-dev/hls.js/compare/v1.6.16...v1.6.17) Updates `marked` from 18.0.7 to 18.0.9 - [Release notes](https://github.com/markedjs/marked/releases) - [Commits](https://github.com/markedjs/marked/compare/v18.0.7...v18.0.9) Updates `video.js` from 8.23.9 to 8.24.0 - [Release notes](https://github.com/videojs/video.js/releases) - [Changelog](https://github.com/videojs/video.js/blob/main/CHANGELOG.md) - [Commits](https://github.com/videojs/video.js/compare/v8.23.9...v8.24.0) Updates `@playwright/test` from 1.62.0 to 1.62.1 - [Release notes](https://github.com/microsoft/playwright/releases) - [Commits](https://github.com/microsoft/playwright/compare/v1.62.0...v1.62.1) Updates `@swc/core` from 1.15.46 to 1.15.47 - [Release notes](https://github.com/swc-project/swc/releases) - [Changelog](https://github.com/swc-project/swc/blob/main/CHANGELOG.md) - [Commits](https://github.com/swc-project/swc/commits/v1.15.47/packages/core) Updates `@typescript-eslint/eslint-plugin` from 8.65.0 to 8.66.0 - [Release notes](https://github.com/typescript-eslint/typescript-eslint/releases) - [Changelog](https://github.com/typescript-eslint/typescript-eslint/blob/main/packages/eslint-plugin/CHANGELOG.md) - [Commits](https://github.com/typescript-eslint/typescript-eslint/commits/v8.66.0/packages/eslint-plugin) Updates `@typescript-eslint/parser` from 8.65.0 to 8.66.0 - [Release notes](https://github.com/typescript-eslint/typescript-eslint/releases) - [Changelog](https://github.com/typescript-eslint/typescript-eslint/blob/main/packages/parser/CHANGELOG.md) - [Commits](https://github.com/typescript-eslint/typescript-eslint/commits/v8.66.0/packages/parser) Updates `@typescript-eslint/utils` from 8.65.0 to 8.66.0 - [Release notes](https://github.com/typescript-eslint/typescript-eslint/releases) - [Changelog](https://github.com/typescript-eslint/typescript-eslint/blob/main/packages/utils/CHANGELOG.md) - [Commits](https://github.com/typescript-eslint/typescript-eslint/commits/v8.66.0/packages/utils) Updates `ng-mocks` from 14.15.3 to 14.16.1 - [Release notes](https://github.com/help-me-mom/ng-mocks/releases) - [Changelog](https://github.com/help-me-mom/ng-mocks/blob/main/CHANGELOG.md) - [Commits](https://github.com/help-me-mom/ng-mocks/compare/v14.15.3...v14.16.1) Updates `tsx` from 4.23.1 to 4.23.11 - [Release notes](https://github.com/privatenumber/tsx/releases) - [Changelog](https://github.com/privatenumber/tsx/blob/master/release.config.cjs) - [Commits](https://github.com/privatenumber/tsx/compare/v4.23.1...v4.23.11) Updates `typescript-eslint` from 8.65.0 to 8.66.0 - [Release notes](https://github.com/typescript-eslint/typescript-eslint/releases) - [Changelog](https://github.com/typescript-eslint/typescript-eslint/blob/main/packages/typescript-eslint/CHANGELOG.md) - [Commits](https://github.com/typescript-eslint/typescript-eslint/commits/v8.66.0/packages/typescript-eslint) --- updated-dependencies: - dependency-name: "@playwright/test" dependency-version: 1.62.1 dependency-type: direct:development update-type: version-update:semver-patch dependency-group: npm-minor-patch - dependency-name: "@swc/core" dependency-version: 1.15.47 dependency-type: direct:development update-type: version-update:semver-patch dependency-group: npm-minor-patch - dependency-name: "@typescript-eslint/eslint-plugin" dependency-version: 8.66.0 dependency-type: direct:development update-type: version-update:semver-minor dependency-group: npm-minor-patch - dependency-name: "@typescript-eslint/parser" dependency-version: 8.66.0 dependency-type: direct:development update-type: version-update:semver-minor dependency-group: npm-minor-patch - dependency-name: "@typescript-eslint/utils" dependency-version: 8.66.0 dependency-type: direct:development update-type: version-update:semver-minor dependency-group: npm-minor-patch - dependency-name: hls.js dependency-version: 1.6.17 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: npm-minor-patch - dependency-name: marked dependency-version: 18.0.9 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: npm-minor-patch - dependency-name: ng-mocks dependency-version: 14.16.1 dependency-type: direct:development update-type: version-update:semver-minor dependency-group: npm-minor-patch - dependency-name: tsx dependency-version: 4.23.11 dependency-type: direct:development update-type: version-update:semver-patch dependency-group: npm-minor-patch - dependency-name: typescript-eslint dependency-version: 8.66.0 dependency-type: direct:development update-type: version-update:semver-minor dependency-group: npm-minor-patch - dependency-name: video.js dependency-version: 8.24.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: npm-minor-patch ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> |
||
|
|
10e8187b16 | chore(deps): update epg-parser to 0.5.0 (#1413) | ||
|
|
de77c6d467 | fix(playback): update mpegts.js to 1.8.1 (#1412) | ||
|
|
77842b9d04 | fix(playback): update Shaka Player to 5.2.4 (#1411) | ||
|
|
861c6798ee | ci(deps): split sensitive dependency updates (#1409) | ||
|
|
73f6eb9b17 |
chore(deps): bump the actions-minor-patch group with 2 updates (#1403)
* chore(deps): bump the actions-minor-patch group with 2 updates Bumps the actions-minor-patch group with 2 updates: [pnpm/action-setup](https://github.com/pnpm/action-setup) and [github/codeql-action](https://github.com/github/codeql-action). Updates `pnpm/action-setup` from 6.0.9 to 6.0.10 - [Release notes](https://github.com/pnpm/action-setup/releases) - [Commits](https://github.com/pnpm/action-setup/compare/v6.0.9...v6.0.10) Updates `github/codeql-action` from 4.37.4 to 4.37.6 - [Release notes](https://github.com/github/codeql-action/releases) - [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md) - [Commits](https://github.com/github/codeql-action/compare/v4.37.4...v4.37.6) --- updated-dependencies: - dependency-name: pnpm/action-setup dependency-version: 6.0.10 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: actions-minor-patch - dependency-name: github/codeql-action dependency-version: 4.37.6 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: actions-minor-patch ... Signed-off-by: dependabot[bot] <support@github.com> * test(packaging): allow updated pnpm action --------- Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: 4gray <serega05@gmail.com> |
||
|
|
728df1a68c |
fix(packaging): restore Snap desktop runtime (#1406)
* fix(packaging): restore Snap desktop runtime * docs(packaging): publish Snap launch repair note * fix(packaging): declare Node 22.12 floor * docs(architecture): update SQLite pin rationale * fix(tooling): align Node engine floor * fix(tooling): constrain supported Node releases * docs(architecture): correct node-abi consumer |
||
|
|
2a7d7c315e |
chore(deps-dev): bump the nx-version-updates group across 1 directory with 11 updates (#1401)
Bumps the nx-version-updates group with 11 updates in the / directory: | Package | From | To | | --- | --- | --- | | [@nx/angular](https://github.com/nrwl/nx/tree/HEAD/packages/angular) | `22.7.7` | `22.7.8` | | [@nx/devkit](https://github.com/nrwl/nx/tree/HEAD/packages/devkit) | `22.7.7` | `22.7.8` | | [@nx/esbuild](https://github.com/nrwl/nx/tree/HEAD/packages/esbuild) | `22.7.7` | `22.7.8` | | [@nx/eslint](https://github.com/nrwl/nx/tree/HEAD/packages/eslint) | `22.7.7` | `22.7.8` | | [@nx/eslint-plugin](https://github.com/nrwl/nx/tree/HEAD/packages/eslint-plugin) | `22.7.7` | `22.7.8` | | [@nx/jest](https://github.com/nrwl/nx/tree/HEAD/packages/jest) | `22.7.7` | `22.7.8` | | [@nx/js](https://github.com/nrwl/nx/tree/HEAD/packages/js) | `22.7.7` | `22.7.8` | | [@nx/playwright](https://github.com/nrwl/nx/tree/HEAD/packages/playwright) | `22.7.7` | `22.7.8` | | [@nx/web](https://github.com/nrwl/nx/tree/HEAD/packages/web) | `22.7.7` | `22.7.8` | | [@nx/workspace](https://github.com/nrwl/nx/tree/HEAD/packages/workspace) | `22.7.7` | `22.7.8` | | [nx](https://github.com/nrwl/nx/tree/HEAD/packages/nx) | `22.7.7` | `22.7.8` | Updates `@nx/angular` from 22.7.7 to 22.7.8 - [Release notes](https://github.com/nrwl/nx/releases) - [Commits](https://github.com/nrwl/nx/commits/22.7.8/packages/angular) Updates `@nx/devkit` from 22.7.7 to 22.7.8 - [Release notes](https://github.com/nrwl/nx/releases) - [Commits](https://github.com/nrwl/nx/commits/22.7.8/packages/devkit) Updates `@nx/esbuild` from 22.7.7 to 22.7.8 - [Release notes](https://github.com/nrwl/nx/releases) - [Commits](https://github.com/nrwl/nx/commits/22.7.8/packages/esbuild) Updates `@nx/eslint` from 22.7.7 to 22.7.8 - [Release notes](https://github.com/nrwl/nx/releases) - [Commits](https://github.com/nrwl/nx/commits/22.7.8/packages/eslint) Updates `@nx/eslint-plugin` from 22.7.7 to 22.7.8 - [Release notes](https://github.com/nrwl/nx/releases) - [Commits](https://github.com/nrwl/nx/commits/22.7.8/packages/eslint-plugin) Updates `@nx/jest` from 22.7.7 to 22.7.8 - [Release notes](https://github.com/nrwl/nx/releases) - [Commits](https://github.com/nrwl/nx/commits/22.7.8/packages/jest) Updates `@nx/js` from 22.7.7 to 22.7.8 - [Release notes](https://github.com/nrwl/nx/releases) - [Commits](https://github.com/nrwl/nx/commits/22.7.8/packages/js) Updates `@nx/playwright` from 22.7.7 to 22.7.8 - [Release notes](https://github.com/nrwl/nx/releases) - [Commits](https://github.com/nrwl/nx/commits/22.7.8/packages/playwright) Updates `@nx/web` from 22.7.7 to 22.7.8 - [Release notes](https://github.com/nrwl/nx/releases) - [Commits](https://github.com/nrwl/nx/commits/22.7.8/packages/web) Updates `@nx/workspace` from 22.7.7 to 22.7.8 - [Release notes](https://github.com/nrwl/nx/releases) - [Commits](https://github.com/nrwl/nx/commits/22.7.8/packages/workspace) Updates `nx` from 22.7.7 to 22.7.8 - [Release notes](https://github.com/nrwl/nx/releases) - [Commits](https://github.com/nrwl/nx/commits/22.7.8/packages/nx) --- updated-dependencies: - dependency-name: "@nx/angular" dependency-version: 22.7.8 dependency-type: direct:development update-type: version-update:semver-patch dependency-group: nx-version-updates - dependency-name: "@nx/devkit" dependency-version: 22.7.8 dependency-type: direct:development update-type: version-update:semver-patch dependency-group: nx-version-updates - dependency-name: "@nx/esbuild" dependency-version: 22.7.8 dependency-type: direct:development update-type: version-update:semver-patch dependency-group: nx-version-updates - dependency-name: "@nx/eslint" dependency-version: 22.7.8 dependency-type: direct:development update-type: version-update:semver-patch dependency-group: nx-version-updates - dependency-name: "@nx/eslint-plugin" dependency-version: 22.7.8 dependency-type: direct:development update-type: version-update:semver-patch dependency-group: nx-version-updates - dependency-name: "@nx/jest" dependency-version: 22.7.8 dependency-type: direct:development update-type: version-update:semver-patch dependency-group: nx-version-updates - dependency-name: "@nx/js" dependency-version: 22.7.8 dependency-type: direct:development update-type: version-update:semver-patch dependency-group: nx-version-updates - dependency-name: "@nx/playwright" dependency-version: 22.7.8 dependency-type: direct:development update-type: version-update:semver-patch dependency-group: nx-version-updates - dependency-name: "@nx/web" dependency-version: 22.7.8 dependency-type: direct:development update-type: version-update:semver-patch dependency-group: nx-version-updates - dependency-name: "@nx/workspace" dependency-version: 22.7.8 dependency-type: direct:development update-type: version-update:semver-patch dependency-group: nx-version-updates - dependency-name: nx dependency-version: 22.7.8 dependency-type: direct:development update-type: version-update:semver-patch dependency-group: nx-version-updates ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> |
||
|
|
d73551d780 |
fix(pwa): tolerate broken IPv6 routes and surface provider error codes (#1404)
* fix(pwa): tolerate broken IPv6 routes and surface provider error codes Node's happy-eyeballs racing gives each address attempt only 250 ms, so a dual-stack provider hostname behind an IPv4-only VPN namespace (Gluetun, WireGuard) exhausts every attempt and the web backend answered with a bare 502. Raise the per-attempt budget to 2500 ms at startup — keeping the IPv6->IPv4 fallback automatic — while an explicit --network-family-autoselection-attempt-timeout from NODE_OPTIONS still wins. Provider proxy failures now log the target hostname plus the underlying Node error codes (never the URL query, which carries credentials) and return the primary code in the error body, so the app shows "Bad Gateway (ETIMEDOUT)" instead of an unexplained 502. Closes #1400 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(pwa): surface proxy network codes in import/refresh toasts, document runtime contract Codex: /parse connection failures arrive as HTTP 500 whose body carries the new code field, but fetchFromUrl()/refreshPlaylist() mapped only the HTTP status, so the toast stayed generic. Append the code to the translated message (regression-covered for both flows). Greptile: record the web-backend happy-eyeballs/diagnostics runtime contract in CLAUDE.md's monorepo structure section. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(pwa): drop provider reason phrases from logs, honor underscore flag spellings Codex round 2: the HTTP reason phrase is provider-controlled and can echo the credential-bearing request URL, so the failure log now carries only the numeric status; and Node treats underscores and dashes interchangeably in flag names, so the explicit-override check normalizes spelling before matching (verified live: --network_family_autoselection_attempt_timeout applies in both CLI and NODE_OPTIONS forms). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(pwa): match the timeout flag as a complete NODE_OPTIONS token Codex round 3 (P3): a raw substring search also fired on the flag text embedded in another option's value, silently skipping the 2500 ms default. Tokenize NODE_OPTIONS on whitespace and match the normalized option name exactly or with '='. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
5ad86e094c |
refactor(stalker): drop unused limit state from selection feature (#1402)
* refactor(stalker): drop unused limit state from selection feature The `limit` field and `setLimit` method lost their last consumers when catalog pagination was replaced by infinite scroll (#1392/#1395): the facade no longer calls setLimit and getTotalPages is gone. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * docs(stalker): retire pagination API from the store baseline The compatibility baseline still told future refactors to preserve `limit`/`setLimit` and `getTotalPages`, all three of which are gone with the catalog pagination removal. Record them in the doc's existing Removed section instead. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5 <noreply@anthropic.com> |