mirror of
https://github.com/p-stream/p-stream.git
synced 2026-03-11 17:55:33 +00:00
add auto segment skipping
This commit is contained in:
parent
14d45b4a63
commit
248da37056
8 changed files with 157 additions and 2 deletions
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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 />
|
||||
|
|
|
|||
87
src/components/player/internals/AutoSkipSegments.tsx
Normal file
87
src/components/player/internals/AutoSkipSegments.tsx
Normal 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;
|
||||
}
|
||||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
Loading…
Reference in a new issue