diff --git a/src/hooks/useMetadata.ts b/src/hooks/useMetadata.ts index 98ee76ca..185a1ae4 100644 --- a/src/hooks/useMetadata.ts +++ b/src/hooks/useMetadata.ts @@ -12,6 +12,7 @@ import { usePersistentSeasons } from './usePersistentSeasons'; import AsyncStorage from '@react-native-async-storage/async-storage'; import { Stream } from '../types/metadata'; import { storageService } from '../services/storageService'; +import { useSettings } from './useSettings'; // Constants for timeouts and retries const API_TIMEOUT = 10000; // 10 seconds @@ -109,6 +110,7 @@ interface UseMetadataReturn { } export const useMetadata = ({ id, type, addonId }: UseMetadataProps): UseMetadataReturn => { + const { settings } = useSettings(); const [metadata, setMetadata] = useState(null); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); @@ -300,6 +302,11 @@ export const useMetadata = ({ id, type, addonId }: UseMetadataProps): UseMetadat if (__DEV__) logger.log('[loadCast] Starting cast fetch for:', id); setLoadingCast(true); try { + if (!settings.enrichMetadataWithTMDB) { + if (__DEV__) logger.log('[loadCast] TMDB enrichment disabled by settings'); + setLoadingCast(false); + return; + } // Check cache first const cachedCast = cacheService.getCast(id, type); if (cachedCast) { @@ -394,6 +401,22 @@ export const useMetadata = ({ id, type, addonId }: UseMetadataProps): UseMetadat // Handle TMDB-specific IDs let actualId = id; if (id.startsWith('tmdb:')) { + // If enrichment disabled, resolve to an addon-friendly ID (IMDb) before calling addons + if (!settings.enrichMetadataWithTMDB) { + const tmdbRaw = id.split(':')[1]; + try { + if (__DEV__) logger.log('[loadMetadata] enrichment=OFF; resolving TMDB→Stremio ID', { type, tmdbId: tmdbRaw }); + const stremioId = await catalogService.getStremioId(type === 'series' ? 'tv' : 'movie', tmdbRaw); + if (stremioId) { + actualId = stremioId; + if (__DEV__) logger.log('[loadMetadata] resolved TMDB→Stremio ID', { actualId }); + } else { + if (__DEV__) logger.warn('[loadMetadata] failed to resolve TMDB→Stremio ID; addon fetch may fail', { type, tmdbId: tmdbRaw }); + } + } catch (e) { + if (__DEV__) logger.error('[loadMetadata] error resolving TMDB→Stremio ID', e); + } + } else { const tmdbId = id.split(':')[1]; // For TMDB IDs, we need to handle metadata differently if (type === 'movie') { @@ -539,9 +562,11 @@ export const useMetadata = ({ id, type, addonId }: UseMetadataProps): UseMetadat logger.error('Failed to fetch TV show details from TMDB:', error); } } + } } // Load all data in parallel + if (__DEV__) logger.log('[loadMetadata] fetching addon metadata', { type, actualId, addonId }); const [content, castData] = await Promise.allSettled([ // Load content with timeout and retry withRetry(async () => { @@ -553,6 +578,7 @@ export const useMetadata = ({ id, type, addonId }: UseMetadataProps): UseMetadat if (actualId.startsWith('tt')) { setImdbId(actualId); } + if (__DEV__) logger.log('[loadMetadata] addon metadata fetched', { hasResult: Boolean(result) }); return result; }), // Start loading cast immediately in parallel @@ -560,6 +586,7 @@ export const useMetadata = ({ id, type, addonId }: UseMetadataProps): UseMetadat ]); if (content.status === 'fulfilled' && content.value) { + if (__DEV__) logger.log('[loadMetadata] addon metadata:success', { id: content.value?.id, type: content.value?.type, name: content.value?.name }); setMetadata(content.value); // Check if item is in library const isInLib = catalogService.getLibraryItems().some(item => item.id === id); @@ -571,6 +598,7 @@ export const useMetadata = ({ id, type, addonId }: UseMetadataProps): UseMetadat // Update cache cacheService.setMetadata(id, type, content.value); } else { + if (__DEV__) logger.warn('[loadMetadata] addon metadata:not found or failed', { status: content.status, reason: (content as any)?.reason?.message }); throw new Error('Content not found'); } } catch (error) { @@ -636,29 +664,33 @@ export const useMetadata = ({ id, type, addonId }: UseMetadataProps): UseMetadat if (__DEV__) logger.log(`📺 Processed addon episodes into ${Object.keys(groupedAddonEpisodes).length} seasons`); - // Fetch season posters from TMDB - try { - const tmdbIdToUse = tmdbId || (id.startsWith('tt') ? await tmdbService.findTMDBIdByIMDB(id) : null); - if (tmdbIdToUse) { - if (!tmdbId) setTmdbId(tmdbIdToUse); - const showDetails = await tmdbService.getTVShowDetails(tmdbIdToUse); - if (showDetails?.seasons) { - Object.keys(groupedAddonEpisodes).forEach(seasonStr => { - const seasonNum = parseInt(seasonStr, 10); - const seasonInfo = showDetails.seasons.find(s => s.season_number === seasonNum); - const seasonPosterPath = seasonInfo?.poster_path; - if (seasonPosterPath) { - groupedAddonEpisodes[seasonNum] = groupedAddonEpisodes[seasonNum].map(ep => ({ - ...ep, - season_poster_path: seasonPosterPath, - })); - } - }); - if (__DEV__) logger.log('🖼️ Successfully fetched and attached TMDB season posters to addon episodes.'); + // Fetch season posters from TMDB only if enrichment is enabled; otherwise skip quietly + if (settings.enrichMetadataWithTMDB) { + try { + const tmdbIdToUse = tmdbId || (id.startsWith('tt') ? await tmdbService.findTMDBIdByIMDB(id) : null); + if (tmdbIdToUse) { + if (!tmdbId) setTmdbId(tmdbIdToUse); + const showDetails = await tmdbService.getTVShowDetails(tmdbIdToUse); + if (showDetails?.seasons) { + Object.keys(groupedAddonEpisodes).forEach(seasonStr => { + const seasonNum = parseInt(seasonStr, 10); + const seasonInfo = showDetails.seasons.find(s => s.season_number === seasonNum); + const seasonPosterPath = seasonInfo?.poster_path; + if (seasonPosterPath) { + groupedAddonEpisodes[seasonNum] = groupedAddonEpisodes[seasonNum].map(ep => ({ + ...ep, + season_poster_path: seasonPosterPath, + })); + } + }); + if (__DEV__) logger.log('🖼️ Successfully fetched and attached TMDB season posters to addon episodes.'); + } } + } catch (error) { + logger.error('Failed to fetch TMDB season posters for addon episodes:', error); } - } catch (error) { - logger.error('Failed to fetch TMDB season posters for addon episodes:', error); + } else { + if (__DEV__) logger.log('[loadSeriesData] TMDB enrichment disabled; skipping season poster fetch'); } setGroupedEpisodes(groupedAddonEpisodes); @@ -680,6 +712,10 @@ export const useMetadata = ({ id, type, addonId }: UseMetadataProps): UseMetadat } // Try to get TMDB ID for additional metadata (cast, etc.) but don't override episodes + if (!settings.enrichMetadataWithTMDB) { + if (__DEV__) logger.log('[loadSeriesData] TMDB enrichment disabled; skipping TMDB episode fallback (preserving current episodes)'); + return; + } const tmdbIdResult = await tmdbService.findTMDBIdByIMDB(id); if (tmdbIdResult) { setTmdbId(tmdbIdResult); @@ -1078,11 +1114,11 @@ export const useMetadata = ({ id, type, addonId }: UseMetadataProps): UseMetadat if (metadata?.imdb_id) { // Replace the series ID in episodeId with the IMDb ID const [, season, episode] = episodeId.split(':'); - stremioEpisodeId = `series:${metadata.imdb_id}:${season}:${episode}`; + stremioEpisodeId = `${metadata.imdb_id}:${season}:${episode}`; if (__DEV__) console.log('✅ [loadEpisodeStreams] Using IMDb ID from metadata for Stremio episode:', stremioEpisodeId); } else if (imdbId) { const [, season, episode] = episodeId.split(':'); - stremioEpisodeId = `series:${imdbId}:${season}:${episode}`; + stremioEpisodeId = `${imdbId}:${season}:${episode}`; if (__DEV__) console.log('✅ [loadEpisodeStreams] Using stored IMDb ID for Stremio episode:', stremioEpisodeId); } else { // Convert TMDB ID to IMDb ID for Stremio addons @@ -1091,7 +1127,7 @@ export const useMetadata = ({ id, type, addonId }: UseMetadataProps): UseMetadat if (externalIds?.imdb_id) { const [, season, episode] = episodeId.split(':'); - stremioEpisodeId = `series:${externalIds.imdb_id}:${season}:${episode}`; + stremioEpisodeId = `${externalIds.imdb_id}:${season}:${episode}`; if (__DEV__) console.log('✅ [loadEpisodeStreams] Converted TMDB to IMDb ID for Stremio episode:', stremioEpisodeId); } else { if (__DEV__) console.log('⚠️ [loadEpisodeStreams] No IMDb ID found for TMDB ID, using original episode ID:', stremioEpisodeId); @@ -1105,6 +1141,12 @@ export const useMetadata = ({ id, type, addonId }: UseMetadataProps): UseMetadat if (__DEV__) console.log('📝 [loadEpisodeStreams] Converting IMDB ID to TMDB ID...'); tmdbId = await withTimeout(tmdbService.findTMDBIdByIMDB(id), API_TIMEOUT); if (__DEV__) console.log('✅ [loadEpisodeStreams] Converted to TMDB ID:', tmdbId); + // Normalize episode id to 'tt:season:episode' format for addons that expect tt prefix + const parts = episodeId.split(':'); + if (parts.length === 3 && parts[0] === 'series') { + stremioEpisodeId = `${id}:${parts[1]}:${parts[2]}`; + if (__DEV__) console.log('🔧 [loadEpisodeStreams] Normalized episode ID for addons:', stremioEpisodeId); + } } else { tmdbId = id; if (__DEV__) console.log('ℹ️ [loadEpisodeStreams] Using ID as both TMDB and Stremio ID:', tmdbId); @@ -1212,6 +1254,10 @@ export const useMetadata = ({ id, type, addonId }: UseMetadataProps): UseMetadat }, [metadata?.videos, type]); const loadRecommendations = useCallback(async () => { + if (!settings.enrichMetadataWithTMDB) { + if (__DEV__) console.log('[useMetadata] enrichment disabled; skip recommendations'); + return; + } if (!tmdbId) return; setLoadingRecommendations(true); @@ -1240,6 +1286,10 @@ export const useMetadata = ({ id, type, addonId }: UseMetadataProps): UseMetadat // Fetch TMDB ID if needed and then recommendations useEffect(() => { const fetchTmdbIdAndRecommendations = async () => { + if (!settings.enrichMetadataWithTMDB) { + if (__DEV__) console.log('[useMetadata] enrichment disabled; skip TMDB id extraction and certification (extract path)'); + return; + } if (metadata && !tmdbId) { try { const tmdbService = TMDBService.getInstance(); @@ -1268,23 +1318,29 @@ export const useMetadata = ({ id, type, addonId }: UseMetadataProps): UseMetadat }; fetchTmdbIdAndRecommendations(); - }, [metadata, id]); + }, [metadata, id, settings.enrichMetadataWithTMDB]); useEffect(() => { if (tmdbId) { - if (__DEV__) console.log('[useMetadata] tmdbId available; loading recommendations and enabling certification checks', { tmdbId }); - loadRecommendations(); + if (settings.enrichMetadataWithTMDB) { + if (__DEV__) console.log('[useMetadata] tmdbId available; loading recommendations and enabling certification checks', { tmdbId }); + loadRecommendations(); + } // Reset recommendations when tmdbId changes return () => { setRecommendations([]); setLoadingRecommendations(true); }; } - }, [tmdbId, loadRecommendations]); + }, [tmdbId, loadRecommendations, settings.enrichMetadataWithTMDB]); // Ensure certification is attached whenever a TMDB id is known and metadata lacks it useEffect(() => { const maybeAttachCertification = async () => { + if (!settings.enrichMetadataWithTMDB) { + if (__DEV__) console.log('[useMetadata] enrichment disabled; skip certification (attach path)'); + return; + } try { if (!metadata) { if (__DEV__) console.warn('[useMetadata] skip certification attach: metadata not ready'); @@ -1311,7 +1367,7 @@ export const useMetadata = ({ id, type, addonId }: UseMetadataProps): UseMetadat } }; maybeAttachCertification(); - }, [tmdbId, metadata, type]); + }, [tmdbId, metadata, type, settings.enrichMetadataWithTMDB]); // Reset tmdbId when id changes useEffect(() => { diff --git a/src/hooks/useSettings.ts b/src/hooks/useSettings.ts index 00cd7963..f782b5b4 100644 --- a/src/hooks/useSettings.ts +++ b/src/hooks/useSettings.ts @@ -76,6 +76,8 @@ export interface AppSettings { trailerMuted: boolean; // Default to muted for better user experience // AI aiChatEnabled: boolean; // Enable/disable Ask AI and AI features + // Metadata enrichment + enrichMetadataWithTMDB: boolean; // Use TMDB to enrich metadata (cast, certification, posters, fallbacks) } export const DEFAULT_SETTINGS: AppSettings = { @@ -124,6 +126,8 @@ export const DEFAULT_SETTINGS: AppSettings = { trailerMuted: true, // Default to muted for better user experience // AI aiChatEnabled: false, + // Metadata enrichment + enrichMetadataWithTMDB: true, }; const SETTINGS_STORAGE_KEY = 'app_settings'; diff --git a/src/screens/TMDBSettingsScreen.tsx b/src/screens/TMDBSettingsScreen.tsx index d3e1f3c4..b505a85b 100644 --- a/src/screens/TMDBSettingsScreen.tsx +++ b/src/screens/TMDBSettingsScreen.tsx @@ -27,6 +27,7 @@ import { logger } from '../utils/logger'; import { useTheme } from '../contexts/ThemeContext'; import { useSafeAreaInsets } from 'react-native-safe-area-context'; import CustomAlert from '../components/CustomAlert'; +// (duplicate import removed) const TMDB_API_KEY_STORAGE_KEY = 'tmdb_api_key'; const USE_CUSTOM_TMDB_API_KEY = 'use_custom_tmdb_api_key'; @@ -48,6 +49,7 @@ const TMDBSettingsScreen = () => { const apiKeyInputRef = useRef(null); const { currentTheme } = useTheme(); const insets = useSafeAreaInsets(); + const { settings, updateSetting } = useSettings(); const openAlert = ( title: string, @@ -288,6 +290,19 @@ const TMDBSettingsScreen = () => { keyboardShouldPersistTaps="handled" showsVerticalScrollIndicator={false} > + + + Enrich Metadata with TMDb + When enabled, the app augments addon metadata with TMDb for cast, certification, logos/posters, and episode fallback. Disable to strictly use addon metadata only. + + updateSetting('enrichMetadataWithTMDB', v)} + trackColor={{ false: 'rgba(255,255,255,0.1)', true: currentTheme.colors.primary }} + thumbColor={Platform.OS === 'android' ? (settings.enrichMetadataWithTMDB ? currentTheme.colors.white : currentTheme.colors.white) : ''} + ios_backgroundColor={'rgba(255,255,255,0.1)'} + /> + Use Custom TMDb API Key