fix(plex): the subtitle poll backs off on errors, a failed <track> is replaced, and a resume asks for the right window
Deep-review R2, plus one defect found while verifying it on localdev. - The status poll re-armed every second after a FAILED request: `data` stays empty on an error, so it read as "still preparing" for as long as the player was open — and the request was not quiet, so a persistent 500 re-opened the global error dialog every second. It now backs off to 10 s (a blip still heals on its own) and is quiet; the player already shows the failure inline. - A <track> whose own VTT fetch failed never fetched again: with the server's answer unchanged its key was unchanged, so React kept the dead element — silently blank subtitles. The key (now built in the hook, where it is tested) carries a failed-load count; after two remounts the track is reported as unavailable instead. - Found in UAT: until the first timeupdate `abs` is 0, so after a resume every poll said at=0 and the first window was cut around the head of the film (a resume at 20:00 polled at=0 for as long as it stayed paused). The poll reports the session's start until playback ticks. And the window's dedup identity is the track plus a window-length bucket of its start — keyed on the track alone (R1), the right window queued behind that wrong one. The poll's start/stop rules finally have tests (every earlier one resolved a single fixed answer). Verified on localdev against Westworld S1E6 resumed at 20:00: the polls carried at=1197, one subtitle line on screen at 21:02, the poll stopped at `ready`; the real file's window (56 cues) + full pass (608) merged to 608 — the old merge would have served 664. 11 mutations, all red.
This commit is contained in:
@@ -439,14 +439,17 @@ def _schedule(rating_key: str, sub_ord: int, at: float, want_window: bool) -> No
|
||||
full pass that makes it complete. Each is deduplicated by identity, so a 1 s status poll (or a
|
||||
second viewer) joins the running job instead of starting another ffmpeg.
|
||||
|
||||
A window's identity is its TRACK, never its position. The poll sends the live playhead, so a
|
||||
position-keyed window minted a new identity every second and queued one more read of the file per
|
||||
poll until the first window landed. One window per track is in flight at a time; a seek past it
|
||||
is picked up by the next poll after it publishes. The full pass is per ITEM, because one read
|
||||
fans out to every embedded text track of the file."""
|
||||
A window's identity is its track plus WHICH STRETCH of the film it is for — never the exact
|
||||
position. The poll sends the live playhead, so a position-keyed window minted a new identity every
|
||||
second and queued one more read of the file per poll until the first window landed; bucketing the
|
||||
start by the window's own length makes a playhead creeping along join the job already running. It
|
||||
must not be the track alone either: then a window cut around the wrong spot (the first poll after
|
||||
a resume can still report 0) made the right one wait for it. The full pass is per ITEM, because
|
||||
one read fans out to every embedded text track of the file."""
|
||||
jobs: list[tuple[tuple, float | None]] = []
|
||||
if want_window:
|
||||
jobs.append((("win", rating_key, sub_ord), max(0.0, at - WINDOW_PRE_S)))
|
||||
w_start = max(0.0, at - WINDOW_PRE_S)
|
||||
jobs.append((("win", rating_key, sub_ord, int(w_start // WINDOW_LEN_S)), w_start))
|
||||
jobs.append((("full", rating_key), None))
|
||||
for ident, window_start in jobs:
|
||||
with _LOCK:
|
||||
|
||||
Reference in New Issue
Block a user