Improved trakt.

This commit is contained in:
tapframe 2025-07-29 14:28:52 +05:30
parent df89c16246
commit 6405fd2c71
6 changed files with 185 additions and 225 deletions

3
.gitignore vendored
View file

@ -42,4 +42,5 @@ CHANGELOG.md
android/ android/
HEATING_OPTIMIZATIONS.md HEATING_OPTIMIZATIONS.md
ios ios
android android
sliderreadme.md

View file

@ -264,16 +264,16 @@ const AndroidVideoPlayer: React.FC = () => {
}, []); }, []);
const startOpeningAnimation = () => { const startOpeningAnimation = () => {
// Logo entrance animation // Logo entrance animation - optimized for faster appearance
Animated.parallel([ Animated.parallel([
Animated.timing(logoOpacityAnim, { Animated.timing(logoOpacityAnim, {
toValue: 1, toValue: 1,
duration: 600, duration: 300, // Reduced from 600ms to 300ms
useNativeDriver: true, useNativeDriver: true,
}), }),
Animated.spring(logoScaleAnim, { Animated.spring(logoScaleAnim, {
toValue: 1, toValue: 1,
tension: 50, tension: 80, // Increased tension for faster spring
friction: 8, friction: 8,
useNativeDriver: true, useNativeDriver: true,
}), }),
@ -284,12 +284,12 @@ const AndroidVideoPlayer: React.FC = () => {
return Animated.sequence([ return Animated.sequence([
Animated.timing(pulseAnim, { Animated.timing(pulseAnim, {
toValue: 1.05, toValue: 1.05,
duration: 1000, duration: 800, // Reduced from 1000ms to 800ms
useNativeDriver: true, useNativeDriver: true,
}), }),
Animated.timing(pulseAnim, { Animated.timing(pulseAnim, {
toValue: 1, toValue: 1,
duration: 1000, duration: 800, // Reduced from 1000ms to 800ms
useNativeDriver: true, useNativeDriver: true,
}), }),
]); ]);
@ -303,38 +303,34 @@ const AndroidVideoPlayer: React.FC = () => {
}); });
}; };
// Start pulsing after a short delay // Start pulsing immediately without delay
setTimeout(() => { // Removed the 800ms delay
if (!isOpeningAnimationComplete) { loopPulse();
loopPulse();
}
}, 800);
}; };
const completeOpeningAnimation = () => { const completeOpeningAnimation = () => {
Animated.parallel([ Animated.parallel([
Animated.timing(openingFadeAnim, { Animated.timing(openingFadeAnim, {
toValue: 1, toValue: 1,
duration: 600, duration: 300, // Reduced from 600ms to 300ms
useNativeDriver: true, useNativeDriver: true,
}), }),
Animated.timing(openingScaleAnim, { Animated.timing(openingScaleAnim, {
toValue: 1, toValue: 1,
duration: 700, duration: 350, // Reduced from 700ms to 350ms
useNativeDriver: true, useNativeDriver: true,
}), }),
Animated.timing(backgroundFadeAnim, { Animated.timing(backgroundFadeAnim, {
toValue: 0, toValue: 0,
duration: 800, duration: 400, // Reduced from 800ms to 400ms
useNativeDriver: true, useNativeDriver: true,
}), }),
]).start(() => { ]).start(() => {
openingScaleAnim.setValue(1); openingScaleAnim.setValue(1);
openingFadeAnim.setValue(1); openingFadeAnim.setValue(1);
setIsOpeningAnimationComplete(true); setIsOpeningAnimationComplete(true);
setTimeout(() => { // Removed the 100ms delay
backgroundFadeAnim.setValue(0); backgroundFadeAnim.setValue(0);
}, 100);
}); });
}; };
@ -396,15 +392,14 @@ const AndroidVideoPlayer: React.FC = () => {
clearInterval(progressSaveInterval); clearInterval(progressSaveInterval);
} }
// Use the user's configured sync frequency with increased minimum to reduce heating // IMMEDIATE SYNC: Reduce sync interval to 5 seconds for near real-time sync
// Minimum interval increased from 5s to 30s to reduce CPU usage const syncInterval = 5000; // 5 seconds for immediate sync
const syncInterval = Math.max(30000, traktSettings.syncFrequency);
const interval = setInterval(() => { const interval = setInterval(() => {
saveWatchProgress(); saveWatchProgress();
}, syncInterval); }, 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); setProgressSaveInterval(interval);
return () => { return () => {
@ -412,7 +407,7 @@ const AndroidVideoPlayer: React.FC = () => {
setProgressSaveInterval(null); setProgressSaveInterval(null);
}; };
} }
}, [id, type, paused, currentTime, duration, traktSettings.syncFrequency]); }, [id, type, paused, currentTime, duration]);
useEffect(() => { useEffect(() => {
return () => { return () => {
@ -583,8 +578,12 @@ const AndroidVideoPlayer: React.FC = () => {
traktAutosync.handlePlaybackStart(currentTime, videoDuration); traktAutosync.handlePlaybackStart(currentTime, videoDuration);
} }
// Complete opening animation immediately before seeking
completeOpeningAnimation();
if (initialPosition && !isInitialSeekComplete) { if (initialPosition && !isInitialSeekComplete) {
logger.log(`[AndroidVideoPlayer] Seeking to initial position: ${initialPosition}s (duration: ${videoDuration}s)`); logger.log(`[AndroidVideoPlayer] Seeking to initial position: ${initialPosition}s (duration: ${videoDuration}s)`);
// Reduced timeout from 1000ms to 500ms
setTimeout(() => { setTimeout(() => {
if (videoRef.current && videoDuration > 0 && isMounted.current) { if (videoRef.current && videoDuration > 0 && isMounted.current) {
seekToTime(initialPosition); seekToTime(initialPosition);
@ -593,9 +592,9 @@ const AndroidVideoPlayer: React.FC = () => {
} else { } else {
logger.error(`[AndroidVideoPlayer] Initial seek failed: videoRef=${!!videoRef.current}, duration=${videoDuration}, mounted=${isMounted.current}`); logger.error(`[AndroidVideoPlayer] Initial seek failed: videoRef=${!!videoRef.current}, duration=${videoDuration}, mounted=${isMounted.current}`);
} }
}, 1000); }, 500);
} }
completeOpeningAnimation();
controlsTimeout.current = setTimeout(hideControls, 5000); controlsTimeout.current = setTimeout(hideControls, 5000);
} catch (error) { } catch (error) {
logger.error('[AndroidVideoPlayer] Error in onLoad:', error); logger.error('[AndroidVideoPlayer] Error in onLoad:', error);
@ -651,9 +650,13 @@ const AndroidVideoPlayer: React.FC = () => {
}; };
const handleClose = async () => { const handleClose = async () => {
logger.log('[AndroidVideoPlayer] Close button pressed - syncing to Trakt before closing'); // Prevent multiple close attempts
if (isSyncingBeforeClose) {
// Set syncing state to prevent multiple close attempts 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); setIsSyncingBeforeClose(true);
// Make sure we have the most accurate current time // 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)}%)`); logger.log(`[AndroidVideoPlayer] Current progress: ${actualCurrentTime}/${duration} (${progressPercent.toFixed(1)}%)`);
try { // Navigate immediately without delay
// Force one last progress update (scrobble/pause) with the exact time ScreenOrientation.unlockAsync().then(() => {
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
disableImmersiveMode(); disableImmersiveMode();
navigation.goBack(); 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 () => { const handleResume = async () => {
@ -752,10 +745,8 @@ const AndroidVideoPlayer: React.FC = () => {
logger.log('[AndroidVideoPlayer] Video ended naturally, sending final progress update with 100%'); logger.log('[AndroidVideoPlayer] Video ended naturally, sending final progress update with 100%');
await traktAutosync.handleProgressUpdate(finalTime, duration, true); await traktAutosync.handleProgressUpdate(finalTime, duration, true);
// Small delay to ensure the progress update is processed // IMMEDIATE SYNC: Remove delay for instant sync
await new Promise(resolve => setTimeout(resolve, 300)); // Now send the stop call immediately
// Now send the stop call
logger.log('[AndroidVideoPlayer] Sending final stop call after natural end'); logger.log('[AndroidVideoPlayer] Sending final stop call after natural end');
await traktAutosync.handlePlaybackEnd(finalTime, duration, 'ended'); await traktAutosync.handlePlaybackEnd(finalTime, duration, 'ended');
@ -1280,4 +1271,4 @@ const AndroidVideoPlayer: React.FC = () => {
); );
}; };
export default AndroidVideoPlayer; export default AndroidVideoPlayer;

View file

@ -286,16 +286,16 @@ const VideoPlayer: React.FC = () => {
}, []); }, []);
const startOpeningAnimation = () => { const startOpeningAnimation = () => {
// Logo entrance animation // Logo entrance animation - optimized for faster appearance
Animated.parallel([ Animated.parallel([
Animated.timing(logoOpacityAnim, { Animated.timing(logoOpacityAnim, {
toValue: 1, toValue: 1,
duration: 600, duration: 300, // Reduced from 600ms to 300ms
useNativeDriver: true, useNativeDriver: true,
}), }),
Animated.spring(logoScaleAnim, { Animated.spring(logoScaleAnim, {
toValue: 1, toValue: 1,
tension: 50, tension: 80, // Increased tension for faster spring
friction: 8, friction: 8,
useNativeDriver: true, useNativeDriver: true,
}), }),
@ -306,12 +306,12 @@ const VideoPlayer: React.FC = () => {
return Animated.sequence([ return Animated.sequence([
Animated.timing(pulseAnim, { Animated.timing(pulseAnim, {
toValue: 1.05, toValue: 1.05,
duration: 1000, duration: 800, // Reduced from 1000ms to 800ms
useNativeDriver: true, useNativeDriver: true,
}), }),
Animated.timing(pulseAnim, { Animated.timing(pulseAnim, {
toValue: 1, toValue: 1,
duration: 1000, duration: 800, // Reduced from 1000ms to 800ms
useNativeDriver: true, useNativeDriver: true,
}), }),
]); ]);
@ -325,29 +325,26 @@ const VideoPlayer: React.FC = () => {
}); });
}; };
// Start pulsing after a short delay // Start pulsing immediately without delay
setTimeout(() => { // Removed the 800ms delay
if (!isOpeningAnimationComplete) { loopPulse();
loopPulse();
}
}, 800);
}; };
const completeOpeningAnimation = () => { const completeOpeningAnimation = () => {
Animated.parallel([ Animated.parallel([
Animated.timing(openingFadeAnim, { Animated.timing(openingFadeAnim, {
toValue: 1, toValue: 1,
duration: 600, duration: 300, // Reduced from 600ms to 300ms
useNativeDriver: true, useNativeDriver: true,
}), }),
Animated.timing(openingScaleAnim, { Animated.timing(openingScaleAnim, {
toValue: 1, toValue: 1,
duration: 700, duration: 350, // Reduced from 700ms to 350ms
useNativeDriver: true, useNativeDriver: true,
}), }),
Animated.timing(backgroundFadeAnim, { Animated.timing(backgroundFadeAnim, {
toValue: 0, toValue: 0,
duration: 800, duration: 400, // Reduced from 800ms to 400ms
useNativeDriver: true, useNativeDriver: true,
}), }),
]).start(() => { ]).start(() => {
@ -418,15 +415,14 @@ const VideoPlayer: React.FC = () => {
clearInterval(progressSaveInterval); clearInterval(progressSaveInterval);
} }
// Use the user's configured sync frequency with increased minimum to reduce heating // IMMEDIATE SYNC: Reduce sync interval to 5 seconds for near real-time sync
// Minimum interval increased from 5s to 30s to reduce CPU usage const syncInterval = 5000; // 5 seconds for immediate sync
const syncInterval = Math.max(30000, traktSettings.syncFrequency);
const interval = setInterval(() => { const interval = setInterval(() => {
saveWatchProgress(); saveWatchProgress();
}, syncInterval); }, 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); setProgressSaveInterval(interval);
return () => { return () => {
@ -434,7 +430,7 @@ const VideoPlayer: React.FC = () => {
setProgressSaveInterval(null); setProgressSaveInterval(null);
}; };
} }
}, [id, type, paused, currentTime, duration, traktSettings.syncFrequency]); }, [id, type, paused, currentTime, duration]);
useEffect(() => { useEffect(() => {
return () => { return () => {
@ -629,8 +625,12 @@ const VideoPlayer: React.FC = () => {
traktAutosync.handlePlaybackStart(currentTime, videoDuration); traktAutosync.handlePlaybackStart(currentTime, videoDuration);
} }
// Complete opening animation immediately before seeking
completeOpeningAnimation();
if (initialPosition && !isInitialSeekComplete) { if (initialPosition && !isInitialSeekComplete) {
logger.log(`[VideoPlayer] Seeking to initial position: ${initialPosition}s (duration: ${videoDuration}s)`); logger.log(`[VideoPlayer] Seeking to initial position: ${initialPosition}s (duration: ${videoDuration}s)`);
// Reduced timeout from 1000ms to 500ms
setTimeout(() => { setTimeout(() => {
if (vlcRef.current && videoDuration > 0 && isMounted.current) { if (vlcRef.current && videoDuration > 0 && isMounted.current) {
seekToTime(initialPosition); seekToTime(initialPosition);
@ -639,9 +639,9 @@ const VideoPlayer: React.FC = () => {
} else { } else {
logger.error(`[VideoPlayer] Initial seek failed: vlcRef=${!!vlcRef.current}, duration=${videoDuration}, mounted=${isMounted.current}`); logger.error(`[VideoPlayer] Initial seek failed: vlcRef=${!!vlcRef.current}, duration=${videoDuration}, mounted=${isMounted.current}`);
} }
}, 1000); }, 500);
} }
completeOpeningAnimation();
controlsTimeout.current = setTimeout(hideControls, 5000); controlsTimeout.current = setTimeout(hideControls, 5000);
} catch (error) { } catch (error) {
logger.error('[VideoPlayer] Error in onLoad:', error); logger.error('[VideoPlayer] Error in onLoad:', error);
@ -704,9 +704,13 @@ const VideoPlayer: React.FC = () => {
}; };
const handleClose = async () => { 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); setIsSyncingBeforeClose(true);
// Make sure we have the most accurate current time // 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)}%)`); logger.log(`[VideoPlayer] Current progress: ${actualCurrentTime}/${duration} (${progressPercent.toFixed(1)}%)`);
try { // Cleanup and navigate back immediately without delay
// Force one last progress update (scrobble/pause) with the exact time const cleanup = async () => {
await traktAutosync.handleProgressUpdate(actualCurrentTime, duration, true); 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 // Disable immersive mode
await traktAutosync.handlePlaybackEnd(actualCurrentTime, duration, 'unmount'); disableImmersiveMode();
// Start exit animation // Navigate back with proper handling for fullscreen modal
Animated.parallel([ try {
Animated.timing(fadeAnim, { if (navigation.canGoBack()) {
toValue: 0, navigation.goBack();
duration: 150, } else {
useNativeDriver: true, // Fallback: navigate to main tabs if can't go back
}),
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
navigation.navigate('MainTabs'); navigation.navigate('MainTabs');
} }
}; logger.log('[VideoPlayer] Navigation completed');
} catch (navError) {
// Delay to ensure Trakt sync completes and animations finish logger.error('[VideoPlayer] Navigation error:', navError);
setTimeout(cleanup, 500); // Last resort: try to navigate to home
} 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 {
navigation.navigate('MainTabs'); 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 () => { const handleResume = async () => {
@ -836,10 +821,8 @@ const VideoPlayer: React.FC = () => {
logger.log('[VideoPlayer] Video ended naturally, sending final progress update with 100%'); logger.log('[VideoPlayer] Video ended naturally, sending final progress update with 100%');
await traktAutosync.handleProgressUpdate(finalTime, duration, true); await traktAutosync.handleProgressUpdate(finalTime, duration, true);
// Small delay to ensure the progress update is processed // IMMEDIATE SYNC: Remove delay for instant sync
await new Promise(resolve => setTimeout(resolve, 300)); // Now send the stop call immediately
// Now send the stop call
logger.log('[VideoPlayer] Sending final stop call after natural end'); logger.log('[VideoPlayer] Sending final stop call after natural end');
await traktAutosync.handlePlaybackEnd(finalTime, duration, 'ended'); await traktAutosync.handlePlaybackEnd(finalTime, duration, 'ended');

View file

@ -165,11 +165,11 @@ export function useTraktAutosync(options: TraktAutosyncOptions) {
const progressPercent = (currentTime / duration) * 100; const progressPercent = (currentTime / duration) * 100;
const now = Date.now(); const now = Date.now();
// Use the user's configured sync frequency // IMMEDIATE SYNC: Remove all debouncing and frequency checks for instant sync
const timeSinceLastSync = now - lastSyncTime.current;
const progressDiff = Math.abs(progressPercent - lastSyncProgress.current); 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; return;
} }
@ -195,7 +195,7 @@ export function useTraktAutosync(options: TraktAutosyncOptions) {
} catch (error) { } catch (error) {
logger.error('[TraktAutosync] Error syncing progress:', 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 // Handle playback end/pause
const handlePlaybackEnd = useCallback(async (currentTime: number, duration: number, reason: 'ended' | 'unmount' = 'ended') => { 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) // IMMEDIATE SYNC: Remove debouncing for instant sync when closing
// Bypass for significant updates // Only prevent truly duplicate calls (within 1 second)
if (!isSignificantUpdate && now - lastStopCall.current < 5000) { if (!isSignificantUpdate && now - lastStopCall.current < 1000) {
logger.log(`[TraktAutosync] Ignoring rapid successive stop call within 5 seconds (reason: ${reason})`); logger.log(`[TraktAutosync] Ignoring duplicate stop call within 1 second (reason: ${reason})`);
return; return;
} }
@ -377,4 +377,4 @@ export function useTraktAutosync(options: TraktAutosyncOptions) {
handlePlaybackEnd, handlePlaybackEnd,
resetState resetState
}; };
} }

View file

@ -645,60 +645,45 @@ export const StreamsScreen = () => {
}, [selectedEpisode, groupedEpisodes, id]); }, [selectedEpisode, groupedEpisodes, id]);
const navigateToPlayer = useCallback(async (stream: Stream) => { 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 { try {
// Lock orientation to landscape before navigation to prevent glitches ScreenOrientation.lockAsync(ScreenOrientation.OrientationLock.LANDSCAPE)
await ScreenOrientation.lockAsync(ScreenOrientation.OrientationLock.LANDSCAPE); .catch(error => {
logger.error('[StreamsScreen] Error locking orientation after navigation:', error);
// 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,
});
} catch (error) { } catch (error) {
logger.error('[StreamsScreen] Error locking orientation before navigation:', error); logger.error('[StreamsScreen] Error locking orientation after 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,
});
} }
}, [metadata, type, currentEpisode, navigation, id, selectedEpisode, imdbId, episodeStreams, groupedStreams, bannerImage]); }, [metadata, type, currentEpisode, navigation, id, selectedEpisode, imdbId, episodeStreams, groupedStreams, bannerImage]);
// Update handleStreamPress // Update handleStreamPress
const handleStreamPress = useCallback(async (stream: Stream) => { const handleStreamPress = useCallback(async (stream: Stream) => {
try { try {

View file

@ -269,14 +269,14 @@ export class TraktService {
private readonly SCROBBLE_EXPIRY_MS = 46 * 60 * 1000; // 46 minutes (based on Trakt's expiry window) private readonly SCROBBLE_EXPIRY_MS = 46 * 60 * 1000; // 46 minutes (based on Trakt's expiry window)
private scrobbledTimestamps: Map<string, number> = new Map(); private scrobbledTimestamps: Map<string, number> = new Map();
// Track currently watching sessions to avoid duplicate starts // Track currently watching sessions to avoid duplicate starts// Sync debouncing
private currentlyWatching: Set<string> = new Set(); private currentlyWatching: Set<string> = new Set();
private lastSyncTimes: Map<string, number> = new Map(); private lastSyncTimes: Map<string, number> = 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 // Debounce for stop calls
private lastStopCalls: Map<string, number> = new Map(); private lastStopCalls: Map<string, number> = 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) // Default completion threshold (overridden by user settings)
private readonly DEFAULT_COMPLETION_THRESHOLD = 80; // 80% private readonly DEFAULT_COMPLETION_THRESHOLD = 80; // 80%
@ -1336,8 +1336,8 @@ export class TraktService {
const watchingKey = this.getWatchingKey(contentData); const watchingKey = this.getWatchingKey(contentData);
const lastSync = this.lastSyncTimes.get(watchingKey) || 0; const lastSync = this.lastSyncTimes.get(watchingKey) || 0;
// Debounce API calls unless forced // IMMEDIATE SYNC: Remove debouncing for instant sync, only prevent truly rapid calls (< 500ms)
if (!force && (now - lastSync) < this.SYNC_DEBOUNCE_MS) { if (!force && (now - lastSync) < 500) {
return true; // Skip this sync, but return success return true; // Skip this sync, but return success
} }
@ -1377,9 +1377,9 @@ export class TraktService {
const watchingKey = this.getWatchingKey(contentData); const watchingKey = this.getWatchingKey(contentData);
const now = Date.now(); 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); 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)`); 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 return true; // Return success to avoid error handling
} }
@ -1588,4 +1588,4 @@ export class TraktService {
} }
// Export a singleton instance // Export a singleton instance
export const traktService = TraktService.getInstance(); export const traktService = TraktService.getInstance();