feat: add comprehensive segment skip with all 5 types and settings submenu

- Add SegmentSkipMode type ('none', 'ask', 'auto') in settings.ts
- Create 5 segment skip settings: skipIntro, skipOutro, skipRecap, skipCommercial, skipPreview
- Update segments.ts to fetch all 5 segment types from Jellyfin MediaSegments API (10.11+)
- Create unified useSegmentSkipper hook supporting all segment types with 3 modes
- Update video player Controls.tsx with priority system (Commercial > Recap > Intro > Preview > Outro)
- Add dynamic skip button text in BottomControls.tsx
- Create dedicated settings submenu at settings/segment-skip/page.tsx
- Simplify PlaybackControlsSettings.tsx with navigation to submenu
- Extend DownloadedItem interface with all segment types for offline support
- Add 13+ translation keys for segment skip UI
This commit is contained in:
Uruk
2026-01-14 13:44:45 +01:00
parent 81f79a54af
commit 62f50590d4
9 changed files with 552 additions and 24 deletions

View File

@@ -134,6 +134,9 @@ export enum VideoPlayer {
MPV = 0,
}
// Segment skip behavior options
export type SegmentSkipMode = "none" | "ask" | "auto";
// Audio transcoding mode - controls how surround audio is handled
// This controls server-side transcoding behavior for audio streams.
// MPV decodes via FFmpeg and supports most formats, but mobile devices
@@ -181,6 +184,12 @@ export type Settings = {
maxAutoPlayEpisodeCount: MaxAutoPlayEpisodeCount;
autoPlayEpisodeCount: number;
autoPlayNextEpisode: boolean;
// Media segment skip preferences
skipIntro: SegmentSkipMode;
skipOutro: SegmentSkipMode;
skipRecap: SegmentSkipMode;
skipCommercial: SegmentSkipMode;
skipPreview: SegmentSkipMode;
// Playback speed settings
defaultPlaybackSpeed: number;
playbackSpeedPerMedia: Record<string, number>;
@@ -266,6 +275,12 @@ export const defaultValues: Settings = {
maxAutoPlayEpisodeCount: { key: "3", value: 3 },
autoPlayEpisodeCount: 0,
autoPlayNextEpisode: true,
// Media segment skip defaults
skipIntro: "ask",
skipOutro: "ask",
skipRecap: "ask",
skipCommercial: "ask",
skipPreview: "ask",
// Playback speed defaults
defaultPlaybackSpeed: 1.0,
playbackSpeedPerMedia: {},

View File

@@ -74,10 +74,16 @@ export const getSegmentsForItem = (
): {
introSegments: MediaTimeSegment[];
creditSegments: MediaTimeSegment[];
recapSegments: MediaTimeSegment[];
commercialSegments: MediaTimeSegment[];
previewSegments: MediaTimeSegment[];
} => {
return {
introSegments: item.introSegments || [],
creditSegments: item.creditSegments || [],
recapSegments: item.recapSegments || [],
commercialSegments: item.commercialSegments || [],
previewSegments: item.previewSegments || [],
};
};
@@ -95,6 +101,9 @@ const fetchMediaSegments = async (
): Promise<{
introSegments: MediaTimeSegment[];
creditSegments: MediaTimeSegment[];
recapSegments: MediaTimeSegment[];
commercialSegments: MediaTimeSegment[];
previewSegments: MediaTimeSegment[];
} | null> => {
try {
const response = await api.axiosInstance.get<MediaSegmentsResponse>(
@@ -102,13 +111,22 @@ const fetchMediaSegments = async (
{
headers: getAuthHeaders(api),
params: {
includeSegmentTypes: ["Intro", "Outro"],
includeSegmentTypes: [
"Intro",
"Outro",
"Recap",
"Commercial",
"Preview",
],
},
},
);
const introSegments: MediaTimeSegment[] = [];
const creditSegments: MediaTimeSegment[] = [];
const recapSegments: MediaTimeSegment[] = [];
const commercialSegments: MediaTimeSegment[] = [];
const previewSegments: MediaTimeSegment[] = [];
response.data.Items.forEach((segment) => {
const timeSegment: MediaTimeSegment = {
@@ -124,13 +142,27 @@ const fetchMediaSegments = async (
case "Outro":
creditSegments.push(timeSegment);
break;
// Optionally handle other types like Recap, Commercial, Preview
case "Recap":
recapSegments.push(timeSegment);
break;
case "Commercial":
commercialSegments.push(timeSegment);
break;
case "Preview":
previewSegments.push(timeSegment);
break;
default:
break;
}
});
return { introSegments, creditSegments };
return {
introSegments,
creditSegments,
recapSegments,
commercialSegments,
previewSegments,
};
} catch (_error) {
// Return null to indicate we should try legacy endpoints
return null;
@@ -146,6 +178,9 @@ const fetchLegacySegments = async (
): Promise<{
introSegments: MediaTimeSegment[];
creditSegments: MediaTimeSegment[];
recapSegments: MediaTimeSegment[];
commercialSegments: MediaTimeSegment[];
previewSegments: MediaTimeSegment[];
}> => {
const introSegments: MediaTimeSegment[] = [];
const creditSegments: MediaTimeSegment[] = [];
@@ -184,7 +219,13 @@ const fetchLegacySegments = async (
console.error("Failed to fetch legacy segments", error);
}
return { introSegments, creditSegments };
return {
introSegments,
creditSegments,
recapSegments: [],
commercialSegments: [],
previewSegments: [],
};
};
export const fetchAndParseSegments = async (
@@ -193,6 +234,9 @@ export const fetchAndParseSegments = async (
): Promise<{
introSegments: MediaTimeSegment[];
creditSegments: MediaTimeSegment[];
recapSegments: MediaTimeSegment[];
commercialSegments: MediaTimeSegment[];
previewSegments: MediaTimeSegment[];
}> => {
// Try new API first (Jellyfin 10.11+)
const newSegments = await fetchMediaSegments(itemId, api);