diff --git a/src/hooks/useMetadata.ts b/src/hooks/useMetadata.ts index 761272cf..98ee76ca 100644 --- a/src/hooks/useMetadata.ts +++ b/src/hooks/useMetadata.ts @@ -870,19 +870,6 @@ export const useMetadata = ({ id, type, addonId }: UseMetadataProps): UseMetadat let hasStreamResource = false; let supportsIdPrefix = false; - // Extract ID prefix from the ID - let idPrefix = id.split(':')[0]; - - // For IMDb IDs (tt...), extract just the 'tt' prefix - if (idPrefix.startsWith('tt')) { - idPrefix = 'tt'; - } - // For Kitsu IDs, keep the full prefix - else if (idPrefix === 'kitsu') { - idPrefix = 'kitsu'; - } - // For other prefixes, keep as is - for (const resource of addon.resources) { // Check if the current element is a ResourceObject if (typeof resource === 'object' && resource !== null && 'name' in resource) { @@ -892,13 +879,13 @@ export const useMetadata = ({ id, type, addonId }: UseMetadataProps): UseMetadat typedResource.types.includes(type)) { hasStreamResource = true; - // Check if this addon supports the ID prefix - if (Array.isArray(typedResource.idPrefixes)) { - supportsIdPrefix = typedResource.idPrefixes.includes(idPrefix); - } else { - // If no idPrefixes specified, assume it supports all prefixes - supportsIdPrefix = 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; + } break; } } @@ -906,9 +893,9 @@ export const useMetadata = ({ id, type, addonId }: UseMetadataProps): UseMetadat 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 - if (addon.idPrefixes && Array.isArray(addon.idPrefixes)) { - supportsIdPrefix = addon.idPrefixes.includes(idPrefix); + // 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; @@ -920,6 +907,7 @@ export const useMetadata = ({ id, type, addonId }: UseMetadataProps): UseMetadat return hasStreamResource && supportsIdPrefix; }); + if (__DEV__) console.log('[useMetadata.loadStreams] Eligible stream addons:', streamAddons.map(a => a.id)); // Initialize scraper statuses for tracking const initialStatuses: ScraperStatus[] = []; @@ -1217,7 +1205,7 @@ export const useMetadata = ({ id, type, addonId }: UseMetadataProps): UseMetadat // Re-run series data loading when metadata updates with videos useEffect(() => { - if (metadata && type === 'series' && metadata.videos && metadata.videos.length > 0) { + if (metadata && metadata.videos && metadata.videos.length > 0) { logger.log(`🎬 Metadata updated with ${metadata.videos.length} episodes, reloading series data`); loadSeriesData().catch((error) => { if (__DEV__) console.error(error); }); } diff --git a/src/screens/MetadataScreen.tsx b/src/screens/MetadataScreen.tsx index a23dcf2b..38d6325c 100644 --- a/src/screens/MetadataScreen.tsx +++ b/src/screens/MetadataScreen.tsx @@ -108,7 +108,7 @@ const MetadataScreen: React.FC = () => { } = useMetadata({ id, type, addonId }); // Optimized hooks with memoization and conditional loading - const watchProgressData = useWatchProgress(id, type as 'movie' | 'series', episodeId, episodes); + const watchProgressData = useWatchProgress(id, Object.keys(groupedEpisodes).length > 0 ? 'series' : type as 'movie' | 'series', episodeId, episodes); const assetData = useMetadataAssets(metadata, id, type, imdbId, settings, setMetadata); const animations = useMetadataAnimations(safeAreaTop, watchProgressData.watchProgress); @@ -277,9 +277,9 @@ const MetadataScreen: React.FC = () => { item.type === 'movie' && item.movie?.ids.imdb === id.replace('tt', '') ); - } else if (type === 'series') { - relevantProgress = allProgress.filter(item => - item.type === 'episode' && + } else if (Object.keys(groupedEpisodes).length > 0) { + relevantProgress = allProgress.filter(item => + item.type === 'episode' && item.show?.ids.imdb === id.replace('tt', '') ); } @@ -290,7 +290,7 @@ const MetadataScreen: React.FC = () => { if (__DEV__) console.log(`[MetadataScreen] Found ${relevantProgress.length} Trakt progress items for ${type}`); // Find most recent progress if multiple episodes - if (type === 'series' && relevantProgress.length > 1) { + if (Object.keys(groupedEpisodes).length > 0 && relevantProgress.length > 1) { const mostRecent = relevantProgress.sort((a, b) => new Date(b.paused_at).getTime() - new Date(a.paused_at).getTime() )[0]; @@ -411,7 +411,7 @@ const MetadataScreen: React.FC = () => { return ep.stremioId || `${id}:${ep.season_number}:${ep.episode_number}`; }; - if (type === 'series') { + if (Object.keys(groupedEpisodes).length > 0) { // Determine if current episode is finished let progressPercent = 0; if (watchProgress && watchProgress.duration > 0) { @@ -581,7 +581,7 @@ const MetadataScreen: React.FC = () => { // Show loading screen if metadata is not yet available if (loading || !isContentReady) { - return ; + return 0 ? 'series' : type as 'movie' | 'series'} />; } return ( @@ -634,7 +634,7 @@ const MetadataScreen: React.FC = () => { watchProgressOpacity={animations.watchProgressOpacity} watchProgressWidth={animations.watchProgressWidth} watchProgress={watchProgressData.watchProgress} - type={type as 'movie' | 'series'} + type={Object.keys(groupedEpisodes).length > 0 ? 'series' : type as 'movie' | 'series'} getEpisodeDetails={watchProgressData.getEpisodeDetails} handleShowStreams={handleShowStreams} handleToggleLibrary={handleToggleLibrary} @@ -655,11 +655,11 @@ const MetadataScreen: React.FC = () => { 0 ? 'series' : type as 'movie' | 'series'} contentId={id} loadingMetadata={false} renderRatings={() => imdbId && shouldLoadSecondaryData ? ( - + 0 ? 'show' : 'movie'} /> ) : null} /> @@ -681,7 +681,7 @@ const MetadataScreen: React.FC = () => { )} {/* Series/Movie Content with episode skeleton when loading */} - {type === 'series' ? ( + {Object.keys(groupedEpisodes).length > 0 ? ( { // Skip processing if component is unmounting if (!isMounted.current) return; - const currentStreamsData = type === 'series' ? episodeStreams : groupedStreams; + const currentStreamsData = (type === 'series' || (type === 'other' && selectedEpisode)) ? episodeStreams : groupedStreams; + if (__DEV__) console.log('[StreamsScreen] streams state changed', { providerKeys: Object.keys(currentStreamsData || {}), type }); // Update available providers immediately when streams change const providersWithStreams = Object.entries(currentStreamsData) @@ -680,6 +681,7 @@ export const StreamsScreen = () => { // Only update if we have new providers, don't remove existing ones during loading setAvailableProviders(prevProviders => { const newProviders = new Set([...prevProviders, ...providersWithStreamsSet]); + if (__DEV__) console.log('[StreamsScreen] availableProviders ->', Array.from(newProviders)); return newProviders; }); } @@ -701,6 +703,7 @@ export const StreamsScreen = () => { changed = true; } }); + if (changed && __DEV__) console.log('[StreamsScreen] loadingProviders ->', nextLoading); return changed ? nextLoading : prevLoading; }); @@ -717,7 +720,7 @@ export const StreamsScreen = () => { } // Check if provider exists in current streams data - const currentStreamsData = type === 'series' ? episodeStreams : groupedStreams; + const currentStreamsData = (type === 'series' || (type === 'other' && selectedEpisode)) ? episodeStreams : groupedStreams; const hasStreamsForProvider = currentStreamsData[selectedProvider] && currentStreamsData[selectedProvider].streams && currentStreamsData[selectedProvider].streams.length > 0; @@ -733,14 +736,19 @@ export const StreamsScreen = () => { // Update useEffect to check for sources useEffect(() => { const checkProviders = async () => { + if (__DEV__) console.log('[StreamsScreen] checkProviders() start', { id, type, episodeId, fromPlayer }); + logger.log(`[StreamsScreen] checkProviders() start id=${id} type=${type} episodeId=${episodeId || 'none'} fromPlayer=${!!fromPlayer}`); // Check for Stremio addons const hasStremioProviders = await stremioService.hasStreamProviders(); + if (__DEV__) console.log('[StreamsScreen] hasStremioProviders:', hasStremioProviders); // Check for local scrapers (only if enabled in settings) 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}`); if (!isMounted.current) return; @@ -748,22 +756,43 @@ 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); } else { - if (type === 'series' && episodeId) { + if ((type === 'series' || type === 'other') && episodeId) { logger.log(`🎬 Loading episode streams for: ${episodeId}`); setLoadingProviders({ 'stremio': true }); setSelectedEpisode(episodeId); setStreamsLoadStart(Date.now()); + if (__DEV__) console.log('[StreamsScreen] calling loadEpisodeStreams', episodeId); loadEpisodeStreams(episodeId); } else if (type === 'movie') { logger.log(`🎬 Loading movie streams for: ${id}`); setStreamsLoadStart(Date.now()); + if (__DEV__) console.log('[StreamsScreen] calling loadStreams (movie)', id); + loadStreams(); + } else if ((type === 'series' || type === 'other') && !episodeId) { + // Series with no episodes (e.g., TV/live channels) – fetch streams directly + logger.log(`🎬 Loading series streams (no episodes) for: ${id}`); + setLoadingProviders({ + 'stremio': true + }); + setStreamsLoadStart(Date.now()); + if (__DEV__) console.log('[StreamsScreen] calling loadStreams (series no episodeId)', id); + loadStreams(); + } else if (type === 'tv') { + // TV/live content – fetch streams directly + logger.log(`📺 Loading TV streams for: ${id}`); + setLoadingProviders({ + 'stremio': true + }); + setStreamsLoadStart(Date.now()); + if (__DEV__) console.log('[StreamsScreen] calling loadStreams (tv)', id); loadStreams(); } @@ -972,7 +1001,7 @@ export const StreamsScreen = () => { await new Promise(resolve => setTimeout(resolve, 50)); // Prepare available streams for the change source feature - const streamsToPass = type === 'series' ? episodeStreams : groupedStreams; + const streamsToPass = (type === 'series' || (type === 'other' && selectedEpisode)) ? episodeStreams : groupedStreams; // Determine the stream name using the same logic as StreamCard const streamName = stream.name || stream.title || 'Unnamed Stream'; @@ -1027,9 +1056,9 @@ export const StreamsScreen = () => { navigation.navigate(playerRoute as any, { uri: stream.url, title: metadata?.name || '', - episodeTitle: type === 'series' ? currentEpisode?.name : undefined, - season: type === 'series' ? currentEpisode?.season_number : undefined, - episode: type === 'series' ? currentEpisode?.episode_number : undefined, + episodeTitle: (type === 'series' || type === 'other') ? currentEpisode?.name : undefined, + season: (type === 'series' || type === 'other') ? currentEpisode?.season_number : undefined, + episode: (type === 'series' || type === 'other') ? currentEpisode?.episode_number : undefined, quality: (stream.title?.match(/(\d+)p/) || [])[1] || undefined, year: metadata?.year, streamProvider: streamProvider, @@ -1040,7 +1069,7 @@ export const StreamsScreen = () => { forceVlc, id, type, - episodeId: type === 'series' && selectedEpisode ? selectedEpisode : undefined, + episodeId: (type === 'series' || type === 'other') && selectedEpisode ? selectedEpisode : undefined, imdbId: imdbId || undefined, availableStreams: streamsToPass, backdrop: bannerImage || undefined, @@ -1221,8 +1250,8 @@ export const StreamsScreen = () => { const success = await VideoPlayerService.playVideo(stream.url, { useExternalPlayer: true, title: metadata?.name || 'Video', - episodeTitle: type === 'series' ? currentEpisode?.name : undefined, - episodeNumber: type === 'series' && currentEpisode ? `S${currentEpisode.season_number}E${currentEpisode.episode_number}` : undefined, + episodeTitle: (type === 'series' || type === 'other') ? currentEpisode?.name : undefined, + episodeNumber: (type === 'series' || type === 'other') && currentEpisode ? `S${currentEpisode.season_number}E${currentEpisode.episode_number}` : undefined, }); if (!success) { @@ -1280,7 +1309,7 @@ export const StreamsScreen = () => { !autoplayTriggered && isAutoplayWaiting ) { - const streams = type === 'series' ? episodeStreams : groupedStreams; + const streams = (type === 'series' || (type === 'other' && selectedEpisode)) ? episodeStreams : groupedStreams; if (Object.keys(streams).length > 0) { const bestStream = getBestStream(streams); @@ -1311,7 +1340,7 @@ export const StreamsScreen = () => { const filterItems = useMemo(() => { const installedAddons = stremioService.getInstalledAddons(); - const streams = type === 'series' ? episodeStreams : groupedStreams; + const streams = (type === 'series' || (type === 'other' && selectedEpisode)) ? episodeStreams : groupedStreams; // Make sure we include all providers with streams, not just those in availableProviders const allProviders = new Set([ @@ -1389,7 +1418,7 @@ export const StreamsScreen = () => { }, [availableProviders, type, episodeStreams, groupedStreams, settings.streamDisplayMode]); const sections = useMemo(() => { - const streams = type === 'series' ? episodeStreams : groupedStreams; + const streams = (type === 'series' || (type === 'other' && selectedEpisode)) ? episodeStreams : groupedStreams; const installedAddons = stremioService.getInstalledAddons(); // Filter streams by selected provider @@ -1681,8 +1710,8 @@ export const StreamsScreen = () => { }); }, [episodeImage, bannerImage, metadata]); - const isLoading = type === 'series' ? loadingEpisodeStreams : loadingStreams; - const streams = type === 'series' ? episodeStreams : groupedStreams; + const isLoading = (type === 'series' || (type === 'other' && selectedEpisode)) ? loadingEpisodeStreams : loadingStreams; + const streams = (type === 'series' || (type === 'other' && selectedEpisode)) ? episodeStreams : groupedStreams; // Determine extended loading phases const streamsEmpty = Object.keys(streams).length === 0; @@ -1747,7 +1776,7 @@ export const StreamsScreen = () => { > - {type === 'series' ? 'Back to Episodes' : 'Back to Info'} + {(type === 'series' || (type === 'other' && selectedEpisode)) ? 'Back to Episodes' : 'Back to Info'} @@ -1772,7 +1801,7 @@ export const StreamsScreen = () => { )} - {type === 'series' && ( + {(type === 'series' || (type === 'other' && selectedEpisode)) && ( { showAlert={(t, m) => openAlert(t, m)} parentTitle={metadata?.name} parentType={type as 'movie' | 'series'} - parentSeason={type === 'series' ? currentEpisode?.season_number : undefined} - parentEpisode={type === 'series' ? currentEpisode?.episode_number : undefined} - parentEpisodeTitle={type === 'series' ? currentEpisode?.name : undefined} + parentSeason={(type === 'series' || type === 'other') ? currentEpisode?.season_number : undefined} + parentEpisode={(type === 'series' || type === 'other') ? currentEpisode?.episode_number : undefined} + parentEpisodeTitle={(type === 'series' || type === 'other') ? currentEpisode?.name : undefined} parentPosterUrl={episodeImage || metadata?.poster || undefined} providerName={streams && Object.keys(streams).find(pid => (streams as any)[pid]?.streams?.includes?.(item))} /> diff --git a/src/services/stremioService.ts b/src/services/stremioService.ts index a3e727a5..5bd5326a 100644 --- a/src/services/stremioService.ts +++ b/src/services/stremioService.ts @@ -639,7 +639,8 @@ class StremioService { // Special handling for Cinemeta if (manifest.id === 'com.linvo.cinemeta') { const baseUrl = 'https://v3-cinemeta.strem.io'; - let url = `${baseUrl}/catalog/${type}/${id}.json`; + const encodedId = encodeURIComponent(id); + let url = `${baseUrl}/catalog/${type}/${encodedId}.json`; // Add paging url += `?skip=${(page - 1) * this.DEFAULT_PAGE_SIZE}`; @@ -670,9 +671,10 @@ class StremioService { try { const { baseUrl, queryParams } = this.getAddonBaseURL(manifest.url); - + // Build the catalog URL - let url = `${baseUrl}/catalog/${type}/${id}.json`; + const encodedId = encodeURIComponent(id); + let url = `${baseUrl}/catalog/${type}/${encodedId}.json`; // Add paging url += `?skip=${(page - 1) * this.DEFAULT_PAGE_SIZE}`; @@ -710,12 +712,15 @@ class StremioService { // If a preferred addon is specified, try it first if (preferredAddonId) { + logger.log(`🔍 [getMetaDetails] Looking for preferred addon: ${preferredAddonId}`); const preferredAddon = addons.find(addon => addon.id === preferredAddonId); + logger.log(`🔍 [getMetaDetails] Found preferred addon: ${preferredAddon ? preferredAddon.id : 'null'}`); if (preferredAddon && preferredAddon.resources) { // Build URL for metadata request const { baseUrl, queryParams } = this.getAddonBaseURL(preferredAddon.url || ''); - const url = queryParams ? `${baseUrl}/meta/${type}/${id}.json?${queryParams}` : `${baseUrl}/meta/${type}/${id}.json`; + const encodedId = encodeURIComponent(id); + const url = queryParams ? `${baseUrl}/meta/${type}/${encodedId}.json?${queryParams}` : `${baseUrl}/meta/${type}/${encodedId}.json`; // Check if addon supports meta resource for this type let hasMetaSupport = false; @@ -744,7 +749,8 @@ class StremioService { if (hasMetaSupport) { try { - + logger.log(`🔗 [${preferredAddon.name}] Requesting metadata: ${url} (preferred, id=${id}, type=${type})`); + const response = await this.retryRequest(async () => { return await axios.get(url, { timeout: 10000 }); }); @@ -767,7 +773,8 @@ class StremioService { for (const baseUrl of cinemetaUrls) { try { - const url = `${baseUrl}/meta/${type}/${id}.json`; + const encodedId = encodeURIComponent(id); + const url = `${baseUrl}/meta/${type}/${encodedId}.json`; const response = await this.retryRequest(async () => { return await axios.get(url, { timeout: 10000 }); @@ -786,8 +793,9 @@ class StremioService { for (const addon of addons) { if (!addon.resources || addon.id === 'com.linvo.cinemeta' || addon.id === preferredAddonId) continue; - // Check if addon supports meta resource for this type (handles both string and object formats) + // Check if addon supports meta resource for this type AND idPrefix (handles both string and object formats) let hasMetaSupport = false; + let supportsIdPrefix = false; for (const resource of addon.resources) { // Check if the current element is a ResourceObject @@ -797,6 +805,12 @@ class StremioService { Array.isArray(typedResource.types) && typedResource.types.includes(type)) { hasMetaSupport = true; + // Match idPrefixes if present; otherwise assume support + if (Array.isArray(typedResource.idPrefixes) && typedResource.idPrefixes.length > 0) { + supportsIdPrefix = typedResource.idPrefixes.some(p => id.startsWith(p)); + } else { + supportsIdPrefix = true; + } break; } } @@ -804,18 +818,25 @@ class StremioService { else if (typeof resource === 'string' && resource === 'meta' && addon.types) { if (Array.isArray(addon.types) && addon.types.includes(type)) { hasMetaSupport = true; + // For simple resources, check addon-level idPrefixes if present + if (addon.idPrefixes && Array.isArray(addon.idPrefixes) && addon.idPrefixes.length > 0) { + supportsIdPrefix = addon.idPrefixes.some(p => id.startsWith(p)); + } else { + supportsIdPrefix = true; + } break; } } } - - if (!hasMetaSupport) continue; + // Require both meta support and idPrefix compatibility + if (!(hasMetaSupport && supportsIdPrefix)) continue; try { const { baseUrl, queryParams } = this.getAddonBaseURL(addon.url || ''); - const url = queryParams ? `${baseUrl}/meta/${type}/${id}.json?${queryParams}` : `${baseUrl}/meta/${type}/${id}.json`; - - logger.log(`🔗 [${addon.name}] Requesting metadata: ${url}`); + const encodedId = encodeURIComponent(id); + const url = queryParams ? `${baseUrl}/meta/${type}/${encodedId}.json?${queryParams}` : `${baseUrl}/meta/${type}/${encodedId}.json`; + + logger.log(`🔗 [${addon.name}] Requesting metadata: ${url} (id=${id}, type=${type})`); const response = await this.retryRequest(async () => { return await axios.get(url, { timeout: 10000 }); }); @@ -978,19 +999,17 @@ class StremioService { if (idType === 'imdb') { const tmdbService = TMDBService.getInstance(); const tmdbIdNumber = await tmdbService.findTMDBIdByIMDB(baseId); - if (tmdbIdNumber) { tmdbId = tmdbIdNumber.toString(); } else { - return; // Skip local scrapers if we can't convert the ID + logger.log('🔧 [getStreams] Skipping local scrapers: could not convert IMDb to TMDB for', baseId); } - } else { + } else if (idType === 'kitsu') { // For kitsu IDs, skip local scrapers as they don't support kitsu logger.log('🔧 [getStreams] Skipping local scrapers for kitsu ID:', baseId); - // Don't return here - continue to Stremio addon processing } } catch (error) { - return; // Skip local scrapers if ID parsing fails + logger.warn('🔧 [getStreams] Skipping local scrapers due to ID parsing error:', error); } // Execute local scrapers asynchronously with TMDB ID (only for IMDb IDs) @@ -1006,6 +1025,8 @@ class StremioService { } } }); + } else { + logger.log('🔧 [getStreams] Local scrapers not executed for this ID/type; continuing with Stremio addons'); } } } @@ -1033,21 +1054,6 @@ class StremioService { let hasStreamResource = false; let supportsIdPrefix = false; - // Extract ID prefix from the ID - let idPrefix = id.split(':')[0]; - - // For IMDb IDs (tt...), extract just the 'tt' prefix - if (idPrefix.startsWith('tt')) { - idPrefix = 'tt'; - } - // For Kitsu IDs, keep the full prefix - else if (idPrefix === 'kitsu') { - idPrefix = 'kitsu'; - } - // For other prefixes, keep as is - - logger.log(`🔍 [getStreams] Checking if addon supports ID prefix: ${idPrefix} (from ${id.split(':')[0]})`); - // Iterate through the resources array, checking each element for (const resource of addon.resources) { // Check if the current element is a ResourceObject @@ -1058,10 +1064,10 @@ class StremioService { typedResource.types.includes(type)) { hasStreamResource = true; - // Check if this addon supports the ID prefix - if (Array.isArray(typedResource.idPrefixes)) { - supportsIdPrefix = typedResource.idPrefixes.includes(idPrefix); - logger.log(`🔍 [getStreams] Addon ${addon.id} supports prefixes: ${typedResource.idPrefixes.join(', ')}`); + // 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)); + logger.log(`🔍 [getStreams] Addon ${addon.id} supports prefixes: ${typedResource.idPrefixes.join(', ')} → matches=${supportsIdPrefix}`); } else { // If no idPrefixes specified, assume it supports all prefixes supportsIdPrefix = true; @@ -1074,10 +1080,10 @@ class StremioService { 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 - if (addon.idPrefixes && Array.isArray(addon.idPrefixes)) { - supportsIdPrefix = addon.idPrefixes.includes(idPrefix); - logger.log(`🔍 [getStreams] Addon ${addon.id} supports prefixes: ${addon.idPrefixes.join(', ')}`); + // 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)); + logger.log(`🔍 [getStreams] Addon ${addon.id} supports prefixes: ${addon.idPrefixes.join(', ')} → matches=${supportsIdPrefix}`); } else { // If no idPrefixes specified, assume it supports all prefixes supportsIdPrefix = true; @@ -1093,9 +1099,9 @@ class StremioService { if (!hasStreamResource) { logger.log(`❌ [getStreams] Addon ${addon.id} does not support streaming ${type}`); } else if (!supportsIdPrefix) { - logger.log(`❌ [getStreams] Addon ${addon.id} supports ${type} but not ID prefix ${idPrefix}`); + logger.log(`❌ [getStreams] Addon ${addon.id} supports ${type} but its idPrefixes did not match id=${id}`); } else { - logger.log(`✅ [getStreams] Addon ${addon.id} supports streaming ${type} with prefix ${idPrefix}`); + logger.log(`✅ [getStreams] Addon ${addon.id} supports streaming ${type} for id=${id}`); } return canHandleRequest; @@ -1122,7 +1128,8 @@ class StremioService { } const { baseUrl, queryParams } = this.getAddonBaseURL(addon.url); - const url = queryParams ? `${baseUrl}/stream/${type}/${id}.json?${queryParams}` : `${baseUrl}/stream/${type}/${id}.json`; + const encodedId = encodeURIComponent(id); + const url = queryParams ? `${baseUrl}/stream/${type}/${encodedId}.json?${queryParams}` : `${baseUrl}/stream/${type}/${encodedId}.json`; logger.log(`🔗 [getStreams] Requesting streams from ${addon.name} (${addon.id}): ${url}`); @@ -1165,7 +1172,8 @@ class StremioService { } const { baseUrl, queryParams } = this.getAddonBaseURL(addon.url); - const streamPath = `/stream/${type}/${id}.json`; + const encodedId = encodeURIComponent(id); + const streamPath = `/stream/${type}/${encodedId}.json`; const url = queryParams ? `${baseUrl}${streamPath}?${queryParams}` : `${baseUrl}${streamPath}`; logger.log(`Fetching streams from URL: ${url}`); @@ -1363,10 +1371,11 @@ class StremioService { const { baseUrl } = this.getAddonBaseURL(addon.url || ''); let url = ''; if (type === 'series' && videoId) { - const episodeInfo = videoId.replace('series:', ''); + const episodeInfo = encodeURIComponent(videoId.replace('series:', '')); url = `${baseUrl}/subtitles/series/${episodeInfo}.json`; } else { - url = `${baseUrl}/subtitles/${type}/${id}.json`; + const encodedId = encodeURIComponent(id); + url = `${baseUrl}/subtitles/${type}/${encodedId}.json`; } logger.log(`Fetching subtitles from ${addon.name}: ${url}`); const response = await this.retryRequest(async () => axios.get(url, { timeout: 10000 }));