mirror of
https://github.com/4gray/iptvnator.git
synced 2026-10-12 12:06:16 -08:00
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>
This commit is contained in:
105 files changed
+8329
-299
No files matched your search
@@ -282,7 +282,148 @@ variants, contextual buttons, and theme-aware styling.
|
||||
ignores search/category filtering while honoring route scope, so hiding a
|
||||
card never makes its disk footprint appear to vanish.
|
||||
|
||||
## Live-TV recordings
|
||||
|
||||
Recordings made with the embedded MPV player live beside downloads, not inside
|
||||
them: a recording has no source URL to re-fetch, no byte totals, and no
|
||||
retry/resume semantics, so it gets its own `recordings` SQLite table (no
|
||||
unique index — re-recording a channel is normal; `playlist_id` carries no FK
|
||||
so recordings survive source deletion, with `playlist_name` stored as a
|
||||
display snapshot via `playlistDisplayLabel`).
|
||||
|
||||
- **Lifecycle tracking** is owned by `EmbeddedMpvRecordingTracker`
|
||||
(`apps/electron-backend/src/app/services/embedded-mpv-recording-tracker.ts`):
|
||||
explicit start/stop hooks in `EmbeddedMpvNativeService` plus a
|
||||
session-snapshot observer. The stop hook is a *request*, not an outcome —
|
||||
`addon.stopRecording()` only dispatches (async mpv property set, or a
|
||||
command written to the frame-copy helper), so finalization always waits for
|
||||
the snapshot reporting the recording inactive; statting or unlinking
|
||||
earlier would report a short recording as failed and could delete bytes mpv
|
||||
is still flushing. macOS native-view clears `recordingActive` *before*
|
||||
dispatching the async property set and restores it if that request is
|
||||
rejected, so an inactive snapshot must additionally survive a 1.5 s settle
|
||||
window (three poll cycles) before it counts as an acknowledgement; a revived
|
||||
recording cancels the pending finalization. A 10 s bound finalizes anyway if
|
||||
the acknowledgement never arrives. None of this timing is load-bearing for
|
||||
stop enrichment — that path does not wait for finalization at all. The same observer covers the stop paths that never call
|
||||
`stopRecording()` at all (stream-replacement auto-stop, frame-copy helper
|
||||
crash, session error/close). A synthetic error/closed snapshot arrives
|
||||
while teardown may still be flushing (the frame-copy helper exits up to
|
||||
~2 s after `disposeSession()`), so it defers finalization behind a 2.5 s
|
||||
flush window instead of statting immediately — the row stays `recording`
|
||||
(startup-repairable) through the window, and an already-armed stop settle
|
||||
timer keeps its `completed` verdict. Statuses: `recording` → `completed` (acknowledged
|
||||
stop, `fs.stat` size) / `interrupted` (implicit stop with a playable partial
|
||||
— MPEG-TS is streamable) / `failed` (start error or absent/empty file). Only
|
||||
a recording that never went active has its empty pre-reserved file unlinked;
|
||||
once mpv owned the file, the bytes are left alone. Rows carry `owner_pid`,
|
||||
so startup repair (`recording-recovery.ts`, `reconcileStaleRecordings`)
|
||||
resolves what a hard kill left in `recording` while skipping rows another
|
||||
live instance still owns (`IPTVNATOR_ALLOW_MULTIPLE_INSTANCES`) and rows
|
||||
the tracker itself is still tracking (`activeRowIds()`) — the renderer is
|
||||
interactive before this pass runs, so a recording started during bootstrap
|
||||
carries this process's own pid and only the tracker's ledger proves it
|
||||
live. Bare pid liveness is not ownership: the holder must also look like
|
||||
an IPTVnator/Electron process (`ps`/`tasklist` name, best-effort) and must
|
||||
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, even
|
||||
when it landed on another Electron app; unreadable evidence stays
|
||||
conservative and keeps the skip. Repair changes broadcast one
|
||||
`RECORDINGS_UPDATE_EVENT` because the renderer may already hold the
|
||||
pre-repair list. Both repair and tracker finalization stat through a
|
||||
bounded async probe (3 s deadline; only `ENOENT`/`ENOTDIR` proves
|
||||
absence): repair leaves an unjudgeable row in `recording` for a later
|
||||
startup, finalization keeps the requested status with an unknown size —
|
||||
a recording directory on a dead network mount must neither block the
|
||||
main thread/queue nor brand a likely-good file `failed`. Repair probes
|
||||
its whole batch concurrently, so `main.ts` awaits roughly one deadline,
|
||||
not one per row. List decoration likewise preserves an inconclusive
|
||||
probe: `ElectronRecordingItem.fileAvailability` adds `'unknown'`, and
|
||||
consumers gate on `=== 'missing'` so only proven absence reaches Needs
|
||||
attention or hides file actions. The focused
|
||||
recording detail route is guarded by the `supportsRecordings` capability
|
||||
(redirect to the manager) since `RecordingsService` never becomes
|
||||
authoritative in the PWA. Tracker finalization is bound to the exact open entry, never the
|
||||
reusable session id: a stop → immediate restart on the same session leaves
|
||||
the old entry to its own settle timer, and replacing the map entry arms
|
||||
that timer if no stop was ever observed, so neither row can be finalized
|
||||
by the other's lifecycle.
|
||||
- **Metadata is captured at recording start** (EPG is time-sensitive and
|
||||
Xtream/Stalker EPG never reaches SQLite): each live host — M3U player,
|
||||
Xtream live layout, Stalker ITV layout, unified live tab — assembles a
|
||||
`RecordingStartMetadata` (channel name/logo, playlist id + display-label
|
||||
snapshot, source type, EPG key, current program) that flows
|
||||
`WebPlayerViewComponent → EmbeddedMpvPlayerComponent →
|
||||
EmbeddedMpvControlsAdapter → EmbeddedMpvRecordingStartOptions.metadata`.
|
||||
`EmbeddedMpvPlayerComponent` watches the session snapshot for the
|
||||
active→inactive recording edge and emits `recordingStopped` — one owner for
|
||||
every trigger, including a Stop clicked in the download manager, which
|
||||
talks to the main process directly and never reaches the player's own
|
||||
toggle. The event carries the EPG key captured while the recording was
|
||||
active, because a channel switch auto-stops the recording and the host's own
|
||||
state already describes the new channel by the time the stop is handled;
|
||||
each host enriches only when that key still matches its current channel.
|
||||
The EPG key is not unique for M3U items (a `tvgId`, or the display-name
|
||||
fallback, can be shared by several list entries), so hosts with M3U
|
||||
selections additionally set `RecordingStartMetadata.sourceItemKey` (the
|
||||
unified tab's `item.uid`, the M3U player's `channel.id`) — captured and
|
||||
carried through the stop event the same way, and compared before
|
||||
enriching, so switching between two same-keyed items cannot attach the
|
||||
second item's schedule to the first item's recording. The host answers with **stop enrichment**: it filters its in-memory
|
||||
program list to the programs overlapping `[startedAt, endedAt]`
|
||||
(`filterRecordingProgramsOverlap` in `@iptvnator/shared/interfaces`) and
|
||||
sends them through `RECORDINGS_UPDATE_PROGRAMS`, keyed by the unique
|
||||
target path — that is how a recording spanning a program boundary lists
|
||||
every covered show. The handler is deliberately independent of finalization: it
|
||||
looks the row up in **any** status (the newest for that path — `openSync
|
||||
('wx')` keeps a reserved path exclusive while its recording owns it) and
|
||||
`finalize()` never touches `programs_json`, so the two writes commit in
|
||||
either order. The only wait is the tracker's queue drain, which guarantees
|
||||
the row's INSERT exists — no deadline, and therefore no way for a one-shot
|
||||
enrichment to be dropped by a clock. A recording stopped while no player is mounted on that
|
||||
channel keeps its start snapshot.
|
||||
- **IPC surface** (`recordings.events.ts`): `RECORDINGS_GET_LIST/GET/STOP/
|
||||
REMOVE/UPDATE_PROGRAMS/REVEAL_FILE/PLAY_FILE` plus the dedicated
|
||||
`RECORDINGS_UPDATE_EVENT` bare ping (not shared with downloads, so
|
||||
recording transitions do not force availability-probed download refetches).
|
||||
Active rows are decorated with a live `fs.stat` size — `file_size_bytes` is
|
||||
only persisted at finalization, so the manager's growing size comes from
|
||||
there. Recording totals also feed the manager-wide All chip and the header's
|
||||
active badge, so a page listing only recordings never reads "All 0".
|
||||
Reveal/play are gated by `isManagedRecordingFile` — the path must exist in
|
||||
the recordings table, mirroring `isManagedDownloadFile`, so the
|
||||
renderer-supplied recording directory stays a write-location preference
|
||||
rather than a shell-access grant. `RECORDINGS_STOP` resolves the row's
|
||||
`session_id` and stops through `EmbeddedMpvNativeService`, so the manager
|
||||
can stop a recording without knowing about MPV sessions — but only for rows
|
||||
this process owns: session ids restart per process, so dispatching a
|
||||
foreign row's id would stop an unrelated local recording. Remove keeps
|
||||
finished files on disk (same contract as downloads) and cleans up a failed
|
||||
row's leftover reservation only while no other row claims that path — a
|
||||
retry within the same timestamp second reuses the freed name. Renderer gate: a separate
|
||||
`supportsRecordings` capability allowlist — deliberately NOT folded into
|
||||
`supportsDownloads`, which would strip older builds of the whole manager.
|
||||
- **UI**: `RecordingsService` mirrors `DownloadsService` (one global list,
|
||||
coalesced refreshes via `DownloadListLoadState`, refetch on ping). The
|
||||
manager adds a `recording` filter chip; `recording-manager.viewmodel.ts`
|
||||
partitions rows into a "Recording now" queue section (pulsing REC chip with
|
||||
elapsed time and live file size — never a percentage, the length is
|
||||
unknown; the size comes from a bounded, in-flight-coalesced `stat` with a
|
||||
1 s deadline so a recording directory on a dead network filesystem
|
||||
degrades to "no size" instead of wedging every list load), a recordings-only Needs attention list (Remove only: a broadcast
|
||||
cannot be re-recorded), and a "Recordings" library section of 16:9
|
||||
channel-logo cards (`recording-queue.component.*`,
|
||||
`recording-library.component.*`). Card titles use the captured program
|
||||
title, falling back to channel + start time. The focused detail
|
||||
(`recording-detail/`, route `/workspace/downloads/recording/:recordingId`,
|
||||
context panel and route search hidden via `workspace-shell-route.utils.ts`)
|
||||
shows the recorded time range, covered programs when a recording spans ≥2
|
||||
shows, file path, and Play/Reveal/Stop/Remove; a missing file degrades to
|
||||
Back + Remove.
|
||||
|
||||
Keeping the backend queue, IPC handlers, shared schema, and renderer signals
|
||||
synchronized minimizes drift between platform rules and the UI. Future work
|
||||
might cover recordings, queue reordering, bulk pause/cancel actions, disk-free
|
||||
space telemetry, or playback analytics.
|
||||
might cover queue reordering, bulk pause/cancel actions, disk-free space
|
||||
telemetry, playback analytics, or frame-copy screenshot posters for
|
||||
recordings.
|
||||
@@ -532,7 +532,7 @@ replacement, engine handoff, or component destruction cancels pending
|
||||
ownership, timers, and stale feedback. Native-view retains its existing
|
||||
recording UI and timer path.
|
||||
|
||||
The default recording folder is `app.getPath('downloads')`, matching the desktop download manager's fallback. Users can override it in Settings through `Settings.recordingFolder`; an empty setting means system Downloads. Recordings are intentionally not inserted into the Downloads database or queue in v1 because MPV writes from the active playback session while the download manager owns independent backend download jobs.
|
||||
The default recording folder is `app.getPath('downloads')`, matching the desktop download manager's fallback. Users can override it in Settings through `Settings.recordingFolder`; an empty setting means system Downloads. Recordings never enter the Downloads queue (MPV writes from the active playback session while the download manager owns independent backend download jobs), but their lifecycle IS tracked: `EmbeddedMpvRecordingTracker` (`apps/electron-backend/src/app/services/embedded-mpv-recording-tracker.ts`) persists each recording into the dedicated `recordings` SQLite table — start hook and explicit-stop hook from `EmbeddedMpvNativeService`, plus a session-snapshot observer that catches the three implicit stop paths (stream-replacement auto-stop, frame-copy helper crash leaving `active: true` behind, session error/close). Rows left in `recording` by a hard app kill are repaired at startup into playable `interrupted` partials or `failed`. The download manager surfaces these rows; see `docs/architecture/download-manager.md` ("Live-TV recordings").
|
||||
|
||||
mpv's own caveats apply: the output container is inferred from the target extension, and seeking or switching streams while recording can produce broken output. IPTVnator limits the UI to live streams and stops recording on playback replacement to avoid the most obvious corruption path, but the feature should still be treated as an experimental embedded MPV capability.
|
||||
|
||||
|
||||
Reference in new issue
Block a user