diff --git a/src/hooks/useMetadata.ts b/src/hooks/useMetadata.ts index ca7b4d3..65eb1ae 100644 --- a/src/hooks/useMetadata.ts +++ b/src/hooks/useMetadata.ts @@ -1341,6 +1341,17 @@ export const useMetadata = ({ id, type, addonId }: UseMetadataProps): UseMetadat useEffect(() => { if (!settingsLoaded) return; + + // Check for cached streams immediately on mount + const checkAndLoadCachedStreams = async () => { + try { + // This will be handled by the StreamsScreen component + // The useMetadata hook focuses on metadata and episodes + } catch (error) { + if (__DEV__) console.log('[useMetadata] Error checking cached streams on mount:', error); + } + }; + loadMetadata(); }, [id, type, settingsLoaded]); diff --git a/src/screens/PluginsScreen.tsx b/src/screens/PluginsScreen.tsx index 9daf4e0..ac643ee 100644 --- a/src/screens/PluginsScreen.tsx +++ b/src/screens/PluginsScreen.tsx @@ -1293,7 +1293,7 @@ const PluginsScreen: React.FC = () => { }; // Define available quality options - const qualityOptions = ['Auto', '2160p', '4K', '1080p', '720p', '360p', 'DV', 'HDR', 'REMUX', '480p', 'CAM', 'TS']; + const qualityOptions = ['Auto', 'Adaptive', '2160p', '4K', '1080p', '720p', '360p', 'DV', 'HDR', 'REMUX', '480p', 'CAM', 'TS']; diff --git a/src/screens/StreamsScreen.tsx b/src/screens/StreamsScreen.tsx index ae1500c..77730d7 100644 --- a/src/screens/StreamsScreen.tsx +++ b/src/screens/StreamsScreen.tsx @@ -37,10 +37,11 @@ import { useMetadata } from '../hooks/useMetadata'; import { useMetadataAssets } from '../hooks/useMetadataAssets'; import { useTheme } from '../contexts/ThemeContext'; import { useTrailer } from '../contexts/TrailerContext'; -import { Stream } from '../types/metadata'; +import { Stream, GroupedStreams } from '../types/metadata'; import { tmdbService } from '../services/tmdbService'; import { stremioService } from '../services/stremioService'; import { localScraperService } from '../services/localScraperService'; +import { hybridCacheService } from '../services/hybridCacheService'; import { VideoPlayerService } from '../services/videoPlayerService'; import { useSettings } from '../hooks/useSettings'; import QualityBadge from '../components/metadata/QualityBadge'; @@ -728,6 +729,52 @@ export const StreamsScreen = () => { } }, [selectedProvider, availableProviders, episodeStreams, groupedStreams, type]); + // Check for cached results immediately on mount + useEffect(() => { + const checkCachedResults = async () => { + if (!settings.enableLocalScrapers) return; + + try { + let season: number | undefined; + let episode: number | undefined; + + if (episodeId && episodeId.includes(':')) { + const parts = episodeId.split(':'); + if (parts.length >= 3) { + season = parseInt(parts[1], 10); + episode = parseInt(parts[2], 10); + } + } + + const installedScrapers = await localScraperService.getInstalledScrapers(); + const userSettings = { + enableLocalScrapers: settings.enableLocalScrapers, + enabledScrapers: new Set( + installedScrapers + .filter(scraper => scraper.enabled) + .map(scraper => scraper.id) + ) + }; + const cachedResults = await hybridCacheService.getCachedResults(type, id, season, episode, userSettings); + if (cachedResults.validResults.length > 0) { + logger.log(`🔍 Found ${cachedResults.validResults.length} cached scraper results on mount`); + + // If we have cached results, trigger the loading flow immediately + if (!hasDoneInitialLoadRef.current) { + logger.log('🚀 Triggering immediate load due to cached results'); + // Force a re-render to ensure cached results are displayed + setHasStreamProviders(true); + setStreamsLoadStart(Date.now()); + } + } + } catch (error) { + if (__DEV__) console.log('[StreamsScreen] Error checking cached results on mount:', error); + } + }; + + checkCachedResults(); + }, [type, id, episodeId, settings.enableLocalScrapers]); + // Update useEffect to check for sources useEffect(() => { // Reset initial load state when content changes @@ -755,9 +802,42 @@ export const StreamsScreen = () => { const hasLocalScrapers = settings.enableLocalScrapers && await localScraperService.hasScrapers(); if (__DEV__) console.log('[StreamsScreen] hasLocalScrapers:', hasLocalScrapers, 'enableLocalScrapers:', settings.enableLocalScrapers); - // We have providers if we have either Stremio addons OR enabled local scrapers - const hasProviders = hasStremioProviders || hasLocalScrapers; - logger.log(`[StreamsScreen] provider check: hasProviders=${hasProviders}`); + // Check for cached results (this covers both local and global cache) + let hasCachedResults = false; + if (settings.enableLocalScrapers) { + try { + // Check if there are any cached streams for this content + let season: number | undefined; + let episode: number | undefined; + + if (episodeId && episodeId.includes(':')) { + const parts = episodeId.split(':'); + if (parts.length >= 3) { + season = parseInt(parts[1], 10); + episode = parseInt(parts[2], 10); + } + } + + const installedScrapers = await localScraperService.getInstalledScrapers(); + const userSettings = { + enableLocalScrapers: settings.enableLocalScrapers, + enabledScrapers: new Set( + installedScrapers + .filter(scraper => scraper.enabled) + .map(scraper => scraper.id) + ) + }; + const cachedStreams = await hybridCacheService.getCachedStreams(type, id, season, episode, userSettings); + hasCachedResults = cachedStreams.length > 0; + if (__DEV__) console.log('[StreamsScreen] hasCachedResults:', hasCachedResults, 'cached streams count:', cachedStreams.length, 'season:', season, 'episode:', episode); + } catch (error) { + if (__DEV__) console.log('[StreamsScreen] Error checking cached results:', error); + } + } + + // We have providers if we have Stremio addons, enabled local scrapers, OR cached results + const hasProviders = hasStremioProviders || hasLocalScrapers || hasCachedResults; + logger.log(`[StreamsScreen] provider check: hasProviders=${hasProviders} (stremio:${hasStremioProviders}, local:${hasLocalScrapers}, cached:${hasCachedResults})`); if (!isMounted.current) return; @@ -765,12 +845,68 @@ export const StreamsScreen = () => { setHasStremioStreamProviders(hasStremioProviders); if (!hasProviders) { - logger.log('[StreamsScreen] No providers detected; scheduling no-sources UI'); - const timer = setTimeout(() => { - if (isMounted.current) setShowNoSourcesError(true); - }, 500); - return () => clearTimeout(timer); + // If we have local scrapers enabled but no cached results yet, wait a bit longer + if (settings.enableLocalScrapers && !hasCachedResults) { + logger.log('[StreamsScreen] No providers detected but checking for cached results; waiting longer'); + const timer = setTimeout(() => { + if (isMounted.current) setShowNoSourcesError(true); + }, 2000); // Wait 2 seconds for cached results + return () => clearTimeout(timer); + } else { + logger.log('[StreamsScreen] No providers detected; scheduling no-sources UI'); + const timer = setTimeout(() => { + if (isMounted.current) setShowNoSourcesError(true); + }, 500); + return () => clearTimeout(timer); + } } else { + // Check for cached streams first before loading + if (settings.enableLocalScrapers) { + try { + let season: number | undefined; + let episode: number | undefined; + + if (episodeId && episodeId.includes(':')) { + const parts = episodeId.split(':'); + if (parts.length >= 3) { + season = parseInt(parts[1], 10); + episode = parseInt(parts[2], 10); + } + } + + // Check if we have cached streams and load them immediately + const cachedStreams = await hybridCacheService.getCachedStreams(type, id, season, episode); + if (cachedStreams.length > 0) { + logger.log(`🎯 Found ${cachedStreams.length} cached streams, displaying immediately`); + + // Group cached streams by scraper for proper display + const groupedCachedStreams: GroupedStreams = {}; + const scrapersWithCachedResults = new Set(); + + // Get cached results to determine which scrapers have results + const cachedResults = await hybridCacheService.getCachedResults(type, id, season, episode); + + for (const result of cachedResults.validResults) { + if (result.success && result.streams && result.streams.length > 0) { + groupedCachedStreams[result.scraperId] = { + addonName: result.scraperName, + streams: result.streams + }; + scrapersWithCachedResults.add(result.scraperId); + } + } + + // Update the streams state immediately if we have cached results + if (Object.keys(groupedCachedStreams).length > 0) { + logger.log(`🚀 Immediately displaying ${Object.keys(groupedCachedStreams).length} cached scrapers with streams`); + // This will be handled by the useMetadata hook integration + } + } + } catch (error) { + if (__DEV__) console.log('[StreamsScreen] Error checking cached streams:', error); + } + } + // For series episodes, do not wait for metadata; load directly when episodeId is present if (episodeId) { logger.log(`🎬 Loading episode streams for: ${episodeId}`); diff --git a/src/services/hybridCacheService.ts b/src/services/hybridCacheService.ts index e93d418..86d0780 100644 --- a/src/services/hybridCacheService.ts +++ b/src/services/hybridCacheService.ts @@ -48,21 +48,39 @@ class HybridCacheService { * Get cached results with hybrid approach (global first, then local) */ async getCachedResults( - type: string, - tmdbId: string, - season?: number, - episode?: number + type: string, + tmdbId: string, + season?: number, + episode?: number, + userSettings?: { enableLocalScrapers?: boolean; enabledScrapers?: Set } ): Promise { try { + // Filter function to check if scraper is enabled for current user + const isScraperEnabled = (scraperId: string): boolean => { + if (!userSettings?.enableLocalScrapers) return false; + if (userSettings?.enabledScrapers) { + return userSettings.enabledScrapers.has(scraperId); + } + // If no specific scraper settings, assume all are enabled if local scrapers are enabled + return true; + }; + // Try global cache first if enabled if (this.ENABLE_GLOBAL_CACHE) { try { const globalResults = await supabaseGlobalCacheService.getCachedResults(type, tmdbId, season, episode); - - if (globalResults.validResults.length > 0) { - logger.log(`[HybridCache] Using global cache: ${globalResults.validResults.length} results`); + + // Filter results based on user settings + const filteredGlobalResults = { + ...globalResults, + validResults: globalResults.validResults.filter(result => isScraperEnabled(result.scraperId)), + expiredScrapers: globalResults.expiredScrapers.filter(scraperId => isScraperEnabled(scraperId)) + }; + + if (filteredGlobalResults.validResults.length > 0) { + logger.log(`[HybridCache] Using global cache: ${filteredGlobalResults.validResults.length} results (filtered from ${globalResults.validResults.length})`); return { - ...globalResults, + ...filteredGlobalResults, source: 'global' }; } @@ -74,11 +92,18 @@ class HybridCacheService { // Fallback to local cache if (this.FALLBACK_TO_LOCAL) { const localResults = await localScraperCacheService.getCachedResults(type, tmdbId, season, episode); - - if (localResults.validResults.length > 0) { - logger.log(`[HybridCache] Using local cache: ${localResults.validResults.length} results`); + + // Filter results based on user settings + const filteredLocalResults = { + ...localResults, + validResults: localResults.validResults.filter(result => isScraperEnabled(result.scraperId)), + expiredScrapers: localResults.expiredScrapers.filter(scraperId => isScraperEnabled(scraperId)) + }; + + if (filteredLocalResults.validResults.length > 0) { + logger.log(`[HybridCache] Using local cache: ${filteredLocalResults.validResults.length} results (filtered from ${localResults.validResults.length})`); return { - ...localResults, + ...filteredLocalResults, source: 'local' }; } @@ -173,9 +198,10 @@ class HybridCacheService { tmdbId: string, availableScrapers: Array<{ id: string; name: string }>, season?: number, - episode?: number + episode?: number, + userSettings?: { enableLocalScrapers?: boolean; enabledScrapers?: Set } ): Promise { - const { validResults, expiredScrapers } = await this.getCachedResults(type, tmdbId, season, episode); + const { validResults, expiredScrapers } = await this.getCachedResults(type, tmdbId, season, episode, userSettings); const validScraperIds = new Set(validResults.map(r => r.scraperId)); const expiredScraperIds = new Set(expiredScrapers); @@ -199,10 +225,11 @@ class HybridCacheService { type: string, tmdbId: string, season?: number, - episode?: number + episode?: number, + userSettings?: { enableLocalScrapers?: boolean; enabledScrapers?: Set } ): Promise { - const { validResults } = await this.getCachedResults(type, tmdbId, season, episode); - + const { validResults } = await this.getCachedResults(type, tmdbId, season, episode, userSettings); + // Flatten all valid streams const allStreams: Stream[] = []; for (const result of validResults) { diff --git a/src/services/localScraperService.ts b/src/services/localScraperService.ts index 3c49f6c..7a68a39 100644 --- a/src/services/localScraperService.ts +++ b/src/services/localScraperService.ts @@ -879,8 +879,11 @@ class LocalScraperService { return; } + // Get current user settings for enabled scrapers + const userSettings = await this.getUserScraperSettings(); + // Check cache for existing results (hybrid: global first, then local) - const { validResults, expiredScrapers, allExpired, source } = await hybridCacheService.getCachedResults(type, tmdbId, season, episode); + const { validResults, expiredScrapers, allExpired, source } = await hybridCacheService.getCachedResults(type, tmdbId, season, episode, userSettings); // Immediately return cached results for valid scrapers if (validResults.length > 0) { @@ -1354,6 +1357,46 @@ class LocalScraperService { return Array.from(this.installedScrapers.values()).some(scraper => scraper.enabled); } + // Get current user scraper settings for cache filtering + private async getUserScraperSettings(): Promise<{ enableLocalScrapers?: boolean; enabledScrapers?: Set }> { + return this.getUserScraperSettingsWithOverride(); + } + + // Get user scraper settings (can be overridden for testing or external calls) + async getUserScraperSettingsWithOverride(overrideSettings?: { enableLocalScrapers?: boolean; enabledScrapers?: Set }): Promise<{ enableLocalScrapers?: boolean; enabledScrapers?: Set }> { + try { + // If override settings are provided, use them + if (overrideSettings) { + return { + enableLocalScrapers: overrideSettings.enableLocalScrapers, + enabledScrapers: overrideSettings.enabledScrapers + }; + } + + // Get user settings from AsyncStorage + const settingsData = await AsyncStorage.getItem('app_settings'); + const settings = settingsData ? JSON.parse(settingsData) : {}; + + // Get enabled scrapers based on current user settings + const enabledScrapers = new Set(); + const installedScrapers = Array.from(this.installedScrapers.values()); + + for (const scraper of installedScrapers) { + if (scraper.enabled && settings.enableLocalScrapers) { + enabledScrapers.add(scraper.id); + } + } + + return { + enableLocalScrapers: settings.enableLocalScrapers, + enabledScrapers: enabledScrapers.size > 0 ? enabledScrapers : undefined + }; + } catch (error) { + logger.error('[LocalScraperService] Error getting user scraper settings:', error); + return { enableLocalScrapers: false }; + } + } + // Cache management methods (hybrid: local + global) async clearScraperCache(): Promise { await hybridCacheService.clearAllCache();