From d510c8a534ffd24399325f15dbb86e628415f120 Mon Sep 17 00:00:00 2001 From: npeter83 Date: Sun, 27 Sep 2026 03:30:04 +0200 Subject: [PATCH] fix(plex): the show and season restart names its object and starts at 0 (E28 S3f2) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The header button said only "Play from start" and resumed the first episode at its saved point. It now reads "Season from the start · S6 · E1" or "Show from the start · S1 · E1" and asks the player for 0. --- frontend/src/components/PlexBrowse.tsx | 4 +- .../components/plexViews.restart.dom.test.tsx | 131 ++++++++++++++++++ frontend/src/components/plexViews.tsx | 12 +- frontend/src/i18n/locales/en/plex.json | 3 +- frontend/src/i18n/locales/hu/plex.json | 3 +- 5 files changed, 145 insertions(+), 8 deletions(-) create mode 100644 frontend/src/components/plexViews.restart.dom.test.tsx diff --git a/frontend/src/components/PlexBrowse.tsx b/frontend/src/components/PlexBrowse.tsx index 1482622b..c2c2cc53 100644 --- a/frontend/src/components/PlexBrowse.tsx +++ b/frontend/src/components/PlexBrowse.tsx @@ -484,7 +484,9 @@ export default function PlexBrowse() { sub.open({ kind: "player", id: epRk, queue })} + onPlay={(epRk, queue, fromStart) => + sub.open({ kind: "player", id: epRk, queue, fromStart }) + } onOpenSeason={(seasonId) => sub.open({ kind: "season", showId, seasonId })} onOpenShow={(id) => sub.open({ kind: "show", id })} onFilter={applyFilter} diff --git a/frontend/src/components/plexViews.restart.dom.test.tsx b/frontend/src/components/plexViews.restart.dom.test.tsx new file mode 100644 index 00000000..cb5d3182 --- /dev/null +++ b/frontend/src/components/plexViews.restart.dom.test.tsx @@ -0,0 +1,131 @@ +// @vitest-environment jsdom +// +// The show's and the season's restart button. It said only "Play from start" — of what? (the owner, +// 2026-09-27) — and it resumed the first episode at its saved point instead of at 0. Now it names its +// object and the episode ("Season from the start · S6 · E1"), and it asks the player for 0. +import { describe, it, expect, afterEach, beforeAll, vi } from "vitest"; +import { render, screen, fireEvent, cleanup } from "@testing-library/react"; +import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; + +beforeAll(() => { + class Stub { + observe() {} + unobserve() {} + disconnect() {} + takeRecords() { + return []; + } + } + globalThis.ResizeObserver ??= Stub as unknown as typeof ResizeObserver; +}); + +vi.mock("react-i18next", async (importOriginal) => { + const actual = await importOriginal(); + return { ...actual, useTranslation: () => ({ t: (key: string) => key }) }; +}); + +const ep = (id: string, season: number, episode: number, position = 0) => ({ + id, + type: "episode", + title: `Ep ${id}`, + season_number: season, + episode_number: episode, + duration_seconds: 1500, + thumb: `/api/plex/image/${id}`, + playable: "direct", + status: position > 0 ? "in_progress" : "new", + position_seconds: position, +}); + +// The first episode is half-watched: a restart that resumed it would start at 600, not 0. +const s1 = [ep("e11", 1, 1, 600), ep("e12", 1, 2)]; +const s2 = [ep("e21", 2, 1, 300), ep("e22", 2, 2)]; +const detail = { + show: { + id: "show", + title: "Show", + thumb: "", + art: "", + genres: [], + cast: [], + status: "in_progress", + resume: s2[1], + first: s1[0], + episode_count: 4, + collection_keys: [], + }, + seasons: [ + { + id: "s1", + season_number: 1, + title: "Season 1", + thumb: "", + episode_count: 2, + status: "in_progress", + resume: s1[1], + first: s1[0], + episodes: s1, + }, + { + id: "s2", + season_number: 2, + title: "Season 2", + thumb: "", + episode_count: 2, + status: "in_progress", + resume: s2[1], + first: s2[0], + episodes: s2, + }, + ], + related: [], +}; + +vi.mock("../lib/api", () => ({ + api: { + plexShow: () => Promise.resolve(detail), + plexSetState: () => Promise.resolve(), + plexSeasonState: () => Promise.resolve(), + }, + EMPTY_PLEX_FILTERS: {}, + plexFilterCount: () => 0, +})); + +import { PlexSeasonView, PlexShowView } from "./plexViews"; + +afterEach(cleanup); + +function wrap(ui: React.ReactElement) { + const qc = new QueryClient({ defaultOptions: { queries: { retry: false } } }); + return render({ui}); +} + +const ALL = ["e11", "e12", "e21", "e22"]; + +describe("the restart button on a show and a season", () => { + it("a season's names the season and its first episode, and starts that episode at 0", async () => { + const onPlay = vi.fn(); + wrap( {}} onPlay={onPlay} />); + const btn = await screen.findByRole("button", { name: /plex\.series\.seasonFromStart/ }); + expect(btn.textContent).toContain("S2 · E1"); + fireEvent.click(btn); + expect(onPlay).toHaveBeenCalledWith("e21", ALL, true); + }); + + it("a show's names the show and its first episode, and starts that episode at 0", async () => { + const onPlay = vi.fn(); + wrap( + {}} + onPlay={onPlay} + onOpenSeason={() => {}} + onOpenShow={() => {}} + />, + ); + const btn = await screen.findByRole("button", { name: /plex\.series\.showFromStart/ }); + expect(btn.textContent).toContain("S1 · E1"); + fireEvent.click(btn); + expect(onPlay).toHaveBeenCalledWith("e11", ALL, true); + }); +}); diff --git a/frontend/src/components/plexViews.tsx b/frontend/src/components/plexViews.tsx index 81f0b53b..9f6d910f 100644 --- a/frontend/src/components/plexViews.tsx +++ b/frontend/src/components/plexViews.tsx @@ -328,7 +328,7 @@ export function PlexShowView({ }: { showId: string; onBack: () => void; - onPlay: (epRk: string, queue: string[]) => void; + onPlay: (epRk: string, queue: string[], fromStart?: boolean) => void; onOpenSeason: (seasonId: string) => void; onOpenShow: (id: string) => void; onFilter?: (patch: Partial) => void; @@ -559,9 +559,10 @@ export function PlexShowView({ )} {show.first && show.status !== "new" && ( onPlay(show.first!.id, allKeys)} + onClick={() => onPlay(show.first!.id, allKeys, true)} icon={RotateCcw} - label={t("plex.series.playFromStart")} + label={t("plex.series.showFromStart")} + sub={epLabel(show.first)} /> )} onPlay(se.first!.id, allKeys)} + onClick={() => onPlay(se.first!.id, allKeys, true)} icon={RotateCcw} - label={t("plex.series.playFromStart")} + label={t("plex.series.seasonFromStart")} + sub={epLabel(se.first)} /> )}