diff --git a/src/components/player/AndroidVideoPlayer.tsx b/src/components/player/AndroidVideoPlayer.tsx index f6336425..331937f3 100644 --- a/src/components/player/AndroidVideoPlayer.tsx +++ b/src/components/player/AndroidVideoPlayer.tsx @@ -223,7 +223,7 @@ const AndroidVideoPlayer: React.FC = () => { const nextEpisodeHook = useNextEpisode(type, season, episode, groupedEpisodes, (metadataResult as any)?.groupedEpisodes, episodeId); - const { outroSegment } = useSkipSegments({ + const { segments: skipIntervals, outroSegment } = useSkipSegments({ imdbId: imdbId || (id?.startsWith('tt') ? id : undefined), type, season, @@ -986,6 +986,7 @@ const AndroidVideoPlayer: React.FC = () => { episode={episode} malId={(metadata as any)?.mal_id || (metadata as any)?.external_ids?.mal_id} kitsuId={id?.startsWith('kitsu:') ? id.split(':')[1] : undefined} + skipIntervals={skipIntervals} currentTime={playerState.currentTime} onSkip={(endTime) => controlsHook.seekToTime(endTime)} controlsVisible={playerState.showControls} diff --git a/src/components/player/KSPlayerCore.tsx b/src/components/player/KSPlayerCore.tsx index dc794c94..231d71d8 100644 --- a/src/components/player/KSPlayerCore.tsx +++ b/src/components/player/KSPlayerCore.tsx @@ -210,7 +210,7 @@ const KSPlayerCore: React.FC = () => { episodeId }); - const { outroSegment } = useSkipSegments({ + const { segments: skipIntervals, outroSegment } = useSkipSegments({ imdbId: imdbId || (id?.startsWith('tt') ? id : undefined), type, season, @@ -956,6 +956,7 @@ const KSPlayerCore: React.FC = () => { episode={episode} malId={(metadata as any)?.mal_id || (metadata as any)?.external_ids?.mal_id} kitsuId={id?.startsWith('kitsu:') ? id.split(':')[1] : undefined} + skipIntervals={skipIntervals} currentTime={currentTime} onSkip={(endTime) => controls.seekToTime(endTime)} controlsVisible={showControls} @@ -1114,4 +1115,4 @@ const KSPlayerCore: React.FC = () => { ); }; -export default KSPlayerCore; \ No newline at end of file +export default KSPlayerCore; diff --git a/src/components/player/hooks/useSkipSegments.ts b/src/components/player/hooks/useSkipSegments.ts index 996fdf97..3a81e86d 100644 --- a/src/components/player/hooks/useSkipSegments.ts +++ b/src/components/player/hooks/useSkipSegments.ts @@ -28,10 +28,12 @@ export const useSkipSegments = ({ useEffect(() => { const key = `${imdbId}-${season}-${episode}-${malId}-${kitsuId}`; - + if (!enabled || type !== 'series' || (!imdbId && !malId && !kitsuId) || !season || !episode) { setSegments([]); + setIsLoading(false); fetchedRef.current = false; + lastKeyRef.current = ''; return; } @@ -39,23 +41,41 @@ export const useSkipSegments = ({ return; } + // Clear stale intervals while resolving a new episode/key. + if (lastKeyRef.current !== key) { + setSegments([]); + fetchedRef.current = false; + } + lastKeyRef.current = key; - fetchedRef.current = true; setIsLoading(true); + let cancelled = false; const fetchSegments = async () => { try { const intervals = await introService.getSkipTimes(imdbId, season, episode, malId, kitsuId); + + // Ignore stale responses from old requests. + if (cancelled || lastKeyRef.current !== key) return; setSegments(intervals); + fetchedRef.current = true; } catch (error) { + if (cancelled || lastKeyRef.current !== key) return; logger.error('[useSkipSegments] Error fetching skip data:', error); setSegments([]); + // Keep this key retryable on transient failures. + fetchedRef.current = false; } finally { + if (cancelled || lastKeyRef.current !== key) return; setIsLoading(false); } }; fetchSegments(); + + return () => { + cancelled = true; + }; }, [imdbId, type, season, episode, malId, kitsuId, enabled]); const getActiveSegment = (currentTime: number) => { @@ -64,7 +84,12 @@ export const useSkipSegments = ({ ); }; - const outroSegment = segments.find(s => ['ed', 'outro', 'mixed-ed'].includes(s.type)); + const outroSegment = segments + .filter(s => ['ed', 'outro', 'mixed-ed'].includes(s.type)) + .reduce((latest, interval) => { + if (!latest || interval.endTime > latest.endTime) return interval; + return latest; + }, null); return { segments, diff --git a/src/components/player/overlays/SkipIntroButton.tsx b/src/components/player/overlays/SkipIntroButton.tsx index 476b4d08..67f72173 100644 --- a/src/components/player/overlays/SkipIntroButton.tsx +++ b/src/components/player/overlays/SkipIntroButton.tsx @@ -23,6 +23,7 @@ interface SkipIntroButtonProps { episode?: number; malId?: string; kitsuId?: string; + skipIntervals?: SkipInterval[] | null; currentTime: number; onSkip: (endTime: number) => void; controlsVisible?: boolean; @@ -36,6 +37,7 @@ export const SkipIntroButton: React.FC = ({ episode, malId, kitsuId, + skipIntervals: externalSkipIntervals, currentTime, onSkip, controlsVisible = false, @@ -47,15 +49,17 @@ export const SkipIntroButton: React.FC = ({ const skipIntroEnabled = settings.skipIntroEnabled; - const { segments: skipIntervals } = useSkipSegments({ + const { segments: fetchedSkipIntervals } = useSkipSegments({ imdbId, type, season, episode, malId, kitsuId, - enabled: skipIntroEnabled + // Allow parent components to provide pre-fetched intervals to avoid duplicate requests. + enabled: skipIntroEnabled && !externalSkipIntervals }); + const skipIntervals = externalSkipIntervals ?? fetchedSkipIntervals; // State const [currentInterval, setCurrentInterval] = useState(null); @@ -293,4 +297,4 @@ const styles = StyleSheet.create({ }, }); -export default SkipIntroButton; \ No newline at end of file +export default SkipIntroButton;