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
*/