add auto segment skipping

This commit is contained in:
Pas 2026-02-13 14:29:27 -07:00
parent 14d45b4a63
commit 248da37056
8 changed files with 157 additions and 2 deletions

View file

@ -1330,6 +1330,9 @@
"skipCredits": "Skip End Credits",
"skipCreditsDescription": "When enabled, automatically play the next episode at 99% completion to skip end credits. When disabled, wait until the episode is fully completed.",
"skipCreditsLabel": "Skip end credits",
"autoSkipSegments": "Auto Skip Segments",
"autoSkipSegmentsDescription": "When enabled, automatically skip intro, recap, and preview segments while watching.",
"autoSkipSegmentsLabel": "Auto skip segments",
"lowPerformanceMode": "Low performance/bandwidth mode",
"lowPerformanceModeDescription": "Optimizes the application for slower connections and devices by disabling bandwidth-heavy features. This mode reduces data usage and improves performance while keeping the core search and watch functionality intact. ",
"lowPerformanceModeLabel": "Low performance mode",

View file

@ -15,6 +15,7 @@ export interface SettingsInput {
enableThumbnails?: boolean;
enableAutoplay?: boolean;
enableSkipCredits?: boolean;
enableAutoSkipSegments?: boolean;
enableDiscover?: boolean;
enableFeatured?: boolean;
enableDetailsModal?: boolean;
@ -50,6 +51,7 @@ export interface SettingsResponse {
enableThumbnails?: boolean;
enableAutoplay?: boolean;
enableSkipCredits?: boolean;
enableAutoSkipSegments?: boolean;
enableDiscover?: boolean;
enableFeatured?: boolean;
enableDetailsModal?: boolean;

View file

@ -1,6 +1,7 @@
import { ReactNode, RefObject, useEffect, useRef } from "react";
import { OverlayDisplay } from "@/components/overlays/OverlayDisplay";
import { AutoSkipSegments } from "@/components/player/internals/AutoSkipSegments";
import { SkipTracker } from "@/components/player/internals/Backend/SkipTracker";
import { CastingInternal } from "@/components/player/internals/CastingInternal";
import { HeadUpdater } from "@/components/player/internals/HeadUpdater";
@ -100,6 +101,7 @@ export function Container(props: PlayerProps) {
<WatchPartyReporter />
<SkipTracker />
<WatchPartyResetter />
<AutoSkipSegments />
<div className="relative h-screen overflow-hidden">
<VideoClickTarget showingControls={props.showingControls} />
<HeadUpdater />

View file

@ -0,0 +1,87 @@
import { useEffect, useRef } from "react";
import { useSkipTime } from "@/components/player/hooks/useSkipTime";
import { usePlayerStore } from "@/stores/player/store";
import { usePreferencesStore } from "@/stores/preferences";
interface SegmentSkipState {
segmentId: string;
hasSkipped: boolean;
}
/**
* Component that automatically skips segments (intro, recap, preview, credits)
* when the enableAutoSkipSegments preference is enabled.
* For credits segments, only skips if end_ms is null (end of video).
*/
export function AutoSkipSegments() {
const enableAutoSkipSegments = usePreferencesStore(
(s) => s.enableAutoSkipSegments,
);
const skipCredits = usePreferencesStore((s) => s.enableSkipCredits);
const display = usePlayerStore((s) => s.display);
const time = usePlayerStore((s) => s.progress.time);
const meta = usePlayerStore((s) => s.meta);
const segments = useSkipTime();
// Track which segments we've already skipped to avoid re-skipping
const skippedSegmentsRef = useRef<Map<string, SegmentSkipState>>(new Map());
// Reset skip state when media changes
useEffect(() => {
skippedSegmentsRef.current.clear();
}, [meta?.tmdbId, meta?.season?.number, meta?.episode?.number]);
useEffect(() => {
if (!enableAutoSkipSegments || !display) return;
const currentSeconds = time;
for (const segment of segments) {
// For credits, only skip if skipCredits is enabled and end_ms is null (end of video)
const isCreditsSegment = segment.type === "credits";
if (isCreditsSegment) {
if (!skipCredits) continue;
// Check if credits go to end of video (end_ms is null)
if (segment.end_ms !== null) continue;
} else if (segment.end_ms === null) {
// For intro, recap, preview - skip if enabled and end time is defined
continue;
}
const startSeconds = (segment.start_ms ?? 0) / 1000;
const endSeconds = segment.end_ms ? segment.end_ms / 1000 : Infinity;
const segmentId = `${segment.type}-${startSeconds}-${endSeconds}`;
// Check if we're inside the segment
if (currentSeconds >= startSeconds && currentSeconds < endSeconds) {
const skipState = skippedSegmentsRef.current.get(segmentId);
// Only skip if we haven't skipped this segment yet
if (!skipState || !skipState.hasSkipped) {
// Skip to the end of the segment
display.setTime(
endSeconds === Infinity ? currentSeconds + 10 : endSeconds,
);
// Mark this segment as skipped
skippedSegmentsRef.current.set(segmentId, {
segmentId,
hasSkipped: true,
});
}
}
}
}, [
enableAutoSkipSegments,
skipCredits,
display,
time,
segments,
meta?.tmdbId,
meta?.season?.number,
meta?.episode?.number,
]);
return null;
}

View file

@ -58,6 +58,8 @@ export function useSettingsState(
| undefined,
enableThumbnails: boolean,
enableAutoplay: boolean,
enableSkipCredits: boolean,
enableAutoSkipSegments: boolean,
enableDiscover: boolean,
enableFeatured: boolean,
enableDetailsModal: boolean,
@ -68,7 +70,6 @@ export function useSettingsState(
embedOrder: string[],
enableEmbedOrder: boolean,
proxyTmdb: boolean,
enableSkipCredits: boolean,
enableImageLogos: boolean,
enableCarouselView: boolean,
enableMinimalCards: boolean,
@ -143,6 +144,12 @@ export function useSettingsState(
resetEnableSkipCredits,
enableSkipCreditsChanged,
] = useDerived(enableSkipCredits);
const [
enableAutoSkipSegmentsState,
setEnableAutoSkipSegmentsState,
resetEnableAutoSkipSegments,
enableAutoSkipSegmentsChanged,
] = useDerived(enableAutoSkipSegments);
const [
enableDiscoverState,
setEnableDiscoverState,
@ -282,6 +289,7 @@ export function useSettingsState(
resetEnableThumbnails();
resetEnableAutoplay();
resetEnableSkipCredits();
resetEnableAutoSkipSegments();
resetEnableDiscover();
resetEnableFeatured();
resetEnableDetailsModal();
@ -321,6 +329,7 @@ export function useSettingsState(
enableThumbnailsChanged ||
enableAutoplayChanged ||
enableSkipCreditsChanged ||
enableAutoSkipSegmentsChanged ||
enableDiscoverChanged ||
enableFeaturedChanged ||
enableDetailsModalChanged ||
@ -421,6 +430,11 @@ export function useSettingsState(
set: setEnableSkipCreditsState,
changed: enableSkipCreditsChanged,
},
enableAutoSkipSegments: {
state: enableAutoSkipSegmentsState,
set: setEnableAutoSkipSegmentsState,
changed: enableAutoSkipSegmentsChanged,
},
enableDiscover: {
state: enableDiscoverState,
set: setEnableDiscoverState,

View file

@ -399,6 +399,13 @@ export function SettingsPage() {
(s) => s.setEnableSkipCredits,
);
const enableAutoSkipSegments = usePreferencesStore(
(s) => s.enableAutoSkipSegments,
);
const setEnableAutoSkipSegments = usePreferencesStore(
(s) => s.setEnableAutoSkipSegments,
);
const sourceOrder = usePreferencesStore((s) => s.sourceOrder);
const setSourceOrder = usePreferencesStore((s) => s.setSourceOrder);
@ -558,6 +565,8 @@ export function SettingsPage() {
account ? account.profile : undefined,
enableThumbnails,
enableAutoplay,
enableSkipCredits,
enableAutoSkipSegments,
enableDiscover,
enableFeatured,
enableDetailsModal,
@ -568,7 +577,6 @@ export function SettingsPage() {
embedOrder,
enableEmbedOrder,
proxyTmdb,
enableSkipCredits,
enableImageLogos,
enableCarouselView,
enableMinimalCards,
@ -629,6 +637,7 @@ export function SettingsPage() {
state.enableThumbnails.changed ||
state.enableAutoplay.changed ||
state.enableSkipCredits.changed ||
state.enableAutoSkipSegments.changed ||
state.enableDiscover.changed ||
state.enableFeatured.changed ||
state.enableDetailsModal.changed ||
@ -658,6 +667,7 @@ export function SettingsPage() {
enableThumbnails: state.enableThumbnails.state,
enableAutoplay: state.enableAutoplay.state,
enableSkipCredits: state.enableSkipCredits.state,
enableAutoSkipSegments: state.enableAutoSkipSegments.state,
enableDiscover: state.enableDiscover.state,
enableFeatured: state.enableFeatured.state,
enableDetailsModal: state.enableDetailsModal.state,
@ -706,6 +716,7 @@ export function SettingsPage() {
setEnableThumbnails(state.enableThumbnails.state);
setEnableAutoplay(state.enableAutoplay.state);
setEnableSkipCredits(state.enableSkipCredits.state);
setEnableAutoSkipSegments(state.enableAutoSkipSegments.state);
setEnableDiscover(state.enableDiscover.state);
setEnableFeatured(state.enableFeatured.state);
setEnableDetailsModal(state.enableDetailsModal.state);
@ -769,6 +780,7 @@ export function SettingsPage() {
setTIDBKey,
setEnableAutoplay,
setEnableSkipCredits,
setEnableAutoSkipSegments,
setEnableDiscover,
setEnableFeatured,
setEnableDetailsModal,
@ -855,6 +867,8 @@ export function SettingsPage() {
setEnableAutoplay={state.enableAutoplay.set}
enableSkipCredits={state.enableSkipCredits.state}
setEnableSkipCredits={state.enableSkipCredits.set}
enableAutoSkipSegments={state.enableAutoSkipSegments.state}
setEnableAutoSkipSegments={state.enableAutoSkipSegments.set}
sourceOrder={availableSources}
setSourceOrder={state.sourceOrder.set}
enableSourceOrder={state.enableSourceOrder.state}

View file

@ -24,6 +24,8 @@ export function PreferencesPart(props: {
setEnableAutoplay: (v: boolean) => void;
enableSkipCredits: boolean;
setEnableSkipCredits: (v: boolean) => void;
enableAutoSkipSegments: boolean;
setEnableAutoSkipSegments: (v: boolean) => void;
sourceOrder: string[];
setSourceOrder: (v: string[]) => void;
enableSourceOrder: boolean;
@ -175,6 +177,29 @@ export function PreferencesPart(props: {
{t("settings.preferences.skipCreditsLabel")}
</p>
</div>
{/* Auto Skip Segments Preference */}
<div className="pt-4 mt-4">
<p className="text-white font-bold mb-3">
{t("settings.preferences.autoSkipSegments")}
</p>
<p className="max-w-[25rem] font-medium">
{t("settings.preferences.autoSkipSegmentsDescription")}
</p>
<div
onClick={() =>
props.setEnableAutoSkipSegments(
!props.enableAutoSkipSegments,
)
}
className="bg-dropdown-background hover:bg-dropdown-hoverBackground select-none my-4 cursor-pointer space-x-3 flex items-center max-w-[25rem] py-3 px-4 rounded-lg"
>
<Toggle enabled={props.enableAutoSkipSegments} />
<p className="flex-1 text-white font-bold">
{t("settings.preferences.autoSkipSegmentsLabel")}
</p>
</div>
</div>
</div>
)}
</div>

View file

@ -11,6 +11,7 @@ export interface PreferencesStore {
enableThumbnails: boolean;
enableAutoplay: boolean;
enableSkipCredits: boolean;
enableAutoSkipSegments: boolean;
enableDiscover: boolean;
enableFeatured: boolean;
enableDetailsModal: boolean;
@ -42,6 +43,7 @@ export interface PreferencesStore {
setEnableThumbnails(v: boolean): void;
setEnableAutoplay(v: boolean): void;
setEnableSkipCredits(v: boolean): void;
setEnableAutoSkipSegments(v: boolean): void;
setEnableDiscover(v: boolean): void;
setEnableFeatured(v: boolean): void;
setEnableDetailsModal(v: boolean): void;
@ -77,6 +79,7 @@ export const usePreferencesStore = create(
enableThumbnails: false,
enableAutoplay: true,
enableSkipCredits: true,
enableAutoSkipSegments: false,
enableDiscover: true,
enableFeatured: false,
enableDetailsModal: false,
@ -119,6 +122,11 @@ export const usePreferencesStore = create(
s.enableSkipCredits = v;
});
},
setEnableAutoSkipSegments(v) {
set((s) => {
s.enableAutoSkipSegments = v;
});
},
setEnableDiscover(v) {
set((s) => {
s.enableDiscover = v;