Add live introdb key reload and recap playback markers

- reload introdb API key on setting updates
- support recap/preview markers in playback and next-episode flow
- add profile defaults for recap and preview auto-play settings
This commit is contained in:
Silo Server Migration
2026-05-24 00:14:17 -04:00
parent 04a7e8c82d
commit d41ffd4d5e
13 changed files with 191 additions and 35 deletions
+2 -2
View File
@@ -128,8 +128,8 @@ export interface Profile {
show_forced_subtitles?: boolean;
auto_skip_intro: boolean;
auto_skip_credits: boolean;
auto_skip_recap: boolean;
auto_play_next_preview: boolean;
auto_skip_recap?: boolean;
auto_play_next_preview?: boolean;
library_restrictions_enabled: boolean;
allowed_library_ids: number[] | null;
max_playback_quality: string;
@@ -1,18 +1,19 @@
interface IntroSkipButtonProps {
onSkip: () => void;
label?: string;
}
/**
* "Skip Intro" button visible during the intro time range.
*/
export function IntroSkipButton({ onSkip }: IntroSkipButtonProps) {
export function IntroSkipButton({ onSkip, label = "Skip Intro" }: IntroSkipButtonProps) {
return (
<button
onClick={onSkip}
type="button"
className="absolute right-6 bottom-24 z-50 rounded border border-white/40 bg-black/70 px-6 py-2 text-sm font-medium text-white transition-colors hover:bg-white/20"
>
Skip Intro
{label}
</button>
);
}
+51 -1
View File
@@ -72,6 +72,10 @@ interface VideoPlayerProps {
intro: PlayerTimeRange | null;
autoSkipIntro?: boolean;
credits: PlayerTimeRange | null;
recap?: PlayerTimeRange | null;
autoSkipRecap?: boolean;
preview?: PlayerTimeRange | null;
autoPlayNextPreview?: boolean;
duration?: number;
seriesContext?: SeriesContext;
onNavigateEpisode?: (contentId: string) => void;
@@ -154,6 +158,10 @@ export function VideoPlayer({
intro,
autoSkipIntro = false,
credits,
recap = null,
autoSkipRecap = false,
preview = null,
autoPlayNextPreview = false,
duration: propDuration,
seriesContext,
onNavigateEpisode,
@@ -190,6 +198,7 @@ export function VideoPlayer({
const backendDurationRef = useRef(propDuration ?? 0);
const autoEnterPictureInPictureAttemptedRef = useRef(false);
const autoSkippedIntroKeyRef = useRef<string | null>(null);
const autoSkippedRecapKeyRef = useRef<string | null>(null);
const endedFiredRef = useRef(false);
const [hasEnded, setHasEnded] = useState(false);
const onEndedRef = useRef(onEnded);
@@ -822,7 +831,7 @@ export function VideoPlayer({
);
const nextEpisode = useNextEpisode(
roomPlaybackActive ? null : credits,
roomPlaybackActive ? null : autoPlayNextPreview && preview ? preview : credits,
roomPlaybackActive ? undefined : seriesContext,
currentTime,
handleNavigate,
@@ -886,11 +895,16 @@ export function VideoPlayer({
// -- Intro skip --
const showIntroSkip = intro != null && currentTime >= intro.start && currentTime < intro.end;
const showRecapSkip = recap != null && currentTime >= recap.start && currentTime < recap.end;
const skipIntro = useCallback(() => {
if (intro) handlePlayerSeek(intro.end);
}, [intro, handlePlayerSeek]);
const skipRecap = useCallback(() => {
if (recap) handlePlayerSeek(recap.end);
}, [recap, handlePlayerSeek]);
useEffect(() => {
if (!autoSkipIntro || !intro || !isPlayerReady || awaitingFirstFrame) {
return;
@@ -926,6 +940,41 @@ export function VideoPlayer({
watchTogetherSync.attachedSessionId,
]);
useEffect(() => {
if (!autoSkipRecap || !recap || !isPlayerReady || awaitingFirstFrame) {
return;
}
if (currentTime < recap.start || currentTime >= recap.end) {
return;
}
if (
roomPlaybackActive &&
(!watchTogether.room?.self_can_manage_room ||
watchTogetherSync.attachedSessionId !== sessionId)
) {
return;
}
const recapKey = `${sessionId}:${activeFileId ?? "unknown"}:${recap.start}:${recap.end}`;
if (autoSkippedRecapKeyRef.current === recapKey) {
return;
}
autoSkippedRecapKeyRef.current = recapKey;
handlePlayerSeek(recap.end);
}, [
activeFileId,
autoSkipRecap,
awaitingFirstFrame,
currentTime,
handlePlayerSeek,
isPlayerReady,
recap,
roomPlaybackActive,
sessionId,
watchTogether.room?.self_can_manage_room,
watchTogetherSync.attachedSessionId,
]);
// Stabilize the dependency – only the bitrate matters for buffer sizing.
const selectedVersionBitrate = transcodeQuality.effectiveVersion?.bitrate ?? 0;
@@ -1972,6 +2021,7 @@ export function VideoPlayer({
{/* Intro skip button */}
{!isDetached && showIntroSkip && <IntroSkipButton onSkip={skipIntro} />}
{!isDetached && showRecapSkip && <IntroSkipButton onSkip={skipRecap} label="Skip Recap" />}
{/* Next episode overlay */}
{!isDetached && nextEpisode.showCountdown && nextEpisode.nextEpisode && (
+6
View File
@@ -77,6 +77,8 @@ export function WatchPage({
showForcedSubtitles,
profileLanguage,
autoSkipIntro,
autoSkipRecap,
autoPlayNextPreview,
seriesContext,
onNavigateEpisode,
onEnded,
@@ -411,6 +413,10 @@ export function WatchPage({
intro={activeMarkers.intro}
autoSkipIntro={autoSkipIntro}
credits={activeMarkers.credits}
recap={activeMarkers.recap}
autoSkipRecap={autoSkipRecap}
preview={activeMarkers.preview}
autoPlayNextPreview={autoPlayNextPreview}
duration={selectedDuration}
qualityPreference={qualityPreference}
seriesContext={seriesContext}
+6 -6
View File
@@ -10,11 +10,11 @@ interface NextEpisodeState {
}
/**
* Detects when the current playback position enters the credits region,
* Detects when the current playback position enters the configured trigger region,
* starts a 10-second countdown, and provides the next episode reference.
*/
export function useNextEpisode(
credits: PlayerTimeRange | null,
triggerRegion: PlayerTimeRange | null,
seriesContext: SeriesContext | undefined,
currentTime: number,
onNavigate: (contentId: string) => void,
@@ -41,11 +41,11 @@ export function useNextEpisode(
}
}, [currentEpisodeKey]);
// Detect entry into credits region.
// Detect entry into the configured trigger region.
useEffect(() => {
if (!credits || !nextEpisode || cancelledRef.current) return;
if (!triggerRegion || !nextEpisode || cancelledRef.current) return;
if (currentTime >= credits.start && !showCountdown) {
if (currentTime >= triggerRegion.start && !showCountdown) {
setShowCountdown(true);
setSecondsRemaining(10);
@@ -60,7 +60,7 @@ export function useNextEpisode(
});
}, 1000);
}
}, [currentTime, credits, nextEpisode, showCountdown, onNavigate]);
}, [currentTime, triggerRegion, nextEpisode, showCountdown, onNavigate]);
// Clean up interval on unmount.
useEffect(() => {