fix(player): only the credits that end an item move on to the next one
An item can carry several credits blocks with mid- and post-credit scenes between them, and Plex flags the one that really ends it. The player left at the first block. Now the Plex-final block moves on (by the auto-skip or the Skip button), an earlier block is only skipped to its end by the button, and with no final flag the last block ends the item by the button alone.
This commit is contained in:
@@ -358,7 +358,7 @@ describe("keyboard focus in the bands", () => {
|
||||
it("a Space or Enter on Skip intro, outside the faded bands, stays Skip intro's", async () => {
|
||||
const intro = {
|
||||
...item("A"),
|
||||
markers: [{ type: "intro", start_s: 50, end_s: 200 }],
|
||||
markers: [{ type: "intro", start_s: 50, end_s: 200, final: false }],
|
||||
} as unknown as PlexItemDetail;
|
||||
mount({ details: { A: intro } });
|
||||
await playingAt("/media/A", 100);
|
||||
|
||||
@@ -334,7 +334,7 @@ describe("leaving an item that was played through", () => {
|
||||
prefs({ autoSkipCredits: true, autoSkipDelay: 0 });
|
||||
const withCredits = {
|
||||
...item("A"),
|
||||
markers: [{ type: "credits", start_s: 2900, end_s: 2995 }],
|
||||
markers: [{ type: "credits", start_s: 2900, end_s: 2995, final: true }],
|
||||
} as unknown as PlexItemDetail;
|
||||
const { qc, progress } = mount({
|
||||
details: { A: withCredits, B: item("B") },
|
||||
@@ -446,7 +446,7 @@ describe("leaving an item that was played through", () => {
|
||||
prefs({ upNext: false, autoSkipCredits: true, autoSkipDelay: 0 });
|
||||
const withCredits = {
|
||||
...item("A"),
|
||||
markers: [{ type: "credits", start_s: 2000, end_s: 2990 }],
|
||||
markers: [{ type: "credits", start_s: 2000, end_s: 2990, final: true }],
|
||||
} as unknown as PlexItemDetail;
|
||||
const { qc, progress } = mount({
|
||||
details: { A: withCredits, B: item("B") },
|
||||
@@ -463,6 +463,56 @@ describe("leaving an item that was played through", () => {
|
||||
expect(qc.getQueryData<PlexItemDetail>(qk.plexItem("A", "en"))?.status).not.toBe("watched");
|
||||
});
|
||||
|
||||
it("lets a credits block that does not end the item play on, its Skip only jumping past it", async () => {
|
||||
// An item can carry several credits blocks with scenes between them: the first one moving on
|
||||
// to the next item skipped a post-credit scene.
|
||||
prefs({ autoSkipCredits: true, autoSkipDelay: 0 });
|
||||
const twoCredits = {
|
||||
...item("A"),
|
||||
markers: [
|
||||
{ type: "credits", start_s: 2000, end_s: 2100, final: false },
|
||||
{ type: "credits", start_s: 2900, end_s: 2995, final: true },
|
||||
],
|
||||
} as unknown as PlexItemDetail;
|
||||
const { qc } = mount({
|
||||
details: { A: twoCredits, B: item("B") },
|
||||
sessions: { A: () => Promise.resolve(direct("A")), B: () => new Promise(() => {}) },
|
||||
queue: ["A", "B"],
|
||||
});
|
||||
const setState = vi.spyOn(api, "plexSetState").mockResolvedValue(undefined as never);
|
||||
await playingAt("/media/A", 2010);
|
||||
await tick();
|
||||
expect(screen.queryByText("Title B")).toBeNull();
|
||||
expect(qc.getQueryData<PlexItemDetail>(qk.plexItem("B", "en"))).toBeUndefined();
|
||||
fireEvent.click(screen.getByRole("button", { name: /plex\.player\.skipCredits/ }));
|
||||
await waitFor(() => expect(startsAsked("A").slice(-1)).toEqual([2100])); // past the block, still A
|
||||
expect(screen.queryByText("Title B")).toBeNull();
|
||||
expect(setState).not.toHaveBeenCalled();
|
||||
await playingAt("/media/A", 2910); // the final block: the auto-skip moves on
|
||||
await screen.findByText("Title B");
|
||||
expect(setState).toHaveBeenCalledWith("A", "watched");
|
||||
});
|
||||
|
||||
it("does not leave on its own at credits Plex did not flag final, but its Skip does", async () => {
|
||||
prefs({ autoSkipCredits: true, autoSkipDelay: 0 });
|
||||
const unflagged = {
|
||||
...item("A"),
|
||||
markers: [{ type: "credits", start_s: 2900, end_s: 2995, final: false }],
|
||||
} as unknown as PlexItemDetail;
|
||||
mount({
|
||||
details: { A: unflagged, B: item("B") },
|
||||
sessions: { A: () => Promise.resolve(direct("A")), B: () => new Promise(() => {}) },
|
||||
queue: ["A", "B"],
|
||||
});
|
||||
const setState = vi.spyOn(api, "plexSetState").mockResolvedValue(undefined as never);
|
||||
await playingAt("/media/A", 2910);
|
||||
await tick();
|
||||
expect(screen.queryByText("Title B")).toBeNull();
|
||||
fireEvent.click(screen.getByRole("button", { name: /plex\.player\.skipCredits/ }));
|
||||
await screen.findByText("Title B");
|
||||
expect(setState).toHaveBeenCalledWith("A", "watched");
|
||||
});
|
||||
|
||||
it("records a second viewing when the viewer plays on from the finished frame", async () => {
|
||||
// `finished` is a latch that makes every checkpoint a no-op; the only thing that lowers it is
|
||||
// the video's own 'play' event. Nothing tested that, so dropping the listener would have
|
||||
|
||||
Reference in New Issue
Block a user