From cdec184c1408270c85ec76b3183245ac1bcea703 Mon Sep 17 00:00:00 2001 From: tapframe Date: Thu, 19 Jun 2025 22:56:04 +0530 Subject: [PATCH] Integrate Trakt support into watch progress management This update enhances the watch progress functionality by incorporating Trakt integration across various components. Key changes include the addition of Trakt-related properties in the watch progress state, improved synchronization logic, and enhanced UI elements to reflect Trakt sync status. The useTraktIntegration and useWatchProgress hooks have been updated to manage Trakt authentication and playback progress more effectively, ensuring a seamless user experience when tracking viewing history across devices. --- src/components/metadata/HeroSection.tsx | 71 +++++++++++++++++++--- src/contexts/TraktContext.tsx | 1 + src/hooks/useTraktIntegration.ts | 79 ++++++++++++++++++++++--- src/hooks/useWatchProgress.ts | 74 ++++++++++++++++++++--- src/screens/TraktSettingsScreen.tsx | 22 +++---- src/services/storageService.ts | 22 +++++-- 6 files changed, 229 insertions(+), 40 deletions(-) diff --git a/src/components/metadata/HeroSection.tsx b/src/components/metadata/HeroSection.tsx index 457d56fe..b74cdc86 100644 --- a/src/components/metadata/HeroSection.tsx +++ b/src/components/metadata/HeroSection.tsx @@ -23,6 +23,7 @@ import Animated, { withRepeat, } from 'react-native-reanimated'; import { useTheme } from '../../contexts/ThemeContext'; +import { useTraktContext } from '../../contexts/TraktContext'; import { logger } from '../../utils/logger'; import { TMDBService } from '../../services/tmdbService'; @@ -52,6 +53,8 @@ interface HeroSectionProps { duration: number; lastUpdated: number; episodeId?: string; + traktSynced?: boolean; + traktProgress?: number; } | null; type: 'movie' | 'series'; getEpisodeDetails: (episodeId: string) => { seasonNumber: string; episodeNumber: string; episodeName: string } | null; @@ -196,21 +199,29 @@ const ActionButtons = React.memo(({ ); }); -// Ultra-optimized WatchProgress Component +// Enhanced WatchProgress Component with Trakt integration const WatchProgressDisplay = React.memo(({ watchProgress, type, getEpisodeDetails, animatedStyle, }: { - watchProgress: { currentTime: number; duration: number; lastUpdated: number; episodeId?: string } | null; + watchProgress: { + currentTime: number; + duration: number; + lastUpdated: number; + episodeId?: string; + traktSynced?: boolean; + traktProgress?: number; + } | null; type: 'movie' | 'series'; getEpisodeDetails: (episodeId: string) => { seasonNumber: string; episodeNumber: string; episodeName: string } | null; animatedStyle: any; }) => { const { currentTheme } = useTheme(); + const { isAuthenticated: isTraktAuthenticated } = useTraktContext(); - // Memoized progress calculation + // Memoized progress calculation with Trakt integration const progressData = useMemo(() => { if (!watchProgress || watchProgress.duration === 0) return null; @@ -225,13 +236,33 @@ const WatchProgressDisplay = React.memo(({ } } + // Enhanced display text with Trakt integration + let displayText = progressPercent >= 95 ? 'Watched' : `${Math.round(progressPercent)}% watched`; + let syncStatus = ''; + + // Show Trakt sync status if user is authenticated + if (isTraktAuthenticated) { + if (watchProgress.traktSynced) { + syncStatus = ' • Synced with Trakt'; + // If we have specific Trakt progress that differs from local, mention it + if (watchProgress.traktProgress !== undefined && + Math.abs(progressPercent - watchProgress.traktProgress) > 5) { + displayText = `${Math.round(progressPercent)}% watched (${Math.round(watchProgress.traktProgress)}% on Trakt)`; + } + } else { + syncStatus = ' • Sync pending'; + } + } + return { progressPercent, formattedTime, episodeInfo, - displayText: progressPercent >= 95 ? 'Watched' : `${Math.round(progressPercent)}% watched` + displayText, + syncStatus, + isTraktSynced: watchProgress.traktSynced && isTraktAuthenticated }; - }, [watchProgress, type, getEpisodeDetails]); + }, [watchProgress, type, getEpisodeDetails, isTraktAuthenticated]); if (!progressData) return null; @@ -243,13 +274,26 @@ const WatchProgressDisplay = React.memo(({ styles.watchProgressFill, { width: `${progressData.progressPercent}%`, - backgroundColor: currentTheme.colors.primary + backgroundColor: progressData.isTraktSynced + ? '#E50914' // Netflix red for Trakt synced content + : currentTheme.colors.primary } ]} /> + {/* Trakt sync indicator */} + {progressData.isTraktSynced && ( + + + + )} {progressData.displayText}{progressData.episodeInfo} • Last watched on {progressData.formattedTime} + {progressData.syncStatus} ); @@ -280,6 +324,7 @@ const HeroSection: React.FC = ({ setLogoLoadError, }) => { const { currentTheme } = useTheme(); + const { isAuthenticated: isTraktAuthenticated } = useTraktContext(); // Enhanced state for smooth image loading const [imageError, setImageError] = useState(false); @@ -470,7 +515,7 @@ const HeroSection: React.FC = ({ - {/* Optimized Watch Progress */} + {/* Enhanced Watch Progress with Trakt integration */} Promise; markMovieAsWatched: (imdbId: string, watchedAt?: Date) => Promise; markEpisodeAsWatched: (imdbId: string, season: number, episode: number, watchedAt?: Date) => Promise; + forceSyncTraktProgress?: () => Promise; } const TraktContext = createContext(undefined); diff --git a/src/hooks/useTraktIntegration.ts b/src/hooks/useTraktIntegration.ts index 027205c6..7b4e71b2 100644 --- a/src/hooks/useTraktIntegration.ts +++ b/src/hooks/useTraktIntegration.ts @@ -13,15 +13,20 @@ export function useTraktIntegration() { // Check authentication status const checkAuthStatus = useCallback(async () => { + logger.log('[useTraktIntegration] checkAuthStatus called'); setIsLoading(true); try { const authenticated = await traktService.isAuthenticated(); + logger.log(`[useTraktIntegration] Authentication check result: ${authenticated}`); setIsAuthenticated(authenticated); if (authenticated) { + logger.log('[useTraktIntegration] User is authenticated, fetching profile...'); const profile = await traktService.getUserProfile(); + logger.log(`[useTraktIntegration] User profile: ${profile.username}`); setUserProfile(profile); } else { + logger.log('[useTraktIntegration] User is not authenticated'); setUserProfile(null); } @@ -187,10 +192,18 @@ export function useTraktIntegration() { // Get playback progress from Trakt const getTraktPlaybackProgress = useCallback(async (type?: 'movies' | 'shows'): Promise => { - if (!isAuthenticated) return []; + logger.log(`[useTraktIntegration] getTraktPlaybackProgress called - isAuthenticated: ${isAuthenticated}, type: ${type || 'all'}`); + + if (!isAuthenticated) { + logger.log('[useTraktIntegration] getTraktPlaybackProgress: Not authenticated'); + return []; + } try { - return await traktService.getPlaybackProgress(type); + logger.log('[useTraktIntegration] Calling traktService.getPlaybackProgress...'); + const result = await traktService.getPlaybackProgress(type); + logger.log(`[useTraktIntegration] traktService.getPlaybackProgress returned ${result.length} items`); + return result; } catch (error) { logger.error('[useTraktIntegration] Error getting playback progress:', error); return []; @@ -260,10 +273,22 @@ export function useTraktIntegration() { // Fetch and merge Trakt progress with local progress const fetchAndMergeTraktProgress = useCallback(async (): Promise => { - if (!isAuthenticated) return false; + logger.log(`[useTraktIntegration] fetchAndMergeTraktProgress called - isAuthenticated: ${isAuthenticated}`); + + if (!isAuthenticated) { + logger.log('[useTraktIntegration] Not authenticated, skipping Trakt progress fetch'); + return false; + } try { + logger.log('[useTraktIntegration] Fetching Trakt playback progress...'); const traktProgress = await getTraktPlaybackProgress(); + logger.log(`[useTraktIntegration] Retrieved ${traktProgress.length} Trakt progress items`); + + if (traktProgress.length === 0) { + logger.log('[useTraktIntegration] No Trakt progress found - user may not have any content in progress'); + return true; // Not an error, just no data + } for (const item of traktProgress) { try { @@ -274,14 +299,18 @@ export function useTraktIntegration() { if (item.type === 'movie' && item.movie) { id = item.movie.ids.imdb; type = 'movie'; + logger.log(`[useTraktIntegration] Processing Trakt movie: ${item.movie.title} (${id}) - ${item.progress}%`); } else if (item.type === 'episode' && item.show && item.episode) { id = item.show.ids.imdb; type = 'series'; - episodeId = `S${item.episode.season}E${item.episode.number}`; + episodeId = `${id}:${item.episode.season}:${item.episode.number}`; + logger.log(`[useTraktIntegration] Processing Trakt episode: ${item.show.title} S${item.episode.season}E${item.episode.number} (${id}) - ${item.progress}%`); } else { + logger.warn(`[useTraktIntegration] Skipping invalid Trakt item:`, item); continue; } + logger.log(`[useTraktIntegration] Merging progress for ${type} ${id}: ${item.progress}% from ${item.paused_at}`); await storageService.mergeWithTraktProgress( id, type, @@ -294,7 +323,7 @@ export function useTraktIntegration() { } } - logger.log(`[useTraktIntegration] Merged ${traktProgress.length} Trakt progress entries`); + logger.log(`[useTraktIntegration] Successfully merged ${traktProgress.length} Trakt progress entries`); return true; } catch (error) { logger.error('[useTraktIntegration] Error fetching and merging Trakt progress:', error); @@ -314,14 +343,47 @@ export function useTraktIntegration() { } }, [isAuthenticated, loadWatchedItems]); - // Auto-sync when authenticated changes + // Auto-sync when authenticated changes OR when auth status is refreshed useEffect(() => { if (isAuthenticated) { // Fetch Trakt progress and merge with local - fetchAndMergeTraktProgress(); + logger.log('[useTraktIntegration] User authenticated, fetching Trakt progress to replace local data'); + fetchAndMergeTraktProgress().then((success) => { + if (success) { + logger.log('[useTraktIntegration] Trakt progress merged successfully - local data replaced with Trakt data'); + } else { + logger.warn('[useTraktIntegration] Failed to merge Trakt progress'); + } + // Small delay to ensure storage subscribers are notified + setTimeout(() => { + logger.log('[useTraktIntegration] Trakt progress merge completed, UI should refresh'); + }, 100); + }); } }, [isAuthenticated, fetchAndMergeTraktProgress]); + // Trigger sync when auth status is manually refreshed (for login scenarios) + useEffect(() => { + if (isAuthenticated) { + logger.log('[useTraktIntegration] Auth status refresh detected, triggering Trakt progress merge'); + fetchAndMergeTraktProgress().then((success) => { + if (success) { + logger.log('[useTraktIntegration] Trakt progress merged after manual auth refresh'); + } + }); + } + }, [lastAuthCheck, isAuthenticated, fetchAndMergeTraktProgress]); + + // Manual force sync function for testing/troubleshooting + const forceSyncTraktProgress = useCallback(async (): Promise => { + logger.log('[useTraktIntegration] Manual force sync triggered'); + if (!isAuthenticated) { + logger.log('[useTraktIntegration] Cannot force sync - not authenticated'); + return false; + } + return await fetchAndMergeTraktProgress(); + }, [isAuthenticated, fetchAndMergeTraktProgress]); + return { isAuthenticated, isLoading, @@ -341,6 +403,7 @@ export function useTraktIntegration() { syncProgress, // legacy getTraktPlaybackProgress, syncAllProgress, - fetchAndMergeTraktProgress + fetchAndMergeTraktProgress, + forceSyncTraktProgress // For manual testing }; } \ No newline at end of file diff --git a/src/hooks/useWatchProgress.ts b/src/hooks/useWatchProgress.ts index 7c71539b..0dcc9df0 100644 --- a/src/hooks/useWatchProgress.ts +++ b/src/hooks/useWatchProgress.ts @@ -1,5 +1,6 @@ import { useState, useCallback, useEffect } from 'react'; import { useFocusEffect } from '@react-navigation/native'; +import { useTraktContext } from '../contexts/TraktContext'; import { logger } from '../utils/logger'; import { storageService } from '../services/storageService'; @@ -8,6 +9,8 @@ interface WatchProgressData { duration: number; lastUpdated: number; episodeId?: string; + traktSynced?: boolean; + traktProgress?: number; } export const useWatchProgress = ( @@ -17,6 +20,7 @@ export const useWatchProgress = ( episodes: any[] = [] ) => { const [watchProgress, setWatchProgress] = useState(null); + const { isAuthenticated: isTraktAuthenticated } = useTraktContext(); // Function to get episode details from episodeId const getEpisodeDetails = useCallback((episodeId: string): { seasonNumber: string; episodeNumber: string; episodeName: string } | null => { @@ -52,7 +56,7 @@ export const useWatchProgress = ( return null; }, [episodes]); - // Load watch progress + // Enhanced load watch progress with Trakt integration const loadWatchProgress = useCallback(async () => { try { if (id && type) { @@ -119,9 +123,20 @@ export const useWatchProgress = ( `${id}:${nextEpisode.season_number}:${nextEpisode.episode_number}`; const nextProgress = await storageService.getWatchProgress(id, type, nextEpisodeId); if (nextProgress) { - setWatchProgress({ ...nextProgress, episodeId: nextEpisodeId }); + setWatchProgress({ + ...nextProgress, + episodeId: nextEpisodeId, + traktSynced: nextProgress.traktSynced, + traktProgress: nextProgress.traktProgress + }); } else { - setWatchProgress({ currentTime: 0, duration: 0, lastUpdated: Date.now(), episodeId: nextEpisodeId }); + setWatchProgress({ + currentTime: 0, + duration: 0, + lastUpdated: Date.now(), + episodeId: nextEpisodeId, + traktSynced: false + }); } return; } @@ -132,7 +147,12 @@ export const useWatchProgress = ( } // If current episode is not finished, show its progress - setWatchProgress({ ...progress, episodeId }); + setWatchProgress({ + ...progress, + episodeId, + traktSynced: progress.traktSynced, + traktProgress: progress.traktProgress + }); } else { setWatchProgress(null); } @@ -151,9 +171,20 @@ export const useWatchProgress = ( `${id}:${unfinishedEpisode.season_number}:${unfinishedEpisode.episode_number}`; const progress = await storageService.getWatchProgress(id, type, epId); if (progress) { - setWatchProgress({ ...progress, episodeId: epId }); + setWatchProgress({ + ...progress, + episodeId: epId, + traktSynced: progress.traktSynced, + traktProgress: progress.traktProgress + }); } else { - setWatchProgress({ currentTime: 0, duration: 0, lastUpdated: Date.now(), episodeId: epId }); + setWatchProgress({ + currentTime: 0, + duration: 0, + lastUpdated: Date.now(), + episodeId: epId, + traktSynced: false + }); } } else { setWatchProgress(null); @@ -167,7 +198,12 @@ export const useWatchProgress = ( if (progressPercent >= 95) { setWatchProgress(null); } else { - setWatchProgress({ ...progress, episodeId }); + setWatchProgress({ + ...progress, + episodeId, + traktSynced: progress.traktSynced, + traktProgress: progress.traktProgress + }); } } else { setWatchProgress(null); @@ -180,7 +216,7 @@ export const useWatchProgress = ( } }, [id, type, episodeId, episodes]); - // Function to get play button text based on watch progress + // Enhanced function to get play button text with Trakt awareness const getPlayButtonText = useCallback(() => { if (!watchProgress || watchProgress.currentTime <= 0) { return 'Play'; @@ -192,9 +228,21 @@ export const useWatchProgress = ( return 'Play'; } + // If we have Trakt data and it differs significantly from local, show "Resume" + // but the UI will show the discrepancy return 'Resume'; }, [watchProgress]); + // Subscribe to storage changes for real-time updates + useEffect(() => { + const unsubscribe = storageService.subscribeToWatchProgressUpdates(() => { + logger.log('[useWatchProgress] Storage updated, reloading progress'); + loadWatchProgress(); + }); + + return unsubscribe; + }, [loadWatchProgress]); + // Initial load useEffect(() => { loadWatchProgress(); @@ -207,6 +255,16 @@ export const useWatchProgress = ( }, [loadWatchProgress]) ); + // Re-load when Trakt authentication status changes + useEffect(() => { + if (isTraktAuthenticated !== undefined) { + // Small delay to ensure Trakt context is fully initialized + setTimeout(() => { + loadWatchProgress(); + }, 100); + } + }, [isTraktAuthenticated, loadWatchProgress]); + return { watchProgress, getEpisodeDetails, diff --git a/src/screens/TraktSettingsScreen.tsx b/src/screens/TraktSettingsScreen.tsx index d9f9e474..ec3ad88e 100644 --- a/src/screens/TraktSettingsScreen.tsx +++ b/src/screens/TraktSettingsScreen.tsx @@ -330,18 +330,18 @@ const TraktSettingsScreen: React.FC = () => { - - Auto-sync playback progress - - + + Auto-sync playback progress + + Automatically sync watch progress to Trakt - + localProgress.lastUpdated; + // Always prioritize Trakt progress when merging + const localProgressPercent = (localProgress.currentTime / localProgress.duration) * 100; - if (shouldUseTraktProgress && localProgress.duration > 0) { + if (localProgress.duration > 0) { + // Use Trakt progress, keeping the existing duration const updatedProgress: WatchProgress = { ...localProgress, currentTime: (traktProgress / 100) * localProgress.duration, @@ -221,9 +222,20 @@ class StorageService { traktProgress }; await this.setWatchProgress(id, type, updatedProgress, episodeId); + logger.log(`[StorageService] Replaced local progress (${localProgressPercent.toFixed(1)}%) with Trakt progress (${traktProgress}%)`); } else { - // Local is newer, just mark as needing sync - await this.updateTraktSyncStatus(id, type, false, undefined, episodeId); + // If no duration, estimate it from Trakt progress + const estimatedDuration = traktProgress > 0 ? (100 / traktProgress) * 100 : 3600; + const updatedProgress: WatchProgress = { + currentTime: (traktProgress / 100) * estimatedDuration, + duration: estimatedDuration, + lastUpdated: traktTimestamp, + traktSynced: true, + traktLastSynced: Date.now(), + traktProgress + }; + await this.setWatchProgress(id, type, updatedProgress, episodeId); + logger.log(`[StorageService] Replaced local progress (${localProgressPercent.toFixed(1)}%) with Trakt progress (${traktProgress}%) - estimated duration`); } } } catch (error) {