From 6405fd2c71aa2da963842524e4d3a5af6b328376 Mon Sep 17 00:00:00 2001 From: tapframe Date: Tue, 29 Jul 2025 14:28:52 +0530 Subject: [PATCH] Improved trakt. --- .gitignore | 3 +- src/components/player/AndroidVideoPlayer.tsx | 127 +++++++-------- src/components/player/VideoPlayer.tsx | 163 +++++++++---------- src/hooks/useTraktAutosync.ts | 18 +- src/screens/StreamsScreen.tsx | 83 ++++------ src/services/traktService.ts | 16 +- 6 files changed, 185 insertions(+), 225 deletions(-) diff --git a/.gitignore b/.gitignore index be2b4be..cd33c43 100644 --- a/.gitignore +++ b/.gitignore @@ -42,4 +42,5 @@ CHANGELOG.md android/ HEATING_OPTIMIZATIONS.md ios -android \ No newline at end of file +android +sliderreadme.md diff --git a/src/components/player/AndroidVideoPlayer.tsx b/src/components/player/AndroidVideoPlayer.tsx index 2ceca4a..19e5d7d 100644 --- a/src/components/player/AndroidVideoPlayer.tsx +++ b/src/components/player/AndroidVideoPlayer.tsx @@ -264,16 +264,16 @@ const AndroidVideoPlayer: React.FC = () => { }, []); const startOpeningAnimation = () => { - // Logo entrance animation + // Logo entrance animation - optimized for faster appearance Animated.parallel([ Animated.timing(logoOpacityAnim, { toValue: 1, - duration: 600, + duration: 300, // Reduced from 600ms to 300ms useNativeDriver: true, }), Animated.spring(logoScaleAnim, { toValue: 1, - tension: 50, + tension: 80, // Increased tension for faster spring friction: 8, useNativeDriver: true, }), @@ -284,12 +284,12 @@ const AndroidVideoPlayer: React.FC = () => { return Animated.sequence([ Animated.timing(pulseAnim, { toValue: 1.05, - duration: 1000, + duration: 800, // Reduced from 1000ms to 800ms useNativeDriver: true, }), Animated.timing(pulseAnim, { toValue: 1, - duration: 1000, + duration: 800, // Reduced from 1000ms to 800ms useNativeDriver: true, }), ]); @@ -303,38 +303,34 @@ const AndroidVideoPlayer: React.FC = () => { }); }; - // Start pulsing after a short delay - setTimeout(() => { - if (!isOpeningAnimationComplete) { - loopPulse(); - } - }, 800); + // Start pulsing immediately without delay + // Removed the 800ms delay + loopPulse(); }; const completeOpeningAnimation = () => { Animated.parallel([ Animated.timing(openingFadeAnim, { toValue: 1, - duration: 600, + duration: 300, // Reduced from 600ms to 300ms useNativeDriver: true, }), Animated.timing(openingScaleAnim, { toValue: 1, - duration: 700, + duration: 350, // Reduced from 700ms to 350ms useNativeDriver: true, }), Animated.timing(backgroundFadeAnim, { toValue: 0, - duration: 800, + duration: 400, // Reduced from 800ms to 400ms useNativeDriver: true, }), ]).start(() => { openingScaleAnim.setValue(1); openingFadeAnim.setValue(1); setIsOpeningAnimationComplete(true); - setTimeout(() => { - backgroundFadeAnim.setValue(0); - }, 100); + // Removed the 100ms delay + backgroundFadeAnim.setValue(0); }); }; @@ -396,15 +392,14 @@ const AndroidVideoPlayer: React.FC = () => { clearInterval(progressSaveInterval); } - // Use the user's configured sync frequency with increased minimum to reduce heating - // Minimum interval increased from 5s to 30s to reduce CPU usage - const syncInterval = Math.max(30000, traktSettings.syncFrequency); + // IMMEDIATE SYNC: Reduce sync interval to 5 seconds for near real-time sync + const syncInterval = 5000; // 5 seconds for immediate sync const interval = setInterval(() => { saveWatchProgress(); }, syncInterval); - logger.log(`[AndroidVideoPlayer] Watch progress save interval set to ${syncInterval}ms`); + logger.log(`[AndroidVideoPlayer] Watch progress save interval set to ${syncInterval}ms (immediate sync mode)`); setProgressSaveInterval(interval); return () => { @@ -412,7 +407,7 @@ const AndroidVideoPlayer: React.FC = () => { setProgressSaveInterval(null); }; } - }, [id, type, paused, currentTime, duration, traktSettings.syncFrequency]); + }, [id, type, paused, currentTime, duration]); useEffect(() => { return () => { @@ -583,8 +578,12 @@ const AndroidVideoPlayer: React.FC = () => { traktAutosync.handlePlaybackStart(currentTime, videoDuration); } + // Complete opening animation immediately before seeking + completeOpeningAnimation(); + if (initialPosition && !isInitialSeekComplete) { logger.log(`[AndroidVideoPlayer] Seeking to initial position: ${initialPosition}s (duration: ${videoDuration}s)`); + // Reduced timeout from 1000ms to 500ms setTimeout(() => { if (videoRef.current && videoDuration > 0 && isMounted.current) { seekToTime(initialPosition); @@ -593,9 +592,9 @@ const AndroidVideoPlayer: React.FC = () => { } else { logger.error(`[AndroidVideoPlayer] Initial seek failed: videoRef=${!!videoRef.current}, duration=${videoDuration}, mounted=${isMounted.current}`); } - }, 1000); + }, 500); } - completeOpeningAnimation(); + controlsTimeout.current = setTimeout(hideControls, 5000); } catch (error) { logger.error('[AndroidVideoPlayer] Error in onLoad:', error); @@ -651,9 +650,13 @@ const AndroidVideoPlayer: React.FC = () => { }; const handleClose = async () => { - logger.log('[AndroidVideoPlayer] Close button pressed - syncing to Trakt before closing'); - - // Set syncing state to prevent multiple close attempts + // Prevent multiple close attempts + if (isSyncingBeforeClose) { + logger.log('[AndroidVideoPlayer] Close already in progress, ignoring duplicate call'); + return; + } + + logger.log('[AndroidVideoPlayer] Close button pressed - closing immediately and syncing to Trakt in background'); setIsSyncingBeforeClose(true); // Make sure we have the most accurate current time @@ -662,44 +665,34 @@ const AndroidVideoPlayer: React.FC = () => { logger.log(`[AndroidVideoPlayer] Current progress: ${actualCurrentTime}/${duration} (${progressPercent.toFixed(1)}%)`); - try { - // Force one last progress update (scrobble/pause) with the exact time - await traktAutosync.handleProgressUpdate(actualCurrentTime, duration, true); - - // Sync progress to Trakt before closing - await traktAutosync.handlePlaybackEnd(actualCurrentTime, duration, 'unmount'); - - // Start exit animation - Animated.parallel([ - Animated.timing(fadeAnim, { - toValue: 0, - duration: 150, - useNativeDriver: true, - }), - Animated.timing(openingFadeAnim, { - toValue: 0, - duration: 150, - useNativeDriver: true, - }), - ]).start(); - - // Longer delay to ensure Trakt sync completes - setTimeout(() => { - ScreenOrientation.unlockAsync().then(() => { - disableImmersiveMode(); - navigation.goBack(); - }).catch(() => { - // Fallback: navigate even if orientation unlock fails - disableImmersiveMode(); - navigation.goBack(); - }); - }, 500); // Increased from 100ms to 500ms - } catch (error) { - logger.error('[AndroidVideoPlayer] Error syncing to Trakt before closing:', error); - // Navigate anyway even if sync fails + // Navigate immediately without delay + ScreenOrientation.unlockAsync().then(() => { disableImmersiveMode(); navigation.goBack(); - } + }).catch(() => { + // Fallback: navigate even if orientation unlock fails + disableImmersiveMode(); + navigation.goBack(); + }); + + // Send Trakt sync in background (don't await) + const backgroundSync = async () => { + try { + logger.log('[AndroidVideoPlayer] Starting background Trakt sync'); + // Force one last progress update (scrobble/pause) with the exact time + await traktAutosync.handleProgressUpdate(actualCurrentTime, duration, true); + + // Sync progress to Trakt + await traktAutosync.handlePlaybackEnd(actualCurrentTime, duration, 'unmount'); + + logger.log('[AndroidVideoPlayer] Background Trakt sync completed successfully'); + } catch (error) { + logger.error('[AndroidVideoPlayer] Error in background Trakt sync:', error); + } + }; + + // Start background sync without blocking UI + backgroundSync(); }; const handleResume = async () => { @@ -752,10 +745,8 @@ const AndroidVideoPlayer: React.FC = () => { logger.log('[AndroidVideoPlayer] Video ended naturally, sending final progress update with 100%'); await traktAutosync.handleProgressUpdate(finalTime, duration, true); - // Small delay to ensure the progress update is processed - await new Promise(resolve => setTimeout(resolve, 300)); - - // Now send the stop call + // IMMEDIATE SYNC: Remove delay for instant sync + // Now send the stop call immediately logger.log('[AndroidVideoPlayer] Sending final stop call after natural end'); await traktAutosync.handlePlaybackEnd(finalTime, duration, 'ended'); @@ -1280,4 +1271,4 @@ const AndroidVideoPlayer: React.FC = () => { ); }; -export default AndroidVideoPlayer; \ No newline at end of file +export default AndroidVideoPlayer; \ No newline at end of file diff --git a/src/components/player/VideoPlayer.tsx b/src/components/player/VideoPlayer.tsx index 45b552b..0678dc5 100644 --- a/src/components/player/VideoPlayer.tsx +++ b/src/components/player/VideoPlayer.tsx @@ -286,16 +286,16 @@ const VideoPlayer: React.FC = () => { }, []); const startOpeningAnimation = () => { - // Logo entrance animation + // Logo entrance animation - optimized for faster appearance Animated.parallel([ Animated.timing(logoOpacityAnim, { toValue: 1, - duration: 600, + duration: 300, // Reduced from 600ms to 300ms useNativeDriver: true, }), Animated.spring(logoScaleAnim, { toValue: 1, - tension: 50, + tension: 80, // Increased tension for faster spring friction: 8, useNativeDriver: true, }), @@ -306,12 +306,12 @@ const VideoPlayer: React.FC = () => { return Animated.sequence([ Animated.timing(pulseAnim, { toValue: 1.05, - duration: 1000, + duration: 800, // Reduced from 1000ms to 800ms useNativeDriver: true, }), Animated.timing(pulseAnim, { toValue: 1, - duration: 1000, + duration: 800, // Reduced from 1000ms to 800ms useNativeDriver: true, }), ]); @@ -325,29 +325,26 @@ const VideoPlayer: React.FC = () => { }); }; - // Start pulsing after a short delay - setTimeout(() => { - if (!isOpeningAnimationComplete) { - loopPulse(); - } - }, 800); + // Start pulsing immediately without delay + // Removed the 800ms delay + loopPulse(); }; const completeOpeningAnimation = () => { Animated.parallel([ Animated.timing(openingFadeAnim, { toValue: 1, - duration: 600, + duration: 300, // Reduced from 600ms to 300ms useNativeDriver: true, }), Animated.timing(openingScaleAnim, { toValue: 1, - duration: 700, + duration: 350, // Reduced from 700ms to 350ms useNativeDriver: true, }), Animated.timing(backgroundFadeAnim, { toValue: 0, - duration: 800, + duration: 400, // Reduced from 800ms to 400ms useNativeDriver: true, }), ]).start(() => { @@ -418,15 +415,14 @@ const VideoPlayer: React.FC = () => { clearInterval(progressSaveInterval); } - // Use the user's configured sync frequency with increased minimum to reduce heating - // Minimum interval increased from 5s to 30s to reduce CPU usage - const syncInterval = Math.max(30000, traktSettings.syncFrequency); + // IMMEDIATE SYNC: Reduce sync interval to 5 seconds for near real-time sync + const syncInterval = 5000; // 5 seconds for immediate sync const interval = setInterval(() => { saveWatchProgress(); }, syncInterval); - logger.log(`[VideoPlayer] Watch progress save interval set to ${syncInterval}ms`); + logger.log(`[VideoPlayer] Watch progress save interval set to ${syncInterval}ms (immediate sync mode)`); setProgressSaveInterval(interval); return () => { @@ -434,7 +430,7 @@ const VideoPlayer: React.FC = () => { setProgressSaveInterval(null); }; } - }, [id, type, paused, currentTime, duration, traktSettings.syncFrequency]); + }, [id, type, paused, currentTime, duration]); useEffect(() => { return () => { @@ -629,8 +625,12 @@ const VideoPlayer: React.FC = () => { traktAutosync.handlePlaybackStart(currentTime, videoDuration); } + // Complete opening animation immediately before seeking + completeOpeningAnimation(); + if (initialPosition && !isInitialSeekComplete) { logger.log(`[VideoPlayer] Seeking to initial position: ${initialPosition}s (duration: ${videoDuration}s)`); + // Reduced timeout from 1000ms to 500ms setTimeout(() => { if (vlcRef.current && videoDuration > 0 && isMounted.current) { seekToTime(initialPosition); @@ -639,9 +639,9 @@ const VideoPlayer: React.FC = () => { } else { logger.error(`[VideoPlayer] Initial seek failed: vlcRef=${!!vlcRef.current}, duration=${videoDuration}, mounted=${isMounted.current}`); } - }, 1000); + }, 500); } - completeOpeningAnimation(); + controlsTimeout.current = setTimeout(hideControls, 5000); } catch (error) { logger.error('[VideoPlayer] Error in onLoad:', error); @@ -704,9 +704,13 @@ const VideoPlayer: React.FC = () => { }; const handleClose = async () => { - logger.log('[VideoPlayer] Close button pressed - syncing to Trakt before closing'); + // Prevent multiple close attempts + if (isSyncingBeforeClose) { + logger.log('[VideoPlayer] Close already in progress, ignoring duplicate call'); + return; + } - // Set syncing state to prevent multiple close attempts + logger.log('[VideoPlayer] Close button pressed - closing immediately and syncing to Trakt in background'); setIsSyncingBeforeClose(true); // Make sure we have the most accurate current time @@ -715,75 +719,56 @@ const VideoPlayer: React.FC = () => { logger.log(`[VideoPlayer] Current progress: ${actualCurrentTime}/${duration} (${progressPercent.toFixed(1)}%)`); - try { - // Force one last progress update (scrobble/pause) with the exact time - await traktAutosync.handleProgressUpdate(actualCurrentTime, duration, true); + // Cleanup and navigate back immediately without delay + const cleanup = async () => { + try { + // Unlock orientation first + await ScreenOrientation.unlockAsync(); + logger.log('[VideoPlayer] Orientation unlocked'); + } catch (orientationError) { + logger.warn('[VideoPlayer] Failed to unlock orientation:', orientationError); + } - // Sync progress to Trakt before closing - await traktAutosync.handlePlaybackEnd(actualCurrentTime, duration, 'unmount'); + // Disable immersive mode + disableImmersiveMode(); - // Start exit animation - Animated.parallel([ - Animated.timing(fadeAnim, { - toValue: 0, - duration: 150, - useNativeDriver: true, - }), - Animated.timing(openingFadeAnim, { - toValue: 0, - duration: 150, - useNativeDriver: true, - }), - ]).start(); - - // Cleanup and navigate back - const cleanup = async () => { - try { - // Unlock orientation first - await ScreenOrientation.unlockAsync(); - logger.log('[VideoPlayer] Orientation unlocked'); - } catch (orientationError) { - logger.warn('[VideoPlayer] Failed to unlock orientation:', orientationError); - } - - // Disable immersive mode - disableImmersiveMode(); - - // Navigate back with proper handling for fullscreen modal - try { - if (navigation.canGoBack()) { - navigation.goBack(); - } else { - // Fallback: navigate to main tabs if can't go back - navigation.navigate('MainTabs'); - } - logger.log('[VideoPlayer] Navigation completed'); - } catch (navError) { - logger.error('[VideoPlayer] Navigation error:', navError); - // Last resort: try to navigate to home + // Navigate back with proper handling for fullscreen modal + try { + if (navigation.canGoBack()) { + navigation.goBack(); + } else { + // Fallback: navigate to main tabs if can't go back navigation.navigate('MainTabs'); } - }; - - // Delay to ensure Trakt sync completes and animations finish - setTimeout(cleanup, 500); - - } catch (error) { - logger.error('[VideoPlayer] Error syncing to Trakt before closing:', error); - // Navigate anyway even if sync fails - disableImmersiveMode(); - try { - await ScreenOrientation.unlockAsync(); - } catch (orientationError) { - // Ignore orientation unlock errors - } - - if (navigation.canGoBack()) { - navigation.goBack(); - } else { + logger.log('[VideoPlayer] Navigation completed'); + } catch (navError) { + logger.error('[VideoPlayer] Navigation error:', navError); + // Last resort: try to navigate to home navigation.navigate('MainTabs'); } - } + }; + + // Navigate immediately + cleanup(); + + // Send Trakt sync in background (don't await) + const backgroundSync = async () => { + try { + logger.log('[VideoPlayer] Starting background Trakt sync'); + // Force one last progress update (scrobble/pause) with the exact time + await traktAutosync.handleProgressUpdate(actualCurrentTime, duration, true); + + // Sync progress to Trakt + await traktAutosync.handlePlaybackEnd(actualCurrentTime, duration, 'unmount'); + + logger.log('[VideoPlayer] Background Trakt sync completed successfully'); + } catch (error) { + logger.error('[VideoPlayer] Error in background Trakt sync:', error); + } + }; + + // Start background sync without blocking UI + backgroundSync(); }; const handleResume = async () => { @@ -836,10 +821,8 @@ const VideoPlayer: React.FC = () => { logger.log('[VideoPlayer] Video ended naturally, sending final progress update with 100%'); await traktAutosync.handleProgressUpdate(finalTime, duration, true); - // Small delay to ensure the progress update is processed - await new Promise(resolve => setTimeout(resolve, 300)); - - // Now send the stop call + // IMMEDIATE SYNC: Remove delay for instant sync + // Now send the stop call immediately logger.log('[VideoPlayer] Sending final stop call after natural end'); await traktAutosync.handlePlaybackEnd(finalTime, duration, 'ended'); diff --git a/src/hooks/useTraktAutosync.ts b/src/hooks/useTraktAutosync.ts index 9e0239b..8c14ea9 100644 --- a/src/hooks/useTraktAutosync.ts +++ b/src/hooks/useTraktAutosync.ts @@ -165,11 +165,11 @@ export function useTraktAutosync(options: TraktAutosyncOptions) { const progressPercent = (currentTime / duration) * 100; const now = Date.now(); - // Use the user's configured sync frequency - const timeSinceLastSync = now - lastSyncTime.current; + // IMMEDIATE SYNC: Remove all debouncing and frequency checks for instant sync const progressDiff = Math.abs(progressPercent - lastSyncProgress.current); - if (!force && timeSinceLastSync < autosyncSettings.syncFrequency && progressDiff < 5) { + // Only skip if not forced and progress difference is minimal (< 1%) + if (!force && progressDiff < 1) { return; } @@ -195,7 +195,7 @@ export function useTraktAutosync(options: TraktAutosyncOptions) { } catch (error) { logger.error('[TraktAutosync] Error syncing progress:', error); } - }, [isAuthenticated, autosyncSettings.enabled, autosyncSettings.syncFrequency, updateProgress, buildContentData, options]); + }, [isAuthenticated, autosyncSettings.enabled, updateProgress, buildContentData, options]); // Handle playback end/pause const handlePlaybackEnd = useCallback(async (currentTime: number, duration: number, reason: 'ended' | 'unmount' = 'ended') => { @@ -232,10 +232,10 @@ export function useTraktAutosync(options: TraktAutosyncOptions) { } } - // ENHANCED DEDUPLICATION: Prevent rapid successive calls (within 5 seconds) - // Bypass for significant updates - if (!isSignificantUpdate && now - lastStopCall.current < 5000) { - logger.log(`[TraktAutosync] Ignoring rapid successive stop call within 5 seconds (reason: ${reason})`); + // IMMEDIATE SYNC: Remove debouncing for instant sync when closing + // Only prevent truly duplicate calls (within 1 second) + if (!isSignificantUpdate && now - lastStopCall.current < 1000) { + logger.log(`[TraktAutosync] Ignoring duplicate stop call within 1 second (reason: ${reason})`); return; } @@ -377,4 +377,4 @@ export function useTraktAutosync(options: TraktAutosyncOptions) { handlePlaybackEnd, resetState }; -} \ No newline at end of file +} \ No newline at end of file diff --git a/src/screens/StreamsScreen.tsx b/src/screens/StreamsScreen.tsx index ecfbfc3..13e87e9 100644 --- a/src/screens/StreamsScreen.tsx +++ b/src/screens/StreamsScreen.tsx @@ -645,60 +645,45 @@ export const StreamsScreen = () => { }, [selectedEpisode, groupedEpisodes, id]); const navigateToPlayer = useCallback(async (stream: Stream) => { + // Prepare available streams for the change source feature + const streamsToPass = type === 'series' ? episodeStreams : groupedStreams; + + // Determine the stream name using the same logic as StreamCard + const streamName = stream.name || stream.title || 'Unnamed Stream'; + + // Navigate to player immediately without waiting for orientation lock + // This prevents delay in player opening + navigation.navigate('Player', { + uri: stream.url, + title: metadata?.name || '', + episodeTitle: type === 'series' ? currentEpisode?.name : undefined, + season: type === 'series' ? currentEpisode?.season_number : undefined, + episode: type === 'series' ? currentEpisode?.episode_number : undefined, + quality: stream.title?.match(/(\d+)p/)?.[1] || undefined, + year: metadata?.year, + streamProvider: stream.name, + streamName: streamName, + id, + type, + episodeId: type === 'series' && selectedEpisode ? selectedEpisode : undefined, + imdbId: imdbId || undefined, + availableStreams: streamsToPass, + backdrop: bannerImage || undefined, + }); + + // Lock orientation to landscape after navigation has started + // This allows the player to open immediately while orientation is being set try { - // Lock orientation to landscape before navigation to prevent glitches - await ScreenOrientation.lockAsync(ScreenOrientation.OrientationLock.LANDSCAPE); - - // Small delay to ensure orientation is set before navigation - await new Promise(resolve => setTimeout(resolve, 100)); - - // Prepare available streams for the change source feature - const streamsToPass = type === 'series' ? episodeStreams : groupedStreams; - - // Determine the stream name using the same logic as StreamCard - const streamName = stream.name || stream.title || 'Unnamed Stream'; - - navigation.navigate('Player', { - uri: stream.url, - title: metadata?.name || '', - episodeTitle: type === 'series' ? currentEpisode?.name : undefined, - season: type === 'series' ? currentEpisode?.season_number : undefined, - episode: type === 'series' ? currentEpisode?.episode_number : undefined, - quality: stream.title?.match(/(\d+)p/)?.[1] || undefined, - year: metadata?.year, - streamProvider: stream.name, - streamName: streamName, - id, - type, - episodeId: type === 'series' && selectedEpisode ? selectedEpisode : undefined, - imdbId: imdbId || undefined, - availableStreams: streamsToPass, - backdrop: bannerImage || undefined, - }); + ScreenOrientation.lockAsync(ScreenOrientation.OrientationLock.LANDSCAPE) + .catch(error => { + logger.error('[StreamsScreen] Error locking orientation after navigation:', error); + }); } catch (error) { - logger.error('[StreamsScreen] Error locking orientation before navigation:', error); - // Fallback: navigate anyway - const streamsToPass = type === 'series' ? episodeStreams : groupedStreams; - - navigation.navigate('Player', { - uri: stream.url, - title: metadata?.name || '', - episodeTitle: type === 'series' ? currentEpisode?.name : undefined, - season: type === 'series' ? currentEpisode?.season_number : undefined, - episode: type === 'series' ? currentEpisode?.episode_number : undefined, - quality: stream.title?.match(/(\d+)p/)?.[1] || undefined, - year: metadata?.year, - streamProvider: stream.name, - id, - type, - episodeId: type === 'series' && selectedEpisode ? selectedEpisode : undefined, - imdbId: imdbId || undefined, - availableStreams: streamsToPass, - backdrop: bannerImage || undefined, - }); + logger.error('[StreamsScreen] Error locking orientation after navigation:', error); } }, [metadata, type, currentEpisode, navigation, id, selectedEpisode, imdbId, episodeStreams, groupedStreams, bannerImage]); + // Update handleStreamPress const handleStreamPress = useCallback(async (stream: Stream) => { try { diff --git a/src/services/traktService.ts b/src/services/traktService.ts index 2b3d3ee..cbd3c32 100644 --- a/src/services/traktService.ts +++ b/src/services/traktService.ts @@ -269,14 +269,14 @@ export class TraktService { private readonly SCROBBLE_EXPIRY_MS = 46 * 60 * 1000; // 46 minutes (based on Trakt's expiry window) private scrobbledTimestamps: Map = new Map(); - // Track currently watching sessions to avoid duplicate starts + // Track currently watching sessions to avoid duplicate starts// Sync debouncing private currentlyWatching: Set = new Set(); private lastSyncTimes: Map = new Map(); - private readonly SYNC_DEBOUNCE_MS = 60000; // 60 seconds + private readonly SYNC_DEBOUNCE_MS = 1000; // 1 second for immediate sync // Debounce for stop calls private lastStopCalls: Map = new Map(); - private readonly STOP_DEBOUNCE_MS = 10000; // 10 seconds debounce for stop calls + private readonly STOP_DEBOUNCE_MS = 1000; // 1 second debounce for immediate stop calls // Default completion threshold (overridden by user settings) private readonly DEFAULT_COMPLETION_THRESHOLD = 80; // 80% @@ -1336,8 +1336,8 @@ export class TraktService { const watchingKey = this.getWatchingKey(contentData); const lastSync = this.lastSyncTimes.get(watchingKey) || 0; - // Debounce API calls unless forced - if (!force && (now - lastSync) < this.SYNC_DEBOUNCE_MS) { + // IMMEDIATE SYNC: Remove debouncing for instant sync, only prevent truly rapid calls (< 500ms) + if (!force && (now - lastSync) < 500) { return true; // Skip this sync, but return success } @@ -1377,9 +1377,9 @@ export class TraktService { const watchingKey = this.getWatchingKey(contentData); const now = Date.now(); - // Enhanced deduplication: Check if we recently stopped this content + // IMMEDIATE SYNC: Reduce debouncing for instant sync, only prevent truly duplicate calls (< 1 second) const lastStopTime = this.lastStopCalls.get(watchingKey); - if (lastStopTime && (now - lastStopTime) < this.STOP_DEBOUNCE_MS) { + if (lastStopTime && (now - lastStopTime) < 1000) { logger.log(`[TraktService] Ignoring duplicate stop call for ${contentData.title} (last stop ${((now - lastStopTime) / 1000).toFixed(1)}s ago)`); return true; // Return success to avoid error handling } @@ -1588,4 +1588,4 @@ export class TraktService { } // Export a singleton instance -export const traktService = TraktService.getInstance(); \ No newline at end of file +export const traktService = TraktService.getInstance(); \ No newline at end of file