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).
- by_rating_key: generic-typed (type[_M] -> _M | None) so call sites keep their
Model | None static type.
- Migrate the last 2 open-coded PlexCollection-by-rating_key lookups (sync.py
resync_collection + delete_collection_local) to by_rating_key; zero open-coded
rating_key lookups now remain in the package.
- Add test_playlist_bulk_add_and_remove covering the bulk add/remove endpoints
and pinning the remove-bulk synchronize_session=False path.
Full suite 258 passed, 2 skipped; ruff clean.
- Restore synchronize_session=False on the remove-bulk Core delete (the 2.0
default is 'auto'; keep the pre-conversion contract explicit).
- _episode_nav: list(...scalars()) instead of [r[0] for r in ...all()], matching
the house style used across the conversion.
- Extract app/plex/lookups.by_rating_key() and migrate the get-by-rating_key
pattern at 13 sites (imgcache x4, _shared.item_or_404, playback show/season,
browse x6).
- Coverage: add TestFacetsWithData (the converted wsq aggregation with real
data — the highest-risk untested builder), TestPlaylistAndCollectionEndpoints
(playlist CRUD + collections list), and an item-state playback test.
Full suite 256 passed, 2 skipped; ruff clean.