fix(plex): subtitles — a retried full pass resumes, a stalled track is polled slowly

A full pass that ran out of time kept what it reached, but its retry an hour
later started again at 0, so a file too big to read inside the extraction
timeout never completed. The pass now starts where every embedded track's
coverage from the head ends (less the window pre-roll, so a cue across the
join is read whole). The seek needs -avoid_negative_ts on this read as on a
window, measured on a real file: without it the cue running at the start
shifts the whole output.

A partial track whose full pass had failed never reached a settled state, so
the player polled it every 2 s for the rest of the film. The status now says
whether the track is still growing, and a stalled one is polled every 15 s —
still inside the 60 s lookahead that reads the next window before an edge.
This commit is contained in:
2026-09-19 07:07:53 +02:00
parent e6b76e156a
commit c60359bfed
5 changed files with 126 additions and 5 deletions
@@ -412,6 +412,54 @@ def test_a_full_pass_that_died_is_not_cached_as_the_whole_track(monkeypatch):
assert (after["state"], after["code"]) == ("unavailable", "extract_failed")
def test_a_retried_full_pass_resumes_where_every_track_already_reaches(monkeypatch):
"""A pass that ran out of time keeps what it reached, but the retry an hour later started again
at 0 — so a file too big to read inside `EXTRACT_TIMEOUT_S` never completed. It now starts at
the end of the span every track has from the start (less `WINDOW_PRE_S`, so a cue across the
join is read whole), and finishing from there completes the track."""
calls = _stub_item(monkeypatch, clean=False, cues=("39:58.000", "40:00.000"))
_manual_pool(monkeypatch)
subtitles._extract_and_publish("98614", 3, None)
assert calls[-1] == ([0, 2], None, None), "the first pass reads from the head"
calls = _stub_item(monkeypatch, clean=True, cues=("00:05.000", "00:07.000"))
subtitles._extract_and_publish("98614", 3, None)
assert calls == [([0, 2], 2370.0, None)]
for sub_ord in (1, 3):
assert subtitles.read_meta(subtitles.cache_key("98614", sub_ord))[1] is True
assert "00:39:35.000 --> 00:39:37.000" in (
subtitles.cache_read(subtitles.cache_key("98614", 3)) or ""
), "the resumed read's cues sit at absolute time (2370 + 5 s)"
def test_a_full_pass_starts_at_the_head_while_any_track_lacks_it(monkeypatch):
"""One read feeds every embedded track, so the least-covered one decides: a window elsewhere in
the film, or one track with the head and another without, still means reading from 0."""
calls = _stub_item(monkeypatch)
_manual_pool(monkeypatch)
subtitles._extract_and_publish("98614", 3, 3570.0) # a window mid-film, both tracks
subtitles.publish(subtitles.cache_key("98614", 1), _vtt(), (0.0, 600.0), False)
subtitles._extract_and_publish("98614", 3, None)
assert calls[-1] == ([0, 2], None, None)
def test_the_status_says_whether_the_track_is_still_growing(monkeypatch):
"""`partial` alone kept the player polling every 2 s for the rest of the film once the full
pass had failed and backed off — a state that never reaches `ready` on its own."""
_stub_item(monkeypatch, clean=True, cues=("00:05.000", "00:07.000"))
pool = _manual_pool(monkeypatch)
subtitles._extract_and_publish("98614", 3, 0.0)
assert subtitles.ensure("98614", 3, at=10.0)["growing"] is True
pool.jobs.clear()
_stub_item(monkeypatch, clean=False, cues=("39:58.000", "40:00.000"))
subtitles._extract_and_publish("98614", 3, None)
assert subtitles.ensure("98614", 3, at=100.0) == {
"state": "partial",
"code": None,
"gen": subtitles.generation(subtitles.cache_key("98614", 3)),
"growing": False,
}
def test_polls_that_follow_the_playhead_join_one_window(monkeypatch):
"""The poll sends the LIVE position, so it moves a second per poll. A window keyed on that
position was a new job every second — another read of the file per poll until the first landed."""
@@ -797,6 +845,25 @@ def test_a_window_lines_up_with_the_full_pass_on_a_real_file(tmp_path, monkeypat
assert merged.count("-->") == 30, "a window cue survived beside its full-pass twin"
@needs_ffmpeg
def test_a_full_pass_resumed_mid_file_lines_up_on_a_real_file(tmp_path, monkeypatch):
"""A retried full pass seeks like a window does but reads to the end (no `-t`): the cue already
running at its start must come out on time, not shifted to 0 with everything after it."""
src = _make_mkv(tmp_path)
monkeypatch.setattr(subtitles, "CACHE_DIR", tmp_path / "cache")
head, clean = subtitles._ffmpeg_extract(str(src), [0], None, 30.0)
assert clean
rest, clean = subtitles._ffmpeg_extract(str(src), [0], 21.3, None)
assert clean
key = subtitles.cache_key("1", 0)
subtitles.publish(key, head[0], (0.0, 30.0), False)
subtitles.publish(key, subtitles.shift_vtt(rest[0], -21.3), (21.3, subtitles._FULL_END_S), True)
merged = subtitles.cache_read(key) or ""
assert "00:00:22.000 --> 00:00:23.500\nline 11" in merged, merged
assert "00:00:58.000 --> 00:00:59.500\nline 29" in merged, merged
assert merged.count("-->") == 30, "a cue across the join was kept twice or lost"
# --- the third deep-review round ----------------------------------------------------------------