fix(plex): a refused finish restores no resume point at the media's own end

The rollback of a refused watched mark guarded against Plex's REPORTED
duration. On a file shorter than its metadata the end sits outside the
finish margin, so playing to the end and having the mark refused stored the
very end as a resume point on an unwatched row: a ~99% bar that reopened in
the last seconds. The media's own ended flag now decides.
This commit is contained in:
2026-09-19 06:58:11 +02:00
parent bacd6aec4f
commit 2df45b27d3
2 changed files with 23 additions and 1 deletions
@@ -420,6 +420,24 @@ describe("leaving an item that was played through", () => {
expect(qc.getQueryData<PlexItemDetail>(qk.plexItem("A", "en"))?.status).not.toBe("watched");
});
it("restores nothing when the media ended short of the duration Plex reports", async () => {
// Plex's metadata can overrun the file. Ending at 2900 of a reported 3000 is outside the finish
// margin, so the duration-based guard alone wrote 2900 back as the resume point: an unwatched
// item with a ~99% bar that reopened in its last seconds. The media's own `ended` decides.
prefs({ upNext: false });
const { progress } = mount({
details: { A: item("A"), B: item("B") },
sessions: { A: () => Promise.resolve(direct("A")), B: () => new Promise(() => {}) },
queue: ["A", "B"],
});
vi.spyOn(api, "plexSetState").mockRejectedValue(new Error("offline"));
await playingAt("/media/A", 2900);
endMedia();
await screen.findByText("Title B");
await tick();
expect(progress.mock.calls.filter(([id, , , final]) => id === "A" && final)).toEqual([]);
});
it("does restore the position when the refused finish came from the credits skip", async () => {
// The other caller of the same helper captures a MID-episode position, and there the rollback
// is the point: without it the viewer loses the stretch they watched. One helper, two callers,