diff --git a/src/hooks/useSettings.ts b/src/hooks/useSettings.ts index 576ff3a4b..3f55f4ec0 100644 --- a/src/hooks/useSettings.ts +++ b/src/hooks/useSettings.ts @@ -33,6 +33,7 @@ export interface AppSettings { featuredContentSource: 'tmdb' | 'catalogs'; selectedHeroCatalogs: string[]; // Array of catalog IDs to display in hero section logoSourcePreference: 'metahub' | 'tmdb'; // Preferred source for title logos + tmdbLanguagePreference: string; // Preferred language for TMDB logos (ISO 639-1 code) } export const DEFAULT_SETTINGS: AppSettings = { @@ -48,6 +49,7 @@ export const DEFAULT_SETTINGS: AppSettings = { featuredContentSource: 'tmdb', selectedHeroCatalogs: [], // Empty array means all catalogs are selected logoSourcePreference: 'metahub', // Default to Metahub as first source + tmdbLanguagePreference: 'en', // Default to English }; const SETTINGS_STORAGE_KEY = 'app_settings'; diff --git a/src/screens/LogoSourceSettings.tsx b/src/screens/LogoSourceSettings.tsx index 2ebb9beef..1ae37e778 100644 --- a/src/screens/LogoSourceSettings.tsx +++ b/src/screens/LogoSourceSettings.tsx @@ -81,11 +81,21 @@ const LogoSourceSettings = () => { settings.logoSourcePreference || 'metahub' ); + // TMDB Language Preference + const [selectedTmdbLanguage, setSelectedTmdbLanguage] = useState( + settings.tmdbLanguagePreference || 'en' + ); + // Make sure logoSource stays in sync with settings useEffect(() => { setLogoSource(settings.logoSourcePreference || 'metahub'); }, [settings.logoSourcePreference]); + // Keep selectedTmdbLanguage in sync with settings + useEffect(() => { + setSelectedTmdbLanguage(settings.tmdbLanguagePreference || 'en'); + }, [settings.tmdbLanguagePreference]); + // Selected example show const [selectedShow, setSelectedShow] = useState(EXAMPLE_SHOWS[0]); @@ -97,7 +107,6 @@ const LogoSourceSettings = () => { const [loadingLogos, setLoadingLogos] = useState(true); // State for TMDB language selection - const [selectedTmdbLanguage, setSelectedTmdbLanguage] = useState('en'); // Store unique language codes as strings const [uniqueTmdbLanguages, setUniqueTmdbLanguages] = useState([]); const [tmdbLogosData, setTmdbLogosData] = useState | null>(null); @@ -117,7 +126,6 @@ const LogoSourceSettings = () => { // Reset unique languages and logos data setUniqueTmdbLanguages([]); setTmdbLogosData(null); - setSelectedTmdbLanguage('en'); // Reset to default language try { const tmdbService = TMDBService.getInstance(); @@ -147,21 +155,31 @@ const LogoSourceSettings = () => { const uniqueCodes: string[] = [...new Set(validLogoLanguages)]; setUniqueTmdbLanguages(uniqueCodes); - // Find initial logo (prefer 'en') + // Find initial logo (prefer selectedTmdbLanguage, then 'en') let initialLogoPath: string | null = null; - let initialLanguage = 'en'; + let initialLanguage = selectedTmdbLanguage; - const englishLogo = imagesData.logos.find((logo: { iso_639_1: string; file_path: string }) => logo.iso_639_1 === 'en'); + // 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 === selectedTmdbLanguage); - if (englishLogo) { - initialLogoPath = englishLogo.file_path; - initialLanguage = 'en'; - logger.log(`[LogoSourceSettings] Found initial English TMDB logo for ${show.name}`); - } 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}`); + if (preferredLogo) { + initialLogoPath = preferredLogo.file_path; + initialLanguage = selectedTmdbLanguage; + logger.log(`[LogoSourceSettings] Found initial ${selectedTmdbLanguage} TMDB logo for ${show.name}`); + } 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}`); + } 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}`); + } } if (initialLogoPath) { @@ -228,7 +246,7 @@ const LogoSourceSettings = () => { } }; - // Apply setting and show confirmation + // Apply logo source setting and show confirmation const applyLogoSourceSetting = (source: 'metahub' | 'tmdb') => { setLogoSource(source); updateSetting('logoSourcePreference', source); @@ -248,6 +266,26 @@ const LogoSourceSettings = () => { ); }; + // Apply TMDB language setting + const applyTmdbLanguageSetting = (languageCode: string) => { + setSelectedTmdbLanguage(languageCode); + updateSetting('tmdbLanguagePreference', languageCode); + + // Clear any cached logo data in storage + try { + AsyncStorage.removeItem('_last_logos_'); + } catch (e) { + console.error('Error clearing logo cache:', e); + } + + // Show confirmation toast or feedback + Alert.alert( + 'TMDB Language Updated', + `TMDB logo language preference set to ${languageCode.toUpperCase()}. Changes will apply when you navigate to content.`, + [{ text: 'OK' }] + ); + }; + // Save selected show to AsyncStorage to persist across navigation const saveSelectedShow = async (show: typeof EXAMPLE_SHOWS[0]) => { try { @@ -323,7 +361,9 @@ const LogoSourceSettings = () => { // Handle TMDB language selection const handleTmdbLanguageSelect = (languageCode: string) => { + // Update local state for the example setSelectedTmdbLanguage(languageCode); + if (tmdbLogosData) { const selectedLogoData = tmdbLogosData.find(logo => logo.iso_639_1 === languageCode); if (selectedLogoData) { @@ -333,6 +373,9 @@ const LogoSourceSettings = () => { logger.warn(`[LogoSourceSettings] Could not find logo data for selected language: ${languageCode}`); } } + + // Also update the app-wide setting + applyTmdbLanguageSetting(languageCode); }; return ( @@ -447,7 +490,10 @@ const LogoSourceSettings = () => { {/* TMDB Language Selector */} {uniqueTmdbLanguages.length > 1 && ( - Available logo languages: + Logo Language + + Select your preferred language for TMDB logos. This affects all content when TMDB is used as the logo source. + { ))} + + Note: Not all titles have logos in all languages. If a logo isn't available in your preferred language, English will be used as a fallback. + )} @@ -633,6 +682,21 @@ const LogoSourceSettings = () => { }, languageSelectorContainer: { marginTop: 16, + padding: 12, + backgroundColor: 'rgba(255,255,255,0.05)', + borderRadius: 8, + }, + languageSelectorTitle: { + color: colors.white, + fontSize: 16, + fontWeight: '600', + marginBottom: 8, + }, + languageSelectorDescription: { + color: colors.mediumEmphasis, + fontSize: 14, + lineHeight: 20, + marginBottom: 12, }, languageSelectorLabel: { color: colors.mediumEmphasis, @@ -640,7 +704,7 @@ const LogoSourceSettings = () => { marginBottom: 8, }, languageScrollContent: { - paddingRight: 16, // Match container padding + paddingVertical: 4, }, languageItem: { paddingHorizontal: 12, @@ -663,6 +727,12 @@ const LogoSourceSettings = () => { selectedLanguageItemText: { color: colors.white, }, + noteText: { + color: colors.mediumEmphasis, + fontSize: 12, + marginTop: 12, + fontStyle: 'italic', + }, bannerContainer: { height: 120, width: '100%', diff --git a/src/screens/MetadataScreen.tsx b/src/screens/MetadataScreen.tsx index db02a12fc..d2ba5a905 100644 --- a/src/screens/MetadataScreen.tsx +++ b/src/screens/MetadataScreen.tsx @@ -690,10 +690,12 @@ const MetadataScreen = () => { if (id.startsWith('tmdb:')) { const tmdbId = id.split(':')[1]; const tmdbType = type === 'series' ? 'tv' : 'movie'; - logger.log(`[MetadataScreen] Attempting to fetch logo from TMDB for ${tmdbType} (ID: ${tmdbId})`); + const preferredLanguage = settings.tmdbLanguagePreference || 'en'; + + logger.log(`[MetadataScreen] Attempting to fetch logo from TMDB for ${tmdbType} (ID: ${tmdbId}, preferred language: ${preferredLanguage})`); try { const tmdbService = TMDBService.getInstance(); - tmdbLogoUrl = await tmdbService.getContentLogo(tmdbType, tmdbId); + tmdbLogoUrl = await tmdbService.getContentLogo(tmdbType, tmdbId, preferredLanguage); if (tmdbLogoUrl) { logger.log(`[MetadataScreen] Successfully fetched logo from TMDB: ${tmdbLogoUrl}`); @@ -705,7 +707,8 @@ const MetadataScreen = () => { } } 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`); + const preferredLanguage = settings.tmdbLanguagePreference || 'en'; + logger.log(`[MetadataScreen] Content has IMDB ID (${imdbId}), looking up TMDB ID for TMDB logo, preferred language: ${preferredLanguage}`); try { const tmdbService = TMDBService.getInstance(); const foundTmdbId = await tmdbService.findTMDBIdByIMDB(imdbId); @@ -714,7 +717,7 @@ const MetadataScreen = () => { 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()); + tmdbLogoUrl = await tmdbService.getContentLogo(type === 'series' ? 'tv' : 'movie', foundTmdbId.toString(), preferredLanguage); if (tmdbLogoUrl) { logger.log(`[MetadataScreen] Successfully fetched logo from TMDB via IMDB lookup: ${tmdbLogoUrl}`); diff --git a/src/services/tmdbService.ts b/src/services/tmdbService.ts index 3a1d2dff0..5fca12ed2 100644 --- a/src/services/tmdbService.ts +++ b/src/services/tmdbService.ts @@ -568,14 +568,14 @@ export class TMDBService { /** * Get movie images (logos, posters, backdrops) by TMDB ID */ - async getMovieImages(movieId: number | string): Promise { + async getMovieImages(movieId: number | string, preferredLanguage: string = 'en'): Promise { try { - logger.log(`[TMDBService] Fetching movie images for TMDB ID: ${movieId}`); + logger.log(`[TMDBService] Fetching movie images for TMDB ID: ${movieId}, preferred language: ${preferredLanguage}`); const response = await axios.get(`${BASE_URL}/movie/${movieId}/images`, { headers: await this.getHeaders(), params: await this.getParams({ - include_image_language: 'en,null' + include_image_language: `${preferredLanguage},en,null` }), }); @@ -583,7 +583,40 @@ export class TMDBService { logger.log(`[TMDBService] Retrieved ${images?.logos?.length || 0} logos for movie ID ${movieId}`); if (images && images.logos && images.logos.length > 0) { - // First prioritize English SVG logos + // First prioritize preferred language SVG logos if not English + if (preferredLanguage !== 'en') { + const preferredSvgLogo = images.logos.find((logo: any) => + logo.file_path && + logo.file_path.endsWith('.svg') && + logo.iso_639_1 === preferredLanguage + ); + if (preferredSvgLogo) { + logger.log(`[TMDBService] Found ${preferredLanguage} SVG logo for movie ID ${movieId}: ${preferredSvgLogo.file_path}`); + return this.getImageUrl(preferredSvgLogo.file_path); + } + + // Then preferred language PNG logos + const preferredPngLogo = images.logos.find((logo: any) => + logo.file_path && + logo.file_path.endsWith('.png') && + logo.iso_639_1 === preferredLanguage + ); + if (preferredPngLogo) { + logger.log(`[TMDBService] Found ${preferredLanguage} PNG logo for movie ID ${movieId}: ${preferredPngLogo.file_path}`); + return this.getImageUrl(preferredPngLogo.file_path); + } + + // Then any preferred language logo + const preferredLogo = images.logos.find((logo: any) => + logo.iso_639_1 === preferredLanguage + ); + if (preferredLogo) { + logger.log(`[TMDBService] Found ${preferredLanguage} logo for movie ID ${movieId}: ${preferredLogo.file_path}`); + return this.getImageUrl(preferredLogo.file_path); + } + } + + // Then prioritize English SVG logos const enSvgLogo = images.logos.find((logo: any) => logo.file_path && logo.file_path.endsWith('.svg') && @@ -649,14 +682,14 @@ export class TMDBService { /** * Get TV show images (logos, posters, backdrops) by TMDB ID */ - async getTvShowImages(showId: number | string): Promise { + async getTvShowImages(showId: number | string, preferredLanguage: string = 'en'): Promise { try { - logger.log(`[TMDBService] Fetching TV show images for TMDB ID: ${showId}`); + logger.log(`[TMDBService] Fetching TV show images for TMDB ID: ${showId}, preferred language: ${preferredLanguage}`); const response = await axios.get(`${BASE_URL}/tv/${showId}/images`, { headers: await this.getHeaders(), params: await this.getParams({ - include_image_language: 'en,null' + include_image_language: `${preferredLanguage},en,null` }), }); @@ -664,6 +697,39 @@ export class TMDBService { logger.log(`[TMDBService] Retrieved ${images?.logos?.length || 0} logos for TV show ID ${showId}`); if (images && images.logos && images.logos.length > 0) { + // First prioritize preferred language SVG logos if not English + if (preferredLanguage !== 'en') { + const preferredSvgLogo = images.logos.find((logo: any) => + logo.file_path && + logo.file_path.endsWith('.svg') && + logo.iso_639_1 === preferredLanguage + ); + if (preferredSvgLogo) { + logger.log(`[TMDBService] Found ${preferredLanguage} SVG logo for TV show ID ${showId}: ${preferredSvgLogo.file_path}`); + return this.getImageUrl(preferredSvgLogo.file_path); + } + + // Then preferred language PNG logos + const preferredPngLogo = images.logos.find((logo: any) => + logo.file_path && + logo.file_path.endsWith('.png') && + logo.iso_639_1 === preferredLanguage + ); + if (preferredPngLogo) { + logger.log(`[TMDBService] Found ${preferredLanguage} PNG logo for TV show ID ${showId}: ${preferredPngLogo.file_path}`); + return this.getImageUrl(preferredPngLogo.file_path); + } + + // Then any preferred language logo + const preferredLogo = images.logos.find((logo: any) => + logo.iso_639_1 === preferredLanguage + ); + if (preferredLogo) { + logger.log(`[TMDBService] Found ${preferredLanguage} logo for TV show ID ${showId}: ${preferredLogo.file_path}`); + return this.getImageUrl(preferredLogo.file_path); + } + } + // First prioritize English SVG logos const enSvgLogo = images.logos.find((logo: any) => logo.file_path && @@ -730,13 +796,13 @@ export class TMDBService { /** * Get content logo based on type (movie or TV show) */ - async getContentLogo(type: 'movie' | 'tv', id: number | string): Promise { + async getContentLogo(type: 'movie' | 'tv', id: number | string, preferredLanguage: string = 'en'): Promise { try { - logger.log(`[TMDBService] Getting content logo for ${type} with ID ${id}`); + logger.log(`[TMDBService] Getting content logo for ${type} with ID ${id}, preferred language: ${preferredLanguage}`); const result = type === 'movie' - ? await this.getMovieImages(id) - : await this.getTvShowImages(id); + ? await this.getMovieImages(id, preferredLanguage) + : await this.getTvShowImages(id, preferredLanguage); if (result) { logger.log(`[TMDBService] Successfully retrieved logo for ${type} ID ${id}: ${result}`);