metascreen optimization

This commit is contained in:
tapframe 2025-12-09 14:32:33 +05:30
parent 057c709b41
commit 1b990aa6ec
2 changed files with 1001 additions and 995 deletions

View file

@ -344,55 +344,55 @@ export const useMetadata = ({ id, type, addonId }: UseMetadataProps): UseMetadat
// The function now returns void, just await to let callbacks fire // The function now returns void, just await to let callbacks fire
if (__DEV__) logger.log(`🏁 [${logPrefix}:${sourceName}] Stremio fetching process initiated`); if (__DEV__) logger.log(`🏁 [${logPrefix}:${sourceName}] Stremio fetching process initiated`);
} catch (error) { } catch (error) {
// Catch errors from the initial call to getStreams (e.g., initialization errors) // Catch errors from the initial call to getStreams (e.g., initialization errors)
logger.error(`❌ [${logPrefix}:${sourceName}] Initial call failed:`, error); logger.error(`❌ [${logPrefix}:${sourceName}] Initial call failed:`, error);
// Remove all addons and scrapers from active fetching since the entire request failed // Remove all addons and scrapers from active fetching since the entire request failed
setActiveFetchingScrapers(prev => { setActiveFetchingScrapers(prev => {
// Get both Stremio addon names and local scraper names // Get both Stremio addon names and local scraper names
const stremioAddons = stremioService.getInstalledAddons(); const stremioAddons = stremioService.getInstalledAddons();
const stremioNames = stremioAddons.map(addon => addon.name); const stremioNames = stremioAddons.map(addon => addon.name);
// Get local scraper names // Get local scraper names
localScraperService.getInstalledScrapers().then(localScrapers => { localScraperService.getInstalledScrapers().then(localScrapers => {
const localScraperNames = localScrapers.filter(s => s.enabled).map(s => s.name); const localScraperNames = localScrapers.filter(s => s.enabled).map(s => s.name);
const allNames = [...stremioNames, ...localScraperNames]; const allNames = [...stremioNames, ...localScraperNames];
// Remove all from active fetching // Remove all from active fetching
setActiveFetchingScrapers(current => setActiveFetchingScrapers(current =>
current.filter(name => !allNames.includes(name)) current.filter(name => !allNames.includes(name))
); );
}).catch(() => { }).catch(() => {
// If we can't get local scrapers, just remove Stremio addons // If we can't get local scrapers, just remove Stremio addons
setActiveFetchingScrapers(current => setActiveFetchingScrapers(current =>
current.filter(name => !stremioNames.includes(name)) current.filter(name => !stremioNames.includes(name))
); );
}); });
// Immediately remove Stremio addons (local scrapers will be removed async above) // Immediately remove Stremio addons (local scrapers will be removed async above)
return prev.filter(name => !stremioNames.includes(name)); return prev.filter(name => !stremioNames.includes(name));
}); });
// Update scraper statuses to mark all scrapers as failed // Update scraper statuses to mark all scrapers as failed
setScraperStatuses(prevStatuses => { setScraperStatuses(prevStatuses => {
const stremioAddons = stremioService.getInstalledAddons(); const stremioAddons = stremioService.getInstalledAddons();
return prevStatuses.map(status => { return prevStatuses.map(status => {
const isStremioAddon = stremioAddons.some(addon => addon.id === status.id || addon.name === status.name); const isStremioAddon = stremioAddons.some(addon => addon.id === status.id || addon.name === status.name);
// Mark both Stremio addons and local scrapers as failed // Mark both Stremio addons and local scrapers as failed
if (isStremioAddon || !status.hasCompleted) { if (isStremioAddon || !status.hasCompleted) {
return { return {
...status, ...status,
isLoading: false, isLoading: false,
hasCompleted: true, hasCompleted: true,
error: error instanceof Error ? error.message : 'Initial request failed', error: error instanceof Error ? error.message : 'Initial request failed',
endTime: Date.now() endTime: Date.now()
}; };
} }
return status; return status;
}); });
}); });
} }
// Note: This function completes when getStreams returns, not when all callbacks have fired. // Note: This function completes when getStreams returns, not when all callbacks have fired.
// Loading indicators should probably be managed based on callbacks completing. // Loading indicators should probably be managed based on callbacks completing.
@ -543,40 +543,46 @@ export const useMetadata = ({ id, type, addonId }: UseMetadataProps): UseMetadat
actualId = id; actualId = id;
console.log('🔍 [useMetadata] TMDB enrichment disabled, using original TMDB ID:', { actualId }); console.log('🔍 [useMetadata] TMDB enrichment disabled, using original TMDB ID:', { actualId });
} else { } else {
const tmdbId = id.split(':')[1]; const tmdbId = id.split(':')[1];
// For TMDB IDs, we need to handle metadata differently // For TMDB IDs, we need to handle metadata differently
if (type === 'movie') { if (type === 'movie') {
if (__DEV__) logger.log('Fetching movie details from TMDB for:', tmdbId); if (__DEV__) logger.log('Fetching movie details from TMDB for:', tmdbId);
const movieDetails = await tmdbService.getMovieDetails( const movieDetails = await tmdbService.getMovieDetails(
tmdbId, tmdbId,
settings.useTmdbLocalizedMetadata ? `${settings.tmdbLanguagePreference || 'en'}-US` : 'en-US' settings.useTmdbLocalizedMetadata ? `${settings.tmdbLanguagePreference || 'en'}-US` : 'en-US'
); );
if (movieDetails) { if (movieDetails) {
const imdbId = movieDetails.imdb_id || movieDetails.external_ids?.imdb_id; const imdbId = movieDetails.imdb_id || movieDetails.external_ids?.imdb_id;
if (imdbId) { if (imdbId) {
// Use the imdbId for compatibility with the rest of the app // Use the imdbId for compatibility with the rest of the app
actualId = imdbId; actualId = imdbId;
setImdbId(imdbId); setImdbId(imdbId);
// Also store the TMDB ID for later use // Also store the TMDB ID for later use
setTmdbId(parseInt(tmdbId)); setTmdbId(parseInt(tmdbId));
} else { } else {
// If no IMDb ID, directly call loadTMDBMovie (create this function if needed) // If no IMDb ID, directly call loadTMDBMovie (create this function if needed)
const formattedMovie: StreamingContent = { const formattedMovie: StreamingContent = {
id: `tmdb:${tmdbId}`, id: `tmdb:${tmdbId}`,
type: 'movie', type: 'movie',
name: movieDetails.title, name: movieDetails.title,
poster: tmdbService.getImageUrl(movieDetails.poster_path) || '', poster: tmdbService.getImageUrl(movieDetails.poster_path) || '',
banner: tmdbService.getImageUrl(movieDetails.backdrop_path) || '', banner: tmdbService.getImageUrl(movieDetails.backdrop_path) || '',
description: movieDetails.overview || '', description: movieDetails.overview || '',
year: movieDetails.release_date ? parseInt(movieDetails.release_date.substring(0, 4)) : undefined, year: movieDetails.release_date ? parseInt(movieDetails.release_date.substring(0, 4)) : undefined,
genres: movieDetails.genres?.map((g: { name: string }) => g.name) || [], genres: movieDetails.genres?.map((g: { name: string }) => g.name) || [],
inLibrary: false, inLibrary: false,
}; };
// Fetch credits to get director and crew information // OPTIMIZATION: Fetch credits and logo in parallel instead of sequentially
try { const preferredLanguage = settings.tmdbLanguagePreference || 'en';
const credits = await tmdbService.getCredits(parseInt(tmdbId), 'movie'); const [creditsResult, logoResult] = await Promise.allSettled([
if (credits && credits.crew) { tmdbService.getCredits(parseInt(tmdbId), 'movie'),
tmdbService.getContentLogo('movie', tmdbId, preferredLanguage)
]);
// Process credits result
if (creditsResult.status === 'fulfilled' && creditsResult.value?.crew) {
const credits = creditsResult.value;
// Extract directors // Extract directors
const directors = credits.crew const directors = credits.crew
.filter((person: any) => person.job === 'Director') .filter((person: any) => person.job === 'Director')
@ -597,70 +603,73 @@ export const useMetadata = ({ id, type, addonId }: UseMetadataProps): UseMetadat
(formattedMovie as any).creators = writers; (formattedMovie as any).creators = writers;
(formattedMovie as any).writer = writers; (formattedMovie as any).writer = writers;
} }
} else if (creditsResult.status === 'rejected') {
logger.error('Failed to fetch credits for movie:', creditsResult.reason);
} }
} catch (error) {
logger.error('Failed to fetch credits for movie:', error);
}
// Fetch movie logo from TMDB // Process logo result
try { if (logoResult.status === 'fulfilled') {
const preferredLanguage = settings.tmdbLanguagePreference || 'en'; formattedMovie.logo = logoResult.value || undefined;
const logoUrl = await tmdbService.getContentLogo('movie', tmdbId, preferredLanguage); if (__DEV__) logger.log(`Successfully fetched logo for movie ${tmdbId} from TMDB`);
formattedMovie.logo = logoUrl || undefined; // TMDB logo or undefined (no addon fallback) } else {
if (__DEV__) logger.log(`Successfully fetched logo for movie ${tmdbId} from TMDB`); logger.error('Failed to fetch logo from TMDB:', logoResult.reason);
} catch (error) { formattedMovie.logo = undefined;
logger.error('Failed to fetch logo from TMDB:', error); }
formattedMovie.logo = undefined; // Error means no logo
}
setMetadata(formattedMovie); setMetadata(formattedMovie);
cacheService.setMetadata(id, type, formattedMovie); cacheService.setMetadata(id, type, formattedMovie);
(async () => { (async () => {
const items = await catalogService.getLibraryItems(); const items = await catalogService.getLibraryItems();
const isInLib = items.some(item => item.id === id); const isInLib = items.some(item => item.id === id);
setInLibrary(isInLib); setInLibrary(isInLib);
})(); })();
setLoading(false); setLoading(false);
return; return;
}
} }
} } else if (type === 'series') {
} else if (type === 'series') { // Handle TV shows with TMDB IDs
// Handle TV shows with TMDB IDs if (__DEV__) logger.log('Fetching TV show details from TMDB for:', tmdbId);
if (__DEV__) logger.log('Fetching TV show details from TMDB for:', tmdbId); try {
try { const showDetails = await tmdbService.getTVShowDetails(
const showDetails = await tmdbService.getTVShowDetails( parseInt(tmdbId),
parseInt(tmdbId), settings.useTmdbLocalizedMetadata ? `${settings.tmdbLanguagePreference || 'en'}-US` : 'en-US'
settings.useTmdbLocalizedMetadata ? `${settings.tmdbLanguagePreference || 'en'}-US` : 'en-US' );
); if (showDetails) {
if (showDetails) { // OPTIMIZATION: Fetch external IDs, credits, and logo in parallel
// Get external IDs to check for IMDb ID const preferredLanguage = settings.tmdbLanguagePreference || 'en';
const externalIds = await tmdbService.getShowExternalIds(parseInt(tmdbId)); const [externalIdsResult, creditsResult, logoResult] = await Promise.allSettled([
const imdbId = externalIds?.imdb_id; tmdbService.getShowExternalIds(parseInt(tmdbId)),
tmdbService.getCredits(parseInt(tmdbId), 'series'),
tmdbService.getContentLogo('tv', tmdbId, preferredLanguage)
]);
if (imdbId) { const externalIds = externalIdsResult.status === 'fulfilled' ? externalIdsResult.value : null;
// Use the imdbId for compatibility with the rest of the app const imdbId = externalIds?.imdb_id;
actualId = imdbId;
setImdbId(imdbId);
// Also store the TMDB ID for later use
setTmdbId(parseInt(tmdbId));
} else {
// If no IMDb ID, create formatted show from TMDB data
const formattedShow: StreamingContent = {
id: `tmdb:${tmdbId}`,
type: 'series',
name: showDetails.name,
poster: tmdbService.getImageUrl(showDetails.poster_path) || '',
banner: tmdbService.getImageUrl(showDetails.backdrop_path) || '',
description: showDetails.overview || '',
year: showDetails.first_air_date ? parseInt(showDetails.first_air_date.substring(0, 4)) : undefined,
genres: showDetails.genres?.map((g: { name: string }) => g.name) || [],
inLibrary: false,
};
// Fetch credits to get creators if (imdbId) {
try { // Use the imdbId for compatibility with the rest of the app
const credits = await tmdbService.getCredits(parseInt(tmdbId), 'series'); actualId = imdbId;
if (credits && credits.crew) { setImdbId(imdbId);
// Also store the TMDB ID for later use
setTmdbId(parseInt(tmdbId));
} else {
// If no IMDb ID, create formatted show from TMDB data
const formattedShow: StreamingContent = {
id: `tmdb:${tmdbId}`,
type: 'series',
name: showDetails.name,
poster: tmdbService.getImageUrl(showDetails.poster_path) || '',
banner: tmdbService.getImageUrl(showDetails.backdrop_path) || '',
description: showDetails.overview || '',
year: showDetails.first_air_date ? parseInt(showDetails.first_air_date.substring(0, 4)) : undefined,
genres: showDetails.genres?.map((g: { name: string }) => g.name) || [],
inLibrary: false,
};
// Process credits result (already fetched in parallel)
if (creditsResult.status === 'fulfilled' && creditsResult.value?.crew) {
const credits = creditsResult.value;
// Extract creators // Extract creators
const creators = credits.crew const creators = credits.crew
.filter((person: any) => .filter((person: any) =>
@ -674,43 +683,40 @@ export const useMetadata = ({ id, type, addonId }: UseMetadataProps): UseMetadat
if (creators.length > 0) { if (creators.length > 0) {
(formattedShow as any).creators = creators.slice(0, 3); (formattedShow as any).creators = creators.slice(0, 3);
} }
} else if (creditsResult.status === 'rejected') {
logger.error('Failed to fetch credits for TV show:', creditsResult.reason);
} }
} catch (error) {
logger.error('Failed to fetch credits for TV show:', error); // Process logo result (already fetched in parallel)
if (logoResult.status === 'fulfilled') {
formattedShow.logo = logoResult.value || undefined;
if (__DEV__) logger.log(`Successfully fetched logo for TV show ${tmdbId} from TMDB`);
} else {
logger.error('Failed to fetch logo from TMDB:', (logoResult as PromiseRejectedResult).reason);
formattedShow.logo = undefined;
}
setMetadata(formattedShow);
cacheService.setMetadata(id, type, formattedShow);
// Load series data (episodes)
setTmdbId(parseInt(tmdbId));
loadSeriesData().catch((error) => { if (__DEV__) console.error(error); });
(async () => {
const items = await catalogService.getLibraryItems();
const isInLib = items.some(item => item.id === id);
setInLibrary(isInLib);
})();
setLoading(false);
return;
} }
// Fetch TV show logo from TMDB
try {
const preferredLanguage = settings.tmdbLanguagePreference || 'en';
const logoUrl = await tmdbService.getContentLogo('tv', tmdbId, preferredLanguage);
formattedShow.logo = logoUrl || undefined; // TMDB logo or undefined (no addon fallback)
if (__DEV__) logger.log(`Successfully fetched logo for TV show ${tmdbId} from TMDB`);
} catch (error) {
logger.error('Failed to fetch logo from TMDB:', error);
formattedShow.logo = undefined; // Error means no logo
}
setMetadata(formattedShow);
cacheService.setMetadata(id, type, formattedShow);
// Load series data (episodes)
setTmdbId(parseInt(tmdbId));
loadSeriesData().catch((error) => { if (__DEV__) console.error(error); });
(async () => {
const items = await catalogService.getLibraryItems();
const isInLib = items.some(item => item.id === id);
setInLibrary(isInLib);
})();
setLoading(false);
return;
} }
} catch (error) {
logger.error('Failed to fetch TV show details from TMDB:', error);
} }
} catch (error) {
logger.error('Failed to fetch TV show details from TMDB:', error);
} }
} }
}
} }
// Load all data in parallel // Load all data in parallel
@ -728,7 +734,7 @@ export const useMetadata = ({ id, type, addonId }: UseMetadataProps): UseMetadat
withRetry(async () => { withRetry(async () => {
console.log('🔍 [useMetadata] Calling catalogService.getEnhancedContentDetails:', { type, actualId, addonId }); console.log('🔍 [useMetadata] Calling catalogService.getEnhancedContentDetails:', { type, actualId, addonId });
const result = await withTimeout( const result = await withTimeout(
catalogService.getEnhancedContentDetails(type, actualId, addonId), catalogService.getEnhancedContentDetails(type, actualId, addonId),
API_TIMEOUT API_TIMEOUT
); );
// Store the actual ID used (could be IMDB) // Store the actual ID used (could be IMDB)
@ -772,7 +778,7 @@ export const useMetadata = ({ id, type, addonId }: UseMetadataProps): UseMetadat
const [content, castData] = await Promise.allSettled([ const [content, castData] = await Promise.allSettled([
withRetry(async () => { withRetry(async () => {
const result = await withTimeout( const result = await withTimeout(
catalogService.getEnhancedContentDetails(type, stremioId, addonId), catalogService.getEnhancedContentDetails(type, stremioId, addonId),
API_TIMEOUT API_TIMEOUT
); );
if (stremioId.startsWith('tt')) { if (stremioId.startsWith('tt')) {
@ -844,8 +850,8 @@ export const useMetadata = ({ id, type, addonId }: UseMetadataProps): UseMetadat
}; };
const productionInfo = Array.isArray(localized.production_companies) const productionInfo = Array.isArray(localized.production_companies)
? localized.production_companies ? localized.production_companies
.map((c: any) => ({ id: c?.id, name: c?.name, logo: tmdbSvc.getImageUrl(c?.logo_path, 'w185') })) .map((c: any) => ({ id: c?.id, name: c?.name, logo: tmdbSvc.getImageUrl(c?.logo_path, 'w185') }))
.filter((c: any) => c && (c.logo || c.name)) .filter((c: any) => c && (c.logo || c.name))
: []; : [];
finalMetadata = { finalMetadata = {
@ -859,7 +865,7 @@ export const useMetadata = ({ id, type, addonId }: UseMetadataProps): UseMetadat
} else { // 'series' } else { // 'series'
const localized = await tmdbSvc.getTVShowDetails(Number(finalTmdbId), lang); const localized = await tmdbSvc.getTVShowDetails(Number(finalTmdbId), lang);
if (localized) { if (localized) {
const tvDetails = { const tvDetails = {
status: localized.status, status: localized.status,
firstAirDate: localized.first_air_date, firstAirDate: localized.first_air_date,
lastAirDate: localized.last_air_date, lastAirDate: localized.last_air_date,
@ -877,12 +883,12 @@ export const useMetadata = ({ id, type, addonId }: UseMetadataProps): UseMetadat
}; };
const productionInfo = Array.isArray(localized.networks) const productionInfo = Array.isArray(localized.networks)
? localized.networks ? localized.networks
.map((n: any) => ({ .map((n: any) => ({
id: n?.id, id: n?.id,
name: n?.name, name: n?.name,
logo: tmdbSvc.getImageUrl(n?.logo_path, 'w185') || undefined logo: tmdbSvc.getImageUrl(n?.logo_path, 'w185') || undefined
})) }))
.filter((n: any) => n && (n.logo || n.name)) .filter((n: any) => n && (n.logo || n.name))
: []; : [];
finalMetadata = { finalMetadata = {
@ -1057,7 +1063,7 @@ export const useMetadata = ({ id, type, addonId }: UseMetadataProps): UseMetadat
// Group addon episodes by season // Group addon episodes by season
const groupedAddonEpisodes: GroupedEpisodes = {}; const groupedAddonEpisodes: GroupedEpisodes = {};
addonVideos.forEach((video: any) => { addonVideos.forEach((video: any) => {
const seasonNumber = video.season; const seasonNumber = video.season;
if (!seasonNumber || seasonNumber < 1) { if (!seasonNumber || seasonNumber < 1) {
return; // Skip season 0, which often contains extras return; // Skip season 0, which often contains extras
@ -1160,7 +1166,7 @@ export const useMetadata = ({ id, type, addonId }: UseMetadataProps): UseMetadat
overview: data.overview || ep.overview, overview: data.overview || ep.overview,
}; };
} }
} catch {} } catch { }
return ep; return ep;
}) })
); );
@ -1377,27 +1383,27 @@ export const useMetadata = ({ id, type, addonId }: UseMetadataProps): UseMetadat
} }
// Initialize scraper tracking // Initialize scraper tracking
try { try {
const allStremioAddons = await stremioService.getInstalledAddons(); const allStremioAddons = await stremioService.getInstalledAddons();
const localScrapers = await localScraperService.getInstalledScrapers(); const localScrapers = await localScraperService.getInstalledScrapers();
// Filter Stremio addons to only include those that provide streams for this content type // Filter Stremio addons to only include those that provide streams for this content type
const streamAddons = allStremioAddons.filter(addon => { const streamAddons = allStremioAddons.filter(addon => {
if (!addon.resources || !Array.isArray(addon.resources)) { if (!addon.resources || !Array.isArray(addon.resources)) {
return false; return false;
} }
let hasStreamResource = false; let hasStreamResource = false;
let supportsIdPrefix = false; let supportsIdPrefix = false;
for (const resource of addon.resources) { for (const resource of addon.resources) {
// Check if the current element is a ResourceObject // Check if the current element is a ResourceObject
if (typeof resource === 'object' && resource !== null && 'name' in resource) { if (typeof resource === 'object' && resource !== null && 'name' in resource) {
const typedResource = resource as any; const typedResource = resource as any;
if (typedResource.name === 'stream' && if (typedResource.name === 'stream' &&
Array.isArray(typedResource.types) && Array.isArray(typedResource.types) &&
typedResource.types.includes(type)) { typedResource.types.includes(type)) {
hasStreamResource = true; hasStreamResource = true;
// Check if this addon supports the ID prefix generically: any prefix must match start of id // 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) { if (Array.isArray(typedResource.idPrefixes) && typedResource.idPrefixes.length > 0) {
@ -1406,67 +1412,67 @@ export const useMetadata = ({ id, type, addonId }: UseMetadataProps): UseMetadat
// If no idPrefixes specified, assume it supports all prefixes // If no idPrefixes specified, assume it supports all prefixes
supportsIdPrefix = true; supportsIdPrefix = true;
} }
break; break;
} }
} }
// Check if the element is the simple string "stream" AND the addon has a top-level types array // 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) { else if (typeof resource === 'string' && resource === 'stream' && addon.types) {
if (Array.isArray(addon.types) && addon.types.includes(type)) { if (Array.isArray(addon.types) && addon.types.includes(type)) {
hasStreamResource = true; hasStreamResource = true;
// For simple string resources, check addon-level idPrefixes generically // For simple string resources, check addon-level idPrefixes generically
if (addon.idPrefixes && Array.isArray(addon.idPrefixes) && addon.idPrefixes.length > 0) { if (addon.idPrefixes && Array.isArray(addon.idPrefixes) && addon.idPrefixes.length > 0) {
supportsIdPrefix = addon.idPrefixes.some((p: string) => id.startsWith(p)); supportsIdPrefix = addon.idPrefixes.some((p: string) => id.startsWith(p));
} else { } else {
// If no idPrefixes specified, assume it supports all prefixes // If no idPrefixes specified, assume it supports all prefixes
supportsIdPrefix = true; supportsIdPrefix = true;
} }
break; break;
} }
} }
} }
return hasStreamResource && supportsIdPrefix; return hasStreamResource && supportsIdPrefix;
}); });
if (__DEV__) console.log('[useMetadata.loadStreams] Eligible stream addons:', streamAddons.map(a => a.id)); if (__DEV__) console.log('[useMetadata.loadStreams] Eligible stream addons:', streamAddons.map(a => a.id));
// Initialize scraper statuses for tracking // Initialize scraper statuses for tracking
const initialStatuses: ScraperStatus[] = []; const initialStatuses: ScraperStatus[] = [];
const initialActiveFetching: string[] = []; const initialActiveFetching: string[] = [];
// Add stream-capable Stremio addons only // Add stream-capable Stremio addons only
streamAddons.forEach(addon => { streamAddons.forEach(addon => {
initialStatuses.push({ initialStatuses.push({
id: addon.id, id: addon.id,
name: addon.name, name: addon.name,
isLoading: true, isLoading: true,
hasCompleted: false, hasCompleted: false,
error: null, error: null,
startTime: Date.now(), startTime: Date.now(),
endTime: null endTime: null
});
initialActiveFetching.push(addon.name);
});
// Add local scrapers if enabled
localScrapers.filter((scraper: ScraperInfo) => scraper.enabled).forEach((scraper: ScraperInfo) => {
initialStatuses.push({
id: scraper.id,
name: scraper.name,
isLoading: true,
hasCompleted: false,
error: null,
startTime: Date.now(),
endTime: null
});
initialActiveFetching.push(scraper.name);
}); });
initialActiveFetching.push(addon.name);
});
setScraperStatuses(initialStatuses); // Add local scrapers if enabled
setActiveFetchingScrapers(initialActiveFetching); localScrapers.filter((scraper: ScraperInfo) => scraper.enabled).forEach((scraper: ScraperInfo) => {
console.log('🔍 [loadStreams] Initialized activeFetchingScrapers:', initialActiveFetching); initialStatuses.push({
} catch (error) { id: scraper.id,
if (__DEV__) console.error('Failed to initialize scraper tracking:', error); name: scraper.name,
} isLoading: true,
hasCompleted: false,
error: null,
startTime: Date.now(),
endTime: null
});
initialActiveFetching.push(scraper.name);
});
setScraperStatuses(initialStatuses);
setActiveFetchingScrapers(initialActiveFetching);
console.log('🔍 [loadStreams] Initialized activeFetchingScrapers:', initialActiveFetching);
} catch (error) {
if (__DEV__) console.error('Failed to initialize scraper tracking:', error);
}
// Start Stremio request using the converted ID format // Start Stremio request using the converted ID format
if (__DEV__) console.log('🎬 [loadStreams] Using ID for Stremio addons:', stremioId); if (__DEV__) console.log('🎬 [loadStreams] Using ID for Stremio addons:', stremioId);
@ -1523,79 +1529,79 @@ export const useMetadata = ({ id, type, addonId }: UseMetadataProps): UseMetadat
setAddonResponseOrder([]); // Reset response order setAddonResponseOrder([]); // Reset response order
// Initialize scraper tracking for episodes // Initialize scraper tracking for episodes
try { try {
const allStremioAddons = await stremioService.getInstalledAddons(); const allStremioAddons = await stremioService.getInstalledAddons();
const localScrapers = await localScraperService.getInstalledScrapers(); const localScrapers = await localScraperService.getInstalledScrapers();
// Filter Stremio addons to only include those that provide streams for series content // Filter Stremio addons to only include those that provide streams for series content
const streamAddons = allStremioAddons.filter(addon => { const streamAddons = allStremioAddons.filter(addon => {
if (!addon.resources || !Array.isArray(addon.resources)) { if (!addon.resources || !Array.isArray(addon.resources)) {
return false; return false;
} }
let hasStreamResource = false; let hasStreamResource = false;
for (const resource of addon.resources) { for (const resource of addon.resources) {
// Check if the current element is a ResourceObject // Check if the current element is a ResourceObject
if (typeof resource === 'object' && resource !== null && 'name' in resource) { if (typeof resource === 'object' && resource !== null && 'name' in resource) {
const typedResource = resource as any; const typedResource = resource as any;
if (typedResource.name === 'stream' && if (typedResource.name === 'stream' &&
Array.isArray(typedResource.types) && Array.isArray(typedResource.types) &&
typedResource.types.includes('series')) { typedResource.types.includes('series')) {
hasStreamResource = true; hasStreamResource = true;
break; break;
} }
} }
// Check if the element is the simple string "stream" AND the addon has a top-level types array // 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) { else if (typeof resource === 'string' && resource === 'stream' && addon.types) {
if (Array.isArray(addon.types) && addon.types.includes('series')) { if (Array.isArray(addon.types) && addon.types.includes('series')) {
hasStreamResource = true; hasStreamResource = true;
break; break;
} }
} }
} }
return hasStreamResource; return hasStreamResource;
}); });
// Initialize scraper statuses for tracking // Initialize scraper statuses for tracking
const initialStatuses: ScraperStatus[] = []; const initialStatuses: ScraperStatus[] = [];
const initialActiveFetching: string[] = []; const initialActiveFetching: string[] = [];
// Add stream-capable Stremio addons only // Add stream-capable Stremio addons only
streamAddons.forEach(addon => { streamAddons.forEach(addon => {
initialStatuses.push({ initialStatuses.push({
id: addon.id, id: addon.id,
name: addon.name, name: addon.name,
isLoading: true, isLoading: true,
hasCompleted: false, hasCompleted: false,
error: null, error: null,
startTime: Date.now(), startTime: Date.now(),
endTime: null endTime: null
}); });
initialActiveFetching.push(addon.name); initialActiveFetching.push(addon.name);
}); });
// Add local scrapers if enabled // Add local scrapers if enabled
localScrapers.filter((scraper: ScraperInfo) => scraper.enabled).forEach((scraper: ScraperInfo) => { localScrapers.filter((scraper: ScraperInfo) => scraper.enabled).forEach((scraper: ScraperInfo) => {
initialStatuses.push({ initialStatuses.push({
id: scraper.id, id: scraper.id,
name: scraper.name, name: scraper.name,
isLoading: true, isLoading: true,
hasCompleted: false, hasCompleted: false,
error: null, error: null,
startTime: Date.now(), startTime: Date.now(),
endTime: null endTime: null
}); });
initialActiveFetching.push(scraper.name); initialActiveFetching.push(scraper.name);
}); });
setScraperStatuses(initialStatuses); setScraperStatuses(initialStatuses);
setActiveFetchingScrapers(initialActiveFetching); setActiveFetchingScrapers(initialActiveFetching);
console.log('🔍 [loadEpisodeStreams] Initialized activeFetchingScrapers:', initialActiveFetching); console.log('🔍 [loadEpisodeStreams] Initialized activeFetchingScrapers:', initialActiveFetching);
} catch (error) { } catch (error) {
if (__DEV__) console.error('Failed to initialize episode scraper tracking:', error); if (__DEV__) console.error('Failed to initialize episode scraper tracking:', error);
} }
// Get TMDB ID for external sources and determine the correct ID for Stremio addons // Get TMDB ID for external sources and determine the correct ID for Stremio addons
if (__DEV__) console.log('🔍 [loadEpisodeStreams] Getting TMDB ID for:', id); if (__DEV__) console.log('🔍 [loadEpisodeStreams] Getting TMDB ID for:', id);
@ -1983,12 +1989,12 @@ export const useMetadata = ({ id, type, addonId }: UseMetadataProps): UseMetadat
if (showDetails.networks) { if (showDetails.networks) {
productionInfo = Array.isArray(showDetails.networks) productionInfo = Array.isArray(showDetails.networks)
? showDetails.networks ? showDetails.networks
.map((n: any) => ({ .map((n: any) => ({
id: n?.id, id: n?.id,
name: n?.name, name: n?.name,
logo: tmdbService.getImageUrl(n?.logo_path, 'w185') || undefined, logo: tmdbService.getImageUrl(n?.logo_path, 'w185') || undefined,
})) }))
.filter((n: any) => n && (n.logo || n.name)) .filter((n: any) => n && (n.logo || n.name))
: []; : [];
} }
@ -2030,12 +2036,12 @@ export const useMetadata = ({ id, type, addonId }: UseMetadataProps): UseMetadat
if (movieDetails.production_companies) { if (movieDetails.production_companies) {
productionInfo = Array.isArray(movieDetails.production_companies) productionInfo = Array.isArray(movieDetails.production_companies)
? movieDetails.production_companies ? movieDetails.production_companies
.map((c: any) => ({ .map((c: any) => ({
id: c?.id, id: c?.id,
name: c?.name, name: c?.name,
logo: tmdbService.getImageUrl(c?.logo_path, 'w185'), logo: tmdbService.getImageUrl(c?.logo_path, 'w185'),
})) }))
.filter((c: any) => c && (c.logo || c.name)) .filter((c: any) => c && (c.logo || c.name))
: []; : [];
} }

File diff suppressed because it is too large Load diff