From 5a64adec225c05441563647e3540429981f2e489 Mon Sep 17 00:00:00 2001 From: tapframe Date: Sat, 3 May 2025 17:11:16 +0530 Subject: [PATCH] Enhance logo loading and error handling in FeaturedContent, MetadataScreen, and SearchScreen components. Introduce state management for logo load errors, improve image prefetching logic, and update UI to fallback to text when logos fail to load. Refactor TMDBService to include detailed logging for image URL construction and fetching processes. --- src/components/home/FeaturedContent.tsx | 37 +- src/screens/MetadataScreen.tsx | 42 +- src/screens/SearchScreen.tsx | 594 ++++++++++++++++++------ src/services/tmdbService.ts | 52 ++- src/utils/logoUtils.ts | 83 ++++ 5 files changed, 663 insertions(+), 145 deletions(-) create mode 100644 src/utils/logoUtils.ts diff --git a/src/components/home/FeaturedContent.tsx b/src/components/home/FeaturedContent.tsx index 405208d8..da73b27e 100644 --- a/src/components/home/FeaturedContent.tsx +++ b/src/components/home/FeaturedContent.tsx @@ -28,6 +28,7 @@ import Animated, { } from 'react-native-reanimated'; import { StreamingContent } from '../../services/catalogService'; import { SkeletonFeatured } from './SkeletonLoaders'; +import { isValidMetahubLogo, hasValidLogoFormat } from '../../utils/logoUtils'; interface FeaturedContentProps { featuredContent: StreamingContent | null; @@ -45,6 +46,8 @@ const FeaturedContent = ({ featuredContent, isSaved, handleSaveToLibrary }: Feat 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); // Animation values const posterOpacity = useSharedValue(0); @@ -74,15 +77,37 @@ const FeaturedContent = ({ featuredContent, isSaved, handleSaveToLibrary }: Feat if (imageCache[url]) return true; try { + // For Metahub logos, only do validation if enabled + // Note: Temporarily disable metahub validation until fixed + if (false && url.includes('metahub.space')) { + try { + const isValid = await isValidMetahubLogo(url); + if (!isValid) { + console.warn(`[FeaturedContent] Metahub logo validation failed: ${url}`); + return false; + } + } catch (validationError) { + // If validation fails, still try to load the image + console.warn(`[FeaturedContent] Logo validation error, will try to load anyway: ${url}`, validationError); + } + } + + // Always attempt to prefetch the image regardless of format validation await ExpoImage.prefetch(url); imageCache[url] = true; + console.log(`[FeaturedContent] Successfully preloaded image: ${url}`); return true; } catch (error) { - console.error('Error preloading image:', error); + console.error('[FeaturedContent] Error preloading image:', error); return false; } }; + // Reset logo error state when content changes + useEffect(() => { + setLogoLoadError(false); + }, [featuredContent?.id]); + // Load poster and logo useEffect(() => { if (!featuredContent) return; @@ -124,6 +149,10 @@ const FeaturedContent = ({ featuredContent, isSaved, handleSaveToLibrary }: Feat duration: 500, easing: Easing.bezier(0.25, 0.1, 0.25, 1) })); + } 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}`); } } }; @@ -165,7 +194,7 @@ const FeaturedContent = ({ featuredContent, isSaved, handleSaveToLibrary }: Feat - {featuredContent.logo ? ( + {featuredContent.logo && !logoLoadError ? ( { + console.warn(`[FeaturedContent] Logo failed to load: ${featuredContent.logo}`); + setLogoLoadError(true); + }} /> ) : ( diff --git a/src/screens/MetadataScreen.tsx b/src/screens/MetadataScreen.tsx index 75fa51a3..246fdfe7 100644 --- a/src/screens/MetadataScreen.tsx +++ b/src/screens/MetadataScreen.tsx @@ -56,6 +56,7 @@ import { TMDBService } from '../services/tmdbService'; import { storageService } from '../services/storageService'; import { logger } from '../utils/logger'; import { useGenres } from '../contexts/GenreContext'; +import { isValidMetahubLogo, isMetahubUrl, isTmdbUrl } from '../utils/logoUtils'; const { width, height } = Dimensions.get('window'); @@ -264,6 +265,14 @@ const MetadataScreen = () => { episodeId?: string; } | null>(null); + // Add state to track image load errors + const [logoLoadError, setLogoLoadError] = useState(false); + + // Reset logo load error when metadata changes + useEffect(() => { + setLogoLoadError(false); + }, [metadata?.logo]); + // Add wrapper for toggleLibrary that includes haptic feedback const handleToggleLibrary = useCallback(() => { // Trigger appropriate haptic feedback based on action @@ -324,7 +333,7 @@ const MetadataScreen = () => { logger.log(`[MetadataScreen] Attempting to fetch logo from Metahub for ${imdbId}`); - // Test if Metahub logo exists with a HEAD request + // For now, skip detailed validation and just check if URL is accessible try { const response = await fetch(metahubUrl, { method: 'HEAD' }); if (response.ok) { @@ -340,6 +349,8 @@ const MetadataScreen = () => { logo: metahubUrl })); return; // Exit if Metahub logo was found + } else { + logger.warn(`[MetadataScreen] Metahub logo request failed with status ${response.status}`); } } catch (metahubError) { logger.warn(`[MetadataScreen] Failed to fetch logo from Metahub:`, metahubError); @@ -367,8 +378,16 @@ const MetadataScreen = () => { logo: logoUrl })); } else { - logger.warn(`[MetadataScreen] No logo found from either Metahub or TMDB for ${type} (ID: ${id})`); + // If both Metahub and TMDB fail, use the title as text instead of a logo + logger.warn(`[MetadataScreen] No logo found from either Metahub or TMDB for ${type} (ID: ${id}), using title text instead`); + + // Leave logo as null/undefined to trigger fallback to text } + } else { + // If no TMDB ID and Metahub failed, use the title as text instead of a logo + logger.warn(`[MetadataScreen] No logo found for ${type} (ID: ${id}), using title text instead`); + + // Leave logo as null/undefined to trigger fallback to text } } catch (error) { logger.error('[MetadataScreen] Failed to fetch logo from all sources:', { @@ -385,6 +404,7 @@ const MetadataScreen = () => { - Content ID: ${id} - Content Type: ${type} - Logo URL: ${metadata.logo} + - Source: ${isMetahubUrl(metadata.logo) ? 'Metahub' : (isTmdbUrl(metadata.logo) ? 'TMDB' : 'Other')} `); } }, [id, type, metadata, setMetadata, imdbId]); @@ -1077,12 +1097,16 @@ const MetadataScreen = () => { - {metadata.logo ? ( + {metadata.logo && !logoLoadError ? ( { + logger.warn(`[MetadataScreen] Logo failed to load: ${metadata.logo}`); + setLogoLoadError(true); + }} /> ) : ( {metadata.name} @@ -1120,12 +1144,16 @@ const MetadataScreen = () => { - {metadata.logo ? ( + {metadata.logo && !logoLoadError ? ( { + logger.warn(`[MetadataScreen] Logo failed to load: ${metadata.logo}`); + setLogoLoadError(true); + }} /> ) : ( {metadata.name} @@ -1181,12 +1209,16 @@ const MetadataScreen = () => { {/* Title */} - {metadata.logo ? ( + {metadata.logo && !logoLoadError ? ( { + logger.warn(`[MetadataScreen] Logo failed to load: ${metadata.logo}`); + setLogoLoadError(true); + }} /> ) : ( {metadata.name} diff --git a/src/screens/SearchScreen.tsx b/src/screens/SearchScreen.tsx index 63459f80..4e7591c3 100644 --- a/src/screens/SearchScreen.tsx +++ b/src/screens/SearchScreen.tsx @@ -1,4 +1,4 @@ -import React, { useState, useEffect, useCallback, useMemo } from 'react'; +import React, { useState, useEffect, useCallback, useMemo, useRef } from 'react'; import { View, Text, @@ -14,6 +14,9 @@ import { Dimensions, ScrollView, Animated as RNAnimated, + Pressable, + Platform, + Easing, } from 'react-native'; import { useNavigation } from '@react-navigation/native'; import { NavigationProp } from '@react-navigation/native'; @@ -23,9 +26,22 @@ import { catalogService, StreamingContent } from '../services/catalogService'; import { Image } from 'expo-image'; import debounce from 'lodash/debounce'; import AsyncStorage from '@react-native-async-storage/async-storage'; -import Animated, { FadeIn, FadeOut, SlideInRight } from 'react-native-reanimated'; +import Animated, { + FadeIn, + FadeOut, + SlideInRight, + useAnimatedStyle, + useSharedValue, + withTiming, + interpolate, + withSpring, + withDelay, + ZoomIn +} from 'react-native-reanimated'; import { RootStackParamList } from '../navigation/AppNavigator'; import { logger } from '../utils/logger'; +import { BlurView } from 'expo-blur'; +import { useSafeAreaInsets } from 'react-native-safe-area-context'; const { width } = Dimensions.get('window'); const HORIZONTAL_ITEM_WIDTH = width * 0.3; @@ -37,6 +53,8 @@ const MAX_RECENT_SEARCHES = 10; const PLACEHOLDER_POSTER = 'https://placehold.co/300x450/222222/CCCCCC?text=No+Poster'; +const AnimatedTouchable = Animated.createAnimatedComponent(TouchableOpacity); + const SkeletonLoader = () => { const pulseAnim = React.useRef(new RNAnimated.Value(0)).current; @@ -91,6 +109,72 @@ const SkeletonLoader = () => { ); }; +const ANDROID_STATUSBAR_HEIGHT = StatusBar.currentHeight || 0; + +// Create a simple, elegant animation component +const SimpleSearchAnimation = () => { + // Simple animation values that work reliably + const spinAnim = React.useRef(new RNAnimated.Value(0)).current; + const fadeAnim = React.useRef(new RNAnimated.Value(0)).current; + + React.useEffect(() => { + // Rotation animation + const spin = RNAnimated.loop( + RNAnimated.timing(spinAnim, { + toValue: 1, + duration: 1500, + easing: Easing.linear, + useNativeDriver: true, + }) + ); + + // Fade animation + const fade = RNAnimated.timing(fadeAnim, { + toValue: 1, + duration: 300, + useNativeDriver: true, + }); + + // Start animations + spin.start(); + fade.start(); + + // Clean up + return () => { + spin.stop(); + }; + }, [spinAnim, fadeAnim]); + + // Simple rotation interpolation + const spin = spinAnim.interpolate({ + inputRange: [0, 1], + outputRange: ['0deg', '360deg'], + }); + + return ( + + + + + + Searching + + + ); +}; + const SearchScreen = () => { const navigation = useNavigation>(); const isDarkMode = true; @@ -100,6 +184,30 @@ const SearchScreen = () => { const [searched, setSearched] = useState(false); const [recentSearches, setRecentSearches] = useState([]); const [showRecent, setShowRecent] = useState(true); + const inputRef = useRef(null); + const insets = useSafeAreaInsets(); + + // Animation values + const searchBarWidth = useSharedValue(width - 32); + const searchBarOpacity = useSharedValue(1); + const backButtonOpacity = useSharedValue(0); + + // Force consistent status bar settings + useEffect(() => { + const applyStatusBarConfig = () => { + StatusBar.setBarStyle('light-content'); + if (Platform.OS === 'android') { + StatusBar.setTranslucent(true); + StatusBar.setBackgroundColor('transparent'); + } + }; + + applyStatusBarConfig(); + + // Re-apply on focus + const unsubscribe = navigation.addListener('focus', applyStatusBarConfig); + return unsubscribe; + }, [navigation]); React.useLayoutEffect(() => { navigation.setOptions({ @@ -111,6 +219,55 @@ const SearchScreen = () => { loadRecentSearches(); }, []); + const animatedSearchBarStyle = useAnimatedStyle(() => { + return { + width: searchBarWidth.value, + opacity: searchBarOpacity.value, + }; + }); + + const animatedBackButtonStyle = useAnimatedStyle(() => { + return { + opacity: backButtonOpacity.value, + transform: [ + { + translateX: interpolate( + backButtonOpacity.value, + [0, 1], + [-20, 0] + ) + } + ] + }; + }); + + const handleSearchFocus = () => { + // Animate search bar when focused + searchBarWidth.value = withTiming(width - 80); + backButtonOpacity.value = withTiming(1); + }; + + const handleSearchBlur = () => { + if (!query) { + // Only animate back if query is empty + searchBarWidth.value = withTiming(width - 32); + backButtonOpacity.value = withTiming(0); + } + }; + + const handleBackPress = () => { + Keyboard.dismiss(); + if (query) { + setQuery(''); + setResults([]); + setSearched(false); + setShowRecent(true); + loadRecentSearches(); + } else { + navigation.goBack(); + } + }; + const loadRecentSearches = async () => { try { const savedSearches = await AsyncStorage.getItem(RECENT_SEARCHES_KEY); @@ -147,7 +304,9 @@ const SearchScreen = () => { try { const searchResults = await catalogService.searchContentCinemeta(searchQuery); setResults(searchResults); - await saveRecentSearch(searchQuery); + if (searchResults.length > 0) { + await saveRecentSearch(searchQuery); + } } catch (error) { logger.error('Search failed:', error); setResults([]); @@ -178,50 +337,66 @@ const SearchScreen = () => { setSearched(false); setShowRecent(true); loadRecentSearches(); + inputRef.current?.focus(); }; const renderRecentSearches = () => { if (!showRecent || recentSearches.length === 0) return null; return ( - - + + Recent Searches {recentSearches.map((search, index) => ( - { setQuery(search); Keyboard.dismiss(); }} + entering={FadeIn.duration(300).delay(index * 50)} > - + {search} - + { + const newRecentSearches = [...recentSearches]; + newRecentSearches.splice(index, 1); + setRecentSearches(newRecentSearches); + AsyncStorage.setItem(RECENT_SEARCHES_KEY, JSON.stringify(newRecentSearches)); + }} + hitSlop={{ top: 10, bottom: 10, left: 10, right: 10 }} + style={styles.recentSearchDeleteButton} + > + + + ))} - + ); }; - const renderHorizontalItem = ({ item }: { item: StreamingContent }) => { + const renderHorizontalItem = ({ item, index }: { item: StreamingContent, index: number }) => { return ( - { navigation.navigate('Metadata', { id: item.id, type: item.type }); }} + entering={FadeIn.duration(500).delay(index * 100)} + activeOpacity={0.7} > { contentFit="cover" transition={300} /> + + {item.type === 'movie' ? 'MOVIE' : 'SERIES'} + + {item.imdbRating && ( + + + {item.imdbRating} + + )} {item.name} - + {item.year && ( + {item.year} + )} + ); }; @@ -253,148 +440,204 @@ const SearchScreen = () => { return movieResults.length > 0 || seriesResults.length > 0; }, [movieResults, seriesResults]); + const headerBaseHeight = Platform.OS === 'android' ? 80 : 60; + const topSpacing = Platform.OS === 'android' ? (StatusBar.currentHeight || 0) : insets.top; + const headerHeight = headerBaseHeight + topSpacing + 60; + return ( - + - - Search - - - - {query.length > 0 && ( - + + + {/* Header Section with proper top spacing */} + + Search + + + + {query.length > 0 && ( + + + + )} + + + + {/* Content Container */} + + {searching ? ( + + ) : searched && !hasResultsToShow ? ( + - + + No results found + + + Try different keywords or check your spelling + + + ) : ( + + {!query.trim() && renderRecentSearches()} + + {movieResults.length > 0 && ( + + Movies ({movieResults.length}) + `movie-${item.id}`} + horizontal + showsHorizontalScrollIndicator={false} + contentContainerStyle={styles.horizontalListContent} + /> + + )} + + {seriesResults.length > 0 && ( + + TV Shows ({seriesResults.length}) + `series-${item.id}`} + horizontal + showsHorizontalScrollIndicator={false} + contentContainerStyle={styles.horizontalListContent} + /> + + )} + + )} - - {searching ? ( - - ) : searched && !hasResultsToShow ? ( - - - - No results found - - - Try different keywords or check your spelling - - - ) : ( - - {!query.trim() && renderRecentSearches()} - - {movieResults.length > 0 && ( - - Movies ({movieResults.length}) - `movie-${item.id}`} - horizontal - showsHorizontalScrollIndicator={false} - contentContainerStyle={styles.horizontalListContent} - /> - - )} - - {seriesResults.length > 0 && ( - - TV Shows ({seriesResults.length}) - `series-${item.id}`} - horizontal - showsHorizontalScrollIndicator={false} - contentContainerStyle={styles.horizontalListContent} - /> - - )} - - - )} - + ); }; const styles = StyleSheet.create({ container: { flex: 1, + backgroundColor: colors.black, + }, + headerBackground: { + position: 'absolute', + top: 0, + left: 0, + right: 0, + backgroundColor: colors.black, + zIndex: 1, + }, + contentContainer: { + flex: 1, + backgroundColor: colors.black, + paddingTop: 0, }, header: { - paddingHorizontal: 16, - paddingTop: 40, - paddingBottom: 12, - backgroundColor: colors.black, - gap: 16, + paddingHorizontal: 20, + justifyContent: 'flex-end', + paddingBottom: 8, + backgroundColor: 'transparent', + zIndex: 2, }, headerTitle: { fontSize: 32, fontWeight: '800', color: colors.white, letterSpacing: 0.5, + marginBottom: 12, + }, + searchBarContainer: { + flexDirection: 'row', + alignItems: 'center', + justifyContent: 'center', + marginBottom: 8, + }, + searchBarWrapper: { + flex: 1, }, searchBar: { flexDirection: 'row', alignItems: 'center', - borderRadius: 24, + borderRadius: 12, paddingHorizontal: 16, height: 48, + backgroundColor: colors.darkGray, + shadowColor: "#000", + shadowOffset: { + width: 0, + height: 2, + }, + shadowOpacity: 0.1, + shadowRadius: 3.84, + elevation: 5, + }, + backButton: { + marginRight: 10, + width: 40, + height: 40, + alignItems: 'center', + justifyContent: 'center', }, searchIcon: { marginRight: 12, @@ -403,6 +646,7 @@ const styles = StyleSheet.create({ flex: 1, fontSize: 16, height: '100%', + color: colors.white, }, clearButton: { padding: 4, @@ -412,6 +656,7 @@ const styles = StyleSheet.create({ }, scrollViewContent: { paddingBottom: 20, + paddingHorizontal: 0, }, carouselContainer: { marginBottom: 24, @@ -424,7 +669,7 @@ const styles = StyleSheet.create({ paddingHorizontal: 16, }, horizontalListContent: { - paddingHorizontal: 16, + paddingHorizontal: 12, paddingRight: 8, }, horizontalItem: { @@ -434,10 +679,12 @@ const styles = StyleSheet.create({ horizontalItemPosterContainer: { width: HORIZONTAL_ITEM_WIDTH, height: HORIZONTAL_POSTER_HEIGHT, - borderRadius: 8, + borderRadius: 12, overflow: 'hidden', backgroundColor: colors.darkBackground, marginBottom: 8, + borderWidth: 1, + borderColor: 'rgba(255,255,255,0.05)', }, horizontalItemPoster: { width: '100%', @@ -445,19 +692,30 @@ const styles = StyleSheet.create({ }, horizontalItemTitle: { fontSize: 14, - fontWeight: '500', + fontWeight: '600', lineHeight: 18, textAlign: 'left', + color: colors.white, + }, + yearText: { + fontSize: 12, + color: colors.mediumGray, + marginTop: 2, }, recentSearchesContainer: { - paddingHorizontal: 0, + paddingHorizontal: 16, paddingBottom: 16, + paddingTop: 8, + borderBottomWidth: 1, + borderBottomColor: 'rgba(255,255,255,0.05)', + marginBottom: 8, }, recentSearchItem: { flexDirection: 'row', alignItems: 'center', paddingVertical: 10, paddingHorizontal: 16, + marginVertical: 1, }, recentSearchIcon: { marginRight: 12, @@ -465,6 +723,10 @@ const styles = StyleSheet.create({ recentSearchText: { fontSize: 16, flex: 1, + color: colors.white, + }, + recentSearchDeleteButton: { + padding: 4, }, loadingContainer: { flex: 1, @@ -474,6 +736,7 @@ const styles = StyleSheet.create({ loadingText: { marginTop: 16, fontSize: 16, + color: colors.white, }, emptyContainer: { flex: 1, @@ -486,14 +749,20 @@ const styles = StyleSheet.create({ fontWeight: 'bold', marginTop: 16, marginBottom: 8, + color: colors.white, }, emptySubtext: { fontSize: 14, textAlign: 'center', lineHeight: 20, + color: colors.lightGray, }, skeletonContainer: { - padding: 16, + flexDirection: 'row', + flexWrap: 'wrap', + paddingHorizontal: 12, + paddingTop: 16, + justifyContent: 'space-between', }, skeletonVerticalItem: { flexDirection: 'row', @@ -535,6 +804,67 @@ const styles = StyleSheet.create({ marginBottom: 16, borderRadius: 4, }, + itemTypeContainer: { + position: 'absolute', + top: 8, + left: 8, + backgroundColor: 'rgba(0,0,0,0.7)', + paddingHorizontal: 6, + paddingVertical: 2, + borderRadius: 4, + }, + itemTypeText: { + color: colors.white, + fontSize: 8, + fontWeight: '700', + }, + ratingContainer: { + position: 'absolute', + bottom: 8, + right: 8, + backgroundColor: 'rgba(0,0,0,0.7)', + flexDirection: 'row', + alignItems: 'center', + paddingHorizontal: 6, + paddingVertical: 3, + borderRadius: 4, + }, + ratingText: { + color: colors.white, + fontSize: 10, + fontWeight: '700', + marginLeft: 2, + }, + simpleAnimationContainer: { + flex: 1, + justifyContent: 'center', + alignItems: 'center', + }, + simpleAnimationContent: { + alignItems: 'center', + }, + spinnerContainer: { + width: 64, + height: 64, + borderRadius: 32, + backgroundColor: colors.primary, + justifyContent: 'center', + alignItems: 'center', + marginBottom: 16, + shadowColor: "#000", + shadowOffset: { + width: 0, + height: 2, + }, + shadowOpacity: 0.2, + shadowRadius: 4, + elevation: 3, + }, + simpleAnimationText: { + color: colors.white, + fontSize: 16, + fontWeight: '600', + }, }); export default SearchScreen; \ No newline at end of file diff --git a/src/services/tmdbService.ts b/src/services/tmdbService.ts index a2161966..3a1d2dff 100644 --- a/src/services/tmdbService.ts +++ b/src/services/tmdbService.ts @@ -375,8 +375,16 @@ export class TMDBService { * Get image URL for TMDB images */ getImageUrl(path: string | null, size: 'original' | 'w500' | 'w300' | 'w185' | 'profile' = 'original'): string | null { - if (!path) return null; - return `https://image.tmdb.org/t/p/${size}${path}`; + if (!path) { + logger.warn(`[TMDBService] Cannot construct image URL from null path`); + return null; + } + + const baseImageUrl = 'https://image.tmdb.org/t/p/'; + const fullUrl = `${baseImageUrl}${size}${path}`; + logger.log(`[TMDBService] Constructed image URL: ${fullUrl}`); + + return fullUrl; } /** @@ -562,6 +570,8 @@ export class TMDBService { */ async getMovieImages(movieId: number | string): Promise { try { + logger.log(`[TMDBService] Fetching movie images for TMDB ID: ${movieId}`); + const response = await axios.get(`${BASE_URL}/movie/${movieId}/images`, { headers: await this.getHeaders(), params: await this.getParams({ @@ -570,6 +580,8 @@ export class TMDBService { }); const images = response.data; + logger.log(`[TMDBService] Retrieved ${images?.logos?.length || 0} logos for movie ID ${movieId}`); + if (images && images.logos && images.logos.length > 0) { // First prioritize English SVG logos const enSvgLogo = images.logos.find((logo: any) => @@ -578,6 +590,7 @@ export class TMDBService { logo.iso_639_1 === 'en' ); if (enSvgLogo) { + logger.log(`[TMDBService] Found English SVG logo for movie ID ${movieId}: ${enSvgLogo.file_path}`); return this.getImageUrl(enSvgLogo.file_path); } @@ -588,6 +601,7 @@ export class TMDBService { logo.iso_639_1 === 'en' ); if (enPngLogo) { + logger.log(`[TMDBService] Found English PNG logo for movie ID ${movieId}: ${enPngLogo.file_path}`); return this.getImageUrl(enPngLogo.file_path); } @@ -596,6 +610,7 @@ export class TMDBService { logo.iso_639_1 === 'en' ); if (enLogo) { + logger.log(`[TMDBService] Found English logo for movie ID ${movieId}: ${enLogo.file_path}`); return this.getImageUrl(enLogo.file_path); } @@ -604,6 +619,7 @@ export class TMDBService { logo.file_path && logo.file_path.endsWith('.svg') ); if (svgLogo) { + logger.log(`[TMDBService] Found SVG logo for movie ID ${movieId}: ${svgLogo.file_path}`); return this.getImageUrl(svgLogo.file_path); } @@ -612,17 +628,20 @@ export class TMDBService { logo.file_path && logo.file_path.endsWith('.png') ); if (pngLogo) { + logger.log(`[TMDBService] Found PNG logo for movie ID ${movieId}: ${pngLogo.file_path}`); return this.getImageUrl(pngLogo.file_path); } // Last resort: any logo + logger.log(`[TMDBService] Using first available logo for movie ID ${movieId}: ${images.logos[0].file_path}`); return this.getImageUrl(images.logos[0].file_path); } + logger.warn(`[TMDBService] No logos found for movie ID ${movieId}`); return null; // No logos found } catch (error) { // Log error but don't throw, just return null if fetching images fails - logger.error(`Failed to get movie images for ID ${movieId}:`, error); + logger.error(`[TMDBService] Failed to get movie images for ID ${movieId}:`, error); return null; } } @@ -632,6 +651,8 @@ export class TMDBService { */ async getTvShowImages(showId: number | string): Promise { try { + logger.log(`[TMDBService] Fetching TV show images for TMDB ID: ${showId}`); + const response = await axios.get(`${BASE_URL}/tv/${showId}/images`, { headers: await this.getHeaders(), params: await this.getParams({ @@ -640,6 +661,8 @@ export class TMDBService { }); const images = response.data; + logger.log(`[TMDBService] Retrieved ${images?.logos?.length || 0} logos for TV show ID ${showId}`); + if (images && images.logos && images.logos.length > 0) { // First prioritize English SVG logos const enSvgLogo = images.logos.find((logo: any) => @@ -648,6 +671,7 @@ export class TMDBService { logo.iso_639_1 === 'en' ); if (enSvgLogo) { + logger.log(`[TMDBService] Found English SVG logo for TV show ID ${showId}: ${enSvgLogo.file_path}`); return this.getImageUrl(enSvgLogo.file_path); } @@ -658,6 +682,7 @@ export class TMDBService { logo.iso_639_1 === 'en' ); if (enPngLogo) { + logger.log(`[TMDBService] Found English PNG logo for TV show ID ${showId}: ${enPngLogo.file_path}`); return this.getImageUrl(enPngLogo.file_path); } @@ -666,6 +691,7 @@ export class TMDBService { logo.iso_639_1 === 'en' ); if (enLogo) { + logger.log(`[TMDBService] Found English logo for TV show ID ${showId}: ${enLogo.file_path}`); return this.getImageUrl(enLogo.file_path); } @@ -674,6 +700,7 @@ export class TMDBService { logo.file_path && logo.file_path.endsWith('.svg') ); if (svgLogo) { + logger.log(`[TMDBService] Found SVG logo for TV show ID ${showId}: ${svgLogo.file_path}`); return this.getImageUrl(svgLogo.file_path); } @@ -682,17 +709,20 @@ export class TMDBService { logo.file_path && logo.file_path.endsWith('.png') ); if (pngLogo) { + logger.log(`[TMDBService] Found PNG logo for TV show ID ${showId}: ${pngLogo.file_path}`); return this.getImageUrl(pngLogo.file_path); } // Last resort: any logo + logger.log(`[TMDBService] Using first available logo for TV show ID ${showId}: ${images.logos[0].file_path}`); return this.getImageUrl(images.logos[0].file_path); } + logger.warn(`[TMDBService] No logos found for TV show ID ${showId}`); return null; // No logos found } catch (error) { // Log error but don't throw, just return null if fetching images fails - logger.error(`Failed to get TV show images for ID ${showId}:`, error); + logger.error(`[TMDBService] Failed to get TV show images for ID ${showId}:`, error); return null; } } @@ -702,11 +732,21 @@ export class TMDBService { */ async getContentLogo(type: 'movie' | 'tv', id: number | string): Promise { try { - return type === 'movie' + logger.log(`[TMDBService] Getting content logo for ${type} with ID ${id}`); + + const result = type === 'movie' ? await this.getMovieImages(id) : await this.getTvShowImages(id); + + if (result) { + logger.log(`[TMDBService] Successfully retrieved logo for ${type} ID ${id}: ${result}`); + } else { + logger.warn(`[TMDBService] No logo found for ${type} ID ${id}`); + } + + return result; } catch (error) { - logger.error(`Failed to get content logo for ${type} ID ${id}:`, error); + logger.error(`[TMDBService] Failed to get content logo for ${type} ID ${id}:`, error); return null; } } diff --git a/src/utils/logoUtils.ts b/src/utils/logoUtils.ts new file mode 100644 index 00000000..f82700be --- /dev/null +++ b/src/utils/logoUtils.ts @@ -0,0 +1,83 @@ +import { logger } from './logger'; + +/** + * Checks if a URL is a valid Metahub logo by performing a HEAD request + * @param url The Metahub logo URL to check + * @returns True if the logo is valid, false otherwise + */ +export const isValidMetahubLogo = async (url: string): Promise => { + if (!url || !url.includes('metahub.space')) { + return false; + } + + try { + const response = await fetch(url, { method: 'HEAD' }); + + // Check if request was successful + if (!response.ok) { + logger.warn(`[logoUtils] Logo URL returned status ${response.status}: ${url}`); + return false; + } + + // Check file size to detect "Missing Image" placeholders + const contentLength = response.headers.get('content-length'); + const fileSize = contentLength ? parseInt(contentLength, 10) : 0; + + // If content-length header is missing, we can't check file size, so assume it's valid + if (!contentLength) { + logger.warn(`[logoUtils] No content-length header for URL: ${url}`); + return true; // Give it the benefit of the doubt + } + + // If file size is suspiciously small, it might be a "Missing Image" placeholder + // Check for extremely small files (less than 100 bytes) which are definitely placeholders + if (fileSize < 100) { + logger.warn(`[logoUtils] Logo URL returned extremely small file (${fileSize} bytes), likely a placeholder: ${url}`); + return false; + } + + // For file sizes between 100-500 bytes, they might be small legitimate SVG files + // So we'll allow them through + return true; + } catch (error) { + logger.error(`[logoUtils] Error checking logo URL: ${url}`, error); + // Don't fail hard on network errors, let the image component try to load it + return true; + } +}; + +/** + * Utility to determine if a URL is likely to be a valid logo + * @param url The logo URL to check + * @returns True if the URL pattern suggests a valid logo + */ +export const hasValidLogoFormat = (url: string | null): boolean => { + if (!url) return false; + + // Only reject explicit placeholders, otherwise be permissive + if (url.includes('missing') || url.includes('placeholder.') || url.includes('not-found')) { + return false; + } + + return true; // Allow most URLs to pass through +}; + +/** + * Checks if a URL is from Metahub + * @param url The URL to check + * @returns True if the URL is from Metahub + */ +export const isMetahubUrl = (url: string | null): boolean => { + if (!url) return false; + return url.includes('metahub.space'); +}; + +/** + * Checks if a URL is from TMDB + * @param url The URL to check + * @returns True if the URL is from TMDB + */ +export const isTmdbUrl = (url: string | null): boolean => { + if (!url) return false; + return url.includes('themoviedb.org') || url.includes('tmdb.org') || url.includes('image.tmdb.org'); +}; \ No newline at end of file