mirror of
https://github.com/tapframe/NuvioStreaming.git
synced 2026-01-11 20:10:25 +00:00
some tmdb logo fetching logic changes
This commit is contained in:
parent
42c236e235
commit
238f08192f
10 changed files with 425 additions and 1016 deletions
|
|
@ -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(() => {
|
||||
|
|
|
|||
|
|
@ -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];
|
||||
|
|
|
|||
|
|
@ -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<StreamingContent> => {
|
||||
|
|
@ -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<StreamingContent> => {
|
||||
|
|
@ -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<string, string>();
|
||||
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 {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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
|
|||
},
|
||||
}}
|
||||
/>
|
||||
<Stack.Screen
|
||||
name="LogoSourceSettings"
|
||||
component={LogoSourceSettings}
|
||||
options={{
|
||||
animation: Platform.OS === 'android' ? 'slide_from_right' : 'fade',
|
||||
animationDuration: Platform.OS === 'android' ? 250 : 200,
|
||||
presentation: 'card',
|
||||
gestureEnabled: true,
|
||||
gestureDirection: 'horizontal',
|
||||
headerShown: false,
|
||||
contentStyle: {
|
||||
backgroundColor: currentTheme.colors.darkBackground,
|
||||
},
|
||||
}}
|
||||
/>
|
||||
<Stack.Screen
|
||||
name="ThemeSettings"
|
||||
component={ThemeScreen}
|
||||
|
|
|
|||
|
|
@ -1,908 +0,0 @@
|
|||
import React, { useState, useEffect } from 'react';
|
||||
import {
|
||||
View,
|
||||
Text,
|
||||
StyleSheet,
|
||||
TouchableOpacity,
|
||||
ScrollView,
|
||||
Switch,
|
||||
SafeAreaView,
|
||||
Image,
|
||||
StatusBar,
|
||||
Platform,
|
||||
ActivityIndicator,
|
||||
} 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 { useSettings, DEFAULT_SETTINGS } from '../hooks/useSettings';
|
||||
import { useSafeAreaInsets } from 'react-native-safe-area-context';
|
||||
import { TMDBService } from '../services/tmdbService';
|
||||
import { logger } from '../utils/logger';
|
||||
import { useTheme } from '../contexts/ThemeContext';
|
||||
import CustomAlert from '../components/CustomAlert';
|
||||
|
||||
// TMDB API key - since the default key might be private in the service, we'll use our own
|
||||
const TMDB_API_KEY = '439c478a771f35c05022f9feabcca01c';
|
||||
|
||||
// Extra TMDB logo languages to always offer (only Arabic per request)
|
||||
const COMMON_TMDB_LANGUAGES: string[] = ['ar'];
|
||||
|
||||
// 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
|
||||
}
|
||||
];
|
||||
|
||||
// Create a styles creator function that accepts the theme colors
|
||||
const createStyles = (colors: any) => 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<NavigationProp<any>>();
|
||||
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<Array<{ label: string; onPress: () => 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<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);
|
||||
// Track which language the preview is actually using and if it is a fallback
|
||||
const [previewLanguage, setPreviewLanguage] = useState<string>('');
|
||||
const [isPreviewFallback, setIsPreviewFallback] = useState<boolean>(false);
|
||||
|
||||
// State for TMDB language selection
|
||||
// Store unique language codes as strings
|
||||
const [uniqueTmdbLanguages, setUniqueTmdbLanguages] = useState<string[]>([]);
|
||||
const [tmdbLogosData, setTmdbLogosData] = useState<Array<{ iso_639_1: string; file_path: string }> | 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<string>(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 (
|
||||
<View style={[styles.exampleImage, styles.loadingContainer]}>
|
||||
<ActivityIndicator size="small" color={colors.primary} />
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<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>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<SafeAreaView style={[styles.container]}>
|
||||
<StatusBar barStyle="light-content" />
|
||||
{/* Header */}
|
||||
<View style={styles.header}>
|
||||
<TouchableOpacity
|
||||
onPress={handleBack}
|
||||
style={styles.backButton}
|
||||
hitSlop={{ top: 10, bottom: 10, left: 10, right: 10 }}
|
||||
>
|
||||
<MaterialIcons name="arrow-back" size={24} color={colors.white} />
|
||||
<Text style={styles.backText}>Settings</Text>
|
||||
</TouchableOpacity>
|
||||
<View style={styles.headerActions}>
|
||||
{/* Empty for now, but ready for future actions */}
|
||||
</View>
|
||||
</View>
|
||||
<Text style={styles.headerTitle}>Logo Source</Text>
|
||||
<ScrollView
|
||||
style={styles.scrollView}
|
||||
contentContainerStyle={styles.scrollContent}
|
||||
showsVerticalScrollIndicator={true}
|
||||
scrollEventThrottle={32}
|
||||
decelerationRate="normal"
|
||||
>
|
||||
{/* Description */}
|
||||
<View style={styles.descriptionContainer}>
|
||||
<Text style={styles.description}>
|
||||
Choose the primary source for content logos and backgrounds. The selected source will be used exclusively.
|
||||
</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}
|
||||
scrollEventThrottle={32}
|
||||
decelerationRate="normal"
|
||||
>
|
||||
{EXAMPLE_SHOWS.map((show) => (
|
||||
<TouchableOpacity
|
||||
key={show.imdbId}
|
||||
style={[
|
||||
styles.showItem,
|
||||
selectedShow.imdbId === show.imdbId && styles.selectedShowItem
|
||||
]}
|
||||
onPress={() => handleShowSelect(show)}
|
||||
activeOpacity={0.7}
|
||||
delayPressIn={100}
|
||||
>
|
||||
<Text
|
||||
style={[
|
||||
styles.showItemText,
|
||||
selectedShow.imdbId === show.imdbId && styles.selectedShowItemText
|
||||
]}
|
||||
>
|
||||
{show.name}
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
))}
|
||||
</ScrollView>
|
||||
</View>
|
||||
|
||||
{/* Options */}
|
||||
<View style={styles.optionsContainer}>
|
||||
<TouchableOpacity
|
||||
style={[
|
||||
styles.optionCard,
|
||||
logoSource === 'metahub' && styles.selectedCard
|
||||
]}
|
||||
onPress={() => applyLogoSourceSetting('metahub')}
|
||||
activeOpacity={0.7}
|
||||
delayPressIn={100}
|
||||
>
|
||||
<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}>
|
||||
High-quality logos from Metahub. Best for popular titles.
|
||||
</Text>
|
||||
|
||||
<View style={styles.exampleContainer}>
|
||||
<Text style={styles.exampleLabel}>Example:</Text>
|
||||
{renderLogoExample(metahubLogo, metahubBanner, loadingLogos)}
|
||||
<Text style={styles.logoSourceLabel}>{selectedShow.name} logo from Metahub</Text>
|
||||
</View>
|
||||
</TouchableOpacity>
|
||||
|
||||
<TouchableOpacity
|
||||
style={[
|
||||
styles.optionCard,
|
||||
logoSource === 'tmdb' && styles.selectedCard
|
||||
]}
|
||||
onPress={() => applyLogoSourceSetting('tmdb')}
|
||||
activeOpacity={0.7}
|
||||
delayPressIn={100}
|
||||
>
|
||||
<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}>
|
||||
Logos from TMDB. Offers localized options and better coverage for recent content.
|
||||
</Text>
|
||||
|
||||
<View style={styles.exampleContainer}>
|
||||
<Text style={styles.exampleLabel}>Example:</Text>
|
||||
{renderLogoExample(tmdbLogo, tmdbBanner, loadingLogos)}
|
||||
<Text style={styles.logoSourceLabel}>
|
||||
{`Preview language: ${(previewLanguage || '').toUpperCase() || 'N/A'}${isPreviewFallback ? ' (fallback)' : ''}`}
|
||||
</Text>
|
||||
<Text style={styles.logoSourceLabel}>{selectedShow.name} logo from TMDB</Text>
|
||||
</View>
|
||||
|
||||
{/* TMDB Language Selector */}
|
||||
{true && (
|
||||
<View style={styles.languageSelectorContainer}>
|
||||
<Text style={styles.languageSelectorTitle}>Logo Language</Text>
|
||||
<Text style={styles.languageSelectorDescription}>
|
||||
Select your preferred language for TMDB logos (includes common languages like Arabic even if not shown in this preview).
|
||||
</Text>
|
||||
<ScrollView
|
||||
horizontal
|
||||
showsHorizontalScrollIndicator={false}
|
||||
contentContainerStyle={styles.languageScrollContent}
|
||||
scrollEventThrottle={32}
|
||||
decelerationRate="normal"
|
||||
>
|
||||
{/* Merge unique languages from TMDB with a common list to ensure wider options */}
|
||||
{Array.from(new Set<string>([...uniqueTmdbLanguages, ...COMMON_TMDB_LANGUAGES])).map((langCode) => (
|
||||
<TouchableOpacity
|
||||
key={langCode} // Use the unique code as key
|
||||
style={[
|
||||
styles.languageItem,
|
||||
preferredTmdbLanguage === langCode && styles.selectedLanguageItem
|
||||
]}
|
||||
onPress={() => handleTmdbLanguageSelect(langCode)}
|
||||
activeOpacity={0.7}
|
||||
delayPressIn={150}
|
||||
>
|
||||
<Text
|
||||
style={[
|
||||
styles.languageItemText,
|
||||
preferredTmdbLanguage === langCode && styles.selectedLanguageItemText
|
||||
]}
|
||||
>
|
||||
{(langCode || '').toUpperCase() || '??'}
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
))}
|
||||
</ScrollView>
|
||||
<Text style={styles.noteText}>
|
||||
If unavailable in preferred language, English will be used as fallback.
|
||||
</Text>
|
||||
</View>
|
||||
)}
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
|
||||
{/* Additional Info */}
|
||||
<View style={styles.infoBox}>
|
||||
<Text style={styles.infoText}>
|
||||
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.
|
||||
</Text>
|
||||
</View>
|
||||
</ScrollView>
|
||||
<CustomAlert
|
||||
visible={alertVisible}
|
||||
title={alertTitle}
|
||||
message={alertMessage}
|
||||
onClose={() => setAlertVisible(false)}
|
||||
actions={alertActions}
|
||||
/>
|
||||
</SafeAreaView>
|
||||
);
|
||||
};
|
||||
|
||||
export default LogoSourceSettings;
|
||||
|
|
@ -557,18 +557,10 @@ const SettingsScreen: React.FC = () => {
|
|||
/>
|
||||
<SettingItem
|
||||
title="TMDB"
|
||||
description="Metadata provider"
|
||||
description="Metadata & logo source provider"
|
||||
icon="movie"
|
||||
renderControl={ChevronRight}
|
||||
onPress={() => navigation.navigate('TMDBSettings')}
|
||||
isTablet={isTablet}
|
||||
/>
|
||||
<SettingItem
|
||||
title="Media Sources"
|
||||
description="Logo & image preferences"
|
||||
icon="image"
|
||||
renderControl={ChevronRight}
|
||||
onPress={() => navigation.navigate('LogoSourceSettings')}
|
||||
isLast={true}
|
||||
isTablet={isTablet}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -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<string | null>(null);
|
||||
const [tmdbBanner, setTmdbBanner] = useState<string | null>(null);
|
||||
const [loadingLogos, setLoadingLogos] = useState(true);
|
||||
const [previewLanguage, setPreviewLanguage] = useState<string>('');
|
||||
const [isPreviewFallback, setIsPreviewFallback] = useState<boolean>(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 (
|
||||
<View style={[styles.exampleImage, styles.loadingContainer]}>
|
||||
<ActivityIndicator size="small" color={currentTheme.colors.primary} />
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<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>
|
||||
);
|
||||
};
|
||||
|
||||
// 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 = () => {
|
|||
<Text style={[styles.languageButtonText, { color: currentTheme.colors.white }]}>Change</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
|
||||
{/* Logo Preview */}
|
||||
<View style={styles.divider} />
|
||||
|
||||
<Text style={[styles.settingTitle, { color: currentTheme.colors.text, marginBottom: 8 }]}>Logo Preview</Text>
|
||||
<Text style={[styles.settingDescription, { color: currentTheme.colors.mediumEmphasis, marginBottom: 12 }]}>
|
||||
Preview shows how localized logos will appear in the selected language.
|
||||
</Text>
|
||||
|
||||
{/* Show selector */}
|
||||
<Text style={[styles.selectorLabel, { color: currentTheme.colors.mediumEmphasis }]}>Example:</Text>
|
||||
<ScrollView
|
||||
horizontal
|
||||
showsHorizontalScrollIndicator={false}
|
||||
contentContainerStyle={styles.showsScrollContent}
|
||||
style={styles.showsScrollView}
|
||||
>
|
||||
{EXAMPLE_SHOWS.map((show) => (
|
||||
<TouchableOpacity
|
||||
key={show.imdbId}
|
||||
style={[
|
||||
styles.showItem,
|
||||
{ backgroundColor: currentTheme.colors.elevation1 },
|
||||
selectedShow.imdbId === show.imdbId && [styles.selectedShowItem, { borderColor: currentTheme.colors.primary }]
|
||||
]}
|
||||
onPress={() => handleShowSelect(show)}
|
||||
activeOpacity={0.7}
|
||||
>
|
||||
<Text
|
||||
style={[
|
||||
styles.showItemText,
|
||||
{ color: currentTheme.colors.mediumEmphasis },
|
||||
selectedShow.imdbId === show.imdbId && [styles.selectedShowItemText, { color: currentTheme.colors.white }]
|
||||
]}
|
||||
>
|
||||
{show.name}
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
))}
|
||||
</ScrollView>
|
||||
|
||||
{/* Preview card */}
|
||||
<View style={[styles.logoPreviewCard, { backgroundColor: currentTheme.colors.elevation1 }]}>
|
||||
{renderLogoExample(tmdbLogo, tmdbBanner, loadingLogos)}
|
||||
{tmdbLogo && (
|
||||
<Text style={[styles.logoSourceLabel, { color: currentTheme.colors.mediumEmphasis }]}>
|
||||
{`Language: ${(previewLanguage || '').toUpperCase() || 'N/A'}${isPreviewFallback ? ' (fallback to available)' : ''}`}
|
||||
</Text>
|
||||
)}
|
||||
</View>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
|
|
@ -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;
|
||||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
Loading…
Reference in a new issue