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
@@ -208,6 +208,22 @@ describe("usePlexTracks — when the status poll asks again, and when it stops",
expect(status).toHaveBeenCalledTimes(1);
});
it("slows down on a partial track that will not grow on its own, and keeps asking", async () => {
// The full pass failed and is backed off for an hour: `partial` never turns `ready` by itself,
// and the 2 s cadence ran for the rest of the film. It still carries the position — reading
// ahead of an edge depends on it — so it slows rather than stops.
const status = vi
.spyOn(api, "plexSubtitleStatus")
.mockResolvedValue({ state: "partial", code: null, gen: 4, growing: false });
const { result } = mount(client());
result.current.setSubOrd(0);
await waitFor(() => expect(status).toHaveBeenCalledTimes(1));
await vi.advanceTimersByTimeAsync(10_000);
expect(status).toHaveBeenCalledTimes(1);
await vi.advanceTimersByTimeAsync(6000);
expect(status).toHaveBeenCalledTimes(2);
});
it("backs off after a failed request, and still asks again so a blip heals on its own", async () => {
// A failed request leaves `data` empty; it used to read as "still preparing" and re-ask every
// second for as long as the player stayed open.