mirror of
https://github.com/tapframe/NuvioStreaming.git
synced 2026-03-28 13:28:48 +00:00
This update enhances the Trakt integration by modifying the data fetching methods to include images for watched movies, shows, watchlist items, and collections. The useTraktIntegration hook and LibraryScreen have been updated accordingly to handle and display these images, improving the visual representation of content. Additionally, new interfaces have been introduced in the TraktService to support these changes, ensuring a more comprehensive user experience.
512 lines
No EOL
18 KiB
TypeScript
512 lines
No EOL
18 KiB
TypeScript
import { useState, useEffect, useCallback } from 'react';
|
|
import { AppState, AppStateStatus } from 'react-native';
|
|
import {
|
|
traktService,
|
|
TraktUser,
|
|
TraktWatchedItem,
|
|
TraktWatchlistItem,
|
|
TraktCollectionItem,
|
|
TraktRatingItem,
|
|
TraktContentData,
|
|
TraktPlaybackItem
|
|
} from '../services/traktService';
|
|
import { storageService } from '../services/storageService';
|
|
import { logger } from '../utils/logger';
|
|
|
|
export function useTraktIntegration() {
|
|
const [isAuthenticated, setIsAuthenticated] = useState<boolean>(false);
|
|
const [isLoading, setIsLoading] = useState<boolean>(true);
|
|
const [userProfile, setUserProfile] = useState<TraktUser | null>(null);
|
|
const [watchedMovies, setWatchedMovies] = useState<TraktWatchedItem[]>([]);
|
|
const [watchedShows, setWatchedShows] = useState<TraktWatchedItem[]>([]);
|
|
const [watchlistMovies, setWatchlistMovies] = useState<TraktWatchlistItem[]>([]);
|
|
const [watchlistShows, setWatchlistShows] = useState<TraktWatchlistItem[]>([]);
|
|
const [collectionMovies, setCollectionMovies] = useState<TraktCollectionItem[]>([]);
|
|
const [collectionShows, setCollectionShows] = useState<TraktCollectionItem[]>([]);
|
|
const [continueWatching, setContinueWatching] = useState<TraktPlaybackItem[]>([]);
|
|
const [ratedContent, setRatedContent] = useState<TraktRatingItem[]>([]);
|
|
const [lastAuthCheck, setLastAuthCheck] = useState<number>(Date.now());
|
|
|
|
// 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);
|
|
}
|
|
|
|
// Update the last auth check timestamp to trigger dependent components to update
|
|
setLastAuthCheck(Date.now());
|
|
} catch (error) {
|
|
logger.error('[useTraktIntegration] Error checking auth status:', error);
|
|
} finally {
|
|
setIsLoading(false);
|
|
}
|
|
}, []);
|
|
|
|
// Function to force refresh the auth status
|
|
const refreshAuthStatus = useCallback(async () => {
|
|
logger.log('[useTraktIntegration] Refreshing auth status');
|
|
await checkAuthStatus();
|
|
}, [checkAuthStatus]);
|
|
|
|
// Load watched items
|
|
const loadWatchedItems = useCallback(async () => {
|
|
if (!isAuthenticated) return;
|
|
|
|
setIsLoading(true);
|
|
try {
|
|
const [movies, shows] = await Promise.all([
|
|
traktService.getWatchedMoviesWithImages(),
|
|
traktService.getWatchedShowsWithImages()
|
|
]);
|
|
setWatchedMovies(movies);
|
|
setWatchedShows(shows);
|
|
} catch (error) {
|
|
logger.error('[useTraktIntegration] Error loading watched items:', error);
|
|
} finally {
|
|
setIsLoading(false);
|
|
}
|
|
}, [isAuthenticated]);
|
|
|
|
// Load all collections (watchlist, collection, continue watching, ratings)
|
|
const loadAllCollections = useCallback(async () => {
|
|
if (!isAuthenticated) return;
|
|
|
|
setIsLoading(true);
|
|
try {
|
|
const [
|
|
watchlistMovies,
|
|
watchlistShows,
|
|
collectionMovies,
|
|
collectionShows,
|
|
continueWatching,
|
|
ratings
|
|
] = await Promise.all([
|
|
traktService.getWatchlistMoviesWithImages(),
|
|
traktService.getWatchlistShowsWithImages(),
|
|
traktService.getCollectionMoviesWithImages(),
|
|
traktService.getCollectionShowsWithImages(),
|
|
traktService.getPlaybackProgressWithImages(),
|
|
traktService.getRatingsWithImages()
|
|
]);
|
|
|
|
setWatchlistMovies(watchlistMovies);
|
|
setWatchlistShows(watchlistShows);
|
|
setCollectionMovies(collectionMovies);
|
|
setCollectionShows(collectionShows);
|
|
setContinueWatching(continueWatching);
|
|
setRatedContent(ratings);
|
|
} catch (error) {
|
|
logger.error('[useTraktIntegration] Error loading all collections:', error);
|
|
} finally {
|
|
setIsLoading(false);
|
|
}
|
|
}, [isAuthenticated]);
|
|
|
|
// Check if a movie is watched
|
|
const isMovieWatched = useCallback(async (imdbId: string): Promise<boolean> => {
|
|
if (!isAuthenticated) return false;
|
|
|
|
try {
|
|
return await traktService.isMovieWatched(imdbId);
|
|
} catch (error) {
|
|
logger.error('[useTraktIntegration] Error checking if movie is watched:', error);
|
|
return false;
|
|
}
|
|
}, [isAuthenticated]);
|
|
|
|
// Check if an episode is watched
|
|
const isEpisodeWatched = useCallback(async (
|
|
imdbId: string,
|
|
season: number,
|
|
episode: number
|
|
): Promise<boolean> => {
|
|
if (!isAuthenticated) return false;
|
|
|
|
try {
|
|
return await traktService.isEpisodeWatched(imdbId, season, episode);
|
|
} catch (error) {
|
|
logger.error('[useTraktIntegration] Error checking if episode is watched:', error);
|
|
return false;
|
|
}
|
|
}, [isAuthenticated]);
|
|
|
|
// Mark a movie as watched
|
|
const markMovieAsWatched = useCallback(async (
|
|
imdbId: string,
|
|
watchedAt: Date = new Date()
|
|
): Promise<boolean> => {
|
|
if (!isAuthenticated) return false;
|
|
|
|
try {
|
|
const result = await traktService.addToWatchedMovies(imdbId, watchedAt);
|
|
if (result) {
|
|
// Refresh watched movies list
|
|
await loadWatchedItems();
|
|
}
|
|
return result;
|
|
} catch (error) {
|
|
logger.error('[useTraktIntegration] Error marking movie as watched:', error);
|
|
return false;
|
|
}
|
|
}, [isAuthenticated, loadWatchedItems]);
|
|
|
|
// Mark an episode as watched
|
|
const markEpisodeAsWatched = useCallback(async (
|
|
imdbId: string,
|
|
season: number,
|
|
episode: number,
|
|
watchedAt: Date = new Date()
|
|
): Promise<boolean> => {
|
|
if (!isAuthenticated) return false;
|
|
|
|
try {
|
|
const result = await traktService.addToWatchedEpisodes(imdbId, season, episode, watchedAt);
|
|
if (result) {
|
|
// Refresh watched shows list
|
|
await loadWatchedItems();
|
|
}
|
|
return result;
|
|
} catch (error) {
|
|
logger.error('[useTraktIntegration] Error marking episode as watched:', error);
|
|
return false;
|
|
}
|
|
}, [isAuthenticated, loadWatchedItems]);
|
|
|
|
// Start watching content (scrobble start)
|
|
const startWatching = useCallback(async (contentData: TraktContentData, progress: number): Promise<boolean> => {
|
|
if (!isAuthenticated) return false;
|
|
|
|
try {
|
|
return await traktService.scrobbleStart(contentData, progress);
|
|
} catch (error) {
|
|
logger.error('[useTraktIntegration] Error starting watch:', error);
|
|
return false;
|
|
}
|
|
}, [isAuthenticated]);
|
|
|
|
// Update progress while watching (scrobble pause)
|
|
const updateProgress = useCallback(async (
|
|
contentData: TraktContentData,
|
|
progress: number,
|
|
force: boolean = false
|
|
): Promise<boolean> => {
|
|
if (!isAuthenticated) return false;
|
|
|
|
try {
|
|
return await traktService.scrobblePause(contentData, progress, force);
|
|
} catch (error) {
|
|
logger.error('[useTraktIntegration] Error updating progress:', error);
|
|
return false;
|
|
}
|
|
}, [isAuthenticated]);
|
|
|
|
// Stop watching content (scrobble stop)
|
|
const stopWatching = useCallback(async (contentData: TraktContentData, progress: number): Promise<boolean> => {
|
|
if (!isAuthenticated) return false;
|
|
|
|
try {
|
|
return await traktService.scrobbleStop(contentData, progress);
|
|
} catch (error) {
|
|
logger.error('[useTraktIntegration] Error stopping watch:', error);
|
|
return false;
|
|
}
|
|
}, [isAuthenticated]);
|
|
|
|
// Sync progress to Trakt (legacy method)
|
|
const syncProgress = useCallback(async (
|
|
contentData: TraktContentData,
|
|
progress: number,
|
|
force: boolean = false
|
|
): Promise<boolean> => {
|
|
if (!isAuthenticated) return false;
|
|
|
|
try {
|
|
return await traktService.syncProgressToTrakt(contentData, progress, force);
|
|
} catch (error) {
|
|
logger.error('[useTraktIntegration] Error syncing progress:', error);
|
|
return false;
|
|
}
|
|
}, [isAuthenticated]);
|
|
|
|
// Get playback progress from Trakt
|
|
const getTraktPlaybackProgress = useCallback(async (type?: 'movies' | 'shows'): Promise<TraktPlaybackItem[]> => {
|
|
logger.log(`[useTraktIntegration] getTraktPlaybackProgress called - isAuthenticated: ${isAuthenticated}, type: ${type || 'all'}`);
|
|
|
|
if (!isAuthenticated) {
|
|
logger.log('[useTraktIntegration] getTraktPlaybackProgress: Not authenticated');
|
|
return [];
|
|
}
|
|
|
|
try {
|
|
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 [];
|
|
}
|
|
}, [isAuthenticated]);
|
|
|
|
// Sync all local progress to Trakt
|
|
const syncAllProgress = useCallback(async (): Promise<boolean> => {
|
|
if (!isAuthenticated) return false;
|
|
|
|
try {
|
|
const unsyncedProgress = await storageService.getUnsyncedProgress();
|
|
logger.log(`[useTraktIntegration] Found ${unsyncedProgress.length} unsynced progress entries`);
|
|
|
|
let syncedCount = 0;
|
|
const batchSize = 5; // Process in smaller batches
|
|
const delayBetweenBatches = 2000; // 2 seconds between batches
|
|
|
|
// Process items in batches to avoid overwhelming the API
|
|
for (let i = 0; i < unsyncedProgress.length; i += batchSize) {
|
|
const batch = unsyncedProgress.slice(i, i + batchSize);
|
|
|
|
// Process batch items with individual error handling
|
|
const batchPromises = batch.map(async (item) => {
|
|
try {
|
|
// Build content data from stored progress
|
|
const contentData: TraktContentData = {
|
|
type: item.type as 'movie' | 'episode',
|
|
imdbId: item.id,
|
|
title: 'Unknown', // We don't store title in progress, this would need metadata lookup
|
|
year: 0,
|
|
season: item.episodeId ? parseInt(item.episodeId.split('S')[1]?.split('E')[0] || '0') : undefined,
|
|
episode: item.episodeId ? parseInt(item.episodeId.split('E')[1] || '0') : undefined
|
|
};
|
|
|
|
const progressPercent = (item.progress.currentTime / item.progress.duration) * 100;
|
|
|
|
const success = await traktService.syncProgressToTrakt(contentData, progressPercent, true);
|
|
if (success) {
|
|
await storageService.updateTraktSyncStatus(item.id, item.type, true, progressPercent, item.episodeId);
|
|
return true;
|
|
}
|
|
return false;
|
|
} catch (error) {
|
|
logger.error('[useTraktIntegration] Error syncing individual progress:', error);
|
|
return false;
|
|
}
|
|
});
|
|
|
|
// Wait for batch to complete
|
|
const batchResults = await Promise.all(batchPromises);
|
|
syncedCount += batchResults.filter(result => result).length;
|
|
|
|
// Delay between batches to avoid rate limiting
|
|
if (i + batchSize < unsyncedProgress.length) {
|
|
await new Promise(resolve => setTimeout(resolve, delayBetweenBatches));
|
|
}
|
|
}
|
|
|
|
logger.log(`[useTraktIntegration] Synced ${syncedCount}/${unsyncedProgress.length} progress entries`);
|
|
return syncedCount > 0;
|
|
} catch (error) {
|
|
logger.error('[useTraktIntegration] Error syncing all progress:', error);
|
|
return false;
|
|
}
|
|
}, [isAuthenticated]);
|
|
|
|
// Fetch and merge Trakt progress with local progress
|
|
const fetchAndMergeTraktProgress = useCallback(async (): Promise<boolean> => {
|
|
logger.log(`[useTraktIntegration] fetchAndMergeTraktProgress called - isAuthenticated: ${isAuthenticated}`);
|
|
|
|
if (!isAuthenticated) {
|
|
logger.log('[useTraktIntegration] Not authenticated, skipping Trakt progress fetch');
|
|
return false;
|
|
}
|
|
|
|
try {
|
|
// Fetch both playback progress and recently watched movies
|
|
logger.log('[useTraktIntegration] Fetching Trakt playback progress and watched movies...');
|
|
const [traktProgress, watchedMovies] = await Promise.all([
|
|
getTraktPlaybackProgress(),
|
|
traktService.getWatchedMovies()
|
|
]);
|
|
|
|
logger.log(`[useTraktIntegration] Retrieved ${traktProgress.length} Trakt progress items, ${watchedMovies.length} watched movies`);
|
|
|
|
// Process playback progress (in-progress items)
|
|
for (const item of traktProgress) {
|
|
try {
|
|
let id: string;
|
|
let type: string;
|
|
let episodeId: string | undefined;
|
|
|
|
if (item.type === 'movie' && item.movie) {
|
|
id = item.movie.ids.imdb;
|
|
type = 'movie';
|
|
logger.log(`[useTraktIntegration] Processing Trakt movie progress: ${item.movie.title} (${id}) - ${item.progress}%`);
|
|
} else if (item.type === 'episode' && item.show && item.episode) {
|
|
id = item.show.ids.imdb;
|
|
type = 'series';
|
|
episodeId = `${id}:${item.episode.season}:${item.episode.number}`;
|
|
logger.log(`[useTraktIntegration] Processing Trakt episode progress: ${item.show.title} S${item.episode.season}E${item.episode.number} (${id}) - ${item.progress}%`);
|
|
} else {
|
|
logger.warn(`[useTraktIntegration] Skipping invalid Trakt progress item:`, item);
|
|
continue;
|
|
}
|
|
|
|
logger.log(`[useTraktIntegration] Merging progress for ${type} ${id}: ${item.progress}% from ${item.paused_at}`);
|
|
await storageService.mergeWithTraktProgress(
|
|
id,
|
|
type,
|
|
item.progress,
|
|
item.paused_at,
|
|
episodeId
|
|
);
|
|
} catch (error) {
|
|
logger.error('[useTraktIntegration] Error merging individual Trakt progress:', error);
|
|
}
|
|
}
|
|
|
|
// Process watched movies (100% completed)
|
|
for (const movie of watchedMovies) {
|
|
try {
|
|
if (movie.movie?.ids?.imdb) {
|
|
const id = movie.movie.ids.imdb;
|
|
const watchedAt = movie.last_watched_at;
|
|
logger.log(`[useTraktIntegration] Processing watched movie: ${movie.movie.title} (${id}) - 100% watched on ${watchedAt}`);
|
|
|
|
await storageService.mergeWithTraktProgress(
|
|
id,
|
|
'movie',
|
|
100, // 100% progress for watched items
|
|
watchedAt
|
|
);
|
|
}
|
|
} catch (error) {
|
|
logger.error('[useTraktIntegration] Error merging watched movie:', error);
|
|
}
|
|
}
|
|
|
|
logger.log(`[useTraktIntegration] Successfully merged ${traktProgress.length} progress items + ${watchedMovies.length} watched movies`);
|
|
return true;
|
|
} catch (error) {
|
|
logger.error('[useTraktIntegration] Error fetching and merging Trakt progress:', error);
|
|
return false;
|
|
}
|
|
}, [isAuthenticated, getTraktPlaybackProgress]);
|
|
|
|
// Initialize and check auth status
|
|
useEffect(() => {
|
|
checkAuthStatus();
|
|
}, [checkAuthStatus]);
|
|
|
|
// Load watched items when authenticated
|
|
useEffect(() => {
|
|
if (isAuthenticated) {
|
|
loadWatchedItems();
|
|
}
|
|
}, [isAuthenticated, loadWatchedItems]);
|
|
|
|
// Auto-sync when authenticated changes OR when auth status is refreshed
|
|
useEffect(() => {
|
|
if (isAuthenticated) {
|
|
// Fetch Trakt progress and merge with local
|
|
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]);
|
|
|
|
// App focus sync - sync when app comes back into focus (much smarter than periodic)
|
|
useEffect(() => {
|
|
if (!isAuthenticated) return;
|
|
|
|
const handleAppStateChange = (nextAppState: AppStateStatus) => {
|
|
if (nextAppState === 'active') {
|
|
logger.log('[useTraktIntegration] App became active, syncing Trakt data');
|
|
fetchAndMergeTraktProgress().then((success) => {
|
|
if (success) {
|
|
logger.log('[useTraktIntegration] App focus sync completed successfully');
|
|
}
|
|
}).catch(error => {
|
|
logger.error('[useTraktIntegration] App focus sync failed:', error);
|
|
});
|
|
}
|
|
};
|
|
|
|
const subscription = AppState.addEventListener('change', handleAppStateChange);
|
|
|
|
return () => {
|
|
subscription?.remove();
|
|
};
|
|
}, [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,
|
|
userProfile,
|
|
watchedMovies,
|
|
watchedShows,
|
|
watchlistMovies,
|
|
watchlistShows,
|
|
collectionMovies,
|
|
collectionShows,
|
|
continueWatching,
|
|
ratedContent,
|
|
checkAuthStatus,
|
|
loadWatchedItems,
|
|
loadAllCollections,
|
|
isMovieWatched,
|
|
isEpisodeWatched,
|
|
markMovieAsWatched,
|
|
markEpisodeAsWatched,
|
|
refreshAuthStatus,
|
|
startWatching,
|
|
updateProgress,
|
|
stopWatching,
|
|
syncProgress, // legacy
|
|
getTraktPlaybackProgress,
|
|
syncAllProgress,
|
|
fetchAndMergeTraktProgress,
|
|
forceSyncTraktProgress // For manual testing
|
|
};
|
|
}
|