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.
This commit is contained in:
tapframe 2025-06-19 22:56:04 +05:30
parent fb7b58b97c
commit cdec184c14
6 changed files with 229 additions and 40 deletions

View file

@ -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 && (
<View style={styles.traktSyncIndicator}>
<MaterialIcons
name="sync"
size={8}
color="rgba(255,255,255,0.9)"
/>
</View>
)}
</View>
<Text style={[styles.watchProgressText, { color: currentTheme.colors.textMuted }]}>
{progressData.displayText}{progressData.episodeInfo} Last watched on {progressData.formattedTime}
{progressData.syncStatus}
</Text>
</Animated.View>
);
@ -280,6 +324,7 @@ const HeroSection: React.FC<HeroSectionProps> = ({
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<HeroSectionProps> = ({
</Animated.View>
</View>
{/* Optimized Watch Progress */}
{/* Enhanced Watch Progress with Trakt integration */}
<WatchProgressDisplay
watchProgress={watchProgress}
type={type}
@ -636,12 +681,22 @@ const styles = StyleSheet.create({
backgroundColor: 'rgba(255, 255, 255, 0.2)',
borderRadius: 1.25,
overflow: 'hidden',
marginBottom: 6
marginBottom: 6,
position: 'relative',
},
watchProgressFill: {
height: '100%',
borderRadius: 1.25,
},
traktSyncIndicator: {
position: 'absolute',
right: 2,
top: -2,
bottom: -2,
width: 12,
alignItems: 'center',
justifyContent: 'center',
},
watchProgressText: {
fontSize: 11,
textAlign: 'center',

View file

@ -15,6 +15,7 @@ interface TraktContextProps {
isEpisodeWatched: (imdbId: string, season: number, episode: number) => Promise<boolean>;
markMovieAsWatched: (imdbId: string, watchedAt?: Date) => Promise<boolean>;
markEpisodeAsWatched: (imdbId: string, season: number, episode: number, watchedAt?: Date) => Promise<boolean>;
forceSyncTraktProgress?: () => Promise<boolean>;
}
const TraktContext = createContext<TraktContextProps | undefined>(undefined);

View file

@ -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<TraktPlaybackItem[]> => {
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<boolean> => {
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<boolean> => {
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
};
}

View file

@ -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<WatchProgressData | null>(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,

View file

@ -330,18 +330,18 @@ const TraktSettingsScreen: React.FC = () => {
<View style={styles.settingItem}>
<View style={{ flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center' }}>
<View style={{ flex: 1 }}>
<Text style={[
styles.settingLabel,
{ color: isDarkMode ? currentTheme.colors.highEmphasis : currentTheme.colors.textDark }
]}>
Auto-sync playback progress
</Text>
<Text style={[
styles.settingDescription,
{ color: isDarkMode ? currentTheme.colors.mediumEmphasis : currentTheme.colors.textMutedDark }
]}>
<Text style={[
styles.settingLabel,
{ color: isDarkMode ? currentTheme.colors.highEmphasis : currentTheme.colors.textDark }
]}>
Auto-sync playback progress
</Text>
<Text style={[
styles.settingDescription,
{ color: isDarkMode ? currentTheme.colors.mediumEmphasis : currentTheme.colors.textMutedDark }
]}>
Automatically sync watch progress to Trakt
</Text>
</Text>
</View>
<Switch
value={autosyncSettings.enabled}

View file

@ -208,10 +208,11 @@ class StorageService {
};
await this.setWatchProgress(id, type, newProgress, episodeId);
} else {
// Merge with existing local progress
const shouldUseTraktProgress = traktTimestamp > 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) {