From 77b8b59734b532dd61ade9d3d561ca5c071be920 Mon Sep 17 00:00:00 2001 From: tapframe <85391825+tapframe@users.noreply.github.com> Date: Thu, 5 Feb 2026 12:17:38 +0530 Subject: [PATCH] fix: stream addon selection logic with fallback for non-standard types --- src/hooks/useMetadata.ts | 163 +++++++++++++------------ src/navigation/AppNavigator.tsx | 1 - src/services/stremioService.ts | 206 +++++++++++++++++++++++--------- 3 files changed, 237 insertions(+), 133 deletions(-) diff --git a/src/hooks/useMetadata.ts b/src/hooks/useMetadata.ts index b0d1b5c6..48cbdd72 100644 --- a/src/hooks/useMetadata.ts +++ b/src/hooks/useMetadata.ts @@ -1486,10 +1486,10 @@ export const useMetadata = ({ id, type, addonId }: UseMetadataProps): UseMetadat setActiveFetchingScrapers([]); setAddonResponseOrder([]); // Reset response order - // Get TMDB ID for external sources and determine the correct ID for Stremio addons if (__DEV__) console.log('🔍 [loadStreams] Getting TMDB ID for:', id); let tmdbId; - let stremioId = id; // Default to original ID + let stremioId = id; + let effectiveStreamType: string = type; if (id.startsWith('tmdb:')) { tmdbId = id.split(':')[1]; @@ -1544,56 +1544,66 @@ export const useMetadata = ({ id, type, addonId }: UseMetadataProps): UseMetadat const allStremioAddons = await stremioService.getInstalledAddons(); const localScrapers = await localScraperService.getInstalledScrapers(); - // Map app-level "tv" type to Stremio "series" for addon capability checks - const stremioType = type === 'tv' ? 'series' : type; + const requestedStreamType = type; - // Filter Stremio addons to only include those that provide streams for this content type - const streamAddons = allStremioAddons.filter(addon => { - if (!addon.resources || !Array.isArray(addon.resources)) { - return false; - } + const pickEligibleStreamAddons = (requestType: string) => + allStremioAddons.filter(addon => { + if (!addon.resources || !Array.isArray(addon.resources)) { + return false; + } - let hasStreamResource = false; - let supportsIdPrefix = false; + let hasStreamResource = false; + let supportsIdPrefix = false; - for (const resource of addon.resources) { - // Check if the current element is a ResourceObject - if (typeof resource === 'object' && resource !== null && 'name' in resource) { - const typedResource = resource as any; - if (typedResource.name === 'stream' && - Array.isArray(typedResource.types) && - typedResource.types.includes(stremioType)) { - hasStreamResource = true; + for (const resource of addon.resources) { + if (typeof resource === 'object' && resource !== null && 'name' in resource) { + const typedResource = resource as any; + if (typedResource.name === 'stream' && + Array.isArray(typedResource.types) && + typedResource.types.includes(requestType)) { + hasStreamResource = true; - // Check if this addon supports the ID prefix generically: any prefix must match start of id - if (Array.isArray(typedResource.idPrefixes) && typedResource.idPrefixes.length > 0) { - supportsIdPrefix = typedResource.idPrefixes.some((p: string) => id.startsWith(p)); - } else { - // If no idPrefixes specified, assume it supports all prefixes - supportsIdPrefix = true; + if (Array.isArray(typedResource.idPrefixes) && typedResource.idPrefixes.length > 0) { + supportsIdPrefix = typedResource.idPrefixes.some((p: string) => stremioId.startsWith(p)); + } else { + supportsIdPrefix = true; + } + break; + } + } else if (typeof resource === 'string' && resource === 'stream' && addon.types) { + if (Array.isArray(addon.types) && addon.types.includes(requestType)) { + hasStreamResource = true; + if (addon.idPrefixes && Array.isArray(addon.idPrefixes) && addon.idPrefixes.length > 0) { + supportsIdPrefix = addon.idPrefixes.some((p: string) => stremioId.startsWith(p)); + } else { + supportsIdPrefix = true; + } + break; } - break; } } - // Check if the element is the simple string "stream" AND the addon has a top-level types array - else if (typeof resource === 'string' && resource === 'stream' && addon.types) { - if (Array.isArray(addon.types) && addon.types.includes(stremioType)) { - hasStreamResource = true; - // For simple string resources, check addon-level idPrefixes generically - if (addon.idPrefixes && Array.isArray(addon.idPrefixes) && addon.idPrefixes.length > 0) { - supportsIdPrefix = addon.idPrefixes.some((p: string) => id.startsWith(p)); - } else { - // If no idPrefixes specified, assume it supports all prefixes - supportsIdPrefix = true; - } - break; - } + + return hasStreamResource && supportsIdPrefix; + }); + + effectiveStreamType = requestedStreamType; + let eligibleStreamAddons = pickEligibleStreamAddons(requestedStreamType); + + if (eligibleStreamAddons.length === 0) { + const fallbackTypes = ['series', 'movie'].filter(t => t !== requestedStreamType); + for (const fallbackType of fallbackTypes) { + const fallback = pickEligibleStreamAddons(fallbackType); + if (fallback.length > 0) { + effectiveStreamType = fallbackType; + eligibleStreamAddons = fallback; + if (__DEV__) console.log(`[useMetadata.loadStreams] No addons for '${requestedStreamType}', falling back to '${fallbackType}'`); + break; } } + } - return hasStreamResource && supportsIdPrefix; - }); - if (__DEV__) console.log('[useMetadata.loadStreams] Eligible stream addons:', streamAddons.map(a => a.id)); + const streamAddons = eligibleStreamAddons; + if (__DEV__) console.log('[useMetadata.loadStreams] Eligible stream addons:', streamAddons.map(a => a.id), { requestedStreamType, effectiveStreamType }); // Initialize scraper statuses for tracking const initialStatuses: ScraperStatus[] = []; @@ -1645,9 +1655,9 @@ export const useMetadata = ({ id, type, addonId }: UseMetadataProps): UseMetadat // Start Stremio request using the converted ID format if (__DEV__) console.log('🎬 [loadStreams] Using ID for Stremio addons:', stremioId); - // Map app-level "tv" type to Stremio "series" when requesting streams - const stremioContentType = type === 'tv' ? 'series' : type; - processStremioSource(stremioContentType, stremioId, false); + // Use the effective type we selected when building the eligible addon list. + // This stays aligned with Stremio manifest filtering rules and avoids hard-mapping non-standard types. + processStremioSource(effectiveStreamType, stremioId, false); // Also extract any embedded streams from metadata (PPV-style addons) extractEmbeddedStreams(); @@ -1707,36 +1717,41 @@ export const useMetadata = ({ id, type, addonId }: UseMetadataProps): UseMetadat const allStremioAddons = await stremioService.getInstalledAddons(); const localScrapers = await localScraperService.getInstalledScrapers(); - // Filter Stremio addons to only include those that provide streams for series content - const streamAddons = allStremioAddons.filter(addon => { - if (!addon.resources || !Array.isArray(addon.resources)) { + // We don't yet know the final episode ID format here (it can be normalized later), + // but we can still pre-filter by stream capability for the most likely types. + const pickStreamCapableAddons = (requestType: string) => + allStremioAddons.filter(addon => { + if (!addon.resources || !Array.isArray(addon.resources)) return false; + + for (const resource of addon.resources) { + if (typeof resource === 'object' && resource !== null && 'name' in resource) { + const typedResource = resource as any; + if (typedResource.name === 'stream' && Array.isArray(typedResource.types) && typedResource.types.includes(requestType)) { + return true; + } + } else if (typeof resource === 'string' && resource === 'stream' && addon.types) { + if (Array.isArray(addon.types) && addon.types.includes(requestType)) { + return true; + } + } + } return false; - } + }); - let hasStreamResource = false; - - for (const resource of addon.resources) { - // Check if the current element is a ResourceObject - if (typeof resource === 'object' && resource !== null && 'name' in resource) { - const typedResource = resource as any; - if (typedResource.name === 'stream' && - Array.isArray(typedResource.types) && - typedResource.types.includes('series')) { - hasStreamResource = true; - break; - } - } - // Check if the element is the simple string "stream" AND the addon has a top-level types array - else if (typeof resource === 'string' && resource === 'stream' && addon.types) { - if (Array.isArray(addon.types) && addon.types.includes('series')) { - hasStreamResource = true; - break; - } + const requestedEpisodeType = type; + let streamAddons = pickStreamCapableAddons(requestedEpisodeType); + + if (streamAddons.length === 0) { + const fallbackTypes = ['series', 'movie'].filter(t => t !== requestedEpisodeType); + for (const fallbackType of fallbackTypes) { + const fallback = pickStreamCapableAddons(fallbackType); + if (fallback.length > 0) { + streamAddons = fallback; + if (__DEV__) console.log(`[useMetadata.loadEpisodeStreams] No addons for '${requestedEpisodeType}', falling back to '${fallbackType}'`); + break; } } - - return hasStreamResource; - }); + } // Initialize scraper statuses for tracking const initialStatuses: ScraperStatus[] = []; @@ -1923,10 +1938,8 @@ export const useMetadata = ({ id, type, addonId }: UseMetadataProps): UseMetadat // Start Stremio request using the converted episode ID format if (__DEV__) console.log('🎬 [loadEpisodeStreams] Using episode ID for Stremio addons:', stremioEpisodeId); - // For collections, treat episodes as individual movies, not series - // For other types (e.g. StreamsPPV), preserve the original type unless it's explicitly 'series' logic we want - // Map app-level "tv" type to Stremio "series" for addon stream endpoint - const contentType = isCollection ? 'movie' : (type === 'tv' ? 'series' : type); + const requestedContentType = isCollection ? 'movie' : type; + const contentType = requestedContentType; if (__DEV__) console.log(`🎬 [loadEpisodeStreams] Using content type: ${contentType} for ${isCollection ? 'collection' : type}`); processStremioSource(contentType, stremioEpisodeId, true); diff --git a/src/navigation/AppNavigator.tsx b/src/navigation/AppNavigator.tsx index ec219b70..2d6017aa 100644 --- a/src/navigation/AppNavigator.tsx +++ b/src/navigation/AppNavigator.tsx @@ -1924,7 +1924,6 @@ const ConditionalPostHogProvider: React.FC<{ children: React.ReactNode }> = ({ c apiKey="phc_sk6THCtV3thEAn6cTaA9kL2cHuKDBnlYiSL40ywdS6C" options={{ host: "https://us.i.posthog.com", - autocapture: analyticsEnabled, // Start opted out if analytics is disabled defaultOptIn: analyticsEnabled, }} diff --git a/src/services/stremioService.ts b/src/services/stremioService.ts index e155ad26..ecebc623 100644 --- a/src/services/stremioService.ts +++ b/src/services/stremioService.ts @@ -1250,6 +1250,49 @@ class StremioService { const addons = this.getInstalledAddons(); + // Some addons use non-standard meta types (e.g. "anime") but expect streams under the "series" endpoint. + // We'll try the requested type first, then (if no addons match) fall back to "series". + const pickStreamAddons = (requestType: string) => + addons.filter(addon => { + if (!addon.resources || !Array.isArray(addon.resources)) { + logger.log(`⚠️ [getStreams] Addon ${addon.id} has no valid resources array`); + return false; + } + + let hasStreamResource = false; + let supportsIdPrefix = false; + + for (const resource of addon.resources) { + if (typeof resource === 'object' && resource !== null && 'name' in resource) { + const typedResource = resource as ResourceObject; + if (typedResource.name === 'stream' && + Array.isArray(typedResource.types) && + typedResource.types.includes(requestType)) { + hasStreamResource = true; + + if (Array.isArray(typedResource.idPrefixes) && typedResource.idPrefixes.length > 0) { + supportsIdPrefix = typedResource.idPrefixes.some(p => id.startsWith(p)); + } else { + supportsIdPrefix = true; + } + break; + } + } else if (typeof resource === 'string' && resource === 'stream' && addon.types) { + if (Array.isArray(addon.types) && addon.types.includes(requestType)) { + hasStreamResource = true; + if (addon.idPrefixes && Array.isArray(addon.idPrefixes) && addon.idPrefixes.length > 0) { + supportsIdPrefix = addon.idPrefixes.some(p => id.startsWith(p)); + } else { + supportsIdPrefix = true; + } + break; + } + } + } + + return hasStreamResource && supportsIdPrefix; + }); + // Check if local scrapers are enabled and execute them first try { // Load settings from AsyncStorage directly (scoped with fallback) @@ -1396,64 +1439,109 @@ class StremioService { // TMDB Embed addon not found } - // Find addons that provide streams and sort them by installation order - const streamAddons = addons - .filter(addon => { - if (!addon.resources || !Array.isArray(addon.resources)) { - logger.log(`⚠️ [getStreams] Addon ${addon.id} has no valid resources array`); - return false; + let effectiveType = type; + let streamAddons = pickStreamAddons(type); + + logger.log(`🧭 [getStreams] Resolving stream addons for type='${type}' id='${id}' (matched=${streamAddons.length})`); + + if (streamAddons.length === 0) { + const fallbackTypes = ['series', 'movie', 'tv', 'channel'].filter(t => t !== type); + for (const fallbackType of fallbackTypes) { + const fallbackAddons = pickStreamAddons(fallbackType); + if (fallbackAddons.length > 0) { + effectiveType = fallbackType; + streamAddons = fallbackAddons; + logger.log(`🔁 [getStreams] No stream addons for type '${type}', falling back to '${effectiveType}' for id '${id}'`); + break; } + } + } - let hasStreamResource = false; - let supportsIdPrefix = false; - - // Iterate through the resources array, checking each element - for (const resource of addon.resources) { - // Check if the current element is a ResourceObject - if (typeof resource === 'object' && resource !== null && 'name' in resource) { - const typedResource = resource as ResourceObject; - if (typedResource.name === 'stream' && - Array.isArray(typedResource.types) && - typedResource.types.includes(type)) { - hasStreamResource = true; - - // Check if this addon supports the ID prefix (generic: any prefix that matches start of id) - if (Array.isArray(typedResource.idPrefixes) && typedResource.idPrefixes.length > 0) { - supportsIdPrefix = typedResource.idPrefixes.some(p => id.startsWith(p)); - } else { - // If no idPrefixes specified, assume it supports all prefixes - supportsIdPrefix = true; - } - break; // Found the stream resource object, no need to check further - } - } - // Check if the element is the simple string "stream" AND the addon has a top-level types array - else if (typeof resource === 'string' && resource === 'stream' && addon.types) { - if (Array.isArray(addon.types) && addon.types.includes(type)) { - hasStreamResource = true; - // For simple string resources, check addon-level idPrefixes (generic) - if (addon.idPrefixes && Array.isArray(addon.idPrefixes) && addon.idPrefixes.length > 0) { - supportsIdPrefix = addon.idPrefixes.some(p => id.startsWith(p)); - } else { - // If no idPrefixes specified, assume it supports all prefixes - supportsIdPrefix = true; - } - break; // Found the simple stream resource string and type support - } - } - } - - const canHandleRequest = hasStreamResource && supportsIdPrefix; - - return canHandleRequest; - }); - - + if (effectiveType !== type) { + logger.log(`🧭 [getStreams] Using effectiveType='${effectiveType}' (requested='${type}') for id='${id}'`); + } if (streamAddons.length === 0) { logger.warn('⚠️ [getStreams] No addons found that can provide streams'); - // Optionally call callback with an empty result or specific status? - // For now, just return if no addons. + + // Log what the URL would have been for debugging + const encodedId = encodeURIComponent(id); + const exampleUrl = `/stream/${effectiveType}/${encodedId}.json`; + logger.log(`🚫 [getStreams] No stream addons matched. Would have requested: ${exampleUrl}`); + logger.log(`🚫 [getStreams] Details: requestedType='${type}' effectiveType='${effectiveType}' id='${id}'`); + + // Show which addons have stream capability but didn't match + const streamCapableAddons = addons.filter(addon => { + if (!addon.resources || !Array.isArray(addon.resources)) return false; + return addon.resources.some(resource => { + if (typeof resource === 'object' && resource !== null && 'name' in resource) { + return (resource as ResourceObject).name === 'stream'; + } + return typeof resource === 'string' && resource === 'stream'; + }); + }); + + if (streamCapableAddons.length > 0) { + logger.log(`🚫 [getStreams] Found ${streamCapableAddons.length} stream-capable addon(s) that didn't match:`); + + for (const addon of streamCapableAddons) { + const streamResources = addon.resources!.filter(resource => { + if (typeof resource === 'object' && resource !== null && 'name' in resource) { + return (resource as ResourceObject).name === 'stream'; + } + return typeof resource === 'string' && resource === 'stream'; + }); + + for (const resource of streamResources) { + if (typeof resource === 'object' && resource !== null) { + const typedResource = resource as ResourceObject; + const types = typedResource.types || []; + const prefixes = typedResource.idPrefixes || []; + const typeMatch = types.includes(effectiveType); + const prefixMatch = prefixes.length === 0 || prefixes.some(p => id.startsWith(p)); + + if (addon.url) { + const { baseUrl, queryParams } = this.getAddonBaseURL(addon.url); + const wouldBeUrl = queryParams + ? `${baseUrl}/stream/${effectiveType}/${encodedId}.json?${queryParams}` + : `${baseUrl}/stream/${effectiveType}/${encodedId}.json`; + + console.log( + ` ❌ ${addon.name} (${addon.id}):\n` + + ` types=[${types.join(',')}] typeMatch=${typeMatch}\n` + + ` prefixes=[${prefixes.join(',')}] prefixMatch=${prefixMatch}\n` + + ` url=${wouldBeUrl}` + ); + } else { + console.log(` ❌ ${addon.name} (${addon.id}): no URL configured`); + } + } else if (typeof resource === 'string' && resource === 'stream') { + // String resource - check addon-level types and prefixes + const addonTypes = addon.types || []; + const addonPrefixes = addon.idPrefixes || []; + const typeMatch = addonTypes.includes(effectiveType); + const prefixMatch = addonPrefixes.length === 0 || addonPrefixes.some(p => id.startsWith(p)); + + if (addon.url) { + const { baseUrl, queryParams } = this.getAddonBaseURL(addon.url); + const wouldBeUrl = queryParams + ? `${baseUrl}/stream/${effectiveType}/${encodedId}.json?${queryParams}` + : `${baseUrl}/stream/${effectiveType}/${encodedId}.json`; + + console.log( + ` ❌ ${addon.name} (${addon.id}) [addon-level]:\n` + + ` types=[${addonTypes.join(',')}] typeMatch=${typeMatch}\n` + + ` prefixes=[${addonPrefixes.join(',')}] prefixMatch=${prefixMatch}\n` + + ` url=${wouldBeUrl}` + ); + } + } + } + } + } else { + logger.log(`🚫 [getStreams] No stream-capable addons installed`); + } + return; } @@ -1470,9 +1558,11 @@ class StremioService { const { baseUrl, queryParams } = this.getAddonBaseURL(addon.url); const encodedId = encodeURIComponent(id); - const url = queryParams ? `${baseUrl}/stream/${type}/${encodedId}.json?${queryParams}` : `${baseUrl}/stream/${type}/${encodedId}.json`; + const url = queryParams ? `${baseUrl}/stream/${effectiveType}/${encodedId}.json?${queryParams}` : `${baseUrl}/stream/${effectiveType}/${encodedId}.json`; - logger.log(`🔗 [getStreams] Requesting streams from ${addon.name} (${addon.id}) [${addon.installationId}]: ${url}`); + logger.log( + `🔗 [getStreams] GET ${url} (addon='${addon.name}' id='${addon.id}' install='${addon.installationId}' requestedType='${type}' effectiveType='${effectiveType}' rawId='${id}')` + ); const response = await this.retryRequest(async () => { return await axios.get(url, safeAxiosConfig); @@ -1517,14 +1607,16 @@ class StremioService { const streamPath = `/stream/${type}/${encodedId}.json`; const url = queryParams ? `${baseUrl}${streamPath}?${queryParams}` : `${baseUrl}${streamPath}`; - logger.log(`Fetching streams from URL: ${url}`); + logger.log( + `🔗 [fetchStreamsFromAddon] GET ${url} (addon='${addon.name}' id='${addon.id}' install='${addon.installationId}' type='${type}' rawId='${id}')` + ); try { // Increase timeout for debrid services const timeout = addon.id.toLowerCase().includes('torrentio') ? 60000 : 10000; const response = await this.retryRequest(async () => { - logger.log(`Making request to ${url} with timeout ${timeout}ms`); + logger.log(`🌐 [fetchStreamsFromAddon] Requesting ${url} (timeout=${timeout}ms)`); return await axios.get(url, createSafeAxiosConfig(timeout, { headers: { 'Accept': 'application/json',