diff --git a/app.json b/app.json index e58c6d94..21407fc2 100644 --- a/app.json +++ b/app.json @@ -82,8 +82,8 @@ ], "updates": { "enabled": true, - "checkAutomatically": "ON_LOAD", - "fallbackToCacheTimeout": 0, + "checkAutomatically": "ON_ERROR", + "fallbackToCacheTimeout": 30000, "url": "https://grim-reyna-tapframe-69970143.koyeb.app/api/manifest" }, "runtimeVersion": "0.6.0-beta.10" diff --git a/src/components/calendar/CalendarSection.tsx b/src/components/calendar/CalendarSection.tsx index 2d443e13..337598d2 100644 --- a/src/components/calendar/CalendarSection.tsx +++ b/src/components/calendar/CalendarSection.tsx @@ -85,7 +85,7 @@ export const CalendarSection: React.FC = ({ // Process episodes to identify dates with content useEffect(() => { - console.log(`[CalendarSection] Processing ${episodes.length} episodes for calendar dots`); + if (__DEV__) console.log(`[CalendarSection] Processing ${episodes.length} episodes for calendar dots`); const dateMap: { [key: string]: boolean } = {}; episodes.forEach(episode => { @@ -96,7 +96,7 @@ export const CalendarSection: React.FC = ({ } }); - console.log(`[CalendarSection] Found ${Object.keys(dateMap).length} unique dates with episodes`); + if (__DEV__) console.log(`[CalendarSection] Found ${Object.keys(dateMap).length} unique dates with episodes`); setDatesWithEpisodes(dateMap); }, [episodes]); diff --git a/src/components/home/ContentItem.tsx b/src/components/home/ContentItem.tsx index e4f748d1..3893384b 100644 --- a/src/components/home/ContentItem.tsx +++ b/src/components/home/ContentItem.tsx @@ -174,7 +174,7 @@ const ContentItem = ({ item, onPress }: ContentItemProps) => { setImageError(false); }} onError={(error) => { - console.warn('Image load error for:', item.poster, error); + if (__DEV__) console.warn('Image load error for:', item.poster, error); // Try fallback URL on first error if (retryCount === 0 && item.poster && !item.poster.includes('metahub.space')) { setRetryCount(1); diff --git a/src/components/metadata/CastDetailsModal.tsx b/src/components/metadata/CastDetailsModal.tsx index 20ba8246..132b80a7 100644 --- a/src/components/metadata/CastDetailsModal.tsx +++ b/src/components/metadata/CastDetailsModal.tsx @@ -91,7 +91,7 @@ export const CastDetailsModal: React.FC = ({ setPersonDetails(details); setHasFetched(true); } catch (error) { - console.error('Error fetching person details:', error); + if (__DEV__) console.error('Error fetching person details:', error); } finally { setLoading(false); } diff --git a/src/components/metadata/MoreLikeThisSection.tsx b/src/components/metadata/MoreLikeThisSection.tsx index f2df2e4e..727cb5cf 100644 --- a/src/components/metadata/MoreLikeThisSection.tsx +++ b/src/components/metadata/MoreLikeThisSection.tsx @@ -79,7 +79,7 @@ export const MoreLikeThisSection: React.FC = ({ throw new Error('Could not find Stremio ID'); } } catch (error) { - console.error('Error navigating to recommendation:', error); + if (__DEV__) console.error('Error navigating to recommendation:', error); Alert.alert( 'Error', 'Unable to load this content. Please try again later.', diff --git a/src/components/metadata/SeriesContent.tsx b/src/components/metadata/SeriesContent.tsx index d3efac1f..c5a7eb54 100644 --- a/src/components/metadata/SeriesContent.tsx +++ b/src/components/metadata/SeriesContent.tsx @@ -1,7 +1,7 @@ import React, { useEffect, useState, useRef } from 'react'; import { View, Text, StyleSheet, ScrollView, TouchableOpacity, ActivityIndicator, Dimensions, useWindowDimensions, useColorScheme, FlatList } from 'react-native'; import { Image } from 'expo-image'; -import MaterialIcons from 'react-native-vector-icons/MaterialIcons'; +import { MaterialIcons } from '@expo/vector-icons'; import { LinearGradient } from 'expo-linear-gradient'; import { FlashList, FlashListRef } from '@shopify/flash-list'; import { useTheme } from '../../contexts/ThemeContext'; @@ -70,10 +70,10 @@ export const SeriesContent: React.FC = ({ const savedMode = await AsyncStorage.getItem(`season_view_mode_${metadata.id}`); if (savedMode === 'text' || savedMode === 'posters') { setSeasonViewMode(savedMode); - console.log('[SeriesContent] Loaded saved view mode:', savedMode, 'for show:', metadata.id); + if (__DEV__) console.log('[SeriesContent] Loaded saved view mode:', savedMode, 'for show:', metadata.id); } } catch (error) { - console.log('[SeriesContent] Error loading view mode preference:', error); + if (__DEV__) console.log('[SeriesContent] Error loading view mode preference:', error); } } }; @@ -99,7 +99,7 @@ export const SeriesContent: React.FC = ({ setSeasonViewMode(newMode); if (metadata?.id) { AsyncStorage.setItem(`season_view_mode_${metadata.id}`, newMode).catch(error => { - console.log('[SeriesContent] Error saving view mode preference:', error); + if (__DEV__) console.log('[SeriesContent] Error saving view mode preference:', error); }); } }; @@ -328,7 +328,7 @@ export const SeriesContent: React.FC = ({ return null; } - console.log('[SeriesContent] renderSeasonSelector called, current view mode:', seasonViewMode); + if (__DEV__) console.log('[SeriesContent] renderSeasonSelector called, current view mode:', seasonViewMode); const seasons = Object.keys(groupedEpisodes).map(Number).sort((a, b) => a - b); @@ -357,7 +357,7 @@ export const SeriesContent: React.FC = ({ onPress={() => { const newMode = seasonViewMode === 'posters' ? 'text' : 'posters'; updateViewMode(newMode); - console.log('[SeriesContent] View mode changed to:', newMode, 'Current ref value:', seasonViewMode); + if (__DEV__) console.log('[SeriesContent] View mode changed to:', newMode, 'Current ref value:', seasonViewMode); }} activeOpacity={0.7} > @@ -398,7 +398,7 @@ export const SeriesContent: React.FC = ({ if (seasonViewMode === 'text') { // Text-only view - console.log('[SeriesContent] Rendering text view for season:', season, 'View mode ref:', seasonViewMode); + if (__DEV__) console.log('[SeriesContent] Rendering text view for season:', season, 'View mode ref:', seasonViewMode); return ( = ({ } // Poster view (current implementation) - console.log('[SeriesContent] Rendering poster view for season:', season, 'View mode ref:', seasonViewMode); + if (__DEV__) console.log('[SeriesContent] Rendering poster view for season:', season, 'View mode ref:', seasonViewMode); return ( { const newScale = Math.max(1, Math.min(lastZoomScale * scale, 1.1)); setZoomScale(newScale); if (DEBUG_MODE) { - logger.log(`[AndroidVideoPlayer] Center Zoom: ${newScale.toFixed(2)}x`); + if (__DEV__) logger.log(`[AndroidVideoPlayer] Center Zoom: ${newScale.toFixed(2)}x`); } }; @@ -321,7 +321,7 @@ const AndroidVideoPlayer: React.FC = () => { if (event.nativeEvent.state === State.END) { setLastZoomScale(zoomScale); if (DEBUG_MODE) { - logger.log(`[AndroidVideoPlayer] Pinch ended - saved scale: ${zoomScale.toFixed(2)}x`); + if (__DEV__) logger.log(`[AndroidVideoPlayer] Pinch ended - saved scale: ${zoomScale.toFixed(2)}x`); } } }; @@ -331,7 +331,7 @@ const AndroidVideoPlayer: React.FC = () => { setZoomScale(targetZoom); setLastZoomScale(targetZoom); if (DEBUG_MODE) { - logger.log(`[AndroidVideoPlayer] Zoom reset to ${targetZoom}x (16:9: ${is16by9Content})`); + if (__DEV__) logger.log(`[AndroidVideoPlayer] Zoom reset to ${targetZoom}x (16:9: ${is16by9Content})`); } }; @@ -345,7 +345,7 @@ const AndroidVideoPlayer: React.FC = () => { ); setCustomVideoStyles(styles); if (DEBUG_MODE) { - logger.log(`[AndroidVideoPlayer] Screen dimensions changed, recalculated styles:`, styles); + if (__DEV__) logger.log(`[AndroidVideoPlayer] Screen dimensions changed, recalculated styles:`, styles); } } }, [screenDimensions, videoAspectRatio]); @@ -439,7 +439,7 @@ const AndroidVideoPlayer: React.FC = () => { // Fallback: ensure animation completes even if something goes wrong setTimeout(() => { if (!isOpeningAnimationComplete) { - logger.warn('[AndroidVideoPlayer] Opening animation fallback triggered'); + if (__DEV__) logger.warn('[AndroidVideoPlayer] Opening animation fallback triggered'); setIsOpeningAnimationComplete(true); openingScaleAnim.setValue(1); openingFadeAnim.setValue(1); @@ -452,40 +452,40 @@ const AndroidVideoPlayer: React.FC = () => { const loadWatchProgress = async () => { if (id && type) { try { - logger.log(`[AndroidVideoPlayer] Loading watch progress for ${type}:${id}${episodeId ? `:${episodeId}` : ''}`); + if (__DEV__) logger.log(`[AndroidVideoPlayer] Loading watch progress for ${type}:${id}${episodeId ? `:${episodeId}` : ''}`); const savedProgress = await storageService.getWatchProgress(id, type, episodeId); - logger.log(`[AndroidVideoPlayer] Saved progress:`, savedProgress); + if (__DEV__) logger.log(`[AndroidVideoPlayer] Saved progress:`, savedProgress); if (savedProgress) { const progressPercent = (savedProgress.currentTime / savedProgress.duration) * 100; - logger.log(`[AndroidVideoPlayer] Progress: ${progressPercent.toFixed(1)}% (${savedProgress.currentTime}/${savedProgress.duration})`); + if (__DEV__) logger.log(`[AndroidVideoPlayer] Progress: ${progressPercent.toFixed(1)}% (${savedProgress.currentTime}/${savedProgress.duration})`); if (progressPercent < 85) { setResumePosition(savedProgress.currentTime); setSavedDuration(savedProgress.duration); - logger.log(`[AndroidVideoPlayer] Set resume position to: ${savedProgress.currentTime} of ${savedProgress.duration}`); + if (__DEV__) logger.log(`[AndroidVideoPlayer] Set resume position to: ${savedProgress.currentTime} of ${savedProgress.duration}`); if (appSettings.alwaysResume) { // Only prepare auto-resume state and seek when AlwaysResume is enabled setInitialPosition(savedProgress.currentTime); initialSeekTargetRef.current = savedProgress.currentTime; - logger.log(`[AndroidVideoPlayer] AlwaysResume enabled. Auto-seeking to ${savedProgress.currentTime}`); + if (__DEV__) logger.log(`[AndroidVideoPlayer] AlwaysResume enabled. Auto-seeking to ${savedProgress.currentTime}`); seekToTime(savedProgress.currentTime); } else { // Do not set initialPosition; start from beginning with no auto-seek setShowResumeOverlay(true); - logger.log(`[AndroidVideoPlayer] AlwaysResume disabled. Not auto-seeking; overlay shown (if enabled)`); + if (__DEV__) logger.log(`[AndroidVideoPlayer] AlwaysResume disabled. Not auto-seeking; overlay shown (if enabled)`); } } else { - logger.log(`[AndroidVideoPlayer] Progress too high (${progressPercent.toFixed(1)}%), not showing resume overlay`); + if (__DEV__) logger.log(`[AndroidVideoPlayer] Progress too high (${progressPercent.toFixed(1)}%), not showing resume overlay`); } } else { - logger.log(`[AndroidVideoPlayer] No saved progress found`); + if (__DEV__) logger.log(`[AndroidVideoPlayer] No saved progress found`); } } catch (error) { logger.error('[AndroidVideoPlayer] Error loading watch progress:', error); } } else { - logger.log(`[AndroidVideoPlayer] Missing id or type: id=${id}, type=${type}`); + if (__DEV__) logger.log(`[AndroidVideoPlayer] Missing id or type: id=${id}, type=${type}`); } }; loadWatchProgress(); @@ -545,7 +545,7 @@ const AndroidVideoPlayer: React.FC = () => { const timeInSeconds = Math.max(0, Math.min(rawSeconds, duration > 0 ? duration - END_EPSILON : rawSeconds)); if (videoRef.current && duration > 0 && !isSeeking.current) { if (DEBUG_MODE) { - logger.log(`[AndroidVideoPlayer] Seeking to ${timeInSeconds.toFixed(2)}s out of ${duration.toFixed(2)}s`); + if (__DEV__) logger.log(`[AndroidVideoPlayer] Seeking to ${timeInSeconds.toFixed(2)}s out of ${duration.toFixed(2)}s`); } isSeeking.current = true; @@ -779,7 +779,7 @@ const AndroidVideoPlayer: React.FC = () => { NativeModules.StatusBarManager.setHidden(true); } } catch (error) { - console.log('Immersive mode error:', error); + if (__DEV__) console.log('Immersive mode error:', error); } } }; diff --git a/src/components/player/VideoPlayer.tsx b/src/components/player/VideoPlayer.tsx index 9325d753..da4bb6aa 100644 --- a/src/components/player/VideoPlayer.tsx +++ b/src/components/player/VideoPlayer.tsx @@ -61,15 +61,17 @@ const VideoPlayer: React.FC = () => { // - Xprime streams on any platform // - Non-MKV files on iOS (unless forceVlc is set) const shouldUseAndroidPlayer = Platform.OS === 'android' || isXprimeStream || (Platform.OS === 'ios' && !isMkvFile && !forceVlc); - logger.log('[VideoPlayer] Player selection:', { - platform: Platform.OS, - isXprimeStream, - isMkvFile, - forceVlc: !!forceVlc, - selected: shouldUseAndroidPlayer ? 'AndroidVideoPlayer' : 'VLCPlayer', - streamProvider, - uri - }); + if (__DEV__) { + logger.log('[VideoPlayer] Player selection:', { + platform: Platform.OS, + isXprimeStream, + isMkvFile, + forceVlc: !!forceVlc, + selected: shouldUseAndroidPlayer ? 'AndroidVideoPlayer' : 'VLCPlayer', + streamProvider, + uri + }); + } if (shouldUseAndroidPlayer) { return ; } @@ -335,7 +337,7 @@ const VideoPlayer: React.FC = () => { const newScale = Math.max(1, Math.min(lastZoomScale * scale, 1.1)); setZoomScale(newScale); if (DEBUG_MODE) { - logger.log(`[VideoPlayer] Center Zoom: ${newScale.toFixed(2)}x`); + if (__DEV__) logger.log(`[VideoPlayer] Center Zoom: ${newScale.toFixed(2)}x`); } }; @@ -343,7 +345,7 @@ const VideoPlayer: React.FC = () => { if (event.nativeEvent.state === State.END) { setLastZoomScale(zoomScale); if (DEBUG_MODE) { - logger.log(`[VideoPlayer] Pinch ended - saved scale: ${zoomScale.toFixed(2)}x`); + if (__DEV__) logger.log(`[VideoPlayer] Pinch ended - saved scale: ${zoomScale.toFixed(2)}x`); } } }; @@ -353,7 +355,7 @@ const VideoPlayer: React.FC = () => { setZoomScale(targetZoom); setLastZoomScale(targetZoom); if (DEBUG_MODE) { - logger.log(`[VideoPlayer] Zoom reset to ${targetZoom}x (16:9: ${is16by9Content})`); + if (__DEV__) logger.log(`[VideoPlayer] Zoom reset to ${targetZoom}x (16:9: ${is16by9Content})`); } }; @@ -367,7 +369,7 @@ const VideoPlayer: React.FC = () => { ); setCustomVideoStyles(styles); if (DEBUG_MODE) { - logger.log(`[VideoPlayer] Screen dimensions changed, recalculated styles:`, styles); + if (__DEV__) logger.log(`[VideoPlayer] Screen dimensions changed, recalculated styles:`, styles); } } }, [effectiveDimensions, videoAspectRatio]); @@ -377,7 +379,7 @@ const VideoPlayer: React.FC = () => { const lockOrientation = async () => { try { await ScreenOrientation.lockAsync(ScreenOrientation.OrientationLock.LANDSCAPE); - logger.log('[VideoPlayer] Locked to landscape orientation'); + if (__DEV__) logger.log('[VideoPlayer] Locked to landscape orientation'); } catch (error) { logger.warn('[VideoPlayer] Failed to lock orientation:', error); } @@ -483,32 +485,36 @@ const VideoPlayer: React.FC = () => { const loadWatchProgress = async () => { if (id && type) { try { - logger.log(`[VideoPlayer] Loading watch progress for ${type}:${id}${episodeId ? `:${episodeId}` : ''}`); + if (__DEV__) { + logger.log(`[VideoPlayer] Loading watch progress for ${type}:${id}${episodeId ? `:${episodeId}` : ''}`); + } const savedProgress = await storageService.getWatchProgress(id, type, episodeId); - logger.log(`[VideoPlayer] Saved progress:`, savedProgress); + if (__DEV__) { + logger.log(`[VideoPlayer] Saved progress:`, savedProgress); + } if (savedProgress) { const progressPercent = (savedProgress.currentTime / savedProgress.duration) * 100; - logger.log(`[VideoPlayer] Progress: ${progressPercent.toFixed(1)}% (${savedProgress.currentTime}/${savedProgress.duration})`); + if (__DEV__) logger.log(`[VideoPlayer] Progress: ${progressPercent.toFixed(1)}% (${savedProgress.currentTime}/${savedProgress.duration})`); if (progressPercent < 85) { setResumePosition(savedProgress.currentTime); setSavedDuration(savedProgress.duration); - logger.log(`[VideoPlayer] Set resume position to: ${savedProgress.currentTime} of ${savedProgress.duration}`); + if (__DEV__) logger.log(`[VideoPlayer] Set resume position to: ${savedProgress.currentTime} of ${savedProgress.duration}`); if (appSettings.alwaysResume) { // Only prepare auto-resume state and seek when AlwaysResume is enabled setInitialPosition(savedProgress.currentTime); initialSeekTargetRef.current = savedProgress.currentTime; - logger.log(`[VideoPlayer] AlwaysResume enabled. Auto-seeking to ${savedProgress.currentTime}`); + if (__DEV__) logger.log(`[VideoPlayer] AlwaysResume enabled. Auto-seeking to ${savedProgress.currentTime}`); // Seek immediately after load seekToTime(savedProgress.currentTime); } else { // Do not set initialPosition; start from beginning with no auto-seek setShowResumeOverlay(true); - logger.log(`[VideoPlayer] AlwaysResume disabled. Not auto-seeking; overlay shown (if enabled)`); + if (__DEV__) logger.log(`[VideoPlayer] AlwaysResume disabled. Not auto-seeking; overlay shown (if enabled)`); } } else { - logger.log(`[VideoPlayer] Progress too high (${progressPercent.toFixed(1)}%), not showing resume overlay`); + if (__DEV__) logger.log(`[VideoPlayer] Progress too high (${progressPercent.toFixed(1)}%), not showing resume overlay`); } } else { logger.log(`[VideoPlayer] No saved progress found`); @@ -517,7 +523,7 @@ const VideoPlayer: React.FC = () => { logger.error('[VideoPlayer] Error loading watch progress:', error); } } else { - logger.log(`[VideoPlayer] Missing id or type: id=${id}, type=${type}`); + if (__DEV__) logger.log(`[VideoPlayer] Missing id or type: id=${id}, type=${type}`); } }; loadWatchProgress(); @@ -547,8 +553,7 @@ const VideoPlayer: React.FC = () => { clearInterval(progressSaveInterval); } - // HEATING FIX: Increase sync interval to 15 seconds to reduce CPU load - const syncInterval = 15000; // 15 seconds to prevent heating + const syncInterval = 20000; // 20s to further reduce CPU load const interval = setInterval(() => { saveWatchProgress(); @@ -560,7 +565,7 @@ const VideoPlayer: React.FC = () => { setProgressSaveInterval(null); }; } - }, [id, type, paused, currentTime, duration]); + }, [id, type, paused, duration]); useEffect(() => { return () => { @@ -597,7 +602,7 @@ const VideoPlayer: React.FC = () => { const timeInSeconds = Math.max(0, Math.min(rawSeconds, duration > 0 ? duration - END_EPSILON : rawSeconds)); if (vlcRef.current && duration > 0 && !isSeeking.current) { if (DEBUG_MODE) { - logger.log(`[VideoPlayer] Seeking to ${timeInSeconds.toFixed(2)}s out of ${duration.toFixed(2)}s`); + if (__DEV__) logger.log(`[VideoPlayer] Seeking to ${timeInSeconds.toFixed(2)}s out of ${duration.toFixed(2)}s`); } isSeeking.current = true; @@ -799,7 +804,7 @@ const VideoPlayer: React.FC = () => { NativeModules.StatusBarManager.setHidden(true); } } catch (error) { - console.log('Immersive mode error:', error); + if (__DEV__) console.log('Immersive mode error:', error); } } }; diff --git a/src/contexts/AccountContext.tsx b/src/contexts/AccountContext.tsx index 1e4021a5..5eab8480 100644 --- a/src/contexts/AccountContext.tsx +++ b/src/contexts/AccountContext.tsx @@ -32,13 +32,13 @@ export const AccountProvider: React.FC<{ children: React.ReactNode }> = ({ child if (u) { try { await syncService.migrateLocalScopeToUser(); - // Small yield to event loop - await new Promise(resolve => setTimeout(resolve, 50)); + // Longer yield to event loop to reduce CPU pressure + await new Promise(resolve => setTimeout(resolve, 100)); await syncService.subscribeRealtime(); - await new Promise(resolve => setTimeout(resolve, 50)); + await new Promise(resolve => setTimeout(resolve, 100)); // Pull first to hydrate local state, then push to avoid wiping server with empty local await syncService.fullPull(); - await new Promise(resolve => setTimeout(resolve, 50)); + await new Promise(resolve => setTimeout(resolve, 100)); await syncService.fullPush(); } catch {} } diff --git a/src/contexts/ThemeContext.tsx b/src/contexts/ThemeContext.tsx index b3f46c26..8f0b9e43 100644 --- a/src/contexts/ThemeContext.tsx +++ b/src/contexts/ThemeContext.tsx @@ -181,7 +181,7 @@ export function ThemeProvider({ children }: { children: ReactNode }) { if (theme) setCurrentThemeState(theme); } } catch (error) { - console.error('Failed to load themes:', error); + if (__DEV__) console.error('Failed to load themes:', error); } }; loadThemes(); @@ -241,7 +241,7 @@ export function ThemeProvider({ children }: { children: ReactNode }) { await AsyncStorage.setItem(CURRENT_THEME_KEY, id); // Do not emit global settings sync for themes } catch (error) { - console.error('Failed to add custom theme:', error); + if (__DEV__) console.error('Failed to add custom theme:', error); } }; @@ -279,7 +279,7 @@ export function ThemeProvider({ children }: { children: ReactNode }) { } // Do not emit global settings sync for themes } catch (error) { - console.error('Failed to update custom theme:', error); + if (__DEV__) console.error('Failed to update custom theme:', error); } }; @@ -316,7 +316,7 @@ export function ThemeProvider({ children }: { children: ReactNode }) { } // Do not emit global settings sync for themes } catch (error) { - console.error('Failed to delete custom theme:', error); + if (__DEV__) console.error('Failed to delete custom theme:', error); } }; diff --git a/src/hooks/useDominantColor.ts b/src/hooks/useDominantColor.ts index 22c627bc..fe62a2b2 100644 --- a/src/hooks/useDominantColor.ts +++ b/src/hooks/useDominantColor.ts @@ -142,7 +142,7 @@ const selectBestColor = (result: ImageColorsResult): string => { export const preloadDominantColor = async (imageUri: string | null) => { if (!imageUri || colorCache.has(imageUri)) return; - console.log('[useDominantColor] Preloading color for URI:', imageUri); + if (__DEV__) console.log('[useDominantColor] Preloading color for URI:', imageUri); try { // Fast first-pass: prioritize speed to avoid visible delay @@ -157,7 +157,7 @@ export const preloadDominantColor = async (imageUri: string | null) => { const extractedColor = selectBestColor(result); colorCache.set(imageUri, extractedColor); } catch (err) { - console.warn('[preloadDominantColor] Failed to preload color:', err); + if (__DEV__) console.warn('[preloadDominantColor] Failed to preload color:', err); colorCache.set(imageUri, '#1a1a1a'); } }; @@ -236,7 +236,7 @@ export const useDominantColor = (imageUri: string | null): DominantColorResult = // Ignore refine errors silently }); } catch (err) { - console.warn('[useDominantColor] Failed to extract color:', err); + if (__DEV__) console.warn('[useDominantColor] Failed to extract color:', err); setError(err instanceof Error ? err.message : 'Failed to extract color'); const fallbackColor = '#1a1a1a'; colorCache.set(uri, fallbackColor); // Cache fallback to avoid repeated failures diff --git a/src/hooks/useLibrary.ts b/src/hooks/useLibrary.ts index 2cf4cba7..e3df9bd3 100644 --- a/src/hooks/useLibrary.ts +++ b/src/hooks/useLibrary.ts @@ -1,6 +1,6 @@ import { useState, useEffect, useCallback } from 'react'; import AsyncStorage from '@react-native-async-storage/async-storage'; -import { StreamingContent } from '../types/metadata'; +import { StreamingContent } from '../services/catalogService'; import { catalogService } from '../services/catalogService'; const LEGACY_LIBRARY_STORAGE_KEY = 'stremio-library'; @@ -36,7 +36,7 @@ export const useLibrary = () => { } } } catch (error) { - console.error('Error loading library items:', error); + if (__DEV__) console.error('Error loading library items:', error); } finally { setLoading(false); } @@ -56,7 +56,7 @@ export const useLibrary = () => { // keep legacy for backward-compat await AsyncStorage.setItem(LEGACY_LIBRARY_STORAGE_KEY, JSON.stringify(itemsObject)); } catch (error) { - console.error('Error saving library items:', error); + if (__DEV__) console.error('Error saving library items:', error); } }, []); @@ -66,7 +66,7 @@ export const useLibrary = () => { await catalogService.addToLibrary({ ...item, inLibrary: true }); return true; } catch (e) { - console.error('Error adding to library via catalogService:', e); + if (__DEV__) console.error('Error adding to library via catalogService:', e); // Fallback local write const updatedItems = [...libraryItems, { ...item, inLibrary: true }]; setLibraryItems(updatedItems); @@ -82,7 +82,7 @@ export const useLibrary = () => { await catalogService.removeFromLibrary(type, id); return true; } catch (e) { - console.error('Error removing from library via catalogService:', e); + if (__DEV__) console.error('Error removing from library via catalogService:', e); // Fallback local write const updatedItems = libraryItems.filter(item => item.id !== id); setLibraryItems(updatedItems); @@ -115,7 +115,7 @@ export const useLibrary = () => { // Subscribe to catalogService library updates useEffect(() => { const unsubscribe = catalogService.subscribeToLibraryUpdates((items) => { - console.log('[useLibrary] Received library update from catalogService:', items.length, 'items'); + if (__DEV__) console.log('[useLibrary] Received library update from catalogService:', items.length, 'items'); setLibraryItems(items); setLoading(false); }); diff --git a/src/hooks/useMetadata.ts b/src/hooks/useMetadata.ts index 538e48f3..25af80a9 100644 --- a/src/hooks/useMetadata.ts +++ b/src/hooks/useMetadata.ts @@ -143,7 +143,7 @@ export const useMetadata = ({ id, type, addonId }: UseMetadataProps): UseMetadat const logPrefix = isEpisode ? 'loadEpisodeStreams' : 'loadStreams'; const sourceName = 'stremio'; - logger.log(`🔍 [${logPrefix}:${sourceName}] Starting fetch`); + if (__DEV__) logger.log(`🔍 [${logPrefix}:${sourceName}] Starting fetch`); try { await stremioService.getStreams(type, id, @@ -180,12 +180,12 @@ export const useMetadata = ({ id, type, addonId }: UseMetadataProps): UseMetadat if (error) { logger.error(`❌ [${logPrefix}:${sourceName}] Error for addon ${addonName} (${addonId}):`, error); } else if (streams && addonId && addonName) { - logger.log(`✅ [${logPrefix}:${sourceName}] Received ${streams.length} streams from ${addonName} (${addonId}) after ${processTime}ms`); + if (__DEV__) logger.log(`✅ [${logPrefix}:${sourceName}] Received ${streams.length} streams from ${addonName} (${addonId}) after ${processTime}ms`); if (streams.length > 0) { // Use the streams directly as they are already processed by stremioService const updateState = (prevState: GroupedStreams): GroupedStreams => { - logger.log(`🔄 [${logPrefix}:${sourceName}] Updating state for addon ${addonName} (${addonId})`); + if (__DEV__) logger.log(`🔄 [${logPrefix}:${sourceName}] Updating state for addon ${addonName} (${addonId})`); return { ...prevState, [addonId]: { @@ -205,16 +205,16 @@ export const useMetadata = ({ id, type, addonId }: UseMetadataProps): UseMetadat setLoadingStreams(false); } } else { - logger.log(`🤷 [${logPrefix}:${sourceName}] No streams found for addon ${addonName} (${addonId})`); + if (__DEV__) logger.log(`🤷 [${logPrefix}:${sourceName}] No streams found for addon ${addonName} (${addonId})`); } } else { // Handle case where callback provides null streams without error (e.g., empty results) - logger.log(`🏁 [${logPrefix}:${sourceName}] Finished fetching for addon ${addonName} (${addonId}) with no streams after ${processTime}ms`); + if (__DEV__) logger.log(`🏁 [${logPrefix}:${sourceName}] Finished fetching for addon ${addonName} (${addonId}) with no streams after ${processTime}ms`); } } ); // The function now returns void, just await to let callbacks fire - logger.log(`🏁 [${logPrefix}:${sourceName}] Stremio fetching process initiated`); + if (__DEV__) logger.log(`🏁 [${logPrefix}:${sourceName}] Stremio fetching process initiated`); } catch (error) { // Catch errors from the initial call to getStreams (e.g., initialization errors) logger.error(`❌ [${logPrefix}:${sourceName}] Initial call failed:`, error); @@ -225,13 +225,13 @@ export const useMetadata = ({ id, type, addonId }: UseMetadataProps): UseMetadat }; const loadCast = async () => { - logger.log('[loadCast] Starting cast fetch for:', id); + if (__DEV__) logger.log('[loadCast] Starting cast fetch for:', id); setLoadingCast(true); try { // Check cache first const cachedCast = cacheService.getCast(id, type); if (cachedCast) { - logger.log('[loadCast] Using cached cast data'); + if (__DEV__) logger.log('[loadCast] Using cached cast data'); setCast(cachedCast); setLoadingCast(false); return; @@ -240,7 +240,7 @@ export const useMetadata = ({ id, type, addonId }: UseMetadataProps): UseMetadat // Handle TMDB IDs if (id.startsWith('tmdb:')) { const tmdbId = id.split(':')[1]; - logger.log('[loadCast] Using TMDB ID directly:', tmdbId); + if (__DEV__) logger.log('[loadCast] Using TMDB ID directly:', tmdbId); const castData = await tmdbService.getCredits(parseInt(tmdbId), type); if (castData && castData.cast) { const formattedCast = castData.cast.map((actor: any) => ({ @@ -249,7 +249,7 @@ export const useMetadata = ({ id, type, addonId }: UseMetadataProps): UseMetadat character: actor.character, profile_path: actor.profile_path })); - logger.log(`[loadCast] Found ${formattedCast.length} cast members from TMDB`); + if (__DEV__) logger.log(`[loadCast] Found ${formattedCast.length} cast members from TMDB`); setCast(formattedCast); cacheService.setCast(id, type, formattedCast); setLoadingCast(false); @@ -260,12 +260,12 @@ export const useMetadata = ({ id, type, addonId }: UseMetadataProps): UseMetadat // Handle IMDb IDs or convert to TMDB ID let tmdbId; if (id.startsWith('tt')) { - logger.log('[loadCast] Converting IMDb ID to TMDB ID'); + if (__DEV__) logger.log('[loadCast] Converting IMDb ID to TMDB ID'); tmdbId = await tmdbService.findTMDBIdByIMDB(id); } if (tmdbId) { - logger.log('[loadCast] Fetching cast using TMDB ID:', tmdbId); + if (__DEV__) logger.log('[loadCast] Fetching cast using TMDB ID:', tmdbId); const castData = await tmdbService.getCredits(tmdbId, type); if (castData && castData.cast) { const formattedCast = castData.cast.map((actor: any) => ({ @@ -274,12 +274,12 @@ export const useMetadata = ({ id, type, addonId }: UseMetadataProps): UseMetadat character: actor.character, profile_path: actor.profile_path })); - logger.log(`[loadCast] Found ${formattedCast.length} cast members`); + if (__DEV__) logger.log(`[loadCast] Found ${formattedCast.length} cast members`); setCast(formattedCast); cacheService.setCast(id, type, formattedCast); } } else { - logger.warn('[loadCast] Could not find TMDB ID for cast fetch'); + if (__DEV__) logger.warn('[loadCast] Could not find TMDB ID for cast fetch'); } } catch (error) { logger.error('[loadCast] Failed to load cast:', error); @@ -325,7 +325,7 @@ export const useMetadata = ({ id, type, addonId }: UseMetadataProps): UseMetadat const tmdbId = id.split(':')[1]; // For TMDB IDs, we need to handle metadata differently if (type === 'movie') { - logger.log('Fetching movie details from TMDB for:', tmdbId); + if (__DEV__) logger.log('Fetching movie details from TMDB for:', tmdbId); const movieDetails = await tmdbService.getMovieDetails(tmdbId); if (movieDetails) { const imdbId = movieDetails.imdb_id || movieDetails.external_ids?.imdb_id; @@ -388,7 +388,7 @@ export const useMetadata = ({ id, type, addonId }: UseMetadataProps): UseMetadat } } else if (type === 'series') { // Handle TV shows with TMDB IDs - logger.log('Fetching TV show details from TMDB for:', tmdbId); + if (__DEV__) logger.log('Fetching TV show details from TMDB for:', tmdbId); try { const showDetails = await tmdbService.getTVShowDetails(parseInt(tmdbId)); if (showDetails) { @@ -443,7 +443,7 @@ export const useMetadata = ({ id, type, addonId }: UseMetadataProps): UseMetadat const logoUrl = await tmdbService.getTvShowImages(tmdbId); if (logoUrl) { formattedShow.logo = logoUrl; - logger.log(`Successfully fetched logo for TV show ${tmdbId} from TMDB`); + if (__DEV__) logger.log(`Successfully fetched logo for TV show ${tmdbId} from TMDB`); } } catch (error) { logger.error('Failed to fetch logo from TMDB:', error); @@ -455,7 +455,7 @@ export const useMetadata = ({ id, type, addonId }: UseMetadataProps): UseMetadat // Load series data (episodes) setTmdbId(parseInt(tmdbId)); - loadSeriesData().catch(console.error); + loadSeriesData().catch((error) => { if (__DEV__) console.error(error); }); const isInLib = catalogService.getLibraryItems().some(item => item.id === id); setInLibrary(isInLib); @@ -502,7 +502,7 @@ export const useMetadata = ({ id, type, addonId }: UseMetadataProps): UseMetadat throw new Error('Content not found'); } } catch (error) { - console.error('Failed to load metadata:', error); + if (__DEV__) console.error('Failed to load metadata:', error); const errorMessage = error instanceof Error ? error.message : 'Failed to load content'; setError(errorMessage); @@ -522,7 +522,7 @@ export const useMetadata = ({ id, type, addonId }: UseMetadataProps): UseMetadat // First check if we have episode data from the addon const addonVideos = metadata?.videos; if (addonVideos && Array.isArray(addonVideos) && addonVideos.length > 0) { - logger.log(`đŸŽŦ Found ${addonVideos.length} episodes from addon metadata for ${metadata?.name || id}`); + if (__DEV__) logger.log(`đŸŽŦ Found ${addonVideos.length} episodes from addon metadata for ${metadata?.name || id}`); // Group addon episodes by season const groupedAddonEpisodes: GroupedEpisodes = {}; @@ -562,7 +562,7 @@ export const useMetadata = ({ id, type, addonId }: UseMetadataProps): UseMetadat groupedAddonEpisodes[parseInt(season)].sort((a, b) => a.episode_number - b.episode_number); }); - logger.log(`đŸ“ē Processed addon episodes into ${Object.keys(groupedAddonEpisodes).length} seasons`); + if (__DEV__) logger.log(`đŸ“ē Processed addon episodes into ${Object.keys(groupedAddonEpisodes).length} seasons`); // Fetch season posters from TMDB try { @@ -582,7 +582,7 @@ export const useMetadata = ({ id, type, addonId }: UseMetadataProps): UseMetadat })); } }); - logger.log('đŸ–ŧī¸ Successfully fetched and attached TMDB season posters to addon episodes.'); + if (__DEV__) logger.log('đŸ–ŧī¸ Successfully fetched and attached TMDB season posters to addon episodes.'); } } } catch (error) { @@ -697,7 +697,7 @@ export const useMetadata = ({ id, type, addonId }: UseMetadataProps): UseMetadat setEpisodes(transformedEpisodes[selectedSeasonNumber] || []); } } catch (error) { - console.error('Failed to load episodes:', error); + if (__DEV__) console.error('Failed to load episodes:', error); } finally { setLoadingSeasons(false); } @@ -724,7 +724,7 @@ export const useMetadata = ({ id, type, addonId }: UseMetadataProps): UseMetadat const loadStreams = async () => { const startTime = Date.now(); try { - console.log('🚀 [loadStreams] START - Loading streams for:', id); + if (__DEV__) console.log('🚀 [loadStreams] START - Loading streams for:', id); updateLoadingState(); // Reset scraper tracking @@ -732,21 +732,21 @@ export const useMetadata = ({ id, type, addonId }: UseMetadataProps): UseMetadat setActiveFetchingScrapers([]); // Get TMDB ID for external sources and determine the correct ID for Stremio addons - console.log('🔍 [loadStreams] Getting TMDB ID for:', id); + if (__DEV__) console.log('🔍 [loadStreams] Getting TMDB ID for:', id); let tmdbId; let stremioId = id; // Default to original ID if (id.startsWith('tmdb:')) { tmdbId = id.split(':')[1]; - console.log('✅ [loadStreams] Using TMDB ID from ID:', tmdbId); + if (__DEV__) console.log('✅ [loadStreams] Using TMDB ID from ID:', tmdbId); // Try to get IMDb ID from metadata first, then convert if needed if (metadata?.imdb_id) { stremioId = metadata.imdb_id; - console.log('✅ [loadStreams] Using IMDb ID from metadata for Stremio:', stremioId); + if (__DEV__) console.log('✅ [loadStreams] Using IMDb ID from metadata for Stremio:', stremioId); } else if (imdbId) { stremioId = imdbId; - console.log('✅ [loadStreams] Using stored IMDb ID for Stremio:', stremioId); + if (__DEV__) console.log('✅ [loadStreams] Using stored IMDb ID for Stremio:', stremioId); } else { // Convert TMDB ID to IMDb ID for Stremio addons (they expect IMDb format) try { @@ -760,24 +760,24 @@ export const useMetadata = ({ id, type, addonId }: UseMetadataProps): UseMetadat if (externalIds?.imdb_id) { stremioId = externalIds.imdb_id; - console.log('✅ [loadStreams] Converted TMDB to IMDb ID for Stremio:', stremioId); + if (__DEV__) console.log('✅ [loadStreams] Converted TMDB to IMDb ID for Stremio:', stremioId); } else { - console.log('âš ī¸ [loadStreams] No IMDb ID found for TMDB ID, using original:', stremioId); + if (__DEV__) console.log('âš ī¸ [loadStreams] No IMDb ID found for TMDB ID, using original:', stremioId); } } catch (error) { - console.log('âš ī¸ [loadStreams] Failed to convert TMDB to IMDb, using original ID:', error); + if (__DEV__) console.log('âš ī¸ [loadStreams] Failed to convert TMDB to IMDb, using original ID:', error); } } } else if (id.startsWith('tt')) { // This is already an IMDB ID, perfect for Stremio stremioId = id; - console.log('📝 [loadStreams] Converting IMDB ID to TMDB ID...'); + if (__DEV__) console.log('📝 [loadStreams] Converting IMDB ID to TMDB ID...'); tmdbId = await withTimeout(tmdbService.findTMDBIdByIMDB(id), API_TIMEOUT); - console.log('✅ [loadStreams] Converted to TMDB ID:', tmdbId); + if (__DEV__) console.log('✅ [loadStreams] Converted to TMDB ID:', tmdbId); } else { tmdbId = id; stremioId = id; - console.log('â„šī¸ [loadStreams] Using ID as both TMDB and Stremio ID:', tmdbId); + if (__DEV__) console.log('â„šī¸ [loadStreams] Using ID as both TMDB and Stremio ID:', tmdbId); } // Initialize scraper tracking @@ -851,11 +851,11 @@ export const useMetadata = ({ id, type, addonId }: UseMetadataProps): UseMetadat setScraperStatuses(initialStatuses); setActiveFetchingScrapers(initialActiveFetching); } catch (error) { - console.error('Failed to initialize scraper tracking:', error); + if (__DEV__) console.error('Failed to initialize scraper tracking:', error); } // Start Stremio request using the converted ID format - console.log('đŸŽŦ [loadStreams] Using ID for Stremio addons:', stremioId); + if (__DEV__) console.log('đŸŽŦ [loadStreams] Using ID for Stremio addons:', stremioId); processStremioSource(type, stremioId, false); // Monitor scraper completion status instead of using fixed timeout @@ -881,7 +881,7 @@ export const useMetadata = ({ id, type, addonId }: UseMetadataProps): UseMetadat }, 30000); } catch (error) { - console.error('❌ [loadStreams] Failed to load streams:', error); + if (__DEV__) console.error('❌ [loadStreams] Failed to load streams:', error); setError('Failed to load streams'); setLoadingStreams(false); } @@ -890,7 +890,7 @@ export const useMetadata = ({ id, type, addonId }: UseMetadataProps): UseMetadat const loadEpisodeStreams = async (episodeId: string) => { const startTime = Date.now(); try { - console.log('🚀 [loadEpisodeStreams] START - Loading episode streams for:', episodeId); + if (__DEV__) console.log('🚀 [loadEpisodeStreams] START - Loading episode streams for:', episodeId); updateEpisodeLoadingState(); // Reset scraper tracking for episodes @@ -968,28 +968,28 @@ export const useMetadata = ({ id, type, addonId }: UseMetadataProps): UseMetadat setScraperStatuses(initialStatuses); setActiveFetchingScrapers(initialActiveFetching); } catch (error) { - console.error('Failed to initialize episode scraper tracking:', error); + if (__DEV__) console.error('Failed to initialize episode scraper tracking:', error); } // Get TMDB ID for external sources and determine the correct ID for Stremio addons - console.log('🔍 [loadEpisodeStreams] Getting TMDB ID for:', id); + if (__DEV__) console.log('🔍 [loadEpisodeStreams] Getting TMDB ID for:', id); let tmdbId; let stremioEpisodeId = episodeId; // Default to original episode ID if (id.startsWith('tmdb:')) { tmdbId = id.split(':')[1]; - console.log('✅ [loadEpisodeStreams] Using TMDB ID from ID:', tmdbId); + if (__DEV__) console.log('✅ [loadEpisodeStreams] Using TMDB ID from ID:', tmdbId); // Try to get IMDb ID from metadata first, then convert if needed if (metadata?.imdb_id) { // Replace the series ID in episodeId with the IMDb ID const [, season, episode] = episodeId.split(':'); stremioEpisodeId = `series:${metadata.imdb_id}:${season}:${episode}`; - console.log('✅ [loadEpisodeStreams] Using IMDb ID from metadata for Stremio episode:', stremioEpisodeId); + if (__DEV__) console.log('✅ [loadEpisodeStreams] Using IMDb ID from metadata for Stremio episode:', stremioEpisodeId); } else if (imdbId) { const [, season, episode] = episodeId.split(':'); stremioEpisodeId = `series:${imdbId}:${season}:${episode}`; - console.log('✅ [loadEpisodeStreams] Using stored IMDb ID for Stremio episode:', stremioEpisodeId); + if (__DEV__) console.log('✅ [loadEpisodeStreams] Using stored IMDb ID for Stremio episode:', stremioEpisodeId); } else { // Convert TMDB ID to IMDb ID for Stremio addons try { @@ -998,33 +998,33 @@ export const useMetadata = ({ id, type, addonId }: UseMetadataProps): UseMetadat if (externalIds?.imdb_id) { const [, season, episode] = episodeId.split(':'); stremioEpisodeId = `series:${externalIds.imdb_id}:${season}:${episode}`; - console.log('✅ [loadEpisodeStreams] Converted TMDB to IMDb ID for Stremio episode:', stremioEpisodeId); + if (__DEV__) console.log('✅ [loadEpisodeStreams] Converted TMDB to IMDb ID for Stremio episode:', stremioEpisodeId); } else { - console.log('âš ī¸ [loadEpisodeStreams] No IMDb ID found for TMDB ID, using original episode ID:', stremioEpisodeId); + if (__DEV__) console.log('âš ī¸ [loadEpisodeStreams] No IMDb ID found for TMDB ID, using original episode ID:', stremioEpisodeId); } } catch (error) { - console.log('âš ī¸ [loadEpisodeStreams] Failed to convert TMDB to IMDb, using original episode ID:', error); + if (__DEV__) console.log('âš ī¸ [loadEpisodeStreams] Failed to convert TMDB to IMDb, using original episode ID:', error); } } } else if (id.startsWith('tt')) { // This is already an IMDB ID, perfect for Stremio - console.log('📝 [loadEpisodeStreams] Converting IMDB ID to TMDB ID...'); + if (__DEV__) console.log('📝 [loadEpisodeStreams] Converting IMDB ID to TMDB ID...'); tmdbId = await withTimeout(tmdbService.findTMDBIdByIMDB(id), API_TIMEOUT); - console.log('✅ [loadEpisodeStreams] Converted to TMDB ID:', tmdbId); + if (__DEV__) console.log('✅ [loadEpisodeStreams] Converted to TMDB ID:', tmdbId); } else { tmdbId = id; - console.log('â„šī¸ [loadEpisodeStreams] Using ID as both TMDB and Stremio ID:', tmdbId); + if (__DEV__) console.log('â„šī¸ [loadEpisodeStreams] Using ID as both TMDB and Stremio ID:', tmdbId); } // Extract episode info from the episodeId for logging const [, season, episode] = episodeId.split(':'); const episodeQuery = `?s=${season}&e=${episode}`; - console.log(`â„šī¸ [loadEpisodeStreams] Episode query: ${episodeQuery}`); + if (__DEV__) console.log(`â„šī¸ [loadEpisodeStreams] Episode query: ${episodeQuery}`); - console.log('🔄 [loadEpisodeStreams] Starting stream requests'); + if (__DEV__) console.log('🔄 [loadEpisodeStreams] Starting stream requests'); // Start Stremio request using the converted episode ID format - console.log('đŸŽŦ [loadEpisodeStreams] Using episode ID for Stremio addons:', stremioEpisodeId); + if (__DEV__) console.log('đŸŽŦ [loadEpisodeStreams] Using episode ID for Stremio addons:', stremioEpisodeId); processStremioSource('series', stremioEpisodeId, true); // Monitor scraper completion status instead of using fixed timeout @@ -1040,7 +1040,7 @@ export const useMetadata = ({ id, type, addonId }: UseMetadataProps): UseMetadat }; // Check completion less frequently to reduce CPU load - const episodeCompletionInterval = setInterval(checkEpisodeScrapersCompletion, 2000); + const episodeCompletionInterval = setInterval(checkEpisodeScrapersCompletion, 3000); // Fallback timeout after 30 seconds const episodeFallbackTimeout = setTimeout(() => { @@ -1050,7 +1050,7 @@ export const useMetadata = ({ id, type, addonId }: UseMetadataProps): UseMetadat }, 30000); } catch (error) { - console.error('❌ [loadEpisodeStreams] Failed to load episode streams:', error); + if (__DEV__) console.error('❌ [loadEpisodeStreams] Failed to load episode streams:', error); setError('Failed to load episode streams'); setLoadingEpisodeStreams(false); } @@ -1103,7 +1103,7 @@ export const useMetadata = ({ id, type, addonId }: UseMetadataProps): UseMetadat useEffect(() => { if (metadata && type === 'series' && metadata.videos && metadata.videos.length > 0) { logger.log(`đŸŽŦ Metadata updated with ${metadata.videos.length} episodes, reloading series data`); - loadSeriesData().catch(console.error); + loadSeriesData().catch((error) => { if (__DEV__) console.error(error); }); } }, [metadata?.videos, type]); @@ -1126,7 +1126,7 @@ export const useMetadata = ({ id, type, addonId }: UseMetadataProps): UseMetadat setRecommendations(formattedRecommendations); } catch (error) { - console.error('Failed to load recommendations:', error); + if (__DEV__) console.error('Failed to load recommendations:', error); setRecommendations([]); } finally { setLoadingRecommendations(false); @@ -1141,24 +1141,24 @@ export const useMetadata = ({ id, type, addonId }: UseMetadataProps): UseMetadat const tmdbService = TMDBService.getInstance(); const fetchedTmdbId = await tmdbService.extractTMDBIdFromStremioId(id); if (fetchedTmdbId) { - console.log('[useMetadata] extracted TMDB id from content id', { id, fetchedTmdbId }); + if (__DEV__) console.log('[useMetadata] extracted TMDB id from content id', { id, fetchedTmdbId }); setTmdbId(fetchedTmdbId); // Fetch certification const certification = await tmdbService.getCertification(type, fetchedTmdbId); if (certification) { - console.log('[useMetadata] fetched certification via TMDB id (extract path)', { type, fetchedTmdbId, certification }); + if (__DEV__) console.log('[useMetadata] fetched certification via TMDB id (extract path)', { type, fetchedTmdbId, certification }); setMetadata(prev => prev ? { ...prev, certification } : null); } else { - console.warn('[useMetadata] certification not returned from TMDB (extract path)', { type, fetchedTmdbId }); + if (__DEV__) console.warn('[useMetadata] certification not returned from TMDB (extract path)', { type, fetchedTmdbId }); } } else { - console.warn('[useMetadata] Could not determine TMDB ID for recommendations / certification', { id }); + if (__DEV__) console.warn('[useMetadata] Could not determine TMDB ID for recommendations / certification', { id }); } } catch (error) { - console.error('[useMetadata] Error fetching TMDB ID (extract path):', error); + if (__DEV__) console.error('[useMetadata] Error fetching TMDB ID (extract path):', error); } } }; @@ -1168,7 +1168,7 @@ export const useMetadata = ({ id, type, addonId }: UseMetadataProps): UseMetadat useEffect(() => { if (tmdbId) { - console.log('[useMetadata] tmdbId available; loading recommendations and enabling certification checks', { tmdbId }); + if (__DEV__) console.log('[useMetadata] tmdbId available; loading recommendations and enabling certification checks', { tmdbId }); loadRecommendations(); // Reset recommendations when tmdbId changes return () => { @@ -1183,27 +1183,27 @@ export const useMetadata = ({ id, type, addonId }: UseMetadataProps): UseMetadat const maybeAttachCertification = async () => { try { if (!metadata) { - console.warn('[useMetadata] skip certification attach: metadata not ready'); + if (__DEV__) console.warn('[useMetadata] skip certification attach: metadata not ready'); return; } if (!tmdbId) { - console.warn('[useMetadata] skip certification attach: tmdbId not available yet'); + if (__DEV__) console.warn('[useMetadata] skip certification attach: tmdbId not available yet'); return; } if ((metadata as any).certification) { - console.log('[useMetadata] certification already present on metadata; skipping fetch'); + if (__DEV__) console.log('[useMetadata] certification already present on metadata; skipping fetch'); return; } const tmdbSvc = TMDBService.getInstance(); const cert = await tmdbSvc.getCertification(type, tmdbId); if (cert) { - console.log('[useMetadata] fetched certification (attach path)', { type, tmdbId, cert }); + if (__DEV__) console.log('[useMetadata] fetched certification (attach path)', { type, tmdbId, cert }); setMetadata(prev => prev ? { ...prev, certification: cert } : prev); } else { - console.warn('[useMetadata] TMDB returned no certification (attach path)', { type, tmdbId }); + if (__DEV__) console.warn('[useMetadata] TMDB returned no certification (attach path)', { type, tmdbId }); } } catch (err) { - console.error('[useMetadata] error attaching certification', err); + if (__DEV__) console.error('[useMetadata] error attaching certification', err); } }; maybeAttachCertification(); diff --git a/src/hooks/useMetadataAnimations.ts b/src/hooks/useMetadataAnimations.ts index d72a7e55..0aa6e9e4 100644 --- a/src/hooks/useMetadataAnimations.ts +++ b/src/hooks/useMetadataAnimations.ts @@ -89,7 +89,7 @@ export const useMetadataAnimations = (safeAreaTop: number, watchProgress: any) = }); } catch (error) { // Silently handle any animation errors - console.warn('Animation error in enterAnimations:', error); + if (__DEV__) console.warn('Animation error in enterAnimations:', error); } }; @@ -97,7 +97,7 @@ export const useMetadataAnimations = (safeAreaTop: number, watchProgress: any) = try { runOnUI(enterAnimations)(); } catch (error) { - console.warn('Failed to run enter animations:', error); + if (__DEV__) console.warn('Failed to run enter animations:', error); } }, []); @@ -114,14 +114,14 @@ export const useMetadataAnimations = (safeAreaTop: number, watchProgress: any) = easing: easings.fast }); } catch (error) { - console.warn('Animation error in updateProgress:', error); + if (__DEV__) console.warn('Animation error in updateProgress:', error); } }; try { runOnUI(updateProgress)(); } catch (error) { - console.warn('Failed to run progress animation:', error); + if (__DEV__) console.warn('Failed to run progress animation:', error); } }, [watchProgress]); @@ -140,7 +140,7 @@ export const useMetadataAnimations = (safeAreaTop: number, watchProgress: any) = cancelAnimation(headerProgress); cancelAnimation(staticHeaderElementsY); } catch (error) { - console.warn('Error canceling animations:', error); + if (__DEV__) console.warn('Error canceling animations:', error); } }; }, []); @@ -166,7 +166,7 @@ export const useMetadataAnimations = (safeAreaTop: number, watchProgress: any) = }); } } catch (error) { - console.warn('Animation error in scroll handler:', error); + if (__DEV__) console.warn('Animation error in scroll handler:', error); } }, }); diff --git a/src/hooks/useSettings.ts b/src/hooks/useSettings.ts index 81e42c93..3aacd667 100644 --- a/src/hooks/useSettings.ts +++ b/src/hooks/useSettings.ts @@ -172,7 +172,7 @@ export const useSettings = () => { if (merged) setSettings({ ...DEFAULT_SETTINGS, ...merged }); else setSettings(DEFAULT_SETTINGS); } catch (error) { - console.error('Failed to load settings:', error); + if (__DEV__) console.error('Failed to load settings:', error); // Fallback to default settings on error setSettings(DEFAULT_SETTINGS); } @@ -195,18 +195,18 @@ export const useSettings = () => { // Ensure a current scope exists to avoid future loads missing the chosen scope await AsyncStorage.setItem('@user:current', scope); setSettings(newSettings); - console.log(`Setting updated: ${key}`, value); + if (__DEV__) console.log(`Setting updated: ${key}`, value); // Notify all subscribers that settings have changed (if requested) if (emitEvent) { - console.log('Emitting settings change event'); + if (__DEV__) console.log('Emitting settings change event'); settingsEmitter.emit(); } // If authenticated, push settings to server to prevent overwrite on next pull try { syncService.pushSettings(); } catch {} } catch (error) { - console.error('Failed to save settings:', error); + if (__DEV__) console.error('Failed to save settings:', error); } }, [settings]); diff --git a/src/hooks/useUpdatePopup.ts b/src/hooks/useUpdatePopup.ts index 02141110..a98d7f41 100644 --- a/src/hooks/useUpdatePopup.ts +++ b/src/hooks/useUpdatePopup.ts @@ -1,17 +1,8 @@ import { useState, useEffect, useCallback } from 'react'; import { Alert } from 'react-native'; -import UpdateService from '../services/updateService'; +import UpdateService, { UpdateInfo } from '../services/updateService'; import AsyncStorage from '@react-native-async-storage/async-storage'; -interface UpdateInfo { - isAvailable: boolean; - manifest?: { - id: string; - version?: string; - description?: string; - }; -} - interface UseUpdatePopupReturn { showUpdatePopup: boolean; updateInfo: UpdateInfo; @@ -24,6 +15,7 @@ interface UseUpdatePopupReturn { const UPDATE_POPUP_STORAGE_KEY = '@update_popup_dismissed'; const UPDATE_LATER_STORAGE_KEY = '@update_later_timestamp'; +const UPDATE_LAST_CHECK_TS_KEY = '@update_last_check_ts'; export const useUpdatePopup = (): UseUpdatePopupReturn => { const [showUpdatePopup, setShowUpdatePopup] = useState(false); @@ -59,7 +51,7 @@ export const useUpdatePopup = (): UseUpdatePopupReturn => { setShowUpdatePopup(true); } } catch (error) { - console.error('Error checking for updates:', error); + if (__DEV__) console.error('Error checking for updates:', error); // Don't show popup on error, just log it } }, [updateInfo.manifest?.id]); @@ -102,7 +94,7 @@ export const useUpdatePopup = (): UseUpdatePopupReturn => { setShowUpdatePopup(true); } } catch (error) { - console.error('Error installing update:', error); + if (__DEV__) console.error('Error installing update:', error); Alert.alert( 'Update Error', 'An error occurred while installing the update. Please try again later.' @@ -120,7 +112,7 @@ export const useUpdatePopup = (): UseUpdatePopupReturn => { await AsyncStorage.setItem(UPDATE_LATER_STORAGE_KEY, Date.now().toString()); setShowUpdatePopup(false); } catch (error) { - console.error('Error storing update later preference:', error); + if (__DEV__) console.error('Error storing update later preference:', error); setShowUpdatePopup(false); } }, []); @@ -134,7 +126,7 @@ export const useUpdatePopup = (): UseUpdatePopupReturn => { } setShowUpdatePopup(false); } catch (error) { - console.error('Error storing dismiss preference:', error); + if (__DEV__) console.error('Error storing dismiss preference:', error); setShowUpdatePopup(false); } }, [updateInfo.manifest?.id]); @@ -143,7 +135,21 @@ export const useUpdatePopup = (): UseUpdatePopupReturn => { useEffect(() => { // Add a small delay to ensure the app is fully loaded const timer = setTimeout(() => { - checkForUpdates(); + (async () => { + try { + const lastCheckTs = await AsyncStorage.getItem(UPDATE_LAST_CHECK_TS_KEY); + const last = lastCheckTs ? parseInt(lastCheckTs, 10) : 0; + const now = Date.now(); + const twentyFourHours = 24 * 60 * 60 * 1000; + if (now - last < twentyFourHours) { + return; // Throttle: only auto-check once per 24h + } + await checkForUpdates(); + await AsyncStorage.setItem(UPDATE_LAST_CHECK_TS_KEY, String(now)); + } catch { + // ignore + } + })(); }, 2000); // 2 second delay return () => clearTimeout(timer); diff --git a/src/screens/CastMoviesScreen.tsx b/src/screens/CastMoviesScreen.tsx index dd3c5e96..0f416174 100644 --- a/src/screens/CastMoviesScreen.tsx +++ b/src/screens/CastMoviesScreen.tsx @@ -144,7 +144,7 @@ const CastMoviesScreen: React.FC = () => { setMovies(allCredits); } } catch (error) { - console.error('Error fetching cast credits:', error); + if (__DEV__) console.error('Error fetching cast credits:', error); } finally { setLoading(false); } @@ -206,8 +206,9 @@ const CastMoviesScreen: React.FC = () => { }, [displayLimit, filteredAndSortedMovies.length, isLoadingMore]); const handleMoviePress = async (movie: CastMovie) => { - console.log('=== CastMoviesScreen: Movie Press ==='); - console.log('Movie data:', { + if (__DEV__) { + console.log('=== CastMoviesScreen: Movie Press ==='); + console.log('Movie data:', { id: movie.id, title: movie.title, media_type: movie.media_type, @@ -219,15 +220,15 @@ const CastMoviesScreen: React.FC = () => { }); try { - console.log('Attempting to get Stremio ID for:', movie.media_type, movie.id.toString()); + if (__DEV__) console.log('Attempting to get Stremio ID for:', movie.media_type, movie.id.toString()); // Get Stremio ID using catalogService const stremioId = await catalogService.getStremioId(movie.media_type, movie.id.toString()); - console.log('Stremio ID result:', stremioId); + if (__DEV__) console.log('Stremio ID result:', stremioId); if (stremioId) { - console.log('Successfully found Stremio ID, navigating to Metadata with:', { + if (__DEV__) console.log('Successfully found Stremio ID, navigating to Metadata with:', { id: stremioId, type: movie.media_type }); @@ -235,7 +236,7 @@ const CastMoviesScreen: React.FC = () => { // Convert TMDB media type to Stremio media type const stremioType = movie.media_type === 'tv' ? 'series' : movie.media_type; - console.log('Navigating with Stremio type conversion:', { + if (__DEV__) console.log('Navigating with Stremio type conversion:', { originalType: movie.media_type, stremioType: stremioType, id: stremioId @@ -248,15 +249,17 @@ const CastMoviesScreen: React.FC = () => { }) ); } else { - console.warn('Stremio ID is null/undefined for movie:', movie.title); + if (__DEV__) console.warn('Stremio ID is null/undefined for movie:', movie.title); throw new Error('Could not find Stremio ID'); } } catch (error: any) { - console.error('=== Error in handleMoviePress ==='); - console.error('Movie:', movie.title); - console.error('Error details:', error); - console.error('Error message:', error.message); - console.error('Error stack:', error.stack); + if (__DEV__) { + console.error('=== Error in handleMoviePress ==='); + console.error('Movie:', movie.title); + console.error('Error details:', error); + console.error('Error message:', error.message); + console.error('Error stack:', error.stack); + } Alert.alert( 'Error', diff --git a/src/screens/HeroCatalogsScreen.tsx b/src/screens/HeroCatalogsScreen.tsx index 6c84b81f..79514c8d 100644 --- a/src/screens/HeroCatalogsScreen.tsx +++ b/src/screens/HeroCatalogsScreen.tsx @@ -119,7 +119,7 @@ const HeroCatalogsScreen: React.FC = () => { setCatalogs(catalogItems); } catch (error) { - console.error('Failed to load catalogs:', error); + if (__DEV__) console.error('Failed to load catalogs:', error); Alert.alert('Error', 'Failed to load catalogs'); } finally { setLoading(false); diff --git a/src/screens/HomeScreen.tsx b/src/screens/HomeScreen.tsx index 5319586b..aa1ce859 100644 --- a/src/screens/HomeScreen.tsx +++ b/src/screens/HomeScreen.tsx @@ -158,7 +158,7 @@ const HomeScreen = () => { let catalogIndex = 0; // Limit concurrent catalog loading to prevent overwhelming the system - const MAX_CONCURRENT_CATALOGS = 2; // Very low concurrency to reduce heating + const MAX_CONCURRENT_CATALOGS = 1; // Single catalog at a time to minimize heating let activeCatalogLoads = 0; const catalogQueue: (() => Promise)[] = []; @@ -170,7 +170,7 @@ const HomeScreen = () => { catalogLoader().finally(async () => { activeCatalogLoads--; // Yield to event loop to avoid JS thread starvation and reduce heating - await new Promise(resolve => setTimeout(resolve, 50)); + await new Promise(resolve => setTimeout(resolve, 100)); processCatalogQueue(); // Process next in queue }); } @@ -257,7 +257,7 @@ const HomeScreen = () => { }); } } catch (error) { - console.error(`[HomeScreen] Failed to load ${catalog.name} from ${addon.name}:`, error); + if (__DEV__) console.error(`[HomeScreen] Failed to load ${catalog.name} from ${addon.name}:`, error); } finally { setLoadedCatalogCount(prev => { const next = prev + 1; @@ -285,7 +285,7 @@ const HomeScreen = () => { // Start processing the catalog queue (parallel fetching continues in background) processCatalogQueue(); } catch (error) { - console.error('[HomeScreen] Error in progressive catalog loading:', error); + if (__DEV__) console.error('[HomeScreen] Error in progressive catalog loading:', error); setCatalogsLoading(false); } }, []); @@ -414,7 +414,7 @@ const HomeScreen = () => { try { ExpoImage.clearMemoryCache(); } catch (error) { - console.warn('Failed to clear image cache:', error); + if (__DEV__) console.warn('Failed to clear image cache:', error); } }; }, [currentTheme.colors.darkBackground]); @@ -527,7 +527,7 @@ const HomeScreen = () => { setHasContinueWatching(hasContent); } catch (error) { - console.error('[HomeScreen] Error refreshing continue watching:', error); + if (__DEV__) console.error('[HomeScreen] Error refreshing continue watching:', error); setHasContinueWatching(false); } } diff --git a/src/screens/LogoSourceSettings.tsx b/src/screens/LogoSourceSettings.tsx index c3e87d22..0bfc40f2 100644 --- a/src/screens/LogoSourceSettings.tsx +++ b/src/screens/LogoSourceSettings.tsx @@ -605,7 +605,7 @@ const LogoSourceSettings = () => { try { await AsyncStorage.setItem('logo_settings_selected_show', show.imdbId); } catch (e) { - console.error('Error saving selected show:', e); + if (__DEV__) console.error('Error saving selected show:', e); } }; @@ -621,7 +621,7 @@ const LogoSourceSettings = () => { } } } catch (e) { - console.error('Error loading selected show:', e); + if (__DEV__) console.error('Error loading selected show:', e); } }; diff --git a/src/screens/MetadataScreen.tsx b/src/screens/MetadataScreen.tsx index 9dbff95f..d4a917ed 100644 --- a/src/screens/MetadataScreen.tsx +++ b/src/screens/MetadataScreen.tsx @@ -192,7 +192,7 @@ const MetadataScreen: React.FC = () => { // Debug logging for color extraction timing useEffect(() => { if (__DEV__ && heroImageUri && dominantColor) { - console.log('[MetadataScreen] Dynamic background color:', { + if (__DEV__) console.log('[MetadataScreen] Dynamic background color:', { dominantColor, fallback: currentTheme.colors.darkBackground, finalColor: dynamicBackgroundColor, @@ -242,7 +242,7 @@ const MetadataScreen: React.FC = () => { const isAuthenticated = await traktService.isAuthenticated(); if (!isAuthenticated) { - console.log(`[MetadataScreen] Not authenticated with Trakt`); + if (__DEV__) console.log(`[MetadataScreen] Not authenticated with Trakt`); return; } @@ -283,7 +283,7 @@ const MetadataScreen: React.FC = () => { } } catch (error) { - console.error(`[MetadataScreen] Failed to fetch Trakt progress:`, error); + if (__DEV__) console.error(`[MetadataScreen] Failed to fetch Trakt progress:`, error); } }, [shouldLoadSecondaryData, metadata, id, type]); @@ -315,7 +315,7 @@ const MetadataScreen: React.FC = () => { const timer = setTimeout(() => { const renderTime = Date.now() - startTime; if (renderTime > 100) { - console.warn(`[MetadataScreen] Slow render detected: ${renderTime}ms for ${metadata.name}`); + if (__DEV__) console.warn(`[MetadataScreen] Slow render detected: ${renderTime}ms for ${metadata.name}`); } }, 0); return () => clearTimeout(timer); @@ -448,7 +448,7 @@ const MetadataScreen: React.FC = () => { const handleEpisodeSelect = useCallback((episode: Episode) => { if (!isScreenFocused) return; - console.log('[MetadataScreen] Selected Episode:', episode.episode_number, episode.season_number); + if (__DEV__) console.log('[MetadataScreen] Selected Episode:', episode.episode_number, episode.season_number); const episodeId = episode.stremioId || `${id}:${episode.season_number}:${episode.episode_number}`; // Optimize navigation with requestAnimationFrame diff --git a/src/screens/OnboardingScreen.tsx b/src/screens/OnboardingScreen.tsx index 658de1ad..b47e2dd6 100644 --- a/src/screens/OnboardingScreen.tsx +++ b/src/screens/OnboardingScreen.tsx @@ -117,7 +117,7 @@ const OnboardingScreen = () => { navigation.reset({ index: 0, routes: [{ name: 'MainTabs' }] }); } } catch (error) { - console.error('Error saving onboarding status:', error); + if (__DEV__) console.error('Error saving onboarding status:', error); navigation.reset({ index: 0, routes: [{ name: user ? 'MainTabs' : 'Account', params: !user ? ({ fromOnboarding: true } as any) : undefined }] }); } }; diff --git a/src/screens/ProfilesScreen.tsx b/src/screens/ProfilesScreen.tsx index 18c6130c..008f3b48 100644 --- a/src/screens/ProfilesScreen.tsx +++ b/src/screens/ProfilesScreen.tsx @@ -13,7 +13,7 @@ import { Modal } from 'react-native'; import { useNavigation } from '@react-navigation/native'; -import MaterialIcons from 'react-native-vector-icons/MaterialIcons'; +import { MaterialIcons } from '@expo/vector-icons'; import { useTheme } from '../contexts/ThemeContext'; import { useTraktContext } from '../contexts/TraktContext'; import AsyncStorage from '@react-native-async-storage/async-storage'; @@ -58,7 +58,7 @@ const ProfilesScreen: React.FC = () => { await AsyncStorage.setItem(PROFILE_STORAGE_KEY, JSON.stringify([defaultProfile])); } } catch (error) { - console.error('Error loading profiles:', error); + if (__DEV__) console.error('Error loading profiles:', error); Alert.alert('Error', 'Failed to load profiles'); } finally { setIsLoading(false); @@ -84,7 +84,7 @@ const ProfilesScreen: React.FC = () => { try { await AsyncStorage.setItem(PROFILE_STORAGE_KEY, JSON.stringify(updatedProfiles)); } catch (error) { - console.error('Error saving profiles:', error); + if (__DEV__) console.error('Error saving profiles:', error); Alert.alert('Error', 'Failed to save profiles'); } }, []); diff --git a/src/screens/SettingsScreen.tsx b/src/screens/SettingsScreen.tsx index ed1be0c3..cf890e4b 100644 --- a/src/screens/SettingsScreen.tsx +++ b/src/screens/SettingsScreen.tsx @@ -244,7 +244,7 @@ const SettingsScreen: React.FC = () => { // Refresh auth status if (isAuthenticated || userProfile) { // Just to be cautious, log the current state - console.log('SettingsScreen focused, refreshing auth status. Current state:', { isAuthenticated, userProfile: userProfile?.username }); + if (__DEV__) console.log('SettingsScreen focused, refreshing auth status. Current state:', { isAuthenticated, userProfile: userProfile?.username }); } refreshAuthStatus(); }); @@ -291,7 +291,7 @@ const SettingsScreen: React.FC = () => { setMdblistKeySet(!!mdblistKey); } catch (error) { - console.error('Error loading settings data:', error); + if (__DEV__) console.error('Error loading settings data:', error); } }, []); @@ -343,7 +343,7 @@ const SettingsScreen: React.FC = () => { Alert.alert("Success", "MDBList cache has been cleared."); } catch (error) { Alert.alert("Error", "Could not clear MDBList cache."); - console.error('Error clearing MDBList cache:', error); + if (__DEV__) console.error('Error clearing MDBList cache:', error); } } } diff --git a/src/screens/StreamsScreen.tsx b/src/screens/StreamsScreen.tsx index 14cd1ed8..734e66e6 100644 --- a/src/screens/StreamsScreen.tsx +++ b/src/screens/StreamsScreen.tsx @@ -403,7 +403,7 @@ export const StreamsScreen = () => { }, []); useEffect(() => { - console.log('[StreamsScreen] Received thumbnail from params:', episodeThumbnail); + if (__DEV__) console.log('[StreamsScreen] Received thumbnail from params:', episodeThumbnail); }, [episodeThumbnail]); // Pause trailer when StreamsScreen is opened @@ -1081,29 +1081,29 @@ export const StreamsScreen = () => { return; } - console.log(`Attempting to open stream in ${settings.preferredPlayer}`); + if (__DEV__) console.log(`Attempting to open stream in ${settings.preferredPlayer}`); // Try each URL format in sequence const tryNextUrl = (index: number) => { if (index >= externalPlayerUrls.length) { - console.log(`All ${settings.preferredPlayer} formats failed, falling back to direct URL`); + if (__DEV__) console.log(`All ${settings.preferredPlayer} formats failed, falling back to direct URL`); // Try direct URL as last resort Linking.openURL(stream.url) - .then(() => console.log('Opened with direct URL')) + .then(() => { if (__DEV__) console.log('Opened with direct URL'); }) .catch(() => { - console.log('Direct URL failed, falling back to built-in player'); + if (__DEV__) console.log('Direct URL failed, falling back to built-in player'); navigateToPlayer(stream); }); return; } const url = externalPlayerUrls[index]; - console.log(`Trying ${settings.preferredPlayer} URL format ${index + 1}: ${url}`); + if (__DEV__) console.log(`Trying ${settings.preferredPlayer} URL format ${index + 1}: ${url}`); Linking.openURL(url) - .then(() => console.log(`Successfully opened stream with ${settings.preferredPlayer} format ${index + 1}`)) + .then(() => { if (__DEV__) console.log(`Successfully opened stream with ${settings.preferredPlayer} format ${index + 1}`); }) .catch(err => { - console.log(`Format ${index + 1} failed: ${err.message}`, err); + if (__DEV__) console.log(`Format ${index + 1} failed: ${err.message}`, err); tryNextUrl(index + 1); }); }; @@ -1112,7 +1112,7 @@ export const StreamsScreen = () => { tryNextUrl(0); } catch (error) { - console.error(`Error with ${settings.preferredPlayer}:`, error); + if (__DEV__) console.error(`Error with ${settings.preferredPlayer}:`, error); // Fallback to the built-in player navigateToPlayer(stream); } @@ -1120,18 +1120,18 @@ export const StreamsScreen = () => { // For Android with external player preference else if (Platform.OS === 'android' && settings.useExternalPlayer) { try { - console.log('Opening stream with Android native app chooser'); + if (__DEV__) console.log('Opening stream with Android native app chooser'); // For Android, determine if the URL is a direct http/https URL or a magnet link const isMagnet = stream.url.startsWith('magnet:'); if (isMagnet) { // For magnet links, open directly which will trigger the torrent app chooser - console.log('Opening magnet link directly'); + if (__DEV__) console.log('Opening magnet link directly'); Linking.openURL(stream.url) - .then(() => console.log('Successfully opened magnet link')) - .catch(err => { - console.error('Failed to open magnet link:', err); + .then(() => { if (__DEV__) console.log('Successfully opened magnet link'); }) + .catch(err => { + if (__DEV__) console.error('Failed to open magnet link:', err); // No good fallback for magnet links navigateToPlayer(stream); }); @@ -1145,12 +1145,12 @@ export const StreamsScreen = () => { }); if (!success) { - console.log('VideoPlayerService failed, falling back to built-in player'); + if (__DEV__) console.log('VideoPlayerService failed, falling back to built-in player'); navigateToPlayer(stream); } } } catch (error) { - console.error('Error with external player:', error); + if (__DEV__) console.error('Error with external player:', error); // Fallback to the built-in player navigateToPlayer(stream); } @@ -1161,7 +1161,7 @@ export const StreamsScreen = () => { } } } catch (error) { - console.error('Error in handleStreamPress:', error); + if (__DEV__) console.error('Error in handleStreamPress:', error); // Final fallback: Use built-in player navigateToPlayer(stream); } diff --git a/src/screens/UpdateScreen.tsx b/src/screens/UpdateScreen.tsx index 53408b2d..c7fca3fd 100644 --- a/src/screens/UpdateScreen.tsx +++ b/src/screens/UpdateScreen.tsx @@ -99,7 +99,7 @@ const UpdateScreen: React.FC = () => { setLastOperation('No updates available'); } } catch (error) { - console.error('Error checking for updates:', error); + if (__DEV__) console.error('Error checking for updates:', error); setUpdateStatus('error'); setLastOperation(`Error: ${error instanceof Error ? error.message : 'Unknown error'}`); Alert.alert('Error', 'Failed to check for updates'); @@ -144,7 +144,7 @@ const UpdateScreen: React.FC = () => { Alert.alert('No Update', 'No update available to install'); } } catch (error) { - console.error('Error installing update:', error); + if (__DEV__) console.error('Error installing update:', error); setUpdateStatus('error'); setLastOperation(`Installation error: ${error instanceof Error ? error.message : 'Unknown error'}`); Alert.alert('Error', 'Failed to install update'); @@ -226,7 +226,7 @@ const UpdateScreen: React.FC = () => { setLastOperation('Update server is not reachable'); } } catch (error) { - console.error('Error testing connectivity:', error); + if (__DEV__) console.error('Error testing connectivity:', error); setLastOperation(`Connectivity test error: ${error instanceof Error ? error.message : 'Unknown error'}`); const logs = UpdateService.getLogs(); setLogs(logs); @@ -241,7 +241,7 @@ const UpdateScreen: React.FC = () => { setLogs(logs); setLastOperation('Asset URL testing completed'); } catch (error) { - console.error('Error testing asset URLs:', error); + if (__DEV__) console.error('Error testing asset URLs:', error); setLastOperation(`Asset URL test error: ${error instanceof Error ? error.message : 'Unknown error'}`); const logs = UpdateService.getLogs(); setLogs(logs); diff --git a/src/services/SyncService.ts b/src/services/SyncService.ts index f4e569d3..e4c53827 100644 --- a/src/services/SyncService.ts +++ b/src/services/SyncService.ts @@ -137,7 +137,7 @@ class SyncService { logger.log('[Sync] fullPull (debounced) start'); this.fullPull() .then(() => logger.log('[Sync] fullPull (debounced) done')) - .catch((e) => console.warn('[Sync] fullPull (debounced) error', e)); + .catch((e) => { if (__DEV__) console.warn('[Sync] fullPull (debounced) error', e); }); }, 300); }; @@ -168,7 +168,7 @@ class SyncService { logger.log(`[Sync][rt] user_library ${payload.eventType} ${mediaType}:${mediaId}`); } } catch (e) { - console.warn('[Sync][rt] user_library handler error', e); + if (__DEV__) console.warn('[Sync][rt] user_library handler error', e); } finally { this.suppressLibraryPush = false; } @@ -455,7 +455,7 @@ class SyncService { .select('media_type, media_id, title, poster_url, year, deleted_at, updated_at') .eq('user_id', userId); if (error) { - console.warn('[SyncService] pull library error', error); + if (__DEV__) console.warn('[SyncService] pull library error', error); return; } const obj: Record = {}; @@ -475,7 +475,7 @@ class SyncService { await AsyncStorage.setItem('stremio-library', JSON.stringify(obj)); logger.log(`[Sync] pull user_library wrote items=${Object.keys(obj).length}`); } catch (e) { - console.warn('[SyncService] pullLibrary exception', e); + if (__DEV__) console.warn('[SyncService] pullLibrary exception', e); } } @@ -516,12 +516,12 @@ class SyncService { const { error: upErr } = await supabase .from('user_library') .upsert(rows, { onConflict: 'user_id,media_type,media_id' }); - if (upErr) console.warn('[SyncService] push library upsert error', upErr); + if (upErr && __DEV__) console.warn('[SyncService] push library upsert error', upErr); else await AsyncStorage.setItem(`@user:${user.id}:library_initialized`, 'true'); } // No computed deletions; removals happen only via explicit user action (soft delete) } catch (e) { - console.warn('[SyncService] pushLibrary exception', e); + if (__DEV__) console.warn('[SyncService] pushLibrary exception', e); } } @@ -540,9 +540,9 @@ class SyncService { updated_at: new Date().toISOString(), }; const { error } = await supabase.from('user_library').upsert(row, { onConflict: 'user_id,media_type,media_id' }); - if (error) console.warn('[SyncService] pushLibraryAdd error', error); + if (error && __DEV__) console.warn('[SyncService] pushLibraryAdd error', error); } catch (e) { - console.warn('[SyncService] pushLibraryAdd exception', e); + if (__DEV__) console.warn('[SyncService] pushLibraryAdd exception', e); } } @@ -556,9 +556,9 @@ class SyncService { .eq('user_id', user.id) .eq('media_type', type === 'movie' ? 'movie' : 'series') .eq('media_id', id); - if (error) console.warn('[SyncService] pushLibraryRemove error', error); + if (error && __DEV__) console.warn('[SyncService] pushLibraryRemove error', error); } catch (e) { - console.warn('[SyncService] pushLibraryRemove exception', e); + if (__DEV__) console.warn('[SyncService] pushLibraryRemove exception', e); } } @@ -570,7 +570,7 @@ class SyncService { .eq('user_id', userId) .order('position', { ascending: true }); if (addonsErr) { - console.warn('[SyncService] pull addons error', addonsErr); + if (__DEV__) console.warn('[SyncService] pull addons error', addonsErr); return; } if (!(addons && Array.isArray(addons))) return; @@ -603,7 +603,7 @@ class SyncService { manifest.id = a.addon_id; map.set(a.addon_id, manifest); } catch (e) { - console.warn('[SyncService] failed to fetch manifest for', a.addon_id, e); + if (__DEV__) console.warn('[SyncService] failed to fetch manifest for', a.addon_id, e); } } @@ -715,7 +715,7 @@ class SyncService { const { error } = await supabase .from('watch_progress') .upsert(filteredRows, { onConflict: 'user_id,media_type,media_id,episode_id' }); - if (error) console.warn('[SyncService] push watch_progress error', error); + if (error && __DEV__) console.warn('[SyncService] push watch_progress error', error); else logger.log('[Sync] push watch_progress upsert ok'); } } catch (e) { @@ -723,7 +723,7 @@ class SyncService { const { error } = await supabase .from('watch_progress') .upsert(rows, { onConflict: 'user_id,media_type,media_id,episode_id' }); - if (error) console.warn('[SyncService] push watch_progress error', error); + if (error && __DEV__) console.warn('[SyncService] push watch_progress error', error); else logger.log('[Sync] push watch_progress upsert ok'); } } @@ -742,9 +742,9 @@ class SyncService { .eq('media_type', type) .eq('media_id', id) .eq('episode_id', episodeId || ''); - if (error) console.warn('[SyncService] softDeleteWatchProgress error', error); + if (error && __DEV__) console.warn('[SyncService] softDeleteWatchProgress error', error); } catch (e) { - console.warn('[SyncService] softDeleteWatchProgress exception', e); + if (__DEV__) console.warn('[SyncService] softDeleteWatchProgress exception', e); } } @@ -767,7 +767,7 @@ class SyncService { app_settings: appSettings, subtitle_settings: subtitleSettings, }); - if (error) console.warn('[SyncService] push settings error', error); + if (error && __DEV__) console.warn('[SyncService] push settings error', error); else logger.log('[Sync] push user_settings ok'); } @@ -807,14 +807,14 @@ class SyncService { .delete() .eq('user_id', userId) .in('addon_id', toDelete); - if (del.error) console.warn('[SyncService] delete addons error', del.error); + if (del.error && __DEV__) console.warn('[SyncService] delete addons error', del.error); } } } catch (e) { - console.warn('[SyncService] deletion sync for addons failed', e); + if (__DEV__) console.warn('[SyncService] deletion sync for addons failed', e); } const { error } = await supabase.from('installed_addons').upsert(rows, { onConflict: 'user_id,addon_id' }); - if (error) console.warn('[SyncService] push addons error', error); + if (error && __DEV__) console.warn('[SyncService] push addons error', error); } // Excluded: pushLocalScrapers (local scrapers are device-local only) diff --git a/src/services/catalogService.ts b/src/services/catalogService.ts index a1129fb5..6b5a81ce 100644 --- a/src/services/catalogService.ts +++ b/src/services/catalogService.ts @@ -844,18 +844,20 @@ class CatalogService { } async getStremioId(type: string, tmdbId: string): Promise { - console.log('=== CatalogService.getStremioId ==='); - console.log('Input type:', type); - console.log('Input tmdbId:', tmdbId); + if (__DEV__) { + console.log('=== CatalogService.getStremioId ==='); + console.log('Input type:', type); + console.log('Input tmdbId:', tmdbId); + } try { // For movies, use the tt prefix with IMDb ID if (type === 'movie') { - console.log('Processing movie - fetching TMDB details...'); + if (__DEV__) console.log('Processing movie - fetching TMDB details...'); const tmdbService = TMDBService.getInstance(); const movieDetails = await tmdbService.getMovieDetails(tmdbId); - console.log('Movie details result:', { + if (__DEV__) console.log('Movie details result:', { id: movieDetails?.id, title: movieDetails?.title, imdb_id: movieDetails?.imdb_id, @@ -863,7 +865,7 @@ class CatalogService { }); if (movieDetails?.imdb_id) { - console.log('Successfully found IMDb ID:', movieDetails.imdb_id); + if (__DEV__) console.log('Successfully found IMDb ID:', movieDetails.imdb_id); return movieDetails.imdb_id; } else { console.warn('No IMDb ID found for movie:', tmdbId); @@ -872,25 +874,25 @@ class CatalogService { } // For TV shows, get the IMDb ID like movies else if (type === 'tv' || type === 'series') { - console.log('Processing TV show - fetching TMDB details for IMDb ID...'); + if (__DEV__) console.log('Processing TV show - fetching TMDB details for IMDb ID...'); const tmdbService = TMDBService.getInstance(); // Get TV show external IDs to find IMDb ID const externalIds = await tmdbService.getShowExternalIds(parseInt(tmdbId)); - console.log('TV show external IDs result:', { + if (__DEV__) console.log('TV show external IDs result:', { tmdbId: tmdbId, imdb_id: externalIds?.imdb_id, hasImdbId: !!externalIds?.imdb_id }); if (externalIds?.imdb_id) { - console.log('Successfully found IMDb ID for TV show:', externalIds.imdb_id); + if (__DEV__) console.log('Successfully found IMDb ID for TV show:', externalIds.imdb_id); return externalIds.imdb_id; } else { console.warn('No IMDb ID found for TV show, falling back to kitsu format:', tmdbId); const fallbackId = `kitsu:${tmdbId}`; - console.log('Generated fallback Stremio ID for TV:', fallbackId); + if (__DEV__) console.log('Generated fallback Stremio ID for TV:', fallbackId); return fallbackId; } } @@ -899,11 +901,13 @@ class CatalogService { return null; } } catch (error: any) { - console.error('=== Error in getStremioId ==='); - console.error('Type:', type); - console.error('TMDB ID:', tmdbId); - console.error('Error details:', error); - console.error('Error message:', error.message); + if (__DEV__) { + console.error('=== Error in getStremioId ==='); + console.error('Type:', type); + console.error('TMDB ID:', tmdbId); + console.error('Error details:', error); + console.error('Error message:', error.message); + } logger.error('Error getting Stremio ID:', error); return null; } diff --git a/src/services/imageCacheService.ts b/src/services/imageCacheService.ts index 72b582a0..12344ee2 100644 --- a/src/services/imageCacheService.ts +++ b/src/services/imageCacheService.ts @@ -1,5 +1,6 @@ import { logger } from '../utils/logger'; import { Image as ExpoImage } from 'expo-image'; +import { AppState, AppStateStatus } from 'react-native'; interface CachedImage { url: string; @@ -14,16 +15,20 @@ interface CachedImage { class ImageCacheService { private cache = new Map(); private readonly CACHE_DURATION = 24 * 60 * 60 * 1000; // 24 hours - private readonly MAX_CACHE_SIZE = 100; // Increased maximum number of cached images - private readonly MAX_MEMORY_MB = 150; // Increased maximum memory usage in MB + private readonly MAX_CACHE_SIZE = 25; // Further reduced maximum number of cached images + private readonly MAX_MEMORY_MB = 40; // Further reduced maximum memory usage in MB private currentMemoryUsage = 0; private cleanupInterval: NodeJS.Timeout | null = null; + private appStateSubscription: any = null; constructor() { - // Start cleanup interval every 30 minutes (less churn) + // Start cleanup interval every 15 minutes (more frequent cleanup to reduce memory pressure) this.cleanupInterval = setInterval(() => { this.performCleanup(); - }, 30 * 60 * 1000); + }, 15 * 60 * 1000); + + // Reduce memory footprint when app goes to background + this.appStateSubscription = AppState.addEventListener('change', this.handleAppStateChange); } /** @@ -255,8 +260,32 @@ class ImageCacheService { clearInterval(this.cleanupInterval); this.cleanupInterval = null; } + if (this.appStateSubscription) { + this.appStateSubscription.remove(); + this.appStateSubscription = null; + } this.clearAllCache(); } + + private handleAppStateChange = (nextState: AppStateStatus) => { + if (nextState !== 'active') { + // On background/inactive, aggressively trim cache to 25% to reduce memory pressure + const targetSize = Math.floor(this.MAX_CACHE_SIZE * 0.25); + if (this.cache.size > targetSize) { + const entries = Array.from(this.cache.entries()); + const toRemove = this.cache.size - targetSize; + for (let i = 0; i < toRemove; i++) { + const [url, cached] = entries[i]; + this.cache.delete(url); + this.currentMemoryUsage -= cached.size || 0; + } + } + // Force aggressive memory cleanup + this.enforceMemoryLimits(); + // Clear any remaining memory pressure + this.currentMemoryUsage = Math.min(this.currentMemoryUsage, this.MAX_MEMORY_MB * 1024 * 1024 * 0.3); + } + }; } export const imageCacheService = new ImageCacheService(); \ No newline at end of file diff --git a/src/services/traktService.ts b/src/services/traktService.ts index 9b806921..5a6d1a4e 100644 --- a/src/services/traktService.ts +++ b/src/services/traktService.ts @@ -1,4 +1,5 @@ import AsyncStorage from '@react-native-async-storage/async-storage'; +import { AppState, AppStateStatus } from 'react-native'; import { logger } from '../utils/logger'; import { imageCacheService } from './imageCacheService'; @@ -260,7 +261,7 @@ export class TraktService { // Rate limiting private lastApiCall: number = 0; - private readonly MIN_API_INTERVAL = 2000; // Minimum 2 seconds between API calls (reduce heating) + private readonly MIN_API_INTERVAL = 3000; // Minimum 3 seconds between API calls (further reduce heating) private requestQueue: Array<() => Promise> = []; private isProcessingQueue: boolean = false; @@ -272,7 +273,7 @@ export class TraktService { // Track currently watching sessions to avoid duplicate starts// Sync debouncing private currentlyWatching: Set = new Set(); private lastSyncTimes: Map = new Map(); - private readonly SYNC_DEBOUNCE_MS = 15000; // 15 seconds to align with player save interval + private readonly SYNC_DEBOUNCE_MS = 20000; // 20 seconds to further reduce API calls // Debounce for stop calls private lastStopCalls: Map = new Map(); @@ -285,6 +286,9 @@ export class TraktService { // Increased cleanup interval from 5 minutes to 15 minutes to reduce heating setInterval(() => this.cleanupOldStopCalls(), 15 * 60 * 1000); // Clean up every 15 minutes + // Add AppState cleanup to reduce memory pressure + AppState.addEventListener('change', this.handleAppStateChange); + // Load user settings this.loadCompletionThreshold(); } @@ -1566,6 +1570,24 @@ export class TraktService { return []; } } + + /** + * Handle app state changes to reduce memory pressure + */ + private handleAppStateChange = (nextState: AppStateStatus) => { + if (nextState !== 'active') { + // Clear tracking maps to reduce memory pressure when app goes to background + this.scrobbledItems.clear(); + this.scrobbledTimestamps.clear(); + this.currentlyWatching.clear(); + this.lastSyncTimes.clear(); + this.lastStopCalls.clear(); + + // Clear request queue to prevent background processing + this.requestQueue = []; + this.isProcessingQueue = false; + } + }; } // Export a singleton instance diff --git a/src/services/updateService.ts b/src/services/updateService.ts index ef3d3f16..0df92974 100644 --- a/src/services/updateService.ts +++ b/src/services/updateService.ts @@ -38,8 +38,17 @@ export class UpdateService { this.logs = this.logs.slice(0, this.MAX_LOGS); } - // Always log to console - this will be visible in adb logcat for production builds - // Use different console methods for better filtering in logcat + // Console logging policy: + // - Development: log INFO/WARN/ERROR for visibility + // - Production: only log ERROR to reduce JS<->native bridge traffic and CPU usage + if (!__DEV__) { + if (level === 'ERROR') { + console.error(`[UpdateService] ${logEntry}`); + } + return; + } + + // Development detailed logging if (level === 'ERROR') { console.error(`[UpdateService] ${logEntry}`); } else if (level === 'WARN') { @@ -47,9 +56,11 @@ export class UpdateService { } else { console.log(`[UpdateService] ${logEntry}`); } - - // Also log with a consistent prefix for easy filtering - console.log(`UpdateService: ${logEntry}`); + + // Additional prefixed line for easier filtering during development only + if (__DEV__) { + console.log(`UpdateService: ${logEntry}`); + } } /**