some addon improvements
This commit is contained in:
parent
704c642a8f
commit
7dceb23e3d
5 changed files with 405 additions and 47 deletions
|
|
@ -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);
|
||||
}
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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 (
|
||||
|
|
|
|||
|
|
@ -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'} />;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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.)
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
Loading…
Reference in a new issue