some addon improvements

This commit is contained in:
tapframe 2025-10-10 23:42:28 +05:30
parent 704c642a8f
commit 7dceb23e3d
5 changed files with 405 additions and 47 deletions

View file

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

View file

@ -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<NavigationProp<RootStackParamList>>();
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<LibraryItem[]>([]);
@ -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 (

View file

@ -73,6 +73,11 @@ const MetadataScreen: React.FC = () => {
const navigation = useNavigation<NavigationProp<RootStackParamList>>();
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 <MetadataLoadingScreen type={Object.keys(groupedEpisodes).length > 0 ? 'series' : type as 'movie' | 'series'} />;
}

View file

@ -499,6 +499,7 @@ class CatalogService {
}
async getContentDetails(type: string, id: string, preferredAddonId?: string): Promise<StreamingContent | null> {
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<StreamingContent | null> {
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.)

View file

@ -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<MetaDetails | null> {
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) {