mirror of
https://github.com/tapframe/NuvioStreaming.git
synced 2026-03-11 17:45:38 +00:00
trakt scrobble improvements
This commit is contained in:
parent
78553d8323
commit
48300bf767
4 changed files with 130 additions and 18 deletions
|
|
@ -604,7 +604,7 @@ const ContinueWatchingSection = React.forwardRef<ContinueWatchingRef>((props, re
|
|||
}
|
||||
});
|
||||
|
||||
// TRÅKT: fetch history and merge incrementally as well
|
||||
// TRAKT: fetch playback progress (in-progress items) and history, merge incrementally
|
||||
const traktMergePromise = (async () => {
|
||||
try {
|
||||
const traktService = TraktService.getInstance();
|
||||
|
|
@ -619,28 +619,132 @@ const ContinueWatchingSection = React.forwardRef<ContinueWatchingRef>((props, re
|
|||
}
|
||||
|
||||
lastTraktSyncRef.current = now;
|
||||
const historyItems = await traktService.getWatchedEpisodesHistory(1, 200);
|
||||
|
||||
// Fetch both playback progress (paused items) and watch history in parallel
|
||||
const [playbackItems, historyItems, watchedShows] = await Promise.all([
|
||||
traktService.getPlaybackProgress(), // Items with actual progress %
|
||||
traktService.getWatchedEpisodesHistory(1, 200), // Completed episodes
|
||||
traktService.getWatchedShows(), // For reset_at handling
|
||||
]);
|
||||
|
||||
// Build a map of shows with reset_at for re-watching support
|
||||
const showResetMap: Record<string, number> = {};
|
||||
for (const show of watchedShows) {
|
||||
if (show.show?.ids?.imdb && show.reset_at) {
|
||||
const imdbId = show.show.ids.imdb.startsWith('tt')
|
||||
? show.show.ids.imdb
|
||||
: `tt${show.show.ids.imdb}`;
|
||||
showResetMap[imdbId] = new Date(show.reset_at).getTime();
|
||||
}
|
||||
}
|
||||
|
||||
const traktBatch: ContinueWatchingItem[] = [];
|
||||
const processedShows = new Set<string>(); // Track which shows we've added
|
||||
|
||||
// STEP 1: Process playback progress items (in-progress, paused)
|
||||
// These have actual progress percentage from Trakt
|
||||
for (const item of playbackItems) {
|
||||
try {
|
||||
// Skip items with very low or very high progress
|
||||
if (item.progress <= 0 || item.progress >= 85) continue;
|
||||
|
||||
if (item.type === 'movie' && item.movie?.ids?.imdb) {
|
||||
const imdbId = item.movie.ids.imdb.startsWith('tt')
|
||||
? item.movie.ids.imdb
|
||||
: `tt${item.movie.ids.imdb}`;
|
||||
|
||||
// Check if recently removed
|
||||
const movieKey = `movie:${imdbId}`;
|
||||
if (recentlyRemovedRef.current.has(movieKey)) continue;
|
||||
|
||||
const cachedData = await getCachedMetadata('movie', imdbId);
|
||||
if (!cachedData?.basicContent) continue;
|
||||
|
||||
const pausedAt = new Date(item.paused_at).getTime();
|
||||
traktBatch.push({
|
||||
...cachedData.basicContent,
|
||||
id: imdbId,
|
||||
type: 'movie',
|
||||
progress: item.progress,
|
||||
lastUpdated: pausedAt,
|
||||
} as ContinueWatchingItem);
|
||||
|
||||
logger.log(`📺 [TraktPlayback] Adding movie ${item.movie.title} with ${item.progress.toFixed(1)}% progress`);
|
||||
|
||||
} else if (item.type === 'episode' && item.show?.ids?.imdb && item.episode) {
|
||||
const showImdb = item.show.ids.imdb.startsWith('tt')
|
||||
? item.show.ids.imdb
|
||||
: `tt${item.show.ids.imdb}`;
|
||||
|
||||
// Check if recently removed
|
||||
const showKey = `series:${showImdb}`;
|
||||
if (recentlyRemovedRef.current.has(showKey)) continue;
|
||||
|
||||
// Check reset_at - skip if this was paused before re-watch started
|
||||
const resetTime = showResetMap[showImdb];
|
||||
const pausedAt = new Date(item.paused_at).getTime();
|
||||
if (resetTime && pausedAt < resetTime) {
|
||||
logger.log(`🔄 [TraktPlayback] Skipping ${showImdb} S${item.episode.season}E${item.episode.number} - paused before reset_at`);
|
||||
continue;
|
||||
}
|
||||
|
||||
const cachedData = await getCachedMetadata('series', showImdb);
|
||||
if (!cachedData?.basicContent) continue;
|
||||
|
||||
traktBatch.push({
|
||||
...cachedData.basicContent,
|
||||
id: showImdb,
|
||||
type: 'series',
|
||||
progress: item.progress,
|
||||
lastUpdated: pausedAt,
|
||||
season: item.episode.season,
|
||||
episode: item.episode.number,
|
||||
episodeTitle: item.episode.title || `Episode ${item.episode.number}`,
|
||||
} as ContinueWatchingItem);
|
||||
|
||||
processedShows.add(showImdb);
|
||||
logger.log(`📺 [TraktPlayback] Adding ${item.show.title} S${item.episode.season}E${item.episode.number} with ${item.progress.toFixed(1)}% progress`);
|
||||
}
|
||||
} catch (err) {
|
||||
// Continue with other items
|
||||
}
|
||||
}
|
||||
|
||||
// STEP 2: Process watch history for shows NOT in playback progress
|
||||
// Find the next episode for completed shows
|
||||
const latestWatchedByShow: Record<string, { season: number; episode: number; watchedAt: number }> = {};
|
||||
for (const item of historyItems) {
|
||||
if (item.type !== 'episode') continue;
|
||||
const showImdb = item.show?.ids?.imdb ? `tt${item.show.ids.imdb.replace(/^tt/, '')}` : null;
|
||||
const showImdb = item.show?.ids?.imdb
|
||||
? (item.show.ids.imdb.startsWith('tt') ? item.show.ids.imdb : `tt${item.show.ids.imdb}`)
|
||||
: null;
|
||||
if (!showImdb) continue;
|
||||
|
||||
// Skip if we already have an in-progress episode for this show
|
||||
if (processedShows.has(showImdb)) continue;
|
||||
|
||||
const season = item.episode?.season;
|
||||
const epNum = item.episode?.number;
|
||||
if (season === undefined || epNum === undefined) continue;
|
||||
|
||||
const watchedAt = new Date(item.watched_at).getTime();
|
||||
|
||||
// Check reset_at - skip episodes watched before re-watch started
|
||||
const resetTime = showResetMap[showImdb];
|
||||
if (resetTime && watchedAt < resetTime) {
|
||||
continue; // This was watched in a previous viewing
|
||||
}
|
||||
|
||||
const existing = latestWatchedByShow[showImdb];
|
||||
if (!existing || existing.watchedAt < watchedAt) {
|
||||
latestWatchedByShow[showImdb] = { season, episode: epNum, watchedAt };
|
||||
}
|
||||
}
|
||||
|
||||
// Collect all valid Trakt items first, then merge as a batch
|
||||
const traktBatch: ContinueWatchingItem[] = [];
|
||||
|
||||
// Add next episodes for completed shows
|
||||
for (const [showId, info] of Object.entries(latestWatchedByShow)) {
|
||||
try {
|
||||
// Check if this show was recently removed by the user
|
||||
// Check if this show was recently removed
|
||||
const showKey = `series:${showId}`;
|
||||
if (recentlyRemovedRef.current.has(showKey)) {
|
||||
logger.log(`🚫 [TraktSync] Skipping recently removed show: ${showKey}`);
|
||||
|
|
@ -659,7 +763,7 @@ const ContinueWatchingSection = React.forwardRef<ContinueWatchingRef>((props, re
|
|||
...basicContent,
|
||||
id: showId,
|
||||
type: 'series',
|
||||
progress: 0,
|
||||
progress: 0, // Next episode, not started
|
||||
lastUpdated: info.watchedAt,
|
||||
season: nextEpisodeVideo.season,
|
||||
episode: nextEpisodeVideo.episode,
|
||||
|
|
@ -668,13 +772,12 @@ const ContinueWatchingSection = React.forwardRef<ContinueWatchingRef>((props, re
|
|||
}
|
||||
}
|
||||
|
||||
// Persist "watched" progress for the episode that Trakt reported (only if not recently removed)
|
||||
// Persist "watched" progress for the episode that Trakt reported
|
||||
if (!recentlyRemovedRef.current.has(showKey)) {
|
||||
const watchedEpisodeId = `${showId}:${info.season}:${info.episode}`;
|
||||
const existingProgress = allProgress[`series:${showId}:${watchedEpisodeId}`];
|
||||
const existingPercent = existingProgress ? (existingProgress.currentTime / existingProgress.duration) * 100 : 0;
|
||||
if (!existingProgress || existingPercent < 85) {
|
||||
logger.log(`💾 [TraktSync] Adding local progress for ${showId}: S${info.season}E${info.episode}`);
|
||||
await storageService.setWatchProgress(
|
||||
showId,
|
||||
'series',
|
||||
|
|
@ -688,20 +791,19 @@ const ContinueWatchingSection = React.forwardRef<ContinueWatchingRef>((props, re
|
|||
`${info.season}:${info.episode}`
|
||||
);
|
||||
}
|
||||
} else {
|
||||
logger.log(`🚫 [TraktSync] Skipping local progress for recently removed show: ${showKey}`);
|
||||
}
|
||||
} catch (err) {
|
||||
// Continue with other shows even if one fails
|
||||
// Continue with other shows
|
||||
}
|
||||
}
|
||||
|
||||
// Merge all Trakt items as a single batch to ensure proper sorting
|
||||
if (traktBatch.length > 0) {
|
||||
logger.log(`📋 [TraktSync] Merging ${traktBatch.length} items from Trakt (playback + history)`);
|
||||
await mergeBatchIntoState(traktBatch);
|
||||
}
|
||||
} catch (err) {
|
||||
// Continue even if Trakt history merge fails
|
||||
logger.error('[TraktSync] Error in Trakt merge:', err);
|
||||
}
|
||||
})();
|
||||
|
||||
|
|
|
|||
|
|
@ -1821,7 +1821,7 @@ const AndroidVideoPlayer: React.FC = () => {
|
|||
const backgroundSync = async () => {
|
||||
try {
|
||||
logger.log('[AndroidVideoPlayer] Starting background Trakt sync');
|
||||
// IMMEDIATE: Force immediate progress update (scrobble/pause) with the exact time
|
||||
// IMMEDIATE: Force immediate progress update (uses scrobble/stop which handles pause/scrobble)
|
||||
await traktAutosync.handleProgressUpdate(actualCurrentTime, duration, true);
|
||||
|
||||
// IMMEDIATE: Use user_close reason to trigger immediate scrobble stop
|
||||
|
|
|
|||
|
|
@ -1432,7 +1432,7 @@ const KSPlayerCore: React.FC = () => {
|
|||
const backgroundSync = async () => {
|
||||
try {
|
||||
logger.log('[VideoPlayer] Starting background Trakt sync');
|
||||
// IMMEDIATE: Force immediate progress update (scrobble/pause) with the exact time
|
||||
// IMMEDIATE: Force immediate progress update (uses scrobble/stop which handles pause/scrobble)
|
||||
await traktAutosync.handleProgressUpdate(actualCurrentTime, duration, true);
|
||||
|
||||
// IMMEDIATE: Use user_close reason to trigger immediate scrobble stop
|
||||
|
|
|
|||
|
|
@ -52,6 +52,8 @@ export interface TraktWatchedItem {
|
|||
};
|
||||
plays: number;
|
||||
last_watched_at: string;
|
||||
last_updated_at?: string; // Timestamp for syncing - only re-process if newer
|
||||
reset_at?: string | null; // When user started re-watching - ignore episodes watched before this
|
||||
seasons?: {
|
||||
number: number;
|
||||
episodes: {
|
||||
|
|
@ -1637,7 +1639,14 @@ export class TraktService {
|
|||
}
|
||||
|
||||
/**
|
||||
* Pause watching content (scrobble pause)
|
||||
* Pause watching content - saves playback progress
|
||||
*
|
||||
* NOTE: Trakt API does NOT have a /scrobble/pause endpoint.
|
||||
* Instead, /scrobble/stop handles both cases:
|
||||
* - Progress 1-79%: Treated as "pause", saves playback progress to /sync/playback
|
||||
* - Progress ≥80%: Treated as "scrobble", marks as watched
|
||||
*
|
||||
* This method uses /scrobble/stop which automatically handles the pause/scrobble logic.
|
||||
*/
|
||||
public async pauseWatching(contentData: TraktContentData, progress: number): Promise<TraktScrobbleResponse | null> {
|
||||
try {
|
||||
|
|
@ -1653,7 +1662,8 @@ export class TraktService {
|
|||
return null;
|
||||
}
|
||||
|
||||
return this.apiRequest<TraktScrobbleResponse>('/scrobble/pause', 'POST', payload);
|
||||
// Use /scrobble/stop - Trakt automatically treats <80% as pause, ≥80% as scrobble
|
||||
return this.apiRequest<TraktScrobbleResponse>('/scrobble/stop', 'POST', payload);
|
||||
} catch (error) {
|
||||
logger.error('[TraktService] Failed to pause watching:', error);
|
||||
return null;
|
||||
|
|
|
|||
Loading…
Reference in a new issue