diff --git a/src/components/home/FeaturedContent.tsx b/src/components/home/FeaturedContent.tsx index 54ceec14..a0d337fa 100644 --- a/src/components/home/FeaturedContent.tsx +++ b/src/components/home/FeaturedContent.tsx @@ -49,6 +49,10 @@ const { width, height } = Dimensions.get('window'); // Utility to determine if device is tablet-sized const isTablet = width >= 768; +// Simple perf timer helper +const nowMs = () => Date.now(); +const since = (start: number) => `${(nowMs() - start).toFixed(0)}ms`; + const NoFeaturedContent = () => { const navigation = useNavigation>(); const { currentTheme } = useTheme(); @@ -142,6 +146,19 @@ const FeaturedContent = ({ featuredContent, isSaved, handleSaveToLibrary, loadin const [logoLoadError, setLogoLoadError] = useState(false); // Add a ref to track logo fetch in progress const logoFetchInProgress = useRef(false); + const firstRenderTsRef = useRef(nowMs()); + const lastContentChangeTsRef = useRef(0); + + // Initial diagnostics + useEffect(() => { + logger.info('[FeaturedContent] mounted', { + isTablet, + screen: { width, height }, + }); + return () => { + logger.info('[FeaturedContent] unmounted'); + }; + }, []); // Enhanced poster transition animations const posterScale = useSharedValue(1); @@ -185,6 +202,8 @@ const FeaturedContent = ({ featuredContent, isSaved, handleSaveToLibrary, loadin // Preload the image const preloadImage = async (url: string): Promise => { + const t0 = nowMs(); + logger.debug('[FeaturedContent] preloadImage:start', { url }); // Skip if already cached to prevent redundant prefetch if (imageCache[url]) return true; @@ -205,10 +224,12 @@ const FeaturedContent = ({ featuredContent, isSaved, handleSaveToLibrary, loadin timeout, ]); imageCache[url] = true; + logger.debug('[FeaturedContent] preloadImage:success', { url, duration: since(t0) }); return true; } catch (error) { // Clear any partial cache entry on error delete imageCache[url]; + logger.warn('[FeaturedContent] preloadImage:error', { url, duration: since(t0), error: String(error) }); return false; } }; @@ -224,6 +245,8 @@ const FeaturedContent = ({ featuredContent, isSaved, handleSaveToLibrary, loadin const fetchLogo = async () => { logoFetchInProgress.current = true; + const t0 = nowMs(); + logger.info('[FeaturedContent] fetchLogo:start', { id: featuredContent?.id, type: featuredContent?.type }); try { const contentId = featuredContent.id; @@ -234,8 +257,10 @@ const FeaturedContent = ({ featuredContent, isSaved, handleSaveToLibrary, loadin const logoPreference = settings.logoSourcePreference || 'metahub'; const preferredLanguage = settings.tmdbLanguagePreference || 'en'; - // Reset state for new fetch - setLogoUrl(null); + // Reset state for new fetch only if switching to a different item + if (prevContentIdRef.current !== contentId) { + setLogoUrl(null); + } setLogoLoadError(false); // Extract IDs @@ -274,6 +299,7 @@ const FeaturedContent = ({ featuredContent, isSaved, handleSaveToLibrary, loadin let fallbackAttempted = false; // --- Logo Fetching Logic --- + logger.debug('[FeaturedContent] fetchLogo:ids', { imdbId, tmdbId, preference: logoPreference, lang: preferredLanguage }); if (logoPreference === 'metahub') { // Primary: Metahub (needs imdbId) @@ -281,9 +307,11 @@ const FeaturedContent = ({ featuredContent, isSaved, handleSaveToLibrary, loadin primaryAttempted = true; const metahubUrl = `https://images.metahub.space/logo/medium/${imdbId}/img`; try { + const tHead = nowMs(); const response = await fetch(metahubUrl, { method: 'HEAD' }); if (response.ok) { finalLogoUrl = metahubUrl; + logger.debug('[FeaturedContent] fetchLogo:metahub:ok', { url: metahubUrl, duration: since(tHead) }); } } catch (error) { /* Log if needed */ } } @@ -293,9 +321,11 @@ const FeaturedContent = ({ featuredContent, isSaved, handleSaveToLibrary, loadin fallbackAttempted = true; try { const tmdbService = TMDBService.getInstance(); + const tTmdb = nowMs(); const logoUrl = await tmdbService.getContentLogo(tmdbType, tmdbId, preferredLanguage); if (logoUrl) { finalLogoUrl = logoUrl; + logger.debug('[FeaturedContent] fetchLogo:tmdb:fallback:ok', { url: logoUrl, duration: since(tTmdb) }); } } catch (error) { /* Log if needed */ } } @@ -306,9 +336,11 @@ const FeaturedContent = ({ featuredContent, isSaved, handleSaveToLibrary, loadin primaryAttempted = true; try { const tmdbService = TMDBService.getInstance(); + const tTmdb = nowMs(); const logoUrl = await tmdbService.getContentLogo(tmdbType, tmdbId, preferredLanguage); if (logoUrl) { finalLogoUrl = logoUrl; + logger.debug('[FeaturedContent] fetchLogo:tmdb:ok', { url: logoUrl, duration: since(tTmdb) }); } } catch (error) { /* Log if needed */ } } @@ -318,9 +350,11 @@ const FeaturedContent = ({ featuredContent, isSaved, handleSaveToLibrary, loadin fallbackAttempted = true; const metahubUrl = `https://images.metahub.space/logo/medium/${imdbId}/img`; try { + const tHead = nowMs(); const response = await fetch(metahubUrl, { method: 'HEAD' }); if (response.ok) { finalLogoUrl = metahubUrl; + logger.debug('[FeaturedContent] fetchLogo:metahub:fallback:ok', { url: metahubUrl, duration: since(tHead) }); } } catch (error) { /* Log if needed */ } } @@ -329,18 +363,22 @@ 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) }); } 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) }); } else { // No logo found from any source setLogoLoadError(true); + logger.warn('[FeaturedContent] fetchLogo:none', { id: contentId, primaryAttempted, fallbackAttempted, duration: since(t0) }); // logger.warn(`[FeaturedContent] No logo found for ${contentData.name} (${contentId}) with preference ${logoPreference}. Primary attempted: ${primaryAttempted}, Fallback attempted: ${fallbackAttempted}`); } } catch (error) { // logger.error('[FeaturedContent] Error in fetchLogo:', error); setLogoLoadError(true); + logger.error('[FeaturedContent] fetchLogo:error', { error: String(error), duration: since(t0) }); } finally { logoFetchInProgress.current = false; } @@ -357,6 +395,8 @@ const FeaturedContent = ({ featuredContent, isSaved, handleSaveToLibrary, loadin const posterUrl = featuredContent.banner || featuredContent.poster; const contentId = featuredContent.id; const isContentChange = contentId !== prevContentIdRef.current; + const t0 = nowMs(); + logger.info('[FeaturedContent] content:update', { id: contentId, isContentChange, posterUrlExists: Boolean(posterUrl), sinceMount: since(firstRenderTsRef.current) }); // Enhanced content change detection and animations if (isContentChange) { @@ -405,6 +445,7 @@ const FeaturedContent = ({ featuredContent, isSaved, handleSaveToLibrary, loadin // Load poster with enhanced transition if (posterUrl) { + const tPoster = nowMs(); const posterSuccess = await preloadImage(posterUrl); if (posterSuccess) { // Animate in new poster with scale and fade @@ -420,6 +461,7 @@ const FeaturedContent = ({ featuredContent, isSaved, handleSaveToLibrary, loadin duration: 600, easing: Easing.out(Easing.cubic) }); + logger.debug('[FeaturedContent] poster:ready', { id: contentId, duration: since(tPoster) }); // Animate content back in with delay contentOpacity.value = withDelay(200, withTiming(1, { @@ -435,16 +477,20 @@ const FeaturedContent = ({ featuredContent, isSaved, handleSaveToLibrary, loadin // Load logo if available with enhanced timing if (logoUrl) { + const tLogo = nowMs(); const logoSuccess = await preloadImage(logoUrl); if (logoSuccess) { logoOpacity.value = withDelay(500, withTiming(1, { duration: 600, easing: Easing.out(Easing.cubic) })); + logger.debug('[FeaturedContent] logo:ready', { id: contentId, duration: since(tLogo) }); } else { setLogoLoadError(true); + logger.warn('[FeaturedContent] logo:failed', { id: contentId, duration: since(tLogo) }); } } + logger.info('[FeaturedContent] images:load:done', { id: contentId, total: since(t0) }); }; loadImages(); @@ -453,6 +499,7 @@ const FeaturedContent = ({ featuredContent, isSaved, handleSaveToLibrary, loadin const onLogoLoadError = () => { setLogoLoaded(true); // Treat error as "loaded" to stop spinner setLogoError(true); + logger.warn('[FeaturedContent] logo:onError', { id: featuredContent?.id, url: logoUrl }); }; const handleInfoPress = () => { @@ -464,13 +511,15 @@ const FeaturedContent = ({ featuredContent, isSaved, handleSaveToLibrary, loadin } }; - // Show skeleton while loading to avoid empty state flash and sluggish feel - if (loading) { + // Show skeleton only if we're loading AND no content is available yet + if (loading && !featuredContent) { + logger.debug('[FeaturedContent] render:loading', { sinceMount: since(firstRenderTsRef.current) }); return ; } if (!featuredContent) { // Suppress empty state while loading to avoid flash on startup/hydration + logger.debug('[FeaturedContent] render:no-featured-content', { sinceMount: since(firstRenderTsRef.current) }); return ; } diff --git a/src/components/metadata/MetadataSourceSelector.tsx b/src/components/metadata/MetadataSourceSelector.tsx index acff96a1..c34f516c 100644 --- a/src/components/metadata/MetadataSourceSelector.tsx +++ b/src/components/metadata/MetadataSourceSelector.tsx @@ -31,8 +31,6 @@ interface MetadataSourceSelectorProps { contentType: string; onSourceChange: (sourceId: string, sourceType: 'addon' | 'tmdb') => void; disabled?: boolean; - enableComplementary?: boolean; - onComplementaryToggle?: (enabled: boolean) => void; } const MetadataSourceSelector: React.FC = ({ @@ -41,8 +39,6 @@ const MetadataSourceSelector: React.FC = ({ contentType, onSourceChange, disabled = false, - enableComplementary = false, - onComplementaryToggle, }) => { const { currentTheme } = useTheme(); const [isVisible, setIsVisible] = useState(false); @@ -292,46 +288,7 @@ const MetadataSourceSelector: React.FC = ({ - {/* Complementary Metadata Toggle */} - - - - - - Complementary Metadata - - - Fetch missing data from other sources - - - - onComplementaryToggle?.(!enableComplementary)} - activeOpacity={0.7} - > - - - + {loading ? ( @@ -561,51 +518,6 @@ const styles = StyleSheet.create({ checkContainer: { padding: 4, }, - complementaryToggle: { - flexDirection: 'row', - alignItems: 'center', - justifyContent: 'space-between', - paddingHorizontal: 20, - paddingVertical: 16, - borderBottomWidth: 1, - borderBottomColor: 'rgba(255, 255, 255, 0.08)', - }, - toggleContent: { - flexDirection: 'row', - alignItems: 'center', - flex: 1, - }, - toggleText: { - marginLeft: 16, - flex: 1, - }, - toggleTitle: { - fontSize: 16, - fontWeight: '600', - marginBottom: 2, - }, - toggleDescription: { - fontSize: 13, - lineHeight: 18, - opacity: 0.8, - }, - toggleSwitch: { - width: 44, - height: 24, - borderRadius: 12, - justifyContent: 'center', - paddingHorizontal: 2, - }, - toggleThumb: { - width: 18, - height: 18, - borderRadius: 9, - shadowColor: '#000', - shadowOffset: { width: 0, height: 2 }, - shadowOpacity: 0.2, - shadowRadius: 2, - elevation: 2, - }, }); export default MetadataSourceSelector; \ No newline at end of file diff --git a/src/hooks/useFeaturedContent.ts b/src/hooks/useFeaturedContent.ts index 0997cba3..2f4fe198 100644 --- a/src/hooks/useFeaturedContent.ts +++ b/src/hooks/useFeaturedContent.ts @@ -24,6 +24,7 @@ const persistentStore = { // Cache timeout in milliseconds (e.g., 5 minutes) const CACHE_TIMEOUT = 5 * 60 * 1000; const STORAGE_KEY = 'featured_content_cache_v1'; +const DISABLE_CACHE = true; export function useFeaturedContent() { const [featuredContent, setFeaturedContent] = useState(persistentStore.featuredContent); @@ -52,32 +53,42 @@ export function useFeaturedContent() { }, []); const loadFeaturedContent = useCallback(async (forceRefresh = false) => { + const t0 = Date.now(); + logger.info('[useFeaturedContent] load:start', { forceRefresh, contentSource, selectedCatalogsCount: (selectedCatalogs || []).length }); // First, ensure contentSource matches current settings (could be outdated due to async updates) if (contentSource !== settings.featuredContentSource) { - console.log(`Updating content source from ${contentSource} to ${settings.featuredContentSource}`); + logger.debug('[useFeaturedContent] load:source-mismatch', { from: contentSource, to: settings.featuredContentSource }); setContentSource(settings.featuredContentSource); // We return here and let the effect triggered by contentSource change handle the loading return; } - // Check if we should use cached data + // Check if we should use cached data (disabled if DISABLE_CACHE) const now = Date.now(); const cacheAge = now - persistentStore.lastFetchTime; - - if (!forceRefresh && - persistentStore.featuredContent && - persistentStore.allFeaturedContent.length > 0 && - cacheAge < CACHE_TIMEOUT) { - // Use cached data - console.log('Using cached featured content data'); - setFeaturedContent(persistentStore.featuredContent); - setAllFeaturedContent(persistentStore.allFeaturedContent); - setLoading(false); - persistentStore.isFirstLoad = false; - return; + logger.debug('[useFeaturedContent] cache:status', { + disabled: DISABLE_CACHE, + hasFeatured: Boolean(persistentStore.featuredContent), + allCount: persistentStore.allFeaturedContent?.length || 0, + cacheAgeMs: cacheAge, + timeoutMs: CACHE_TIMEOUT, + }); + if (!DISABLE_CACHE) { + if (!forceRefresh && + persistentStore.featuredContent && + persistentStore.allFeaturedContent.length > 0 && + cacheAge < CACHE_TIMEOUT) { + // Use cached data + logger.info('[useFeaturedContent] cache:use', { duration: `${Date.now() - t0}ms` }); + setFeaturedContent(persistentStore.featuredContent); + setAllFeaturedContent(persistentStore.allFeaturedContent); + setLoading(false); + persistentStore.isFirstLoad = false; + return; + } } - console.log(`Loading featured content from ${contentSource}`); + logger.info('[useFeaturedContent] fetch:start', { source: contentSource }); setLoading(true); cleanup(); abortControllerRef.current = new AbortController(); @@ -88,7 +99,9 @@ export function useFeaturedContent() { if (contentSource === 'tmdb') { // Load from TMDB trending + const tTmdb = Date.now(); const trendingResults = await tmdbService.getTrending('movie', 'day'); + logger.info('[useFeaturedContent] tmdb:trending', { count: trendingResults?.length || 0, duration: `${Date.now() - tTmdb}ms` }); if (signal.aborted) return; @@ -115,6 +128,7 @@ export function useFeaturedContent() { }); // Then fetch logos for each item + const tLogos = Date.now(); formattedContent = await Promise.all( preFormattedContent.map(async (item) => { try { @@ -135,10 +149,13 @@ export function useFeaturedContent() { } }) ); + logger.info('[useFeaturedContent] tmdb:logos', { count: formattedContent.length, duration: `${Date.now() - tLogos}ms` }); } } else { // Load from installed catalogs + const tCats = Date.now(); const catalogs = await catalogService.getHomeCatalogs(); + logger.info('[useFeaturedContent] catalogs:list', { count: catalogs?.length || 0, duration: `${Date.now() - tCats}ms` }); if (signal.aborted) return; @@ -153,14 +170,17 @@ export function useFeaturedContent() { return selectedCatalogs.includes(catalogId); }) : catalogs; // Use all catalogs if none specifically selected + logger.debug('[useFeaturedContent] catalogs:filtered', { filteredCount: filteredCatalogs.length, selectedCount: selectedCatalogs?.length || 0 }); // Flatten all catalog items into a single array, filter out items without posters + const tFlat = Date.now(); const allItems = filteredCatalogs.flatMap(catalog => catalog.items) .filter(item => item.poster) .filter((item, index, self) => // Remove duplicates based on ID index === self.findIndex(t => t.id === item.id) ); + logger.info('[useFeaturedContent] catalogs:items', { total: allItems.length, duration: `${Date.now() - tFlat}ms` }); // Sort by popular, newest, etc. (possibly enhanced later) formattedContent = allItems.sort(() => Math.random() - 0.5).slice(0, 10); @@ -171,6 +191,7 @@ export function useFeaturedContent() { // Safety guard: if nothing came back within a reasonable time, stop loading if (!formattedContent || formattedContent.length === 0) { + logger.warn('[useFeaturedContent] results:empty'); // Fall back to any cached featured item so UI can render something const cachedJson = await AsyncStorage.getItem(STORAGE_KEY).catch(() => null); if (cachedJson) { @@ -180,14 +201,17 @@ export function useFeaturedContent() { formattedContent = Array.isArray(parsed.allFeaturedContent) && parsed.allFeaturedContent.length > 0 ? parsed.allFeaturedContent : [parsed.featuredContent]; + logger.info('[useFeaturedContent] fallback:storage', { count: formattedContent.length }); } } catch {} } } - // Update persistent store with the new data + // Update persistent store with the new data (no lastFetchTime when cache disabled) persistentStore.allFeaturedContent = formattedContent; - persistentStore.lastFetchTime = now; + if (!DISABLE_CACHE) { + persistentStore.lastFetchTime = now; + } persistentStore.isFirstLoad = false; setAllFeaturedContent(formattedContent); @@ -196,40 +220,51 @@ export function useFeaturedContent() { persistentStore.featuredContent = formattedContent[0]; setFeaturedContent(formattedContent[0]); currentIndexRef.current = 0; - // Persist cache for fast startup - try { - await AsyncStorage.setItem( - STORAGE_KEY, - JSON.stringify({ - ts: now, - featuredContent: formattedContent[0], - allFeaturedContent: formattedContent, - }) - ); - } catch {} + // Persist cache for fast startup (skipped when cache disabled) + if (!DISABLE_CACHE) { + try { + await AsyncStorage.setItem( + STORAGE_KEY, + JSON.stringify({ + ts: now, + featuredContent: formattedContent[0], + allFeaturedContent: formattedContent, + }) + ); + logger.debug('[useFeaturedContent] cache:written', { firstId: formattedContent[0]?.id }); + } catch {} + } } else { persistentStore.featuredContent = null; setFeaturedContent(null); - // Clear persisted cache on empty - try { await AsyncStorage.removeItem(STORAGE_KEY); } catch {} + // Clear persisted cache on empty (skipped when cache disabled) + if (!DISABLE_CACHE) { + try { await AsyncStorage.removeItem(STORAGE_KEY); } catch {} + } } } catch (error) { if (signal.aborted) { - logger.info('Featured content fetch aborted'); + logger.info('[useFeaturedContent] fetch:aborted'); } else { - logger.error('Failed to load featured content:', error); + logger.error('[useFeaturedContent] fetch:error', { error: String(error) }); } setFeaturedContent(null); setAllFeaturedContent([]); } finally { if (!signal.aborted) { setLoading(false); + logger.info('[useFeaturedContent] load:done', { duration: `${Date.now() - t0}ms` }); } } }, [cleanup, genreMap, loadingGenres, contentSource, selectedCatalogs]); // Hydrate from persisted cache immediately for instant render useEffect(() => { + if (DISABLE_CACHE) { + // Skip hydration entirely + logger.debug('[useFeaturedContent] hydrate:skipped'); + return; + } let cancelled = false; (async () => { try { @@ -245,6 +280,7 @@ export function useFeaturedContent() { setFeaturedContent(parsed.featuredContent); setAllFeaturedContent(persistentStore.allFeaturedContent); setLoading(false); + logger.info('[useFeaturedContent] hydrate:storage', { allCount: persistentStore.allFeaturedContent.length }); } } catch {} })(); @@ -268,6 +304,7 @@ export function useFeaturedContent() { // Force refresh if settings changed during app restart if (settingsChanged) { + logger.info('[useFeaturedContent] settings:changed', { source: settings.featuredContentSource, selectedCount: settings.selectedHeroCatalogs?.length || 0 }); loadFeaturedContent(true); } }, [settings, loadFeaturedContent]); @@ -278,9 +315,7 @@ export function useFeaturedContent() { // Only refresh if current content source is different from settings // This prevents duplicate refreshes when HomeScreen also handles this event if (contentSource !== settings.featuredContentSource) { - console.log('Content source changed, refreshing featured content'); - console.log('Current content source:', contentSource); - console.log('New settings source:', settings.featuredContentSource); + logger.info('[useFeaturedContent] event:content-source-changed', { from: contentSource, to: settings.featuredContentSource }); // Content source will be updated in the next render cycle due to state updates // No need to call loadFeaturedContent here as it will be triggered by contentSource change } else if ( @@ -288,7 +323,7 @@ export function useFeaturedContent() { JSON.stringify(selectedCatalogs) !== JSON.stringify(settings.selectedHeroCatalogs) ) { // Only refresh if using catalogs and selected catalogs changed - console.log('Selected catalogs changed, refreshing featured content'); + logger.info('[useFeaturedContent] event:selected-catalogs-changed'); loadFeaturedContent(true); } }; diff --git a/src/services/tmdbService.ts b/src/services/tmdbService.ts index db3894e3..c0431f56 100644 --- a/src/services/tmdbService.ts +++ b/src/services/tmdbService.ts @@ -2,7 +2,7 @@ import axios from 'axios'; import AsyncStorage from '@react-native-async-storage/async-storage'; // TMDB API configuration -const DEFAULT_API_KEY = '439c478a771f35c05022f9feabcca01c'; +const DEFAULT_API_KEY = 'd131017ccc6e5462a81c9304d21476de'; const BASE_URL = 'https://api.themoviedb.org/3'; const TMDB_API_KEY_STORAGE_KEY = 'tmdb_api_key'; const USE_CUSTOM_TMDB_API_KEY = 'use_custom_tmdb_api_key';