diff --git a/src/hooks/useMetadata.ts b/src/hooks/useMetadata.ts index e94b9984..d8d63305 100644 --- a/src/hooks/useMetadata.ts +++ b/src/hooks/useMetadata.ts @@ -392,7 +392,17 @@ export const useMetadata = ({ id, type, addonId }: UseMetadataProps): UseMetadat const loadMetadata = async () => { try { + console.log('πŸ” [useMetadata] loadMetadata started:', { + id, + type, + addonId, + loadAttempts, + maxRetries: MAX_RETRIES, + settingsLoaded: settingsLoaded + }); + if (loadAttempts >= MAX_RETRIES) { + console.log('πŸ” [useMetadata] Max retries exceeded:', { loadAttempts, maxRetries: MAX_RETRIES }); setError(`Failed to load content after ${MAX_RETRIES + 1} attempts. Please check your connection and try again.`); setLoading(false); return; @@ -405,6 +415,14 @@ export const useMetadata = ({ id, type, addonId }: UseMetadataProps): UseMetadat // Check metadata screen cache const cachedScreen = cacheService.getMetadataScreen(id, type); if (cachedScreen) { + console.log('πŸ” [useMetadata] Using cached metadata:', { + id, + type, + hasMetadata: !!cachedScreen.metadata, + hasCast: !!cachedScreen.cast, + hasEpisodes: !!cachedScreen.episodes, + tmdbId: cachedScreen.tmdbId + }); setMetadata(cachedScreen.metadata); setCast(cachedScreen.cast); if (type === 'series' && cachedScreen.episodes) { @@ -418,26 +436,21 @@ export const useMetadata = ({ id, type, addonId }: UseMetadataProps): UseMetadat setInLibrary(isInLib); setLoading(false); return; + } else { + console.log('πŸ” [useMetadata] No cached metadata found, proceeding with fresh fetch'); } // Handle TMDB-specific IDs let actualId = id; if (id.startsWith('tmdb:')) { - // If enrichment disabled, resolve to an addon-friendly ID (IMDb) before calling addons + // Always try the original TMDB ID first - let addons decide if they support it + console.log('πŸ” [useMetadata] TMDB ID detected, trying original ID first:', { originalId: id }); + + // If enrichment disabled, try original ID first, then fallback to conversion if needed if (!settings.enrichMetadataWithTMDB) { - const tmdbRaw = id.split(':')[1]; - try { - if (__DEV__) logger.log('[loadMetadata] enrichment=OFF; resolving TMDBβ†’Stremio ID', { type, tmdbId: tmdbRaw }); - const stremioId = await catalogService.getStremioId(type === 'series' ? 'tv' : 'movie', tmdbRaw); - if (stremioId) { - actualId = stremioId; - if (__DEV__) logger.log('[loadMetadata] resolved TMDBβ†’Stremio ID', { actualId }); - } else { - if (__DEV__) logger.warn('[loadMetadata] failed to resolve TMDBβ†’Stremio ID; addon fetch may fail', { type, tmdbId: tmdbRaw }); - } - } catch (e) { - if (__DEV__) logger.error('[loadMetadata] error resolving TMDBβ†’Stremio ID', e); - } + // Keep the original TMDB ID - let the addon system handle it dynamically + 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 @@ -594,26 +607,101 @@ export const useMetadata = ({ id, type, addonId }: UseMetadataProps): UseMetadat } // Load all data in parallel + console.log('πŸ” [useMetadata] Starting parallel data fetch:', { type, actualId, addonId, apiTimeout: API_TIMEOUT }); if (__DEV__) logger.log('[loadMetadata] fetching addon metadata', { type, actualId, addonId }); - const [content, castData] = await Promise.allSettled([ - // Load content with timeout and retry - withRetry(async () => { - const result = await withTimeout( - catalogService.getEnhancedContentDetails(type, actualId, addonId), - API_TIMEOUT - ); - // Store the actual ID used (could be IMDB) - if (actualId.startsWith('tt')) { - setImdbId(actualId); + + let contentResult = null; + let lastError = null; + + // Try with original ID first + try { + console.log('πŸ” [useMetadata] Attempting metadata fetch with original ID:', { type, actualId, addonId }); + const [content, castData] = await Promise.allSettled([ + // Load content with timeout and retry + withRetry(async () => { + console.log('πŸ” [useMetadata] Calling catalogService.getEnhancedContentDetails:', { type, actualId, addonId }); + const result = await withTimeout( + catalogService.getEnhancedContentDetails(type, actualId, addonId), + API_TIMEOUT + ); + // Store the actual ID used (could be IMDB) + if (actualId.startsWith('tt')) { + setImdbId(actualId); + } + console.log('πŸ” [useMetadata] catalogService.getEnhancedContentDetails result:', { + hasResult: Boolean(result), + resultId: result?.id, + resultName: result?.name, + resultType: result?.type + }); + if (__DEV__) logger.log('[loadMetadata] addon metadata fetched', { hasResult: Boolean(result) }); + return result; + }), + // Start loading cast immediately in parallel + loadCast() + ]); + + contentResult = content; + if (content.status === 'fulfilled' && content.value) { + console.log('πŸ” [useMetadata] Successfully got metadata with original ID'); + } else { + console.log('πŸ” [useMetadata] Original ID failed, will try fallback conversion'); + lastError = (content as any)?.reason; + } + } catch (error) { + console.log('πŸ” [useMetadata] Original ID attempt failed:', { error: error instanceof Error ? error.message : String(error) }); + lastError = error; + } + + // If original TMDB ID failed and enrichment is disabled, try ID conversion as fallback + if (!contentResult || (contentResult.status === 'fulfilled' && !contentResult.value) || contentResult.status === 'rejected') { + if (id.startsWith('tmdb:') && !settings.enrichMetadataWithTMDB) { + console.log('πŸ” [useMetadata] Original TMDB ID failed, trying ID conversion fallback'); + const tmdbRaw = id.split(':')[1]; + try { + const stremioId = await catalogService.getStremioId(type === 'series' ? 'tv' : 'movie', tmdbRaw); + if (stremioId && stremioId !== id) { + console.log('πŸ” [useMetadata] Trying converted ID:', { originalId: id, convertedId: stremioId }); + const [content, castData] = await Promise.allSettled([ + withRetry(async () => { + const result = await withTimeout( + catalogService.getEnhancedContentDetails(type, stremioId, addonId), + API_TIMEOUT + ); + if (stremioId.startsWith('tt')) { + setImdbId(stremioId); + } + return result; + }), + loadCast() + ]); + contentResult = content; + } + } catch (e) { + console.log('πŸ” [useMetadata] ID conversion fallback also failed:', { error: e instanceof Error ? e.message : String(e) }); } - if (__DEV__) logger.log('[loadMetadata] addon metadata fetched', { hasResult: Boolean(result) }); - return result; - }), - // Start loading cast immediately in parallel - loadCast() - ]); + } + } + + const content = contentResult || { status: 'rejected' as const, reason: lastError || new Error('No content result') }; + const castData = { status: 'fulfilled' as const, value: undefined }; + + console.log('πŸ” [useMetadata] Promise.allSettled results:', { + contentStatus: content.status, + contentFulfilled: content.status === 'fulfilled', + hasContentValue: content.status === 'fulfilled' ? !!content.value : false, + castStatus: castData.status, + castFulfilled: castData.status === 'fulfilled' + }); if (content.status === 'fulfilled' && content.value) { + console.log('πŸ” [useMetadata] Content fetch successful:', { + id: content.value?.id, + type: content.value?.type, + name: content.value?.name, + hasDescription: !!content.value?.description, + hasPoster: !!content.value?.poster + }); if (__DEV__) logger.log('[loadMetadata] addon metadata:success', { id: content.value?.id, type: content.value?.type, name: content.value?.name }); // Start with addon metadata @@ -666,6 +754,15 @@ export const useMetadata = ({ id, type, addonId }: UseMetadataProps): UseMetadat const reason = (content as any)?.reason; const reasonMessage = reason?.message || String(reason); + console.log('πŸ” [useMetadata] Content fetch failed:', { + status: content.status, + reason: reasonMessage, + fullReason: reason, + isAxiosError: reason?.isAxiosError, + responseStatus: reason?.response?.status, + responseData: reason?.response?.data + }); + if (__DEV__) { console.log('[loadMetadata] addon metadata:not found or failed', { status: content.status, @@ -682,14 +779,25 @@ export const useMetadata = ({ id, type, addonId }: UseMetadataProps): UseMetadat reasonMessage.includes('Network Error') || reasonMessage.includes('Request failed') )) { + console.log('πŸ” [useMetadata] Detected server/network error, preserving original error'); // This was a server/network error, preserve the original error message throw reason instanceof Error ? reason : new Error(reasonMessage); } else { + console.log('πŸ” [useMetadata] Detected content not found error, throwing generic error'); // This was likely a content not found error throw new Error('Content not found'); } } } catch (error) { + console.log('πŸ” [useMetadata] loadMetadata caught error:', { + errorMessage: error instanceof Error ? error.message : String(error), + errorType: typeof error, + isAxiosError: (error as any)?.isAxiosError, + responseStatus: (error as any)?.response?.status, + responseData: (error as any)?.response?.data, + stack: error instanceof Error ? error.stack : undefined + }); + if (__DEV__) { console.error('Failed to load metadata:', error); console.log('Error message being set:', error instanceof Error ? error.message : String(error)); @@ -705,6 +813,7 @@ export const useMetadata = ({ id, type, addonId }: UseMetadataProps): UseMetadat setGroupedEpisodes({}); setEpisodes([]); } finally { + console.log('πŸ” [useMetadata] loadMetadata completed, setting loading to false'); setLoading(false); } }; diff --git a/src/screens/LibraryScreen.tsx b/src/screens/LibraryScreen.tsx index 289efa09..bf619d7e 100644 --- a/src/screens/LibraryScreen.tsx +++ b/src/screens/LibraryScreen.tsx @@ -143,7 +143,7 @@ const TraktItem = React.memo(({ item, width, navigation, currentTheme }: { item: const SkeletonLoader = () => { const pulseAnim = React.useRef(new RNAnimated.Value(0)).current; - const { width } = useWindowDimensions(); + const { width, height } = useWindowDimensions(); const { numColumns, itemWidth } = getGridLayout(width); const { currentTheme } = useTheme(); @@ -204,7 +204,7 @@ const SkeletonLoader = () => { const LibraryScreen = () => { const navigation = useNavigation>(); const isDarkMode = useColorScheme() === 'dark'; - const { width } = useWindowDimensions(); + const { width, height } = useWindowDimensions(); const { numColumns, itemWidth } = useMemo(() => getGridLayout(width), [width]); const [loading, setLoading] = useState(true); const [libraryItems, setLibraryItems] = useState([]); @@ -913,7 +913,14 @@ const LibraryScreen = () => { }; const headerBaseHeight = Platform.OS === 'android' ? 80 : 60; - const topSpacing = Platform.OS === 'android' ? (StatusBar.currentHeight || 0) : insets.top; + // Tablet detection aligned with navigation tablet logic + const isTablet = useMemo(() => { + const smallestDimension = Math.min(width, height); + return (Platform.OS === 'ios' ? (Platform as any).isPad === true : smallestDimension >= 768); + }, [width, height]); + // Keep header below floating top navigator on tablets + const tabletNavOffset = isTablet ? 64 : 0; + const topSpacing = (Platform.OS === 'android' ? (StatusBar.currentHeight || 0) : insets.top) + tabletNavOffset; const headerHeight = headerBaseHeight + topSpacing; return ( diff --git a/src/screens/MetadataScreen.tsx b/src/screens/MetadataScreen.tsx index 4789d3d8..cec3c371 100644 --- a/src/screens/MetadataScreen.tsx +++ b/src/screens/MetadataScreen.tsx @@ -73,6 +73,11 @@ const MetadataScreen: React.FC = () => { const navigation = useNavigation>(); const { id, type, episodeId, addonId } = route.params; + // Log route parameters for debugging + React.useEffect(() => { + console.log('πŸ” [MetadataScreen] Route params:', { id, type, episodeId, addonId }); + }, [id, type, episodeId, addonId]); + // Consolidated hooks for better performance const { settings } = useSettings(); const { currentTheme } = useTheme(); @@ -124,6 +129,22 @@ const MetadataScreen: React.FC = () => { tmdbId, } = useMetadata({ id, type, addonId }); + // Log useMetadata hook state changes for debugging + React.useEffect(() => { + console.log('πŸ” [MetadataScreen] useMetadata state:', { + loading, + hasMetadata: !!metadata, + metadataId: metadata?.id, + metadataName: metadata?.name, + error: metadataError, + hasCast: cast.length > 0, + hasEpisodes: episodes.length > 0, + seasonsCount: Object.keys(groupedEpisodes).length, + imdbId, + tmdbId + }); + }, [loading, metadata, metadataError, cast.length, episodes.length, Object.keys(groupedEpisodes).length, imdbId, tmdbId]); + // Optimized hooks with memoization and conditional loading 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); @@ -391,6 +412,17 @@ const MetadataScreen: React.FC = () => { // Memoized derived values for performance const isReady = useMemo(() => !loading && metadata && !metadataError, [loading, metadata, metadataError]); + // Log readiness state for debugging + React.useEffect(() => { + console.log('πŸ” [MetadataScreen] Readiness state:', { + isReady, + loading, + hasMetadata: !!metadata, + hasError: !!metadataError, + errorMessage: metadataError + }); + }, [isReady, loading, metadata, metadataError]); + // Optimized content ready state management useEffect(() => { if (isReady && isScreenFocused) { @@ -720,11 +752,24 @@ const MetadataScreen: React.FC = () => { // Show error if exists if (metadataError || (!loading && !metadata)) { + console.log('πŸ” [MetadataScreen] Showing error component:', { + hasError: !!metadataError, + errorMessage: metadataError, + isLoading: loading, + hasMetadata: !!metadata, + loadingState: loading + }); return ErrorComponent; } // Show loading screen if metadata is not yet available if (loading || !isContentReady) { + console.log('πŸ” [MetadataScreen] Showing loading screen:', { + isLoading: loading, + isContentReady, + hasMetadata: !!metadata, + errorMessage: metadataError + }); return 0 ? 'series' : type as 'movie' | 'series'} />; } diff --git a/src/services/catalogService.ts b/src/services/catalogService.ts index 7adb983e..572d1cb2 100644 --- a/src/services/catalogService.ts +++ b/src/services/catalogService.ts @@ -499,6 +499,7 @@ class CatalogService { } async getContentDetails(type: string, id: string, preferredAddonId?: string): Promise { + console.log(`πŸ” [CatalogService] getContentDetails called:`, { type, id, preferredAddonId }); try { // Try up to 2 times with increasing delays to reduce CPU load let meta = null; @@ -506,21 +507,48 @@ class CatalogService { for (let i = 0; i < 2; i++) { try { + console.log(`πŸ” [CatalogService] Attempt ${i + 1}/2 for getContentDetails:`, { type, id, preferredAddonId }); + // Skip meta requests for non-content ids (e.g., provider slugs) - if (!stremioService.isValidContentId(type, id)) { + const isValidId = stremioService.isValidContentId(type, id); + console.log(`πŸ” [CatalogService] Content ID validation:`, { type, id, isValidId }); + + if (!isValidId) { + console.log(`πŸ” [CatalogService] Invalid content ID, breaking retry loop`); break; } + + console.log(`πŸ” [CatalogService] Calling stremioService.getMetaDetails:`, { type, id, preferredAddonId }); meta = await stremioService.getMetaDetails(type, id, preferredAddonId); + console.log(`πŸ” [CatalogService] stremioService.getMetaDetails result:`, { + hasMeta: !!meta, + metaId: meta?.id, + metaName: meta?.name, + metaType: meta?.type + }); + if (meta) break; await new Promise(resolve => setTimeout(resolve, 1000 * Math.pow(2, i))); } catch (error) { lastError = error; + console.log(`πŸ” [CatalogService] Attempt ${i + 1} failed:`, { + errorMessage: error instanceof Error ? error.message : String(error), + isAxiosError: (error as any)?.isAxiosError, + responseStatus: (error as any)?.response?.status, + responseData: (error as any)?.response?.data + }); logger.error(`Attempt ${i + 1} failed to get content details for ${type}:${id}:`, error); await new Promise(resolve => setTimeout(resolve, 1000 * Math.pow(2, i))); } } if (meta) { + console.log(`πŸ” [CatalogService] Meta found, converting to StreamingContent:`, { + metaId: meta.id, + metaName: meta.name, + metaType: meta.type + }); + // Add to recent content using enhanced conversion for full metadata const content = this.convertMetaToStreamingContentEnhanced(meta); this.addToRecentContent(content); @@ -528,15 +556,39 @@ class CatalogService { // Check if it's in the library content.inLibrary = this.library[`${type}:${id}`] !== undefined; + console.log(`πŸ” [CatalogService] Successfully converted meta to StreamingContent:`, { + contentId: content.id, + contentName: content.name, + contentType: content.type, + inLibrary: content.inLibrary + }); + return content; } + console.log(`πŸ” [CatalogService] No meta found, checking lastError:`, { + hasLastError: !!lastError, + lastErrorMessage: lastError instanceof Error ? lastError.message : String(lastError) + }); + if (lastError) { + console.log(`πŸ” [CatalogService] Throwing lastError:`, { + errorMessage: lastError instanceof Error ? lastError.message : String(lastError), + isAxiosError: (lastError as any)?.isAxiosError, + responseStatus: (lastError as any)?.response?.status + }); throw lastError; } + console.log(`πŸ” [CatalogService] No meta and no error, returning null`); return null; } catch (error) { + console.log(`πŸ” [CatalogService] getContentDetails caught error:`, { + errorMessage: error instanceof Error ? error.message : String(error), + isAxiosError: (error as any)?.isAxiosError, + responseStatus: (error as any)?.response?.status, + responseData: (error as any)?.response?.data + }); logger.error(`Failed to get content details for ${type}:${id}:`, error); return null; } @@ -544,8 +596,27 @@ class CatalogService { // Public method for getting enhanced metadata details (used by MetadataScreen) async getEnhancedContentDetails(type: string, id: string, preferredAddonId?: string): Promise { + console.log(`πŸ” [CatalogService] getEnhancedContentDetails called:`, { type, id, preferredAddonId }); logger.log(`πŸ” [MetadataScreen] Fetching enhanced metadata for ${type}:${id} ${preferredAddonId ? `from addon ${preferredAddonId}` : ''}`); - return this.getContentDetails(type, id, preferredAddonId); + + try { + const result = await this.getContentDetails(type, id, preferredAddonId); + console.log(`πŸ” [CatalogService] getEnhancedContentDetails result:`, { + hasResult: !!result, + resultId: result?.id, + resultName: result?.name, + resultType: result?.type + }); + return result; + } catch (error) { + console.log(`πŸ” [CatalogService] getEnhancedContentDetails error:`, { + errorMessage: error instanceof Error ? error.message : String(error), + isAxiosError: (error as any)?.isAxiosError, + responseStatus: (error as any)?.response?.status, + responseData: (error as any)?.response?.data + }); + throw error; + } } // Public method for getting basic content details without enhanced processing (used by ContinueWatching, etc.) diff --git a/src/services/stremioService.ts b/src/services/stremioService.ts index 58d387ac..8428b2bd 100644 --- a/src/services/stremioService.ts +++ b/src/services/stremioService.ts @@ -201,6 +201,11 @@ class StremioService { // Get all supported ID prefixes from installed addons const supportedPrefixes = this.getAllSupportedIdPrefixes(type); + // If no addons declare specific prefixes, allow any non-empty string + if (supportedPrefixes.length === 0) { + return true; + } + // Check if the ID matches any supported prefix return supportedPrefixes.some(prefix => lowerId.startsWith(prefix.toLowerCase())); } @@ -232,10 +237,8 @@ class StremioService { } } - // Always include common prefixes as fallback - prefixes.add('tt'); // IMDb - prefixes.add('kitsu:'); // Kitsu - prefixes.add('series:'); // Series + // No hardcoded prefixes - let addons declare their own support dynamically + // If no addons declare prefixes, we'll allow any non-empty string return Array.from(prefixes); } @@ -760,16 +763,23 @@ class StremioService { } async getMetaDetails(type: string, id: string, preferredAddonId?: string): Promise { + console.log(`πŸ” [StremioService] getMetaDetails called:`, { type, id, preferredAddonId }); try { // Validate content ID first - if (!this.isValidContentId(type, id)) { + const isValidId = this.isValidContentId(type, id); + console.log(`πŸ” [StremioService] Content ID validation:`, { type, id, isValidId }); + + if (!isValidId) { + console.log(`πŸ” [StremioService] Invalid content ID, returning null`); return null; } const addons = this.getInstalledAddons(); + console.log(`πŸ” [StremioService] Found ${addons.length} installed addons`); // If a preferred addon is specified, try it first if (preferredAddonId) { + console.log(`πŸ” [StremioService] Preferred addon specified:`, { preferredAddonId }); const preferredAddon = addons.find(addon => addon.id === preferredAddonId); if (preferredAddon && preferredAddon.resources) { @@ -814,18 +824,49 @@ class StremioService { } } - if (hasMetaSupport && supportsIdPrefix) { + console.log(`πŸ” [StremioService] Preferred addon support check:`, { + hasMetaSupport, + supportsIdPrefix, + addonId: preferredAddon.id, + addonName: preferredAddon.name, + hasDeclaredPrefixes: preferredAddon.idPrefixes && preferredAddon.idPrefixes.length > 0 + }); + + // Only require ID prefix compatibility if the addon has declared specific prefixes + const requiresIdPrefix = preferredAddon.idPrefixes && preferredAddon.idPrefixes.length > 0; + const isSupported = hasMetaSupport && (!requiresIdPrefix || supportsIdPrefix); + + if (isSupported) { + console.log(`πŸ” [StremioService] Requesting metadata from preferred addon:`, { url }); try { const response = await this.retryRequest(async () => { return await axios.get(url, { timeout: 10000 }); }); + console.log(`πŸ” [StremioService] Preferred addon response:`, { + hasData: !!response.data, + hasMeta: !!response.data?.meta, + metaId: response.data?.meta?.id, + metaName: response.data?.meta?.name + }); + if (response.data && response.data.meta) { + console.log(`πŸ” [StremioService] Successfully got metadata from preferred addon`); return response.data.meta; + } else { + console.log(`πŸ” [StremioService] Preferred addon returned no metadata`); } } catch (error: any) { + console.log(`πŸ” [StremioService] Preferred addon request failed:`, { + errorMessage: error.message, + isAxiosError: error.isAxiosError, + responseStatus: error.response?.status, + responseData: error.response?.data + }); // Continue trying other addons } + } else { + console.log(`πŸ” [StremioService] Preferred addon doesn't support this content type${requiresIdPrefix ? ' or ID prefix' : ''}`); } } } @@ -836,19 +877,40 @@ class StremioService { 'http://v3-cinemeta.strem.io' ]; + console.log(`πŸ” [StremioService] Trying Cinemeta URLs:`, { cinemetaUrls }); + for (const baseUrl of cinemetaUrls) { try { const encodedId = encodeURIComponent(id); const url = `${baseUrl}/meta/${type}/${encodedId}.json`; + + console.log(`πŸ” [StremioService] Requesting from Cinemeta:`, { url }); const response = await this.retryRequest(async () => { return await axios.get(url, { timeout: 10000 }); }); + console.log(`πŸ” [StremioService] Cinemeta response:`, { + hasData: !!response.data, + hasMeta: !!response.data?.meta, + metaId: response.data?.meta?.id, + metaName: response.data?.meta?.name + }); + if (response.data && response.data.meta) { + console.log(`πŸ” [StremioService] Successfully got metadata from Cinemeta`); return response.data.meta; + } else { + console.log(`πŸ” [StremioService] Cinemeta returned no metadata`); } } catch (error: any) { + console.log(`πŸ” [StremioService] Cinemeta request failed:`, { + baseUrl, + errorMessage: error.message, + isAxiosError: error.isAxiosError, + responseStatus: error.response?.status, + responseData: error.response?.data + }); continue; // Try next URL } } @@ -893,28 +955,75 @@ class StremioService { } } - // Require both meta support and idPrefix compatibility - if (!(hasMetaSupport && supportsIdPrefix)) continue; + // Require meta support, but allow any ID if addon doesn't declare specific prefixes + console.log(`πŸ” [StremioService] Addon support check:`, { + addonId: addon.id, + addonName: addon.name, + hasMetaSupport, + supportsIdPrefix, + hasDeclaredPrefixes: addon.idPrefixes && addon.idPrefixes.length > 0 + }); + + // Only require ID prefix compatibility if the addon has declared specific prefixes + const requiresIdPrefix = addon.idPrefixes && addon.idPrefixes.length > 0; + const isSupported = hasMetaSupport && (!requiresIdPrefix || supportsIdPrefix); + + if (!isSupported) { + console.log(`πŸ” [StremioService] Addon doesn't support this content type${requiresIdPrefix ? ' or ID prefix' : ''}, skipping`); + continue; + } try { const { baseUrl, queryParams } = this.getAddonBaseURL(addon.url || ''); const encodedId = encodeURIComponent(id); const url = queryParams ? `${baseUrl}/meta/${type}/${encodedId}.json?${queryParams}` : `${baseUrl}/meta/${type}/${encodedId}.json`; + + console.log(`πŸ” [StremioService] Requesting from addon:`, { + addonId: addon.id, + addonName: addon.name, + url + }); const response = await this.retryRequest(async () => { return await axios.get(url, { timeout: 10000 }); }); + console.log(`πŸ” [StremioService] Addon response:`, { + addonId: addon.id, + hasData: !!response.data, + hasMeta: !!response.data?.meta, + metaId: response.data?.meta?.id, + metaName: response.data?.meta?.name + }); + if (response.data && response.data.meta) { + console.log(`πŸ” [StremioService] Successfully got metadata from addon:`, { addonId: addon.id }); return response.data.meta; + } else { + console.log(`πŸ” [StremioService] Addon returned no metadata:`, { addonId: addon.id }); } - } catch (error) { + } catch (error: any) { + console.log(`πŸ” [StremioService] Addon request failed:`, { + addonId: addon.id, + addonName: addon.name, + errorMessage: error.message, + isAxiosError: error.isAxiosError, + responseStatus: error.response?.status, + responseData: error.response?.data + }); continue; // Try next addon } } + console.log(`πŸ” [StremioService] No metadata found from any addon`); return null; } catch (error) { + console.log(`πŸ” [StremioService] getMetaDetails caught error:`, { + errorMessage: error instanceof Error ? error.message : String(error), + isAxiosError: (error as any)?.isAxiosError, + responseStatus: (error as any)?.response?.status, + responseData: (error as any)?.response?.data + }); logger.error('Error in getMetaDetails:', error); return null; } @@ -1045,6 +1154,14 @@ class StremioService { season = parseInt(idParts[2], 10); episode = parseInt(idParts[3], 10); } + } else if (idParts[0] === 'tmdb') { + // Format: tmdb:286801:season:episode (direct TMDB ID) + baseId = idParts[1]; + idType = 'tmdb'; + if (scraperType === 'tv' && idParts.length >= 4) { + season = parseInt(idParts[2], 10); + episode = parseInt(idParts[3], 10); + } } else { // Fallback: assume first part is the ID baseId = idParts[0]; @@ -1054,8 +1171,9 @@ class StremioService { } } - // Only try to convert to TMDB ID for IMDb IDs (local scrapers need TMDB) + // Handle ID conversion for local scrapers (they need TMDB ID) if (idType === 'imdb') { + // Convert IMDb ID to TMDB ID const tmdbService = TMDBService.getInstance(); const tmdbIdNumber = await tmdbService.findTMDBIdByIMDB(baseId); if (tmdbIdNumber) { @@ -1063,16 +1181,24 @@ class StremioService { } else { logger.log('πŸ”§ [getStreams] Skipping local scrapers: could not convert IMDb to TMDB for', baseId); } + } else if (idType === 'tmdb') { + // Already have TMDB ID, use it directly + tmdbId = baseId; + logger.log('πŸ”§ [getStreams] Using TMDB ID directly for local scrapers:', tmdbId); } 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); + } else { + // For other ID types, try to use as TMDB ID + tmdbId = baseId; + logger.log('πŸ”§ [getStreams] Using base ID as TMDB ID for local scrapers:', tmdbId); } } catch (error) { logger.warn('πŸ”§ [getStreams] Skipping local scrapers due to ID parsing error:', error); } - // Execute local scrapers asynchronously with TMDB ID (only for IMDb IDs) - if (idType === 'imdb' && tmdbId) { + // Execute local scrapers asynchronously with TMDB ID (when available) + if (tmdbId) { localScraperService.getStreams(scraperType, tmdbId, season, episode, (streams, scraperId, scraperName, error) => { if (error) { if (callback) {