diff --git a/src/components/metadata/HeroSection.tsx b/src/components/metadata/HeroSection.tsx index 9bb99417..04a3996e 100644 --- a/src/components/metadata/HeroSection.tsx +++ b/src/components/metadata/HeroSection.tsx @@ -97,7 +97,8 @@ const ActionButtons = memo(({ watchProgress, groupedEpisodes, metadata, - aiChatEnabled + aiChatEnabled, + settings }: { handleShowStreams: () => void; toggleLibrary: () => void; @@ -112,6 +113,7 @@ const ActionButtons = memo(({ groupedEpisodes?: { [seasonNumber: number]: any[] }; metadata: any; aiChatEnabled?: boolean; + settings: any; }) => { const { currentTheme } = useTheme(); @@ -135,7 +137,7 @@ const ActionButtons = memo(({ if (!isNaN(parsedId)) { finalTmdbId = parsedId; } - } else if (id.startsWith('tt')) { + } else if (id.startsWith('tt') && settings.enrichMetadataWithTMDB) { try { const tmdbService = TMDBService.getInstance(); const convertedId = await tmdbService.findTMDBIdByIMDB(id); @@ -158,7 +160,7 @@ const ActionButtons = memo(({ navigation.navigate('ShowRatings', { showId: finalTmdbId }); }); } - }, [id, navigation]); + }, [id, navigation, settings.enrichMetadataWithTMDB]); // Optimized play button style calculation const playButtonStyle = useMemo(() => { @@ -1538,6 +1540,7 @@ const HeroSection: React.FC = memo(({ groupedEpisodes={groupedEpisodes} metadata={metadata} aiChatEnabled={settings?.aiChatEnabled} + settings={settings} /> diff --git a/src/hooks/useMetadata.ts b/src/hooks/useMetadata.ts index d8d63305..04c4fc87 100644 --- a/src/hooks/useMetadata.ts +++ b/src/hooks/useMetadata.ts @@ -232,6 +232,28 @@ export const useMetadata = ({ id, type, addonId }: UseMetadataProps): UseMetadat (streams, addonId, addonName, error) => { const processTime = Date.now() - sourceStartTime; + console.log('🔍 [processStremioSource] Callback received:', { + addonId, + addonName, + streamCount: streams?.length || 0, + error: error?.message || null, + processTime + }); + + // ALWAYS remove from active fetching list when callback is received + // This ensures that even failed scrapers are removed from the "Fetching from:" chip + if (addonName) { + setActiveFetchingScrapers(prev => { + const updated = prev.filter(name => name !== addonName); + console.log('🔍 [processStremioSource] Removing from activeFetchingScrapers:', { + addonName, + before: prev, + after: updated + }); + return updated; + }); + } + // Update scraper status when we get a callback if (addonId && addonName) { setScraperStatuses(prevStatuses => { @@ -254,9 +276,6 @@ export const useMetadata = ({ id, type, addonId }: UseMetadataProps): UseMetadat return [...prevStatuses, newStatus]; } }); - - // Remove from active fetching list - setActiveFetchingScrapers(prev => prev.filter(name => name !== addonName)); } if (error) { @@ -314,7 +333,53 @@ export const useMetadata = ({ id, type, addonId }: UseMetadataProps): UseMetadat } catch (error) { // Catch errors from the initial call to getStreams (e.g., initialization errors) logger.error(`❌ [${logPrefix}:${sourceName}] Initial call failed:`, error); - // Maybe update state to show a general Stremio error? + + // Remove all addons and scrapers from active fetching since the entire request failed + setActiveFetchingScrapers(prev => { + // Get both Stremio addon names and local scraper names + const stremioAddons = stremioService.getInstalledAddons(); + const stremioNames = stremioAddons.map(addon => addon.name); + + // Get local scraper names + localScraperService.getInstalledScrapers().then(localScrapers => { + const localScraperNames = localScrapers.filter(s => s.enabled).map(s => s.name); + const allNames = [...stremioNames, ...localScraperNames]; + + // Remove all from active fetching + setActiveFetchingScrapers(current => + current.filter(name => !allNames.includes(name)) + ); + }).catch(() => { + // If we can't get local scrapers, just remove Stremio addons + setActiveFetchingScrapers(current => + current.filter(name => !stremioNames.includes(name)) + ); + }); + + // Immediately remove Stremio addons (local scrapers will be removed async above) + return prev.filter(name => !stremioNames.includes(name)); + }); + + // Update scraper statuses to mark all scrapers as failed + setScraperStatuses(prevStatuses => { + const stremioAddons = stremioService.getInstalledAddons(); + + return prevStatuses.map(status => { + const isStremioAddon = stremioAddons.some(addon => addon.id === status.id || addon.name === status.name); + + // Mark both Stremio addons and local scrapers as failed + if (isStremioAddon || !status.hasCompleted) { + return { + ...status, + isLoading: false, + hasCompleted: true, + error: error instanceof Error ? error.message : 'Initial request failed', + endTime: Date.now() + }; + } + return status; + }); + }); } // Note: This function completes when getStreams returns, not when all callbacks have fired. // Loading indicators should probably be managed based on callbacks completing. @@ -358,9 +423,9 @@ export const useMetadata = ({ id, type, addonId }: UseMetadataProps): UseMetadat } } - // Handle IMDb IDs or convert to TMDB ID + // Handle IMDb IDs or convert to TMDB ID (only if enrichment is enabled) let tmdbId; - if (id.startsWith('tt')) { + if (id.startsWith('tt') && settings.enrichMetadataWithTMDB) { if (__DEV__) logger.log('[loadCast] Converting IMDb ID to TMDB ID'); tmdbId = await tmdbService.findTMDBIdByIMDB(id); } @@ -1119,9 +1184,13 @@ export const useMetadata = ({ id, type, addonId }: UseMetadataProps): UseMetadat } else if (id.startsWith('tt')) { // This is already an IMDB ID, perfect for Stremio stremioId = id; - if (__DEV__) console.log('📝 [loadStreams] Converting IMDB ID to TMDB ID...'); - tmdbId = await withTimeout(tmdbService.findTMDBIdByIMDB(id), API_TIMEOUT); - if (__DEV__) console.log('✅ [loadStreams] Converted to TMDB ID:', tmdbId); + if (settings.enrichMetadataWithTMDB) { + if (__DEV__) console.log('📝 [loadStreams] Converting IMDB ID to TMDB ID...'); + tmdbId = await withTimeout(tmdbService.findTMDBIdByIMDB(id), API_TIMEOUT); + if (__DEV__) console.log('✅ [loadStreams] Converted to TMDB ID:', tmdbId); + } else { + if (__DEV__) console.log('📝 [loadStreams] TMDB enrichment disabled, skipping IMDB to TMDB conversion'); + } } else { tmdbId = id; stremioId = id; @@ -1215,6 +1284,7 @@ export const useMetadata = ({ id, type, addonId }: UseMetadataProps): UseMetadat setScraperStatuses(initialStatuses); setActiveFetchingScrapers(initialActiveFetching); + console.log('🔍 [loadStreams] Initialized activeFetchingScrapers:', initialActiveFetching); } catch (error) { if (__DEV__) console.error('Failed to initialize scraper tracking:', error); } @@ -1243,6 +1313,14 @@ export const useMetadata = ({ id, type, addonId }: UseMetadataProps): UseMetadat clearInterval(completionInterval); setLoadingStreams(false); setActiveFetchingScrapers([]); + // Mark all incomplete scrapers as failed + setScraperStatuses(prevStatuses => + prevStatuses.map(status => + !status.hasCompleted && !status.error + ? { ...status, isLoading: false, hasCompleted: true, error: 'Request timed out', endTime: Date.now() } + : status + ) + ); }, 60000); } catch (error) { @@ -1335,6 +1413,7 @@ export const useMetadata = ({ id, type, addonId }: UseMetadataProps): UseMetadat setScraperStatuses(initialStatuses); setActiveFetchingScrapers(initialActiveFetching); + console.log('🔍 [loadEpisodeStreams] Initialized activeFetchingScrapers:', initialActiveFetching); } catch (error) { if (__DEV__) console.error('Failed to initialize episode scraper tracking:', error); } @@ -1356,7 +1435,9 @@ export const useMetadata = ({ id, type, addonId }: UseMetadataProps): UseMetadat // episodeId format for collections: "tt7888964" (IMDb ID of individual movie) if (episodeId.startsWith('tt')) { // This is an IMDb ID of an individual movie in the collection - tmdbId = await withTimeout(tmdbService.findTMDBIdByIMDB(episodeId), API_TIMEOUT); + if (settings.enrichMetadataWithTMDB) { + tmdbId = await withTimeout(tmdbService.findTMDBIdByIMDB(episodeId), API_TIMEOUT); + } stremioEpisodeId = episodeId; // Use the IMDb ID directly for Stremio addons if (__DEV__) console.log('✅ [loadEpisodeStreams] Collection movie - using IMDb ID:', episodeId, 'TMDB ID:', tmdbId); } else { @@ -1397,8 +1478,12 @@ export const useMetadata = ({ id, type, addonId }: UseMetadataProps): UseMetadat } } else if (id.startsWith('tt')) { // This is already an IMDB ID, perfect for Stremio - if (__DEV__) console.log('📝 [loadEpisodeStreams] Converting IMDB ID to TMDB ID...'); - tmdbId = await withTimeout(tmdbService.findTMDBIdByIMDB(id), API_TIMEOUT); + if (settings.enrichMetadataWithTMDB) { + if (__DEV__) console.log('📝 [loadEpisodeStreams] Converting IMDB ID to TMDB ID...'); + tmdbId = await withTimeout(tmdbService.findTMDBIdByIMDB(id), API_TIMEOUT); + } else { + if (__DEV__) console.log('📝 [loadEpisodeStreams] TMDB enrichment disabled, skipping IMDB to TMDB conversion'); + } if (__DEV__) console.log('✅ [loadEpisodeStreams] Converted to TMDB ID:', tmdbId); // Normalize episode id to 'tt:season:episode' format for addons that expect tt prefix const parts = episodeId.split(':'); @@ -1447,6 +1532,14 @@ export const useMetadata = ({ id, type, addonId }: UseMetadataProps): UseMetadat clearInterval(episodeCompletionInterval); setLoadingEpisodeStreams(false); setActiveFetchingScrapers([]); + // Mark all incomplete scrapers as failed + setScraperStatuses(prevStatuses => + prevStatuses.map(status => + !status.hasCompleted && !status.error + ? { ...status, isLoading: false, hasCompleted: true, error: 'Request timed out', endTime: Date.now() } + : status + ) + ); }, 60000); } catch (error) { diff --git a/src/hooks/useMetadataAssets.ts b/src/hooks/useMetadataAssets.ts index 76f6cc64..66961260 100644 --- a/src/hooks/useMetadataAssets.ts +++ b/src/hooks/useMetadataAssets.ts @@ -150,7 +150,7 @@ export const useMetadataAssets = ( // Extract or find TMDB ID in one step if (id.startsWith('tmdb:')) { tmdbId = id.split(':')[1]; - } else if (imdbId) { + } else if (imdbId && settings.enrichMetadataWithTMDB) { try { const tmdbService = TMDBService.getInstance(); const foundId = await tmdbService.findTMDBIdByIMDB(imdbId); @@ -248,7 +248,7 @@ export const useMetadataAssets = ( tmdbId = foundTmdbId; } else if ((metadata as any).tmdbId) { tmdbId = (metadata as any).tmdbId; - } else if (imdbId) { + } else if (imdbId && settings.enrichMetadataWithTMDB) { try { const tmdbService = TMDBService.getInstance(); const foundId = await tmdbService.findTMDBIdByIMDB(imdbId); diff --git a/src/screens/StreamsScreen.tsx b/src/screens/StreamsScreen.tsx index 4b7f5320..e19c1f3c 100644 --- a/src/screens/StreamsScreen.tsx +++ b/src/screens/StreamsScreen.tsx @@ -1427,6 +1427,18 @@ export const StreamsScreen = () => { const sections = useMemo(() => { const streams = metadata?.videos && metadata.videos.length > 1 && selectedEpisode ? episodeStreams : groupedStreams; const installedAddons = stremioService.getInstalledAddons(); + + console.log('🔍 [StreamsScreen] Sections debug:', { + streamsKeys: Object.keys(streams), + installedAddons: installedAddons.map(a => ({ id: a.id, name: a.name })), + selectedProvider, + streamDisplayMode: settings.streamDisplayMode, + streamsData: Object.entries(streams).map(([key, data]) => ({ + provider: key, + addonName: data.addonName, + streamCount: data.streams?.length || 0 + })) + }); // Filter streams by selected provider const filteredEntries = Object.entries(streams) @@ -1444,8 +1456,18 @@ export const StreamsScreen = () => { // Otherwise only show the selected provider return addonId === selectedProvider; - }) - .sort(([addonIdA], [addonIdB]) => { + }); + + console.log('🔍 [StreamsScreen] Filtered entries:', { + filteredCount: filteredEntries.length, + filteredEntries: filteredEntries.map(([addonId, data]) => ({ + addonId, + addonName: data.addonName, + streamCount: data.streams?.length || 0 + })) + }); + + const sortedEntries = filteredEntries.sort(([addonIdA], [addonIdB]) => { // Sort by response order (actual order addons responded) const indexA = addonResponseOrder.indexOf(addonIdA); const indexB = addonResponseOrder.indexOf(addonIdB); @@ -1470,7 +1492,7 @@ export const StreamsScreen = () => { const pluginStreams: Stream[] = []; let totalOriginalCount = 0; - filteredEntries.forEach(([addonId, { addonName, streams: providerStreams }]) => { + sortedEntries.forEach(([addonId, { addonName, streams: providerStreams }]) => { const isInstalledAddon = installedAddons.some(addon => addon.id === addonId); // Count original streams before filtering @@ -1572,15 +1594,25 @@ export const StreamsScreen = () => { combinedStreams.push(...pluginStreams); } - return [{ + const result = [{ title: 'Available Streams', addonId: 'grouped-all', data: combinedStreams, isEmptyDueToQualityFilter: false }]; + + console.log('🔍 [StreamsScreen] Grouped mode result:', { + resultCount: result.length, + combinedStreamsCount: combinedStreams.length, + addonStreamsCount: addonStreams.length, + pluginStreamsCount: pluginStreams.length, + totalOriginalCount + }); + + return result; } else { // Use separate sections for each provider (current behavior) - return filteredEntries.map(([addonId, { addonName, streams: providerStreams }]) => { + return sortedEntries.map(([addonId, { addonName, streams: providerStreams }]) => { const isInstalledAddon = installedAddons.some(addon => addon.id === addonId); // Count original streams before filtering @@ -1591,8 +1623,25 @@ export const StreamsScreen = () => { // Only apply quality filtering to plugins, NOT addons if (!isInstalledAddon) { + console.log('🔍 [StreamsScreen] Applying quality filter to plugin:', { + addonId, + addonName, + originalCount, + excludedQualities: settings.excludedQualities + }); filteredStreams = filterStreamsByQuality(providerStreams); isEmptyDueToQualityFilter = originalCount > 0 && filteredStreams.length === 0; + console.log('🔍 [StreamsScreen] Quality filter result:', { + addonId, + filteredCount: filteredStreams.length, + isEmptyDueToQualityFilter + }); + } else { + console.log('🔍 [StreamsScreen] Skipping quality filter for addon:', { + addonId, + addonName, + originalCount + }); } if (isEmptyDueToQualityFilter) { @@ -1661,15 +1710,38 @@ export const StreamsScreen = () => { }); } - return { + const result = { title: addonName, addonId, data: processedStreams, isEmptyDueToQualityFilter: false }; + + console.log('🔍 [StreamsScreen] Individual mode result:', { + addonId, + addonName, + processedStreamsCount: processedStreams.length, + originalCount, + isInstalledAddon + }); + + return result; }); } - }, [selectedProvider, type, episodeStreams, groupedStreams, settings.streamDisplayMode, filterStreamsByQuality, addonResponseOrder, settings.streamSortMode]); + }, [selectedProvider, type, episodeStreams, groupedStreams, settings.streamDisplayMode, filterStreamsByQuality, addonResponseOrder, settings.streamSortMode, selectedEpisode, metadata]); + + // Debug log for sections result + React.useEffect(() => { + console.log('🔍 [StreamsScreen] Final sections:', { + sectionsCount: sections.length, + sections: sections.map(s => ({ + title: s.title, + addonId: s.addonId, + dataCount: s.data?.length || 0, + isEmptyDueToQualityFilter: s.isEmptyDueToQualityFilter + })) + }); + }, [sections]); const episodeImage = useMemo(() => { if (episodeThumbnail) { @@ -1726,6 +1798,25 @@ export const StreamsScreen = () => { const showInitialLoading = streamsEmpty && (streamsLoadStart === null || loadElapsed < 10000); const showStillFetching = streamsEmpty && loadElapsed >= 10000; + // Debug logging for stream availability + React.useEffect(() => { + console.log('🔍 [StreamsScreen] Streams debug:', { + streamsEmpty, + streamsKeys: Object.keys(streams), + streamsData: Object.entries(streams).map(([key, data]) => ({ + provider: key, + addonName: data.addonName, + streamCount: data.streams?.length || 0, + streams: data.streams?.slice(0, 3).map(s => ({ name: s.name, title: s.title })) || [] + })), + isLoading, + loadingStreams, + loadingEpisodeStreams, + selectedEpisode, + type + }); + }, [streams, streamsEmpty, isLoading, loadingStreams, loadingEpisodeStreams, selectedEpisode, type]); + const renderSectionHeader = useCallback(({ section }: { section: { title: string; addonId: string; isEmptyDueToQualityFilter?: boolean } }) => {