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.
This commit is contained in:
tapframe 2025-05-03 19:10:27 +05:30
parent e1eb88c9ba
commit ba834ed3a8
3 changed files with 696 additions and 239 deletions

View file

@ -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<NavigationProp<RootStackParamList>>();
const { settings } = useSettings();
const [logoUrl, setLogoUrl] = useState<string | null>(null);
const [bannerUrl, setBannerUrl] = useState<string | null>(null);
const prevContentIdRef = useRef<string | null>(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<boolean>(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 <SkeletonFeatured />;
@ -194,16 +357,16 @@ const FeaturedContent = ({ featuredContent, isSaved, handleSaveToLibrary }: Feat
<Animated.View
style={[styles.featuredContentContainer as ViewStyle, contentAnimatedStyle]}
>
{featuredContent.logo && !logoLoadError ? (
{logoUrl && !logoLoadError ? (
<Animated.View style={logoAnimatedStyle}>
<ExpoImage
source={{ uri: logoUrl || featuredContent.logo }}
source={{ uri: logoUrl }}
style={styles.featuredLogo as ImageStyle}
contentFit="contain"
cachePolicy="memory-disk"
transition={400}
onError={() => {
console.warn(`[FeaturedContent] Logo failed to load: ${featuredContent.logo}`);
console.warn(`[FeaturedContent] Logo failed to load: ${logoUrl}`);
setLogoLoadError(true);
}}
/>

View file

@ -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<NavigationProp<any>>();
@ -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<string | null>(null);
const [metahubLogo, setMetahubLogo] = useState<string | null>(null);
const [tmdbBanner, setTmdbBanner] = useState<string | null>(null);
const [metahubBanner, setMetahubBanner] = useState<string | null>(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 (
<View style={[styles.exampleImage, styles.loadingContainer]}>
@ -183,11 +271,26 @@ const LogoSourceSettings = () => {
}
return (
<Image
source={{ uri: url || undefined }}
style={styles.exampleImage}
resizeMode="contain"
/>
<View style={styles.bannerContainer}>
<Image
source={{ uri: banner || undefined }}
style={styles.bannerImage}
resizeMode="cover"
/>
<View style={styles.bannerOverlay} />
{logo && (
<Image
source={{ uri: logo }}
style={styles.logoOverBanner}
resizeMode="contain"
/>
)}
{!logo && (
<View style={styles.noLogoContainer}>
<Text style={styles.noLogoText}>No logo available</Text>
</View>
)}
</View>
);
};
@ -217,6 +320,36 @@ const LogoSourceSettings = () => {
</Text>
</View>
{/* Show selector */}
<View style={styles.showSelectorContainer}>
<Text style={styles.selectorLabel}>Select a show/movie to preview:</Text>
<ScrollView
horizontal
showsHorizontalScrollIndicator={false}
contentContainerStyle={styles.showsScrollContent}
>
{EXAMPLE_SHOWS.map((show) => (
<TouchableOpacity
key={show.imdbId}
style={[
styles.showItem,
selectedShow.imdbId === show.imdbId && styles.selectedShowItem
]}
onPress={() => handleShowSelect(show)}
>
<Text
style={[
styles.showItemText,
selectedShow.imdbId === show.imdbId && styles.selectedShowItemText
]}
>
{show.name}
</Text>
</TouchableOpacity>
))}
</ScrollView>
</View>
{/* Options */}
<View style={styles.optionsContainer}>
<TouchableOpacity
@ -240,8 +373,8 @@ const LogoSourceSettings = () => {
<View style={styles.exampleContainer}>
<Text style={styles.exampleLabel}>Example:</Text>
{renderLogoExample(metahubLogo, loadingLogos)}
<Text style={styles.logoSourceLabel}>Breaking Bad logo from Metahub</Text>
{renderLogoExample(metahubLogo, metahubBanner, loadingLogos)}
<Text style={styles.logoSourceLabel}>{selectedShow.name} logo from Metahub</Text>
</View>
</TouchableOpacity>
@ -266,8 +399,8 @@ const LogoSourceSettings = () => {
<View style={styles.exampleContainer}>
<Text style={styles.exampleLabel}>Example:</Text>
{renderLogoExample(tmdbLogo, loadingLogos)}
<Text style={styles.logoSourceLabel}>Breaking Bad logo from TMDB</Text>
{renderLogoExample(tmdbLogo, tmdbBanner, loadingLogos)}
<Text style={styles.logoSourceLabel}>{selectedShow.name} logo from TMDB</Text>
</View>
</TouchableOpacity>
</View>
@ -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;

View file

@ -222,6 +222,7 @@ const MetadataScreen = () => {
// Add state for custom banner
const [bannerImage, setBannerImage] = useState<string | null>(null);
const forcedBannerRefreshDone = useRef<boolean>(false);
const [loadingBanner, setLoadingBanner] = useState<boolean>(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 = () => {
<Animated.View style={heroAnimatedStyle}>
<View style={styles.heroSection}>
{/* Use Animated.Image directly instead of ImageBackground with imageStyle */}
<Animated.Image
source={{ uri: bannerImage || metadata.banner || metadata.poster }}
style={[styles.absoluteFill, parallaxImageStyle]}
resizeMode="cover"
onError={() => {
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 ? (
<View style={[styles.absoluteFill, { backgroundColor: colors.black }]} />
) : (
<Animated.Image
source={{ uri: bannerImage || metadata.banner || metadata.poster }}
style={[styles.absoluteFill, parallaxImageStyle]}
resizeMode="cover"
onError={() => {
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);
}
}}
/>
)}
<LinearGradient
colors={[
`${colors.darkBackground}00`,