mirror of
https://github.com/tapframe/NuvioStreaming.git
synced 2026-03-11 17:45:38 +00:00
535 lines
No EOL
18 KiB
TypeScript
535 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]);
|
|
|
|
// IMMEDIATE SCROBBLE METHODS - Bypass queue for instant user feedback
|
|
|
|
// Immediate update progress while watching (scrobble pause)
|
|
const updateProgressImmediate = useCallback(async (
|
|
contentData: TraktContentData,
|
|
progress: number
|
|
): Promise<boolean> => {
|
|
if (!isAuthenticated) return false;
|
|
|
|
try {
|
|
return await traktService.scrobblePauseImmediate(contentData, progress);
|
|
} catch (error) {
|
|
logger.error('[useTraktIntegration] Error updating progress immediately:', 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]);
|
|
|
|
// Immediate stop watching content (scrobble stop)
|
|
const stopWatchingImmediate = useCallback(async (contentData: TraktContentData, progress: number): Promise<boolean> => {
|
|
if (!isAuthenticated) return false;
|
|
|
|
try {
|
|
return await traktService.scrobbleStopImmediate(contentData, progress);
|
|
} catch (error) {
|
|
logger.error('[useTraktIntegration] Error stopping watch immediately:', 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[]> => {
|
|
// getTraktPlaybackProgress call logging removed
|
|
|
|
if (!isAuthenticated) {
|
|
logger.log('[useTraktIntegration] getTraktPlaybackProgress: Not authenticated');
|
|
return [];
|
|
}
|
|
|
|
try {
|
|
// traktService.getPlaybackProgress call logging removed
|
|
const result = await traktService.getPlaybackProgress(type);
|
|
// Playback progress logging removed
|
|
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> => {
|
|
if (!isAuthenticated) {
|
|
return false;
|
|
}
|
|
|
|
try {
|
|
// Fetch both playback progress and recently watched movies
|
|
const [traktProgress, watchedMovies] = await Promise.all([
|
|
getTraktPlaybackProgress(),
|
|
traktService.getWatchedMovies()
|
|
]);
|
|
|
|
// Progress retrieval logging removed
|
|
|
|
// Batch process all updates to reduce storage notifications
|
|
const updatePromises: Promise<void>[] = [];
|
|
|
|
// 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';
|
|
} else if (item.type === 'episode' && item.show && item.episode) {
|
|
id = item.show.ids.imdb;
|
|
type = 'series';
|
|
episodeId = `${id}:${item.episode.season}:${item.episode.number}`;
|
|
} else {
|
|
continue;
|
|
}
|
|
|
|
// Try to calculate exact time if we have stored duration
|
|
const exactTime = await (async () => {
|
|
const storedDuration = await storageService.getContentDuration(id, type, episodeId);
|
|
if (storedDuration && storedDuration > 0) {
|
|
return (item.progress / 100) * storedDuration;
|
|
}
|
|
return undefined;
|
|
})();
|
|
|
|
updatePromises.push(
|
|
storageService.mergeWithTraktProgress(
|
|
id,
|
|
type,
|
|
item.progress,
|
|
item.paused_at,
|
|
episodeId,
|
|
exactTime
|
|
)
|
|
);
|
|
} catch (error) {
|
|
logger.error('[useTraktIntegration] Error preparing Trakt progress update:', 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;
|
|
|
|
updatePromises.push(
|
|
storageService.mergeWithTraktProgress(
|
|
id,
|
|
'movie',
|
|
100, // 100% progress for watched items
|
|
watchedAt
|
|
)
|
|
);
|
|
}
|
|
} catch (error) {
|
|
logger.error('[useTraktIntegration] Error preparing watched movie update:', error);
|
|
}
|
|
}
|
|
|
|
// Execute all updates in parallel
|
|
await Promise.all(updatePromises);
|
|
|
|
// Trakt merge logging removed
|
|
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
|
|
fetchAndMergeTraktProgress().then((success) => {
|
|
// Trakt progress merge success logging removed
|
|
});
|
|
}
|
|
}, [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') {
|
|
fetchAndMergeTraktProgress().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) {
|
|
fetchAndMergeTraktProgress();
|
|
}
|
|
}, [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,
|
|
updateProgressImmediate,
|
|
stopWatching,
|
|
stopWatchingImmediate,
|
|
syncProgress, // legacy
|
|
getTraktPlaybackProgress,
|
|
syncAllProgress,
|
|
fetchAndMergeTraktProgress,
|
|
forceSyncTraktProgress // For manual testing
|
|
};
|
|
} |