Add LogoSourceSettings screen and enhance logo fetching in MetadataScreen

Introduce a new LogoSourceSettings screen for users to select their logo source preference. Update the MetadataScreen to fetch banner images based on the selected logo source, improving the logic for handling logo refreshes and error states. Enhance the logoUtils with a new function to fetch banners according to user preferences, ensuring a more robust and user-friendly experience.
This commit is contained in:
tapframe 2025-05-03 18:15:54 +05:30
parent e686caebb8
commit eb90192752
5 changed files with 491 additions and 103 deletions

View file

@ -35,6 +35,7 @@ import HomeScreenSettings from '../screens/HomeScreenSettings';
import HeroCatalogsScreen from '../screens/HeroCatalogsScreen';
import TraktSettingsScreen from '../screens/TraktSettingsScreen';
import PlayerSettingsScreen from '../screens/PlayerSettingsScreen';
import LogoSourceSettings from '../screens/LogoSourceSettings';
// Stack navigator types
export type RootStackParamList = {
@ -90,6 +91,7 @@ export type RootStackParamList = {
HeroCatalogs: undefined;
TraktSettings: undefined;
PlayerSettings: undefined;
LogoSourceSettings: undefined;
};
export type RootStackNavigationProp = NativeStackNavigationProp<RootStackParamList>;
@ -823,6 +825,21 @@ const AppNavigator = () => {
},
}}
/>
<Stack.Screen
name="LogoSourceSettings"
component={LogoSourceSettings}
options={{
animation: 'fade',
animationDuration: 200,
presentation: 'card',
gestureEnabled: true,
gestureDirection: 'horizontal',
headerShown: false,
contentStyle: {
backgroundColor: colors.darkBackground,
},
}}
/>
</Stack.Navigator>
</PaperProvider>
</SafeAreaProvider>

View file

@ -0,0 +1,256 @@
import React, { useState } from 'react';
import {
View,
Text,
StyleSheet,
TouchableOpacity,
ScrollView,
Switch,
SafeAreaView,
Image,
Alert,
StatusBar,
Platform
} from 'react-native';
import { NavigationProp, useNavigation } from '@react-navigation/native';
import { MaterialIcons } from '@expo/vector-icons';
import AsyncStorage from '@react-native-async-storage/async-storage';
import { colors } from '../styles/colors';
import { useSettings } from '../hooks/useSettings';
import { useSafeAreaInsets } from 'react-native-safe-area-context';
const LogoSourceSettings = () => {
const { settings, updateSetting } = useSettings();
const navigation = useNavigation<NavigationProp<any>>();
const insets = useSafeAreaInsets();
// Get current preference
const [logoSource, setLogoSource] = useState<'metahub' | 'tmdb'>(
settings.logoSourcePreference || 'metahub'
);
// Apply setting and show confirmation
const applyLogoSourceSetting = (source: 'metahub' | 'tmdb') => {
setLogoSource(source);
updateSetting('logoSourcePreference', source);
// Clear any cached logo data in storage
try {
AsyncStorage.removeItem('_last_logos_');
} catch (e) {
console.error('Error clearing logo cache:', e);
}
// Show confirmation alert
Alert.alert(
'Settings Updated',
`Logo and background source preference set to ${source === 'metahub' ? 'Metahub' : 'TMDB'}. Changes will apply when you navigate to content.`,
[{ text: 'OK' }]
);
};
// Handle back navigation
const handleBack = () => {
navigation.goBack();
};
return (
<SafeAreaView style={[styles.container]}>
<StatusBar barStyle="light-content" />
{/* Header */}
<View style={[styles.header, { paddingTop: Platform.OS === 'android' ? insets.top : 0 }]}>
<TouchableOpacity
onPress={handleBack}
style={styles.backButton}
hitSlop={{ top: 10, bottom: 10, left: 10, right: 10 }}
>
<MaterialIcons name="arrow-back" size={24} color={colors.white} />
</TouchableOpacity>
<Text style={styles.headerTitle}>Logo Source</Text>
<View style={styles.headerRight} />
</View>
<ScrollView style={styles.scrollView}>
{/* Description */}
<View style={styles.descriptionContainer}>
<Text style={styles.description}>
Choose the primary source for content logos and background images. This affects the appearance
of titles in the metadata screen.
</Text>
</View>
{/* Options */}
<View style={styles.optionsContainer}>
<TouchableOpacity
style={[
styles.optionCard,
logoSource === 'metahub' && styles.selectedCard
]}
onPress={() => applyLogoSourceSetting('metahub')}
>
<View style={styles.optionHeader}>
<Text style={styles.optionTitle}>Metahub</Text>
{logoSource === 'metahub' && (
<MaterialIcons name="check-circle" size={24} color={colors.primary} />
)}
</View>
<Text style={styles.optionDescription}>
Prioritizes high-quality title logos from the Metahub image repository.
Offers good coverage for popular titles.
</Text>
<View style={styles.exampleContainer}>
<Text style={styles.exampleLabel}>Example:</Text>
<Image
source={{ uri: 'https://images.metahub.space/logo/medium/tt1475582/img' }}
style={styles.exampleImage}
resizeMode="contain"
/>
</View>
</TouchableOpacity>
<TouchableOpacity
style={[
styles.optionCard,
logoSource === 'tmdb' && styles.selectedCard
]}
onPress={() => applyLogoSourceSetting('tmdb')}
>
<View style={styles.optionHeader}>
<Text style={styles.optionTitle}>TMDB</Text>
{logoSource === 'tmdb' && (
<MaterialIcons name="check-circle" size={24} color={colors.primary} />
)}
</View>
<Text style={styles.optionDescription}>
Uses logos from The Movie Database. Often includes more localized and newer logos,
with better coverage for recent content.
</Text>
<View style={styles.exampleContainer}>
<Text style={styles.exampleLabel}>Example:</Text>
<Image
source={{ uri: 'https://image.tmdb.org/t/p/original/wwemzKWzjKYJFfCeiB57q3r4Bcm.svg' }}
style={styles.exampleImage}
resizeMode="contain"
/>
</View>
</TouchableOpacity>
</View>
{/* Additional Info */}
<View style={styles.infoBox}>
<Text style={styles.infoText}>
If a logo is not available from your preferred source, the app will automatically fall back to the other source.
If no logo is found, the title text will be shown instead.
</Text>
</View>
</ScrollView>
</SafeAreaView>
);
};
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: colors.darkBackground,
},
header: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-between',
paddingHorizontal: 16,
height: 56,
backgroundColor: colors.elevation2,
},
backButton: {
width: 40,
height: 40,
alignItems: 'center',
justifyContent: 'center',
},
headerTitle: {
color: colors.white,
fontSize: 20,
fontWeight: '600',
},
headerRight: {
width: 40,
},
scrollView: {
flex: 1,
},
descriptionContainer: {
padding: 16,
borderBottomWidth: 1,
borderBottomColor: 'rgba(255,255,255,0.1)',
},
description: {
color: colors.text,
fontSize: 16,
lineHeight: 24,
},
optionsContainer: {
padding: 16,
gap: 16,
},
optionCard: {
backgroundColor: colors.elevation2,
borderRadius: 12,
padding: 16,
borderWidth: 2,
borderColor: 'transparent',
},
selectedCard: {
borderColor: colors.primary,
},
optionHeader: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
marginBottom: 8,
},
optionTitle: {
color: colors.white,
fontSize: 18,
fontWeight: '600',
},
optionDescription: {
color: colors.mediumEmphasis,
fontSize: 14,
lineHeight: 20,
marginBottom: 16,
},
exampleContainer: {
marginTop: 8,
},
exampleLabel: {
color: colors.mediumEmphasis,
fontSize: 14,
marginBottom: 8,
},
exampleImage: {
height: 60,
width: '100%',
backgroundColor: 'rgba(0,0,0,0.3)',
borderRadius: 8,
},
infoBox: {
margin: 16,
padding: 16,
backgroundColor: 'rgba(255,255,255,0.05)',
borderRadius: 8,
borderLeftWidth: 4,
borderLeftColor: colors.primary,
},
infoText: {
color: colors.mediumEmphasis,
fontSize: 14,
lineHeight: 20,
},
});
export default LogoSourceSettings;

View file

@ -56,7 +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';
import { isValidMetahubLogo, isMetahubUrl, isTmdbUrl, fetchBannerWithPreference } from '../utils/logoUtils';
import { useSettings } from '../hooks/useSettings';
const { width, height } = Dimensions.get('window');
@ -218,6 +218,10 @@ const MetadataScreen = () => {
// Add a flag to track if we need to do a forced initial logo refresh
const forcedLogoRefreshDone = useRef<boolean>(false);
// Add state for custom banner
const [bannerImage, setBannerImage] = useState<string | null>(null);
const forcedBannerRefreshDone = useRef<boolean>(false);
// Add debug log for settings when component mounts
useEffect(() => {
@ -268,6 +272,65 @@ const MetadataScreen = () => {
}
}, [metadata?.logo, settings.logoSourcePreference, setMetadata]);
// Store found TMDB ID for banner fetching
const [foundTmdbId, setFoundTmdbId] = useState<string | null>(null);
// 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 (id.startsWith('tmdb:')) {
tmdbId = id.split(':')[1];
}
// Use our stored TMDB ID if we have one
const effectiveTmdbId = foundTmdbId || tmdbId || (metadata as any).tmdbId;
logger.log(`[MetadataScreen] Fetching banner with preference: ${settings.logoSourcePreference}, TMDB ID: ${effectiveTmdbId}`);
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);
}
} catch (error) {
logger.error(`[MetadataScreen] Error fetching banner:`, error);
// Use existing banner as fallback
setBannerImage(metadata.banner || metadata.poster);
}
forcedBannerRefreshDone.current = true;
}
};
fetchBanner();
}, [metadata, id, type, imdbId, settings.logoSourcePreference, foundTmdbId]);
// Reset forced refresh when preference changes
useEffect(() => {
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
setBannerImage(null);
// This will trigger the banner fetch effect to run again
}
}, [settings.logoSourcePreference]);
// Get genres from context
const { genreMap, loadingGenres } = useGenres();
@ -328,11 +391,14 @@ const MetadataScreen = () => {
const currentLogoIsMetahub = isMetahubUrl(metadata.logo);
const currentLogoIsTmdb = isTmdbUrl(metadata.logo);
const preferenceIsMetahub = settings.logoSourcePreference === 'metahub';
const preferenceIsTmdb = settings.logoSourcePreference === 'tmdb';
// Only refresh if the current logo doesn't match the preference
if ((preferenceIsMetahub && !currentLogoIsMetahub) ||
(!preferenceIsMetahub && !currentLogoIsTmdb)) {
logger.log(`[MetadataScreen] Logo preference (${settings.logoSourcePreference}) doesn't match current logo source, refreshing`);
// Only refresh if the current logo source clearly doesn't match the preference
const needsRefresh = (preferenceIsMetahub && currentLogoIsTmdb) ||
(preferenceIsTmdb && currentLogoIsMetahub);
if (needsRefresh) {
logger.log(`[MetadataScreen] Logo preference (${settings.logoSourcePreference}) doesn't match current logo source, triggering one-time refresh`);
// Prevent endless refreshes
if (logoRefreshCounter.current < MAX_LOGO_REFRESHES) {
@ -345,7 +411,7 @@ const MetadataScreen = () => {
logger.warn(`[MetadataScreen] Maximum logo refreshes (${MAX_LOGO_REFRESHES}) reached, stopping to prevent loop`);
}
} else {
logger.log(`[MetadataScreen] Logo source already matches preference, no refresh needed`);
logger.log(`[MetadataScreen] Logo source already matches preference (${settings.logoSourcePreference}), no refresh needed`);
logoRefreshCounter.current = 0; // Reset for future changes
}
}
@ -370,10 +436,14 @@ const MetadataScreen = () => {
const currentLogoIsMetahub = isMetahubUrl(metadata.logo);
const currentLogoIsTmdb = isTmdbUrl(metadata.logo);
const preferenceIsMetahub = settings.logoSourcePreference === 'metahub';
const preferenceIsTmdb = settings.logoSourcePreference === 'tmdb';
if ((preferenceIsMetahub && currentLogoIsMetahub) ||
(!preferenceIsMetahub && currentLogoIsTmdb)) {
logger.log('[MetadataScreen] Logo source now matches preference, refresh complete');
// Check if current logo source matches preference
const logoSourceMatches = (preferenceIsMetahub && currentLogoIsMetahub) ||
(preferenceIsTmdb && currentLogoIsTmdb);
if (logoSourceMatches) {
logger.log(`[MetadataScreen] Logo source (${currentLogoIsMetahub ? 'Metahub' : 'TMDB'}) now matches preference (${settings.logoSourcePreference}), refresh complete`);
logoRefreshCounter.current = 0; // Reset counter since we've achieved our goal
}
}
@ -468,6 +538,9 @@ const MetadataScreen = () => {
...prevMetadata!,
logo: metahubUrl
}));
// Clear fetch in progress flag when done
logoFetchInProgress.current = false;
return; // Exit if Metahub logo was found
} else {
logger.warn(`[MetadataScreen] Metahub logo request failed with status ${response.status}`);
@ -486,7 +559,7 @@ const MetadataScreen = () => {
const logoUrl = await TMDBService.getInstance().getContentLogo(tmdbType, tmdbId);
if (logoUrl) {
logger.log(`[MetadataScreen] Successfully fetched fallback logo from TMDB:
logger.log(`[MetadataScreen] Successfully fetched logo from TMDB:
- Content Type: ${tmdbType}
- TMDB ID: ${tmdbId}
- Logo URL: ${logoUrl}
@ -497,6 +570,10 @@ const MetadataScreen = () => {
...prevMetadata!,
logo: logoUrl
}));
// Clear fetch in progress flag when done
logoFetchInProgress.current = false;
return; // Exit if TMDB logo was found
} else {
// 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`);
@ -524,6 +601,9 @@ const MetadataScreen = () => {
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}`);
}
@ -553,6 +633,9 @@ const MetadataScreen = () => {
...prevMetadata!,
logo: logoUrl
}));
// Clear fetch in progress flag when done
logoFetchInProgress.current = false;
return; // Exit if TMDB logo was found
} else {
logger.warn(`[MetadataScreen] No logo found from TMDB for ${type} (ID: ${tmdbId}), trying Metahub`);
@ -565,34 +648,36 @@ const MetadataScreen = () => {
}
// If TMDB fails or isn't a TMDB ID, try Metahub as fallback
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
}));
} 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`);
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
}));
} 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
}
} catch (metahubError) {
logger.warn(`[MetadataScreen] Failed to fetch logo from Metahub:`, metahubError);
// Leave logo as null/undefined to trigger fallback to text
}
} catch (metahubError) {
logger.warn(`[MetadataScreen] Failed to fetch logo from Metahub:`, metahubError);
// Leave logo as null/undefined to trigger fallback to text
}
}
} catch (error) {
@ -1400,9 +1485,16 @@ const MetadataScreen = () => {
<View style={styles.heroSection}>
{/* Use Animated.Image directly instead of ImageBackground with imageStyle */}
<Animated.Image
source={{ uri: metadata.banner || metadata.poster }}
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={[

View file

@ -352,75 +352,11 @@ const SettingsScreen: React.FC = () => {
/>
<SettingItem
title="Logo Source Preference"
description="Choose primary source for title logos"
description="Choose primary source for title logos and backgrounds"
icon="image"
isDarkMode={isDarkMode}
renderControl={() => (
<View style={styles.selectorContainer}>
<TouchableOpacity
style={[
styles.selectorButton,
settings.logoSourcePreference === 'metahub' && styles.selectorButtonActive
]}
onPress={() => {
console.log('Setting logo source preference to Metahub');
updateSetting('logoSourcePreference', 'metahub');
console.log('New logo source preference:', 'metahub');
// Clear any cached logo data in storage
try {
// This is just to help clear any cached state - the exact implementation may vary
AsyncStorage.removeItem('_last_logos_');
} catch (e) {
console.error('Error clearing logo cache:', e);
}
// Show alert that settings have been updated
Alert.alert(
'Settings Updated',
'Logo source preference set to Metahub. Changes will apply when you navigate to content.',
[{ text: 'OK' }]
);
}}
>
<Text style={[
styles.selectorText,
settings.logoSourcePreference === 'metahub' && styles.selectorTextActive
]}>Metahub</Text>
</TouchableOpacity>
<TouchableOpacity
style={[
styles.selectorButton,
settings.logoSourcePreference === 'tmdb' && styles.selectorButtonActive
]}
onPress={() => {
console.log('Setting logo source preference to TMDB');
updateSetting('logoSourcePreference', 'tmdb');
console.log('New logo source preference:', 'tmdb');
// Clear any cached logo data in storage
try {
// This is just to help clear any cached state - the exact implementation may vary
AsyncStorage.removeItem('_last_logos_');
} catch (e) {
console.error('Error clearing logo cache:', e);
}
// Show alert that settings have been updated
Alert.alert(
'Settings Updated',
'Logo source preference set to TMDB. Changes will apply when you navigate to content.',
[{ text: 'OK' }]
);
}}
>
<Text style={[
styles.selectorText,
settings.logoSourcePreference === 'tmdb' && styles.selectorTextActive
]}>TMDB</Text>
</TouchableOpacity>
</View>
)}
renderControl={ChevronRight}
onPress={() => navigation.navigate('LogoSourceSettings')}
/>
<SettingItem
title="TMDB"

View file

@ -1,4 +1,5 @@
import { logger } from './logger';
import { TMDBService } from '../services/tmdbService';
/**
* Checks if a URL is a valid Metahub logo by performing a HEAD request
@ -80,4 +81,90 @@ export const isMetahubUrl = (url: string | null): boolean => {
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');
};
/**
* Fetches a banner image based on logo source preference
* @param imdbId The IMDB ID of the content
* @param tmdbId The TMDB ID of the content (if available)
* @param type The content type ('movie' or 'series')
* @param preference The logo source preference ('metahub' or 'tmdb')
* @returns The URL of the banner image, or null if none found
*/
export const fetchBannerWithPreference = async (
imdbId: string | null,
tmdbId: number | string | null,
type: 'movie' | 'series',
preference: 'metahub' | 'tmdb'
): Promise<string | null> => {
logger.log(`[logoUtils] Fetching banner with preference ${preference} for ${type} (IMDB: ${imdbId}, TMDB: ${tmdbId})`);
// Determine which source to try first based on preference
if (preference === 'tmdb') {
// Try TMDB first if it's the preferred source
if (tmdbId) {
try {
const tmdbService = TMDBService.getInstance();
// Get backdrop from TMDB
const tmdbType = type === 'series' ? 'tv' : 'movie';
logger.log(`[logoUtils] Attempting to fetch banner from TMDB for ${tmdbType} (ID: ${tmdbId})`);
let bannerUrl = null;
if (tmdbType === 'movie') {
const movieDetails = await tmdbService.getMovieDetails(tmdbId.toString());
if (movieDetails && movieDetails.backdrop_path) {
bannerUrl = tmdbService.getImageUrl(movieDetails.backdrop_path, 'original');
logger.log(`[logoUtils] Found backdrop_path: ${movieDetails.backdrop_path}`);
} else {
logger.warn(`[logoUtils] No backdrop_path found in movie details for ID ${tmdbId}`);
}
} else {
const showDetails = await tmdbService.getTVShowDetails(Number(tmdbId));
if (showDetails && showDetails.backdrop_path) {
bannerUrl = tmdbService.getImageUrl(showDetails.backdrop_path, 'original');
logger.log(`[logoUtils] Found backdrop_path: ${showDetails.backdrop_path}`);
} else {
logger.warn(`[logoUtils] No backdrop_path found in TV show details for ID ${tmdbId}`);
}
}
if (bannerUrl) {
logger.log(`[logoUtils] Successfully fetched ${tmdbType} banner from TMDB: ${bannerUrl}`);
return bannerUrl;
}
} catch (error) {
logger.error(`[logoUtils] Error fetching banner from TMDB for ID ${tmdbId}:`, error);
}
logger.warn(`[logoUtils] No banner found from TMDB for ${type} (ID: ${tmdbId}), falling back to Metahub`);
} else {
logger.warn(`[logoUtils] Cannot fetch from TMDB - no TMDB ID provided, falling back to Metahub`);
}
}
// Try Metahub if it's preferred or TMDB failed
if (imdbId) {
const metahubUrl = `https://images.metahub.space/background/large/${imdbId}/img`;
logger.log(`[logoUtils] Attempting to fetch banner from Metahub for ${imdbId}`);
try {
const response = await fetch(metahubUrl, { method: 'HEAD' });
if (response.ok) {
logger.log(`[logoUtils] Successfully fetched banner from Metahub: ${metahubUrl}`);
return metahubUrl;
} else {
logger.warn(`[logoUtils] Metahub banner request failed with status ${response.status}`);
}
} catch (error) {
logger.warn(`[logoUtils] Failed to fetch banner from Metahub:`, error);
}
} else {
logger.warn(`[logoUtils] Cannot fetch from Metahub - no IMDB ID provided`);
}
// If both sources fail or aren't available, return null
logger.warn(`[logoUtils] No banner found from any source for ${type} (IMDB: ${imdbId}, TMDB: ${tmdbId})`);
return null;
};