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

File diff suppressed because it is too large Load diff