trakt wathclist integration test

This commit is contained in:
tapframe 2025-10-19 14:20:38 +05:30
parent 08f356cfa4
commit a7f850d577
7 changed files with 510 additions and 9 deletions

View file

@ -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
<Feather name="bookmark" size={16} color={currentTheme.colors.white} />
</View>
)}
{isAuthenticated && isInWatchlist(item.id, item.type as 'movie' | 'show') && (
<View style={styles.traktWatchlistBadge}>
<MaterialIcons name="playlist-add-check" size={16} color="#E74C3C" />
</View>
)}
{isAuthenticated && isInCollection(item.id, item.type as 'movie' | 'show') && (
<View style={styles.traktCollectionBadge}>
<MaterialIcons name="video-library" size={16} color="#3498DB" />
</View>
)}
</View>
</TouchableOpacity>
{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',

View file

@ -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);

View file

@ -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(({
</TouchableOpacity>
)}
{/* Trakt Action Buttons */}
{isAuthenticated && (
<>
<TouchableOpacity
style={[styles.actionButton, styles.traktButton, isTablet && styles.tabletTraktButton]}
onPress={onToggleWatchlist}
activeOpacity={0.85}
>
{Platform.OS === 'ios' ? (
GlassViewComp && liquidGlassAvailable ? (
<GlassViewComp
style={styles.blurBackgroundRound}
glassEffectStyle="regular"
/>
) : (
<ExpoBlurView intensity={80} style={styles.blurBackgroundRound} tint="dark" />
)
) : (
<View style={styles.androidFallbackBlurRound} />
)}
<MaterialIcons
name={isInWatchlist ? "playlist-add-check" : "playlist-add"}
size={isTablet ? 28 : 24}
color={isInWatchlist ? "#E74C3C" : currentTheme.colors.white}
/>
</TouchableOpacity>
<TouchableOpacity
style={[styles.actionButton, styles.traktButton, isTablet && styles.tabletTraktButton]}
onPress={onToggleCollection}
activeOpacity={0.85}
>
{Platform.OS === 'ios' ? (
GlassViewComp && liquidGlassAvailable ? (
<GlassViewComp
style={styles.blurBackgroundRound}
glassEffectStyle="regular"
/>
) : (
<ExpoBlurView intensity={80} style={styles.blurBackgroundRound} tint="dark" />
)
) : (
<View style={styles.androidFallbackBlurRound} />
)}
<MaterialIcons
name={isInCollection ? "video-library" : "video-library"}
size={isTablet ? 28 : 24}
color={isInCollection ? "#3498DB" : currentTheme.colors.white}
/>
</TouchableOpacity>
</>
)}
{type === 'series' && (
<TouchableOpacity
style={[styles.iconButton, isTablet && styles.tabletIconButton]}
@ -792,6 +863,12 @@ const HeroSection: React.FC<HeroSectionProps> = 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<HeroSectionProps> = memo(({
metadata={metadata}
aiChatEnabled={settings?.aiChatEnabled}
settings={settings}
// Trakt integration props
isAuthenticated={isAuthenticated}
isInWatchlist={isInWatchlist}
isInCollection={isInCollection}
onToggleWatchlist={onToggleWatchlist}
onToggleCollection={onToggleCollection}
/>
</View>
</LinearGradient>
@ -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',

View file

@ -30,6 +30,13 @@ interface TraktContextProps {
markMovieAsWatched: (imdbId: string, watchedAt?: Date) => Promise<boolean>;
markEpisodeAsWatched: (imdbId: string, season: number, episode: number, watchedAt?: Date) => Promise<boolean>;
forceSyncTraktProgress?: () => Promise<boolean>;
// Trakt content management
addToWatchlist: (imdbId: string, type: 'movie' | 'show') => Promise<boolean>;
removeFromWatchlist: (imdbId: string, type: 'movie' | 'show') => Promise<boolean>;
addToCollection: (imdbId: string, type: 'movie' | 'show') => Promise<boolean>;
removeFromCollection: (imdbId: string, type: 'movie' | 'show') => Promise<boolean>;
isInWatchlist: (imdbId: string, type: 'movie' | 'show') => boolean;
isInCollection: (imdbId: string, type: 'movie' | 'show') => boolean;
}
const TraktContext = createContext<TraktContextProps | undefined>(undefined);

View file

@ -26,6 +26,10 @@ export function useTraktIntegration() {
const [continueWatching, setContinueWatching] = useState<TraktPlaybackItem[]>([]);
const [ratedContent, setRatedContent] = useState<TraktRatingItem[]>([]);
const [lastAuthCheck, setLastAuthCheck] = useState<number>(Date.now());
// State for real-time status tracking
const [watchlistItems, setWatchlistItems] = useState<Set<string>>(new Set());
const [collectionItems, setCollectionItems] = useState<Set<string>>(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<string>();
const newCollectionItems = new Set<string>();
// 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<boolean> => {
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<boolean> => {
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<boolean> => {
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<boolean> => {
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
};
}

View file

@ -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}

View file

@ -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<boolean> {
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<boolean> {
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<boolean> {
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<boolean> {
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<boolean> {
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<boolean> {
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
*/