diff --git a/src/components/home/FeaturedContent.tsx b/src/components/home/FeaturedContent.tsx index 75ef216..f6e16ab 100644 --- a/src/components/home/FeaturedContent.tsx +++ b/src/components/home/FeaturedContent.tsx @@ -253,7 +253,7 @@ const FeaturedContent = ({ featuredContent, isSaved, handleSaveToLibrary, loadin setLogoLoadError(false); }, [featuredContent?.id]); - // Fetch logo based on preference + // Fetch logo when enrichment is enabled; otherwise only use addon logo useEffect(() => { if (!featuredContent || logoFetchInProgress.current) return; @@ -267,8 +267,7 @@ const FeaturedContent = ({ featuredContent, isSaved, handleSaveToLibrary, loadin const contentData = featuredContent; // Use a clearer variable name const currentLogo = contentData.logo; - // Get preferences - const logoPreference = settings.logoSourcePreference || 'tmdb'; + // Get language preference (only relevant when enrichment is enabled) const preferredLanguage = settings.tmdbLanguagePreference || 'en'; // If enrichment is disabled, use addon logo and don't fetch from external sources @@ -281,7 +280,7 @@ const FeaturedContent = ({ featuredContent, isSaved, handleSaveToLibrary, loadin }); // If we have an addon logo, use it and don't fetch external logos - if (contentData.logo && !isTmdbUrl(contentData.logo)) { + if (contentData.logo) { logger.info('[FeaturedContent] enrichment disabled, using addon logo', { logo: contentData.logo }); setLogoUrl(contentData.logo); logoFetchInProgress.current = false; @@ -334,11 +333,11 @@ const FeaturedContent = ({ featuredContent, isSaved, handleSaveToLibrary, loadin let primaryAttempted = false; let fallbackAttempted = false; - // --- Logo Fetching Logic --- - logger.debug('[FeaturedContent] fetchLogo:ids', { imdbId, tmdbId, preference: logoPreference, lang: preferredLanguage }); + // --- Logo Fetching Logic (TMDB only when enrichment is enabled) --- + logger.debug('[FeaturedContent] fetchLogo:ids', { imdbId, tmdbId, lang: preferredLanguage }); - // Only try TMDB if preference is 'tmdb' and we have tmdbId - if (logoPreference === 'tmdb' && tmdbId) { + // Try TMDB if we have a TMDB id + if (tmdbId) { primaryAttempted = true; try { const tmdbService = TMDBService.getInstance(); @@ -354,11 +353,11 @@ const FeaturedContent = ({ featuredContent, isSaved, handleSaveToLibrary, loadin // --- Set Final Logo --- if (finalLogoUrl) { setLogoUrl(finalLogoUrl); - logger.info('[FeaturedContent] fetchLogo:done', { id: contentId, result: 'ok', duration: since(t0) }); + logger.info('[FeaturedContent] fetchLogo:done', { id: contentId, result: 'tmdb', url: finalLogoUrl, duration: since(t0) }); } else if (currentLogo) { // Use existing logo only if primary and fallback failed or weren't applicable setLogoUrl(currentLogo); - logger.info('[FeaturedContent] fetchLogo:done', { id: contentId, result: 'existing', duration: since(t0) }); + logger.info('[FeaturedContent] fetchLogo:done', { id: contentId, result: 'addon', url: currentLogo, duration: since(t0) }); } else { // No logo found from any source setLogoLoadError(true); @@ -377,7 +376,7 @@ const FeaturedContent = ({ featuredContent, isSaved, handleSaveToLibrary, loadin // Trigger fetch when content changes fetchLogo(); - }, [featuredContent, settings.logoSourcePreference, settings.tmdbLanguagePreference, settings.enrichMetadataWithTMDB]); + }, [featuredContent, settings.tmdbLanguagePreference, settings.enrichMetadataWithTMDB]); // Load poster and logo useEffect(() => { diff --git a/src/hooks/useCalendarData.ts b/src/hooks/useCalendarData.ts index f39edc8..a209146 100644 --- a/src/hooks/useCalendarData.ts +++ b/src/hooks/useCalendarData.ts @@ -50,7 +50,6 @@ export const useCalendarData = (): UseCalendarDataReturn => { } = useTraktContext(); const fetchCalendarData = useCallback(async (forceRefresh = false) => { - logger.log("[CalendarData] Starting to fetch calendar data"); setLoading(true); try { @@ -68,14 +67,12 @@ export const useCalendarData = (): UseCalendarDataReturn => { ); if (cachedData) { - logger.log(`[CalendarData] Using cached data with ${cachedData.length} sections`); setCalendarData(cachedData); setLoading(false); return; } } - logger.log("[CalendarData] Fetching fresh data from APIs"); const librarySeries = libraryItems.filter(item => item.type === 'series'); let allSeries: StreamingContent[] = [...librarySeries]; diff --git a/src/hooks/useFeaturedContent.ts b/src/hooks/useFeaturedContent.ts index d156d6b..dfbe6e5 100644 --- a/src/hooks/useFeaturedContent.ts +++ b/src/hooks/useFeaturedContent.ts @@ -131,9 +131,8 @@ export function useFeaturedContent() { }; }); - // Then fetch logos for each item based on preference + // Then fetch logos for each item (TMDB when enrichment enabled) const tLogos = Date.now(); - const preference = settings.logoSourcePreference || 'tmdb'; const preferredLanguage = settings.tmdbLanguagePreference || 'en'; const fetchLogoForItem = async (item: StreamingContent): Promise => { @@ -152,58 +151,21 @@ export function useFeaturedContent() { return item; } - if (preference === 'tmdb') { - logger.debug('[useFeaturedContent] logo:try:tmdb', { name: item.name, id: item.id, tmdbId, lang: preferredLanguage }); - // Resolve TMDB id if we only have IMDb - if (!tmdbId && imdbId) { - const found = await tmdbService.findTMDBIdByIMDB(imdbId); - tmdbId = found ? String(found) : null; - } - if (!tmdbId) return item; - const logoUrl = tmdbId ? await tmdbService.getContentLogo('movie', tmdbId as string, preferredLanguage) : null; - if (logoUrl) { - logger.debug('[useFeaturedContent] logo:tmdb:ok', { name: item.name, id: item.id, url: logoUrl, lang: preferredLanguage }); - return { ...item, logo: logoUrl }; - } - // Fallback to Metahub via IMDb ID - if (!imdbId && tmdbId) { - const movieDetails: any = await tmdbService.getMovieDetails(tmdbId); - imdbId = movieDetails?.imdb_id; - } - if (imdbId) { - const metahubUrl = `https://images.metahub.space/logo/medium/${imdbId}/img`; - logger.debug('[useFeaturedContent] logo:fallback:metahub', { name: item.name, id: item.id, url: metahubUrl }); - return { ...item, logo: metahubUrl }; - } - logger.debug('[useFeaturedContent] logo:none', { name: item.name, id: item.id }); - return item; - } else { - // preference === 'metahub' - // If have IMDb, use directly - if (!imdbId && tmdbId) { - const movieDetails: any = await tmdbService.getMovieDetails(tmdbId); - imdbId = movieDetails?.imdb_id; - } - if (imdbId) { - const metahubUrl = `https://images.metahub.space/logo/medium/${imdbId}/img`; - logger.debug('[useFeaturedContent] logo:metahub:ok', { name: item.name, id: item.id, url: metahubUrl }); - return { ...item, logo: metahubUrl }; - } - // Fallback to TMDB logo - logger.debug('[useFeaturedContent] logo:metahub:miss → fallback:tmdb', { name: item.name, id: item.id, lang: preferredLanguage }); - if (!tmdbId && imdbId) { - const found = await tmdbService.findTMDBIdByIMDB(imdbId); - tmdbId = found ? String(found) : null; - } - if (!tmdbId) return item; - const logoUrl = tmdbId ? await tmdbService.getContentLogo('movie', tmdbId as string, preferredLanguage) : null; - if (logoUrl) { - logger.debug('[useFeaturedContent] logo:tmdb:fallback:ok', { name: item.name, id: item.id, url: logoUrl, lang: preferredLanguage }); - return { ...item, logo: logoUrl }; - } - logger.debug('[useFeaturedContent] logo:none', { name: item.name, id: item.id }); - return item; + // Enrichment path: TMDB only + logger.debug('[useFeaturedContent] logo:try:tmdb', { name: item.name, id: item.id, tmdbId, lang: preferredLanguage }); + // Resolve TMDB id if we only have IMDb + if (!tmdbId && imdbId) { + const found = await tmdbService.findTMDBIdByIMDB(imdbId); + tmdbId = found ? String(found) : null; } + if (!tmdbId) return item; + const logoUrl = tmdbId ? await tmdbService.getContentLogo('movie', tmdbId as string, preferredLanguage) : null; + if (logoUrl) { + logger.debug('[useFeaturedContent] logo:tmdb:ok', { name: item.name, id: item.id, url: logoUrl, lang: preferredLanguage }); + return { ...item, logo: logoUrl }; + } + logger.debug('[useFeaturedContent] logo:none', { name: item.name, id: item.id }); + return item; } catch (error) { logger.error('[useFeaturedContent] logo:error', { name: item.name, id: item.id, error: String(error) }); return item; @@ -220,7 +182,17 @@ export function useFeaturedContent() { logo: item.logo && !isTmdbUrl(item.logo) ? item.logo : undefined })); } - logger.info('[useFeaturedContent] logos:resolved', { count: formattedContent.length, duration: `${Date.now() - tLogos}ms`, preference }); + logger.info('[useFeaturedContent] logos:resolved', { count: formattedContent.length, duration: `${Date.now() - tLogos}ms` }); + try { + const details = formattedContent.slice(0, 20).map((c) => ({ + id: c.id, + name: c.name, + hasLogo: Boolean(c.logo), + logoSource: c.logo ? (isTmdbUrl(String(c.logo)) ? 'tmdb' : 'addon') : 'none', + logo: c.logo || undefined, + })); + logger.debug('[useFeaturedContent] logos:details', { items: details }); + } catch {} } } else { // Load from installed catalogs @@ -256,8 +228,7 @@ export function useFeaturedContent() { // Sort by popular, newest, etc. (possibly enhanced later) and take first 10 const topItems = allItems.sort(() => Math.random() - 0.5).slice(0, 10); - // Optionally enrich with logos based on preference for tmdb-sourced IDs - const preference = settings.logoSourcePreference || 'tmdb'; + // Optionally enrich with logos (TMDB only) for tmdb/imdb sourced IDs const preferredLanguage = settings.tmdbLanguagePreference || 'en'; const enrichLogo = async (item: any): Promise => { @@ -298,8 +269,8 @@ export function useFeaturedContent() { tmdbId = found ? String(found) : null; } if (!tmdbId && !imdbId) return base; - // Only try TMDB if preference is 'tmdb' and we have tmdbId - if (preference === 'tmdb' && tmdbId) { + // Try TMDB if we have a TMDB id + if (tmdbId) { logger.debug('[useFeaturedContent] logo:try:tmdb', { name: item.name, id: item.id, tmdbId, lang: preferredLanguage }); const logoUrl = await tmdbService.getContentLogo(item.type === 'series' ? 'tv' : 'movie', tmdbId as string, preferredLanguage); if (logoUrl) { @@ -314,17 +285,29 @@ export function useFeaturedContent() { } }; - // When enrichment is disabled, only use addon logos and never fetch external logos - if (!settings.enrichMetadataWithTMDB) { - logger.debug('[useFeaturedContent] enrichment disabled, using only addon logos'); - formattedContent = topItems.map((item: any) => { + // Only enrich with logos if enrichment is enabled + if (settings.enrichMetadataWithTMDB) { + formattedContent = await Promise.all(topItems.map(enrichLogo)); + try { + const details = formattedContent.slice(0, 20).map((c) => ({ + id: c.id, + name: c.name, + hasLogo: Boolean(c.logo), + logoSource: c.logo ? (isTmdbUrl(String(c.logo)) ? 'tmdb' : 'addon') : 'none', + logo: c.logo || undefined, + })); + logger.debug('[useFeaturedContent] catalogs:logos:details', { items: details }); + } catch {} + } else { + // When enrichment is disabled, prefer addon-provided logos; if missing, fetch basic meta to pull logo (like HeroSection) + const baseItems = topItems.map((item: any) => { const base: StreamingContent = { id: item.id, type: item.type, name: item.name, poster: item.poster, banner: (item as any).banner, - logo: (item as any).logo && !isTmdbUrl((item as any).logo) ? (item as any).logo : undefined, + logo: (item as any).logo || undefined, description: (item as any).description, year: (item as any).year, genres: (item as any).genres, @@ -332,9 +315,49 @@ export function useFeaturedContent() { }; return base; }); - } else { - // Only enrich with logos if enrichment is enabled - formattedContent = await Promise.all(topItems.map(enrichLogo)); + + // Attempt to fill missing logos from addon meta details for a limited subset + const candidates = baseItems.filter(i => !i.logo).slice(0, 10); + logger.debug('[useFeaturedContent] catalogs:no-enrich:missing-logos', { count: candidates.length }); + + try { + const filled = await Promise.allSettled(candidates.map(async (item) => { + try { + const meta = await catalogService.getBasicContentDetails(item.type, item.id); + if (meta?.logo) { + logger.debug('[useFeaturedContent] catalogs:no-enrich:filled-logo', { id: item.id, name: item.name, logo: meta.logo }); + return { id: item.id, logo: meta.logo } as { id: string; logo: string }; + } + } catch (e) { + logger.warn('[useFeaturedContent] catalogs:no-enrich:fill-failed', { id: item.id, error: String(e) }); + } + return { id: item.id, logo: undefined as any }; + })); + + const idToLogo = new Map(); + filled.forEach(res => { + if (res.status === 'fulfilled' && res.value && res.value.logo) { + idToLogo.set(res.value.id, res.value.logo); + } + }); + + formattedContent = baseItems.map(i => ( + idToLogo.has(i.id) ? { ...i, logo: idToLogo.get(i.id)! } : i + )); + } catch { + formattedContent = baseItems; + } + + try { + const details = formattedContent.slice(0, 20).map((c) => ({ + id: c.id, + name: c.name, + hasLogo: Boolean(c.logo), + logoSource: c.logo ? (isTmdbUrl(String(c.logo)) ? 'tmdb' : 'addon') : 'none', + logo: c.logo || undefined, + })); + logger.debug('[useFeaturedContent] catalogs:logos:details (no-enrich)', { items: details }); + } catch {} } } } diff --git a/src/hooks/useLibrary.ts b/src/hooks/useLibrary.ts index e3df9bd..a929306 100644 --- a/src/hooks/useLibrary.ts +++ b/src/hooks/useLibrary.ts @@ -115,7 +115,6 @@ export const useLibrary = () => { // Subscribe to catalogService library updates useEffect(() => { const unsubscribe = catalogService.subscribeToLibraryUpdates((items) => { - if (__DEV__) console.log('[useLibrary] Received library update from catalogService:', items.length, 'items'); setLibraryItems(items); setLoading(false); }); diff --git a/src/navigation/AppNavigator.tsx b/src/navigation/AppNavigator.tsx index c71a8e3..6b0f235 100644 --- a/src/navigation/AppNavigator.tsx +++ b/src/navigation/AppNavigator.tsx @@ -40,7 +40,6 @@ 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'; import ThemeScreen from '../screens/ThemeScreen'; import OnboardingScreen from '../screens/OnboardingScreen'; import AuthScreen from '../screens/AuthScreen'; @@ -135,7 +134,6 @@ export type RootStackParamList = { HeroCatalogs: undefined; TraktSettings: undefined; PlayerSettings: undefined; - LogoSourceSettings: undefined; ThemeSettings: undefined; ScraperSettings: undefined; CastMovies: { @@ -1239,21 +1237,6 @@ const InnerNavigator = ({ initialRouteName }: { initialRouteName?: keyof RootSta }, }} /> - StyleSheet.create({ - container: { - flex: 1, - backgroundColor: colors.darkBackground, - }, - header: { - flexDirection: 'row', - alignItems: 'center', - justifyContent: 'space-between', - paddingHorizontal: 16, - paddingTop: Platform.OS === 'android' ? (StatusBar.currentHeight || 0) + 8 : 8, - backgroundColor: colors.darkBackground, - }, - backButton: { - flexDirection: 'row', - alignItems: 'center', - padding: 8, - }, - backText: { - fontSize: 17, - marginLeft: 8, - color: colors.white, - }, - headerActions: { - flexDirection: 'row', - alignItems: 'center', - }, - headerButton: { - padding: 8, - marginLeft: 8, - }, - headerTitle: { - fontSize: 34, - fontWeight: 'bold', - paddingHorizontal: 16, - marginBottom: 24, - color: colors.white, - }, - headerRight: { - width: 24, - }, - scrollView: { - flex: 1, - }, - scrollContent: { - paddingHorizontal: 16, - paddingBottom: 24, - }, - descriptionContainer: { - marginBottom: 16, - }, - description: { - color: colors.mediumEmphasis, - fontSize: 15, - lineHeight: 22, - }, - showSelectorContainer: { - marginBottom: 16, - }, - selectorLabel: { - color: colors.highEmphasis, - fontSize: 16, - fontWeight: '500', - marginBottom: 12, - }, - showsScrollContent: { - paddingRight: 16, - }, - showItem: { - paddingHorizontal: 12, - paddingVertical: 6, - backgroundColor: colors.elevation2, - borderRadius: 16, - marginRight: 6, - borderWidth: 1, - borderColor: 'transparent', - shadowColor: '#000', - shadowOffset: { width: 0, height: 1 }, - shadowOpacity: 0.1, - shadowRadius: 1, - elevation: 1, - }, - selectedShowItem: { - borderColor: colors.primary, - backgroundColor: colors.elevation3, - shadowColor: colors.primary, - shadowOffset: { width: 0, height: 1 }, - shadowOpacity: 0.2, - shadowRadius: 2, - elevation: 2, - }, - showItemText: { - color: colors.mediumEmphasis, - fontSize: 14, - }, - selectedShowItemText: { - color: colors.white, - fontWeight: '600', - }, - optionsContainer: { - marginBottom: 16, - gap: 12, - }, - optionCard: { - backgroundColor: colors.elevation2, - borderRadius: 8, - padding: 12, - borderWidth: 2, - borderColor: 'transparent', - marginBottom: 8, - shadowColor: '#000', - shadowOffset: { width: 0, height: 2 }, - shadowOpacity: 0.2, - shadowRadius: 3, - elevation: 2, - }, - selectedCard: { - borderColor: colors.primary, - shadowColor: colors.primary, - shadowOpacity: 0.3, - elevation: 3, - }, - optionHeader: { - flexDirection: 'row', - justifyContent: 'space-between', - alignItems: 'center', - marginBottom: 6, - }, - optionTitle: { - color: colors.white, - fontSize: 16, - fontWeight: '600', - }, - optionDescription: { - color: colors.mediumEmphasis, - fontSize: 13, - lineHeight: 18, - marginBottom: 10, - }, - exampleContainer: { - marginTop: 4, - }, - exampleLabel: { - color: colors.mediumEmphasis, - fontSize: 13, - marginBottom: 4, - }, - exampleImage: { - height: 60, - width: '100%', - backgroundColor: 'rgba(0,0,0,0.5)', - borderRadius: 8, - }, - loadingContainer: { - justifyContent: 'center', - alignItems: 'center', - }, - infoBox: { - marginBottom: 16, - padding: 12, - backgroundColor: 'rgba(255,255,255,0.05)', - borderRadius: 8, - borderLeftWidth: 3, - borderLeftColor: colors.primary, - }, - infoText: { - color: colors.mediumEmphasis, - fontSize: 12, - lineHeight: 18, - }, - logoSourceLabel: { - color: colors.mediumEmphasis, - fontSize: 11, - marginTop: 2, - }, - languageSelectorContainer: { - marginTop: 10, - padding: 10, - backgroundColor: 'rgba(255,255,255,0.05)', - borderRadius: 6, - }, - languageSelectorTitle: { - color: colors.white, - fontSize: 14, - fontWeight: '600', - marginBottom: 4, - }, - languageSelectorDescription: { - color: colors.mediumEmphasis, - fontSize: 12, - lineHeight: 18, - marginBottom: 8, - }, - languageSelectorLabel: { - color: colors.mediumEmphasis, - fontSize: 12, - marginBottom: 6, - }, - languageScrollContent: { - paddingVertical: 2, - }, - languageItem: { - paddingHorizontal: 10, - paddingVertical: 6, - backgroundColor: colors.elevation1, - borderRadius: 12, - marginRight: 6, - borderWidth: 1, - borderColor: colors.elevation3, - marginVertical: 1, - shadowColor: '#000', - shadowOffset: { width: 0, height: 1 }, - shadowOpacity: 0.1, - shadowRadius: 1, - elevation: 1, - }, - selectedLanguageItem: { - backgroundColor: colors.primary, - borderColor: colors.primary, - shadowColor: colors.primary, - shadowOffset: { width: 0, height: 1 }, - shadowOpacity: 0.2, - shadowRadius: 1, - elevation: 2, - }, - languageItemText: { - color: colors.mediumEmphasis, - fontSize: 12, - fontWeight: '600', - }, - selectedLanguageItemText: { - color: colors.white, - }, - noteText: { - color: colors.mediumEmphasis, - fontSize: 11, - marginTop: 8, - fontStyle: 'italic', - }, - bannerContainer: { - height: 90, - width: '100%', - borderRadius: 6, - overflow: 'hidden', - position: 'relative', - }, - bannerImage: { - ...StyleSheet.absoluteFillObject, - }, - bannerOverlay: { - ...StyleSheet.absoluteFillObject, - backgroundColor: 'rgba(0,0,0,0.5)', - }, - logoOverBanner: { - position: 'absolute', - width: '80%', - height: '75%', - alignSelf: 'center', - top: '12.5%', - }, - 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, - }, -}); - -const LogoSourceSettings = () => { - const { settings, updateSetting } = useSettings(); - const navigation = useNavigation>(); - const insets = useSafeAreaInsets(); - const { currentTheme } = useTheme(); - const colors = currentTheme.colors; - const styles = createStyles(colors); - - // CustomAlert state - const [alertVisible, setAlertVisible] = useState(false); - const [alertTitle, setAlertTitle] = useState(''); - const [alertMessage, setAlertMessage] = useState(''); - const [alertActions, setAlertActions] = useState void; style?: object }>>([ - { label: 'OK', onPress: () => setAlertVisible(false) }, - ]); - - const openAlert = ( - title: string, - message: string, - actions?: Array<{ label: string; onPress?: () => void; style?: object }> - ) => { - setAlertTitle(title); - setAlertMessage(message); - if (actions && actions.length > 0) { - setAlertActions( - actions.map(a => ({ - label: a.label, - style: a.style, - onPress: () => { a.onPress?.(); }, - })) - ); - } else { - setAlertActions([{ label: 'OK', onPress: () => setAlertVisible(false) }]); - } - setAlertVisible(true); - }; - - // Get current preference - const [logoSource, setLogoSource] = useState<'metahub' | 'tmdb'>( - settings.logoSourcePreference || 'metahub' - ); - - // 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); - // Track which language the preview is actually using and if it is a fallback - const [previewLanguage, setPreviewLanguage] = useState(''); - const [isPreviewFallback, setIsPreviewFallback] = useState(false); - - // State for TMDB language selection - // Store unique language codes as strings - const [uniqueTmdbLanguages, setUniqueTmdbLanguages] = useState([]); - const [tmdbLogosData, setTmdbLogosData] = useState | null>(null); - - // Load example logos for selected show - useEffect(() => { - 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); - // Reset unique languages and logos data - setUniqueTmdbLanguages([]); - setTmdbLogosData(null); - - 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 preferred language directly from settings - const preferredTmdbLanguage = settings.tmdbLanguagePreference || 'en'; - - // Get TMDB logo and banner - try { - 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(); - - // Store all TMDB logos data and extract unique languages - if (imagesData.logos && imagesData.logos.length > 0) { - setTmdbLogosData(imagesData.logos); - - // Filter for logos with valid language codes and get unique codes - const validLogoLanguages = imagesData.logos - .map((logo: { iso_639_1: string | null }) => logo.iso_639_1) - .filter((lang: string | null): lang is string => lang !== null && typeof lang === 'string'); - - // Explicitly type the Set and resulting array - const uniqueCodes: string[] = [...new Set(validLogoLanguages)]; - setUniqueTmdbLanguages(uniqueCodes); - - // Find initial logo (prefer selectedTmdbLanguage, then 'en') - let initialLogoPath: string | null = null; - let initialLanguage = preferredTmdbLanguage; - - // First try to find a logo in the user's preferred language - const preferredLogo = imagesData.logos.find((logo: { iso_639_1: string; file_path: string }) => logo.iso_639_1 === preferredTmdbLanguage); - - if (preferredLogo) { - initialLogoPath = preferredLogo.file_path; - initialLanguage = preferredTmdbLanguage; - logger.log(`[LogoSourceSettings] Found initial ${preferredTmdbLanguage} TMDB logo for ${show.name}`); - setIsPreviewFallback(false); - } else { - // Fallback to English logo - const englishLogo = imagesData.logos.find((logo: { iso_639_1: string; file_path: string }) => logo.iso_639_1 === 'en'); - - if (englishLogo) { - initialLogoPath = englishLogo.file_path; - initialLanguage = 'en'; - logger.log(`[LogoSourceSettings] Found initial English TMDB logo for ${show.name}`); - setIsPreviewFallback(true); - } else if (imagesData.logos[0]) { - // Fallback to the first available logo - initialLogoPath = imagesData.logos[0].file_path; - initialLanguage = imagesData.logos[0].iso_639_1; - logger.log(`[LogoSourceSettings] No English logo, using first available (${initialLanguage}) TMDB logo for ${show.name}`); - setIsPreviewFallback(true); - } - } - - if (initialLogoPath) { - setTmdbLogo(`https://image.tmdb.org/t/p/original${initialLogoPath}`); - setPreviewLanguage(initialLanguage || ''); - } else { - logger.warn(`[LogoSourceSettings] No valid initial TMDB logo found for ${show.name}`); - } - } else { - logger.warn(`[LogoSourceSettings] No TMDB logos found in response for ${show.name}`); - setUniqueTmdbLanguages([]); // Ensure it's empty if no logos - setPreviewLanguage(''); - setIsPreviewFallback(false); - } - - // 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); - } - - } catch (err) { - logger.error(`[LogoSourceSettings] Error fetching ${show.name} logos:`, err); - } finally { - setLoadingLogos(false); - } - }; - - // Apply logo source setting and show confirmation - const applyLogoSourceSetting = (source: 'metahub' | 'tmdb') => { - // Update local state first - setLogoSource(source); - - // Update using the settings hook - updateSetting('logoSourcePreference', source); - - // Also save directly to AsyncStorage for extra assurance - try { - // Get current settings - AsyncStorage.getItem('app_settings').then((settingsJson) => { - if (settingsJson) { - const currentSettings = JSON.parse(settingsJson); - // Update the logo source preference - const updatedSettings = { - ...currentSettings, - logoSourcePreference: source - }; - // Save back to AsyncStorage - AsyncStorage.setItem('app_settings', JSON.stringify(updatedSettings)) - .then(() => { - logger.log(`[LogoSourceSettings] Successfully saved logo source preference '${source}' to AsyncStorage`); - }) - .catch((error) => { - logger.error(`[LogoSourceSettings] Error saving logo source preference to AsyncStorage:`, error); - }); - } - }).catch((error) => { - logger.error(`[LogoSourceSettings] Error getting current settings:`, error); - }); - - // Clear any cached logo data - AsyncStorage.removeItem('_last_logos_'); - } catch (e) { - logger.error(`[LogoSourceSettings] Error in applyLogoSourceSetting:`, e); - } - - // Show confirmation alert - openAlert( - 'Settings Updated', - `Logo and background source preference set to ${source === 'metahub' ? 'Metahub' : 'TMDB'}. Changes will apply when you navigate to content.` - ); - }; - - // Handle TMDB language selection - const handleTmdbLanguageSelect = (languageCode: string) => { - // Update the preview logo if possible - if (tmdbLogosData) { - const selectedLogoData = tmdbLogosData.find(logo => logo.iso_639_1 === languageCode); - if (selectedLogoData) { - setTmdbLogo(`https://image.tmdb.org/t/p/original${selectedLogoData.file_path}`); - logger.log(`[LogoSourceSettings] Switched TMDB logo preview to language: ${languageCode}`); - setPreviewLanguage(languageCode); - setIsPreviewFallback(false); - } else { - logger.warn(`[LogoSourceSettings] Could not find logo data for selected language: ${languageCode}`); - // Fallback to English, then first available if English is not present - const englishData = tmdbLogosData.find(logo => logo.iso_639_1 === 'en'); - if (englishData) { - setTmdbLogo(`https://image.tmdb.org/t/p/original${englishData.file_path}`); - setPreviewLanguage('en'); - setIsPreviewFallback(true); - } else if (tmdbLogosData[0]) { - setTmdbLogo(`https://image.tmdb.org/t/p/original${tmdbLogosData[0].file_path}`); - setPreviewLanguage(tmdbLogosData[0].iso_639_1 || ''); - setIsPreviewFallback(true); - } else { - setPreviewLanguage(''); - setIsPreviewFallback(false); - } - } - } - - // Then persist the setting globally - saveLanguagePreference(languageCode); - }; - - // Get preferred language directly from settings for UI rendering - const preferredTmdbLanguage = settings.tmdbLanguagePreference || 'en'; - - // Save language preference with proper persistence - const saveLanguagePreference = async (languageCode: string) => { - logger.log(`[LogoSourceSettings] Saving TMDB language preference: ${languageCode}`); - - try { - // First use the settings hook to update the setting - this is crucial - updateSetting('tmdbLanguagePreference', languageCode); - - // Clear any cached logo data - await AsyncStorage.removeItem('_last_logos_'); - - // Show confirmation toast or feedback - openAlert( - 'TMDB Language Updated', - `TMDB logo language preference set to ${languageCode.toUpperCase()}. Changes will apply when you navigate to content.` - ); - } catch (e) { - logger.error(`[LogoSourceSettings] Error in saveLanguagePreference:`, e); - - // Show error notification - openAlert( - 'Error Saving Preference', - 'There was a problem saving your language preference. Please try again.' - ); - } - }; - - // 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) { - if (__DEV__) 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) { - if (__DEV__) 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 and background - const renderLogoExample = (logo: string | null, banner: string | null, isLoading: boolean) => { - if (isLoading) { - return ( - - - - ); - } - - return ( - - - - {logo && ( - - )} - {!logo && ( - - No logo available - - )} - - ); - }; - - return ( - - - {/* Header */} - - - - Settings - - - {/* Empty for now, but ready for future actions */} - - - Logo Source - - {/* Description */} - - - Choose the primary source for content logos and backgrounds. The selected source will be used exclusively. - - - - {/* Show selector */} - - Select a show/movie to preview: - - {EXAMPLE_SHOWS.map((show) => ( - handleShowSelect(show)} - activeOpacity={0.7} - delayPressIn={100} - > - - {show.name} - - - ))} - - - - {/* Options */} - - applyLogoSourceSetting('metahub')} - activeOpacity={0.7} - delayPressIn={100} - > - - Metahub - {logoSource === 'metahub' && ( - - )} - - - - High-quality logos from Metahub. Best for popular titles. - - - - Example: - {renderLogoExample(metahubLogo, metahubBanner, loadingLogos)} - {selectedShow.name} logo from Metahub - - - - applyLogoSourceSetting('tmdb')} - activeOpacity={0.7} - delayPressIn={100} - > - - TMDB - {logoSource === 'tmdb' && ( - - )} - - - - Logos from TMDB. Offers localized options and better coverage for recent content. - - - - Example: - {renderLogoExample(tmdbLogo, tmdbBanner, loadingLogos)} - - {`Preview language: ${(previewLanguage || '').toUpperCase() || 'N/A'}${isPreviewFallback ? ' (fallback)' : ''}`} - - {selectedShow.name} logo from TMDB - - - {/* TMDB Language Selector */} - {true && ( - - Logo Language - - Select your preferred language for TMDB logos (includes common languages like Arabic even if not shown in this preview). - - - {/* Merge unique languages from TMDB with a common list to ensure wider options */} - {Array.from(new Set([...uniqueTmdbLanguages, ...COMMON_TMDB_LANGUAGES])).map((langCode) => ( - handleTmdbLanguageSelect(langCode)} - activeOpacity={0.7} - delayPressIn={150} - > - - {(langCode || '').toUpperCase() || '??'} - - - ))} - - - If unavailable in preferred language, English will be used as fallback. - - - )} - - - - {/* Additional Info */} - - - The app will use only the selected source for logos and backgrounds. If no image is available from your chosen source, a text fallback will be used. - - - - setAlertVisible(false)} - actions={alertActions} - /> - - ); - }; - - export default LogoSourceSettings; \ No newline at end of file diff --git a/src/screens/SettingsScreen.tsx b/src/screens/SettingsScreen.tsx index 35de827..92dff76 100644 --- a/src/screens/SettingsScreen.tsx +++ b/src/screens/SettingsScreen.tsx @@ -557,18 +557,10 @@ const SettingsScreen: React.FC = () => { /> navigation.navigate('TMDBSettings')} - isTablet={isTablet} - /> - navigation.navigate('LogoSourceSettings')} isLast={true} isTablet={isTablet} /> diff --git a/src/screens/TMDBSettingsScreen.tsx b/src/screens/TMDBSettingsScreen.tsx index b6bc446..f331bc9 100644 --- a/src/screens/TMDBSettingsScreen.tsx +++ b/src/screens/TMDBSettingsScreen.tsx @@ -32,6 +32,35 @@ import CustomAlert from '../components/CustomAlert'; const TMDB_API_KEY_STORAGE_KEY = 'tmdb_api_key'; const USE_CUSTOM_TMDB_API_KEY = 'use_custom_tmdb_api_key'; +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: 'Stranger Things', + imdbId: 'tt4574334', + tmdbId: '66732', + type: 'tv' as const + }, + { + name: 'Avatar', + imdbId: 'tt0499549', + tmdbId: '19995', + type: 'movie' as const + }, +]; const TMDBSettingsScreen = () => { const navigation = useNavigation(); @@ -53,6 +82,14 @@ const TMDBSettingsScreen = () => { const { settings, updateSetting } = useSettings(); const [languagePickerVisible, setLanguagePickerVisible] = useState(false); const [languageSearch, setLanguageSearch] = useState(''); + + // Logo preview state + const [selectedShow, setSelectedShow] = useState(EXAMPLE_SHOWS[0]); + const [tmdbLogo, setTmdbLogo] = useState(null); + const [tmdbBanner, setTmdbBanner] = useState(null); + const [loadingLogos, setLoadingLogos] = useState(true); + const [previewLanguage, setPreviewLanguage] = useState(''); + const [isPreviewFallback, setIsPreviewFallback] = useState(false); const openAlert = ( title: string, @@ -253,6 +290,151 @@ const TMDBSettingsScreen = () => { }); }; + // Logo preview functions + const fetchExampleLogos = async (show: typeof EXAMPLE_SHOWS[0]) => { + setLoadingLogos(true); + setTmdbLogo(null); + setTmdbBanner(null); + + try { + const tmdbId = show.tmdbId; + const contentType = show.type; + + logger.log(`[TMDBSettingsScreen] Fetching ${show.name} with TMDB ID: ${tmdbId}`); + + const preferredTmdbLanguage = settings.tmdbLanguagePreference || 'en'; + + 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(); + + if (imagesData.logos && imagesData.logos.length > 0) { + let logoPath: string | null = null; + let logoLanguage = preferredTmdbLanguage; + + // Try to find logo in preferred language + const preferredLogo = imagesData.logos.find((logo: { iso_639_1: string; file_path: string }) => logo.iso_639_1 === preferredTmdbLanguage); + + if (preferredLogo) { + logoPath = preferredLogo.file_path; + logoLanguage = preferredTmdbLanguage; + setIsPreviewFallback(false); + } else { + // Fallback to English + const englishLogo = imagesData.logos.find((logo: { iso_639_1: string; file_path: string }) => logo.iso_639_1 === 'en'); + + if (englishLogo) { + logoPath = englishLogo.file_path; + logoLanguage = 'en'; + setIsPreviewFallback(true); + } else if (imagesData.logos[0]) { + // Fallback to first available + logoPath = imagesData.logos[0].file_path; + logoLanguage = imagesData.logos[0].iso_639_1 || 'unknown'; + setIsPreviewFallback(true); + } + } + + if (logoPath) { + setTmdbLogo(`https://image.tmdb.org/t/p/original${logoPath}`); + setPreviewLanguage(logoLanguage); + } else { + setPreviewLanguage(''); + setIsPreviewFallback(false); + } + } else { + setPreviewLanguage(''); + setIsPreviewFallback(false); + } + + // Get TMDB banner (backdrop) + if (imagesData.backdrops && imagesData.backdrops.length > 0) { + const backdropPath = imagesData.backdrops[0].file_path; + setTmdbBanner(`https://image.tmdb.org/t/p/original${backdropPath}`); + } else { + const detailsResponse = await fetch(`https://api.themoviedb.org/3/${endpoint}/${tmdbId}?api_key=${apiKey}`); + const details = await detailsResponse.json(); + + if (details.backdrop_path) { + setTmdbBanner(`https://image.tmdb.org/t/p/original${details.backdrop_path}`); + } + } + } catch (err) { + logger.error(`[TMDBSettingsScreen] Error fetching ${show.name} preview:`, err); + } finally { + setLoadingLogos(false); + } + }; + + const handleShowSelect = (show: typeof EXAMPLE_SHOWS[0]) => { + setSelectedShow(show); + try { + AsyncStorage.setItem('tmdb_settings_selected_show', show.imdbId); + } catch (e) { + if (__DEV__) console.error('Error saving selected show:', e); + } + }; + + const renderLogoExample = (logo: string | null, banner: string | null, isLoading: boolean) => { + if (isLoading) { + return ( + + + + ); + } + + return ( + + + + {logo && ( + + )} + {!logo && ( + + No logo available + + )} + + ); + }; + + // Load example logos when show or language changes + useEffect(() => { + if (settings.enrichMetadataWithTMDB && settings.useTmdbLocalizedMetadata) { + fetchExampleLogos(selectedShow); + } + }, [selectedShow, settings.enrichMetadataWithTMDB, settings.useTmdbLocalizedMetadata, settings.tmdbLanguagePreference]); + + // Load selected show from AsyncStorage on mount + useEffect(() => { + const loadSelectedShow = async () => { + try { + const savedShowId = await AsyncStorage.getItem('tmdb_settings_selected_show'); + if (savedShowId) { + const foundShow = EXAMPLE_SHOWS.find(show => show.imdbId === savedShowId); + if (foundShow) { + setSelectedShow(foundShow); + } + } + } catch (e) { + if (__DEV__) console.error('Error loading selected show:', e); + } + }; + + loadSelectedShow(); + }, []); + const headerBaseHeight = Platform.OS === 'android' ? 80 : 60; const topSpacing = Platform.OS === 'android' ? (StatusBar.currentHeight || 0) : insets.top; const headerHeight = headerBaseHeight + topSpacing; @@ -357,6 +539,56 @@ const TMDBSettingsScreen = () => { Change + + {/* Logo Preview */} + + + Logo Preview + + Preview shows how localized logos will appear in the selected language. + + + {/* Show selector */} + Example: + + {EXAMPLE_SHOWS.map((show) => ( + handleShowSelect(show)} + activeOpacity={0.7} + > + + {show.name} + + + ))} + + + {/* Preview card */} + + {renderLogoExample(tmdbLogo, tmdbBanner, loadingLogos)} + {tmdbLogo && ( + + {`Language: ${(previewLanguage || '').toUpperCase() || 'N/A'}${isPreviewFallback ? ' (fallback to available)' : ''}`} + + )} + )} @@ -1113,6 +1345,91 @@ const styles = StyleSheet.create({ fontSize: 16, fontWeight: '700', }, + + // Logo Source Styles + selectorLabel: { + fontSize: 13, + marginBottom: 8, + marginTop: 4, + }, + showsScrollView: { + marginBottom: 16, + }, + showsScrollContent: { + paddingRight: 16, + paddingVertical: 2, + }, + showItem: { + paddingHorizontal: 12, + paddingVertical: 6, + borderRadius: 16, + marginRight: 6, + borderWidth: 1, + borderColor: 'transparent', + }, + selectedShowItem: { + borderWidth: 2, + }, + showItemText: { + fontSize: 13, + }, + selectedShowItemText: { + fontWeight: '600', + }, + logoPreviewCard: { + borderRadius: 12, + padding: 12, + marginTop: 12, + }, + exampleImage: { + height: 60, + width: '100%', + backgroundColor: 'rgba(0,0,0,0.5)', + borderRadius: 8, + }, + bannerContainer: { + height: 80, + width: '100%', + borderRadius: 8, + overflow: 'hidden', + position: 'relative', + marginTop: 4, + }, + bannerImage: { + ...StyleSheet.absoluteFillObject, + }, + bannerOverlay: { + ...StyleSheet.absoluteFillObject, + backgroundColor: 'rgba(0,0,0,0.4)', + }, + logoOverBanner: { + position: 'absolute', + width: '80%', + height: '70%', + alignSelf: 'center', + top: '15%', + }, + noLogoContainer: { + position: 'absolute', + top: 0, + left: 0, + right: 0, + bottom: 0, + justifyContent: 'center', + alignItems: 'center', + }, + noLogoText: { + color: '#fff', + fontSize: 13, + backgroundColor: 'rgba(0,0,0,0.5)', + paddingHorizontal: 12, + paddingVertical: 6, + borderRadius: 4, + }, + logoSourceLabel: { + fontSize: 11, + marginTop: 6, + }, }); export default TMDBSettingsScreen; \ No newline at end of file diff --git a/src/services/catalogService.ts b/src/services/catalogService.ts index 323e8a4..083332d 100644 --- a/src/services/catalogService.ts +++ b/src/services/catalogService.ts @@ -588,6 +588,14 @@ class CatalogService { if (!logoUrl || logoUrl.trim() === '' || logoUrl === 'null' || logoUrl === 'undefined') { logoUrl = undefined; } + try { + logger.debug('[CatalogService] convertMetaToStreamingContent:logo', { + id: meta.id, + name: meta.name, + hasLogo: Boolean(logoUrl), + logo: logoUrl || undefined, + }); + } catch {} return { id: meta.id, diff --git a/src/services/robustCalendarCache.ts b/src/services/robustCalendarCache.ts index ad36e9c..9ca3296 100644 --- a/src/services/robustCalendarCache.ts +++ b/src/services/robustCalendarCache.ts @@ -49,7 +49,6 @@ class RobustCalendarCache { return null; } - logger.log(`[Cache] Valid cache found for key ${key}`); return cache.data; } catch (error) { logger.error(`[Cache] Error getting cached data for key ${key}:`, error);