Merge pull request #350 from chrisk325/patch-6

Complete fix for trakt up next thanx to @oceanm8 on discord for the idea
This commit is contained in:
Nayif 2026-01-03 22:05:06 +05:30 committed by GitHub
commit b42401a909
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 170 additions and 65 deletions

View file

@ -227,21 +227,34 @@ const ContinueWatchingSection = React.forwardRef<ContinueWatchingRef>((props, re
try { try {
const shouldFetchMeta = await stremioService.isValidContentId(type, id); const shouldFetchMeta = await stremioService.isValidContentId(type, id);
const [metadata, basicContent, addonContent] = await Promise.all([ const [metadata, basicContent, addonSpecificMeta] = await Promise.all([
shouldFetchMeta ? stremioService.getMetaDetails(type, id) : Promise.resolve(null), shouldFetchMeta ? stremioService.getMetaDetails(type, id) : Promise.resolve(null),
catalogService.getBasicContentDetails(type, id), catalogService.getBasicContentDetails(type, id),
addonId ? stremioService.getMetaDetails(type, id, addonId).catch(() => null) : Promise.resolve(null)
addonId
? stremioService.getMetaDetails(type, id, addonId).catch(() => null)
: Promise.resolve(null)
]); ]);
const preferredAddonMeta = addonSpecificMeta || metadata;
const finalContent = basicContent ? { const finalContent = basicContent ? {
...basicContent, ...basicContent,
...(addonContent?.name && { name: addonContent.name }), ...(preferredAddonMeta?.name && { name: preferredAddonMeta.name }),
...(addonContent?.poster && { poster: addonContent.poster }), ...(preferredAddonMeta?.poster && { poster: preferredAddonMeta.poster }),
...(addonContent?.description && { description: addonContent.description }), ...(preferredAddonMeta?.description && { description: preferredAddonMeta.description }),
} : null; } : null;
if (finalContent) { if (finalContent) {
const result = { metadata, basicContent: finalContent, addonContent, timestamp: now }; const result = {
metadata,
basicContent: finalContent,
addonContent: preferredAddonMeta,
timestamp: now
};
metadataCache.current[cacheKey] = result; metadataCache.current[cacheKey] = result;
return result; return result;
} }
@ -500,23 +513,46 @@ const ContinueWatchingSection = React.forwardRef<ContinueWatchingRef>((props, re
} }
} }
if (currentSeason !== undefined && currentEpisode !== undefined && metadata?.videos) { if (currentSeason !== undefined && currentEpisode !== undefined) {
const nextEpisodeVideo = findNextEpisode(currentSeason, currentEpisode, metadata.videos); const traktService = TraktService.getInstance();
let nextEpisode: any = null;
if (nextEpisodeVideo) { try {
const isAuthed = await traktService.isAuthenticated();
if (isAuthed && typeof (traktService as any).getShowWatchedProgress === 'function') {
const showProgress = await (traktService as any).getShowWatchedProgress(group.id);
if (showProgress && !showProgress.completed && showProgress.next_episode) {
nextEpisode = showProgress.next_episode;
}
}
} catch {
}
if (!nextEpisode && metadata?.videos) {
nextEpisode = findNextEpisode(
currentSeason,
currentEpisode,
metadata.videos
);
}
if (nextEpisode) {
batch.push({ batch.push({
...basicContent, ...basicContent,
id: group.id, id: group.id,
type: group.type, type: group.type,
progress: 0, progress: 0,
lastUpdated: progress.lastUpdated, lastUpdated: progress.lastUpdated,
season: nextEpisodeVideo.season, season: nextEpisode.season,
episode: nextEpisodeVideo.episode, episode: nextEpisode.number ?? nextEpisode.episode,
episodeTitle: `Episode ${nextEpisodeVideo.episode}`, episodeTitle: nextEpisode.title || `Episode ${nextEpisode.number ?? nextEpisode.episode}`,
addonId: progress.addonId, addonId: progress.addonId,
} as ContinueWatchingItem); } as ContinueWatchingItem);
} }
} }
} }
continue; continue;
} }
@ -769,23 +805,35 @@ const ContinueWatchingSection = React.forwardRef<ContinueWatchingRef>((props, re
if (!cachedData?.basicContent) continue; if (!cachedData?.basicContent) continue;
const { metadata, basicContent } = cachedData; const { metadata, basicContent } = cachedData;
if (metadata?.videos) { const traktService = TraktService.getInstance();
const nextEpisodeVideo = findNextEpisode(info.season, info.episode, metadata.videos); let showProgress: any = null;
if (nextEpisodeVideo) {
logger.log(` [TraktSync] Adding next episode for ${showId}: S${nextEpisodeVideo.season}E${nextEpisodeVideo.episode}`); try {
showProgress = await (traktService as any).getShowWatchedProgress?.(showId);
} catch {
showProgress = null;
}
if (!showProgress || showProgress.completed || !showProgress.next_episode) {
logger.log(`🚫 [TraktSync] Skipping completed show: ${showId}`);
continue;
}
const nextEp = showProgress.next_episode;
logger.log(` [TraktSync] Adding next episode for ${showId}: S${nextEp.season}E${nextEp.number}`);
traktBatch.push({ traktBatch.push({
...basicContent, ...basicContent,
id: showId, id: showId,
type: 'series', type: 'series',
progress: 0, // Next episode, not started progress: 0,
lastUpdated: info.watchedAt, lastUpdated: info.watchedAt,
season: nextEpisodeVideo.season, season: nextEp.season,
episode: nextEpisodeVideo.episode, episode: nextEp.number,
episodeTitle: `Episode ${nextEpisodeVideo.episode}`, episodeTitle: nextEp.title || `Episode ${nextEp.number}`,
addonId: undefined, addonId: undefined,
} as ContinueWatchingItem); } as ContinueWatchingItem);
}
}
// Persist "watched" progress for the episode that Trakt reported // Persist "watched" progress for the episode that Trakt reported
if (!recentlyRemovedRef.current.has(showKey)) { if (!recentlyRemovedRef.current.has(showKey)) {

View file

@ -1099,6 +1099,55 @@ export class TraktService {
return this.apiRequest<TraktWatchedItem[]>('/sync/watched/shows'); return this.apiRequest<TraktWatchedItem[]>('/sync/watched/shows');
} }
public async isMovieWatchedAccurate(imdbId: string): Promise<boolean> {
try {
const history = await this.client.get(
`/sync/history/movies/${imdbId}?limit=1`
);
const history = response.data;
return Array.isArray(history) && history.length > 0;
} catch {
return false;
}
}
public async isEpisodeWatchedAccurate(
showId: string,
season: number,
episode: number
): Promise<boolean> {
try {
const history = await this.client.get(
`/sync/history/episodes/${showId}`,
{ params: { limit: 20 } }
);
const history = response.data;
if (!Array.isArray(history)) return false;
for (const entry of history) {
if (
entry.episode?.season === season &&
entry.episode?.number === episode
) {
if (entry.reset_at) {
const watchedAt = new Date(entry.watched_at).getTime();
const resetAt = new Date(entry.reset_at).getTime();
if (watchedAt < resetAt) return false;
}
return true;
}
}
return false;
} catch {
return false;
}
}
/** /**
* Get the user's watchlist movies * Get the user's watchlist movies
*/ */

View file

@ -302,47 +302,55 @@ class WatchedService {
*/ */
public async isMovieWatched(imdbId: string): Promise<boolean> { public async isMovieWatched(imdbId: string): Promise<boolean> {
try { try {
// First check local watched flag const isAuthed = await this.traktService.isAuthenticated();
const localWatched = await mmkvStorage.getItem(`watched:movie:${imdbId}`);
if (localWatched === 'true') { if (isAuthed) {
return true; const traktWatched =
await this.traktService.isMovieWatchedAccurate(imdbId);
if (traktWatched) return true;
} }
// Check local progress const local = await mmkvStorage.getItem(`watched:movie:${imdbId}`);
const progress = await storageService.getWatchProgress(imdbId, 'movie'); return local === 'true';
if (progress) { } catch {
const progressPercent = (progress.currentTime / progress.duration) * 100;
if (progressPercent >= 85) {
return true;
}
}
return false;
} catch (error) {
logger.error('[WatchedService] Error checking movie watched status:', error);
return false; return false;
} }
} }
/** /**
* Check if an episode is marked as watched (locally) * Check if an episode is marked as watched (locally)
*/ */
public async isEpisodeWatched(showId: string, season: number, episode: number): Promise<boolean> { public async isEpisodeWatched(
showId: string,
season: number,
episode: number
): Promise<boolean> {
try { try {
const isAuthed = await this.traktService.isAuthenticated();
if (isAuthed) {
const traktWatched =
await this.traktService.isEpisodeWatchedAccurate(
showId,
season,
episode
);
if (traktWatched) return true;
}
const episodeId = `${showId}:${season}:${episode}`; const episodeId = `${showId}:${season}:${episode}`;
const progress = await storageService.getWatchProgress(
showId,
'series',
episodeId
);
// Check local progress if (!progress) return false;
const progress = await storageService.getWatchProgress(showId, 'series', episodeId);
if (progress) {
const progressPercent = (progress.currentTime / progress.duration) * 100;
if (progressPercent >= 85) {
return true;
}
}
return false; const pct = (progress.currentTime / progress.duration) * 100;
} catch (error) { return pct >= 99;
logger.error('[WatchedService] Error checking episode watched status:', error); } catch {
return false; return false;
} }
} }