mirror of
https://github.com/tapframe/NuvioStreaming.git
synced 2026-03-11 17:45:38 +00:00
Enhance metadata handling and navigation with addon support
This update introduces support for addon IDs in various components, including CatalogSection, ContinueWatchingSection, and MetadataScreen, allowing for enhanced metadata fetching. The CatalogService now includes methods for retrieving both basic and enhanced content details based on the specified addon. Additionally, improvements to the loading process in HomeScreen ensure a more efficient catalog loading experience. These changes enhance user experience by providing richer content details and smoother navigation.
This commit is contained in:
parent
805d7e1fa6
commit
d88962ae01
12 changed files with 365 additions and 43 deletions
|
|
@ -60,7 +60,7 @@ const CatalogSection = ({ catalog }: CatalogSectionProps) => {
|
|||
const { currentTheme } = useTheme();
|
||||
|
||||
const handleContentPress = (id: string, type: string) => {
|
||||
navigation.navigate('Metadata', { id, type });
|
||||
navigation.navigate('Metadata', { id, type, addonId: catalog.addon });
|
||||
};
|
||||
|
||||
const renderContentItem = ({ item, index }: { item: StreamingContent, index: number }) => {
|
||||
|
|
|
|||
|
|
@ -116,8 +116,8 @@ const ContinueWatchingSection = React.forwardRef<ContinueWatchingRef>((props, re
|
|||
|
||||
let content: StreamingContent | null = null;
|
||||
|
||||
// Get content details using catalogService
|
||||
content = await catalogService.getContentDetails(type, id);
|
||||
// Get basic content details using catalogService (no enhanced metadata needed for continue watching)
|
||||
content = await catalogService.getBasicContentDetails(type, id);
|
||||
|
||||
if (content) {
|
||||
// Extract season and episode info from episodeId if available
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ interface MovieContentProps {
|
|||
export const MovieContent: React.FC<MovieContentProps> = ({ metadata }) => {
|
||||
const { currentTheme } = useTheme();
|
||||
const hasCast = Array.isArray(metadata.cast) && metadata.cast.length > 0;
|
||||
const castDisplay = hasCast ? (metadata.cast as string[]).slice(0, 5).join(', ') : '';
|
||||
const castDisplay = hasCast ? metadata.cast!.slice(0, 5).join(', ') : '';
|
||||
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
|
|
@ -23,10 +23,10 @@ export const MovieContent: React.FC<MovieContentProps> = ({ metadata }) => {
|
|||
</View>
|
||||
)}
|
||||
|
||||
{metadata.writer && (
|
||||
{metadata.writer && metadata.writer.length > 0 && (
|
||||
<View style={styles.metadataRow}>
|
||||
<Text style={[styles.metadataLabel, { color: currentTheme.colors.textMuted }]}>Writer:</Text>
|
||||
<Text style={[styles.metadataValue, { color: currentTheme.colors.text }]}>{metadata.writer}</Text>
|
||||
<Text style={[styles.metadataValue, { color: currentTheme.colors.text }]}>{Array.isArray(metadata.writer) ? metadata.writer.join(', ') : metadata.writer}</Text>
|
||||
</View>
|
||||
)}
|
||||
|
||||
|
|
|
|||
|
|
@ -60,6 +60,7 @@ const withRetry = async <T>(
|
|||
interface UseMetadataProps {
|
||||
id: string;
|
||||
type: string;
|
||||
addonId?: string;
|
||||
}
|
||||
|
||||
interface UseMetadataReturn {
|
||||
|
|
@ -94,7 +95,7 @@ interface UseMetadataReturn {
|
|||
imdbId: string | null;
|
||||
}
|
||||
|
||||
export const useMetadata = ({ id, type }: UseMetadataProps): UseMetadataReturn => {
|
||||
export const useMetadata = ({ id, type, addonId }: UseMetadataProps): UseMetadataReturn => {
|
||||
const [metadata, setMetadata] = useState<StreamingContent | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
|
@ -411,7 +412,7 @@ export const useMetadata = ({ id, type }: UseMetadataProps): UseMetadataReturn =
|
|||
|
||||
if (writers.length > 0) {
|
||||
(formattedMovie as any).creators = writers;
|
||||
(formattedMovie as StreamingContent & { writer: string }).writer = writers.join(', ');
|
||||
(formattedMovie as any).writer = writers;
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
|
|
@ -513,10 +514,10 @@ export const useMetadata = ({ id, type }: UseMetadataProps): UseMetadataReturn =
|
|||
const [content, castData] = await Promise.allSettled([
|
||||
// Load content with timeout and retry
|
||||
withRetry(async () => {
|
||||
const result = await withTimeout(
|
||||
catalogService.getContentDetails(type, actualId),
|
||||
API_TIMEOUT
|
||||
);
|
||||
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);
|
||||
|
|
@ -540,8 +541,10 @@ export const useMetadata = ({ id, type }: UseMetadataProps): UseMetadataReturn =
|
|||
cacheService.setMetadata(id, type, content.value);
|
||||
|
||||
if (type === 'series') {
|
||||
// Load series data in parallel with other data
|
||||
loadSeriesData().catch(console.error);
|
||||
// Load series data after the enhanced metadata is processed
|
||||
setTimeout(() => {
|
||||
loadSeriesData().catch(console.error);
|
||||
}, 100);
|
||||
}
|
||||
} else {
|
||||
throw new Error('Content not found');
|
||||
|
|
@ -564,6 +567,67 @@ export const useMetadata = ({ id, type }: UseMetadataProps): UseMetadataReturn =
|
|||
const loadSeriesData = async () => {
|
||||
setLoadingSeasons(true);
|
||||
try {
|
||||
// First check if we have episode data from the addon
|
||||
const addonVideos = metadata?.videos;
|
||||
if (addonVideos && Array.isArray(addonVideos) && addonVideos.length > 0) {
|
||||
logger.log(`🎬 Found ${addonVideos.length} episodes from addon metadata for ${metadata?.name || id}`);
|
||||
|
||||
// Group addon episodes by season
|
||||
const groupedAddonEpisodes: GroupedEpisodes = {};
|
||||
|
||||
addonVideos.forEach((video: any) => {
|
||||
const seasonNumber = video.season || 1;
|
||||
const episodeNumber = video.episode || video.number || 1;
|
||||
|
||||
if (!groupedAddonEpisodes[seasonNumber]) {
|
||||
groupedAddonEpisodes[seasonNumber] = [];
|
||||
}
|
||||
|
||||
// Convert addon episode format to our Episode interface
|
||||
const episode: Episode = {
|
||||
id: video.id,
|
||||
name: video.name || video.title || `Episode ${episodeNumber}`,
|
||||
overview: video.overview || video.description || '',
|
||||
season_number: seasonNumber,
|
||||
episode_number: episodeNumber,
|
||||
air_date: video.released ? video.released.split('T')[0] : video.firstAired ? video.firstAired.split('T')[0] : '',
|
||||
still_path: video.thumbnail ? video.thumbnail.replace('https://image.tmdb.org/t/p/w500', '') : null,
|
||||
vote_average: parseFloat(video.rating) || 0,
|
||||
runtime: undefined,
|
||||
episodeString: `S${seasonNumber.toString().padStart(2, '0')}E${episodeNumber.toString().padStart(2, '0')}`,
|
||||
stremioId: video.id,
|
||||
season_poster_path: null
|
||||
};
|
||||
|
||||
groupedAddonEpisodes[seasonNumber].push(episode);
|
||||
});
|
||||
|
||||
// Sort episodes within each season
|
||||
Object.keys(groupedAddonEpisodes).forEach(season => {
|
||||
groupedAddonEpisodes[parseInt(season)].sort((a, b) => a.episode_number - b.episode_number);
|
||||
});
|
||||
|
||||
logger.log(`📺 Processed addon episodes into ${Object.keys(groupedAddonEpisodes).length} seasons`);
|
||||
setGroupedEpisodes(groupedAddonEpisodes);
|
||||
|
||||
// Set the first available season
|
||||
const seasons = Object.keys(groupedAddonEpisodes).map(Number);
|
||||
const firstSeason = Math.min(...seasons);
|
||||
logger.log(`📺 Setting season ${firstSeason} as selected (${groupedAddonEpisodes[firstSeason]?.length || 0} episodes)`);
|
||||
setSelectedSeason(firstSeason);
|
||||
setEpisodes(groupedAddonEpisodes[firstSeason] || []);
|
||||
|
||||
// Try to get TMDB ID for additional metadata (cast, etc.) but don't override episodes
|
||||
const tmdbIdResult = await tmdbService.findTMDBIdByIMDB(id);
|
||||
if (tmdbIdResult) {
|
||||
setTmdbId(tmdbIdResult);
|
||||
}
|
||||
|
||||
return; // Use addon episodes, skip TMDB loading
|
||||
}
|
||||
|
||||
// Fallback to TMDB if no addon episodes
|
||||
logger.log('📺 No addon episodes found, falling back to TMDB');
|
||||
const tmdbIdResult = await tmdbService.findTMDBIdByIMDB(id);
|
||||
if (tmdbIdResult) {
|
||||
setTmdbId(tmdbIdResult);
|
||||
|
|
@ -866,6 +930,14 @@ export const useMetadata = ({ id, type }: UseMetadataProps): UseMetadataReturn =
|
|||
loadMetadata();
|
||||
}, [id, type]);
|
||||
|
||||
// Re-run series data loading when metadata updates with videos
|
||||
useEffect(() => {
|
||||
if (metadata && type === 'series' && metadata.videos && metadata.videos.length > 0) {
|
||||
logger.log(`🎬 Metadata updated with ${metadata.videos.length} episodes, reloading series data`);
|
||||
loadSeriesData().catch(console.error);
|
||||
}
|
||||
}, [metadata?.videos, type]);
|
||||
|
||||
const loadRecommendations = useCallback(async () => {
|
||||
if (!tmdbId) return;
|
||||
|
||||
|
|
|
|||
|
|
@ -54,6 +54,7 @@ export type RootStackParamList = {
|
|||
id: string;
|
||||
type: string;
|
||||
episodeId?: string;
|
||||
addonId?: string;
|
||||
};
|
||||
Streams: {
|
||||
id: string;
|
||||
|
|
|
|||
|
|
@ -503,7 +503,7 @@ const CatalogScreen: React.FC<CatalogScreenProps> = ({ route, navigation }) => {
|
|||
width: NUM_COLUMNS === 2 ? ITEM_WIDTH : ITEM_WIDTH
|
||||
}
|
||||
]}
|
||||
onPress={() => navigation.navigate('Metadata', { id: item.id, type: item.type })}
|
||||
onPress={() => navigation.navigate('Metadata', { id: item.id, type: item.type, addonId })}
|
||||
activeOpacity={0.7}
|
||||
>
|
||||
<Image
|
||||
|
|
|
|||
|
|
@ -497,12 +497,14 @@ const HomeScreen = () => {
|
|||
// Initialize catalogs array with proper length
|
||||
setCatalogs(new Array(catalogIndex).fill(null));
|
||||
|
||||
// Wait for all catalogs to finish loading (success or failure)
|
||||
await Promise.allSettled(catalogPromises);
|
||||
console.log('[HomeScreen] All catalogs processed');
|
||||
|
||||
// Filter out null values to get only successfully loaded catalogs
|
||||
setCatalogs(prevCatalogs => prevCatalogs.filter(catalog => catalog !== null));
|
||||
// Start all catalog loading promises but don't wait for them
|
||||
// They will update the state progressively as they complete
|
||||
Promise.allSettled(catalogPromises).then(() => {
|
||||
console.log('[HomeScreen] All catalogs processed');
|
||||
|
||||
// Final cleanup: Filter out null values to get only successfully loaded catalogs
|
||||
setCatalogs(prevCatalogs => prevCatalogs.filter(catalog => catalog !== null));
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error('[HomeScreen] Error in progressive catalog loading:', error);
|
||||
|
|
|
|||
|
|
@ -45,9 +45,9 @@ import { TraktService, TraktPlaybackItem } from '../services/traktService';
|
|||
const { height } = Dimensions.get('window');
|
||||
|
||||
const MetadataScreen: React.FC = () => {
|
||||
const route = useRoute<RouteProp<Record<string, RouteParams & { episodeId?: string }>, string>>();
|
||||
const route = useRoute<RouteProp<Record<string, RouteParams & { episodeId?: string; addonId?: string }>, string>>();
|
||||
const navigation = useNavigation<NavigationProp<RootStackParamList>>();
|
||||
const { id, type, episodeId } = route.params;
|
||||
const { id, type, episodeId, addonId } = route.params;
|
||||
|
||||
// Consolidated hooks for better performance
|
||||
const { settings } = useSettings();
|
||||
|
|
@ -78,7 +78,7 @@ const MetadataScreen: React.FC = () => {
|
|||
loadingRecommendations,
|
||||
setMetadata,
|
||||
imdbId,
|
||||
} = useMetadata({ id, type });
|
||||
} = useMetadata({ id, type, addonId });
|
||||
|
||||
// Optimized hooks with memoization
|
||||
const watchProgressData = useWatchProgress(id, type as 'movie' | 'series', episodeId, episodes);
|
||||
|
|
|
|||
|
|
@ -54,6 +54,22 @@ export interface StreamingContent {
|
|||
directors?: string[];
|
||||
creators?: string[];
|
||||
certification?: string;
|
||||
// Enhanced metadata from addons
|
||||
country?: string;
|
||||
writer?: string[];
|
||||
links?: Array<{
|
||||
name: string;
|
||||
category: string;
|
||||
url: string;
|
||||
}>;
|
||||
behaviorHints?: {
|
||||
defaultVideoId?: string;
|
||||
hasScheduledVideos?: boolean;
|
||||
[key: string]: any;
|
||||
};
|
||||
imdb_id?: string;
|
||||
slug?: string;
|
||||
releaseInfo?: string;
|
||||
}
|
||||
|
||||
export interface CatalogContent {
|
||||
|
|
@ -442,7 +458,7 @@ class CatalogService {
|
|||
}
|
||||
}
|
||||
|
||||
async getContentDetails(type: string, id: string): Promise<StreamingContent | null> {
|
||||
async getContentDetails(type: string, id: string, preferredAddonId?: string): Promise<StreamingContent | null> {
|
||||
try {
|
||||
// Try up to 3 times with increasing delays
|
||||
let meta = null;
|
||||
|
|
@ -450,7 +466,7 @@ class CatalogService {
|
|||
|
||||
for (let i = 0; i < 3; i++) {
|
||||
try {
|
||||
meta = await stremioService.getMetaDetails(type, id);
|
||||
meta = await stremioService.getMetaDetails(type, id, preferredAddonId);
|
||||
if (meta) break;
|
||||
await new Promise(resolve => setTimeout(resolve, 1000 * Math.pow(2, i)));
|
||||
} catch (error) {
|
||||
|
|
@ -461,8 +477,8 @@ class CatalogService {
|
|||
}
|
||||
|
||||
if (meta) {
|
||||
// Add to recent content
|
||||
const content = this.convertMetaToStreamingContent(meta);
|
||||
// Add to recent content using enhanced conversion for full metadata
|
||||
const content = this.convertMetaToStreamingContentEnhanced(meta);
|
||||
this.addToRecentContent(content);
|
||||
|
||||
// Check if it's in the library
|
||||
|
|
@ -482,7 +498,54 @@ class CatalogService {
|
|||
}
|
||||
}
|
||||
|
||||
// Public method for getting enhanced metadata details (used by MetadataScreen)
|
||||
async getEnhancedContentDetails(type: string, id: string, preferredAddonId?: string): Promise<StreamingContent | null> {
|
||||
logger.log(`🔍 [MetadataScreen] Fetching enhanced metadata for ${type}:${id} ${preferredAddonId ? `from addon ${preferredAddonId}` : ''}`);
|
||||
return this.getContentDetails(type, id, preferredAddonId);
|
||||
}
|
||||
|
||||
// Public method for getting basic content details without enhanced processing (used by ContinueWatching, etc.)
|
||||
async getBasicContentDetails(type: string, id: string, preferredAddonId?: string): Promise<StreamingContent | null> {
|
||||
try {
|
||||
// Try up to 3 times with increasing delays
|
||||
let meta = null;
|
||||
let lastError = null;
|
||||
|
||||
for (let i = 0; i < 3; i++) {
|
||||
try {
|
||||
meta = await stremioService.getMetaDetails(type, id, preferredAddonId);
|
||||
if (meta) break;
|
||||
await new Promise(resolve => setTimeout(resolve, 1000 * Math.pow(2, i)));
|
||||
} catch (error) {
|
||||
lastError = error;
|
||||
logger.error(`Attempt ${i + 1} failed to get basic content details for ${type}:${id}:`, error);
|
||||
await new Promise(resolve => setTimeout(resolve, 1000 * Math.pow(2, i)));
|
||||
}
|
||||
}
|
||||
|
||||
if (meta) {
|
||||
// Use basic conversion without enhanced metadata processing
|
||||
const content = this.convertMetaToStreamingContent(meta);
|
||||
|
||||
// Check if it's in the library
|
||||
content.inLibrary = this.library[`${type}:${id}`] !== undefined;
|
||||
|
||||
return content;
|
||||
}
|
||||
|
||||
if (lastError) {
|
||||
throw lastError;
|
||||
}
|
||||
|
||||
return null;
|
||||
} catch (error) {
|
||||
logger.error(`Failed to get basic content details for ${type}:${id}:`, error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private convertMetaToStreamingContent(meta: Meta): StreamingContent {
|
||||
// Basic conversion for catalog display - no enhanced metadata processing
|
||||
return {
|
||||
id: meta.id,
|
||||
type: meta.type,
|
||||
|
|
@ -490,17 +553,70 @@ class CatalogService {
|
|||
poster: meta.poster || 'https://via.placeholder.com/300x450/cccccc/666666?text=No+Image',
|
||||
posterShape: 'poster',
|
||||
banner: meta.background,
|
||||
logo: `https://images.metahub.space/logo/medium/${meta.id}/img`,
|
||||
logo: `https://images.metahub.space/logo/medium/${meta.id}/img`, // Use metahub for catalog display
|
||||
imdbRating: meta.imdbRating,
|
||||
year: meta.year,
|
||||
genres: meta.genres,
|
||||
description: meta.description,
|
||||
runtime: meta.runtime,
|
||||
inLibrary: this.library[`${meta.type}:${meta.id}`] !== undefined,
|
||||
certification: meta.certification
|
||||
certification: meta.certification,
|
||||
releaseInfo: meta.releaseInfo,
|
||||
};
|
||||
}
|
||||
|
||||
// Enhanced conversion for detailed metadata (used only when fetching individual content details)
|
||||
private convertMetaToStreamingContentEnhanced(meta: Meta): StreamingContent {
|
||||
// Enhanced conversion to utilize all available metadata from addons
|
||||
const converted: StreamingContent = {
|
||||
id: meta.id,
|
||||
type: meta.type,
|
||||
name: meta.name,
|
||||
poster: meta.poster || 'https://via.placeholder.com/300x450/cccccc/666666?text=No+Image',
|
||||
posterShape: 'poster',
|
||||
banner: meta.background,
|
||||
// Use addon's logo if available, fallback to metahub
|
||||
logo: (meta as any).logo || `https://images.metahub.space/logo/medium/${meta.id}/img`,
|
||||
imdbRating: meta.imdbRating,
|
||||
year: meta.year,
|
||||
genres: meta.genres,
|
||||
description: meta.description,
|
||||
runtime: meta.runtime,
|
||||
inLibrary: this.library[`${meta.type}:${meta.id}`] !== undefined,
|
||||
certification: meta.certification,
|
||||
// Enhanced fields from addon metadata
|
||||
directors: (meta as any).director ?
|
||||
(Array.isArray((meta as any).director) ? (meta as any).director : [(meta as any).director])
|
||||
: undefined,
|
||||
writer: (meta as any).writer || undefined,
|
||||
country: (meta as any).country || undefined,
|
||||
imdb_id: (meta as any).imdb_id || undefined,
|
||||
slug: (meta as any).slug || undefined,
|
||||
releaseInfo: meta.releaseInfo || (meta as any).releaseInfo || undefined,
|
||||
trailerStreams: (meta as any).trailerStreams || undefined,
|
||||
links: (meta as any).links || undefined,
|
||||
behaviorHints: (meta as any).behaviorHints || undefined,
|
||||
};
|
||||
|
||||
// Cast is handled separately by the dedicated CastSection component via TMDB
|
||||
|
||||
// Log if rich metadata is found
|
||||
if ((meta as any).trailerStreams?.length > 0) {
|
||||
logger.log(`🎬 Enhanced metadata: Found ${(meta as any).trailerStreams.length} trailers for ${meta.name}`);
|
||||
}
|
||||
|
||||
if ((meta as any).links?.length > 0) {
|
||||
logger.log(`🔗 Enhanced metadata: Found ${(meta as any).links.length} links for ${meta.name}`);
|
||||
}
|
||||
|
||||
// Handle videos/episodes if available
|
||||
if ((meta as any).videos) {
|
||||
converted.videos = (meta as any).videos;
|
||||
}
|
||||
|
||||
return converted;
|
||||
}
|
||||
|
||||
private notifyLibrarySubscribers(): void {
|
||||
const items = Object.values(this.library);
|
||||
this.librarySubscribers.forEach(callback => callback(items));
|
||||
|
|
|
|||
|
|
@ -26,9 +26,35 @@ export interface Meta {
|
|||
genres?: string[];
|
||||
runtime?: string;
|
||||
cast?: string[];
|
||||
director?: string;
|
||||
writer?: string;
|
||||
director?: string | string[];
|
||||
writer?: string | string[];
|
||||
certification?: string;
|
||||
// Extended fields available from some addons
|
||||
country?: string;
|
||||
imdb_id?: string;
|
||||
slug?: string;
|
||||
released?: string;
|
||||
trailerStreams?: Array<{
|
||||
title: string;
|
||||
ytId: string;
|
||||
}>;
|
||||
links?: Array<{
|
||||
name: string;
|
||||
category: string;
|
||||
url: string;
|
||||
}>;
|
||||
behaviorHints?: {
|
||||
defaultVideoId?: string;
|
||||
hasScheduledVideos?: boolean;
|
||||
[key: string]: any;
|
||||
};
|
||||
app_extras?: {
|
||||
cast?: Array<{
|
||||
name: string;
|
||||
character?: string;
|
||||
photo?: string;
|
||||
}>;
|
||||
};
|
||||
}
|
||||
|
||||
export interface Subtitle {
|
||||
|
|
@ -464,8 +490,71 @@ class StremioService {
|
|||
}
|
||||
}
|
||||
|
||||
async getMetaDetails(type: string, id: string): Promise<MetaDetails | null> {
|
||||
async getMetaDetails(type: string, id: string, preferredAddonId?: string): Promise<MetaDetails | null> {
|
||||
try {
|
||||
const addons = this.getInstalledAddons();
|
||||
|
||||
// If a preferred addon is specified, try it first
|
||||
if (preferredAddonId) {
|
||||
logger.log(`🎯 Trying preferred addon first: ${preferredAddonId}`);
|
||||
const preferredAddon = addons.find(addon => addon.id === preferredAddonId);
|
||||
|
||||
if (preferredAddon && preferredAddon.resources) {
|
||||
// Log what URL would be used for debugging
|
||||
const { baseUrl, queryParams } = this.getAddonBaseURL(preferredAddon.url || '');
|
||||
const wouldBeUrl = queryParams ? `${baseUrl}/meta/${type}/${id}.json?${queryParams}` : `${baseUrl}/meta/${type}/${id}.json`;
|
||||
logger.log(`🔍 Would check URL: ${wouldBeUrl} (addon: ${preferredAddon.name})`);
|
||||
|
||||
// Log addon resources for debugging
|
||||
logger.log(`🔍 Addon resources:`, JSON.stringify(preferredAddon.resources, null, 2));
|
||||
|
||||
// Check if addon supports meta resource for this type
|
||||
let hasMetaSupport = false;
|
||||
|
||||
for (const resource of preferredAddon.resources) {
|
||||
// Check if the current element is a ResourceObject
|
||||
if (typeof resource === 'object' && resource !== null && 'name' in resource) {
|
||||
const typedResource = resource as ResourceObject;
|
||||
if (typedResource.name === 'meta' &&
|
||||
Array.isArray(typedResource.types) &&
|
||||
typedResource.types.includes(type)) {
|
||||
hasMetaSupport = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
// Check if the element is the simple string "meta" AND the addon has a top-level types array
|
||||
else if (typeof resource === 'string' && resource === 'meta' && preferredAddon.types) {
|
||||
if (Array.isArray(preferredAddon.types) && preferredAddon.types.includes(type)) {
|
||||
hasMetaSupport = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
logger.log(`🔍 Meta support check: ${hasMetaSupport} (addon types: ${JSON.stringify(preferredAddon.types)})`);
|
||||
|
||||
if (hasMetaSupport) {
|
||||
try {
|
||||
logger.log(`HTTP GET: ${wouldBeUrl} (preferred addon: ${preferredAddon.name})`);
|
||||
const response = await this.retryRequest(async () => {
|
||||
return await axios.get(wouldBeUrl, { timeout: 10000 });
|
||||
});
|
||||
|
||||
if (response.data && response.data.meta) {
|
||||
logger.log(`✅ Metadata fetched successfully from preferred addon: ${wouldBeUrl}`);
|
||||
return response.data.meta;
|
||||
}
|
||||
} catch (error) {
|
||||
logger.warn(`❌ Failed to fetch meta from preferred addon ${preferredAddon.name}:`, error);
|
||||
}
|
||||
} else {
|
||||
logger.warn(`⚠️ Preferred addon ${preferredAddonId} does not support meta for type ${type}`);
|
||||
}
|
||||
} else {
|
||||
logger.warn(`⚠️ Preferred addon ${preferredAddonId} not found or has no resources`);
|
||||
}
|
||||
}
|
||||
|
||||
// Try Cinemeta with different base URLs
|
||||
const cinemetaUrls = [
|
||||
'https://v3-cinemeta.strem.io',
|
||||
|
|
@ -475,44 +564,66 @@ class StremioService {
|
|||
for (const baseUrl of cinemetaUrls) {
|
||||
try {
|
||||
const url = `${baseUrl}/meta/${type}/${id}.json`;
|
||||
logger.log(`HTTP GET: ${url}`);
|
||||
const response = await this.retryRequest(async () => {
|
||||
return await axios.get(url, { timeout: 10000 });
|
||||
});
|
||||
|
||||
if (response.data && response.data.meta) {
|
||||
logger.log(`✅ Metadata fetched successfully from: ${url}`);
|
||||
return response.data.meta;
|
||||
}
|
||||
} catch (error) {
|
||||
logger.warn(`Failed to fetch meta from ${baseUrl}:`, error);
|
||||
logger.warn(`❌ Failed to fetch meta from ${baseUrl}:`, error);
|
||||
continue; // Try next URL
|
||||
}
|
||||
}
|
||||
|
||||
// If Cinemeta fails, try other addons
|
||||
const addons = this.getInstalledAddons();
|
||||
// If Cinemeta fails, try other addons (excluding the preferred one already tried)
|
||||
|
||||
for (const addon of addons) {
|
||||
if (!addon.resources || addon.id === 'com.linvo.cinemeta') continue;
|
||||
if (!addon.resources || addon.id === 'com.linvo.cinemeta' || addon.id === preferredAddonId) continue;
|
||||
|
||||
const metaResource = addon.resources.find(
|
||||
resource => resource.name === 'meta' && resource.types.includes(type)
|
||||
);
|
||||
// Check if addon supports meta resource for this type (handles both string and object formats)
|
||||
let hasMetaSupport = false;
|
||||
|
||||
if (!metaResource) continue;
|
||||
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 ResourceObject;
|
||||
if (typedResource.name === 'meta' &&
|
||||
Array.isArray(typedResource.types) &&
|
||||
typedResource.types.includes(type)) {
|
||||
hasMetaSupport = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
// Check if the element is the simple string "meta" AND the addon has a top-level types array
|
||||
else if (typeof resource === 'string' && resource === 'meta' && addon.types) {
|
||||
if (Array.isArray(addon.types) && addon.types.includes(type)) {
|
||||
hasMetaSupport = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!hasMetaSupport) continue;
|
||||
|
||||
try {
|
||||
const { baseUrl, queryParams } = this.getAddonBaseURL(addon.url || '');
|
||||
const url = queryParams ? `${baseUrl}/meta/${type}/${id}.json?${queryParams}` : `${baseUrl}/meta/${type}/${id}.json`;
|
||||
|
||||
logger.log(`HTTP GET: ${url}`);
|
||||
const response = await this.retryRequest(async () => {
|
||||
return await axios.get(url, { timeout: 10000 });
|
||||
});
|
||||
|
||||
if (response.data && response.data.meta) {
|
||||
logger.log(`✅ Metadata fetched successfully from: ${url}`);
|
||||
return response.data.meta;
|
||||
}
|
||||
} catch (error) {
|
||||
logger.warn(`Failed to fetch meta from ${addon.name}:`, error);
|
||||
logger.warn(`❌ Failed to fetch meta from ${addon.name} (${addon.id}):`, error);
|
||||
continue; // Try next addon
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -81,6 +81,7 @@ export interface StreamingContent {
|
|||
name: string;
|
||||
description?: string;
|
||||
poster?: string;
|
||||
posterShape?: string;
|
||||
banner?: string;
|
||||
logo?: string;
|
||||
year?: string | number;
|
||||
|
|
@ -88,12 +89,30 @@ export interface StreamingContent {
|
|||
imdbRating?: string;
|
||||
genres?: string[];
|
||||
director?: string;
|
||||
writer?: string;
|
||||
writer?: string[];
|
||||
cast?: string[];
|
||||
releaseInfo?: string;
|
||||
directors?: string[];
|
||||
creators?: string[];
|
||||
certification?: string;
|
||||
released?: string;
|
||||
trailerStreams?: any[];
|
||||
videos?: any[];
|
||||
inLibrary?: boolean;
|
||||
// Enhanced metadata from addons
|
||||
country?: string;
|
||||
links?: Array<{
|
||||
name: string;
|
||||
category: string;
|
||||
url: string;
|
||||
}>;
|
||||
behaviorHints?: {
|
||||
defaultVideoId?: string;
|
||||
hasScheduledVideos?: boolean;
|
||||
[key: string]: any;
|
||||
};
|
||||
imdb_id?: string;
|
||||
slug?: string;
|
||||
}
|
||||
|
||||
// Navigation types
|
||||
|
|
|
|||
1
src/types/navigation.d.ts
vendored
1
src/types/navigation.d.ts
vendored
|
|
@ -6,6 +6,7 @@ export type RootStackParamList = {
|
|||
Metadata: {
|
||||
id: string;
|
||||
type: string;
|
||||
addonId?: string;
|
||||
};
|
||||
Streams: {
|
||||
id: string;
|
||||
|
|
|
|||
Loading…
Reference in a new issue