Deep-review of the bug-hunt round (R1). The background extractor from ed4ab4e1 held, but its seams
did not:
- Window + full pass doubled every cue in the first hour. merge_vtt de-duplicated on the raw timing
text, and the two passes spell a timestamp differently (shift_vtt writes HH:MM:SS.mmm, ffmpeg's
webvtt muxer drops the hour below 1 h; shift_vtt(-0.0) never normalised the full pass). Timing is
now parsed and re-emitted canonically, keyed with the cue text so two cues sharing a time survive.
- A full pass that exited non-zero was published as complete: "ready" forever, subtitles ending
where ffmpeg died, and no eviction to ever retry. It now covers only up to its last cue, is never
complete, and records the failure memo.
- The window's dedup identity held the live playhead, so every 1 s poll queued another read of the
file. It is keyed on the track now, and a queued job re-checks coverage before reading anything.
- A sidecar track waited behind an unrelated embedded full pass (the full job is per item). Its
window job now fetches it whole — one small request.
- The <track> fetch ran the scheduler at position 0: an unrequested window over the head of the file,
and a refusal of a window that was on disk. It is a pure read of the cache now.
- publish() is serialised (a lost update became permanent once complete was set); the pool is shut
down and running ffmpeg children killed at app shutdown (they held interpreter exit for minutes);
?at=inf is a 422, not a 500; and both subtitle routes use current_user instead of the cookie-only
poster gate, which let a suspended or epoch-revoked session keep scheduling reads (pre-existing).
_extract_and_publish finally has tests of its own — every earlier test stubbed it out. 19 mutations,
each red on the assertion that names it.
45 lines
1.7 KiB
Python
45 lines
1.7 KiB
Python
"""The subtitle routes must refuse a session the rest of the app refuses.
|
|
|
|
They used to sit behind `_require_session`, the cookie-only gate the poster proxies use — a signed
|
|
cookie was enough, with none of `current_user`'s checks (is_active, is_suspended, the session epoch
|
|
that "log out everywhere" bumps). Fine for a poster; not for a route that starts reads of the media
|
|
library. Found by the 2026-09 bug-hunt deep-review; the gate itself pre-dates that round.
|
|
"""
|
|
|
|
from fastapi.testclient import TestClient
|
|
|
|
import app.auth as auth
|
|
import app.state as app_state
|
|
from app.db import get_db
|
|
from app.main import app
|
|
from app.plex import subtitles
|
|
from app.routes.plex import playback
|
|
|
|
|
|
class _NoPool:
|
|
def __init__(self) -> None:
|
|
self.jobs: list = []
|
|
|
|
def submit(self, fn, *args) -> None:
|
|
self.jobs.append((fn, args))
|
|
|
|
|
|
def test_a_suspended_account_cannot_drive_the_subtitle_extractor(
|
|
db, make_user, override_dep, monkeypatch, tmp_path
|
|
):
|
|
user = make_user(is_suspended=True)
|
|
override_dep(get_db, lambda: db)
|
|
monkeypatch.setattr(app_state, "_configured_cache", True)
|
|
# A validly SIGNED cookie for that account — which is all the old gate looked at.
|
|
for mod in (auth, playback):
|
|
monkeypatch.setattr(mod, "resolved_user_id", lambda request: (user.id, True))
|
|
monkeypatch.setattr(subtitles, "CACHE_DIR", tmp_path)
|
|
pool = _NoPool()
|
|
monkeypatch.setattr(subtitles, "_POOL", pool)
|
|
subtitles.reset_state()
|
|
|
|
client = TestClient(app)
|
|
assert client.get("/api/plex/subtitle/98614/0/status?at=600").status_code == 401
|
|
assert client.get("/api/plex/subtitle/98614/0").status_code == 401
|
|
assert pool.jobs == [], "a refused session still scheduled an extraction"
|