chore: improved tmdb enrichment logic

This commit is contained in:
tapframe 2026-01-04 15:37:49 +05:30
parent 3d5a9ebf42
commit 4aa22cc1c3
8 changed files with 413 additions and 343 deletions

View file

@ -283,7 +283,7 @@ const ContinueWatchingSection = React.forwardRef<ContinueWatchingRef>((props, re
if (!watchedSet || !showId) return false; if (!watchedSet || !showId) return false;
const cleanShowId = showId.startsWith('tt') ? showId : `tt${showId}`; const cleanShowId = showId.startsWith('tt') ? showId : `tt${showId}`;
return watchedSet.has(`${cleanShowId}:${season}:${episode}`) || return watchedSet.has(`${cleanShowId}:${season}:${episode}`) ||
watchedSet.has(`${showId}:${season}:${episode}`); watchedSet.has(`${showId}:${season}:${episode}`);
}; };
for (const video of sortedVideos) { for (const video of sortedVideos) {
@ -379,7 +379,7 @@ const ContinueWatchingSection = React.forwardRef<ContinueWatchingRef>((props, re
const progressPercent = const progressPercent =
progress.duration > 0 progress.duration > 0
? (progress.currentTime / progress.duration) * 100 ? (progress.currentTime / progress.duration) * 100
: 0; : 0;
// Skip fully watched movies // Skip fully watched movies
if (type === 'movie' && progressPercent >= 85) continue; if (type === 'movie' && progressPercent >= 85) continue;
// Skip movies with no actual progress (ensure > 0%) // Skip movies with no actual progress (ensure > 0%)
@ -711,7 +711,7 @@ const ContinueWatchingSection = React.forwardRef<ContinueWatchingRef>((props, re
const movieKey = `movie:${imdbId}`; const movieKey = `movie:${imdbId}`;
if (recentlyRemovedRef.current.has(movieKey)) continue; if (recentlyRemovedRef.current.has(movieKey)) continue;
const cachedData = await getCachedMetadata('movie', imdbId, item.addonId); const cachedData = await getCachedMetadata('movie', imdbId);
if (!cachedData?.basicContent) continue; if (!cachedData?.basicContent) continue;
const pausedAt = new Date(item.paused_at).getTime(); const pausedAt = new Date(item.paused_at).getTime();
@ -743,7 +743,7 @@ const ContinueWatchingSection = React.forwardRef<ContinueWatchingRef>((props, re
continue; continue;
} }
const cachedData = await getCachedMetadata('series', showImdb, item.addonId); const cachedData = await getCachedMetadata('series', showImdb);
if (!cachedData?.basicContent) continue; if (!cachedData?.basicContent) continue;
traktBatch.push({ traktBatch.push({
@ -1204,41 +1204,40 @@ const ContinueWatchingSection = React.forwardRef<ContinueWatchingRef>((props, re
padding: isTV ? 16 : isLargeTablet ? 14 : isTablet ? 12 : 12 padding: isTV ? 16 : isLargeTablet ? 14 : isTablet ? 12 : 12
} }
]}> ]}>
{(() => { {(() => {
const isUpNext = item.type === 'series' && item.progress === 0; const isUpNext = item.type === 'series' && item.progress === 0;
return ( return (
<View style={styles.titleRow}> <View style={styles.titleRow}>
<Text <Text
style={[ style={[
styles.contentTitle, styles.contentTitle,
{ {
color: currentTheme.colors.highEmphasis, color: currentTheme.colors.highEmphasis,
fontSize: isTV ? 20 : isLargeTablet ? 18 : isTablet ? 17 : 16 fontSize: isTV ? 20 : isLargeTablet ? 18 : isTablet ? 17 : 16
} }
]} ]}
numberOfLines={1} numberOfLines={1}
> >
{item.name} {item.name}
</Text> </Text>
{isUpNext && ( {isUpNext && (
<View style={[ <View style={[
styles.progressBadge, styles.progressBadge,
{ {
backgroundColor: currentTheme.colors.primary, backgroundColor: currentTheme.colors.primary,
paddingHorizontal: isTV ? 12 : isLargeTablet ? 10 : isTablet ? 8 : 8, paddingHorizontal: isTV ? 12 : isLargeTablet ? 10 : isTablet ? 8 : 8,
paddingVertical: isTV ? 6 : isLargeTablet ? 5 : isTablet ? 4 : 3 paddingVertical: isTV ? 6 : isLargeTablet ? 5 : isTablet ? 4 : 3
} }
]}> ]}>
<Text style={[ <Text style={[
styles.progressText, styles.progressText,
{ fontSize: isTV ? 14 : isLargeTablet ? 13 : isTablet ? 12 : 12 } { fontSize: isTV ? 14 : isLargeTablet ? 13 : isTablet ? 12 : 12 }
]}>Up Next</Text> ]}>Up Next</Text>
</View> </View>
)} )}
</View> </View>
); );
})()} })()}
</View>
{/* Episode Info or Year */} {/* Episode Info or Year */}
{(() => { {(() => {

View file

@ -6,7 +6,8 @@ import {
TraktWatchlistItem, TraktWatchlistItem,
TraktCollectionItem, TraktCollectionItem,
TraktRatingItem, TraktRatingItem,
TraktPlaybackItem TraktPlaybackItem,
traktService
} from '../services/traktService'; } from '../services/traktService';
interface TraktContextProps { interface TraktContextProps {
@ -37,6 +38,9 @@ interface TraktContextProps {
removeFromCollection: (imdbId: string, type: 'movie' | 'show') => Promise<boolean>; removeFromCollection: (imdbId: string, type: 'movie' | 'show') => Promise<boolean>;
isInWatchlist: (imdbId: string, type: 'movie' | 'show') => boolean; isInWatchlist: (imdbId: string, type: 'movie' | 'show') => boolean;
isInCollection: (imdbId: string, type: 'movie' | 'show') => boolean; isInCollection: (imdbId: string, type: 'movie' | 'show') => boolean;
// Maintenance mode
isMaintenanceMode: boolean;
maintenanceMessage: string;
} }
const TraktContext = createContext<TraktContextProps | undefined>(undefined); const TraktContext = createContext<TraktContextProps | undefined>(undefined);
@ -44,8 +48,15 @@ const TraktContext = createContext<TraktContextProps | undefined>(undefined);
export function TraktProvider({ children }: { children: ReactNode }) { export function TraktProvider({ children }: { children: ReactNode }) {
const traktIntegration = useTraktIntegration(); const traktIntegration = useTraktIntegration();
// Add maintenance mode values to the context
const contextValue: TraktContextProps = {
...traktIntegration,
isMaintenanceMode: traktService.isMaintenanceMode(),
maintenanceMessage: traktService.getMaintenanceMessage(),
};
return ( return (
<TraktContext.Provider value={traktIntegration}> <TraktContext.Provider value={contextValue}>
{children} {children}
</TraktContext.Provider> </TraktContext.Provider>
); );

View file

@ -550,7 +550,7 @@ export const useMetadata = ({ id, type, addonId }: UseMetadataProps): UseMetadat
if (__DEV__) logger.log('Fetching movie details from TMDB for:', tmdbId); if (__DEV__) logger.log('Fetching movie details from TMDB for:', tmdbId);
const movieDetails = await tmdbService.getMovieDetails( const movieDetails = await tmdbService.getMovieDetails(
tmdbId, tmdbId,
settings.useTmdbLocalizedMetadata ? `${settings.tmdbLanguagePreference || 'en'}-US` : 'en-US' settings.useTmdbLocalizedMetadata ? (settings.tmdbLanguagePreference || 'en') : 'en'
); );
if (movieDetails) { if (movieDetails) {
const imdbId = movieDetails.imdb_id || movieDetails.external_ids?.imdb_id; const imdbId = movieDetails.imdb_id || movieDetails.external_ids?.imdb_id;
@ -634,7 +634,7 @@ export const useMetadata = ({ id, type, addonId }: UseMetadataProps): UseMetadat
try { try {
const showDetails = await tmdbService.getTVShowDetails( const showDetails = await tmdbService.getTVShowDetails(
parseInt(tmdbId), parseInt(tmdbId),
settings.useTmdbLocalizedMetadata ? `${settings.tmdbLanguagePreference || 'en'}-US` : 'en-US' settings.useTmdbLocalizedMetadata ? (settings.tmdbLanguagePreference || 'en') : 'en'
); );
if (showDetails) { if (showDetails) {
// OPTIMIZATION: Fetch external IDs, credits, and logo in parallel // OPTIMIZATION: Fetch external IDs, credits, and logo in parallel
@ -824,9 +824,9 @@ export const useMetadata = ({ id, type, addonId }: UseMetadataProps): UseMetadat
// Store addon logo before TMDB enrichment overwrites it // Store addon logo before TMDB enrichment overwrites it
const addonLogo = (finalMetadata as any).logo; const addonLogo = (finalMetadata as any).logo;
// If localization is enabled, merge TMDB localized text (name/overview) before first render // If localization is enabled AND title/description enrichment is enabled, merge TMDB localized text (name/overview) before first render
try { try {
if (settings.enrichMetadataWithTMDB && settings.useTmdbLocalizedMetadata) { if (settings.enrichMetadataWithTMDB && settings.useTmdbLocalizedMetadata && settings.tmdbEnrichTitleDescription) {
const tmdbSvc = TMDBService.getInstance(); const tmdbSvc = TMDBService.getInstance();
let finalTmdbId: number | null = tmdbId; let finalTmdbId: number | null = tmdbId;
if (!finalTmdbId) { if (!finalTmdbId) {
@ -857,8 +857,8 @@ export const useMetadata = ({ id, type, addonId }: UseMetadataProps): UseMetadat
finalMetadata = { finalMetadata = {
...finalMetadata, ...finalMetadata,
name: finalMetadata.name || localized.title, name: localized.title || finalMetadata.name,
description: finalMetadata.description || localized.overview, description: localized.overview || finalMetadata.description,
movieDetails: movieDetailsObj, movieDetails: movieDetailsObj,
...(productionInfo.length > 0 && { networks: productionInfo }), ...(productionInfo.length > 0 && { networks: productionInfo }),
}; };
@ -894,8 +894,8 @@ export const useMetadata = ({ id, type, addonId }: UseMetadataProps): UseMetadat
finalMetadata = { finalMetadata = {
...finalMetadata, ...finalMetadata,
name: finalMetadata.name || localized.name, name: localized.name || finalMetadata.name,
description: finalMetadata.description || localized.overview, description: localized.overview || finalMetadata.description,
tvDetails, tvDetails,
...(productionInfo.length > 0 && { networks: productionInfo }), ...(productionInfo.length > 0 && { networks: productionInfo }),
}; };
@ -909,14 +909,8 @@ export const useMetadata = ({ id, type, addonId }: UseMetadataProps): UseMetadat
// Centralized logo fetching logic // Centralized logo fetching logic
try { try {
if (addonLogo) { // When TMDB enrichment AND logos are enabled, prioritize TMDB logo over addon logo
finalMetadata.logo = addonLogo; if (settings.enrichMetadataWithTMDB && settings.tmdbEnrichLogos) {
if (__DEV__) {
console.log('[useMetadata] Using addon-provided logo:', { hasLogo: true });
}
// Check both master switch AND granular logos setting
} else if (settings.enrichMetadataWithTMDB && settings.tmdbEnrichLogos) {
// Only use TMDB logos when both enrichment AND logos option are ON
const tmdbService = TMDBService.getInstance(); const tmdbService = TMDBService.getInstance();
const preferredLanguage = settings.tmdbLanguagePreference || 'en'; const preferredLanguage = settings.tmdbLanguagePreference || 'en';
const contentType = type === 'series' ? 'tv' : 'movie'; const contentType = type === 'series' ? 'tv' : 'movie';
@ -932,23 +926,26 @@ export const useMetadata = ({ id, type, addonId }: UseMetadataProps): UseMetadat
if (tmdbIdForLogo) { if (tmdbIdForLogo) {
const logoUrl = await tmdbService.getContentLogo(contentType, tmdbIdForLogo, preferredLanguage); const logoUrl = await tmdbService.getContentLogo(contentType, tmdbIdForLogo, preferredLanguage);
finalMetadata.logo = logoUrl || undefined; // TMDB logo or undefined (no addon fallback) // Use TMDB logo if found, otherwise fall back to addon logo
finalMetadata.logo = logoUrl || addonLogo || undefined;
if (__DEV__) { if (__DEV__) {
console.log('[useMetadata] Logo fetch result:', { console.log('[useMetadata] Logo fetch result:', {
contentType, contentType,
tmdbIdForLogo, tmdbIdForLogo,
preferredLanguage, preferredLanguage,
logoUrl: !!logoUrl, tmdbLogoFound: !!logoUrl,
usingAddonFallback: !logoUrl && !!addonLogo,
enrichmentEnabled: true enrichmentEnabled: true
}); });
} }
} else { } else {
finalMetadata.logo = undefined; // No TMDB ID means no logo // No TMDB ID, fall back to addon logo
if (__DEV__) console.log('[useMetadata] No TMDB ID found for logo, will show text title'); finalMetadata.logo = addonLogo || undefined;
if (__DEV__) console.log('[useMetadata] No TMDB ID found for logo, using addon logo');
} }
} else { } else {
// When enrichment or logos is OFF, keep addon logo or undefined // When enrichment or logos is OFF, use addon logo
finalMetadata.logo = finalMetadata.logo || undefined; finalMetadata.logo = addonLogo || finalMetadata.logo || undefined;
if (__DEV__) { if (__DEV__) {
console.log('[useMetadata] TMDB logo enrichment disabled, using addon logo:', { console.log('[useMetadata] TMDB logo enrichment disabled, using addon logo:', {
hasAddonLogo: !!finalMetadata.logo, hasAddonLogo: !!finalMetadata.logo,
@ -1125,10 +1122,11 @@ export const useMetadata = ({ id, type, addonId }: UseMetadataProps): UseMetadat
// Fetch season posters from TMDB only if enrichment AND season posters are enabled // Fetch season posters from TMDB only if enrichment AND season posters are enabled
if (settings.enrichMetadataWithTMDB && settings.tmdbEnrichSeasonPosters) { if (settings.enrichMetadataWithTMDB && settings.tmdbEnrichSeasonPosters) {
try { try {
const lang = settings.useTmdbLocalizedMetadata ? `${settings.tmdbLanguagePreference || 'en'}` : 'en';
const tmdbIdToUse = tmdbId || (id.startsWith('tt') ? await tmdbService.findTMDBIdByIMDB(id) : null); const tmdbIdToUse = tmdbId || (id.startsWith('tt') ? await tmdbService.findTMDBIdByIMDB(id) : null);
if (tmdbIdToUse) { if (tmdbIdToUse) {
if (!tmdbId) setTmdbId(tmdbIdToUse); if (!tmdbId) setTmdbId(tmdbIdToUse);
const showDetails = await tmdbService.getTVShowDetails(tmdbIdToUse); const showDetails = await tmdbService.getTVShowDetails(tmdbIdToUse, lang);
if (showDetails?.seasons) { if (showDetails?.seasons) {
Object.keys(groupedAddonEpisodes).forEach(seasonStr => { Object.keys(groupedAddonEpisodes).forEach(seasonStr => {
const seasonNum = parseInt(seasonStr, 10); const seasonNum = parseInt(seasonStr, 10);
@ -1156,7 +1154,8 @@ export const useMetadata = ({ id, type, addonId }: UseMetadataProps): UseMetadat
try { try {
const tmdbIdToUse = tmdbId || (id.startsWith('tt') ? await tmdbService.findTMDBIdByIMDB(id) : null); const tmdbIdToUse = tmdbId || (id.startsWith('tt') ? await tmdbService.findTMDBIdByIMDB(id) : null);
if (tmdbIdToUse) { if (tmdbIdToUse) {
const lang = `${settings.tmdbLanguagePreference || 'en'}-US`; // Use just the language code (e.g., 'ar', not 'ar-US') for TMDB API
const lang = settings.tmdbLanguagePreference || 'en';
const seasons = Object.keys(groupedAddonEpisodes).map(Number); const seasons = Object.keys(groupedAddonEpisodes).map(Number);
for (const seasonNum of seasons) { for (const seasonNum of seasons) {
const seasonEps = groupedAddonEpisodes[seasonNum]; const seasonEps = groupedAddonEpisodes[seasonNum];
@ -1264,13 +1263,14 @@ export const useMetadata = ({ id, type, addonId }: UseMetadataProps): UseMetadat
// Fallback to TMDB if no addon episodes // Fallback to TMDB if no addon episodes
logger.log('📺 No addon episodes found, falling back to TMDB'); logger.log('📺 No addon episodes found, falling back to TMDB');
const lang = settings.useTmdbLocalizedMetadata ? `${settings.tmdbLanguagePreference || 'en'}` : 'en';
const tmdbIdResult = await tmdbService.findTMDBIdByIMDB(id); const tmdbIdResult = await tmdbService.findTMDBIdByIMDB(id);
if (tmdbIdResult) { if (tmdbIdResult) {
setTmdbId(tmdbIdResult); setTmdbId(tmdbIdResult);
const [allEpisodes, showDetails] = await Promise.all([ const [allEpisodes, showDetails] = await Promise.all([
tmdbService.getAllEpisodes(tmdbIdResult), tmdbService.getAllEpisodes(tmdbIdResult, lang),
tmdbService.getTVShowDetails(tmdbIdResult) tmdbService.getTVShowDetails(tmdbIdResult, lang)
]); ]);
const transformedEpisodes: GroupedEpisodes = {}; const transformedEpisodes: GroupedEpisodes = {};
@ -2038,7 +2038,8 @@ export const useMetadata = ({ id, type, addonId }: UseMetadataProps): UseMetadat
setLoadingRecommendations(true); setLoadingRecommendations(true);
try { try {
const tmdbService = TMDBService.getInstance(); const tmdbService = TMDBService.getInstance();
const results = await tmdbService.getRecommendations(type === 'movie' ? 'movie' : 'tv', String(tmdbId)); const lang = settings.useTmdbLocalizedMetadata ? (settings.tmdbLanguagePreference || 'en') : 'en';
const results = await tmdbService.getRecommendations(type === 'movie' ? 'movie' : 'tv', String(tmdbId), lang);
// Convert TMDB results to StreamingContent format (simplified) // Convert TMDB results to StreamingContent format (simplified)
const formattedRecommendations: StreamingContent[] = results.map((item: any) => ({ const formattedRecommendations: StreamingContent[] = results.map((item: any) => ({
@ -2056,7 +2057,7 @@ export const useMetadata = ({ id, type, addonId }: UseMetadataProps): UseMetadat
} finally { } finally {
setLoadingRecommendations(false); setLoadingRecommendations(false);
} }
}, [tmdbId, type]); }, [tmdbId, type, settings.useTmdbLocalizedMetadata, settings.tmdbLanguagePreference]);
// Fetch TMDB ID if needed and then recommendations // Fetch TMDB ID if needed and then recommendations
useEffect(() => { useEffect(() => {

View file

@ -92,6 +92,7 @@ export interface AppSettings {
tmdbEnrichMovieDetails: boolean; // Show movie details (budget, revenue, tagline, etc.) tmdbEnrichMovieDetails: boolean; // Show movie details (budget, revenue, tagline, etc.)
tmdbEnrichTvDetails: boolean; // Show TV details (status, seasons count, networks, etc.) tmdbEnrichTvDetails: boolean; // Show TV details (status, seasons count, networks, etc.)
tmdbEnrichCollections: boolean; // Show movie collections/franchises tmdbEnrichCollections: boolean; // Show movie collections/franchises
tmdbEnrichTitleDescription: boolean; // Use TMDB title/description (overrides addon when localization enabled)
// Trakt integration // Trakt integration
showTraktComments: boolean; // Show Trakt comments in metadata screens showTraktComments: boolean; // Show Trakt comments in metadata screens
// Continue Watching behavior // Continue Watching behavior
@ -176,6 +177,7 @@ export const DEFAULT_SETTINGS: AppSettings = {
tmdbEnrichMovieDetails: true, tmdbEnrichMovieDetails: true,
tmdbEnrichTvDetails: true, tmdbEnrichTvDetails: true,
tmdbEnrichCollections: true, tmdbEnrichCollections: true,
tmdbEnrichTitleDescription: true, // Enabled by default for backward compatibility
// Trakt integration // Trakt integration
showTraktComments: true, // Show Trakt comments by default when authenticated showTraktComments: true, // Show Trakt comments by default when authenticated
// Continue Watching behavior // Continue Watching behavior

View file

@ -677,6 +677,23 @@ const TMDBSettingsScreen = () => {
/> />
</View> </View>
{/* Title & Description */}
<View style={styles.settingRow}>
<View style={styles.settingTextContainer}>
<Text style={[styles.settingTitle, { color: currentTheme.colors.text }]}>Title & Description</Text>
<Text style={[styles.settingDescription, { color: currentTheme.colors.mediumEmphasis }]}>
Use TMDb localized title and overview text
</Text>
</View>
<Switch
value={settings.tmdbEnrichTitleDescription}
onValueChange={(v) => updateSetting('tmdbEnrichTitleDescription', v)}
trackColor={{ false: 'rgba(255,255,255,0.1)', true: currentTheme.colors.primary }}
thumbColor={Platform.OS === 'android' ? currentTheme.colors.white : ''}
ios_backgroundColor={'rgba(255,255,255,0.1)'}
/>
</View>
{/* Title Logos */} {/* Title Logos */}
<View style={styles.settingRow}> <View style={styles.settingRow}>
<View style={styles.settingTextContainer}> <View style={styles.settingTextContainer}>

View file

@ -190,7 +190,7 @@ const TraktSettingsScreen: React.FC = () => {
'Sign Out', 'Sign Out',
'Are you sure you want to sign out of your Trakt account?', 'Are you sure you want to sign out of your Trakt account?',
[ [
{ label: 'Cancel', onPress: () => {} }, { label: 'Cancel', onPress: () => { } },
{ {
label: 'Sign Out', label: 'Sign Out',
onPress: async () => { onPress: async () => {
@ -243,6 +243,19 @@ const TraktSettingsScreen: React.FC = () => {
Trakt Settings Trakt Settings
</Text> </Text>
{/* Maintenance Mode Banner */}
{traktService.isMaintenanceMode() && (
<View style={styles.maintenanceBanner}>
<MaterialIcons name="engineering" size={24} color="#FFF" />
<View style={styles.maintenanceBannerTextContainer}>
<Text style={styles.maintenanceBannerTitle}>Under Maintenance</Text>
<Text style={styles.maintenanceBannerMessage}>
{traktService.getMaintenanceMessage()}
</Text>
</View>
</View>
)}
<ScrollView <ScrollView
style={styles.scrollView} style={styles.scrollView}
contentContainerStyle={styles.scrollContent} contentContainerStyle={styles.scrollContent}
@ -255,6 +268,38 @@ const TraktSettingsScreen: React.FC = () => {
<View style={styles.loadingContainer}> <View style={styles.loadingContainer}>
<ActivityIndicator size="large" color={currentTheme.colors.primary} /> <ActivityIndicator size="large" color={currentTheme.colors.primary} />
</View> </View>
) : traktService.isMaintenanceMode() ? (
<View style={styles.signInContainer}>
<TraktIcon
width={120}
height={120}
style={[styles.traktLogo, { opacity: 0.5 }]}
/>
<Text style={[
styles.signInTitle,
{ color: isDarkMode ? currentTheme.colors.highEmphasis : currentTheme.colors.textDark }
]}>
Trakt Unavailable
</Text>
<Text style={[
styles.signInDescription,
{ color: isDarkMode ? currentTheme.colors.mediumEmphasis : currentTheme.colors.textMutedDark }
]}>
The Trakt integration is temporarily paused for maintenance. All syncing and authentication is disabled until maintenance is complete.
</Text>
<TouchableOpacity
style={[
styles.button,
{ backgroundColor: currentTheme.colors.border, opacity: 0.6 }
]}
disabled={true}
>
<MaterialIcons name="engineering" size={20} color={currentTheme.colors.mediumEmphasis} style={{ marginRight: 8 }} />
<Text style={[styles.buttonText, { color: currentTheme.colors.mediumEmphasis }]}>
Service Under Maintenance
</Text>
</TouchableOpacity>
</View>
) : isAuthenticated && userProfile ? ( ) : isAuthenticated && userProfile ? (
<View style={styles.profileContainer}> <View style={styles.profileContainer}>
<View style={styles.profileHeader}> <View style={styles.profileHeader}>
@ -704,6 +749,31 @@ const styles = StyleSheet.create({
fontSize: 13, fontSize: 13,
lineHeight: 18, lineHeight: 18,
}, },
// Maintenance mode styles
maintenanceBanner: {
flexDirection: 'row',
alignItems: 'center',
backgroundColor: '#E67E22',
marginHorizontal: 16,
marginBottom: 16,
padding: 16,
borderRadius: 12,
},
maintenanceBannerTextContainer: {
marginLeft: 12,
flex: 1,
},
maintenanceBannerTitle: {
fontSize: 16,
fontWeight: '600',
color: '#FFF',
marginBottom: 4,
},
maintenanceBannerMessage: {
fontSize: 13,
color: '#FFF',
opacity: 0.9,
},
}); });
export default TraktSettingsScreen; export default TraktSettingsScreen;

View file

@ -7,12 +7,6 @@ const DEFAULT_API_KEY = 'd131017ccc6e5462a81c9304d21476de';
const BASE_URL = 'https://api.themoviedb.org/3'; const BASE_URL = 'https://api.themoviedb.org/3';
const TMDB_API_KEY_STORAGE_KEY = 'tmdb_api_key'; const TMDB_API_KEY_STORAGE_KEY = 'tmdb_api_key';
const USE_CUSTOM_TMDB_API_KEY = 'use_custom_tmdb_api_key'; const USE_CUSTOM_TMDB_API_KEY = 'use_custom_tmdb_api_key';
// Remote cache configuration
const REMOTE_CACHE_URL = process.env.EXPO_PUBLIC_CACHE_SERVER_URL;
const USE_REMOTE_CACHE = process.env.EXPO_PUBLIC_USE_REMOTE_CACHE === 'true';
const REMOTE_CACHE_NAMESPACE = 'tmdb';
// Allow temporarily disabling local MMKV cache (read/write)
const DISABLE_LOCAL_CACHE = process.env.EXPO_PUBLIC_DISABLE_LOCAL_CACHE === 'true';
// Cache configuration // Cache configuration
const TMDB_CACHE_PREFIX = 'tmdb_cache_'; const TMDB_CACHE_PREFIX = 'tmdb_cache_';
@ -140,52 +134,6 @@ export class TMDBService {
this.loadApiKey(); this.loadApiKey();
} }
/**
* Remote cache helpers
*/
private async remoteGetCachedData<T>(key: string): Promise<T | null> {
if (!USE_REMOTE_CACHE || !REMOTE_CACHE_URL) return null;
try {
const url = `${REMOTE_CACHE_URL}/cache/${REMOTE_CACHE_NAMESPACE}/${encodeURIComponent(key)}`;
const response = await axios.get(url, { headers: { 'Content-Type': 'application/json' } });
const payload = response.data;
if (payload && Object.prototype.hasOwnProperty.call(payload, 'data')) {
// Warm local cache for faster subsequent reads (skip if disabled)
if (!DISABLE_LOCAL_CACHE) {
this.setCachedData(key, payload.data);
}
logger.log(`[TMDB Remote Cache] ✅ HIT: ${key}`);
return payload.data as T;
}
return null;
} catch (_) {
logger.log(`[TMDB Remote Cache] ❌ MISS: ${key}`);
return null;
}
}
private async remoteSetCachedData(key: string, data: any): Promise<void> {
if (!USE_REMOTE_CACHE || !REMOTE_CACHE_URL) return;
try {
const url = `${REMOTE_CACHE_URL}/cache/${REMOTE_CACHE_NAMESPACE}/${encodeURIComponent(key)}`;
await axios.put(url, { data, ttlMs: CACHE_TTL_MS }, { headers: { 'Content-Type': 'application/json' } });
logger.log(`[TMDB Remote Cache] 💾 STORED: ${key}`);
} catch (_) {
// best-effort only
}
}
private async remoteClearAllCache(): Promise<void> {
if (!USE_REMOTE_CACHE || !REMOTE_CACHE_URL) return;
try {
const url = `${REMOTE_CACHE_URL}/cache/${REMOTE_CACHE_NAMESPACE}/clear`;
await axios.post(url, {}, { headers: { 'Content-Type': 'application/json' } });
logger.log(`[TMDB Remote Cache] 🗑️ CLEARED namespace ${REMOTE_CACHE_NAMESPACE}`);
} catch (_) {
// ignore
}
}
/** /**
* Generate a unique cache key from endpoint and parameters * Generate a unique cache key from endpoint and parameters
*/ */
@ -206,10 +154,6 @@ export class TMDBService {
* Retrieve cached data if not expired * Retrieve cached data if not expired
*/ */
private getCachedData<T>(key: string): T | null { private getCachedData<T>(key: string): T | null {
if (DISABLE_LOCAL_CACHE) {
logger.log(`[TMDB Cache] 🚫 LOCAL DISABLED: ${key}`);
return null;
}
try { try {
const cachedStr = mmkvStorage.getString(key); const cachedStr = mmkvStorage.getString(key);
if (!cachedStr) { if (!cachedStr) {
@ -236,17 +180,11 @@ export class TMDBService {
} }
} }
/**
* Get from local cache
*/
private async getFromCacheOrRemote<T>(key: string): Promise<T | null> { private async getFromCacheOrRemote<T>(key: string): Promise<T | null> {
// Local-first: serve from MMKV if present; else try remote and warm local return this.getCachedData<T>(key);
if (!DISABLE_LOCAL_CACHE) {
const local = this.getCachedData<T>(key);
if (local !== null) return local;
}
if (USE_REMOTE_CACHE && REMOTE_CACHE_URL) {
const remote = await this.remoteGetCachedData<T>(key);
if (remote !== null) return remote;
}
return null;
} }
/** /**
@ -262,19 +200,12 @@ export class TMDBService {
} }
try { try {
if (!DISABLE_LOCAL_CACHE) {
const cacheEntry = { const cacheEntry = {
data, data,
timestamp: Date.now() timestamp: Date.now()
}; };
mmkvStorage.setString(key, JSON.stringify(cacheEntry)); mmkvStorage.setString(key, JSON.stringify(cacheEntry));
logger.log(`[TMDB Cache] 💾 STORED: ${key}`); logger.log(`[TMDB Cache] 💾 STORED: ${key}`);
} else {
logger.log(`[TMDB Cache] ⛔ LOCAL WRITE SKIPPED: ${key}`);
}
// Best-effort remote write
// eslint-disable-next-line @typescript-eslint/no-floating-promises
this.remoteSetCachedData(key, data);
} catch (error) { } catch (error) {
// Ignore cache errors // Ignore cache errors
} }
@ -358,8 +289,8 @@ export class TMDBService {
/** /**
* Search for a TV show by name * Search for a TV show by name
*/ */
async searchTVShow(query: string): Promise<TMDBShow[]> { async searchTVShow(query: string, language: string = 'en-US'): Promise<TMDBShow[]> {
const cacheKey = this.generateCacheKey('search_tv', { query }); const cacheKey = this.generateCacheKey('search_tv', { query, language });
// Check cache (local or remote) // Check cache (local or remote)
const cached = await this.getFromCacheOrRemote<TMDBShow[]>(cacheKey); const cached = await this.getFromCacheOrRemote<TMDBShow[]>(cacheKey);
@ -372,7 +303,7 @@ export class TMDBService {
params: await this.getParams({ params: await this.getParams({
query, query,
include_adult: false, include_adult: false,
language: 'en-US', language,
page: 1, page: 1,
}), }),
}); });
@ -601,8 +532,8 @@ export class TMDBService {
/** /**
* Find TMDB ID by IMDB ID * Find TMDB ID by IMDB ID
*/ */
async findTMDBIdByIMDB(imdbId: string): Promise<number | null> { async findTMDBIdByIMDB(imdbId: string, language: string = 'en-US'): Promise<number | null> {
const cacheKey = this.generateCacheKey('find_imdb', { imdbId }); const cacheKey = this.generateCacheKey('find_imdb', { imdbId, language });
// Check cache (local or remote) // Check cache (local or remote)
const cached = await this.getFromCacheOrRemote<number>(cacheKey); const cached = await this.getFromCacheOrRemote<number>(cacheKey);
@ -616,7 +547,7 @@ export class TMDBService {
headers: await this.getHeaders(), headers: await this.getHeaders(),
params: await this.getParams({ params: await this.getParams({
external_source: 'imdb_id', external_source: 'imdb_id',
language: 'en-US', language,
}), }),
}); });
@ -658,11 +589,12 @@ export class TMDBService {
/** /**
* Get all episodes for a TV show * Get all episodes for a TV show
* @param language Language for localized episode names/overviews
*/ */
async getAllEpisodes(tmdbId: number): Promise<{ [seasonNumber: number]: TMDBEpisode[] }> { async getAllEpisodes(tmdbId: number, language: string = 'en-US'): Promise<{ [seasonNumber: number]: TMDBEpisode[] }> {
try { try {
// First get the show details to know how many seasons there are // First get the show details to know how many seasons there are
const showDetails = await this.getTVShowDetails(tmdbId); const showDetails = await this.getTVShowDetails(tmdbId, language);
if (!showDetails) return {}; if (!showDetails) return {};
const allEpisodes: { [seasonNumber: number]: TMDBEpisode[] } = {}; const allEpisodes: { [seasonNumber: number]: TMDBEpisode[] } = {};
@ -671,7 +603,7 @@ export class TMDBService {
const seasonPromises = showDetails.seasons const seasonPromises = showDetails.seasons
.filter(season => season.season_number > 0) // Filter out specials (season 0) .filter(season => season.season_number > 0) // Filter out specials (season 0)
.map(async season => { .map(async season => {
const seasonDetails = await this.getSeasonDetails(tmdbId, season.season_number); const seasonDetails = await this.getSeasonDetails(tmdbId, season.season_number, showDetails.name, language);
if (seasonDetails && seasonDetails.episodes) { if (seasonDetails && seasonDetails.episodes) {
allEpisodes[season.season_number] = seasonDetails.episodes; allEpisodes[season.season_number] = seasonDetails.episodes;
} }
@ -727,8 +659,8 @@ export class TMDBService {
} }
} }
async getCredits(tmdbId: number, type: string) { async getCredits(tmdbId: number, type: string, language: string = 'en-US') {
const cacheKey = this.generateCacheKey(`${type}_${tmdbId}_credits`); const cacheKey = this.generateCacheKey(`${type}_${tmdbId}_credits`, { language });
// Check cache (local or remote) // Check cache (local or remote)
const cached = await this.getFromCacheOrRemote<{ cast: any[]; crew: any[] }>(cacheKey); const cached = await this.getFromCacheOrRemote<{ cast: any[]; crew: any[] }>(cacheKey);
@ -738,7 +670,7 @@ export class TMDBService {
const response = await axios.get(`${BASE_URL}/${type === 'series' ? 'tv' : 'movie'}/${tmdbId}/credits`, { const response = await axios.get(`${BASE_URL}/${type === 'series' ? 'tv' : 'movie'}/${tmdbId}/credits`, {
headers: await this.getHeaders(), headers: await this.getHeaders(),
params: await this.getParams({ params: await this.getParams({
language: 'en-US', language,
}), }),
}); });
const data = { const data = {
@ -752,8 +684,8 @@ export class TMDBService {
} }
} }
async getPersonDetails(personId: number) { async getPersonDetails(personId: number, language: string = 'en-US') {
const cacheKey = this.generateCacheKey(`person_${personId}`); const cacheKey = this.generateCacheKey(`person_${personId}`, { language });
// Check cache (local or remote) // Check cache (local or remote)
const cached = await this.getFromCacheOrRemote<any>(cacheKey); const cached = await this.getFromCacheOrRemote<any>(cacheKey);
@ -763,7 +695,7 @@ export class TMDBService {
const response = await axios.get(`${BASE_URL}/person/${personId}`, { const response = await axios.get(`${BASE_URL}/person/${personId}`, {
headers: await this.getHeaders(), headers: await this.getHeaders(),
params: await this.getParams({ params: await this.getParams({
language: 'en-US', language,
}), }),
}); });
const data = response.data; const data = response.data;
@ -777,8 +709,8 @@ export class TMDBService {
/** /**
* Get person's movie credits (cast and crew) * Get person's movie credits (cast and crew)
*/ */
async getPersonMovieCredits(personId: number) { async getPersonMovieCredits(personId: number, language: string = 'en-US') {
const cacheKey = this.generateCacheKey(`person_${personId}_movie_credits`); const cacheKey = this.generateCacheKey(`person_${personId}_movie_credits`, { language });
// Check cache (local or remote) // Check cache (local or remote)
const cached = await this.getFromCacheOrRemote<any>(cacheKey); const cached = await this.getFromCacheOrRemote<any>(cacheKey);
@ -788,7 +720,7 @@ export class TMDBService {
const response = await axios.get(`${BASE_URL}/person/${personId}/movie_credits`, { const response = await axios.get(`${BASE_URL}/person/${personId}/movie_credits`, {
headers: await this.getHeaders(), headers: await this.getHeaders(),
params: await this.getParams({ params: await this.getParams({
language: 'en-US', language,
}), }),
}); });
const data = response.data; const data = response.data;
@ -802,8 +734,8 @@ export class TMDBService {
/** /**
* Get person's TV credits (cast and crew) * Get person's TV credits (cast and crew)
*/ */
async getPersonTvCredits(personId: number) { async getPersonTvCredits(personId: number, language: string = 'en-US') {
const cacheKey = this.generateCacheKey(`person_${personId}_tv_credits`); const cacheKey = this.generateCacheKey(`person_${personId}_tv_credits`, { language });
// Check cache (local or remote) // Check cache (local or remote)
const cached = await this.getFromCacheOrRemote<any>(cacheKey); const cached = await this.getFromCacheOrRemote<any>(cacheKey);
@ -813,7 +745,7 @@ export class TMDBService {
const response = await axios.get(`${BASE_URL}/person/${personId}/tv_credits`, { const response = await axios.get(`${BASE_URL}/person/${personId}/tv_credits`, {
headers: await this.getHeaders(), headers: await this.getHeaders(),
params: await this.getParams({ params: await this.getParams({
language: 'en-US', language,
}), }),
}); });
const data = response.data; const data = response.data;
@ -827,8 +759,8 @@ export class TMDBService {
/** /**
* Get person's combined credits (movies and TV) * Get person's combined credits (movies and TV)
*/ */
async getPersonCombinedCredits(personId: number) { async getPersonCombinedCredits(personId: number, language: string = 'en-US') {
const cacheKey = this.generateCacheKey(`person_${personId}_combined_credits`); const cacheKey = this.generateCacheKey(`person_${personId}_combined_credits`, { language });
// Check cache (local or remote) // Check cache (local or remote)
const cached = await this.getFromCacheOrRemote<any>(cacheKey); const cached = await this.getFromCacheOrRemote<any>(cacheKey);
@ -838,7 +770,7 @@ export class TMDBService {
const response = await axios.get(`${BASE_URL}/person/${personId}/combined_credits`, { const response = await axios.get(`${BASE_URL}/person/${personId}/combined_credits`, {
headers: await this.getHeaders(), headers: await this.getHeaders(),
params: await this.getParams({ params: await this.getParams({
language: 'en-US', language,
}), }),
}); });
const data = response.data; const data = response.data;
@ -899,8 +831,8 @@ export class TMDBService {
} }
} }
async searchMulti(query: string): Promise<any[]> { async searchMulti(query: string, language: string = 'en-US'): Promise<any[]> {
const cacheKey = this.generateCacheKey('search_multi', { query }); const cacheKey = this.generateCacheKey('search_multi', { query, language });
// Check cache (local or remote) // Check cache (local or remote)
const cached = await this.getFromCacheOrRemote<any[]>(cacheKey); const cached = await this.getFromCacheOrRemote<any[]>(cacheKey);
@ -912,7 +844,7 @@ export class TMDBService {
params: await this.getParams({ params: await this.getParams({
query, query,
include_adult: false, include_adult: false,
language: 'en-US', language,
page: 1, page: 1,
}), }),
}); });
@ -1402,9 +1334,10 @@ export class TMDBService {
* Get trending movies or TV shows * Get trending movies or TV shows
* @param type 'movie' or 'tv' * @param type 'movie' or 'tv'
* @param timeWindow 'day' or 'week' * @param timeWindow 'day' or 'week'
* @param language Language for localized results
*/ */
async getTrending(type: 'movie' | 'tv', timeWindow: 'day' | 'week'): Promise<TMDBTrendingResult[]> { async getTrending(type: 'movie' | 'tv', timeWindow: 'day' | 'week', language: string = 'en-US'): Promise<TMDBTrendingResult[]> {
const cacheKey = this.generateCacheKey(`trending_${type}_${timeWindow}`); const cacheKey = this.generateCacheKey(`trending_${type}_${timeWindow}`, { language });
// Check cache (local or remote) // Check cache (local or remote)
const cached = await this.getFromCacheOrRemote<TMDBTrendingResult[]>(cacheKey); const cached = await this.getFromCacheOrRemote<TMDBTrendingResult[]>(cacheKey);
@ -1414,7 +1347,7 @@ export class TMDBService {
const response = await axios.get(`${BASE_URL}/trending/${type}/${timeWindow}`, { const response = await axios.get(`${BASE_URL}/trending/${type}/${timeWindow}`, {
headers: await this.getHeaders(), headers: await this.getHeaders(),
params: await this.getParams({ params: await this.getParams({
language: 'en-US', language,
}), }),
}); });
@ -1451,9 +1384,10 @@ export class TMDBService {
* Get popular movies or TV shows * Get popular movies or TV shows
* @param type 'movie' or 'tv' * @param type 'movie' or 'tv'
* @param page Page number for pagination * @param page Page number for pagination
* @param language Language for localized results
*/ */
async getPopular(type: 'movie' | 'tv', page: number = 1): Promise<TMDBTrendingResult[]> { async getPopular(type: 'movie' | 'tv', page: number = 1, language: string = 'en-US'): Promise<TMDBTrendingResult[]> {
const cacheKey = this.generateCacheKey(`popular_${type}`, { page }); const cacheKey = this.generateCacheKey(`popular_${type}`, { page, language });
// Check cache (local or remote) // Check cache (local or remote)
const cached = await this.getFromCacheOrRemote<TMDBTrendingResult[]>(cacheKey); const cached = await this.getFromCacheOrRemote<TMDBTrendingResult[]>(cacheKey);
@ -1463,7 +1397,7 @@ export class TMDBService {
const response = await axios.get(`${BASE_URL}/${type}/popular`, { const response = await axios.get(`${BASE_URL}/${type}/popular`, {
headers: await this.getHeaders(), headers: await this.getHeaders(),
params: await this.getParams({ params: await this.getParams({
language: 'en-US', language,
page, page,
}), }),
}); });
@ -1501,9 +1435,10 @@ export class TMDBService {
* Get upcoming/now playing content * Get upcoming/now playing content
* @param type 'movie' or 'tv' * @param type 'movie' or 'tv'
* @param page Page number for pagination * @param page Page number for pagination
* @param language Language for localized results
*/ */
async getUpcoming(type: 'movie' | 'tv', page: number = 1): Promise<TMDBTrendingResult[]> { async getUpcoming(type: 'movie' | 'tv', page: number = 1, language: string = 'en-US'): Promise<TMDBTrendingResult[]> {
const cacheKey = this.generateCacheKey(`upcoming_${type}`, { page }); const cacheKey = this.generateCacheKey(`upcoming_${type}`, { page, language });
// Check cache (local or remote) // Check cache (local or remote)
const cached = await this.getFromCacheOrRemote<TMDBTrendingResult[]>(cacheKey); const cached = await this.getFromCacheOrRemote<TMDBTrendingResult[]>(cacheKey);
@ -1516,7 +1451,7 @@ export class TMDBService {
const response = await axios.get(`${BASE_URL}/${type}/${endpoint}`, { const response = await axios.get(`${BASE_URL}/${type}/${endpoint}`, {
headers: await this.getHeaders(), headers: await this.getHeaders(),
params: await this.getParams({ params: await this.getParams({
language: 'en-US', language,
page, page,
}), }),
}); });
@ -1554,9 +1489,10 @@ export class TMDBService {
* Get now playing movies (currently in theaters) * Get now playing movies (currently in theaters)
* @param page Page number for pagination * @param page Page number for pagination
* @param region ISO 3166-1 country code (e.g., 'US', 'GB') * @param region ISO 3166-1 country code (e.g., 'US', 'GB')
* @param language Language for localized results
*/ */
async getNowPlaying(page: number = 1, region: string = 'US'): Promise<TMDBTrendingResult[]> { async getNowPlaying(page: number = 1, region: string = 'US', language: string = 'en-US'): Promise<TMDBTrendingResult[]> {
const cacheKey = this.generateCacheKey('now_playing', { page, region }); const cacheKey = this.generateCacheKey('now_playing', { page, region, language });
// Check cache (local or remote) // Check cache (local or remote)
const cached = await this.getFromCacheOrRemote<TMDBTrendingResult[]>(cacheKey); const cached = await this.getFromCacheOrRemote<TMDBTrendingResult[]>(cacheKey);
@ -1566,7 +1502,7 @@ export class TMDBService {
const response = await axios.get(`${BASE_URL}/movie/now_playing`, { const response = await axios.get(`${BASE_URL}/movie/now_playing`, {
headers: await this.getHeaders(), headers: await this.getHeaders(),
params: await this.getParams({ params: await this.getParams({
language: 'en-US', language,
page, page,
region, // Filter by region to get accurate theater availability region, // Filter by region to get accurate theater availability
}), }),
@ -1603,9 +1539,10 @@ export class TMDBService {
/** /**
* Get the list of official movie genres from TMDB * Get the list of official movie genres from TMDB
* @param language Language for localized genre names
*/ */
async getMovieGenres(): Promise<{ id: number; name: string }[]> { async getMovieGenres(language: string = 'en-US'): Promise<{ id: number; name: string }[]> {
const cacheKey = this.generateCacheKey('genres_movie'); const cacheKey = this.generateCacheKey('genres_movie', { language });
// Check cache (local or remote) // Check cache (local or remote)
const cached = await this.getFromCacheOrRemote<{ id: number; name: string }[]>(cacheKey); const cached = await this.getFromCacheOrRemote<{ id: number; name: string }[]>(cacheKey);
@ -1615,7 +1552,7 @@ export class TMDBService {
const response = await axios.get(`${BASE_URL}/genre/movie/list`, { const response = await axios.get(`${BASE_URL}/genre/movie/list`, {
headers: await this.getHeaders(), headers: await this.getHeaders(),
params: await this.getParams({ params: await this.getParams({
language: 'en-US', language,
}), }),
}); });
const data = response.data.genres || []; const data = response.data.genres || [];
@ -1628,9 +1565,10 @@ export class TMDBService {
/** /**
* Get the list of official TV genres from TMDB * Get the list of official TV genres from TMDB
* @param language Language for localized genre names
*/ */
async getTvGenres(): Promise<{ id: number; name: string }[]> { async getTvGenres(language: string = 'en-US'): Promise<{ id: number; name: string }[]> {
const cacheKey = this.generateCacheKey('genres_tv'); const cacheKey = this.generateCacheKey('genres_tv', { language });
// Check cache (local or remote) // Check cache (local or remote)
const cached = await this.getFromCacheOrRemote<{ id: number; name: string }[]>(cacheKey); const cached = await this.getFromCacheOrRemote<{ id: number; name: string }[]>(cacheKey);
@ -1640,7 +1578,7 @@ export class TMDBService {
const response = await axios.get(`${BASE_URL}/genre/tv/list`, { const response = await axios.get(`${BASE_URL}/genre/tv/list`, {
headers: await this.getHeaders(), headers: await this.getHeaders(),
params: await this.getParams({ params: await this.getParams({
language: 'en-US', language,
}), }),
}); });
const data = response.data.genres || []; const data = response.data.genres || [];
@ -1656,9 +1594,10 @@ export class TMDBService {
* @param type 'movie' or 'tv' * @param type 'movie' or 'tv'
* @param genreName The genre name to filter by * @param genreName The genre name to filter by
* @param page Page number for pagination * @param page Page number for pagination
* @param language Language for localized results
*/ */
async discoverByGenre(type: 'movie' | 'tv', genreName: string, page: number = 1): Promise<TMDBTrendingResult[]> { async discoverByGenre(type: 'movie' | 'tv', genreName: string, page: number = 1, language: string = 'en-US'): Promise<TMDBTrendingResult[]> {
const cacheKey = this.generateCacheKey(`discover_${type}`, { genreName, page }); const cacheKey = this.generateCacheKey(`discover_${type}`, { genreName, page, language });
// Check cache (local or remote) // Check cache (local or remote)
const cached = await this.getFromCacheOrRemote<TMDBTrendingResult[]>(cacheKey); const cached = await this.getFromCacheOrRemote<TMDBTrendingResult[]>(cacheKey);
@ -1667,8 +1606,8 @@ export class TMDBService {
try { try {
// First get the genre ID from the name // First get the genre ID from the name
const genreList = type === 'movie' const genreList = type === 'movie'
? await this.getMovieGenres() ? await this.getMovieGenres(language)
: await this.getTvGenres(); : await this.getTvGenres(language);
const genre = genreList.find(g => g.name.toLowerCase() === genreName.toLowerCase()); const genre = genreList.find(g => g.name.toLowerCase() === genreName.toLowerCase());
@ -1679,13 +1618,12 @@ export class TMDBService {
const response = await axios.get(`${BASE_URL}/discover/${type}`, { const response = await axios.get(`${BASE_URL}/discover/${type}`, {
headers: await this.getHeaders(), headers: await this.getHeaders(),
params: await this.getParams({ params: await this.getParams({
language: 'en-US', language,
sort_by: 'popularity.desc', sort_by: 'popularity.desc',
include_adult: false, include_adult: false,
include_video: false, include_video: false,
page, page,
with_genres: genre.id.toString(), with_genres: genre.id.toString(),
with_original_language: 'en',
}), }),
}); });

View file

@ -577,6 +577,10 @@ export type TraktContentCommentLegacy =
| TraktEpisodeComment | TraktEpisodeComment
| TraktListComment; | TraktListComment;
const TRAKT_MAINTENANCE_MODE = true;
const TRAKT_MAINTENANCE_MESSAGE = 'Trakt integration is temporarily unavailable for maintenance. Please try again later.';
export class TraktService { export class TraktService {
private static instance: TraktService; private static instance: TraktService;
private accessToken: string | null = null; private accessToken: string | null = null;
@ -584,6 +588,16 @@ export class TraktService {
private tokenExpiry: number = 0; private tokenExpiry: number = 0;
private isInitialized: boolean = false; private isInitialized: boolean = false;
public isMaintenanceMode(): boolean {
return TRAKT_MAINTENANCE_MODE;
}
public getMaintenanceMessage(): string {
return TRAKT_MAINTENANCE_MESSAGE;
}
// Rate limiting - Optimized for real-time scrobbling // Rate limiting - Optimized for real-time scrobbling
private lastApiCall: number = 0; private lastApiCall: number = 0;
private readonly MIN_API_INTERVAL = 500; // Reduced to 500ms for faster updates private readonly MIN_API_INTERVAL = 500; // Reduced to 500ms for faster updates
@ -726,6 +740,12 @@ export class TraktService {
* Check if the user is authenticated with Trakt * Check if the user is authenticated with Trakt
*/ */
public async isAuthenticated(): Promise<boolean> { public async isAuthenticated(): Promise<boolean> {
// During maintenance, report as not authenticated to disable all syncing
if (this.isMaintenanceMode()) {
logger.log('[TraktService] Maintenance mode: reporting as not authenticated');
return false;
}
await this.ensureInitialized(); await this.ensureInitialized();
if (!this.accessToken) { if (!this.accessToken) {
@ -756,6 +776,12 @@ export class TraktService {
* Exchange the authorization code for an access token * Exchange the authorization code for an access token
*/ */
public async exchangeCodeForToken(code: string, codeVerifier: string): Promise<boolean> { public async exchangeCodeForToken(code: string, codeVerifier: string): Promise<boolean> {
// Block authentication during maintenance
if (this.isMaintenanceMode()) {
logger.warn('[TraktService] Maintenance mode: blocking new authentication');
return false;
}
await this.ensureInitialized(); await this.ensureInitialized();
try { try {
@ -887,6 +913,12 @@ export class TraktService {
body?: any, body?: any,
retryCount: number = 0 retryCount: number = 0
): Promise<T> { ): Promise<T> {
// Block all API requests during maintenance
if (this.isMaintenanceMode()) {
logger.warn('[TraktService] Maintenance mode: blocking API request to', endpoint);
throw new Error(TRAKT_MAINTENANCE_MESSAGE);
}
await this.ensureInitialized(); await this.ensureInitialized();
// Rate limiting: ensure minimum interval between API calls // Rate limiting: ensure minimum interval between API calls
@ -1106,10 +1138,10 @@ export class TraktService {
? imdbId ? imdbId
: `tt${imdbId}`; : `tt${imdbId}`;
const response = await this.client.get('/sync/watched/movies'); const movies = await this.apiRequest<any[]>('/sync/watched/movies');
const movies = Array.isArray(response.data) ? response.data : []; const moviesArray = Array.isArray(movies) ? movies : [];
return movies.some( return moviesArray.some(
(m: any) => m.movie?.ids?.imdb === imdb (m: any) => m.movie?.ids?.imdb === imdb
); );
} catch (err) { } catch (err) {