Enhance video player components with content duration tracking and progress updates

This update introduces functionality to store and update the actual video duration in both AndroidVideoPlayer and VideoPlayer components. It ensures that the progress is accurately maintained when the video duration changes significantly. Additionally, the useTraktIntegration and storageService have been updated to handle exact playback times from Trakt, improving synchronization and user experience. Logging has been refined for better clarity during progress updates.
This commit is contained in:
tapframe 2025-06-21 12:50:13 +05:30
parent 7627de32a9
commit 5e733f9eb2
6 changed files with 275 additions and 93 deletions

View file

@ -455,6 +455,17 @@ const AndroidVideoPlayer: React.FC = () => {
const videoDuration = data.duration; const videoDuration = data.duration;
if (data.duration > 0) { if (data.duration > 0) {
setDuration(videoDuration); setDuration(videoDuration);
// Store the actual duration for future reference and update existing progress
if (id && type) {
storageService.setContentDuration(id, type, videoDuration, episodeId);
storageService.updateProgressDuration(id, type, videoDuration, episodeId);
// Update the saved duration for resume overlay if it was using an estimate
if (savedDuration && Math.abs(savedDuration - videoDuration) > 60) {
setSavedDuration(videoDuration);
}
}
} }
// Set aspect ratio from video dimensions // Set aspect ratio from video dimensions
@ -621,8 +632,6 @@ const AndroidVideoPlayer: React.FC = () => {
const handleResume = async () => { const handleResume = async () => {
if (resumePosition !== null) { if (resumePosition !== null) {
logger.log(`[AndroidVideoPlayer] Resume requested to position: ${resumePosition}s, duration: ${duration}, isPlayerReady: ${isPlayerReady}`);
if (rememberChoice) { if (rememberChoice) {
try { try {
await AsyncStorage.setItem(RESUME_PREF_KEY, RESUME_PREF.ALWAYS_RESUME); await AsyncStorage.setItem(RESUME_PREF_KEY, RESUME_PREF.ALWAYS_RESUME);
@ -635,11 +644,9 @@ const AndroidVideoPlayer: React.FC = () => {
// If video is already loaded and ready, seek immediately // If video is already loaded and ready, seek immediately
if (isPlayerReady && duration > 0 && videoRef.current) { if (isPlayerReady && duration > 0 && videoRef.current) {
logger.log(`[AndroidVideoPlayer] Video ready, seeking immediately to: ${resumePosition}s`);
seekToTime(resumePosition); seekToTime(resumePosition);
} else { } else {
// Otherwise, set initial position for when video loads // Otherwise, set initial position for when video loads
logger.log(`[AndroidVideoPlayer] Video not ready, setting initial position: ${resumePosition}s`);
setInitialPosition(resumePosition); setInitialPosition(resumePosition);
} }
} }

View file

@ -479,6 +479,17 @@ const VideoPlayer: React.FC = () => {
const videoDuration = data.duration / 1000; const videoDuration = data.duration / 1000;
if (data.duration > 0) { if (data.duration > 0) {
setDuration(videoDuration); setDuration(videoDuration);
// Store the actual duration for future reference and update existing progress
if (id && type) {
storageService.setContentDuration(id, type, videoDuration, episodeId);
storageService.updateProgressDuration(id, type, videoDuration, episodeId);
// Update the saved duration for resume overlay if it was using an estimate
if (savedDuration && Math.abs(savedDuration - videoDuration) > 60) {
setSavedDuration(videoDuration);
}
}
} }
setVideoAspectRatio(data.videoSize.width / data.videoSize.height); setVideoAspectRatio(data.videoSize.width / data.videoSize.height);
@ -636,8 +647,6 @@ const VideoPlayer: React.FC = () => {
const handleResume = async () => { const handleResume = async () => {
if (resumePosition !== null) { if (resumePosition !== null) {
logger.log(`[VideoPlayer] Resume requested to position: ${resumePosition}s, duration: ${duration}, isPlayerReady: ${isPlayerReady}`);
if (rememberChoice) { if (rememberChoice) {
try { try {
await AsyncStorage.setItem(RESUME_PREF_KEY, RESUME_PREF.ALWAYS_RESUME); await AsyncStorage.setItem(RESUME_PREF_KEY, RESUME_PREF.ALWAYS_RESUME);
@ -650,11 +659,9 @@ const VideoPlayer: React.FC = () => {
// If video is already loaded and ready, seek immediately // If video is already loaded and ready, seek immediately
if (isPlayerReady && duration > 0 && vlcRef.current) { if (isPlayerReady && duration > 0 && vlcRef.current) {
logger.log(`[VideoPlayer] Video ready, seeking immediately to: ${resumePosition}s`);
seekToTime(resumePosition); seekToTime(resumePosition);
} else { } else {
// Otherwise, set initial position for when video loads // Otherwise, set initial position for when video loads
logger.log(`[VideoPlayer] Video not ready, setting initial position: ${resumePosition}s`);
setInitialPosition(resumePosition); setInitialPosition(resumePosition);
} }
} }

View file

@ -186,7 +186,8 @@ export function useTraktAutosync(options: TraktAutosyncOptions) {
options.type, options.type,
true, true,
progressPercent, progressPercent,
options.episodeId options.episodeId,
currentTime
); );
logger.log(`[TraktAutosync] Synced progress ${progressPercent.toFixed(1)}%: ${contentData.title}`); logger.log(`[TraktAutosync] Synced progress ${progressPercent.toFixed(1)}%: ${contentData.title}`);
@ -318,7 +319,8 @@ export function useTraktAutosync(options: TraktAutosyncOptions) {
options.type, options.type,
true, true,
progressPercent, progressPercent,
options.episodeId options.episodeId,
currentTime
); );
// Mark session as complete if high progress (scrobbled) // Mark session as complete if high progress (scrobbled)

View file

@ -324,22 +324,21 @@ export function useTraktIntegration() {
// Fetch and merge Trakt progress with local progress // Fetch and merge Trakt progress with local progress
const fetchAndMergeTraktProgress = useCallback(async (): Promise<boolean> => { const fetchAndMergeTraktProgress = useCallback(async (): Promise<boolean> => {
logger.log(`[useTraktIntegration] fetchAndMergeTraktProgress called - isAuthenticated: ${isAuthenticated}`);
if (!isAuthenticated) { if (!isAuthenticated) {
logger.log('[useTraktIntegration] Not authenticated, skipping Trakt progress fetch');
return false; return false;
} }
try { try {
// Fetch both playback progress and recently watched movies // Fetch both playback progress and recently watched movies
logger.log('[useTraktIntegration] Fetching Trakt playback progress and watched movies...');
const [traktProgress, watchedMovies] = await Promise.all([ const [traktProgress, watchedMovies] = await Promise.all([
getTraktPlaybackProgress(), getTraktPlaybackProgress(),
traktService.getWatchedMovies() traktService.getWatchedMovies()
]); ]);
logger.log(`[useTraktIntegration] Retrieved ${traktProgress.length} Trakt progress items, ${watchedMovies.length} watched movies`); logger.log(`[useTraktIntegration] Retrieved ${traktProgress.length} progress items, ${watchedMovies.length} watched movies`);
// Batch process all updates to reduce storage notifications
const updatePromises: Promise<void>[] = [];
// Process playback progress (in-progress items) // Process playback progress (in-progress items)
for (const item of traktProgress) { for (const item of traktProgress) {
@ -351,27 +350,35 @@ export function useTraktIntegration() {
if (item.type === 'movie' && item.movie) { if (item.type === 'movie' && item.movie) {
id = item.movie.ids.imdb; id = item.movie.ids.imdb;
type = 'movie'; type = 'movie';
logger.log(`[useTraktIntegration] Processing Trakt movie progress: ${item.movie.title} (${id}) - ${item.progress}%`);
} else if (item.type === 'episode' && item.show && item.episode) { } else if (item.type === 'episode' && item.show && item.episode) {
id = item.show.ids.imdb; id = item.show.ids.imdb;
type = 'series'; type = 'series';
episodeId = `${id}:${item.episode.season}:${item.episode.number}`; 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 { } else {
logger.warn(`[useTraktIntegration] Skipping invalid Trakt progress item:`, item);
continue; continue;
} }
logger.log(`[useTraktIntegration] Merging progress for ${type} ${id}: ${item.progress}% from ${item.paused_at}`); // Try to calculate exact time if we have stored duration
await storageService.mergeWithTraktProgress( const exactTime = await (async () => {
id, const storedDuration = await storageService.getContentDuration(id, type, episodeId);
type, if (storedDuration && storedDuration > 0) {
item.progress, return (item.progress / 100) * storedDuration;
item.paused_at, }
episodeId return undefined;
})();
updatePromises.push(
storageService.mergeWithTraktProgress(
id,
type,
item.progress,
item.paused_at,
episodeId,
exactTime
)
); );
} catch (error) { } catch (error) {
logger.error('[useTraktIntegration] Error merging individual Trakt progress:', error); logger.error('[useTraktIntegration] Error preparing Trakt progress update:', error);
} }
} }
@ -381,21 +388,25 @@ export function useTraktIntegration() {
if (movie.movie?.ids?.imdb) { if (movie.movie?.ids?.imdb) {
const id = movie.movie.ids.imdb; const id = movie.movie.ids.imdb;
const watchedAt = movie.last_watched_at; const watchedAt = movie.last_watched_at;
logger.log(`[useTraktIntegration] Processing watched movie: ${movie.movie.title} (${id}) - 100% watched on ${watchedAt}`);
await storageService.mergeWithTraktProgress( updatePromises.push(
id, storageService.mergeWithTraktProgress(
'movie', id,
100, // 100% progress for watched items 'movie',
watchedAt 100, // 100% progress for watched items
watchedAt
)
); );
} }
} catch (error) { } catch (error) {
logger.error('[useTraktIntegration] Error merging watched movie:', error); logger.error('[useTraktIntegration] Error preparing watched movie update:', error);
} }
} }
logger.log(`[useTraktIntegration] Successfully merged ${traktProgress.length} progress items + ${watchedMovies.length} watched movies`); // Execute all updates in parallel
await Promise.all(updatePromises);
logger.log(`[useTraktIntegration] Successfully merged ${updatePromises.length} items from Trakt`);
return true; return true;
} catch (error) { } catch (error) {
logger.error('[useTraktIntegration] Error fetching and merging Trakt progress:', error); logger.error('[useTraktIntegration] Error fetching and merging Trakt progress:', error);
@ -419,17 +430,10 @@ export function useTraktIntegration() {
useEffect(() => { useEffect(() => {
if (isAuthenticated) { if (isAuthenticated) {
// Fetch Trakt progress and merge with local // Fetch Trakt progress and merge with local
logger.log('[useTraktIntegration] User authenticated, fetching Trakt progress to replace local data');
fetchAndMergeTraktProgress().then((success) => { fetchAndMergeTraktProgress().then((success) => {
if (success) { if (success) {
logger.log('[useTraktIntegration] Trakt progress merged successfully - local data replaced with Trakt data'); logger.log('[useTraktIntegration] Trakt progress merged successfully');
} 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]); }, [isAuthenticated, fetchAndMergeTraktProgress]);
@ -440,12 +444,7 @@ export function useTraktIntegration() {
const handleAppStateChange = (nextAppState: AppStateStatus) => { const handleAppStateChange = (nextAppState: AppStateStatus) => {
if (nextAppState === 'active') { if (nextAppState === 'active') {
logger.log('[useTraktIntegration] App became active, syncing Trakt data'); fetchAndMergeTraktProgress().catch(error => {
fetchAndMergeTraktProgress().then((success) => {
if (success) {
logger.log('[useTraktIntegration] App focus sync completed successfully');
}
}).catch(error => {
logger.error('[useTraktIntegration] App focus sync failed:', error); logger.error('[useTraktIntegration] App focus sync failed:', error);
}); });
} }
@ -461,12 +460,7 @@ export function useTraktIntegration() {
// Trigger sync when auth status is manually refreshed (for login scenarios) // Trigger sync when auth status is manually refreshed (for login scenarios)
useEffect(() => { useEffect(() => {
if (isAuthenticated) { if (isAuthenticated) {
logger.log('[useTraktIntegration] Auth status refresh detected, triggering Trakt progress merge'); fetchAndMergeTraktProgress();
fetchAndMergeTraktProgress().then((success) => {
if (success) {
logger.log('[useTraktIntegration] Trakt progress merged after manual auth refresh');
}
});
} }
}, [lastAuthCheck, isAuthenticated, fetchAndMergeTraktProgress]); }, [lastAuthCheck, isAuthenticated, fetchAndMergeTraktProgress]);

View file

@ -170,7 +170,6 @@ export const useWatchProgress = (
// Subscribe to storage changes for real-time updates // Subscribe to storage changes for real-time updates
useEffect(() => { useEffect(() => {
const unsubscribe = storageService.subscribeToWatchProgressUpdates(() => { const unsubscribe = storageService.subscribeToWatchProgressUpdates(() => {
logger.log('[useWatchProgress] Storage updated, reloading progress');
loadWatchProgress(); loadWatchProgress();
}); });

View file

@ -13,7 +13,12 @@ interface WatchProgress {
class StorageService { class StorageService {
private static instance: StorageService; private static instance: StorageService;
private readonly WATCH_PROGRESS_KEY = '@watch_progress:'; private readonly WATCH_PROGRESS_KEY = '@watch_progress:';
private readonly CONTENT_DURATION_KEY = '@content_duration:';
private watchProgressSubscribers: (() => void)[] = []; private watchProgressSubscribers: (() => void)[] = [];
private notificationDebounceTimer: NodeJS.Timeout | null = null;
private lastNotificationTime: number = 0;
private readonly NOTIFICATION_DEBOUNCE_MS = 1000; // 1 second debounce
private readonly MIN_NOTIFICATION_INTERVAL = 500; // Minimum 500ms between notifications
private constructor() {} private constructor() {}
@ -25,7 +30,65 @@ class StorageService {
} }
private getWatchProgressKey(id: string, type: string, episodeId?: string): string { private getWatchProgressKey(id: string, type: string, episodeId?: string): string {
return this.WATCH_PROGRESS_KEY + `${type}:${id}${episodeId ? `:${episodeId}` : ''}`; return `${this.WATCH_PROGRESS_KEY}${type}:${id}${episodeId ? `:${episodeId}` : ''}`;
}
private getContentDurationKey(id: string, type: string, episodeId?: string): string {
return `${this.CONTENT_DURATION_KEY}${type}:${id}${episodeId ? `:${episodeId}` : ''}`;
}
public async setContentDuration(
id: string,
type: string,
duration: number,
episodeId?: string
): Promise<void> {
try {
const key = this.getContentDurationKey(id, type, episodeId);
await AsyncStorage.setItem(key, duration.toString());
} catch (error) {
logger.error('Error setting content duration:', error);
}
}
public async getContentDuration(
id: string,
type: string,
episodeId?: string
): Promise<number | null> {
try {
const key = this.getContentDurationKey(id, type, episodeId);
const data = await AsyncStorage.getItem(key);
return data ? parseFloat(data) : null;
} catch (error) {
logger.error('Error getting content duration:', error);
return null;
}
}
public async updateProgressDuration(
id: string,
type: string,
newDuration: number,
episodeId?: string
): Promise<void> {
try {
const existingProgress = await this.getWatchProgress(id, type, episodeId);
if (existingProgress && Math.abs(existingProgress.duration - newDuration) > 60) {
// Calculate the new current time to maintain the same percentage
const progressPercent = (existingProgress.currentTime / existingProgress.duration) * 100;
const updatedProgress: WatchProgress = {
...existingProgress,
currentTime: (progressPercent / 100) * newDuration,
duration: newDuration,
lastUpdated: Date.now()
};
await this.setWatchProgress(id, type, updatedProgress, episodeId);
logger.log(`[StorageService] Updated progress duration from ${(existingProgress.duration/60).toFixed(0)}min to ${(newDuration/60).toFixed(0)}min`);
}
} catch (error) {
logger.error('Error updating progress duration:', error);
}
} }
public async setWatchProgress( public async setWatchProgress(
@ -36,16 +99,56 @@ class StorageService {
): Promise<void> { ): Promise<void> {
try { try {
const key = this.getWatchProgressKey(id, type, episodeId); const key = this.getWatchProgressKey(id, type, episodeId);
// Check if progress has actually changed significantly
const existingProgress = await this.getWatchProgress(id, type, episodeId);
if (existingProgress) {
const timeDiff = Math.abs(progress.currentTime - existingProgress.currentTime);
const durationDiff = Math.abs(progress.duration - existingProgress.duration);
// Only update if there's a significant change (>5 seconds or duration change)
if (timeDiff < 5 && durationDiff < 1) {
return; // Skip update for minor changes
}
}
await AsyncStorage.setItem(key, JSON.stringify(progress)); await AsyncStorage.setItem(key, JSON.stringify(progress));
// Notify subscribers
this.notifyWatchProgressSubscribers(); // Use debounced notification to reduce spam
this.debouncedNotifySubscribers();
} catch (error) { } catch (error) {
logger.error('Error saving watch progress:', error); logger.error('Error setting watch progress:', error);
}
}
private debouncedNotifySubscribers(): void {
const now = Date.now();
// Clear existing timer
if (this.notificationDebounceTimer) {
clearTimeout(this.notificationDebounceTimer);
}
// If we notified recently, debounce longer
const timeSinceLastNotification = now - this.lastNotificationTime;
if (timeSinceLastNotification < this.MIN_NOTIFICATION_INTERVAL) {
this.notificationDebounceTimer = setTimeout(() => {
this.notifyWatchProgressSubscribers();
}, this.NOTIFICATION_DEBOUNCE_MS);
} else {
// Notify immediately if enough time has passed
this.notifyWatchProgressSubscribers();
} }
} }
private notifyWatchProgressSubscribers(): void { private notifyWatchProgressSubscribers(): void {
this.watchProgressSubscribers.forEach(callback => callback()); this.lastNotificationTime = Date.now();
this.notificationDebounceTimer = null;
// Only notify if we have subscribers
if (this.watchProgressSubscribers.length > 0) {
this.watchProgressSubscribers.forEach(callback => callback());
}
} }
public subscribeToWatchProgressUpdates(callback: () => void): () => void { public subscribeToWatchProgressUpdates(callback: () => void): () => void {
@ -115,7 +218,8 @@ class StorageService {
type: string, type: string,
traktSynced: boolean, traktSynced: boolean,
traktProgress?: number, traktProgress?: number,
episodeId?: string episodeId?: string,
exactTime?: number
): Promise<void> { ): Promise<void> {
try { try {
const existingProgress = await this.getWatchProgress(id, type, episodeId); const existingProgress = await this.getWatchProgress(id, type, episodeId);
@ -124,7 +228,9 @@ class StorageService {
...existingProgress, ...existingProgress,
traktSynced, traktSynced,
traktLastSynced: traktSynced ? Date.now() : existingProgress.traktLastSynced, traktLastSynced: traktSynced ? Date.now() : existingProgress.traktLastSynced,
traktProgress: traktProgress !== undefined ? traktProgress : existingProgress.traktProgress traktProgress: traktProgress !== undefined ? traktProgress : existingProgress.traktProgress,
// Update current time with exact time if provided
...(exactTime && exactTime > 0 && { currentTime: exactTime })
}; };
await this.setWatchProgress(id, type, updatedProgress, episodeId); await this.setWatchProgress(id, type, updatedProgress, episodeId);
} }
@ -182,60 +288,127 @@ class StorageService {
} }
/** /**
* Merge Trakt progress with local progress * Merge Trakt progress with local progress using exact time when available
*/ */
public async mergeWithTraktProgress( public async mergeWithTraktProgress(
id: string, id: string,
type: string, type: string,
traktProgress: number, traktProgress: number,
traktPausedAt: string, traktPausedAt: string,
episodeId?: string episodeId?: string,
exactTime?: number // Optional exact time in seconds from Trakt scrobble data
): Promise<void> { ): Promise<void> {
try { try {
const localProgress = await this.getWatchProgress(id, type, episodeId); const localProgress = await this.getWatchProgress(id, type, episodeId);
const traktTimestamp = new Date(traktPausedAt).getTime(); const traktTimestamp = new Date(traktPausedAt).getTime();
if (!localProgress) { if (!localProgress) {
// No local progress, use Trakt data (estimate duration) // No local progress - use stored duration or estimate
const estimatedDuration = traktProgress > 0 ? (100 / traktProgress) * 100 : 3600; // Default 1 hour let duration = await this.getContentDuration(id, type, episodeId);
let currentTime: number;
if (exactTime && exactTime > 0) {
// Use exact time from Trakt if available
currentTime = exactTime;
if (!duration) {
// Calculate duration from exact time and percentage
duration = (exactTime / traktProgress) * 100;
}
} else {
// Fallback to percentage-based calculation
if (!duration) {
// Use reasonable duration estimates as fallback
if (type === 'movie') {
duration = 6600; // 110 minutes for movies
} else if (episodeId) {
duration = 2700; // 45 minutes for TV episodes
} else {
duration = 3600; // 60 minutes default
}
}
currentTime = (traktProgress / 100) * duration;
}
const newProgress: WatchProgress = { const newProgress: WatchProgress = {
currentTime: (traktProgress / 100) * estimatedDuration, currentTime,
duration: estimatedDuration, duration,
lastUpdated: traktTimestamp, lastUpdated: traktTimestamp,
traktSynced: true, traktSynced: true,
traktLastSynced: Date.now(), traktLastSynced: Date.now(),
traktProgress traktProgress
}; };
await this.setWatchProgress(id, type, newProgress, episodeId); await this.setWatchProgress(id, type, newProgress, episodeId);
const timeSource = exactTime ? 'exact' : 'calculated';
const durationSource = await this.getContentDuration(id, type, episodeId) ? 'stored' : 'estimated';
logger.log(`[StorageService] Created progress from Trakt: ${(currentTime/60).toFixed(1)}min (${timeSource}) of ${(duration/60).toFixed(0)}min (${durationSource})`);
} else { } else {
// Always prioritize Trakt progress when merging // Local progress exists - merge intelligently
const localProgressPercent = (localProgress.currentTime / localProgress.duration) * 100; const localProgressPercent = (localProgress.currentTime / localProgress.duration) * 100;
if (localProgress.duration > 0) { // Only proceed if there's a significant difference (>5% or different completion status)
// Use Trakt progress, keeping the existing duration const progressDiff = Math.abs(traktProgress - localProgressPercent);
const updatedProgress: WatchProgress = { if (progressDiff < 5 && traktProgress < 100 && localProgressPercent < 100) {
...localProgress, return; // Skip minor updates
currentTime: (traktProgress / 100) * localProgress.duration, }
lastUpdated: traktTimestamp,
traktSynced: true, let currentTime: number;
traktLastSynced: Date.now(), let duration = localProgress.duration;
traktProgress
}; if (exactTime && exactTime > 0 && localProgress.duration > 0) {
await this.setWatchProgress(id, type, updatedProgress, episodeId); // Use exact time from Trakt, keep local duration
logger.log(`[StorageService] Replaced local progress (${localProgressPercent.toFixed(1)}%) with Trakt progress (${traktProgress}%)`); currentTime = exactTime;
// If exact time doesn't match the duration well, recalculate duration
const calculatedDuration = (exactTime / traktProgress) * 100;
const durationDiff = Math.abs(calculatedDuration - localProgress.duration);
if (durationDiff > 300) { // More than 5 minutes difference
duration = calculatedDuration;
logger.log(`[StorageService] Updated duration based on exact time: ${(localProgress.duration/60).toFixed(0)}min → ${(duration/60).toFixed(0)}min`);
}
} else if (localProgress.duration > 0) {
// Use percentage calculation with local duration
currentTime = (traktProgress / 100) * localProgress.duration;
} else { } else {
// If no duration, estimate it from Trakt progress // No local duration, check stored duration
const estimatedDuration = traktProgress > 0 ? (100 / traktProgress) * 100 : 3600; const storedDuration = await this.getContentDuration(id, type, episodeId);
const updatedProgress: WatchProgress = { duration = storedDuration || 0;
currentTime: (traktProgress / 100) * estimatedDuration,
duration: estimatedDuration, if (!duration || duration <= 0) {
lastUpdated: traktTimestamp, if (exactTime && exactTime > 0) {
traktSynced: true, duration = (exactTime / traktProgress) * 100;
traktLastSynced: Date.now(), currentTime = exactTime;
traktProgress } else {
}; // Final fallback to estimates
await this.setWatchProgress(id, type, updatedProgress, episodeId); if (type === 'movie') {
logger.log(`[StorageService] Replaced local progress (${localProgressPercent.toFixed(1)}%) with Trakt progress (${traktProgress}%) - estimated duration`); duration = 6600; // 110 minutes for movies
} else if (episodeId) {
duration = 2700; // 45 minutes for TV episodes
} else {
duration = 3600; // 60 minutes default
}
currentTime = (traktProgress / 100) * duration;
}
} else {
currentTime = exactTime && exactTime > 0 ? exactTime : (traktProgress / 100) * duration;
}
}
const updatedProgress: WatchProgress = {
...localProgress,
currentTime,
duration,
lastUpdated: traktTimestamp,
traktSynced: true,
traktLastSynced: Date.now(),
traktProgress
};
await this.setWatchProgress(id, type, updatedProgress, episodeId);
// Only log significant changes
if (progressDiff > 10 || traktProgress === 100) {
const timeSource = exactTime ? 'exact' : 'calculated';
logger.log(`[StorageService] Updated progress: ${(currentTime/60).toFixed(1)}min (${timeSource}) = ${traktProgress}%`);
} }
} }
} catch (error) { } catch (error) {