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:
2026-10-07 01:48:10 +02:00
parent f3c94641a7
commit 0cda5b6dd4
9 changed files with 189 additions and 10 deletions
+13 -6
View File
@@ -17,6 +17,7 @@ import { api, type PlexItemDetail, type PlexMarker } from "./api";
import { qk } from "./queryKeys";
import { useLatest } from "./useLatest";
import i18n, { plexLang } from "../i18n";
import { markerSkip } from "./plexMarkers";
import { type PlexPlayerPrefs } from "./plexPlayerPrefs";
import { PLEX_SCALE, normalizeVolumeIn, stepVolumeIn } from "./playerVolume";
@@ -411,9 +412,10 @@ export function usePlayerVolume(opts: {
}
// Auto-skip intro/credits. Entering a marker whose auto-skip is on runs a countdown (skipProgress
// 0→1 over autoSkipDelay s, shown on the Skip button); on completion it skips — intro → jump to the
// marker end, credits → next item (binge). The returned refs (skipProgressRef / cancelAutoSkipRef)
// are the forward-declaration bridge the earlier-defined keyboard/back handlers read to cancel a
// 0→1 over autoSkipDelay s, shown on the Skip button); on completion it skips — to the marker end, or
// for the credits that end the item to the next item (binge). Which credits those are, and which the
// auto-skip may act on alone, is `lib/plexMarkers`. The returned refs (skipProgressRef /
// cancelAutoSkipRef) are the forward-declaration bridge the earlier-defined keyboard/back handlers read to cancel a
// running countdown (Esc/Backspace during the countdown cancels it and must NOT navigate back).
export function useAutoSkip(opts: {
id: string;
@@ -453,6 +455,10 @@ export function useAutoSkip(opts: {
(m) => abs >= m.start_s && abs < m.end_s - 1,
);
const activeMarkerRef = useLatest(activeMarker);
// A boolean, not the list, goes into the countdown's deps: a refetch hands back an equal-but-new
// list and must not restart a countdown under way (see below); doSkip reads the list from a ref.
const activeAuto = activeMarker != null && markerSkip(activeMarker, markers ?? []).auto;
const markersRef = useLatest(markers);
const [skipProgress, setSkipProgress] = useState<number | null>(null);
const skipProgressRef = useLatest(skipProgress);
@@ -470,7 +476,7 @@ export function useAutoSkip(opts: {
const doSkip = useCallback(
(m: PlexMarker) => {
setSkipProgress(null);
if (m.type === "credits" && nextId) {
if (nextId && markerSkip(m, markersRef.current ?? []).leaves) {
// Binge auto-advance: the episode we're leaving was watched through the credits, so mark IT
// (the current id, captured here — not the reactive id, which go() is about to change). The
// write is handed to go() so a refusal takes the finish back: this path has no near-end
@@ -481,7 +487,7 @@ export function useAutoSkip(opts: {
go(nextId, watchedWrite);
} else seekTo(m.end_s);
},
[id, go, nextId, seekTo, reportWatchedErrorRef],
[id, go, nextId, seekTo, markersRef, reportWatchedErrorRef],
);
const cancelAutoSkip = useCallback(() => {
const m = activeMarkerRef.current;
@@ -503,7 +509,7 @@ export function useAutoSkip(opts: {
const m = activeMarkerRef.current;
if (!m) return setSkipProgress(null);
const key = `${m.type}-${m.start_s}`;
const enabled = m.type === "intro" ? autoSkipIntro : autoSkipCredits;
const enabled = (m.type === "intro" ? autoSkipIntro : autoSkipCredits) && activeAuto;
if (!enabled || cancelledMarkerRef.current === key) return setSkipProgress(null);
if (autoSkipDelay <= 0) return doSkip(m); // immediate
let elapsed = 0;
@@ -535,6 +541,7 @@ export function useAutoSkip(opts: {
doSkip,
videoRef,
activeMarkerRef,
activeAuto,
]);
return { activeMarker, skipProgress, skipProgressRef, cancelAutoSkipRef, doSkip };