fix: stream addon selection logic with fallback for non-standard types

This commit is contained in:
tapframe 2026-02-05 12:17:38 +05:30
parent 4c9bcbf64e
commit 77b8b59734
3 changed files with 237 additions and 133 deletions

View file

@ -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);

View file

@ -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,
}}

View file

@ -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',