diff --git a/src/components/home/ContentItem.tsx b/src/components/home/ContentItem.tsx index 2e93eea..702bdab 100644 --- a/src/components/home/ContentItem.tsx +++ b/src/components/home/ContentItem.tsx @@ -11,6 +11,7 @@ import { DropUpMenu } from './DropUpMenu'; import AsyncStorage from '@react-native-async-storage/async-storage'; import { storageService } from '../../services/storageService'; import { TraktService } from '../../services/traktService'; +import { useTraktContext } from '../../contexts/TraktContext'; import Animated, { FadeIn } from 'react-native-reanimated'; interface ContentItemProps { @@ -89,6 +90,9 @@ const ContentItem = ({ item, onPress, shouldLoadImage: shouldLoadImageProp, defe const [isWatched, setIsWatched] = useState(false); const [imageError, setImageError] = useState(false); + // Trakt integration + const { isAuthenticated, isInWatchlist, isInCollection, addToWatchlist, removeFromWatchlist, addToCollection, removeFromCollection } = useTraktContext(); + useEffect(() => { // Reset image error state when item changes, allowing for retry on re-render setImageError(false); @@ -180,8 +184,30 @@ const ContentItem = ({ item, onPress, shouldLoadImage: shouldLoadImageProp, defe Share.share({ message, url, title: item.name }); break; } + case 'trakt-watchlist': { + if (isInWatchlist(item.id, item.type as 'movie' | 'show')) { + await removeFromWatchlist(item.id, item.type as 'movie' | 'show'); + Toast.info('Removed from Trakt Watchlist'); + } else { + await addToWatchlist(item.id, item.type as 'movie' | 'show'); + Toast.success('Added to Trakt Watchlist'); + } + setMenuVisible(false); + break; + } + case 'trakt-collection': { + if (isInCollection(item.id, item.type as 'movie' | 'show')) { + await removeFromCollection(item.id, item.type as 'movie' | 'show'); + Toast.info('Removed from Trakt Collection'); + } else { + await addToCollection(item.id, item.type as 'movie' | 'show'); + Toast.success('Added to Trakt Collection'); + } + setMenuVisible(false); + break; + } } - }, [item, inLibrary, isWatched]); + }, [item, inLibrary, isWatched, isInWatchlist, isInCollection, addToWatchlist, removeFromWatchlist, addToCollection, removeFromCollection]); const handleMenuClose = useCallback(() => { setMenuVisible(false); @@ -282,6 +308,16 @@ const ContentItem = ({ item, onPress, shouldLoadImage: shouldLoadImageProp, defe )} + {isAuthenticated && isInWatchlist(item.id, item.type as 'movie' | 'show') && ( + + + + )} + {isAuthenticated && isInCollection(item.id, item.type as 'movie' | 'show') && ( + + + + )} {settings.showPosterTitles && ( @@ -359,6 +395,22 @@ const styles = StyleSheet.create({ borderRadius: 8, padding: 4, }, + traktWatchlistBadge: { + position: 'absolute', + top: 8, + left: 8, + backgroundColor: 'rgba(231, 76, 60, 0.9)', + borderRadius: 8, + padding: 4, + }, + traktCollectionBadge: { + position: 'absolute', + top: 8, + left: 8, + backgroundColor: 'rgba(52, 152, 219, 0.9)', + borderRadius: 8, + padding: 4, + }, title: { fontSize: 13, fontWeight: '500', diff --git a/src/components/home/DropUpMenu.tsx b/src/components/home/DropUpMenu.tsx index 8df89ef..fe70f35 100644 --- a/src/components/home/DropUpMenu.tsx +++ b/src/components/home/DropUpMenu.tsx @@ -12,6 +12,7 @@ import { } from 'react-native'; import { MaterialIcons } from '@expo/vector-icons'; import FastImage from '@d11/react-native-fast-image'; +import { useTraktContext } from '../../contexts/TraktContext'; import { colors } from '../../styles/colors'; import Animated, { useAnimatedStyle, @@ -43,6 +44,9 @@ export const DropUpMenu = ({ visible, onClose, item, onOptionSelect, isSaved: is const isDarkMode = useColorScheme() === 'dark'; const SNAP_THRESHOLD = 100; + // Trakt integration + const { isAuthenticated, isInWatchlist, isInCollection } = useTraktContext(); + useEffect(() => { if (visible) { opacity.value = withTiming(1, { duration: 200 }); @@ -92,6 +96,9 @@ export const DropUpMenu = ({ visible, onClose, item, onOptionSelect, isSaved: is // Robustly determine if the item is in the library (saved) const isSaved = typeof isSavedProp === 'boolean' ? isSavedProp : !!item.inLibrary; const isWatched = !!isWatchedProp; + const inTraktWatchlist = isAuthenticated && isInWatchlist(item.id, item.type); + const inTraktCollection = isAuthenticated && isInCollection(item.id, item.type); + let menuOptions = [ { icon: 'bookmark', @@ -117,6 +124,22 @@ export const DropUpMenu = ({ visible, onClose, item, onOptionSelect, isSaved: is } ]; + // Add Trakt options if authenticated + if (isAuthenticated) { + menuOptions.push( + { + icon: 'playlist-add-check', + label: inTraktWatchlist ? 'Remove from Trakt Watchlist' : 'Add to Trakt Watchlist', + action: 'trakt-watchlist' + }, + { + icon: 'video-library', + label: inTraktCollection ? 'Remove from Trakt Collection' : 'Add to Trakt Collection', + action: 'trakt-collection' + } + ); + } + // If used in LibraryScreen, only show 'Remove from Library' if item is in library if (isSavedProp === true) { menuOptions = menuOptions.filter(opt => opt.action !== 'library' || isSaved); diff --git a/src/components/metadata/HeroSection.tsx b/src/components/metadata/HeroSection.tsx index 64fef14..7dc2d3a 100644 --- a/src/components/metadata/HeroSection.tsx +++ b/src/components/metadata/HeroSection.tsx @@ -94,6 +94,12 @@ interface HeroSectionProps { getPlayButtonText: () => string; setBannerImage: (bannerImage: string | null) => void; groupedEpisodes?: { [seasonNumber: number]: any[] }; + // Trakt integration props + isAuthenticated?: boolean; + isInWatchlist?: boolean; + isInCollection?: boolean; + onToggleWatchlist?: () => void; + onToggleCollection?: () => void; dynamicBackgroundColor?: string; handleBack: () => void; tmdbId?: number | null; @@ -114,7 +120,13 @@ const ActionButtons = memo(({ groupedEpisodes, metadata, aiChatEnabled, - settings + settings, + // Trakt integration props + isAuthenticated, + isInWatchlist, + isInCollection, + onToggleWatchlist, + onToggleCollection }: { handleShowStreams: () => void; toggleLibrary: () => void; @@ -130,6 +142,12 @@ const ActionButtons = memo(({ metadata: any; aiChatEnabled?: boolean; settings: any; + // Trakt integration props + isAuthenticated?: boolean; + isInWatchlist?: boolean; + isInCollection?: boolean; + onToggleWatchlist?: () => void; + onToggleCollection?: () => void; }) => { const { currentTheme } = useTheme(); @@ -365,6 +383,59 @@ const ActionButtons = memo(({ )} + {/* Trakt Action Buttons */} + {isAuthenticated && ( + <> + + {Platform.OS === 'ios' ? ( + GlassViewComp && liquidGlassAvailable ? ( + + ) : ( + + ) + ) : ( + + )} + + + + + {Platform.OS === 'ios' ? ( + GlassViewComp && liquidGlassAvailable ? ( + + ) : ( + + ) + ) : ( + + )} + + + + )} + {type === 'series' && ( = memo(({ dynamicBackgroundColor, handleBack, tmdbId, + // Trakt integration props + isAuthenticated, + isInWatchlist, + isInCollection, + onToggleWatchlist, + onToggleCollection }) => { const { currentTheme } = useTheme(); const { isAuthenticated: isTraktAuthenticated } = useTraktContext(); @@ -1700,6 +1777,12 @@ const HeroSection: React.FC = memo(({ metadata={metadata} aiChatEnabled={settings?.aiChatEnabled} settings={settings} + // Trakt integration props + isAuthenticated={isAuthenticated} + isInWatchlist={isInWatchlist} + isInCollection={isInCollection} + onToggleWatchlist={onToggleWatchlist} + onToggleCollection={onToggleCollection} /> @@ -1886,6 +1969,16 @@ const styles = StyleSheet.create({ justifyContent: 'center', overflow: 'hidden', }, + traktButton: { + width: 50, + height: 50, + borderRadius: 25, + borderWidth: 1.5, + borderColor: 'rgba(255,255,255,0.7)', + alignItems: 'center', + justifyContent: 'center', + overflow: 'hidden', + }, playButtonText: { color: '#000', fontWeight: '700', @@ -2210,6 +2303,11 @@ const styles = StyleSheet.create({ height: 60, borderRadius: 30, }, + tabletTraktButton: { + width: 60, + height: 60, + borderRadius: 30, + }, tabletHeroTitle: { fontSize: 36, fontWeight: '900', diff --git a/src/contexts/TraktContext.tsx b/src/contexts/TraktContext.tsx index 1cc30fe..0f8c181 100644 --- a/src/contexts/TraktContext.tsx +++ b/src/contexts/TraktContext.tsx @@ -30,6 +30,13 @@ interface TraktContextProps { markMovieAsWatched: (imdbId: string, watchedAt?: Date) => Promise; markEpisodeAsWatched: (imdbId: string, season: number, episode: number, watchedAt?: Date) => Promise; forceSyncTraktProgress?: () => Promise; + // Trakt content management + addToWatchlist: (imdbId: string, type: 'movie' | 'show') => Promise; + removeFromWatchlist: (imdbId: string, type: 'movie' | 'show') => Promise; + addToCollection: (imdbId: string, type: 'movie' | 'show') => Promise; + removeFromCollection: (imdbId: string, type: 'movie' | 'show') => Promise; + isInWatchlist: (imdbId: string, type: 'movie' | 'show') => boolean; + isInCollection: (imdbId: string, type: 'movie' | 'show') => boolean; } const TraktContext = createContext(undefined); diff --git a/src/hooks/useTraktIntegration.ts b/src/hooks/useTraktIntegration.ts index c06f177..0a585f5 100644 --- a/src/hooks/useTraktIntegration.ts +++ b/src/hooks/useTraktIntegration.ts @@ -26,6 +26,10 @@ export function useTraktIntegration() { const [continueWatching, setContinueWatching] = useState([]); const [ratedContent, setRatedContent] = useState([]); const [lastAuthCheck, setLastAuthCheck] = useState(Date.now()); + + // State for real-time status tracking + const [watchlistItems, setWatchlistItems] = useState>(new Set()); + const [collectionItems, setCollectionItems] = useState>(new Set()); // Check authentication status const checkAuthStatus = useCallback(async () => { @@ -108,6 +112,39 @@ export function useTraktIntegration() { setCollectionShows(collectionShows); setContinueWatching(continueWatching); setRatedContent(ratings); + + // Populate watchlist and collection sets for quick lookups + const newWatchlistItems = new Set(); + const newCollectionItems = new Set(); + + // Add movies to sets + watchlistMovies.forEach(item => { + if (item.movie?.ids?.imdb) { + newWatchlistItems.add(`movie:${item.movie.ids.imdb}`); + } + }); + + collectionMovies.forEach(item => { + if (item.movie?.ids?.imdb) { + newCollectionItems.add(`movie:${item.movie.ids.imdb}`); + } + }); + + // Add shows to sets + watchlistShows.forEach(item => { + if (item.show?.ids?.imdb) { + newWatchlistItems.add(`show:${item.show.ids.imdb}`); + } + }); + + collectionShows.forEach(item => { + if (item.show?.ids?.imdb) { + newCollectionItems.add(`show:${item.show.ids.imdb}`); + } + }); + + setWatchlistItems(newWatchlistItems); + setCollectionItems(newCollectionItems); } catch (error) { logger.error('[useTraktIntegration] Error loading all collections:', error); } finally { @@ -163,6 +200,105 @@ export function useTraktIntegration() { } }, [isAuthenticated, loadWatchedItems]); + // Add content to Trakt watchlist + const addToWatchlist = useCallback(async (imdbId: string, type: 'movie' | 'show'): Promise => { + if (!isAuthenticated) return false; + + try { + const success = await traktService.addToWatchlist(imdbId, type); + if (success) { + // Ensure consistent IMDb ID format (with 'tt' prefix) + const normalizedImdbId = imdbId.startsWith('tt') ? imdbId : `tt${imdbId}`; + setWatchlistItems(prev => new Set(prev).add(`${type}:${normalizedImdbId}`)); + // Don't refresh immediately - let the local state handle the UI update + // The data will be refreshed on next app focus or manual refresh + } + return success; + } catch (error) { + logger.error('[useTraktIntegration] Error adding to watchlist:', error); + return false; + } + }, [isAuthenticated]); + + // Remove content from Trakt watchlist + const removeFromWatchlist = useCallback(async (imdbId: string, type: 'movie' | 'show'): Promise => { + if (!isAuthenticated) return false; + + try { + const success = await traktService.removeFromWatchlist(imdbId, type); + if (success) { + // Ensure consistent IMDb ID format (with 'tt' prefix) + const normalizedImdbId = imdbId.startsWith('tt') ? imdbId : `tt${imdbId}`; + setWatchlistItems(prev => { + const newSet = new Set(prev); + newSet.delete(`${type}:${normalizedImdbId}`); + return newSet; + }); + // Don't refresh immediately - let the local state handle the UI update + } + return success; + } catch (error) { + logger.error('[useTraktIntegration] Error removing from watchlist:', error); + return false; + } + }, [isAuthenticated]); + + // Add content to Trakt collection + const addToCollection = useCallback(async (imdbId: string, type: 'movie' | 'show'): Promise => { + if (!isAuthenticated) return false; + + try { + const success = await traktService.addToCollection(imdbId, type); + if (success) { + // Ensure consistent IMDb ID format (with 'tt' prefix) + const normalizedImdbId = imdbId.startsWith('tt') ? imdbId : `tt${imdbId}`; + setCollectionItems(prev => new Set(prev).add(`${type}:${normalizedImdbId}`)); + // Don't refresh immediately - let the local state handle the UI update + } + return success; + } catch (error) { + logger.error('[useTraktIntegration] Error adding to collection:', error); + return false; + } + }, [isAuthenticated]); + + // Remove content from Trakt collection + const removeFromCollection = useCallback(async (imdbId: string, type: 'movie' | 'show'): Promise => { + if (!isAuthenticated) return false; + + try { + const success = await traktService.removeFromCollection(imdbId, type); + if (success) { + // Ensure consistent IMDb ID format (with 'tt' prefix) + const normalizedImdbId = imdbId.startsWith('tt') ? imdbId : `tt${imdbId}`; + setCollectionItems(prev => { + const newSet = new Set(prev); + newSet.delete(`${type}:${normalizedImdbId}`); + return newSet; + }); + // Don't refresh immediately - let the local state handle the UI update + } + return success; + } catch (error) { + logger.error('[useTraktIntegration] Error removing from collection:', error); + return false; + } + }, [isAuthenticated]); + + // Check if content is in Trakt watchlist + const isInWatchlist = useCallback((imdbId: string, type: 'movie' | 'show'): boolean => { + // Ensure consistent IMDb ID format (with 'tt' prefix) + const normalizedImdbId = imdbId.startsWith('tt') ? imdbId : `tt${imdbId}`; + return watchlistItems.has(`${type}:${normalizedImdbId}`); + }, [watchlistItems]); + + // Check if content is in Trakt collection + const isInCollection = useCallback((imdbId: string, type: 'movie' | 'show'): boolean => { + // Ensure consistent IMDb ID format (with 'tt' prefix) + const normalizedImdbId = imdbId.startsWith('tt') ? imdbId : `tt${imdbId}`; + return collectionItems.has(`${type}:${normalizedImdbId}`); + }, [collectionItems]); + // Mark an episode as watched const markEpisodeAsWatched = useCallback(async ( imdbId: string, @@ -530,6 +666,13 @@ export function useTraktIntegration() { getTraktPlaybackProgress, syncAllProgress, fetchAndMergeTraktProgress, - forceSyncTraktProgress // For manual testing + forceSyncTraktProgress, // For manual testing + // Trakt content management + addToWatchlist, + removeFromWatchlist, + addToCollection, + removeFromCollection, + isInWatchlist, + isInCollection }; } \ No newline at end of file diff --git a/src/screens/MetadataScreen.tsx b/src/screens/MetadataScreen.tsx index 40043de..7e94b3e 100644 --- a/src/screens/MetadataScreen.tsx +++ b/src/screens/MetadataScreen.tsx @@ -17,6 +17,7 @@ import { useRoute, useNavigation, useFocusEffect } from '@react-navigation/nativ import { MaterialIcons } from '@expo/vector-icons'; import * as Haptics from 'expo-haptics'; import { useTheme } from '../contexts/ThemeContext'; +import { useTraktContext } from '../contexts/TraktContext'; import { useMetadata } from '../hooks/useMetadata'; import { useDominantColor, preloadDominantColor } from '../hooks/useDominantColor'; import { CastSection } from '../components/metadata/CastSection'; @@ -86,6 +87,9 @@ const MetadataScreen: React.FC = () => { const { top: safeAreaTop } = useSafeAreaInsets(); const { pauseTrailer } = useTrailer(); + // Trakt integration + const { isAuthenticated, isInWatchlist, isInCollection, addToWatchlist, removeFromWatchlist, addToCollection, removeFromCollection } = useTraktContext(); + // Optimized state management - reduced state variables const [isContentReady, setIsContentReady] = useState(false); const [showCastModal, setShowCastModal] = useState(false); @@ -923,6 +927,24 @@ const MetadataScreen: React.FC = () => { getPlayButtonText={watchProgressData.getPlayButtonText} setBannerImage={assetData.setBannerImage} groupedEpisodes={groupedEpisodes} + // Trakt integration props + isAuthenticated={isAuthenticated} + isInWatchlist={isInWatchlist(id, type as 'movie' | 'show')} + isInCollection={isInCollection(id, type as 'movie' | 'show')} + onToggleWatchlist={async () => { + if (isInWatchlist(id, type as 'movie' | 'show')) { + await removeFromWatchlist(id, type as 'movie' | 'show'); + } else { + await addToWatchlist(id, type as 'movie' | 'show'); + } + }} + onToggleCollection={async () => { + if (isInCollection(id, type as 'movie' | 'show')) { + await removeFromCollection(id, type as 'movie' | 'show'); + } else { + await addToCollection(id, type as 'movie' | 'show'); + } + }} dynamicBackgroundColor={dynamicBackgroundColor} handleBack={handleBack} tmdbId={tmdbId} diff --git a/src/services/traktService.ts b/src/services/traktService.ts index 2f42040..c8e2627 100644 --- a/src/services/traktService.ts +++ b/src/services/traktService.ts @@ -1212,10 +1212,10 @@ export class TraktService { // Try multiple search approaches const searchUrls = [ - `${TRAKT_API_URL}/search/${type}?id_type=imdb&id=${cleanImdbId}`, - `${TRAKT_API_URL}/search/${type}?query=${encodeURIComponent(cleanImdbId)}&id_type=imdb`, + `${TRAKT_API_URL}/search/${type === 'show' ? 'shows' : type}?id_type=imdb&id=${cleanImdbId}`, + `${TRAKT_API_URL}/search/${type === 'show' ? 'shows' : type}?query=${encodeURIComponent(cleanImdbId)}&id_type=imdb`, // Also try with the full tt-prefixed ID in case the API accepts it - `${TRAKT_API_URL}/search/${type}?id_type=imdb&id=tt${cleanImdbId}` + `${TRAKT_API_URL}/search/${type === 'show' ? 'shows' : type}?id_type=imdb&id=tt${cleanImdbId}` ]; for (const searchUrl of searchUrls) { @@ -1240,7 +1240,7 @@ export class TraktService { logger.log(`[TraktService] Search response data:`, data); if (data && data.length > 0) { - const traktId = data[0][type]?.ids?.trakt; + const traktId = data[0][type === 'show' ? 'show' : type]?.ids?.trakt; if (traktId) { logger.log(`[TraktService] Found Trakt ID: ${traktId} for IMDb ID: ${cleanImdbId}`); return traktId; @@ -2339,7 +2339,7 @@ export class TraktService { try { logger.log(`[TraktService] Searching Trakt for ${type} with TMDB ID: ${tmdbId}`); - const response = await fetch(`${TRAKT_API_URL}/search/${type}?id_type=tmdb&id=${tmdbId}`, { + const response = await fetch(`${TRAKT_API_URL}/search/${type === 'show' ? 'shows' : type}?id_type=tmdb&id=${tmdbId}`, { headers: { 'Content-Type': 'application/json', 'trakt-api-version': '2', @@ -2356,7 +2356,7 @@ export class TraktService { const data = await response.json(); logger.log(`[TraktService] TMDB search response:`, data); if (data && data.length > 0) { - const traktId = data[0][type]?.ids?.trakt; + const traktId = data[0][type === 'show' ? 'show' : type]?.ids?.trakt; if (traktId) { logger.log(`[TraktService] Found Trakt ID via TMDB: ${traktId} for TMDB ID: ${tmdbId}`); return traktId; @@ -2463,6 +2463,162 @@ export class TraktService { } } + /** + * Add content to Trakt watchlist + */ + public async addToWatchlist(imdbId: string, type: 'movie' | 'show'): Promise { + try { + if (!await this.isAuthenticated()) { + return false; + } + + // Ensure IMDb ID includes the 'tt' prefix + const imdbIdWithPrefix = imdbId.startsWith('tt') ? imdbId : `tt${imdbId}`; + + const payload = type === 'movie' + ? { movies: [{ ids: { imdb: imdbIdWithPrefix } }] } + : { shows: [{ ids: { imdb: imdbIdWithPrefix } }] }; + + await this.apiRequest('/sync/watchlist', 'POST', payload); + logger.log(`[TraktService] Added ${type} to watchlist: ${imdbId}`); + return true; + } catch (error) { + logger.error(`[TraktService] Failed to add ${type} to watchlist:`, error); + return false; + } + } + + /** + * Remove content from Trakt watchlist + */ + public async removeFromWatchlist(imdbId: string, type: 'movie' | 'show'): Promise { + try { + if (!await this.isAuthenticated()) { + return false; + } + + // Ensure IMDb ID includes the 'tt' prefix + const imdbIdWithPrefix = imdbId.startsWith('tt') ? imdbId : `tt${imdbId}`; + + const payload = type === 'movie' + ? { movies: [{ ids: { imdb: imdbIdWithPrefix } }] } + : { shows: [{ ids: { imdb: imdbIdWithPrefix } }] }; + + await this.apiRequest('/sync/watchlist/remove', 'POST', payload); + logger.log(`[TraktService] Removed ${type} from watchlist: ${imdbId}`); + return true; + } catch (error) { + logger.error(`[TraktService] Failed to remove ${type} from watchlist:`, error); + return false; + } + } + + /** + * Add content to Trakt collection + */ + public async addToCollection(imdbId: string, type: 'movie' | 'show'): Promise { + try { + if (!await this.isAuthenticated()) { + return false; + } + + // Ensure IMDb ID includes the 'tt' prefix + const imdbIdWithPrefix = imdbId.startsWith('tt') ? imdbId : `tt${imdbId}`; + + const payload = type === 'movie' + ? { movies: [{ ids: { imdb: imdbIdWithPrefix } }] } + : { shows: [{ ids: { imdb: imdbIdWithPrefix } }] }; + + await this.apiRequest('/sync/collection', 'POST', payload); + logger.log(`[TraktService] Added ${type} to collection: ${imdbId}`); + return true; + } catch (error) { + logger.error(`[TraktService] Failed to add ${type} to collection:`, error); + return false; + } + } + + /** + * Remove content from Trakt collection + */ + public async removeFromCollection(imdbId: string, type: 'movie' | 'show'): Promise { + try { + if (!await this.isAuthenticated()) { + return false; + } + + // Ensure IMDb ID includes the 'tt' prefix + const imdbIdWithPrefix = imdbId.startsWith('tt') ? imdbId : `tt${imdbId}`; + + const payload = type === 'movie' + ? { movies: [{ ids: { imdb: imdbIdWithPrefix } }] } + : { shows: [{ ids: { imdb: imdbIdWithPrefix } }] }; + + await this.apiRequest('/sync/collection/remove', 'POST', payload); + logger.log(`[TraktService] Removed ${type} from collection: ${imdbId}`); + return true; + } catch (error) { + logger.error(`[TraktService] Failed to remove ${type} from collection:`, error); + return false; + } + } + + /** + * Check if content is in Trakt watchlist + */ + public async isInWatchlist(imdbId: string, type: 'movie' | 'show'): Promise { + try { + if (!await this.isAuthenticated()) { + return false; + } + + // Ensure IMDb ID includes the 'tt' prefix + const imdbIdWithPrefix = imdbId.startsWith('tt') ? imdbId : `tt${imdbId}`; + + const watchlistItems = type === 'movie' + ? await this.getWatchlistMovies() + : await this.getWatchlistShows(); + + return watchlistItems.some(item => { + const itemImdbId = type === 'movie' + ? item.movie?.ids?.imdb + : item.show?.ids?.imdb; + return itemImdbId === imdbIdWithPrefix; + }); + } catch (error) { + logger.error(`[TraktService] Failed to check if ${type} is in watchlist:`, error); + return false; + } + } + + /** + * Check if content is in Trakt collection + */ + public async isInCollection(imdbId: string, type: 'movie' | 'show'): Promise { + try { + if (!await this.isAuthenticated()) { + return false; + } + + // Ensure IMDb ID includes the 'tt' prefix + const imdbIdWithPrefix = imdbId.startsWith('tt') ? imdbId : `tt${imdbId}`; + + const collectionItems = type === 'movie' + ? await this.getCollectionMovies() + : await this.getCollectionShows(); + + return collectionItems.some(item => { + const itemImdbId = type === 'movie' + ? item.movie?.ids?.imdb + : item.show?.ids?.imdb; + return itemImdbId === imdbIdWithPrefix; + }); + } catch (error) { + logger.error(`[TraktService] Failed to check if ${type} is in collection:`, error); + return false; + } + } + /** * Handle app state changes to reduce memory pressure */