Merge pull request #554 from chrisk325/patch-5

reduce trakt api calls and fix sort order for trakt continue watching
This commit is contained in:
Nayif 2026-02-28 23:52:39 +05:30 committed by GitHub
commit dc23330ce0
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 125 additions and 28 deletions

View file

@ -855,6 +855,36 @@ const ContinueWatchingSection = React.forwardRef<ContinueWatchingRef>((props, re
const traktBatch: ContinueWatchingItem[] = []; const traktBatch: ContinueWatchingItem[] = [];
// Pre-fetch watched shows so both Step 1 and Step 2 can use the watched episode sets
// This fixes "Up Next" suggesting already-watched episodes when the watched set is missing
let watchedShowsData: Awaited<ReturnType<typeof traktService.getWatchedShows>> = [];
// Map from showImdb -> Set of "imdb:season:episode" strings
const watchedEpisodeSetByShow = new Map<string, Set<string>>();
try {
watchedShowsData = await traktService.getWatchedShows();
for (const ws of watchedShowsData) {
if (!ws.show?.ids?.imdb) continue;
const imdb = ws.show.ids.imdb.startsWith('tt') ? ws.show.ids.imdb : `tt${ws.show.ids.imdb}`;
const resetAt = ws.reset_at ? new Date(ws.reset_at).getTime() : 0;
const episodeSet = new Set<string>();
if (ws.seasons) {
for (const season of ws.seasons) {
for (const episode of season.episodes) {
// Respect reset_at: skip episodes watched before the reset
if (resetAt > 0) {
const watchedAt = new Date(episode.last_watched_at).getTime();
if (watchedAt < resetAt) continue;
}
episodeSet.add(`${imdb}:${season.number}:${episode.number}`);
}
}
}
watchedEpisodeSetByShow.set(imdb, episodeSet);
}
} catch {
// Non-fatal — fall back to no watched set
}
// STEP 1: Process playback progress items (in-progress, paused) // STEP 1: Process playback progress items (in-progress, paused)
// These have actual progress percentage from Trakt // These have actual progress percentage from Trakt
const thirtyDaysAgo = Date.now() - (30 * 24 * 60 * 60 * 1000); const thirtyDaysAgo = Date.now() - (30 * 24 * 60 * 60 * 1000);
@ -918,11 +948,13 @@ const ContinueWatchingSection = React.forwardRef<ContinueWatchingRef>((props, re
if (item.progress >= 85) { if (item.progress >= 85) {
const metadata = cachedData.metadata; const metadata = cachedData.metadata;
if (metadata?.videos) { if (metadata?.videos) {
// Use pre-fetched watched set so already-watched episodes are skipped
const watchedSetForShow = watchedEpisodeSetByShow.get(showImdb);
const nextEpisode = findNextEpisode( const nextEpisode = findNextEpisode(
item.episode.season, item.episode.season,
item.episode.number, item.episode.number,
metadata.videos, metadata.videos,
undefined, // No watched set needed, findNextEpisode handles it watchedSetForShow,
showImdb showImdb
); );
@ -965,13 +997,13 @@ const ContinueWatchingSection = React.forwardRef<ContinueWatchingRef>((props, re
} }
} }
// STEP 2: Get watched shows and find "Up Next" episodes // STEP 2: Find "Up Next" episodes using pre-fetched watched shows data
// This handles cases where episodes are fully completed and removed from playback progress // Reuses watchedShowsData fetched before Step 1 — no extra API call
// Also respects reset_at (Bug 4 fix) and uses pre-built watched episode sets (Bug 3 fix)
try { try {
const watchedShows = await traktService.getWatchedShows();
const thirtyDaysAgoForShows = Date.now() - (30 * 24 * 60 * 60 * 1000); const thirtyDaysAgoForShows = Date.now() - (30 * 24 * 60 * 60 * 1000);
for (const watchedShow of watchedShows) { for (const watchedShow of watchedShowsData) {
try { try {
if (!watchedShow.show?.ids?.imdb) continue; if (!watchedShow.show?.ids?.imdb) continue;
@ -987,7 +1019,9 @@ const ContinueWatchingSection = React.forwardRef<ContinueWatchingRef>((props, re
const showKey = `series:${showImdb}`; const showKey = `series:${showImdb}`;
if (recentlyRemovedRef.current.has(showKey)) continue; if (recentlyRemovedRef.current.has(showKey)) continue;
// Find the last watched episode const resetAt = watchedShow.reset_at ? new Date(watchedShow.reset_at).getTime() : 0;
// Find the last watched episode (respecting reset_at)
let lastWatchedSeason = 0; let lastWatchedSeason = 0;
let lastWatchedEpisode = 0; let lastWatchedEpisode = 0;
let latestEpisodeTimestamp = 0; let latestEpisodeTimestamp = 0;
@ -996,6 +1030,8 @@ const ContinueWatchingSection = React.forwardRef<ContinueWatchingRef>((props, re
for (const season of watchedShow.seasons) { for (const season of watchedShow.seasons) {
for (const episode of season.episodes) { for (const episode of season.episodes) {
const episodeTimestamp = new Date(episode.last_watched_at).getTime(); const episodeTimestamp = new Date(episode.last_watched_at).getTime();
// Skip episodes watched before the user reset their progress
if (resetAt > 0 && episodeTimestamp < resetAt) continue;
if (episodeTimestamp > latestEpisodeTimestamp) { if (episodeTimestamp > latestEpisodeTimestamp) {
latestEpisodeTimestamp = episodeTimestamp; latestEpisodeTimestamp = episodeTimestamp;
lastWatchedSeason = season.number; lastWatchedSeason = season.number;
@ -1011,15 +1047,8 @@ const ContinueWatchingSection = React.forwardRef<ContinueWatchingRef>((props, re
const cachedData = await getCachedMetadata('series', showImdb); const cachedData = await getCachedMetadata('series', showImdb);
if (!cachedData?.basicContent || !cachedData?.metadata?.videos) continue; if (!cachedData?.basicContent || !cachedData?.metadata?.videos) continue;
// Build a set of watched episodes for this show // Use pre-built watched episode set (already respects reset_at)
const watchedEpisodeSet = new Set<string>(); const watchedEpisodeSet = watchedEpisodeSetByShow.get(showImdb) ?? new Set<string>();
if (watchedShow.seasons) {
for (const season of watchedShow.seasons) {
for (const episode of season.episodes) {
watchedEpisodeSet.add(`${showImdb}:${season.number}:${episode.number}`);
}
}
}
// Find the next unwatched episode // Find the next unwatched episode
const nextEpisode = findNextEpisode( const nextEpisode = findNextEpisode(
@ -1054,13 +1083,24 @@ const ContinueWatchingSection = React.forwardRef<ContinueWatchingRef>((props, re
// Trakt mode: show ONLY Trakt items, but override progress with local if local is higher. // Trakt mode: show ONLY Trakt items, but override progress with local if local is higher.
if (traktBatch.length > 0) { if (traktBatch.length > 0) {
// Dedupe (keep most recent per show/movie) // Dedupe (keep in-progress over "Up Next"; then prefer most recent)
const deduped = new Map<string, ContinueWatchingItem>(); const deduped = new Map<string, ContinueWatchingItem>();
for (const item of traktBatch) { for (const item of traktBatch) {
const key = `${item.type}:${item.id}`; const key = `${item.type}:${item.id}`;
const existing = deduped.get(key); const existing = deduped.get(key);
if (!existing || (item.lastUpdated ?? 0) > (existing.lastUpdated ?? 0)) { if (!existing) {
deduped.set(key, item); deduped.set(key, item);
} else {
const existingHasProgress = (existing.progress ?? 0) > 0;
const candidateHasProgress = (item.progress ?? 0) > 0;
if (candidateHasProgress && !existingHasProgress) {
// Always prefer actual in-progress over "Up Next" placeholder
deduped.set(key, item);
} else if (!candidateHasProgress && existingHasProgress) {
// Keep existing in-progress item
} else if ((item.lastUpdated ?? 0) > (existing.lastUpdated ?? 0)) {
deduped.set(key, item);
}
} }
} }
@ -2223,14 +2263,7 @@ const ContinueWatchingSection = React.forwardRef<ContinueWatchingRef>((props, re
</View> </View>
<FlatList <FlatList
data={[...continueWatchingItems].sort((a, b) => { data={continueWatchingItems}
const aProgress = a.progress ?? 0;
const bProgress = b.progress ?? 0;
const aIsUpNext = a.type === 'series' && aProgress <= 0;
const bIsUpNext = b.type === 'series' && bProgress <= 0;
if (aIsUpNext !== bIsUpNext) return aIsUpNext ? 1 : -1;
return (b.lastUpdated ?? 0) - (a.lastUpdated ?? 0);
})}
renderItem={renderContinueWatchingItem} renderItem={renderContinueWatchingItem}
keyExtractor={keyExtractor} keyExtractor={keyExtractor}
horizontal horizontal

View file

@ -68,7 +68,38 @@ export const useCalendarData = (): UseCalendarDataReturn => {
); );
if (cachedData) { if (cachedData) {
setCalendarData(cachedData); // Apply watched filter even on cached data
if (traktAuthenticated && watchedShows && watchedShows.length > 0) {
const cachedWatchedSet = new Set<string>();
for (const ws of watchedShows) {
const imdb = ws.show?.ids?.imdb;
if (!imdb) continue;
const showId = imdb.startsWith('tt') ? imdb : `tt${imdb}`;
const resetAt = ws.reset_at ? new Date(ws.reset_at).getTime() : 0;
if (ws.seasons) {
for (const season of ws.seasons) {
for (const episode of season.episodes) {
if (resetAt > 0 && new Date(episode.last_watched_at).getTime() < resetAt) continue;
cachedWatchedSet.add(`${showId}:${season.number}:${episode.number}`);
}
}
}
}
const filtered = cachedData.map(section => {
if (section.title !== 'This Week') return section;
return {
...section,
data: section.data.filter((ep: any) => {
const showId = ep.seriesId?.startsWith('tt') ? ep.seriesId : `tt${ep.seriesId}`;
return !cachedWatchedSet.has(`${showId}:${ep.season}:${ep.episode}`) &&
!cachedWatchedSet.has(`${ep.seriesId}:${ep.season}:${ep.episode}`);
})
};
});
setCalendarData(filtered);
} else {
setCalendarData(cachedData);
}
setLoading(false); setLoading(false);
return; return;
} }
@ -314,6 +345,29 @@ export const useCalendarData = (): UseCalendarDataReturn => {
logger.log(`[CalendarData] Total episodes fetched: ${allEpisodes.length}`); logger.log(`[CalendarData] Total episodes fetched: ${allEpisodes.length}`);
// Build a set of watched episodes from Trakt so we can filter them out of This Week
const watchedEpisodeSet = new Set<string>();
if (traktAuthenticated && watchedShows) {
for (const ws of watchedShows) {
const imdb = ws.show?.ids?.imdb;
if (!imdb) continue;
const showId = imdb.startsWith('tt') ? imdb : `tt${imdb}`;
const resetAt = ws.reset_at ? new Date(ws.reset_at).getTime() : 0;
if (ws.seasons) {
for (const season of ws.seasons) {
for (const episode of season.episodes) {
// Respect reset_at
if (resetAt > 0) {
const watchedAt = new Date(episode.last_watched_at).getTime();
if (watchedAt < resetAt) continue;
}
watchedEpisodeSet.add(`${showId}:${season.number}:${episode.number}`);
}
}
}
}
}
// Use memory-efficient filtering with error handling // Use memory-efficient filtering with error handling
const thisWeekEpisodes = await memoryManager.filterLargeArray( const thisWeekEpisodes = await memoryManager.filterLargeArray(
allEpisodes, allEpisodes,
@ -321,8 +375,18 @@ export const useCalendarData = (): UseCalendarDataReturn => {
try { try {
if (!ep.releaseDate) return false; if (!ep.releaseDate) return false;
const parsed = parseISO(ep.releaseDate); const parsed = parseISO(ep.releaseDate);
// Show all episodes for this week, including released ones if (!isThisWeek(parsed)) return false;
return isThisWeek(parsed); // Filter out already-watched episodes when Trakt is authenticated
if (traktAuthenticated && watchedEpisodeSet.size > 0) {
const showId = ep.seriesId?.startsWith('tt') ? ep.seriesId : `tt${ep.seriesId}`;
if (
watchedEpisodeSet.has(`${showId}:${ep.season}:${ep.episode}`) ||
watchedEpisodeSet.has(`${ep.seriesId}:${ep.season}:${ep.episode}`)
) {
return false;
}
}
return true;
} catch (error) { } catch (error) {
logger.warn(`[CalendarData] Error parsing date for this week filtering: ${ep.releaseDate}`, error); logger.warn(`[CalendarData] Error parsing date for this week filtering: ${ep.releaseDate}`, error);
return false; return false;