From ba834ed3a8c38d1131ee29ab21e415021a9db531 Mon Sep 17 00:00:00 2001 From: tapframe Date: Sat, 3 May 2025 19:10:27 +0530 Subject: [PATCH] Enhance logo fetching logic in FeaturedContent and MetadataScreen components Refactor the FeaturedContent component to dynamically fetch logos based on user preferences for TMDB or Metahub, improving error handling and loading states. Update the MetadataScreen to implement a more robust banner fetching mechanism, utilizing both sources with appropriate fallbacks. Introduce loading indicators and ensure seamless user experience during logo and banner retrieval processes. --- src/components/home/FeaturedContent.tsx | 185 +++++++++- src/screens/LogoSourceSettings.tsx | 438 +++++++++++++++++------- src/screens/MetadataScreen.tsx | 312 +++++++++++------ 3 files changed, 696 insertions(+), 239 deletions(-) diff --git a/src/components/home/FeaturedContent.tsx b/src/components/home/FeaturedContent.tsx index da73b27e..f161d8dd 100644 --- a/src/components/home/FeaturedContent.tsx +++ b/src/components/home/FeaturedContent.tsx @@ -28,7 +28,10 @@ import Animated, { } from 'react-native-reanimated'; import { StreamingContent } from '../../services/catalogService'; import { SkeletonFeatured } from './SkeletonLoaders'; -import { isValidMetahubLogo, hasValidLogoFormat } from '../../utils/logoUtils'; +import { isValidMetahubLogo, hasValidLogoFormat, isMetahubUrl, isTmdbUrl } from '../../utils/logoUtils'; +import { useSettings } from '../../hooks/useSettings'; +import { TMDBService } from '../../services/tmdbService'; +import { logger } from '../../utils/logger'; interface FeaturedContentProps { featuredContent: StreamingContent | null; @@ -43,11 +46,14 @@ const { width, height } = Dimensions.get('window'); const FeaturedContent = ({ featuredContent, isSaved, handleSaveToLibrary }: FeaturedContentProps) => { const navigation = useNavigation>(); + const { settings } = useSettings(); const [logoUrl, setLogoUrl] = useState(null); const [bannerUrl, setBannerUrl] = useState(null); const prevContentIdRef = useRef(null); // Add state for tracking logo load errors const [logoLoadError, setLogoLoadError] = useState(false); + // Add a ref to track logo fetch in progress + const logoFetchInProgress = useRef(false); // Animation values const posterOpacity = useSharedValue(0); @@ -107,13 +113,171 @@ const FeaturedContent = ({ featuredContent, isSaved, handleSaveToLibrary }: Feat useEffect(() => { setLogoLoadError(false); }, [featuredContent?.id]); + + // Fetch logo based on preference + useEffect(() => { + if (!featuredContent || logoFetchInProgress.current) return; + + const fetchLogo = async () => { + // Set fetch in progress flag + logoFetchInProgress.current = true; + + try { + const contentId = featuredContent.id; + + // Get logo source preference from settings + const logoPreference = settings.logoSourcePreference || 'metahub'; // Default to metahub if not set + + // Check if current logo matches preferences + const currentLogo = featuredContent.logo; + if (currentLogo) { + const isCurrentMetahub = isMetahubUrl(currentLogo); + const isCurrentTmdb = isTmdbUrl(currentLogo); + + // If logo already matches preference, use it + if ((logoPreference === 'metahub' && isCurrentMetahub) || + (logoPreference === 'tmdb' && isCurrentTmdb)) { + setLogoUrl(currentLogo); + logoFetchInProgress.current = false; + return; + } + } + + logger.log(`[FeaturedContent] Fetching logo with preference: ${logoPreference}, ID: ${contentId}`); + + // Extract IMDB ID if available + let imdbId = null; + if (featuredContent.id.startsWith('tt')) { + // If the ID itself is an IMDB ID + imdbId = featuredContent.id; + } else if ((featuredContent as any).imdbId) { + // Try to get IMDB ID from the content object if available + imdbId = (featuredContent as any).imdbId; + } + + // Extract TMDB ID if available + let tmdbId = null; + if (contentId.startsWith('tmdb:')) { + tmdbId = contentId.split(':')[1]; + } + + // First source based on preference + if (logoPreference === 'metahub' && imdbId) { + // Try to get logo from Metahub first + const metahubUrl = `https://images.metahub.space/logo/medium/${imdbId}/img`; + + try { + const response = await fetch(metahubUrl, { method: 'HEAD' }); + if (response.ok) { + logger.log(`[FeaturedContent] Using Metahub logo: ${metahubUrl}`); + setLogoUrl(metahubUrl); + logoFetchInProgress.current = false; + return; // Exit if Metahub logo was found + } + } catch (error) { + logger.warn(`[FeaturedContent] Failed to fetch Metahub logo:`, error); + } + + // Fall back to TMDB if Metahub fails and we have a TMDB ID + if (tmdbId) { + const tmdbType = featuredContent.type === 'series' ? 'tv' : 'movie'; + try { + const tmdbService = TMDBService.getInstance(); + const logoUrl = await tmdbService.getContentLogo(tmdbType, tmdbId); + + if (logoUrl) { + logger.log(`[FeaturedContent] Using fallback TMDB logo: ${logoUrl}`); + setLogoUrl(logoUrl); + } else if (currentLogo) { + // If TMDB fails too, use existing logo if any + setLogoUrl(currentLogo); + } + } catch (error) { + logger.error('[FeaturedContent] Error fetching TMDB logo:', error); + if (currentLogo) setLogoUrl(currentLogo); + } + } else if (currentLogo) { + // Use existing logo if we don't have TMDB ID + setLogoUrl(currentLogo); + } + } else if (logoPreference === 'tmdb') { + // Try to get logo from TMDB first + if (tmdbId) { + const tmdbType = featuredContent.type === 'series' ? 'tv' : 'movie'; + try { + const tmdbService = TMDBService.getInstance(); + const logoUrl = await tmdbService.getContentLogo(tmdbType, tmdbId); + + if (logoUrl) { + logger.log(`[FeaturedContent] Using TMDB logo: ${logoUrl}`); + setLogoUrl(logoUrl); + logoFetchInProgress.current = false; + return; // Exit if TMDB logo was found + } + } catch (error) { + logger.error('[FeaturedContent] Error fetching TMDB logo:', error); + } + } else if (imdbId) { + // If we have IMDB ID but no TMDB ID, try to find TMDB ID + try { + const tmdbService = TMDBService.getInstance(); + const foundTmdbId = await tmdbService.findTMDBIdByIMDB(imdbId); + + if (foundTmdbId) { + const tmdbType = featuredContent.type === 'series' ? 'tv' : 'movie'; + const logoUrl = await tmdbService.getContentLogo(tmdbType, foundTmdbId.toString()); + + if (logoUrl) { + logger.log(`[FeaturedContent] Using TMDB logo via IMDB lookup: ${logoUrl}`); + setLogoUrl(logoUrl); + logoFetchInProgress.current = false; + return; // Exit if TMDB logo was found + } + } + } catch (error) { + logger.error('[FeaturedContent] Error finding TMDB ID from IMDB:', error); + } + } + + // Fall back to Metahub if TMDB fails and we have an IMDB ID + if (imdbId) { + const metahubUrl = `https://images.metahub.space/logo/medium/${imdbId}/img`; + + try { + const response = await fetch(metahubUrl, { method: 'HEAD' }); + if (response.ok) { + logger.log(`[FeaturedContent] Using fallback Metahub logo: ${metahubUrl}`); + setLogoUrl(metahubUrl); + } else if (currentLogo) { + // If Metahub fails too, use existing logo if any + setLogoUrl(currentLogo); + } + } catch (error) { + logger.warn(`[FeaturedContent] Failed to fetch fallback Metahub logo:`, error); + if (currentLogo) setLogoUrl(currentLogo); + } + } else if (currentLogo) { + // Use existing logo if we don't have IMDB ID + setLogoUrl(currentLogo); + } + } + } catch (error) { + logger.error('[FeaturedContent] Error fetching logo:', error); + if (featuredContent?.logo) setLogoUrl(featuredContent.logo); + } finally { + // Clear fetch in progress flag + logoFetchInProgress.current = false; + } + }; + + fetchLogo(); + }, [featuredContent?.id, settings.logoSourcePreference]); // Load poster and logo useEffect(() => { if (!featuredContent) return; const posterUrl = featuredContent.banner || featuredContent.poster; - const titleLogo = featuredContent.logo; const contentId = featuredContent.id; // Reset states for new content @@ -124,9 +288,8 @@ const FeaturedContent = ({ featuredContent, isSaved, handleSaveToLibrary }: Feat prevContentIdRef.current = contentId; - // Set URLs immediately for instant display + // Set poster URL immediately for instant display if (posterUrl) setBannerUrl(posterUrl); - if (titleLogo) setLogoUrl(titleLogo); // Load images in background const loadImages = async () => { @@ -142,8 +305,8 @@ const FeaturedContent = ({ featuredContent, isSaved, handleSaveToLibrary }: Feat } // Load logo if available - if (titleLogo) { - const logoSuccess = await preloadImage(titleLogo); + if (logoUrl) { + const logoSuccess = await preloadImage(logoUrl); if (logoSuccess) { logoOpacity.value = withDelay(300, withTiming(1, { duration: 500, @@ -152,13 +315,13 @@ const FeaturedContent = ({ featuredContent, isSaved, handleSaveToLibrary }: Feat } else { // If prefetch fails, mark as error to show title text instead setLogoLoadError(true); - console.warn(`[FeaturedContent] Logo prefetch failed, falling back to text: ${titleLogo}`); + console.warn(`[FeaturedContent] Logo prefetch failed, falling back to text: ${logoUrl}`); } } }; loadImages(); - }, [featuredContent?.id]); + }, [featuredContent?.id, logoUrl]); if (!featuredContent) { return ; @@ -194,16 +357,16 @@ const FeaturedContent = ({ featuredContent, isSaved, handleSaveToLibrary }: Feat - {featuredContent.logo && !logoLoadError ? ( + {logoUrl && !logoLoadError ? ( { - console.warn(`[FeaturedContent] Logo failed to load: ${featuredContent.logo}`); + console.warn(`[FeaturedContent] Logo failed to load: ${logoUrl}`); setLogoLoadError(true); }} /> diff --git a/src/screens/LogoSourceSettings.tsx b/src/screens/LogoSourceSettings.tsx index 30bb1ab0..e27ee9e0 100644 --- a/src/screens/LogoSourceSettings.tsx +++ b/src/screens/LogoSourceSettings.tsx @@ -11,7 +11,7 @@ import { Alert, StatusBar, Platform, - ActivityIndicator + ActivityIndicator, } from 'react-native'; import { NavigationProp, useNavigation } from '@react-navigation/native'; import { MaterialIcons } from '@expo/vector-icons'; @@ -25,6 +25,52 @@ import { logger } from '../utils/logger'; // TMDB API key - since the default key might be private in the service, we'll use our own const TMDB_API_KEY = '439c478a771f35c05022f9feabcca01c'; +// Define example shows with their IMDB IDs and TMDB IDs +const EXAMPLE_SHOWS = [ + { + name: 'Breaking Bad', + imdbId: 'tt0903747', + tmdbId: '1396', + type: 'tv' as const + }, + { + name: 'Friends', + imdbId: 'tt0108778', + tmdbId: '1668', + type: 'tv' as const + }, + { + name: 'Game of Thrones', + imdbId: 'tt0944947', + tmdbId: '1399', + type: 'tv' as const + }, + { + name: 'Stranger Things', + imdbId: 'tt4574334', + tmdbId: '66732', + type: 'tv' as const + }, + { + name: 'Squid Game', + imdbId: 'tt10919420', + tmdbId: '93405', + type: 'tv' as const + }, + { + name: 'Avatar', + imdbId: 'tt0499549', + tmdbId: '19995', + type: 'movie' as const + }, + { + name: 'The Witcher', + imdbId: 'tt5180504', + tmdbId: '71912', + type: 'tv' as const + } +]; + const LogoSourceSettings = () => { const { settings, updateSetting } = useSettings(); const navigation = useNavigation>(); @@ -35,117 +81,125 @@ const LogoSourceSettings = () => { settings.logoSourcePreference || 'metahub' ); - // Add state for example logos + // Make sure logoSource stays in sync with settings + useEffect(() => { + setLogoSource(settings.logoSourcePreference || 'metahub'); + }, [settings.logoSourcePreference]); + + // Selected example show + const [selectedShow, setSelectedShow] = useState(EXAMPLE_SHOWS[0]); + + // Add state for example logos and banners const [tmdbLogo, setTmdbLogo] = useState(null); const [metahubLogo, setMetahubLogo] = useState(null); + const [tmdbBanner, setTmdbBanner] = useState(null); + const [metahubBanner, setMetahubBanner] = useState(null); const [loadingLogos, setLoadingLogos] = useState(true); - // Load example logos on mount + // Load example logos for selected show useEffect(() => { - const fetchExampleLogos = async () => { - setLoadingLogos(true); - - try { - const tmdbService = TMDBService.getInstance(); - - // Specifically search for Breaking Bad - const searchResults = await tmdbService.searchTVShow("Breaking Bad"); - - if (searchResults && searchResults.length > 0) { - // Get Breaking Bad (should be the first result) - const breakingBad = searchResults[0]; - const breakingBadId = breakingBad.id; - - logger.log(`[LogoSourceSettings] Found Breaking Bad with TMDB ID: ${breakingBadId}`); - - // Get the external IDs to get IMDB ID - const externalIds = await tmdbService.getShowExternalIds(breakingBadId); - - if (externalIds?.imdb_id) { - const imdbId = externalIds.imdb_id; - logger.log(`[LogoSourceSettings] Breaking Bad IMDB ID: ${imdbId}`); - - // Get TMDB logo using the images endpoint - try { - // Manually fetch images from TMDB API - const apiKey = TMDB_API_KEY; // Use the TMDB API key - const response = await fetch(`https://api.themoviedb.org/3/tv/${breakingBadId}/images?api_key=${apiKey}`); - const imagesData = await response.json(); - - if (imagesData.logos && imagesData.logos.length > 0) { - // Look for English logo first - let logoPath = null; - - // First try to find an English logo - const englishLogo = imagesData.logos.find((logo: { iso_639_1: string; file_path: string }) => - logo.iso_639_1 === 'en' - ); - if (englishLogo) { - logoPath = englishLogo.file_path; - } else if (imagesData.logos[0]) { - // Fallback to the first logo - logoPath = imagesData.logos[0].file_path; - } - - if (logoPath) { - const tmdbLogoUrl = `https://image.tmdb.org/t/p/original${logoPath}`; - setTmdbLogo(tmdbLogoUrl); - logger.log(`[LogoSourceSettings] Got Breaking Bad TMDB logo: ${tmdbLogoUrl}`); - } else { - // Fallback to hardcoded Breaking Bad TMDB logo - setTmdbLogo('https://image.tmdb.org/t/p/original/ggFHVNu6YYI5L9pCfOacjizRGt.png'); - logger.log(`[LogoSourceSettings] Using fallback Breaking Bad TMDB logo`); - } - } else { - // No logos found in the response - setTmdbLogo('https://image.tmdb.org/t/p/original/ggFHVNu6YYI5L9pCfOacjizRGt.png'); - logger.log(`[LogoSourceSettings] No logos found in TMDB response, using fallback`); - } - } catch (tmdbError) { - logger.error(`[LogoSourceSettings] Error fetching TMDB images:`, tmdbError); - // Fallback to hardcoded Breaking Bad TMDB logo - setTmdbLogo('https://image.tmdb.org/t/p/original/ggFHVNu6YYI5L9pCfOacjizRGt.png'); - } - - // Get Metahub logo - const metahubLogoUrl = `https://images.metahub.space/logo/medium/${imdbId}/img`; - - // Check if Metahub logo exists - try { - const metahubResponse = await fetch(metahubLogoUrl, { method: 'HEAD' }); - if (metahubResponse.ok) { - setMetahubLogo(metahubLogoUrl); - logger.log(`[LogoSourceSettings] Got Breaking Bad Metahub logo: ${metahubLogoUrl}`); - } else { - // Fallback to hardcoded Breaking Bad Metahub logo - setMetahubLogo('https://images.metahub.space/logo/medium/tt0903747/img'); - logger.log(`[LogoSourceSettings] Using fallback Breaking Bad Metahub logo`); - } - } catch (metahubErr) { - logger.error(`[LogoSourceSettings] Error checking Metahub logo:`, metahubErr); - // Fallback to hardcoded Breaking Bad Metahub logo - setMetahubLogo('https://images.metahub.space/logo/medium/tt0903747/img'); - } - } - } else { - logger.warn(`[LogoSourceSettings] Breaking Bad not found in search results`); - // Use hardcoded Breaking Bad logos - setTmdbLogo('https://image.tmdb.org/t/p/original/ggFHVNu6YYI5L9pCfOacjizRGt.png'); - setMetahubLogo('https://images.metahub.space/logo/medium/tt0903747/img'); - } - } catch (err) { - logger.error('[LogoSourceSettings] Error fetching Breaking Bad logos:', err); - - // Use hardcoded Breaking Bad logos - setTmdbLogo('https://image.tmdb.org/t/p/original/ggFHVNu6YYI5L9pCfOacjizRGt.png'); - setMetahubLogo('https://images.metahub.space/logo/medium/tt0903747/img'); - } finally { - setLoadingLogos(false); - } - }; + fetchExampleLogos(selectedShow); + }, [selectedShow]); + + // Function to fetch logos and banners for a specific show + const fetchExampleLogos = async (show: typeof EXAMPLE_SHOWS[0]) => { + setLoadingLogos(true); + setTmdbLogo(null); + setMetahubLogo(null); + setTmdbBanner(null); + setMetahubBanner(null); - fetchExampleLogos(); - }, []); + try { + const tmdbService = TMDBService.getInstance(); + const imdbId = show.imdbId; + const tmdbId = show.tmdbId; + const contentType = show.type; + + logger.log(`[LogoSourceSettings] Fetching ${show.name} with TMDB ID: ${tmdbId}, IMDB ID: ${imdbId}`); + + // Get TMDB logo and banner + try { + // Manually fetch images from TMDB API + const apiKey = TMDB_API_KEY; + const endpoint = contentType === 'tv' ? 'tv' : 'movie'; + const response = await fetch(`https://api.themoviedb.org/3/${endpoint}/${tmdbId}/images?api_key=${apiKey}`); + const imagesData = await response.json(); + + // Get TMDB logo + if (imagesData.logos && imagesData.logos.length > 0) { + // Look for English logo first + let logoPath = null; + + // First try to find an English logo + const englishLogo = imagesData.logos.find((logo: { iso_639_1: string; file_path: string }) => + logo.iso_639_1 === 'en' + ); + if (englishLogo) { + logoPath = englishLogo.file_path; + } else if (imagesData.logos[0]) { + // Fallback to the first logo + logoPath = imagesData.logos[0].file_path; + } + + if (logoPath) { + const tmdbLogoUrl = `https://image.tmdb.org/t/p/original${logoPath}`; + setTmdbLogo(tmdbLogoUrl); + logger.log(`[LogoSourceSettings] Got ${show.name} TMDB logo: ${tmdbLogoUrl}`); + } + } + + // Get TMDB banner (backdrop) + if (imagesData.backdrops && imagesData.backdrops.length > 0) { + const backdropPath = imagesData.backdrops[0].file_path; + const tmdbBannerUrl = `https://image.tmdb.org/t/p/original${backdropPath}`; + setTmdbBanner(tmdbBannerUrl); + logger.log(`[LogoSourceSettings] Got ${show.name} TMDB banner: ${tmdbBannerUrl}`); + } else { + // Try to get backdrop from details + const detailsResponse = await fetch(`https://api.themoviedb.org/3/${endpoint}/${tmdbId}?api_key=${apiKey}`); + const details = await detailsResponse.json(); + + if (details.backdrop_path) { + const tmdbBannerUrl = `https://image.tmdb.org/t/p/original${details.backdrop_path}`; + setTmdbBanner(tmdbBannerUrl); + logger.log(`[LogoSourceSettings] Got ${show.name} TMDB banner from details: ${tmdbBannerUrl}`); + } + } + } catch (tmdbError) { + logger.error(`[LogoSourceSettings] Error fetching TMDB images:`, tmdbError); + } + + // Get Metahub logo and banner + try { + // Metahub logo + const metahubLogoUrl = `https://images.metahub.space/logo/medium/${imdbId}/img`; + const logoResponse = await fetch(metahubLogoUrl, { method: 'HEAD' }); + + if (logoResponse.ok) { + setMetahubLogo(metahubLogoUrl); + logger.log(`[LogoSourceSettings] Got ${show.name} Metahub logo: ${metahubLogoUrl}`); + } + + // Metahub banner + const metahubBannerUrl = `https://images.metahub.space/background/medium/${imdbId}/img`; + const bannerResponse = await fetch(metahubBannerUrl, { method: 'HEAD' }); + + if (bannerResponse.ok) { + setMetahubBanner(metahubBannerUrl); + logger.log(`[LogoSourceSettings] Got ${show.name} Metahub banner: ${metahubBannerUrl}`); + } else if (tmdbBanner) { + // If Metahub banner doesn't exist, use TMDB banner + setMetahubBanner(tmdbBanner); + } + } catch (metahubErr) { + logger.error(`[LogoSourceSettings] Error checking Metahub images:`, metahubErr); + } + } catch (err) { + logger.error(`[LogoSourceSettings] Error fetching ${show.name} logos:`, err); + } finally { + setLoadingLogos(false); + } + }; // Apply setting and show confirmation const applyLogoSourceSetting = (source: 'metahub' | 'tmdb') => { @@ -167,13 +221,47 @@ const LogoSourceSettings = () => { ); }; + // Save selected show to AsyncStorage to persist across navigation + const saveSelectedShow = async (show: typeof EXAMPLE_SHOWS[0]) => { + try { + await AsyncStorage.setItem('logo_settings_selected_show', show.imdbId); + } catch (e) { + console.error('Error saving selected show:', e); + } + }; + + // Load selected show from AsyncStorage on mount + useEffect(() => { + const loadSelectedShow = async () => { + try { + const savedShowId = await AsyncStorage.getItem('logo_settings_selected_show'); + if (savedShowId) { + const foundShow = EXAMPLE_SHOWS.find(show => show.imdbId === savedShowId); + if (foundShow) { + setSelectedShow(foundShow); + } + } + } catch (e) { + console.error('Error loading selected show:', e); + } + }; + + loadSelectedShow(); + }, []); + + // Update selected show and save to AsyncStorage + const handleShowSelect = (show: typeof EXAMPLE_SHOWS[0]) => { + setSelectedShow(show); + saveSelectedShow(show); + }; + // Handle back navigation const handleBack = () => { navigation.goBack(); }; - // Render logo example with loading state - const renderLogoExample = (url: string | null, isLoading: boolean) => { + // Render logo example with loading state and background + const renderLogoExample = (logo: string | null, banner: string | null, isLoading: boolean) => { if (isLoading) { return ( @@ -183,11 +271,26 @@ const LogoSourceSettings = () => { } return ( - + + + + {logo && ( + + )} + {!logo && ( + + No logo available + + )} + ); }; @@ -217,6 +320,36 @@ const LogoSourceSettings = () => { + {/* Show selector */} + + Select a show/movie to preview: + + {EXAMPLE_SHOWS.map((show) => ( + handleShowSelect(show)} + > + + {show.name} + + + ))} + + + {/* Options */} { Example: - {renderLogoExample(metahubLogo, loadingLogos)} - Breaking Bad logo from Metahub + {renderLogoExample(metahubLogo, metahubBanner, loadingLogos)} + {selectedShow.name} logo from Metahub @@ -266,8 +399,8 @@ const LogoSourceSettings = () => { Example: - {renderLogoExample(tmdbLogo, loadingLogos)} - Breaking Bad logo from TMDB + {renderLogoExample(tmdbLogo, tmdbBanner, loadingLogos)} + {selectedShow.name} logo from TMDB @@ -324,6 +457,39 @@ const styles = StyleSheet.create({ fontSize: 16, lineHeight: 24, }, + showSelectorContainer: { + padding: 16, + paddingBottom: 8, + }, + selectorLabel: { + color: colors.text, + fontSize: 16, + marginBottom: 12, + }, + showsScrollContent: { + paddingRight: 16, + }, + showItem: { + paddingHorizontal: 16, + paddingVertical: 8, + backgroundColor: colors.elevation2, + borderRadius: 20, + marginRight: 8, + borderWidth: 1, + borderColor: 'transparent', + }, + selectedShowItem: { + borderColor: colors.primary, + backgroundColor: colors.elevation3, + }, + showItemText: { + color: colors.mediumEmphasis, + fontSize: 14, + }, + selectedShowItemText: { + color: colors.white, + fontWeight: '600', + }, optionsContainer: { padding: 16, gap: 16, @@ -391,6 +557,44 @@ const styles = StyleSheet.create({ fontSize: 12, marginTop: 4, }, + bannerContainer: { + height: 120, + width: '100%', + borderRadius: 8, + overflow: 'hidden', + position: 'relative', + }, + bannerImage: { + ...StyleSheet.absoluteFillObject, + }, + bannerOverlay: { + ...StyleSheet.absoluteFillObject, + backgroundColor: 'rgba(0,0,0,0.5)', + }, + logoOverBanner: { + position: 'absolute', + width: '80%', + height: '80%', + alignSelf: 'center', + top: '10%', + }, + noLogoContainer: { + position: 'absolute', + top: 0, + left: 0, + right: 0, + bottom: 0, + justifyContent: 'center', + alignItems: 'center', + }, + noLogoText: { + color: colors.white, + fontSize: 14, + backgroundColor: 'rgba(0,0,0,0.5)', + paddingHorizontal: 12, + paddingVertical: 6, + borderRadius: 4, + }, }); export default LogoSourceSettings; \ No newline at end of file diff --git a/src/screens/MetadataScreen.tsx b/src/screens/MetadataScreen.tsx index 16e8c93d..db02a12f 100644 --- a/src/screens/MetadataScreen.tsx +++ b/src/screens/MetadataScreen.tsx @@ -222,6 +222,7 @@ const MetadataScreen = () => { // Add state for custom banner const [bannerImage, setBannerImage] = useState(null); const forcedBannerRefreshDone = useRef(false); + const [loadingBanner, setLoadingBanner] = useState(false); // Add debug log for settings when component mounts useEffect(() => { @@ -278,46 +279,147 @@ const MetadataScreen = () => { // Fetch banner image based on logo source preference useEffect(() => { const fetchBanner = async () => { - if (metadata && (!forcedBannerRefreshDone.current || foundTmdbId)) { - // Extract any existing TMDB ID if available - let tmdbId = null; + if (metadata) { + setLoadingBanner(true); + + // Clear the banner initially when starting a preference-driven fetch + setBannerImage(null); + + let finalBanner: string | null = metadata.banner || metadata.poster; // Default fallback + const preference = settings.logoSourcePreference || 'metahub'; + const apiKey = '439c478a771f35c05022f9feabcca01c'; // Re-using API key + + // Extract IDs + let currentTmdbId = null; if (id.startsWith('tmdb:')) { - tmdbId = id.split(':')[1]; + currentTmdbId = id.split(':')[1]; + } else if (foundTmdbId) { + currentTmdbId = foundTmdbId; + } else if ((metadata as any).tmdbId) { + currentTmdbId = (metadata as any).tmdbId; } - // Use our stored TMDB ID if we have one - const effectiveTmdbId = foundTmdbId || tmdbId || (metadata as any).tmdbId; + const currentImdbId = imdbId; + const contentType = type === 'series' ? 'tv' : 'movie'; - logger.log(`[MetadataScreen] Fetching banner with preference: ${settings.logoSourcePreference}, TMDB ID: ${effectiveTmdbId}`); + logger.log(`[MetadataScreen] Fetching banner with preference: ${preference}, TMDB ID: ${currentTmdbId}, IMDB ID: ${currentImdbId}`); try { - // Use our utility function to get the banner based on preference - const newBanner = await fetchBannerWithPreference( - imdbId, - effectiveTmdbId, - type as 'movie' | 'series', - settings.logoSourcePreference - ); - - if (newBanner) { - logger.log(`[MetadataScreen] Setting new banner: ${newBanner}`); - setBannerImage(newBanner); - } else { - // If no banner found from preferred source, use the existing one from metadata - logger.log(`[MetadataScreen] Using existing banner from metadata: ${metadata.banner}`); - setBannerImage(metadata.banner || metadata.poster); + if (preference === 'tmdb') { + // 1. Try TMDB first + let tmdbBannerUrl: string | null = null; + if (currentTmdbId) { + logger.log(`[MetadataScreen] Attempting TMDB banner fetch with ID: ${currentTmdbId}`); + try { + const endpoint = contentType === 'tv' ? 'tv' : 'movie'; + const response = await fetch(`https://api.themoviedb.org/3/${endpoint}/${currentTmdbId}/images?api_key=${apiKey}`); + const imagesData = await response.json(); + + if (imagesData.backdrops && imagesData.backdrops.length > 0) { + const backdropPath = imagesData.backdrops[0].file_path; + tmdbBannerUrl = `https://image.tmdb.org/t/p/original${backdropPath}`; + logger.log(`[MetadataScreen] Found TMDB banner via images endpoint: ${tmdbBannerUrl}`); + } else { + // Add log for when no backdrops are found + logger.warn(`[MetadataScreen] TMDB API successful, but no backdrops found for ID: ${currentTmdbId}`); + } + } catch (err) { + logger.error(`[MetadataScreen] Error fetching TMDB banner via images endpoint:`, err); + } + } else { + // Add log for when no TMDB ID is available + logger.warn(`[MetadataScreen] No TMDB ID available to fetch TMDB banner.`); + } + + if (tmdbBannerUrl) { + // TMDB SUCCESS: Set banner and EXIT + finalBanner = tmdbBannerUrl; + logger.log(`[MetadataScreen] Setting final banner to TMDB source: ${finalBanner}`); + setBannerImage(finalBanner); + setLoadingBanner(false); + forcedBannerRefreshDone.current = true; + return; // <-- Exit here, don't attempt fallback + } else { + // TMDB FAILED: Proceed to Metahub fallback + logger.log(`[MetadataScreen] TMDB banner failed, trying Metahub fallback.`); + if (currentImdbId) { + const metahubBannerUrl = `https://images.metahub.space/background/medium/${currentImdbId}/img`; + try { + const metahubResponse = await fetch(metahubBannerUrl, { method: 'HEAD' }); + if (metahubResponse.ok) { + finalBanner = metahubBannerUrl; + logger.log(`[MetadataScreen] Found Metahub banner as fallback: ${finalBanner}`); + } + } catch (err) { + logger.error(`[MetadataScreen] Error fetching Metahub fallback banner:`, err); + } + } + } + } else { // Preference is Metahub + // 1. Try Metahub first + let metahubBannerUrl: string | null = null; + if (currentImdbId) { + const url = `https://images.metahub.space/background/medium/${currentImdbId}/img`; + try { + const metahubResponse = await fetch(url, { method: 'HEAD' }); + if (metahubResponse.ok) { + metahubBannerUrl = url; + logger.log(`[MetadataScreen] Found Metahub banner: ${metahubBannerUrl}`); + } + } catch (err) { + logger.error(`[MetadataScreen] Error fetching Metahub banner:`, err); + } + } + + if (metahubBannerUrl) { + // METAHUB SUCCESS: Set banner and EXIT + finalBanner = metahubBannerUrl; + logger.log(`[MetadataScreen] Setting final banner to Metahub source: ${finalBanner}`); + setBannerImage(finalBanner); + setLoadingBanner(false); + forcedBannerRefreshDone.current = true; + return; // <-- Exit here, don't attempt fallback + } else { + // METAHUB FAILED: Proceed to TMDB fallback + logger.log(`[MetadataScreen] Metahub banner failed, trying TMDB fallback.`); + if (currentTmdbId) { + try { + const endpoint = contentType === 'tv' ? 'tv' : 'movie'; + const response = await fetch(`https://api.themoviedb.org/3/${endpoint}/${currentTmdbId}/images?api_key=${apiKey}`); + const imagesData = await response.json(); + + if (imagesData.backdrops && imagesData.backdrops.length > 0) { + const backdropPath = imagesData.backdrops[0].file_path; + finalBanner = `https://image.tmdb.org/t/p/original${backdropPath}`; + logger.log(`[MetadataScreen] Found TMDB banner as fallback: ${finalBanner}`); + } + } catch (err) { + logger.error(`[MetadataScreen] Error fetching TMDB fallback banner:`, err); + } + } + } } + + // Set the final determined banner (could be fallback or initial default) + setBannerImage(finalBanner); + logger.log(`[MetadataScreen] Final banner set after fallbacks (if any): ${finalBanner}`); + } catch (error) { - logger.error(`[MetadataScreen] Error fetching banner:`, error); - // Use existing banner as fallback + logger.error(`[MetadataScreen] General error fetching banner:`, error); + // Fallback to initial banner on general error setBannerImage(metadata.banner || metadata.poster); + } finally { + // Only set loading to false here if we didn't exit early + setLoadingBanner(false); + forcedBannerRefreshDone.current = true; // Mark refresh as done } - - forcedBannerRefreshDone.current = true; } }; + // Only run fetchBanner if metadata exists and preference/content might have changed + // The dependencies array handles triggering this effect fetchBanner(); + }, [metadata, id, type, imdbId, settings.logoSourcePreference, foundTmdbId]); // Reset forced refresh when preference changes @@ -325,7 +427,7 @@ const MetadataScreen = () => { if (forcedBannerRefreshDone.current) { logger.log(`[MetadataScreen] Logo preference changed, resetting banner refresh flag`); forcedBannerRefreshDone.current = false; - // Clear the banner image to force a new fetch + // Clear the banner image immediately to prevent showing the wrong source briefly setBannerImage(null); // This will trigger the banner fetch effect to run again } @@ -582,102 +684,84 @@ const MetadataScreen = () => { } } } else { // TMDB first - // Try to get logo from TMDB first - let tmdbId = null; - const tmdbType = type === 'series' ? 'tv' : 'movie'; + let tmdbLogoUrl: string | null = null; + // 1. Attempt to fetch TMDB logo if (id.startsWith('tmdb:')) { - // Direct TMDB ID - tmdbId = id.split(':')[1]; - logger.log(`[MetadataScreen] Content has direct TMDB ID: ${tmdbId}`); - } else if (id.startsWith('tt')) { - // IMDB ID - need to find the corresponding TMDB ID - logger.log(`[MetadataScreen] Content has IMDB ID (${id}), looking up TMDB ID`); - try { - // Use the passed imdbId if available, otherwise use id directly - const imdbIdToUse = imdbId || id; - logger.log(`[MetadataScreen] Using IMDB ID for lookup: ${imdbIdToUse}`); - - tmdbId = await TMDBService.getInstance().findTMDBIdByIMDB(imdbIdToUse); - if (tmdbId) { - logger.log(`[MetadataScreen] Found TMDB ID ${tmdbId} for IMDB ID ${imdbIdToUse}`); - - // Save the TMDB ID for banner fetching - setFoundTmdbId(String(tmdbId)); - } else { - logger.warn(`[MetadataScreen] Could not find TMDB ID for IMDB ID ${imdbIdToUse}`); - } - } catch (error) { - logger.error(`[MetadataScreen] Error finding TMDB ID for IMDB ID ${id}:`, error); - } - } - - if (tmdbId) { + const tmdbId = id.split(':')[1]; + const tmdbType = type === 'series' ? 'tv' : 'movie'; logger.log(`[MetadataScreen] Attempting to fetch logo from TMDB for ${tmdbType} (ID: ${tmdbId})`); - try { const tmdbService = TMDBService.getInstance(); - logger.log(`[MetadataScreen] Calling getContentLogo with type=${tmdbType}, id=${tmdbId}`); + tmdbLogoUrl = await tmdbService.getContentLogo(tmdbType, tmdbId); - const logoUrl = await tmdbService.getContentLogo(tmdbType, tmdbId); - - if (logoUrl) { - logger.log(`[MetadataScreen] Successfully fetched logo from TMDB: - - Content Type: ${tmdbType} - - TMDB ID: ${tmdbId} - - Logo URL: ${logoUrl} - `); - - // Update metadata with TMDB logo - setMetadata(prevMetadata => ({ - ...prevMetadata!, - logo: logoUrl - })); - - // Clear fetch in progress flag when done - logoFetchInProgress.current = false; - return; // Exit if TMDB logo was found + if (tmdbLogoUrl) { + logger.log(`[MetadataScreen] Successfully fetched logo from TMDB: ${tmdbLogoUrl}`); } else { - logger.warn(`[MetadataScreen] No logo found from TMDB for ${type} (ID: ${tmdbId}), trying Metahub`); + logger.warn(`[MetadataScreen] No logo found from TMDB for ${type} (ID: ${tmdbId})`); } } catch (error) { logger.error(`[MetadataScreen] Error fetching TMDB logo for ID ${tmdbId}:`, error); } - } else { - logger.warn(`[MetadataScreen] No TMDB ID available, falling back to Metahub`); + } else if (imdbId) { + // If we have IMDB ID but no direct TMDB ID, try to find TMDB ID + logger.log(`[MetadataScreen] Content has IMDB ID (${imdbId}), looking up TMDB ID for TMDB logo`); + try { + const tmdbService = TMDBService.getInstance(); + const foundTmdbId = await tmdbService.findTMDBIdByIMDB(imdbId); + + if (foundTmdbId) { + logger.log(`[MetadataScreen] Found TMDB ID ${foundTmdbId} for IMDB ID ${imdbId}`); + setFoundTmdbId(String(foundTmdbId)); // Save for banner fetching + + tmdbLogoUrl = await tmdbService.getContentLogo(type === 'series' ? 'tv' : 'movie', foundTmdbId.toString()); + + if (tmdbLogoUrl) { + logger.log(`[MetadataScreen] Successfully fetched logo from TMDB via IMDB lookup: ${tmdbLogoUrl}`); + } else { + logger.warn(`[MetadataScreen] No logo found from TMDB via IMDB lookup for ${type} (IMDB: ${imdbId})`); + } + } else { + logger.warn(`[MetadataScreen] Could not find TMDB ID for IMDB ID ${imdbId}`); + } + } catch (error) { + logger.error(`[MetadataScreen] Error finding TMDB ID or fetching logo for IMDB ID ${imdbId}:`, error); + } } - // If TMDB fails or isn't a TMDB ID, try Metahub as fallback + // 2. If TMDB logo was fetched successfully, update and return + if (tmdbLogoUrl) { + setMetadata(prevMetadata => ({ + ...prevMetadata!, + logo: tmdbLogoUrl + })); + logoFetchInProgress.current = false; + return; + } + + // 3. If TMDB failed, try Metahub as fallback + logger.log(`[MetadataScreen] TMDB logo fetch failed or not applicable. Attempting Metahub fallback.`); if (imdbId) { const metahubUrl = `https://images.metahub.space/logo/medium/${imdbId}/img`; - logger.log(`[MetadataScreen] Attempting to fetch logo from Metahub as fallback for ${imdbId}`); try { const response = await fetch(metahubUrl, { method: 'HEAD' }); if (response.ok) { - logger.log(`[MetadataScreen] Successfully fetched fallback logo from Metahub: - - Content ID: ${id} - - Content Type: ${type} - - Logo URL: ${metahubUrl} - `); - - // Update metadata with Metahub logo - setMetadata(prevMetadata => ({ - ...prevMetadata!, - logo: metahubUrl - })); + logger.log(`[MetadataScreen] Successfully fetched fallback logo from Metahub: ${metahubUrl}`); + setMetadata(prevMetadata => ({ ...prevMetadata!, logo: metahubUrl })); } else { - // If both TMDB and Metahub fail, use the title as text instead of a logo - logger.warn(`[MetadataScreen] No logo found from either source for ${type} (ID: ${id}), using title text instead`); - - // Leave logo as null/undefined to trigger fallback to text + logger.warn(`[MetadataScreen] Metahub fallback failed. Using title text.`); + setMetadata(prevMetadata => ({ ...prevMetadata!, logo: undefined })); } } catch (metahubError) { - logger.warn(`[MetadataScreen] Failed to fetch logo from Metahub:`, metahubError); - - // Leave logo as null/undefined to trigger fallback to text + logger.warn(`[MetadataScreen] Failed to fetch fallback logo from Metahub:`, metahubError); + setMetadata(prevMetadata => ({ ...prevMetadata!, logo: undefined })); } + } else { + // No IMDB ID for Metahub fallback + logger.warn(`[MetadataScreen] No IMDB ID for Metahub fallback. Using title text.`); + setMetadata(prevMetadata => ({ ...prevMetadata!, logo: undefined })); } } } catch (error) { @@ -686,6 +770,8 @@ const MetadataScreen = () => { contentId: id, contentType: type }); + // Fallback to text on general error + setMetadata(prevMetadata => ({ ...prevMetadata!, logo: undefined })); } finally { // Clear fetch in progress flag when done logoFetchInProgress.current = false; @@ -1484,18 +1570,22 @@ const MetadataScreen = () => { {/* Use Animated.Image directly instead of ImageBackground with imageStyle */} - { - logger.warn(`[MetadataScreen] Banner failed to load: ${bannerImage}`); - // If custom banner fails, fall back to original metadata banner - if (bannerImage !== metadata.banner) { - setBannerImage(metadata.banner || metadata.poster); - } - }} - /> + {loadingBanner ? ( + + ) : ( + { + logger.warn(`[MetadataScreen] Banner failed to load: ${bannerImage}`); + // If custom banner fails, fall back to original metadata banner + if (bannerImage !== metadata.banner) { + setBannerImage(metadata.banner || metadata.poster); + } + }} + /> + )}