addon stream fetching imrpovements

This commit is contained in:
tapframe 2025-09-30 15:32:29 +05:30
parent bbbc22f30f
commit 07eab50848
4 changed files with 128 additions and 112 deletions

View file

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

View file

@ -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 <MetadataLoadingScreen type={type as 'movie' | 'series'} />;
return <MetadataLoadingScreen type={Object.keys(groupedEpisodes).length > 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 = () => {
<MetadataDetails
metadata={metadata}
imdbId={imdbId}
type={type as 'movie' | 'series'}
type={Object.keys(groupedEpisodes).length > 0 ? 'series' : type as 'movie' | 'series'}
contentId={id}
loadingMetadata={false}
renderRatings={() => imdbId && shouldLoadSecondaryData ? (
<MemoizedRatingsSection imdbId={imdbId} type={type === 'series' ? 'show' : 'movie'} />
<MemoizedRatingsSection imdbId={imdbId} type={Object.keys(groupedEpisodes).length > 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 ? (
<MemoizedSeriesContent
episodes={Object.values(groupedEpisodes).flat()}
selectedSeason={selectedSeason}
@ -799,16 +799,6 @@ const styles = StyleSheet.create({
},
});
// Performance Optimizations Applied:
// 1. Memoized components (Cast, Series, Movie, Ratings, Modal)
// 2. Lazy loading of secondary data (cast, recommendations, ratings)
// 3. Focus-based rendering and interaction management
// 4. Debounced Trakt progress fetching with reduced logging
// 5. Optimized callback functions with screen focus checks
// 6. Conditional haptics feedback based on screen focus
// 7. Memory management and cleanup on unmount
// 8. Performance monitoring in development mode
// 9. Reduced re-renders through better state management
// 10. RequestAnimationFrame for navigation optimization
export default MetadataScreen;

View file

@ -666,7 +666,8 @@ export const StreamsScreen = () => {
// 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 = () => {
>
<MaterialIcons name="arrow-back" size={24} color={colors.white} />
<Text style={styles.backButtonText}>
{type === 'series' ? 'Back to Episodes' : 'Back to Info'}
{(type === 'series' || (type === 'other' && selectedEpisode)) ? 'Back to Episodes' : 'Back to Info'}
</Text>
</TouchableOpacity>
</View>
@ -1772,7 +1801,7 @@ export const StreamsScreen = () => {
</View>
)}
{type === 'series' && (
{(type === 'series' || (type === 'other' && selectedEpisode)) && (
<View style={[styles.streamsHeroContainer]}>
<View style={StyleSheet.absoluteFill}>
<View
@ -1972,9 +2001,9 @@ export const StreamsScreen = () => {
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))}
/>

View file

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