NuvioStreaming_backup_24-10-25/src/hooks/useTraktAutosync.ts
tapframe 235a7eff24 Implement Trakt autosync functionality across video player components
This update integrates Trakt autosync capabilities into the AndroidVideoPlayer and VideoPlayer components, allowing for automatic syncing of watch progress and playback events. Key features include starting a watching session, updating progress during playback, and handling playback end events to ensure accurate tracking. Additionally, the useTraktIntegration and useTraktAutosync hooks have been enhanced to support these functionalities, improving the overall user experience by maintaining consistent watch history across devices.
2025-06-19 21:39:47 +05:30

258 lines
No EOL
9.8 KiB
TypeScript

import { useCallback, useRef, useEffect } from 'react';
import { useTraktIntegration } from './useTraktIntegration';
import { useTraktAutosyncSettings } from './useTraktAutosyncSettings';
import { TraktContentData } from '../services/traktService';
import { storageService } from '../services/storageService';
import { logger } from '../utils/logger';
interface TraktAutosyncOptions {
id: string;
type: 'movie' | 'series';
title: string;
year: number | string; // Allow both for compatibility
imdbId: string;
// For episodes
season?: number;
episode?: number;
showTitle?: string;
showYear?: number | string; // Allow both for compatibility
showImdbId?: string;
episodeId?: string;
}
export function useTraktAutosync(options: TraktAutosyncOptions) {
const {
isAuthenticated,
startWatching,
updateProgress,
stopWatching
} = useTraktIntegration();
const { settings: autosyncSettings } = useTraktAutosyncSettings();
const hasStartedWatching = useRef(false);
const lastSyncTime = useRef(0);
const lastSyncProgress = useRef(0);
const sessionKey = useRef<string | null>(null);
const unmountCount = useRef(0);
// Generate a unique session key for this content instance
useEffect(() => {
const contentKey = options.type === 'movie'
? `movie:${options.imdbId}`
: `episode:${options.imdbId}:${options.season}:${options.episode}`;
sessionKey.current = `${contentKey}:${Date.now()}`;
logger.log(`[TraktAutosync] Session started for: ${sessionKey.current}`);
return () => {
unmountCount.current++;
logger.log(`[TraktAutosync] Component unmount #${unmountCount.current} for: ${sessionKey.current}`);
};
}, [options.imdbId, options.season, options.episode, options.type]);
// Build Trakt content data from options
const buildContentData = useCallback((): TraktContentData => {
// Ensure year is a number and valid
const parseYear = (year: number | string | undefined): number => {
if (!year) return 0;
if (typeof year === 'number') return year;
const parsed = parseInt(year.toString(), 10);
return isNaN(parsed) ? 0 : parsed;
};
const numericYear = parseYear(options.year);
const numericShowYear = parseYear(options.showYear);
// Validate required fields
if (!options.title || !options.imdbId) {
logger.warn('[TraktAutosync] Missing required fields:', { title: options.title, imdbId: options.imdbId });
}
if (options.type === 'movie') {
return {
type: 'movie',
imdbId: options.imdbId,
title: options.title,
year: numericYear
};
} else {
return {
type: 'episode',
imdbId: options.imdbId,
title: options.title,
year: numericYear,
season: options.season,
episode: options.episode,
showTitle: options.showTitle || options.title,
showYear: numericShowYear || numericYear,
showImdbId: options.showImdbId || options.imdbId
};
}
}, [options]);
// Start watching (scrobble start)
const handlePlaybackStart = useCallback(async (currentTime: number, duration: number) => {
logger.log(`[TraktAutosync] handlePlaybackStart called: time=${currentTime}, duration=${duration}, authenticated=${isAuthenticated}, enabled=${autosyncSettings.enabled}, alreadyStarted=${hasStartedWatching.current}, session=${sessionKey.current}`);
if (!isAuthenticated || !autosyncSettings.enabled || hasStartedWatching.current) {
logger.log(`[TraktAutosync] Skipping handlePlaybackStart: authenticated=${isAuthenticated}, enabled=${autosyncSettings.enabled}, alreadyStarted=${hasStartedWatching.current}`);
return;
}
if (duration <= 0) {
logger.log(`[TraktAutosync] Skipping handlePlaybackStart: invalid duration (${duration})`);
return;
}
try {
const progressPercent = (currentTime / duration) * 100;
const contentData = buildContentData();
const success = await startWatching(contentData, progressPercent);
if (success) {
hasStartedWatching.current = true;
logger.log(`[TraktAutosync] Started watching: ${contentData.title} (session: ${sessionKey.current})`);
}
} catch (error) {
logger.error('[TraktAutosync] Error starting watch:', error);
}
}, [isAuthenticated, autosyncSettings.enabled, startWatching, buildContentData]);
// Sync progress during playback
const handleProgressUpdate = useCallback(async (
currentTime: number,
duration: number,
force: boolean = false
) => {
if (!isAuthenticated || !autosyncSettings.enabled || duration <= 0) {
return;
}
try {
const progressPercent = (currentTime / duration) * 100;
const now = Date.now();
// Use the user's configured sync frequency
const timeSinceLastSync = now - lastSyncTime.current;
const progressDiff = Math.abs(progressPercent - lastSyncProgress.current);
if (!force && timeSinceLastSync < autosyncSettings.syncFrequency && progressDiff < 5) {
return;
}
const contentData = buildContentData();
const success = await updateProgress(contentData, progressPercent, force);
if (success) {
lastSyncTime.current = now;
lastSyncProgress.current = progressPercent;
// Update local storage sync status
await storageService.updateTraktSyncStatus(
options.id,
options.type,
true,
progressPercent,
options.episodeId
);
logger.log(`[TraktAutosync] Synced progress ${progressPercent.toFixed(1)}%: ${contentData.title}`);
}
} catch (error) {
logger.error('[TraktAutosync] Error syncing progress:', error);
}
}, [isAuthenticated, autosyncSettings.enabled, autosyncSettings.syncFrequency, updateProgress, buildContentData, options]);
// Handle playback end/pause
const handlePlaybackEnd = useCallback(async (currentTime: number, duration: number, reason: 'ended' | 'unmount' = 'ended') => {
logger.log(`[TraktAutosync] handlePlaybackEnd called: reason=${reason}, time=${currentTime}, duration=${duration}, authenticated=${isAuthenticated}, enabled=${autosyncSettings.enabled}, started=${hasStartedWatching.current}, session=${sessionKey.current}, unmountCount=${unmountCount.current}`);
if (!isAuthenticated || !autosyncSettings.enabled) {
logger.log(`[TraktAutosync] Skipping handlePlaybackEnd: authenticated=${isAuthenticated}, enabled=${autosyncSettings.enabled}`);
return;
}
// Skip rapid unmount calls (likely from React strict mode or component remounts)
if (reason === 'unmount' && unmountCount.current > 1) {
logger.log(`[TraktAutosync] Skipping duplicate unmount call #${unmountCount.current}`);
return;
}
try {
let progressPercent = duration > 0 ? (currentTime / duration) * 100 : 0;
logger.log(`[TraktAutosync] Initial progress calculation: ${progressPercent.toFixed(1)}%`);
// If progress is 0 during unmount, use the last synced progress instead
// This happens when video player state is reset before component unmount
if (reason === 'unmount' && progressPercent < 1 && lastSyncProgress.current > 0) {
progressPercent = lastSyncProgress.current;
logger.log(`[TraktAutosync] Using last synced progress for unmount: ${progressPercent.toFixed(1)}%`);
}
// If we have valid progress but no started session, force start one first
if (!hasStartedWatching.current && progressPercent > 1) {
logger.log(`[TraktAutosync] Force starting session for progress: ${progressPercent.toFixed(1)}%`);
const contentData = buildContentData();
const success = await startWatching(contentData, progressPercent);
if (success) {
hasStartedWatching.current = true;
logger.log(`[TraktAutosync] Force started watching: ${contentData.title}`);
}
}
// Only stop if we have meaningful progress (>= 1%) or it's a natural video end
// Skip unmount calls with very low progress unless video actually ended
if (reason === 'unmount' && progressPercent < 1) {
logger.log(`[TraktAutosync] Skipping unmount stop for ${options.title} - too early (${progressPercent.toFixed(1)}%)`);
return;
}
const contentData = buildContentData();
// Use stopWatching for proper scrobble stop
const success = await stopWatching(contentData, progressPercent);
if (success) {
// Update local storage sync status
await storageService.updateTraktSyncStatus(
options.id,
options.type,
true,
progressPercent,
options.episodeId
);
}
// Reset state only for natural end or very high progress unmounts
if (reason === 'ended' || progressPercent >= 80) {
hasStartedWatching.current = false;
lastSyncTime.current = 0;
lastSyncProgress.current = 0;
logger.log(`[TraktAutosync] Reset session state for ${reason} at ${progressPercent.toFixed(1)}%`);
}
logger.log(`[TraktAutosync] Ended watching: ${options.title} (${reason})`);
} catch (error) {
logger.error('[TraktAutosync] Error ending watch:', error);
}
}, [isAuthenticated, autosyncSettings.enabled, stopWatching, buildContentData, options]);
// Reset state (useful when switching content)
const resetState = useCallback(() => {
hasStartedWatching.current = false;
lastSyncTime.current = 0;
lastSyncProgress.current = 0;
unmountCount.current = 0;
sessionKey.current = null;
logger.log(`[TraktAutosync] Manual state reset for: ${options.title}`);
}, [options.title]);
return {
isAuthenticated,
handlePlaybackStart,
handleProgressUpdate,
handlePlaybackEnd,
resetState
};
}