fix(r5): address the fifth /code-review high round — 6 findings

- run_ffmpeg stops the process from ONE exit path (`except BaseException`), so
  an exception out of on_progress (a DB write) can no longer orphan a live
  ffmpeg after a 10s join on its pipes; _stop is now a no-op on a dead process
- both run_ffmpeg callbacks are throttled to ~1s (they were a DB round-trip per
  output line: ~10^5 queries on a long re-encode), mirroring the download hook
- volume arithmetic extracted to lib/playerVolume with unit tests; a corrupt
  level now falls back to the LAST GOOD one instead of full blast, and the
  sanitising happens where the stored value enters
- RSS is back to two except clauses (the shared tail already sits after the try)
- new tests: callback-exception cleanup, the two throttles; the fake Popen grew
  poll() and its wait count is now asserted

Mutation-checked: each new group goes red when its fix is reverted.
This commit is contained in:
2026-07-24 01:50:50 +02:00
parent fc82c8b47a
commit c4942eb590
8 changed files with 268 additions and 33 deletions
+44 -4
View File
@@ -319,6 +319,46 @@ def _make_progress_hook(job_id: int, asset_id: int):
return hook
# ffmpeg's `-progress pipe:1` emits a block of ~12 lines every ~0.5s, and run_ffmpeg calls both of
# its callbacks once per LINE. Each of these is a database round-trip (`_job_status` opens a session
# and SELECTs; `_set_job` opens one and UPDATE+COMMITs), so an unthrottled pair costs ~10^5 queries
# on a long re-encode. Both are therefore rate-limited to ~1s — the same 1s DB throttle the
# download progress hook already applies (see `_make_progress_hook`).
_FFMPEG_CALLBACK_INTERVAL_S = 1.0
def _ffmpeg_progress_writer(job_id: int, phase: str):
"""`on_progress` for run_ffmpeg: persist at most one row write per second, and only when the
whole-percent value actually changed (the bar shows integers)."""
state = {"at": 0.0, "pct": -1}
def write(pct: float) -> None:
whole = int(pct)
now = time.monotonic()
if whole == state["pct"] or now - state["at"] < _FFMPEG_CALLBACK_INTERVAL_S:
return
state["at"] = now
state["pct"] = whole
_set_job(job_id, progress=whole, phase=phase)
return write
def _ffmpeg_cancel_poll(job_id: int):
"""`should_cancel` for run_ffmpeg: ask the DB at most once a second and reuse the answer in
between. Cancellation is sticky, so once it says yes it never queries again."""
state = {"at": 0.0, "cancelled": False}
def cancelled() -> bool:
now = time.monotonic()
if not state["cancelled"] and now - state["at"] >= _FFMPEG_CALLBACK_INTERVAL_S:
state["at"] = now
state["cancelled"] = _job_status(job_id) not in ("running", None)
return state["cancelled"]
return cancelled
def _pp_phase(pp: str) -> str:
"""Map a yt-dlp postprocessor class name to a user-facing phase label. ffmpeg post-steps
aren't byte-progress operations (no %), so the UI shows the step name + an indeterminate
@@ -492,8 +532,8 @@ def _ensure_browser_playable(job_id: int, produced: Path, spec: dict, info: dict
editmod.run_ffmpeg(
editmod.build_browser_safe_cmd(produced, dest, reenc_v, reenc_a),
total_s=total_s,
on_progress=lambda pct: _set_job(job_id, progress=int(pct), phase="processing"),
should_cancel=lambda: _job_status(job_id) not in ("running", None),
on_progress=_ffmpeg_progress_writer(job_id, "processing"),
should_cancel=_ffmpeg_cancel_poll(job_id),
)
produced.unlink(missing_ok=True)
# Keep the stored asset metadata truthful about what's now on disk.
@@ -671,8 +711,8 @@ def _process_edit(job_id: int, asset_id: int) -> None:
editmod.run_ffmpeg(
cmd,
total_s,
on_progress=lambda pct: _set_job(job_id, progress=int(pct), phase="editing"),
should_cancel=lambda: _job_status(job_id) not in ("running", None),
on_progress=_ffmpeg_progress_writer(job_id, "editing"),
should_cancel=_ffmpeg_cancel_poll(job_id),
)
if not dest_staging.exists():
raise RuntimeError("ffmpeg produced no output file")