mirror of
https://github.com/tapframe/NuvioStreaming.git
synced 2026-04-22 17:22:03 +00:00
Changed Trakt Continue watch Sync Behaviour. now fetches directly from api when authenticated and doesn't merges to local storage.
This commit is contained in:
parent
3e63efc178
commit
2835ede747
2 changed files with 235 additions and 400 deletions
|
|
@ -10,7 +10,7 @@ import {
|
||||||
ActivityIndicator,
|
ActivityIndicator,
|
||||||
Platform
|
Platform
|
||||||
} from 'react-native';
|
} from 'react-native';
|
||||||
import { FlashList } from '@shopify/flash-list';
|
import { FlatList } from 'react-native';
|
||||||
import Animated, { FadeIn, Layout } from 'react-native-reanimated';
|
import Animated, { FadeIn, Layout } from 'react-native-reanimated';
|
||||||
import { useNavigation, useFocusEffect } from '@react-navigation/native';
|
import { useNavigation, useFocusEffect } from '@react-navigation/native';
|
||||||
import { NavigationProp } from '@react-navigation/native';
|
import { NavigationProp } from '@react-navigation/native';
|
||||||
|
|
@ -363,300 +363,237 @@ const ContinueWatchingSection = React.forwardRef<ContinueWatchingRef>((props, re
|
||||||
};
|
};
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const allProgress = await storageService.getAllWatchProgress();
|
// If Trakt is authenticated, skip local storage and only use Trakt playback
|
||||||
if (Object.keys(allProgress).length === 0) {
|
const traktService = TraktService.getInstance();
|
||||||
setContinueWatchingItems([]);
|
const isTraktAuthed = await traktService.isAuthenticated();
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Group progress items by content ID
|
// Declare groupPromises outside the if/else block
|
||||||
const contentGroups: Record<string, { type: string; id: string; episodes: Array<{ key: string; episodeId?: string; progress: any; progressPercent: number }> }> = {};
|
let groupPromises: Promise<void>[] = [];
|
||||||
for (const key in allProgress) {
|
|
||||||
const keyParts = key.split(':');
|
|
||||||
const [type, id, ...episodeIdParts] = keyParts;
|
|
||||||
const episodeId = episodeIdParts.length > 0 ? episodeIdParts.join(':') : undefined;
|
|
||||||
const progress = allProgress[key];
|
|
||||||
const progressPercent =
|
|
||||||
progress.duration > 0
|
|
||||||
? (progress.currentTime / progress.duration) * 100
|
|
||||||
: 0;
|
|
||||||
// Skip fully watched movies
|
|
||||||
if (type === 'movie' && progressPercent >= 85) continue;
|
|
||||||
// Skip movies with no actual progress (ensure > 0%)
|
|
||||||
if (type === 'movie' && (!isFinite(progressPercent) || progressPercent <= 0)) continue;
|
|
||||||
const contentKey = `${type}:${id}`;
|
|
||||||
if (!contentGroups[contentKey]) contentGroups[contentKey] = { type, id, episodes: [] };
|
|
||||||
contentGroups[contentKey].episodes.push({ key, episodeId, progress, progressPercent });
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fetch Trakt watched movies once and reuse
|
if (isTraktAuthed) {
|
||||||
const traktMoviesSetPromise = (async () => {
|
// Just skip local storage - Trakt will populate with correct timestamps
|
||||||
try {
|
// Don't clear existing items to avoid flicker
|
||||||
const traktService = TraktService.getInstance();
|
} else {
|
||||||
const isAuthed = await traktService.isAuthenticated();
|
// Non-Trakt: use local storage
|
||||||
if (!isAuthed) return new Set<string>();
|
const allProgress = await storageService.getAllWatchProgress();
|
||||||
if (typeof (traktService as any).getWatchedMovies === 'function') {
|
if (Object.keys(allProgress).length === 0) {
|
||||||
const watched = await (traktService as any).getWatchedMovies();
|
setContinueWatchingItems([]);
|
||||||
const watchedSet = new Set<string>();
|
return;
|
||||||
|
|
||||||
if (Array.isArray(watched)) {
|
|
||||||
watched.forEach((w: any) => {
|
|
||||||
const ids = w?.movie?.ids;
|
|
||||||
if (!ids) return;
|
|
||||||
|
|
||||||
if (ids.imdb) {
|
|
||||||
const imdb = ids.imdb;
|
|
||||||
watchedSet.add(imdb.startsWith('tt') ? imdb : `tt${imdb}`);
|
|
||||||
}
|
|
||||||
if (ids.tmdb) {
|
|
||||||
watchedSet.add(ids.tmdb.toString());
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
return watchedSet;
|
|
||||||
}
|
|
||||||
return new Set<string>();
|
|
||||||
} catch {
|
|
||||||
return new Set<string>();
|
|
||||||
}
|
}
|
||||||
})();
|
|
||||||
|
|
||||||
// Fetch Trakt watched shows once and reuse
|
// Group progress items by content ID
|
||||||
const traktShowsSetPromise = (async () => {
|
const contentGroups: Record<string, { type: string; id: string; episodes: Array<{ key: string; episodeId?: string; progress: any; progressPercent: number }> }> = {};
|
||||||
try {
|
for (const key in allProgress) {
|
||||||
const traktService = TraktService.getInstance();
|
const keyParts = key.split(':');
|
||||||
const isAuthed = await traktService.isAuthenticated();
|
const [type, id, ...episodeIdParts] = keyParts;
|
||||||
if (!isAuthed) return new Set<string>();
|
const episodeId = episodeIdParts.length > 0 ? episodeIdParts.join(':') : undefined;
|
||||||
|
const progress = allProgress[key];
|
||||||
if (typeof (traktService as any).getWatchedShows === 'function') {
|
const progressPercent =
|
||||||
const watched = await (traktService as any).getWatchedShows();
|
progress.duration > 0
|
||||||
const watchedSet = new Set<string>();
|
? (progress.currentTime / progress.duration) * 100
|
||||||
|
: 0;
|
||||||
if (Array.isArray(watched)) {
|
// Skip fully watched movies
|
||||||
watched.forEach((show: any) => {
|
if (type === 'movie' && progressPercent >= 85) continue;
|
||||||
const ids = show?.show?.ids;
|
// Skip movies with no actual progress (ensure > 0%)
|
||||||
if (!ids) return;
|
if (type === 'movie' && (!isFinite(progressPercent) || progressPercent <= 0)) continue;
|
||||||
|
const contentKey = `${type}:${id}`;
|
||||||
const imdbId = ids.imdb;
|
if (!contentGroups[contentKey]) contentGroups[contentKey] = { type, id, episodes: [] };
|
||||||
const tmdbId = ids.tmdb;
|
contentGroups[contentKey].episodes.push({ key, episodeId, progress, progressPercent });
|
||||||
|
|
||||||
if (show.seasons && Array.isArray(show.seasons)) {
|
|
||||||
show.seasons.forEach((season: any) => {
|
|
||||||
if (season.episodes && Array.isArray(season.episodes)) {
|
|
||||||
season.episodes.forEach((episode: any) => {
|
|
||||||
if (imdbId) {
|
|
||||||
const cleanImdbId = imdbId.startsWith('tt') ? imdbId : `tt${imdbId}`;
|
|
||||||
watchedSet.add(`${cleanImdbId}:${season.number}:${episode.number}`);
|
|
||||||
}
|
|
||||||
if (tmdbId) {
|
|
||||||
watchedSet.add(`${tmdbId}:${season.number}:${episode.number}`);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
return watchedSet;
|
|
||||||
}
|
|
||||||
return new Set<string>();
|
|
||||||
} catch {
|
|
||||||
return new Set<string>();
|
|
||||||
}
|
}
|
||||||
})();
|
|
||||||
|
|
||||||
// Process each content group concurrently, merging results as they arrive
|
// Fetch Trakt watched movies once and reuse
|
||||||
const groupPromises = Object.values(contentGroups).map(async (group) => {
|
const traktMoviesSetPromise = (async () => {
|
||||||
try {
|
try {
|
||||||
if (!isSupportedId(group.id)) return;
|
const traktService = TraktService.getInstance();
|
||||||
// Skip movies that are already watched on Trakt
|
const isAuthed = await traktService.isAuthenticated();
|
||||||
if (group.type === 'movie') {
|
if (!isAuthed) return new Set<string>();
|
||||||
const watchedSet = await traktMoviesSetPromise;
|
if (typeof (traktService as any).getWatchedMovies === 'function') {
|
||||||
const imdbId = group.id.startsWith('tt')
|
const watched = await (traktService as any).getWatchedMovies();
|
||||||
? group.id
|
const watchedSet = new Set<string>();
|
||||||
: `tt${group.id}`;
|
|
||||||
if (watchedSet.has(imdbId)) {
|
if (Array.isArray(watched)) {
|
||||||
// Optional: sync local store to watched to prevent reappearance
|
watched.forEach((w: any) => {
|
||||||
try {
|
const ids = w?.movie?.ids;
|
||||||
await storageService.setWatchProgress(group.id, 'movie', {
|
if (!ids) return;
|
||||||
currentTime: 1,
|
|
||||||
duration: 1,
|
if (ids.imdb) {
|
||||||
lastUpdated: Date.now(),
|
const imdb = ids.imdb;
|
||||||
traktSynced: true,
|
watchedSet.add(imdb.startsWith('tt') ? imdb : `tt${imdb}`);
|
||||||
traktProgress: 100,
|
}
|
||||||
} as any);
|
if (ids.tmdb) {
|
||||||
} catch (_e) { }
|
watchedSet.add(ids.tmdb.toString());
|
||||||
return;
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return watchedSet;
|
||||||
}
|
}
|
||||||
|
return new Set<string>();
|
||||||
|
} catch {
|
||||||
|
return new Set<string>();
|
||||||
}
|
}
|
||||||
const cachedData = await getCachedMetadata(group.type, group.id, group.episodes[0]?.progress?.addonId);
|
})();
|
||||||
if (!cachedData?.basicContent) return;
|
|
||||||
const { metadata, basicContent } = cachedData;
|
|
||||||
|
|
||||||
const batch: ContinueWatchingItem[] = [];
|
// Fetch Trakt watched shows once and reuse
|
||||||
for (const episode of group.episodes) {
|
const traktShowsSetPromise = (async () => {
|
||||||
const { episodeId, progress, progressPercent } = episode;
|
try {
|
||||||
|
const traktService = TraktService.getInstance();
|
||||||
|
const isAuthed = await traktService.isAuthenticated();
|
||||||
|
if (!isAuthed) return new Set<string>();
|
||||||
|
|
||||||
if (group.type === 'series' && progressPercent >= 85) {
|
if (typeof (traktService as any).getWatchedShows === 'function') {
|
||||||
// Local progress completion check
|
const watched = await (traktService as any).getWatchedShows();
|
||||||
if (episodeId) {
|
const watchedSet = new Set<string>();
|
||||||
let currentSeason: number | undefined;
|
|
||||||
let currentEpisode: number | undefined;
|
|
||||||
|
|
||||||
const match = episodeId.match(/s(\d+)e(\d+)/i);
|
if (Array.isArray(watched)) {
|
||||||
|
watched.forEach((show: any) => {
|
||||||
|
const ids = show?.show?.ids;
|
||||||
|
if (!ids) return;
|
||||||
|
|
||||||
|
const imdbId = ids.imdb;
|
||||||
|
const tmdbId = ids.tmdb;
|
||||||
|
|
||||||
|
if (show.seasons && Array.isArray(show.seasons)) {
|
||||||
|
show.seasons.forEach((season: any) => {
|
||||||
|
if (season.episodes && Array.isArray(season.episodes)) {
|
||||||
|
season.episodes.forEach((episode: any) => {
|
||||||
|
if (imdbId) {
|
||||||
|
const cleanImdbId = imdbId.startsWith('tt') ? imdbId : `tt${imdbId}`;
|
||||||
|
watchedSet.add(`${cleanImdbId}:${season.number}:${episode.number}`);
|
||||||
|
}
|
||||||
|
if (tmdbId) {
|
||||||
|
watchedSet.add(`${tmdbId}:${season.number}:${episode.number}`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return watchedSet;
|
||||||
|
}
|
||||||
|
return new Set<string>();
|
||||||
|
} catch {
|
||||||
|
return new Set<string>();
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
|
||||||
|
// Process each content group concurrently, merging results as they arrive
|
||||||
|
groupPromises = Object.values(contentGroups).map(async (group) => {
|
||||||
|
try {
|
||||||
|
if (!isSupportedId(group.id)) return;
|
||||||
|
// Skip movies that are already watched on Trakt
|
||||||
|
if (group.type === 'movie') {
|
||||||
|
const watchedSet = await traktMoviesSetPromise;
|
||||||
|
const imdbId = group.id.startsWith('tt')
|
||||||
|
? group.id
|
||||||
|
: `tt${group.id}`;
|
||||||
|
if (watchedSet.has(imdbId)) {
|
||||||
|
// Optional: sync local store to watched to prevent reappearance
|
||||||
|
try {
|
||||||
|
await storageService.setWatchProgress(group.id, 'movie', {
|
||||||
|
currentTime: 1,
|
||||||
|
duration: 1,
|
||||||
|
lastUpdated: Date.now(),
|
||||||
|
traktSynced: true,
|
||||||
|
traktProgress: 100,
|
||||||
|
} as any);
|
||||||
|
} catch (_e) { }
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const cachedData = await getCachedMetadata(group.type, group.id, group.episodes[0]?.progress?.addonId);
|
||||||
|
if (!cachedData?.basicContent) return;
|
||||||
|
const { metadata, basicContent } = cachedData;
|
||||||
|
|
||||||
|
const batch: ContinueWatchingItem[] = [];
|
||||||
|
for (const episode of group.episodes) {
|
||||||
|
const { episodeId, progress, progressPercent } = episode;
|
||||||
|
|
||||||
|
if (group.type === 'series' && progressPercent >= 85) {
|
||||||
|
// Skip completed episodes - don't add "next episode" here
|
||||||
|
// The Trakt playback endpoint handles in-progress items
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
let season: number | undefined;
|
||||||
|
let episodeNumber: number | undefined;
|
||||||
|
let episodeTitle: string | undefined;
|
||||||
|
let isWatchedOnTrakt = false;
|
||||||
|
|
||||||
|
if (episodeId && group.type === 'series') {
|
||||||
|
let match = episodeId.match(/s(\d+)e(\d+)/i);
|
||||||
if (match) {
|
if (match) {
|
||||||
currentSeason = parseInt(match[1], 10);
|
season = parseInt(match[1], 10);
|
||||||
currentEpisode = parseInt(match[2], 10);
|
episodeNumber = parseInt(match[2], 10);
|
||||||
|
episodeTitle = `Episode ${episodeNumber}`;
|
||||||
} else {
|
} else {
|
||||||
const parts = episodeId.split(':');
|
const parts = episodeId.split(':');
|
||||||
if (parts.length >= 2) {
|
if (parts.length >= 3) {
|
||||||
const seasonNum = parseInt(parts[parts.length - 2], 10);
|
const seasonPart = parts[parts.length - 2];
|
||||||
const episodeNum = parseInt(parts[parts.length - 1], 10);
|
const episodePart = parts[parts.length - 1];
|
||||||
|
const seasonNum = parseInt(seasonPart, 10);
|
||||||
|
const episodeNum = parseInt(episodePart, 10);
|
||||||
if (!isNaN(seasonNum) && !isNaN(episodeNum)) {
|
if (!isNaN(seasonNum) && !isNaN(episodeNum)) {
|
||||||
currentSeason = seasonNum;
|
season = seasonNum;
|
||||||
currentEpisode = episodeNum;
|
episodeNumber = episodeNum;
|
||||||
|
episodeTitle = `Episode ${episodeNumber}`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (currentSeason !== undefined && currentEpisode !== undefined) {
|
// Check if this specific episode is watched on Trakt
|
||||||
const traktService = TraktService.getInstance();
|
if (season !== undefined && episodeNumber !== undefined) {
|
||||||
let nextEpisode: any = null;
|
const watchedEpisodesSet = await traktShowsSetPromise;
|
||||||
|
// Try with both raw ID and tt-prefixed ID, and TMDB ID (which is just the ID string)
|
||||||
|
const rawId = group.id.replace(/^tt/, '');
|
||||||
|
const ttId = `tt${rawId}`;
|
||||||
|
|
||||||
try {
|
if (watchedEpisodesSet.has(`${ttId}:${season}:${episodeNumber}`) ||
|
||||||
const isAuthed = await traktService.isAuthenticated();
|
watchedEpisodesSet.has(`${rawId}:${season}:${episodeNumber}`) ||
|
||||||
if (isAuthed && typeof (traktService as any).getShowWatchedProgress === 'function') {
|
watchedEpisodesSet.has(`${group.id}:${season}:${episodeNumber}`)) {
|
||||||
const showProgress = await (traktService as any).getShowWatchedProgress(group.id);
|
isWatchedOnTrakt = true;
|
||||||
|
|
||||||
if (showProgress && !showProgress.completed && showProgress.next_episode) {
|
// Update local storage to reflect watched status
|
||||||
nextEpisode = showProgress.next_episode;
|
try {
|
||||||
}
|
await storageService.setWatchProgress(
|
||||||
}
|
group.id,
|
||||||
} catch {
|
'series',
|
||||||
|
{
|
||||||
}
|
currentTime: 1,
|
||||||
|
duration: 1,
|
||||||
if (!nextEpisode && metadata?.videos) {
|
lastUpdated: Date.now(),
|
||||||
nextEpisode = findNextEpisode(
|
traktSynced: true,
|
||||||
currentSeason,
|
traktProgress: 100,
|
||||||
currentEpisode,
|
} as any,
|
||||||
metadata.videos
|
episodeId
|
||||||
);
|
);
|
||||||
}
|
} catch (_e) { }
|
||||||
|
|
||||||
if (nextEpisode) {
|
|
||||||
batch.push({
|
|
||||||
...basicContent,
|
|
||||||
id: group.id,
|
|
||||||
type: group.type,
|
|
||||||
progress: 0,
|
|
||||||
lastUpdated: progress.lastUpdated,
|
|
||||||
season: nextEpisode.season,
|
|
||||||
episode: nextEpisode.number ?? nextEpisode.episode,
|
|
||||||
episodeTitle: nextEpisode.title || `Episode ${nextEpisode.number ?? nextEpisode.episode}`,
|
|
||||||
addonId: progress.addonId,
|
|
||||||
} as ContinueWatchingItem);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
continue;
|
|
||||||
|
// If watched on Trakt, skip it - Trakt playback handles in-progress items
|
||||||
|
if (isWatchedOnTrakt) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
batch.push({
|
||||||
|
...basicContent,
|
||||||
|
progress: progressPercent,
|
||||||
|
lastUpdated: progress.lastUpdated,
|
||||||
|
season,
|
||||||
|
episode: episodeNumber,
|
||||||
|
episodeTitle,
|
||||||
|
addonId: progress.addonId,
|
||||||
|
} as ContinueWatchingItem);
|
||||||
}
|
}
|
||||||
|
|
||||||
let season: number | undefined;
|
if (batch.length > 0) await mergeBatchIntoState(batch);
|
||||||
let episodeNumber: number | undefined;
|
} catch (error) {
|
||||||
let episodeTitle: string | undefined;
|
// Continue processing other groups even if one fails
|
||||||
let isWatchedOnTrakt = false;
|
|
||||||
|
|
||||||
if (episodeId && group.type === 'series') {
|
|
||||||
let match = episodeId.match(/s(\d+)e(\d+)/i);
|
|
||||||
if (match) {
|
|
||||||
season = parseInt(match[1], 10);
|
|
||||||
episodeNumber = parseInt(match[2], 10);
|
|
||||||
episodeTitle = `Episode ${episodeNumber}`;
|
|
||||||
} else {
|
|
||||||
const parts = episodeId.split(':');
|
|
||||||
if (parts.length >= 3) {
|
|
||||||
const seasonPart = parts[parts.length - 2];
|
|
||||||
const episodePart = parts[parts.length - 1];
|
|
||||||
const seasonNum = parseInt(seasonPart, 10);
|
|
||||||
const episodeNum = parseInt(episodePart, 10);
|
|
||||||
if (!isNaN(seasonNum) && !isNaN(episodeNum)) {
|
|
||||||
season = seasonNum;
|
|
||||||
episodeNumber = episodeNum;
|
|
||||||
episodeTitle = `Episode ${episodeNumber}`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if this specific episode is watched on Trakt
|
|
||||||
if (season !== undefined && episodeNumber !== undefined) {
|
|
||||||
const watchedEpisodesSet = await traktShowsSetPromise;
|
|
||||||
// Try with both raw ID and tt-prefixed ID, and TMDB ID (which is just the ID string)
|
|
||||||
const rawId = group.id.replace(/^tt/, '');
|
|
||||||
const ttId = `tt${rawId}`;
|
|
||||||
|
|
||||||
if (watchedEpisodesSet.has(`${ttId}:${season}:${episodeNumber}`) ||
|
|
||||||
watchedEpisodesSet.has(`${rawId}:${season}:${episodeNumber}`) ||
|
|
||||||
watchedEpisodesSet.has(`${group.id}:${season}:${episodeNumber}`)) {
|
|
||||||
isWatchedOnTrakt = true;
|
|
||||||
|
|
||||||
// Update local storage to reflect watched status
|
|
||||||
try {
|
|
||||||
await storageService.setWatchProgress(
|
|
||||||
group.id,
|
|
||||||
'series',
|
|
||||||
{
|
|
||||||
currentTime: 1,
|
|
||||||
duration: 1,
|
|
||||||
lastUpdated: Date.now(),
|
|
||||||
traktSynced: true,
|
|
||||||
traktProgress: 100,
|
|
||||||
} as any,
|
|
||||||
episodeId
|
|
||||||
);
|
|
||||||
} catch (_e) { }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// If watched on Trakt, treat it as completed (try to find next episode)
|
|
||||||
if (isWatchedOnTrakt) {
|
|
||||||
if (season !== undefined && episodeNumber !== undefined && metadata?.videos) {
|
|
||||||
const nextEpisodeVideo = findNextEpisode(season, episodeNumber, metadata.videos);
|
|
||||||
if (nextEpisodeVideo) {
|
|
||||||
batch.push({
|
|
||||||
...basicContent,
|
|
||||||
id: group.id,
|
|
||||||
type: group.type,
|
|
||||||
progress: 0,
|
|
||||||
lastUpdated: progress.lastUpdated,
|
|
||||||
season: nextEpisodeVideo.season,
|
|
||||||
episode: nextEpisodeVideo.episode,
|
|
||||||
episodeTitle: `Episode ${nextEpisodeVideo.episode}`,
|
|
||||||
addonId: progress.addonId,
|
|
||||||
} as ContinueWatchingItem);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
batch.push({
|
|
||||||
...basicContent,
|
|
||||||
progress: progressPercent,
|
|
||||||
lastUpdated: progress.lastUpdated,
|
|
||||||
season,
|
|
||||||
episode: episodeNumber,
|
|
||||||
episodeTitle,
|
|
||||||
addonId: progress.addonId,
|
|
||||||
} as ContinueWatchingItem);
|
|
||||||
}
|
}
|
||||||
|
});
|
||||||
if (batch.length > 0) await mergeBatchIntoState(batch);
|
} // End of else block for non-Trakt users
|
||||||
} catch (error) {
|
|
||||||
// Continue processing other groups even if one fails
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// TRAKT: fetch playback progress (in-progress items) and history, merge incrementally
|
// TRAKT: fetch playback progress (in-progress items) and history, merge incrementally
|
||||||
const traktMergePromise = (async () => {
|
const traktMergePromise = (async () => {
|
||||||
|
|
@ -674,33 +611,26 @@ const ContinueWatchingSection = React.forwardRef<ContinueWatchingRef>((props, re
|
||||||
|
|
||||||
lastTraktSyncRef.current = now;
|
lastTraktSyncRef.current = now;
|
||||||
|
|
||||||
// Fetch both playback progress (paused items) and watch history in parallel
|
// Fetch only playback progress (paused items with actual progress %)
|
||||||
const [playbackItems, historyItems, watchedShows] = await Promise.all([
|
// Removed: history items and watched shows - redundant with local logic
|
||||||
traktService.getPlaybackProgress(), // Items with actual progress %
|
const playbackItems = await traktService.getPlaybackProgress();
|
||||||
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 traktBatch: ContinueWatchingItem[] = [];
|
||||||
const processedShows = new Set<string>(); // Track which shows we've added
|
|
||||||
|
|
||||||
// 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);
|
||||||
for (const item of playbackItems) {
|
for (const item of playbackItems) {
|
||||||
try {
|
try {
|
||||||
// Skip items with very low or very high progress
|
// Skip items with < 2% progress (accidental clicks)
|
||||||
if (item.progress <= 0 || item.progress >= 85) continue;
|
if (item.progress < 2) continue;
|
||||||
|
// Skip items with >= 85% progress (completed)
|
||||||
|
if (item.progress >= 85) continue;
|
||||||
|
// Skip items older than 30 days
|
||||||
|
const pausedAt = new Date(item.paused_at).getTime();
|
||||||
|
if (pausedAt < thirtyDaysAgo) continue;
|
||||||
|
|
||||||
if (item.type === 'movie' && item.movie?.ids?.imdb) {
|
if (item.type === 'movie' && item.movie?.ids?.imdb) {
|
||||||
const imdbId = item.movie.ids.imdb.startsWith('tt')
|
const imdbId = item.movie.ids.imdb.startsWith('tt')
|
||||||
|
|
@ -735,13 +665,7 @@ 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;
|
||||||
|
|
||||||
// Check reset_at - skip if this was paused before re-watch started
|
|
||||||
const resetTime = showResetMap[showImdb];
|
|
||||||
const pausedAt = new Date(item.paused_at).getTime();
|
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);
|
const cachedData = await getCachedMetadata('series', showImdb);
|
||||||
if (!cachedData?.basicContent) continue;
|
if (!cachedData?.basicContent) continue;
|
||||||
|
|
@ -758,7 +682,6 @@ const ContinueWatchingSection = React.forwardRef<ContinueWatchingRef>((props, re
|
||||||
addonId: undefined,
|
addonId: undefined,
|
||||||
} as ContinueWatchingItem);
|
} 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`);
|
logger.log(`📺 [TraktPlayback] Adding ${item.show.title} S${item.episode.season}E${item.episode.number} with ${item.progress.toFixed(1)}% progress`);
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
|
@ -766,110 +689,22 @@ const ContinueWatchingSection = React.forwardRef<ContinueWatchingRef>((props, re
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// STEP 2: Process watch history for shows NOT in playback progress
|
// Set Trakt playback items as state (replace, don't merge with local storage)
|
||||||
// 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
|
|
||||||
? (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 };
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add next episodes for completed shows
|
|
||||||
for (const [showId, info] of Object.entries(latestWatchedByShow)) {
|
|
||||||
try {
|
|
||||||
// Check if this show was recently removed
|
|
||||||
const showKey = `series:${showId}`;
|
|
||||||
if (recentlyRemovedRef.current.has(showKey)) {
|
|
||||||
logger.log(`🚫 [TraktSync] Skipping recently removed show: ${showKey}`);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
const cachedData = await getCachedMetadata('series', showId);
|
|
||||||
if (!cachedData?.basicContent) continue;
|
|
||||||
const { metadata, basicContent } = cachedData;
|
|
||||||
|
|
||||||
const traktService = TraktService.getInstance();
|
|
||||||
let showProgress: any = null;
|
|
||||||
|
|
||||||
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({
|
|
||||||
...basicContent,
|
|
||||||
id: showId,
|
|
||||||
type: 'series',
|
|
||||||
progress: 0,
|
|
||||||
lastUpdated: info.watchedAt,
|
|
||||||
season: nextEp.season,
|
|
||||||
episode: nextEp.number,
|
|
||||||
episodeTitle: nextEp.title || `Episode ${nextEp.number}`,
|
|
||||||
addonId: undefined,
|
|
||||||
} as ContinueWatchingItem);
|
|
||||||
|
|
||||||
// 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) {
|
|
||||||
await storageService.setWatchProgress(
|
|
||||||
showId,
|
|
||||||
'series',
|
|
||||||
{
|
|
||||||
currentTime: 1,
|
|
||||||
duration: 1,
|
|
||||||
lastUpdated: info.watchedAt,
|
|
||||||
traktSynced: true,
|
|
||||||
traktProgress: 100,
|
|
||||||
} as any,
|
|
||||||
`${info.season}:${info.episode}`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
// Continue with other shows
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Merge all Trakt items as a single batch to ensure proper sorting
|
|
||||||
if (traktBatch.length > 0) {
|
if (traktBatch.length > 0) {
|
||||||
logger.log(`📋 [TraktSync] Merging ${traktBatch.length} items from Trakt (playback + history)`);
|
// Dedupe: for series, keep only the latest episode per show
|
||||||
await mergeBatchIntoState(traktBatch);
|
const deduped = new Map<string, ContinueWatchingItem>();
|
||||||
|
for (const item of traktBatch) {
|
||||||
|
const key = `${item.type}:${item.id}`;
|
||||||
|
const existing = deduped.get(key);
|
||||||
|
if (!existing || (item.lastUpdated ?? 0) > (existing.lastUpdated ?? 0)) {
|
||||||
|
deduped.set(key, item);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const uniqueItems = Array.from(deduped.values());
|
||||||
|
logger.log(`📋 [TraktSync] Setting ${uniqueItems.length} items from Trakt playback (deduped from ${traktBatch.length})`);
|
||||||
|
// Sort by lastUpdated descending and set directly
|
||||||
|
const sortedBatch = uniqueItems.sort((a, b) => (b.lastUpdated ?? 0) - (a.lastUpdated ?? 0));
|
||||||
|
setContinueWatchingItems(sortedBatch);
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
logger.error('[TraktSync] Error in Trakt merge:', err);
|
logger.error('[TraktSync] Error in Trakt merge:', err);
|
||||||
|
|
@ -1353,8 +1188,8 @@ const ContinueWatchingSection = React.forwardRef<ContinueWatchingRef>((props, re
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
<FlashList
|
<FlatList
|
||||||
data={continueWatchingItems}
|
data={[...continueWatchingItems].sort((a, b) => (b.lastUpdated ?? 0) - (a.lastUpdated ?? 0))}
|
||||||
renderItem={renderContinueWatchingItem}
|
renderItem={renderContinueWatchingItem}
|
||||||
keyExtractor={keyExtractor}
|
keyExtractor={keyExtractor}
|
||||||
horizontal
|
horizontal
|
||||||
|
|
|
||||||
|
|
@ -578,7 +578,7 @@ export type TraktContentCommentLegacy =
|
||||||
| TraktListComment;
|
| TraktListComment;
|
||||||
|
|
||||||
|
|
||||||
const TRAKT_MAINTENANCE_MODE = true;
|
const TRAKT_MAINTENANCE_MODE = false;
|
||||||
const TRAKT_MAINTENANCE_MESSAGE = 'Trakt integration is temporarily unavailable for maintenance. Please try again later.';
|
const TRAKT_MAINTENANCE_MESSAGE = 'Trakt integration is temporarily unavailable for maintenance. Please try again later.';
|
||||||
|
|
||||||
export class TraktService {
|
export class TraktService {
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue