fix(plex): a subtitle failure about one position no longer ends the track, and windows are read ahead and never wait behind a full pass

Deep-review round 3 of bug hunt 2026-09.

- `failed` is now final only for a track that can never be text (bitmap, no item/track/file). A Plex
  hiccup or a stretch ffmpeg could not read answers `unavailable`: said, polled again every 5 s, and
  the <track> stays mounted when cues exist elsewhere (gen > 0). Re-picking the selected track asks
  the server again at once.
- Within LOOKAHEAD_S of a covered stretch's end the next window is read ahead, instead of only once
  the playhead has fallen off it (subtitles dropped out at every edge while the full pass was backed
  off or slower than a window lasts).
- Windows and full passes run on separate pools with separate in-flight caps, so a window never
  queues behind (or is refused by) minute-long full passes — the pool comment promised that and
  nothing enforced it.
- A pass that times out is TERMinated and keeps what it reached (published as partial) instead of
  being killed and discarded.
- Lapsed failure memos are swept on every write (they were only pruned when the same key was read
  again, so polling fresh keys grew the process without bound).
- `publish` no longer merges into a sidecar with no VTT behind it, or claims a region whose VTT write
  failed, or overwrites an unreadable track with one window.
- A NUL in a rating_key names no row (it was a DataError, a 500, on every Plex route) and no longer
  500s the status poll.

Tests: 16 new, each mutation-checked (18 mutations, all red on the test that names them).
This commit is contained in:
2026-09-11 18:04:30 +02:00
parent f7faa19ee2
commit 4f37c41021
7 changed files with 453 additions and 98 deletions
+259 -46
View File
@@ -14,8 +14,11 @@ These tests pin the three things that fixes it: the request never waits for ffmp
publishes a window around the playhead before the whole track, and one ffmpeg run covers every
embedded text track — plus what the deep-review of that fix found (cues doubled at the window/full
seam, a truncated pass cached as complete, a poll that respawned ffmpeg every second, a sidecar stuck
behind an unrelated pass). Pure — no Postgres, no ffmpeg (the auth half is in
test_plex_subtitle_auth.py, which needs the DB).
behind an unrelated pass). No Postgres (the auth half is in test_plex_subtitle_auth.py, which needs
the DB). Almost every test stubs ffmpeg out, but not all: a few run a real child process, and
`test_a_window_lines_up_with_the_full_pass_on_a_real_file` needs the real ffmpeg — it is the only
guard of `-avoid_negative_ts disabled`. It skips without one, except in the gate's DB lane, where
conftest refuses to run without ffmpeg rather than let it skip.
"""
import asyncio
@@ -77,6 +80,24 @@ class _ManualPool:
fn(*args)
return len(jobs)
def shutdown(self, wait=True, cancel_futures=False) -> None:
pass # jobs still run only when the test says so — which is what the shutdown tests probe
def _manual_pool(monkeypatch) -> _ManualPool:
"""One manual pool standing in for BOTH extraction pools, so a test sees every job in the order
it was submitted (window or full pass — `window_start` tells them apart)."""
pool = _ManualPool()
monkeypatch.setattr(subtitles, "_WIN_POOL", pool)
monkeypatch.setattr(subtitles, "_FULL_POOL", pool)
return pool
def _real_pools(monkeypatch) -> None:
"""Fresh single-worker pools (the module's own may have been shut down by an earlier test)."""
monkeypatch.setattr(subtitles, "_WIN_POOL", ThreadPoolExecutor(max_workers=1))
monkeypatch.setattr(subtitles, "_FULL_POOL", ThreadPoolExecutor(max_workers=1))
def _cue(start: str, end: str, text: str) -> str:
return f"{start} --> {end}\n{text}"
@@ -120,8 +141,7 @@ def test_a_cold_track_answers_at_once_instead_of_waiting_for_ffmpeg(client, monk
def test_a_burst_of_polls_starts_one_window_and_one_full_pass(monkeypatch):
"""The player polls once a second while it waits. Each poll must JOIN the running jobs, not
start another ffmpeg over the same 17 GB."""
pool = _ManualPool()
monkeypatch.setattr(subtitles, "_POOL", pool)
pool = _manual_pool(monkeypatch)
for _ in range(5):
subtitles.ensure("98614", 0, at=600.0)
assert len(pool.jobs) == 2 # one window (at 600 s) + the one full pass
@@ -135,8 +155,7 @@ def test_a_burst_of_polls_starts_one_window_and_one_full_pass(monkeypatch):
def test_status_walks_preparing_to_partial_to_ready(client, monkeypatch):
"""A window around the playhead makes the track showable long before it is complete, and the
generation changes when it grows so the browser refetches instead of keeping the short VTT."""
pool = _ManualPool()
monkeypatch.setattr(subtitles, "_POOL", pool)
pool = _manual_pool(monkeypatch)
key = subtitles.cache_key("98614", 0)
def _publish(rating_key, sub_ord, window_start):
@@ -170,8 +189,7 @@ def test_status_walks_preparing_to_partial_to_ready(client, monkeypatch):
def test_a_position_outside_the_extracted_window_is_not_called_showable(client, monkeypatch):
"""Honest states: cues exist, but none for where the viewer just seeked to. Saying "ready" there
would put an empty track on screen with no explanation — the original complaint."""
pool = _ManualPool()
monkeypatch.setattr(subtitles, "_POOL", pool)
_manual_pool(monkeypatch)
subtitles.publish(
subtitles.cache_key("98614", 0),
_vtt(_cue("00:10.000", "00:12.000", "x")),
@@ -183,8 +201,7 @@ def test_a_position_outside_the_extracted_window_is_not_called_showable(client,
def test_an_unshowable_track_says_why_and_maps_to_a_status(client, monkeypatch):
pool = _ManualPool()
monkeypatch.setattr(subtitles, "_POOL", pool)
pool = _manual_pool(monkeypatch)
def _bitmap(rating_key, sub_ord, window_start):
raise subtitles.SubtitleUnavailable("bitmap")
@@ -379,10 +396,9 @@ def test_a_full_pass_that_died_is_not_cached_as_the_whole_track(monkeypatch):
"""A truncated full pass used to be published as complete: `ensure` answered "ready" forever, the
player stopped polling, and — the cache has no eviction — the subtitles simply ended wherever
ffmpeg had died, in every later session too. Now it covers what it provably reached; past that
a window is tried, and only when that fails too is the position answered as failed."""
a window is tried, and only when that fails too is the position answered as unavailable."""
_stub_item(monkeypatch, clean=False, cues=("39:58.000", "40:00.000"))
pool = _ManualPool()
monkeypatch.setattr(subtitles, "_POOL", pool)
pool = _manual_pool(monkeypatch)
subtitles._extract_and_publish("98614", 3, None)
key = subtitles.cache_key("98614", 3)
assert subtitles.read_meta(key) == ([[0.0, 2400.0]], False)
@@ -392,14 +408,13 @@ def test_a_full_pass_that_died_is_not_cached_as_the_whole_track(monkeypatch):
_window_raises(monkeypatch)
pool.run_all()
after = subtitles.ensure("98614", 3, at=3000.0)
assert (after["state"], after["code"]) == ("failed", "extract_failed")
assert (after["state"], after["code"]) == ("unavailable", "extract_failed")
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."""
pool = _ManualPool()
monkeypatch.setattr(subtitles, "_POOL", pool)
pool = _manual_pool(monkeypatch)
for at in (600.0, 601.0, 602.0, 603.0, 604.0):
subtitles.ensure("98614", 1, at=at)
assert len(pool.jobs) == 2, "one window + the one full pass"
@@ -409,8 +424,7 @@ def test_a_window_for_a_different_stretch_does_not_wait_behind_the_first(monkeyp
"""Measured on localdev: after a resume the first poll can still report 0 (the position is only
known once playback ticks), so the first window is cut around the wrong spot. The poll that
follows, at the real position, must get its own window at once — not queue behind that one."""
pool = _ManualPool()
monkeypatch.setattr(subtitles, "_POOL", pool)
pool = _manual_pool(monkeypatch)
subtitles.ensure("98614", 1, at=0.0)
subtitles.ensure("98614", 1, at=1198.0)
assert sorted(a[3] for _, a in pool.jobs if a[3] is not None) == [0.0, 1168.0]
@@ -419,8 +433,7 @@ def test_a_window_for_a_different_stretch_does_not_wait_behind_the_first(monkeyp
def test_a_queued_window_that_was_covered_meanwhile_reads_nothing(monkeypatch):
"""Every embedded track rides each pass over its file, so by the time a queued window for one
language runs, another language's window may already have published it."""
pool = _ManualPool()
monkeypatch.setattr(subtitles, "_POOL", pool)
pool = _manual_pool(monkeypatch)
calls = _stub_item(monkeypatch)
subtitles.ensure("98614", 3, at=600.0)
subtitles.publish(
@@ -437,8 +450,7 @@ def test_a_sidecar_does_not_wait_behind_an_embedded_full_pass(monkeypatch):
"""The full pass is one job per ITEM (the embedded fan-out shares it). A sidecar picked while an
embedded track's pass was running used to find that identity taken and wait out the whole read —
minutes — for a track that is one small fetch."""
pool = _ManualPool()
monkeypatch.setattr(subtitles, "_POOL", pool)
pool = _manual_pool(monkeypatch)
_stub_item(monkeypatch)
subtitles._INFLIGHT[("full", "98614")] = None # an embedded track's full pass is running
subtitles.ensure("98614", 0, at=600.0)
@@ -494,7 +506,7 @@ def test_shutdown_kills_a_running_extraction(monkeypatch):
"Popen",
lambda cmd, **kw: real_popen([sys.executable, "-c", "import time; time.sleep(60)"], **kw),
)
monkeypatch.setattr(subtitles, "_POOL", ThreadPoolExecutor(max_workers=1))
_real_pools(monkeypatch)
finished = threading.Event()
def _extract():
@@ -519,8 +531,7 @@ def test_the_track_fetch_serves_what_is_cached_and_schedules_nothing(client, mon
readiness at position 0. A viewer resumed an hour in got an unrequested extraction over the head
of the file, and with a failure memo alive the fetch refused a window that WAS on disk while the
status poll (which knows the position) called the track showable: silently blank subtitles."""
pool = _ManualPool()
monkeypatch.setattr(subtitles, "_POOL", pool)
pool = _manual_pool(monkeypatch)
assert client.get("/api/plex/subtitle/98614/0").status_code == 202
assert pool.jobs == [], "the fetch scheduled an extraction"
key = subtitles.cache_key("98614", 0)
@@ -534,8 +545,7 @@ def test_the_track_fetch_serves_what_is_cached_and_schedules_nothing(client, mon
@pytest.mark.parametrize("at", ["inf", "-1", "nan"])
def test_an_impossible_position_is_rejected(client, monkeypatch, at):
"""`?at=inf` reached the scheduler's arithmetic and came back as a 500 with a traceback."""
pool = _ManualPool()
monkeypatch.setattr(subtitles, "_POOL", pool)
pool = _manual_pool(monkeypatch)
assert client.get(f"/api/plex/subtitle/98614/0/status?at={at}").status_code == 422
assert pool.jobs == []
@@ -549,8 +559,7 @@ def test_an_impossible_offset_is_rejected(client, q):
def test_an_absurd_position_is_rejected_before_it_reaches_ffmpeg(client, monkeypatch):
"""ffmpeg refuses an `-ss` past its int64 microseconds, and that failure was recorded against the
TRACK — every other viewer at an uncovered position was told "failed" for a minute."""
pool = _ManualPool()
monkeypatch.setattr(subtitles, "_POOL", pool)
pool = _manual_pool(monkeypatch)
assert client.get("/api/plex/subtitle/98614/0/status?at=1e13").status_code == 422
assert pool.jobs == []
@@ -582,8 +591,7 @@ def test_a_full_pass_that_failed_is_not_read_again_while_its_output_is_watched(m
re-armed a read of the whole container. A failure that reproduces did so every time: a full
read of the file every few minutes for as long as they watched."""
_stub_item(monkeypatch, clean=False, cues=("39:58.000", "40:00.000"))
pool = _ManualPool()
monkeypatch.setattr(subtitles, "_POOL", pool)
pool = _manual_pool(monkeypatch)
subtitles._extract_and_publish("98614", 3, None)
_later(monkeypatch, subtitles.FAIL_TTL_S + 1)
for _ in range(3):
@@ -595,8 +603,7 @@ def test_a_failed_window_does_not_end_the_poll_while_the_full_pass_may_cover_it(
""" "failed" is final — the player stops asking. A window that failed used to set the TRACK's
failure memo, so a viewer anywhere outside the covered part got "failed" and never heard about
the full pass that landed a minute later."""
pool = _ManualPool()
monkeypatch.setattr(subtitles, "_POOL", pool)
pool = _manual_pool(monkeypatch)
_stub_item(monkeypatch)
_window_raises(monkeypatch)
subtitles.ensure("98614", 3, at=3000.0)
@@ -609,8 +616,7 @@ def test_a_failed_window_does_not_end_the_poll_while_the_full_pass_may_cover_it(
def test_a_backward_seek_gets_its_own_window(monkeypatch):
"""A window bucketed by position joined a seek back to 6:40 onto a running window at 9:10 that
could not cover it — the right one waited for the wrong one."""
pool = _ManualPool()
monkeypatch.setattr(subtitles, "_POOL", pool)
pool = _manual_pool(monkeypatch)
subtitles.ensure("98614", 1, at=580.0)
subtitles.ensure("98614", 1, at=400.0)
assert sorted(a[3] for _, a in pool.jobs if a[3] is not None) == [370.0, 550.0]
@@ -619,8 +625,7 @@ def test_a_backward_seek_gets_its_own_window(monkeypatch):
def test_a_playhead_crossing_a_bucket_edge_joins_the_window_that_covers_it(monkeypatch):
"""…and the same bucketing queued a second read when a playhead crossed an edge while the first
window — which already covered it — was still running."""
pool = _ManualPool()
monkeypatch.setattr(subtitles, "_POOL", pool)
pool = _manual_pool(monkeypatch)
for at in (628.0, 629.0, 630.0, 631.0, 632.0):
subtitles.ensure("98614", 1, at=at)
assert len(pool.jobs) == 2, "one window + the one full pass"
@@ -628,27 +633,26 @@ def test_a_playhead_crossing_a_bucket_edge_joins_the_window_that_covers_it(monke
def test_a_scrubbing_viewer_cannot_queue_a_window_per_stop(monkeypatch):
"""The pool's queue is unbounded and every job is a read of the library."""
pool = _ManualPool()
monkeypatch.setattr(subtitles, "_POOL", pool)
pool = _manual_pool(monkeypatch)
for at in (0.0, 1000.0, 2000.0, 3000.0, 4000.0):
subtitles.ensure("98614", 1, at=at)
assert len([1 for _, a in pool.jobs if a[3] is not None]) == subtitles._MAX_TRACK_WINDOWS
def test_nothing_more_is_queued_while_the_extractor_is_saturated(monkeypatch):
pool = _ManualPool()
monkeypatch.setattr(subtitles, "_POOL", pool)
pool = _manual_pool(monkeypatch)
for rk in range(20):
subtitles.ensure(str(rk), 0, at=0.0)
assert len(pool.jobs) == subtitles._MAX_INFLIGHT
windows = [a for _, a in pool.jobs if a[3] is not None]
assert len(windows) == subtitles._MAX_WINDOWS_INFLIGHT
assert len(pool.jobs) - len(windows) == subtitles._MAX_FULL_INFLIGHT
def test_a_window_starts_where_ffmpeg_is_told_to_seek(monkeypatch):
"""ffmpeg gets `-ss` to the millisecond, and its cues come back on THAT zero. Shifted back by an
unrounded start they landed up to half a millisecond off the full pass's — spelled differently,
and so kept twice by the merge."""
pool = _ManualPool()
monkeypatch.setattr(subtitles, "_POOL", pool)
pool = _manual_pool(monkeypatch)
subtitles.ensure("98614", 1, at=1198.4375)
(window_start,) = [a[3] for _, a in pool.jobs if a[3] is not None]
assert window_start == 1168.438
@@ -690,7 +694,8 @@ def test_shutdown_drops_queued_extractions(monkeypatch):
"""Only the kill half used to be pinned. A job still QUEUED at shutdown would otherwise start
after the kill sweep — a fresh read of the file holding the process open."""
pool = ThreadPoolExecutor(max_workers=1)
monkeypatch.setattr(subtitles, "_POOL", pool)
monkeypatch.setattr(subtitles, "_WIN_POOL", pool)
monkeypatch.setattr(subtitles, "_FULL_POOL", pool)
busy, release, ran = threading.Event(), threading.Event(), threading.Event()
pool.submit(lambda: (busy.set(), release.wait(timeout=5)))
pool.submit(ran.set)
@@ -703,7 +708,7 @@ def test_shutdown_drops_queued_extractions(monkeypatch):
def test_a_worker_that_reaches_ffmpeg_after_shutdown_never_starts_it(monkeypatch):
"""A worker still in its Plex lookup when the kill sweep ran used to start its ffmpeg anyway."""
monkeypatch.setattr(subtitles, "_POOL", ThreadPoolExecutor(max_workers=1))
_real_pools(monkeypatch)
started: list = []
monkeypatch.setattr(subtitles.subprocess, "Popen", lambda cmd, **kw: started.append(cmd))
subtitles.shutdown()
@@ -789,3 +794,211 @@ def test_a_window_lines_up_with_the_full_pass_on_a_real_file(tmp_path, monkeypat
subtitles.publish(key, full[0], (0.0, subtitles._FULL_END_S), True)
merged = subtitles.cache_read(key) or ""
assert merged.count("-->") == 30, "a window cue survived beside its full-pass twin"
# --- the third deep-review round ----------------------------------------------------------------
def test_a_transient_failure_is_said_but_is_not_a_final_answer(monkeypatch):
"""`failed` ends the player's poll and unmounts the track. A Plex hiccup memo (a minute) was given
as `failed`, so one blip ended the subtitles for the rest of the session — at cached positions
too, and after the full pass had completed behind it. It is `unavailable` now: said, asked again."""
pool = _manual_pool(monkeypatch)
key = subtitles.cache_key("98614", 0)
subtitles.publish(key, _vtt(_cue("01:40.000", "01:42.000", "x")), (70.0, 400.0), False)
subtitles._fail(key, "plex_error")
here = subtitles.ensure("98614", 0, at=3000.0)
assert (here["state"], here["code"]) == ("unavailable", "plex_error")
assert here["gen"] > 0, "the client needs to know there are cues elsewhere to keep showing"
assert subtitles.ensure("98614", 0, at=100.0)["state"] == "partial"
assert pool.jobs == [], "nothing is re-read while the memo is alive"
_later(monkeypatch, subtitles.FAIL_TTL_S + 1)
assert subtitles.ensure("98614", 0, at=3000.0)["state"] == "preparing"
assert [a[3] for _, a in pool.jobs] == [2970.0, None], "asked again once the memo lapsed"
def test_the_next_window_is_read_ahead_of_its_edge(monkeypatch):
"""With the full pass backed off (or slower than a window lasts) a window was only asked for once
the playhead had LEFT coverage — the track unmounted at every edge for a poll plus a read."""
pool = _manual_pool(monkeypatch)
subtitles._FULL_FAILED["98614"] = time.monotonic()
key = subtitles.cache_key("98614", 0)
subtitles.publish(key, _vtt(_cue("16:20.000", "16:22.000", "x")), (970.0, 1300.0), False)
assert subtitles.ensure("98614", 0, at=1200.0)["state"] == "partial"
assert pool.jobs == [], "far from the edge: nothing to read yet"
assert subtitles.ensure("98614", 0, at=1250.0)["state"] == "partial"
assert [a[3] for _, a in pool.jobs] == [1271.0], "the next window, before the edge is reached"
def test_a_window_that_died_partway_marks_the_stretch_it_did_not_reach(monkeypatch):
"""Without the span memo, a window that died before the playhead left it uncovered AND not
failed — so every poll queued the same read of the library again."""
_stub_item(monkeypatch, clean=False, cues=("00:10.000", "00:12.000"))
pool = _manual_pool(monkeypatch)
subtitles._extract_and_publish("98614", 3, 3570.0)
key = subtitles.cache_key("98614", 3)
assert subtitles.read_meta(key) == ([[3570.0, 3582.0]], False)
assert subtitles.ensure("98614", 3, at=3700.0)["state"] == "preparing"
assert [a[3] for _, a in pool.jobs] == [None], "the dead stretch was queued again"
def test_a_crashed_pass_is_recorded_like_a_failed_one(monkeypatch):
"""A timeout or a crash lands in `_run`'s catch-all. Recorded against the TRACK, a crashed window
told every viewer at any uncovered position "failed", and a crashed full pass re-armed a
whole-file read every minute instead of backing off."""
def _crash(rating_key, sub_ord, window_start):
raise RuntimeError("boom")
monkeypatch.setattr(subtitles, "_extract_and_publish", _crash)
key = subtitles.cache_key("98614", 3)
subtitles._run(("win", "98614", 3, 570.0), "98614", 3, 570.0, 600.0)
assert subtitles._window_failed(key, 600.0) and not subtitles._window_failed(key, 3000.0)
subtitles._run(("full", "98614"), "98614", 3, None, 600.0)
assert subtitles._full_failed("98614")
assert subtitles._recent_failure(key) is None, "a crash is not a verdict on the whole track"
def test_the_failure_memos_lapse(monkeypatch):
"""Both memos exist to stop a re-read, not to forbid one: permanent, a transient failure would
have held until the process restarted."""
pool = _manual_pool(monkeypatch)
key = subtitles.cache_key("98614", 3)
subtitles._pass_failed("98614", [key], None)
subtitles._pass_failed("98614", [key], 570.0)
assert subtitles.ensure("98614", 3, at=600.0)["state"] == "unavailable"
_later(monkeypatch, subtitles.FAIL_TTL_S + 1)
subtitles.ensure("98614", 3, at=600.0)
assert [a[3] for _, a in pool.jobs] == [570.0], "the window memo lapsed; the full one has not"
pool.jobs.clear()
subtitles._INFLIGHT.clear()
_later(monkeypatch, subtitles.FULL_RETRY_S + 1)
subtitles.ensure("98614", 3, at=600.0)
assert None in [a[3] for _, a in pool.jobs], "the full pass was never tried again"
def test_lapsed_memos_are_swept_not_kept_forever(monkeypatch):
"""They were pruned only when the SAME key was read again, and the status route takes any key:
polling fresh ones grew the API process without bound."""
for i in range(50):
subtitles._fail(f"sub_x{i}_0", "no_item")
subtitles._pass_failed(f"x{i}", [f"sub_x{i}_0"], None)
subtitles._pass_failed(f"x{i}", [f"sub_x{i}_0"], 0.0)
_later(monkeypatch, subtitles.FULL_RETRY_S + 1)
subtitles._fail("sub_y_0", "no_item")
assert list(subtitles._FAILURES) == ["sub_y_0"]
assert subtitles._FULL_FAILED == {} and subtitles._WIN_FAILED == {}
def test_a_window_is_not_refused_while_full_passes_hold_their_slots(monkeypatch):
"""One pool and one cap for both kinds of job: full passes (minutes each) held every worker and
every slot, and a seek window — seconds of work — waited behind them or was not queued at all."""
win, full = _ManualPool(), _ManualPool()
monkeypatch.setattr(subtitles, "_WIN_POOL", win)
monkeypatch.setattr(subtitles, "_FULL_POOL", full)
for rk in range(subtitles._MAX_FULL_INFLIGHT):
subtitles._INFLIGHT[("full", str(rk))] = None
subtitles.ensure("98614", 1, at=600.0)
assert [a[3] for _, a in win.jobs] == [570.0]
assert full.jobs == [], "past its bound no full pass is queued"
def test_a_poll_after_shutdown_queues_nothing(monkeypatch):
pool = _manual_pool(monkeypatch)
subtitles.shutdown()
subtitles.ensure("98614", 1, at=600.0)
assert pool.jobs == []
def test_a_job_dequeued_after_shutdown_reads_nothing(monkeypatch):
pool = _manual_pool(monkeypatch)
ran: list = []
monkeypatch.setattr(subtitles, "_extract_and_publish", lambda *a: ran.append(a))
subtitles.ensure("98614", 1, at=600.0)
subtitles.shutdown()
pool.run_all()
assert ran == []
def test_a_child_started_while_the_shutdown_sweep_runs_is_killed(monkeypatch):
"""The sweep can run between the pre-Popen check and the child's registration."""
killed: list = []
class _Child:
returncode = -9
def kill(self):
killed.append(True)
def communicate(self, timeout=None):
return b"", b""
def _popen(cmd, **kw):
subtitles._STOPPING = True # the shutdown sweep ran while this child was being started
return _Child()
monkeypatch.setattr(subtitles.subprocess, "Popen", _popen)
with pytest.raises(subtitles.SubtitleUnavailable):
subtitles._ffmpeg_extract("/plex-media/f.mkv", [0], None, None)
assert killed == [True]
def test_a_pass_that_times_out_keeps_what_it_reached(monkeypatch):
"""It was killed and its outputs deleted unread — so a file too big to read inside the timeout
never got past the head, and with the full-pass backoff the same doomed read came back hourly."""
real_popen = subprocess.Popen
cue = _vtt(_cue("00:05.000", "00:07.000", "reached"))
def _popen(cmd, **kw):
out = cmd[cmd.index("-y") + 1]
script = f"import time\nopen({out!r}, 'w').write({cue!r})\ntime.sleep(60)"
return real_popen([sys.executable, "-c", script], **kw)
monkeypatch.setattr(subtitles.subprocess, "Popen", _popen)
monkeypatch.setattr(subtitles, "EXTRACT_TIMEOUT_S", 1.0)
out, clean = subtitles._ffmpeg_extract("/plex-media/f.mkv", [0], None, None)
assert not clean, "a pass that ran out of time must not pass for a whole one"
assert "reached" in out[0]
def test_an_orphan_sidecar_is_not_merged_into(monkeypatch):
"""A sidecar with no VTT behind it claims cues that are not there. Merged into, it made one
window read as the whole track — `ready`, and the full pass skipped as already done."""
key = subtitles.cache_key("98614", 0)
subtitles._write_meta(key, [[0.0, subtitles._FULL_END_S]], True)
subtitles.publish(key, _vtt(_cue("16:20.000", "16:22.000", "x")), (970.0, 1300.0), False)
assert subtitles.read_meta(key) == ([[970.0, 1300.0]], False)
assert subtitles.ensure("98614", 0, at=5000.0)["state"] != "ready"
def test_an_unreadable_track_is_not_overwritten_by_one_window(monkeypatch):
"""`cache_read` answers None for a file it could not READ as well as for one that is not there.
Taken for absent, a whole track was replaced by the one window being published."""
key = subtitles.cache_key("98614", 0)
whole = _vtt(_cue("00:01.000", "00:02.000", "whole"))
subtitles.publish(key, whole, (0.0, subtitles._FULL_END_S), True)
monkeypatch.setattr(subtitles, "cache_read", lambda k: None) # an I/O error on the read
subtitles.publish(key, _vtt(_cue("16:20.000", "16:22.000", "x")), (970.0, 1300.0), False)
assert "whole" in subtitles._vtt_path(key).read_text("utf-8")
def test_a_region_that_could_not_be_written_is_not_claimed(monkeypatch):
monkeypatch.setattr(subtitles, "cache_write", lambda key, vtt: False)
key = subtitles.cache_key("98614", 0)
subtitles.publish(key, _vtt(_cue("16:20.000", "16:22.000", "x")), (0.0, 1e9), True)
assert subtitles.read_meta(key) == ([], False)
def test_a_nul_in_the_rating_key_is_not_a_500(client, monkeypatch):
"""`%00` reached `Path.stat`, which raises ValueError rather than OSError — a 500 on every poll,
after the scheduler had already queued two jobs for a key that names nothing."""
_manual_pool(monkeypatch)
assert client.get("/api/plex/subtitle/%00/0/status").status_code == 200
def test_a_nul_rating_key_names_no_row():
"""Postgres refuses NUL in text, so looking it up was a DataError (a 500) on every Plex route."""
from app.models import PlexItem
from app.plex.lookups import by_rating_key
assert by_rating_key(None, PlexItem, "12\x0034") is None # type: ignore[arg-type]