mirror of
https://github.com/tapframe/NuvioStreaming.git
synced 2026-05-01 05:04:33 +00:00
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:
parent
7627de32a9
commit
5e733f9eb2
6 changed files with 275 additions and 93 deletions
|
|
@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -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]);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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) {
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue