From 2feba6f6ebb5f65e8f6e571506477b693b11dc3d Mon Sep 17 00:00:00 2001 From: tapframe Date: Fri, 4 Jul 2025 18:16:13 +0530 Subject: [PATCH] soem fixes for trakt --- src/components/player/AndroidVideoPlayer.tsx | 25 ++++- src/components/player/VideoPlayer.tsx | 28 +++-- src/services/traktService.ts | 106 ++++++++++++++++--- 3 files changed, 133 insertions(+), 26 deletions(-) diff --git a/src/components/player/AndroidVideoPlayer.tsx b/src/components/player/AndroidVideoPlayer.tsx index a7a4d28..e8ecf37 100644 --- a/src/components/player/AndroidVideoPlayer.tsx +++ b/src/components/player/AndroidVideoPlayer.tsx @@ -11,6 +11,7 @@ import { logger } from '../../utils/logger'; import AsyncStorage from '@react-native-async-storage/async-storage'; import { MaterialIcons } from '@expo/vector-icons'; import { useTraktAutosync } from '../../hooks/useTraktAutosync'; +import { useTraktAutosyncSettings } from '../../hooks/useTraktAutosyncSettings'; import { DEFAULT_SUBTITLE_SIZE, @@ -79,6 +80,9 @@ const AndroidVideoPlayer: React.FC = () => { episodeId: episodeId }); + // Get the Trakt autosync settings to use the user-configured sync frequency + const { settings: traktSettings } = useTraktAutosyncSettings(); + safeDebugLog("Android Component mounted with props", { uri, title, season, episode, episodeTitle, quality, year, streamProvider, id, type, episodeId, imdbId @@ -321,16 +325,24 @@ const AndroidVideoPlayer: React.FC = () => { if (progressSaveInterval) { clearInterval(progressSaveInterval); } + + // Use the user's configured sync frequency instead of hard-coded 5000ms + // But ensure we have a minimum interval of 5 seconds + const syncInterval = Math.max(5000, traktSettings.syncFrequency); + const interval = setInterval(() => { saveWatchProgress(); - }, 5000); + }, syncInterval); + + logger.log(`[AndroidVideoPlayer] Watch progress save interval set to ${syncInterval}ms`); + setProgressSaveInterval(interval); return () => { clearInterval(interval); setProgressSaveInterval(null); }; } - }, [id, type, paused, currentTime, duration]); + }, [id, type, paused, currentTime, duration, traktSettings.syncFrequency]); useEffect(() => { return () => { @@ -721,7 +733,14 @@ const AndroidVideoPlayer: React.FC = () => { const togglePlayback = () => { if (videoRef.current) { - setPaused(!paused); + const newPausedState = !paused; + setPaused(newPausedState); + + // Send a forced pause update to Trakt immediately when user pauses + if (newPausedState && duration > 0) { + traktAutosync.handleProgressUpdate(currentTime, duration, true); + logger.log('[AndroidVideoPlayer] Sent forced pause update to Trakt'); + } } }; diff --git a/src/components/player/VideoPlayer.tsx b/src/components/player/VideoPlayer.tsx index 4bd28e3..99fda75 100644 --- a/src/components/player/VideoPlayer.tsx +++ b/src/components/player/VideoPlayer.tsx @@ -12,6 +12,7 @@ import AsyncStorage from '@react-native-async-storage/async-storage'; import { MaterialIcons } from '@expo/vector-icons'; import AndroidVideoPlayer from './AndroidVideoPlayer'; import { useTraktAutosync } from '../../hooks/useTraktAutosync'; +import { useTraktAutosyncSettings } from '../../hooks/useTraktAutosyncSettings'; import { DEFAULT_SUBTITLE_SIZE, @@ -74,6 +75,9 @@ const VideoPlayer: React.FC = () => { episodeId: episodeId }); + // Get the Trakt autosync settings to use the user-configured sync frequency + const { settings: traktSettings } = useTraktAutosyncSettings(); + safeDebugLog("Component mounted with props", { uri, title, season, episode, episodeTitle, quality, year, streamProvider, id, type, episodeId, imdbId @@ -316,16 +320,24 @@ const VideoPlayer: React.FC = () => { if (progressSaveInterval) { clearInterval(progressSaveInterval); } + + // Use the user's configured sync frequency instead of hard-coded 5000ms + // But ensure we have a minimum interval of 5 seconds + const syncInterval = Math.max(5000, traktSettings.syncFrequency); + const interval = setInterval(() => { saveWatchProgress(); - }, 5000); + }, syncInterval); + + logger.log(`[VideoPlayer] Watch progress save interval set to ${syncInterval}ms`); + setProgressSaveInterval(interval); return () => { clearInterval(interval); setProgressSaveInterval(null); }; } - }, [id, type, paused, currentTime, duration]); + }, [id, type, paused, currentTime, duration, traktSettings.syncFrequency]); useEffect(() => { return () => { @@ -341,16 +353,20 @@ const VideoPlayer: React.FC = () => { if (isMounted.current && !isSeeking.current) { setPaused(false); - // Start Trakt watching session only if duration is loaded - if (duration > 0) { - traktAutosync.handlePlaybackStart(currentTime, duration); - } + // Note: handlePlaybackStart is already called in onLoad + // We don't need to call it again here to avoid duplicate calls } }; const onPaused = () => { if (isMounted.current) { setPaused(true); + + // Send a forced pause update to Trakt immediately when user pauses + if (duration > 0) { + traktAutosync.handleProgressUpdate(currentTime, duration, true); + logger.log('[VideoPlayer] Sent forced pause update to Trakt'); + } } }; diff --git a/src/services/traktService.ts b/src/services/traktService.ts index 071deec..4ed8816 100644 --- a/src/services/traktService.ts +++ b/src/services/traktService.ts @@ -64,7 +64,7 @@ export interface TraktWatchlistItem { }; show?: { title: string; - year: number; + year: number ids: { trakt: number; slug: string; @@ -271,37 +271,93 @@ export class TraktService { // Track currently watching sessions to avoid duplicate starts private currentlyWatching: Set = new Set(); - private lastSyncTime: number = 0; + private lastSyncTimes: Map = new Map(); private readonly SYNC_DEBOUNCE_MS = 60000; // 60 seconds - // Enhanced deduplication for stop calls + // Debounce for stop calls private lastStopCalls: Map = new Map(); private readonly STOP_DEBOUNCE_MS = 10000; // 10 seconds debounce for stop calls + + // Default completion threshold (overridden by user settings) + private readonly DEFAULT_COMPLETION_THRESHOLD = 80; // 80% private constructor() { - // Initialization happens in initialize method + // Initialize the cleanup interval for old stop calls + setInterval(() => this.cleanupOldStopCalls(), 5 * 60 * 1000); // Clean up every 5 minutes - // Cleanup old stop call records every 5 minutes - setInterval(() => { - this.cleanupOldStopCalls(); - }, 5 * 60 * 1000); + // Load user settings + this.loadCompletionThreshold(); } /** - * Cleanup old stop call records to prevent memory leaks + * Load user-configured completion threshold from AsyncStorage + */ + private async loadCompletionThreshold(): Promise { + try { + const thresholdStr = await AsyncStorage.getItem('@trakt_completion_threshold'); + if (thresholdStr) { + const threshold = parseInt(thresholdStr, 10); + if (!isNaN(threshold) && threshold >= 50 && threshold <= 100) { + logger.log(`[TraktService] Loaded user completion threshold: ${threshold}%`); + this.completionThreshold = threshold; + } + } + } catch (error) { + logger.error('[TraktService] Error loading completion threshold:', error); + } + } + + /** + * Get the current completion threshold (user-configured or default) + */ + private get completionThreshold(): number { + return this._completionThreshold || this.DEFAULT_COMPLETION_THRESHOLD; + } + + /** + * Set the completion threshold + */ + private set completionThreshold(value: number) { + this._completionThreshold = value; + } + + // Backing field for completion threshold + private _completionThreshold: number | null = null; + + /** + * Clean up old stop call records to prevent memory leaks */ private cleanupOldStopCalls(): void { const now = Date.now(); - const cutoff = now - (this.STOP_DEBOUNCE_MS * 2); // Keep records for 2x the debounce time + let cleanupCount = 0; + // Remove stop calls older than the debounce window for (const [key, timestamp] of this.lastStopCalls.entries()) { - if (timestamp < cutoff) { + if (now - timestamp > this.STOP_DEBOUNCE_MS) { this.lastStopCalls.delete(key); + cleanupCount++; } } - if (this.lastStopCalls.size > 0) { - logger.log(`[TraktService] Cleaned up old stop call records. Remaining: ${this.lastStopCalls.size}`); + // Also clean up old scrobbled timestamps + for (const [key, timestamp] of this.scrobbledTimestamps.entries()) { + if (now - timestamp > this.SCROBBLE_EXPIRY_MS) { + this.scrobbledTimestamps.delete(key); + this.scrobbledItems.delete(key); + cleanupCount++; + } + } + + // Clean up old sync times that haven't been updated in a while + for (const [key, timestamp] of this.lastSyncTimes.entries()) { + if (now - timestamp > 24 * 60 * 60 * 1000) { // 24 hours + this.lastSyncTimes.delete(key); + cleanupCount++; + } + } + + if (cleanupCount > 0) { + logger.log(`[TraktService] Cleaned up ${cleanupCount} old tracking entries`); } } @@ -1109,6 +1165,19 @@ export class TraktService { payload.show.ids.imdb = cleanShowImdbId; } + // Add episode IMDB ID if available (for specific episode IDs) + if (contentData.imdbId && contentData.imdbId !== contentData.showImdbId) { + const cleanEpisodeImdbId = contentData.imdbId.startsWith('tt') + ? contentData.imdbId.substring(2) + : contentData.imdbId; + + if (!payload.episode.ids) { + payload.episode.ids = {}; + } + + payload.episode.ids.imdb = cleanEpisodeImdbId; + } + logger.log('[TraktService] DEBUG episode payload:', JSON.stringify(payload, null, 2)); return payload; } @@ -1250,12 +1319,15 @@ export class TraktService { const now = Date.now(); + const watchingKey = this.getWatchingKey(contentData); + const lastSync = this.lastSyncTimes.get(watchingKey) || 0; + // Debounce API calls unless forced - if (!force && (now - this.lastSyncTime) < this.SYNC_DEBOUNCE_MS) { + if (!force && (now - lastSync) < this.SYNC_DEBOUNCE_MS) { return true; // Skip this sync, but return success } - this.lastSyncTime = now; + this.lastSyncTimes.set(watchingKey, now); const result = await this.queueRequest(async () => { return await this.pauseWatching(contentData, progress); @@ -1309,7 +1381,7 @@ export class TraktService { this.currentlyWatching.delete(watchingKey); // Mark as scrobbled if >= 80% to prevent future duplicates and restarts - if (progress >= 80) { + if (progress >= this.completionThreshold) { this.scrobbledItems.add(watchingKey); this.scrobbledTimestamps.set(watchingKey, Date.now()); logger.log(`[TraktService] Marked as scrobbled to prevent restarts: ${watchingKey}`); @@ -1317,7 +1389,7 @@ export class TraktService { // The stop endpoint automatically handles the 80%+ completion logic // and will mark as scrobbled if >= 80%, or pause if < 80% - const action = progress >= 80 ? 'scrobbled' : 'paused'; + const action = progress >= this.completionThreshold ? 'scrobbled' : 'paused'; logger.log(`[TraktService] Stopped watching ${contentData.type}: ${contentData.title} (${progress.toFixed(1)}% - ${action})`); return true;