mirror of
https://github.com/tapframe/NuvioStreaming.git
synced 2026-03-29 05:48:45 +00:00
trakt test
This commit is contained in:
parent
56234daf82
commit
a27ee4ac56
2 changed files with 311 additions and 192 deletions
|
|
@ -1,9 +1,9 @@
|
|||
import React, { useState, useEffect, useCallback, useRef, useMemo } from 'react';
|
||||
import {
|
||||
View,
|
||||
Text,
|
||||
StyleSheet,
|
||||
TouchableOpacity,
|
||||
import {
|
||||
View,
|
||||
Text,
|
||||
StyleSheet,
|
||||
TouchableOpacity,
|
||||
Dimensions,
|
||||
AppState,
|
||||
AppStateStatus,
|
||||
|
|
@ -55,17 +55,17 @@ const calculatePosterLayout = (screenWidth: number) => {
|
|||
const MIN_POSTER_WIDTH = 120; // Slightly larger for continue watching items
|
||||
const MAX_POSTER_WIDTH = 160; // Maximum poster width for this section
|
||||
const HORIZONTAL_PADDING = 40; // Total horizontal padding/margins
|
||||
|
||||
|
||||
// Calculate how many posters can fit (fewer items for continue watching)
|
||||
const availableWidth = screenWidth - HORIZONTAL_PADDING;
|
||||
const maxColumns = Math.floor(availableWidth / MIN_POSTER_WIDTH);
|
||||
|
||||
|
||||
// Limit to reasonable number of columns (2-5 for continue watching)
|
||||
const numColumns = Math.min(Math.max(maxColumns, 2), 5);
|
||||
|
||||
|
||||
// Calculate actual poster width
|
||||
const posterWidth = Math.min(availableWidth / numColumns, MAX_POSTER_WIDTH);
|
||||
|
||||
|
||||
return {
|
||||
numColumns,
|
||||
posterWidth,
|
||||
|
|
@ -85,7 +85,7 @@ const isSupportedId = (id: string): boolean => {
|
|||
// Function to check if an episode has been released
|
||||
const isEpisodeReleased = (video: any): boolean => {
|
||||
if (!video.released) return false;
|
||||
|
||||
|
||||
try {
|
||||
const releaseDate = new Date(video.released);
|
||||
const now = new Date();
|
||||
|
|
@ -112,16 +112,16 @@ const ContinueWatchingSection = React.forwardRef<ContinueWatchingRef>((props, re
|
|||
const [dimensions, setDimensions] = useState(Dimensions.get('window'));
|
||||
const deviceWidth = dimensions.width;
|
||||
const deviceHeight = dimensions.height;
|
||||
|
||||
|
||||
// Listen for dimension changes (orientation changes)
|
||||
useEffect(() => {
|
||||
const subscription = Dimensions.addEventListener('change', ({ window }) => {
|
||||
setDimensions(window);
|
||||
});
|
||||
|
||||
|
||||
return () => subscription?.remove();
|
||||
}, []);
|
||||
|
||||
|
||||
// Determine device type based on width
|
||||
const getDeviceType = useCallback(() => {
|
||||
if (deviceWidth >= BREAKPOINTS.tv) return 'tv';
|
||||
|
|
@ -129,13 +129,13 @@ const ContinueWatchingSection = React.forwardRef<ContinueWatchingRef>((props, re
|
|||
if (deviceWidth >= BREAKPOINTS.tablet) return 'tablet';
|
||||
return 'phone';
|
||||
}, [deviceWidth]);
|
||||
|
||||
|
||||
const deviceType = getDeviceType();
|
||||
const isTablet = deviceType === 'tablet';
|
||||
const isLargeTablet = deviceType === 'largeTablet';
|
||||
const isTV = deviceType === 'tv';
|
||||
const isLargeScreen = isTablet || isLargeTablet || isTV;
|
||||
|
||||
|
||||
// Enhanced responsive sizing for continue watching items
|
||||
const computedItemWidth = useMemo(() => {
|
||||
switch (deviceType) {
|
||||
|
|
@ -149,7 +149,7 @@ const ContinueWatchingSection = React.forwardRef<ContinueWatchingRef>((props, re
|
|||
return 280; // Original phone size
|
||||
}
|
||||
}, [deviceType]);
|
||||
|
||||
|
||||
const computedItemHeight = useMemo(() => {
|
||||
switch (deviceType) {
|
||||
case 'tv':
|
||||
|
|
@ -162,7 +162,7 @@ const ContinueWatchingSection = React.forwardRef<ContinueWatchingRef>((props, re
|
|||
return 120; // Original phone height
|
||||
}
|
||||
}, [deviceType]);
|
||||
|
||||
|
||||
// Enhanced spacing and padding
|
||||
const horizontalPadding = useMemo(() => {
|
||||
switch (deviceType) {
|
||||
|
|
@ -176,7 +176,7 @@ const ContinueWatchingSection = React.forwardRef<ContinueWatchingRef>((props, re
|
|||
return 16; // phone
|
||||
}
|
||||
}, [deviceType]);
|
||||
|
||||
|
||||
const itemSpacing = useMemo(() => {
|
||||
switch (deviceType) {
|
||||
case 'tv':
|
||||
|
|
@ -198,11 +198,11 @@ const ContinueWatchingSection = React.forwardRef<ContinueWatchingRef>((props, re
|
|||
|
||||
// Use a ref to track if a background refresh is in progress to avoid state updates
|
||||
const isRefreshingRef = useRef(false);
|
||||
|
||||
|
||||
// Track recently removed items to prevent immediate re-addition
|
||||
const recentlyRemovedRef = useRef<Set<string>>(new Set());
|
||||
const REMOVAL_IGNORE_DURATION = 10000; // 10 seconds
|
||||
|
||||
|
||||
// Track last Trakt sync to prevent excessive API calls
|
||||
const lastTraktSyncRef = useRef<number>(0);
|
||||
const TRAKT_SYNC_COOLDOWN = 5 * 60 * 1000; // 5 minutes between Trakt syncs
|
||||
|
|
@ -216,18 +216,18 @@ const ContinueWatchingSection = React.forwardRef<ContinueWatchingRef>((props, re
|
|||
const cacheKey = `${type}:${id}`;
|
||||
const cached = metadataCache.current[cacheKey];
|
||||
const now = Date.now();
|
||||
|
||||
|
||||
if (cached && (now - cached.timestamp) < CACHE_DURATION) {
|
||||
return cached;
|
||||
}
|
||||
|
||||
|
||||
try {
|
||||
const shouldFetchMeta = await stremioService.isValidContentId(type, id);
|
||||
const [metadata, basicContent] = await Promise.all([
|
||||
shouldFetchMeta ? stremioService.getMetaDetails(type, id) : Promise.resolve(null),
|
||||
catalogService.getBasicContentDetails(type, id)
|
||||
]);
|
||||
|
||||
|
||||
if (basicContent) {
|
||||
const result = { metadata, basicContent, timestamp: now };
|
||||
metadataCache.current[cacheKey] = result;
|
||||
|
|
@ -334,13 +334,67 @@ const ContinueWatchingSection = React.forwardRef<ContinueWatchingRef>((props, re
|
|||
if (!isAuthed) return new Set<string>();
|
||||
if (typeof (traktService as any).getWatchedMovies === 'function') {
|
||||
const watched = await (traktService as any).getWatchedMovies();
|
||||
const watchedSet = new Set<string>();
|
||||
|
||||
if (Array.isArray(watched)) {
|
||||
const ids = watched
|
||||
.map((w: any) => w?.movie?.ids?.imdb)
|
||||
.filter(Boolean)
|
||||
.map((imdb: string) => (imdb.startsWith('tt') ? imdb : `tt${imdb}`));
|
||||
return new Set<string>(ids);
|
||||
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
|
||||
const traktShowsSetPromise = (async () => {
|
||||
try {
|
||||
const traktService = TraktService.getInstance();
|
||||
const isAuthed = await traktService.isAuthenticated();
|
||||
if (!isAuthed) return new Set<string>();
|
||||
|
||||
if (typeof (traktService as any).getWatchedShows === 'function') {
|
||||
const watched = await (traktService as any).getWatchedShows();
|
||||
const watchedSet = new Set<string>();
|
||||
|
||||
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 {
|
||||
|
|
@ -365,7 +419,7 @@ const ContinueWatchingSection = React.forwardRef<ContinueWatchingRef>((props, re
|
|||
traktSynced: true,
|
||||
traktProgress: 100,
|
||||
} as any);
|
||||
} catch (_e) {}
|
||||
} catch (_e) { }
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
|
@ -422,6 +476,8 @@ const ContinueWatchingSection = React.forwardRef<ContinueWatchingRef>((props, re
|
|||
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) {
|
||||
|
|
@ -442,6 +498,61 @@ const ContinueWatchingSection = React.forwardRef<ContinueWatchingRef>((props, re
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 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) {
|
||||
let nextSeason = season;
|
||||
let nextEpisode = (episodeNumber || 0) + 1;
|
||||
|
||||
if (nextSeason !== undefined && nextEpisode !== undefined && metadata?.videos && Array.isArray(metadata.videos)) {
|
||||
const nextEpisodeVideo = metadata.videos.find((video: any) =>
|
||||
video.season === nextSeason && video.episode === nextEpisode
|
||||
);
|
||||
if (nextEpisodeVideo && isEpisodeReleased(nextEpisodeVideo)) {
|
||||
batch.push({
|
||||
...basicContent,
|
||||
id: group.id,
|
||||
type: group.type,
|
||||
progress: 0,
|
||||
lastUpdated: progress.lastUpdated,
|
||||
season: nextSeason,
|
||||
episode: nextEpisode,
|
||||
episodeTitle: `Episode ${nextEpisode}`,
|
||||
} as ContinueWatchingItem);
|
||||
}
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
batch.push({
|
||||
|
|
@ -466,14 +577,14 @@ const ContinueWatchingSection = React.forwardRef<ContinueWatchingRef>((props, re
|
|||
const traktService = TraktService.getInstance();
|
||||
const isAuthed = await traktService.isAuthenticated();
|
||||
if (!isAuthed) return;
|
||||
|
||||
|
||||
// Check Trakt sync cooldown to prevent excessive API calls
|
||||
const now = Date.now();
|
||||
if (now - lastTraktSyncRef.current < TRAKT_SYNC_COOLDOWN) {
|
||||
logger.log(`[TraktSync] Skipping Trakt sync - cooldown active (${Math.round((TRAKT_SYNC_COOLDOWN - (now - lastTraktSyncRef.current)) / 1000)}s remaining)`);
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
lastTraktSyncRef.current = now;
|
||||
const historyItems = await traktService.getWatchedEpisodesHistory(1, 200);
|
||||
const latestWatchedByShow: Record<string, { season: number; episode: number; watchedAt: number }> = {};
|
||||
|
|
@ -650,7 +761,7 @@ const ContinueWatchingSection = React.forwardRef<ContinueWatchingRef>((props, re
|
|||
useFocusEffect(
|
||||
useCallback(() => {
|
||||
loadContinueWatching(true);
|
||||
return () => {};
|
||||
return () => { };
|
||||
}, [loadContinueWatching])
|
||||
);
|
||||
|
||||
|
|
@ -667,62 +778,62 @@ const ContinueWatchingSection = React.forwardRef<ContinueWatchingRef>((props, re
|
|||
const handleContentPress = useCallback(async (item: ContinueWatchingItem) => {
|
||||
try {
|
||||
logger.log(`🎬 [ContinueWatching] User clicked on: ${item.name} (${item.type}:${item.id})`);
|
||||
|
||||
|
||||
// Check if cached streams are enabled in settings
|
||||
if (!settings.useCachedStreams) {
|
||||
logger.log(`📺 [ContinueWatching] Cached streams disabled, navigating to ${settings.openMetadataScreenWhenCacheDisabled ? 'MetadataScreen' : 'StreamsScreen'} for ${item.name}`);
|
||||
|
||||
|
||||
// Navigate based on the second setting
|
||||
if (settings.openMetadataScreenWhenCacheDisabled) {
|
||||
// Navigate to MetadataScreen
|
||||
if (item.type === 'series' && item.season && item.episode) {
|
||||
const episodeId = `${item.id}:${item.season}:${item.episode}`;
|
||||
navigation.navigate('Metadata', {
|
||||
id: item.id,
|
||||
type: item.type,
|
||||
episodeId: episodeId
|
||||
navigation.navigate('Metadata', {
|
||||
id: item.id,
|
||||
type: item.type,
|
||||
episodeId: episodeId
|
||||
});
|
||||
} else {
|
||||
navigation.navigate('Metadata', {
|
||||
id: item.id,
|
||||
type: item.type
|
||||
navigation.navigate('Metadata', {
|
||||
id: item.id,
|
||||
type: item.type
|
||||
});
|
||||
}
|
||||
} else {
|
||||
// Navigate to StreamsScreen
|
||||
if (item.type === 'series' && item.season && item.episode) {
|
||||
const episodeId = `${item.id}:${item.season}:${item.episode}`;
|
||||
navigation.navigate('Streams', {
|
||||
id: item.id,
|
||||
type: item.type,
|
||||
episodeId: episodeId
|
||||
navigation.navigate('Streams', {
|
||||
id: item.id,
|
||||
type: item.type,
|
||||
episodeId: episodeId
|
||||
});
|
||||
} else {
|
||||
navigation.navigate('Streams', {
|
||||
id: item.id,
|
||||
type: item.type
|
||||
navigation.navigate('Streams', {
|
||||
id: item.id,
|
||||
type: item.type
|
||||
});
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
// Check if we have a cached stream for this content
|
||||
const episodeId = item.type === 'series' && item.season && item.episode
|
||||
? `${item.id}:${item.season}:${item.episode}`
|
||||
const episodeId = item.type === 'series' && item.season && item.episode
|
||||
? `${item.id}:${item.season}:${item.episode}`
|
||||
: undefined;
|
||||
|
||||
|
||||
logger.log(`🔍 [ContinueWatching] Looking for cached stream with episodeId: ${episodeId || 'none'}`);
|
||||
|
||||
|
||||
const cachedStream = await streamCacheService.getCachedStream(item.id, item.type, episodeId);
|
||||
|
||||
|
||||
if (cachedStream) {
|
||||
// We have a valid cached stream, navigate directly to player
|
||||
logger.log(`🚀 [ContinueWatching] Using cached stream for ${item.name}`);
|
||||
|
||||
|
||||
// Determine the player route based on platform
|
||||
const playerRoute = Platform.OS === 'ios' ? 'PlayerIOS' : 'PlayerAndroid';
|
||||
|
||||
|
||||
// Navigate directly to player with cached stream data
|
||||
navigation.navigate(playerRoute as any, {
|
||||
uri: cachedStream.stream.url,
|
||||
|
|
@ -743,25 +854,25 @@ const ContinueWatchingSection = React.forwardRef<ContinueWatchingRef>((props, re
|
|||
backdrop: cachedStream.metadata?.backdrop || item.banner,
|
||||
videoType: undefined, // Let player auto-detect
|
||||
} as any);
|
||||
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
// No cached stream or cache failed, navigate to StreamsScreen
|
||||
logger.log(`📺 [ContinueWatching] No cached stream, navigating to StreamsScreen for ${item.name}`);
|
||||
|
||||
|
||||
if (item.type === 'series' && item.season && item.episode) {
|
||||
// For series, navigate to the specific episode
|
||||
navigation.navigate('Streams', {
|
||||
id: item.id,
|
||||
type: item.type,
|
||||
episodeId: episodeId
|
||||
navigation.navigate('Streams', {
|
||||
id: item.id,
|
||||
type: item.type,
|
||||
episodeId: episodeId
|
||||
});
|
||||
} else {
|
||||
// For movies or series without specific episode, navigate to main content
|
||||
navigation.navigate('Streams', {
|
||||
id: item.id,
|
||||
type: item.type
|
||||
navigation.navigate('Streams', {
|
||||
id: item.id,
|
||||
type: item.type
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
|
|
@ -769,15 +880,15 @@ const ContinueWatchingSection = React.forwardRef<ContinueWatchingRef>((props, re
|
|||
// Fallback to StreamsScreen on any error
|
||||
if (item.type === 'series' && item.season && item.episode) {
|
||||
const episodeId = `${item.id}:${item.season}:${item.episode}`;
|
||||
navigation.navigate('Streams', {
|
||||
id: item.id,
|
||||
type: item.type,
|
||||
episodeId: episodeId
|
||||
navigation.navigate('Streams', {
|
||||
id: item.id,
|
||||
type: item.type,
|
||||
episodeId: episodeId
|
||||
});
|
||||
} else {
|
||||
navigation.navigate('Streams', {
|
||||
id: item.id,
|
||||
type: item.type
|
||||
navigation.navigate('Streams', {
|
||||
id: item.id,
|
||||
type: item.type
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
@ -798,7 +909,7 @@ const ContinueWatchingSection = React.forwardRef<ContinueWatchingRef>((props, re
|
|||
{
|
||||
label: 'Cancel',
|
||||
style: { color: '#888' },
|
||||
onPress: () => {},
|
||||
onPress: () => { },
|
||||
},
|
||||
{
|
||||
label: 'Remove',
|
||||
|
|
@ -842,7 +953,7 @@ const ContinueWatchingSection = React.forwardRef<ContinueWatchingRef>((props, re
|
|||
const renderContinueWatchingItem = useCallback(({ item }: { item: ContinueWatchingItem }) => (
|
||||
<TouchableOpacity
|
||||
style={[
|
||||
styles.wideContentItem,
|
||||
styles.wideContentItem,
|
||||
{
|
||||
backgroundColor: currentTheme.colors.elevation1,
|
||||
borderColor: currentTheme.colors.border,
|
||||
|
|
@ -864,7 +975,7 @@ const ContinueWatchingSection = React.forwardRef<ContinueWatchingRef>((props, re
|
|||
}
|
||||
]}>
|
||||
<FastImage
|
||||
source={{
|
||||
source={{
|
||||
uri: item.poster || 'https://via.placeholder.com/300x450',
|
||||
priority: FastImage.priority.high,
|
||||
cache: FastImage.cacheControl.immutable
|
||||
|
|
@ -872,7 +983,7 @@ const ContinueWatchingSection = React.forwardRef<ContinueWatchingRef>((props, re
|
|||
style={styles.continueWatchingPoster}
|
||||
resizeMode={FastImage.resizeMode.cover}
|
||||
/>
|
||||
|
||||
|
||||
{/* Delete Indicator Overlay */}
|
||||
{deletingItemId === item.id && (
|
||||
<View style={styles.deletingOverlay}>
|
||||
|
|
@ -893,10 +1004,10 @@ const ContinueWatchingSection = React.forwardRef<ContinueWatchingRef>((props, re
|
|||
const isUpNext = item.type === 'series' && item.progress === 0;
|
||||
return (
|
||||
<View style={styles.titleRow}>
|
||||
<Text
|
||||
<Text
|
||||
style={[
|
||||
styles.contentTitle,
|
||||
{
|
||||
styles.contentTitle,
|
||||
{
|
||||
color: currentTheme.colors.highEmphasis,
|
||||
fontSize: isTV ? 20 : isLargeTablet ? 18 : isTablet ? 17 : 16
|
||||
}
|
||||
|
|
@ -906,19 +1017,19 @@ const ContinueWatchingSection = React.forwardRef<ContinueWatchingRef>((props, re
|
|||
{item.name}
|
||||
</Text>
|
||||
{isUpNext && (
|
||||
<View style={[
|
||||
styles.progressBadge,
|
||||
{
|
||||
backgroundColor: currentTheme.colors.primary,
|
||||
paddingHorizontal: isTV ? 12 : isLargeTablet ? 10 : isTablet ? 8 : 8,
|
||||
paddingVertical: isTV ? 6 : isLargeTablet ? 5 : isTablet ? 4 : 3
|
||||
}
|
||||
]}>
|
||||
<View style={[
|
||||
styles.progressBadge,
|
||||
{
|
||||
backgroundColor: currentTheme.colors.primary,
|
||||
paddingHorizontal: isTV ? 12 : isLargeTablet ? 10 : isTablet ? 8 : 8,
|
||||
paddingVertical: isTV ? 6 : isLargeTablet ? 5 : isTablet ? 4 : 3
|
||||
}
|
||||
]}>
|
||||
<Text style={[
|
||||
styles.progressText,
|
||||
{ fontSize: isTV ? 14 : isLargeTablet ? 13 : isTablet ? 12 : 12 }
|
||||
]}>Up Next</Text>
|
||||
</View>
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
);
|
||||
|
|
@ -931,8 +1042,8 @@ const ContinueWatchingSection = React.forwardRef<ContinueWatchingRef>((props, re
|
|||
return (
|
||||
<View style={styles.episodeRow}>
|
||||
<Text style={[
|
||||
styles.episodeText,
|
||||
{
|
||||
styles.episodeText,
|
||||
{
|
||||
color: currentTheme.colors.mediumEmphasis,
|
||||
fontSize: isTV ? 16 : isLargeTablet ? 15 : isTablet ? 14 : 13
|
||||
}
|
||||
|
|
@ -940,10 +1051,10 @@ const ContinueWatchingSection = React.forwardRef<ContinueWatchingRef>((props, re
|
|||
Season {item.season}
|
||||
</Text>
|
||||
{item.episodeTitle && (
|
||||
<Text
|
||||
<Text
|
||||
style={[
|
||||
styles.episodeTitle,
|
||||
{
|
||||
styles.episodeTitle,
|
||||
{
|
||||
color: currentTheme.colors.mediumEmphasis,
|
||||
fontSize: isTV ? 15 : isLargeTablet ? 14 : isTablet ? 13 : 12
|
||||
}
|
||||
|
|
@ -958,8 +1069,8 @@ const ContinueWatchingSection = React.forwardRef<ContinueWatchingRef>((props, re
|
|||
} else {
|
||||
return (
|
||||
<Text style={[
|
||||
styles.yearText,
|
||||
{
|
||||
styles.yearText,
|
||||
{
|
||||
color: currentTheme.colors.mediumEmphasis,
|
||||
fontSize: isTV ? 16 : isLargeTablet ? 15 : isTablet ? 14 : 13
|
||||
}
|
||||
|
|
@ -979,19 +1090,19 @@ const ContinueWatchingSection = React.forwardRef<ContinueWatchingRef>((props, re
|
|||
height: isTV ? 6 : isLargeTablet ? 5 : isTablet ? 4 : 4
|
||||
}
|
||||
]}>
|
||||
<View
|
||||
<View
|
||||
style={[
|
||||
styles.wideProgressBar,
|
||||
{
|
||||
width: `${item.progress}%`,
|
||||
backgroundColor: currentTheme.colors.primary
|
||||
styles.wideProgressBar,
|
||||
{
|
||||
width: `${item.progress}%`,
|
||||
backgroundColor: currentTheme.colors.primary
|
||||
}
|
||||
]}
|
||||
]}
|
||||
/>
|
||||
</View>
|
||||
<Text style={[
|
||||
styles.progressLabel,
|
||||
{
|
||||
styles.progressLabel,
|
||||
{
|
||||
color: currentTheme.colors.textMuted,
|
||||
fontSize: isTV ? 14 : isLargeTablet ? 13 : isTablet ? 12 : 11
|
||||
}
|
||||
|
|
@ -1023,15 +1134,15 @@ const ContinueWatchingSection = React.forwardRef<ContinueWatchingRef>((props, re
|
|||
<View style={[styles.header, { paddingHorizontal: horizontalPadding }]}>
|
||||
<View style={styles.titleContainer}>
|
||||
<Text style={[
|
||||
styles.title,
|
||||
{
|
||||
styles.title,
|
||||
{
|
||||
color: currentTheme.colors.text,
|
||||
fontSize: isTV ? 32 : isLargeTablet ? 28 : isTablet ? 26 : 24
|
||||
}
|
||||
]}>Continue Watching</Text>
|
||||
<View style={[
|
||||
styles.titleUnderline,
|
||||
{
|
||||
styles.titleUnderline,
|
||||
{
|
||||
backgroundColor: currentTheme.colors.primary,
|
||||
width: isTV ? 50 : isLargeTablet ? 45 : isTablet ? 40 : 40,
|
||||
height: isTV ? 4 : isLargeTablet ? 3.5 : isTablet ? 3 : 3
|
||||
|
|
@ -1039,7 +1150,7 @@ const ContinueWatchingSection = React.forwardRef<ContinueWatchingRef>((props, re
|
|||
]} />
|
||||
</View>
|
||||
</View>
|
||||
|
||||
|
||||
<FlashList
|
||||
data={continueWatchingItems}
|
||||
renderItem={renderContinueWatchingItem}
|
||||
|
|
@ -1048,14 +1159,14 @@ const ContinueWatchingSection = React.forwardRef<ContinueWatchingRef>((props, re
|
|||
showsHorizontalScrollIndicator={false}
|
||||
contentContainerStyle={[
|
||||
styles.wideList,
|
||||
{
|
||||
paddingLeft: horizontalPadding,
|
||||
paddingRight: horizontalPadding
|
||||
{
|
||||
paddingLeft: horizontalPadding,
|
||||
paddingRight: horizontalPadding
|
||||
}
|
||||
]}
|
||||
ItemSeparatorComponent={ItemSeparator}
|
||||
onEndReachedThreshold={0.7}
|
||||
onEndReached={() => {}}
|
||||
onEndReached={() => { }}
|
||||
removeClippedSubviews={true}
|
||||
/>
|
||||
|
||||
|
|
@ -1209,7 +1320,7 @@ const styles = StyleSheet.create({
|
|||
},
|
||||
contentItem: {
|
||||
width: POSTER_WIDTH,
|
||||
aspectRatio: 2/3,
|
||||
aspectRatio: 2 / 3,
|
||||
margin: 0,
|
||||
borderRadius: 8,
|
||||
overflow: 'hidden',
|
||||
|
|
|
|||
|
|
@ -52,6 +52,14 @@ export interface TraktWatchedItem {
|
|||
};
|
||||
plays: number;
|
||||
last_watched_at: string;
|
||||
seasons?: {
|
||||
number: number;
|
||||
episodes: {
|
||||
number: number;
|
||||
plays: number;
|
||||
last_watched_at: string;
|
||||
}[];
|
||||
}[];
|
||||
}
|
||||
|
||||
export interface TraktWatchlistItem {
|
||||
|
|
@ -559,7 +567,7 @@ export class TraktService {
|
|||
private refreshToken: string | null = null;
|
||||
private tokenExpiry: number = 0;
|
||||
private isInitialized: boolean = false;
|
||||
|
||||
|
||||
// Rate limiting - Optimized for real-time scrobbling
|
||||
private lastApiCall: number = 0;
|
||||
private readonly MIN_API_INTERVAL = 500; // Reduced to 500ms for faster updates
|
||||
|
|
@ -575,21 +583,21 @@ export class TraktService {
|
|||
private currentlyWatching: Set<string> = new Set();
|
||||
private lastSyncTimes: Map<string, number> = new Map();
|
||||
private readonly SYNC_DEBOUNCE_MS = 5000; // Reduced from 20000ms to 5000ms for real-time updates
|
||||
|
||||
|
||||
// Debounce for stop calls - Optimized for responsiveness
|
||||
private lastStopCalls: Map<string, number> = new Map();
|
||||
private readonly STOP_DEBOUNCE_MS = 1000; // Reduced from 3000ms to 1000ms for better responsiveness
|
||||
|
||||
|
||||
// Default completion threshold (overridden by user settings)
|
||||
private readonly DEFAULT_COMPLETION_THRESHOLD = 80; // 80%
|
||||
|
||||
private constructor() {
|
||||
// Increased cleanup interval from 5 minutes to 15 minutes to reduce heating
|
||||
setInterval(() => this.cleanupOldStopCalls(), 15 * 60 * 1000); // Clean up every 15 minutes
|
||||
|
||||
|
||||
// Add AppState cleanup to reduce memory pressure
|
||||
AppState.addEventListener('change', this.handleAppStateChange);
|
||||
|
||||
|
||||
// Load user settings
|
||||
this.loadCompletionThreshold();
|
||||
}
|
||||
|
|
@ -611,21 +619,21 @@ export class TraktService {
|
|||
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;
|
||||
|
||||
|
|
@ -635,7 +643,7 @@ export class TraktService {
|
|||
private cleanupOldStopCalls(): void {
|
||||
const now = Date.now();
|
||||
let cleanupCount = 0;
|
||||
|
||||
|
||||
// Remove stop calls older than the debounce window
|
||||
for (const [key, timestamp] of this.lastStopCalls.entries()) {
|
||||
if (now - timestamp > this.STOP_DEBOUNCE_MS) {
|
||||
|
|
@ -643,7 +651,7 @@ export class TraktService {
|
|||
cleanupCount++;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Also clean up old scrobbled timestamps
|
||||
for (const [key, timestamp] of this.scrobbledTimestamps.entries()) {
|
||||
if (now - timestamp > this.SCROBBLE_EXPIRY_MS) {
|
||||
|
|
@ -652,7 +660,7 @@ export class TraktService {
|
|||
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
|
||||
|
|
@ -660,7 +668,7 @@ export class TraktService {
|
|||
cleanupCount++;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Skip verbose cleanup logging to reduce CPU load
|
||||
}
|
||||
|
||||
|
|
@ -703,7 +711,7 @@ export class TraktService {
|
|||
*/
|
||||
public async isAuthenticated(): Promise<boolean> {
|
||||
await this.ensureInitialized();
|
||||
|
||||
|
||||
if (!this.accessToken) {
|
||||
return false;
|
||||
}
|
||||
|
|
@ -908,12 +916,12 @@ export class TraktService {
|
|||
const maxRetries = 3;
|
||||
if (retryCount < maxRetries) {
|
||||
const retryAfter = response.headers.get('Retry-After');
|
||||
const delay = retryAfter
|
||||
? parseInt(retryAfter) * 1000
|
||||
const delay = retryAfter
|
||||
? parseInt(retryAfter) * 1000
|
||||
: Math.min(1000 * Math.pow(2, retryCount), 10000); // Exponential backoff, max 10s
|
||||
|
||||
|
||||
logger.log(`[TraktService] Rate limited (429), retrying in ${delay}ms (attempt ${retryCount + 1}/${maxRetries})`);
|
||||
|
||||
|
||||
await new Promise(resolve => setTimeout(resolve, delay));
|
||||
return this.apiRequest<T>(endpoint, method, body, retryCount + 1);
|
||||
} else {
|
||||
|
|
@ -926,13 +934,13 @@ export class TraktService {
|
|||
if (response.status === 409) {
|
||||
const errorText = await response.text();
|
||||
logger.log(`[TraktService] Content already scrobbled (409) for ${endpoint}:`, errorText);
|
||||
|
||||
|
||||
// Parse the error response to get expiry info
|
||||
try {
|
||||
const errorData = JSON.parse(errorText);
|
||||
if (errorData.watched_at && errorData.expires_at) {
|
||||
logger.log(`[TraktService] Item was already watched at ${errorData.watched_at}, expires at ${errorData.expires_at}`);
|
||||
|
||||
|
||||
// If this is a scrobble endpoint, mark the item as already scrobbled
|
||||
if (endpoint.includes('/scrobble/') && body) {
|
||||
const contentKey = this.getContentKeyFromPayload(body);
|
||||
|
|
@ -942,7 +950,7 @@ export class TraktService {
|
|||
logger.log(`[TraktService] Marked content as already scrobbled: ${contentKey}`);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Return a success-like response for 409 conflicts
|
||||
// This prevents the error from bubbling up and causing retry loops
|
||||
return {
|
||||
|
|
@ -955,7 +963,7 @@ export class TraktService {
|
|||
} catch (parseError) {
|
||||
logger.warn(`[TraktService] Could not parse 409 error response: ${parseError}`);
|
||||
}
|
||||
|
||||
|
||||
// Return a graceful response even if we can't parse the error
|
||||
return {
|
||||
id: 0,
|
||||
|
|
@ -967,7 +975,7 @@ export class TraktService {
|
|||
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text();
|
||||
|
||||
|
||||
// Enhanced error logging for debugging
|
||||
logger.error(`[TraktService] API Error ${response.status} for ${endpoint}:`, {
|
||||
status: response.status,
|
||||
|
|
@ -976,14 +984,14 @@ export class TraktService {
|
|||
requestBody: body ? JSON.stringify(body, null, 2) : 'No body',
|
||||
headers: Object.fromEntries(response.headers.entries())
|
||||
});
|
||||
|
||||
|
||||
// Handle 404 errors more gracefully - they might indicate content not found in Trakt
|
||||
if (response.status === 404) {
|
||||
logger.warn(`[TraktService] Content not found in Trakt database (404) for ${endpoint}. This might indicate:`);
|
||||
logger.warn(`[TraktService] 1. Invalid IMDb ID: ${body?.movie?.ids?.imdb || body?.show?.ids?.imdb || 'N/A'}`);
|
||||
logger.warn(`[TraktService] 2. Content not in Trakt database: ${body?.movie?.title || body?.show?.title || 'N/A'}`);
|
||||
logger.warn(`[TraktService] 3. Authentication issues with token`);
|
||||
|
||||
|
||||
// Return a graceful response for 404s instead of throwing
|
||||
return {
|
||||
id: 0,
|
||||
|
|
@ -992,7 +1000,7 @@ export class TraktService {
|
|||
error: 'Content not found in Trakt database'
|
||||
} as any;
|
||||
}
|
||||
|
||||
|
||||
throw new Error(`API request failed: ${response.status}`);
|
||||
}
|
||||
|
||||
|
|
@ -1016,7 +1024,7 @@ export class TraktService {
|
|||
if (endpoint.includes('/scrobble/')) {
|
||||
// API success logging removed
|
||||
}
|
||||
|
||||
|
||||
return responseData;
|
||||
}
|
||||
|
||||
|
|
@ -1041,7 +1049,7 @@ export class TraktService {
|
|||
*/
|
||||
private isRecentlyScrobbled(contentData: TraktContentData): boolean {
|
||||
const contentKey = this.getWatchingKey(contentData);
|
||||
|
||||
|
||||
// Clean up expired entries
|
||||
const now = Date.now();
|
||||
for (const [key, timestamp] of this.scrobbledTimestamps.entries()) {
|
||||
|
|
@ -1050,7 +1058,7 @@ export class TraktService {
|
|||
this.scrobbledTimestamps.delete(key);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
return this.scrobbledItems.has(contentKey);
|
||||
}
|
||||
|
||||
|
|
@ -1181,7 +1189,7 @@ export class TraktService {
|
|||
if (!images || !images.poster || images.poster.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
// Get the first poster and add https prefix
|
||||
const posterPath = images.poster[0];
|
||||
return posterPath.startsWith('http') ? posterPath : `https://${posterPath}`;
|
||||
|
|
@ -1194,7 +1202,7 @@ export class TraktService {
|
|||
if (!images || !images.fanart || images.fanart.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
// Get the first fanart and add https prefix
|
||||
const fanartPath = images.fanart[0];
|
||||
return fanartPath.startsWith('http') ? fanartPath : `https://${fanartPath}`;
|
||||
|
|
@ -1291,9 +1299,9 @@ export class TraktService {
|
|||
* Add a show episode to user's watched history
|
||||
*/
|
||||
public async addToWatchedEpisodes(
|
||||
imdbId: string,
|
||||
season: number,
|
||||
episode: number,
|
||||
imdbId: string,
|
||||
season: number,
|
||||
episode: number,
|
||||
watchedAt: Date = new Date()
|
||||
): Promise<boolean> {
|
||||
try {
|
||||
|
|
@ -1355,8 +1363,8 @@ export class TraktService {
|
|||
* Check if a show episode is in user's watched history
|
||||
*/
|
||||
public async isEpisodeWatched(
|
||||
imdbId: string,
|
||||
season: number,
|
||||
imdbId: string,
|
||||
season: number,
|
||||
episode: number
|
||||
): Promise<boolean> {
|
||||
try {
|
||||
|
|
@ -1478,19 +1486,19 @@ export class TraktService {
|
|||
*/
|
||||
private validateContentData(contentData: TraktContentData): { isValid: boolean; errors: string[] } {
|
||||
const errors: string[] = [];
|
||||
|
||||
|
||||
if (!contentData.type || !['movie', 'episode'].includes(contentData.type)) {
|
||||
errors.push('Invalid content type');
|
||||
}
|
||||
|
||||
|
||||
if (!contentData.title || contentData.title.trim() === '') {
|
||||
errors.push('Missing or empty title');
|
||||
}
|
||||
|
||||
|
||||
if (!contentData.imdbId || contentData.imdbId.trim() === '') {
|
||||
errors.push('Missing or empty IMDb ID');
|
||||
}
|
||||
|
||||
|
||||
if (contentData.type === 'episode') {
|
||||
if (!contentData.season || contentData.season < 1) {
|
||||
errors.push('Invalid season number');
|
||||
|
|
@ -1505,7 +1513,7 @@ export class TraktService {
|
|||
errors.push('Invalid show year');
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
return {
|
||||
isValid: errors.length === 0,
|
||||
errors
|
||||
|
|
@ -1547,7 +1555,7 @@ export class TraktService {
|
|||
const imdbIdWithPrefix = contentData.imdbId.startsWith('tt')
|
||||
? contentData.imdbId
|
||||
: `tt${contentData.imdbId}`;
|
||||
|
||||
|
||||
const payload = {
|
||||
movie: {
|
||||
title: contentData.title,
|
||||
|
|
@ -1558,7 +1566,7 @@ export class TraktService {
|
|||
},
|
||||
progress: clampedProgress
|
||||
};
|
||||
|
||||
|
||||
logger.log('[TraktService] Movie payload built:', payload);
|
||||
return payload;
|
||||
} else if (contentData.type === 'episode') {
|
||||
|
|
@ -1598,11 +1606,11 @@ export class TraktService {
|
|||
const episodeImdbWithPrefix = contentData.imdbId.startsWith('tt')
|
||||
? contentData.imdbId
|
||||
: `tt${contentData.imdbId}`;
|
||||
|
||||
|
||||
if (!payload.episode.ids) {
|
||||
payload.episode.ids = {};
|
||||
}
|
||||
|
||||
|
||||
payload.episode.ids.imdb = episodeImdbWithPrefix;
|
||||
}
|
||||
|
||||
|
|
@ -1635,7 +1643,7 @@ export class TraktService {
|
|||
} catch (error) {
|
||||
logger.error('[TraktService] Queue request failed:', error);
|
||||
}
|
||||
|
||||
|
||||
// Wait minimum interval before next request
|
||||
if (this.requestQueue.length > 0) {
|
||||
await new Promise(resolve => setTimeout(resolve, this.MIN_API_INTERVAL));
|
||||
|
|
@ -1659,7 +1667,7 @@ export class TraktService {
|
|||
reject(error);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
// Start processing if not already running
|
||||
this.processQueue();
|
||||
});
|
||||
|
|
@ -1702,7 +1710,7 @@ export class TraktService {
|
|||
}
|
||||
|
||||
// Debug log removed to reduce terminal noise
|
||||
|
||||
|
||||
// Only start if not already watching this content
|
||||
if (this.currentlyWatching.has(watchingKey)) {
|
||||
logger.log(`[TraktService] Already watching this content, skipping start: ${contentData.title}`);
|
||||
|
|
@ -1736,10 +1744,10 @@ export class TraktService {
|
|||
}
|
||||
|
||||
const now = Date.now();
|
||||
|
||||
|
||||
const watchingKey = this.getWatchingKey(contentData);
|
||||
const lastSync = this.lastSyncTimes.get(watchingKey) || 0;
|
||||
|
||||
|
||||
// IMMEDIATE SYNC: Remove debouncing for instant sync, only prevent truly rapid calls (< 100ms)
|
||||
if (!force && (now - lastSync) < 100) {
|
||||
return true; // Skip this sync, but return success
|
||||
|
|
@ -1763,7 +1771,7 @@ export class TraktService {
|
|||
logger.warn('[TraktService] Rate limited, will retry later');
|
||||
return true; // Return success to avoid error spam
|
||||
}
|
||||
|
||||
|
||||
logger.error('[TraktService] Failed to update progress:', error);
|
||||
return false;
|
||||
}
|
||||
|
|
@ -1794,7 +1802,7 @@ export class TraktService {
|
|||
// Use pause if below user threshold, stop only when ready to scrobble
|
||||
const useStop = progress >= this.completionThreshold;
|
||||
const result = await this.queueRequest(async () => {
|
||||
return useStop
|
||||
return useStop
|
||||
? await this.stopWatching(contentData, progress)
|
||||
: await this.pauseWatching(contentData, progress);
|
||||
});
|
||||
|
|
@ -1923,8 +1931,8 @@ export class TraktService {
|
|||
* @deprecated Use scrobbleStart, scrobblePause, scrobbleStop instead
|
||||
*/
|
||||
public async syncProgressToTrakt(
|
||||
contentData: TraktContentData,
|
||||
progress: number,
|
||||
contentData: TraktContentData,
|
||||
progress: number,
|
||||
force: boolean = false
|
||||
): Promise<boolean> {
|
||||
// For backward compatibility, treat as a pause update
|
||||
|
|
@ -1937,11 +1945,11 @@ export class TraktService {
|
|||
public async debugTraktConnection(): Promise<any> {
|
||||
try {
|
||||
logger.log('[TraktService] Testing Trakt API connection...');
|
||||
|
||||
|
||||
// Test basic API access
|
||||
const userResponse = await this.apiRequest('/users/me', 'GET');
|
||||
logger.log('[TraktService] User info:', userResponse);
|
||||
|
||||
|
||||
// Test a minimal scrobble start to verify API works
|
||||
const testPayload = {
|
||||
movie: {
|
||||
|
|
@ -1953,19 +1961,19 @@ export class TraktService {
|
|||
},
|
||||
progress: 1.0
|
||||
};
|
||||
|
||||
|
||||
logger.log('[TraktService] Testing scrobble/start endpoint with test payload...');
|
||||
const scrobbleResponse = await this.apiRequest('/scrobble/start', 'POST', testPayload);
|
||||
logger.log('[TraktService] Scrobble test response:', scrobbleResponse);
|
||||
|
||||
return {
|
||||
|
||||
return {
|
||||
authenticated: true,
|
||||
user: userResponse,
|
||||
scrobbleTest: scrobbleResponse
|
||||
user: userResponse,
|
||||
scrobbleTest: scrobbleResponse
|
||||
};
|
||||
} catch (error) {
|
||||
logger.error('[TraktService] Debug connection failed:', error);
|
||||
return {
|
||||
return {
|
||||
authenticated: false,
|
||||
error: error instanceof Error ? error.message : String(error)
|
||||
};
|
||||
|
|
@ -1984,7 +1992,7 @@ export class TraktService {
|
|||
|
||||
const progress = await this.getPlaybackProgress();
|
||||
// Progress logging removed
|
||||
|
||||
|
||||
progress.forEach((item, index) => {
|
||||
if (item.type === 'movie' && item.movie) {
|
||||
// Movie progress logging removed
|
||||
|
|
@ -1992,7 +2000,7 @@ export class TraktService {
|
|||
// Episode progress logging removed
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
if (progress.length === 0) {
|
||||
// No progress logging removed
|
||||
}
|
||||
|
|
@ -2022,16 +2030,16 @@ export class TraktService {
|
|||
public async deletePlaybackForContent(imdbId: string, type: 'movie' | 'series', season?: number, episode?: number): Promise<boolean> {
|
||||
try {
|
||||
logger.log(`🔍 [TraktService] deletePlaybackForContent called for ${type}:${imdbId} (season:${season}, episode:${episode})`);
|
||||
|
||||
|
||||
if (!this.accessToken) {
|
||||
logger.log(`❌ [TraktService] No access token - cannot delete playback`);
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
logger.log(`🔍 [TraktService] Fetching current playback progress...`);
|
||||
const progressItems = await this.getPlaybackProgress();
|
||||
logger.log(`📊 [TraktService] Found ${progressItems.length} playback items`);
|
||||
|
||||
|
||||
const target = progressItems.find(item => {
|
||||
if (type === 'movie' && item.type === 'movie' && item.movie?.ids.imdb === imdbId) {
|
||||
logger.log(`🎯 [TraktService] Found matching movie: ${item.movie?.title}`);
|
||||
|
|
@ -2050,7 +2058,7 @@ export class TraktService {
|
|||
}
|
||||
return false;
|
||||
});
|
||||
|
||||
|
||||
if (target) {
|
||||
logger.log(`🗑️ [TraktService] Deleting playback item with ID: ${target.id}`);
|
||||
const result = await this.deletePlaybackItem(target.id);
|
||||
|
|
@ -2475,7 +2483,7 @@ export class TraktService {
|
|||
// Ensure IMDb ID includes the 'tt' prefix
|
||||
const imdbIdWithPrefix = imdbId.startsWith('tt') ? imdbId : `tt${imdbId}`;
|
||||
|
||||
const payload = type === 'movie'
|
||||
const payload = type === 'movie'
|
||||
? { movies: [{ ids: { imdb: imdbIdWithPrefix } }] }
|
||||
: { shows: [{ ids: { imdb: imdbIdWithPrefix } }] };
|
||||
|
||||
|
|
@ -2500,7 +2508,7 @@ export class TraktService {
|
|||
// Ensure IMDb ID includes the 'tt' prefix
|
||||
const imdbIdWithPrefix = imdbId.startsWith('tt') ? imdbId : `tt${imdbId}`;
|
||||
|
||||
const payload = type === 'movie'
|
||||
const payload = type === 'movie'
|
||||
? { movies: [{ ids: { imdb: imdbIdWithPrefix } }] }
|
||||
: { shows: [{ ids: { imdb: imdbIdWithPrefix } }] };
|
||||
|
||||
|
|
@ -2525,7 +2533,7 @@ export class TraktService {
|
|||
// Ensure IMDb ID includes the 'tt' prefix
|
||||
const imdbIdWithPrefix = imdbId.startsWith('tt') ? imdbId : `tt${imdbId}`;
|
||||
|
||||
const payload = type === 'movie'
|
||||
const payload = type === 'movie'
|
||||
? { movies: [{ ids: { imdb: imdbIdWithPrefix } }] }
|
||||
: { shows: [{ ids: { imdb: imdbIdWithPrefix } }] };
|
||||
|
||||
|
|
@ -2550,7 +2558,7 @@ export class TraktService {
|
|||
// Ensure IMDb ID includes the 'tt' prefix
|
||||
const imdbIdWithPrefix = imdbId.startsWith('tt') ? imdbId : `tt${imdbId}`;
|
||||
|
||||
const payload = type === 'movie'
|
||||
const payload = type === 'movie'
|
||||
? { movies: [{ ids: { imdb: imdbIdWithPrefix } }] }
|
||||
: { shows: [{ ids: { imdb: imdbIdWithPrefix } }] };
|
||||
|
||||
|
|
@ -2575,13 +2583,13 @@ export class TraktService {
|
|||
// Ensure IMDb ID includes the 'tt' prefix
|
||||
const imdbIdWithPrefix = imdbId.startsWith('tt') ? imdbId : `tt${imdbId}`;
|
||||
|
||||
const watchlistItems = type === 'movie'
|
||||
const watchlistItems = type === 'movie'
|
||||
? await this.getWatchlistMovies()
|
||||
: await this.getWatchlistShows();
|
||||
|
||||
return watchlistItems.some(item => {
|
||||
const itemImdbId = type === 'movie'
|
||||
? item.movie?.ids?.imdb
|
||||
const itemImdbId = type === 'movie'
|
||||
? item.movie?.ids?.imdb
|
||||
: item.show?.ids?.imdb;
|
||||
return itemImdbId === imdbIdWithPrefix;
|
||||
});
|
||||
|
|
@ -2603,13 +2611,13 @@ export class TraktService {
|
|||
// Ensure IMDb ID includes the 'tt' prefix
|
||||
const imdbIdWithPrefix = imdbId.startsWith('tt') ? imdbId : `tt${imdbId}`;
|
||||
|
||||
const collectionItems = type === 'movie'
|
||||
const collectionItems = type === 'movie'
|
||||
? await this.getCollectionMovies()
|
||||
: await this.getCollectionShows();
|
||||
|
||||
return collectionItems.some(item => {
|
||||
const itemImdbId = type === 'movie'
|
||||
? item.movie?.ids?.imdb
|
||||
const itemImdbId = type === 'movie'
|
||||
? item.movie?.ids?.imdb
|
||||
: item.show?.ids?.imdb;
|
||||
return itemImdbId === imdbIdWithPrefix;
|
||||
});
|
||||
|
|
@ -2630,7 +2638,7 @@ export class TraktService {
|
|||
this.currentlyWatching.clear();
|
||||
this.lastSyncTimes.clear();
|
||||
this.lastStopCalls.clear();
|
||||
|
||||
|
||||
// Clear request queue to prevent background processing
|
||||
this.requestQueue = [];
|
||||
this.isProcessingQueue = false;
|
||||
|
|
|
|||
Loading…
Reference in a new issue