diff --git a/src/components/metadata/MetadataDetails.tsx b/src/components/metadata/MetadataDetails.tsx index fdc4b43a..bc52276f 100644 --- a/src/components/metadata/MetadataDetails.tsx +++ b/src/components/metadata/MetadataDetails.tsx @@ -4,36 +4,72 @@ import { Text, StyleSheet, TouchableOpacity, + ActivityIndicator, } from 'react-native'; import { MaterialIcons } from '@expo/vector-icons'; import { Image } from 'expo-image'; import Animated, { - Layout, - Easing, FadeIn, } from 'react-native-reanimated'; import { useTheme } from '../../contexts/ThemeContext'; +import MetadataSourceSelector from './MetadataSourceSelector'; interface MetadataDetailsProps { metadata: any; imdbId: string | null; type: 'movie' | 'series'; renderRatings?: () => React.ReactNode; + contentId: string; + currentMetadataSource?: string; + onMetadataSourceChange?: (sourceId: string, sourceType: 'addon' | 'tmdb') => void; + loadingMetadata?: boolean; } const MetadataDetails: React.FC = ({ metadata, - imdbId, + imdbId: _imdbId, type, renderRatings, + contentId, + currentMetadataSource, + onMetadataSourceChange, + loadingMetadata = false, }) => { const { currentTheme } = useTheme(); const [isFullDescriptionOpen, setIsFullDescriptionOpen] = useState(false); return ( <> + {/* Metadata Source Selector */} + {onMetadataSourceChange && ( + + + {currentMetadataSource && currentMetadataSource !== 'auto' && ( + + Currently showing metadata from: {currentMetadataSource === 'tmdb' ? 'TMDB' : currentMetadataSource} + + )} + + )} + + {/* Loading indicator when switching sources */} + {loadingMetadata && ( + + + + Loading metadata... + + + )} + {/* Meta Info */} - + {metadata.year && ( {metadata.year} )} @@ -45,7 +81,7 @@ const MetadataDetails: React.FC = ({ )} {metadata.imdbRating && ( - = ({ {/* Creator/Director Info */} {metadata.directors && metadata.directors.length > 0 && ( @@ -79,11 +115,11 @@ const MetadataDetails: React.FC = ({ {/* Description */} {metadata.description && ( - - setIsFullDescriptionOpen(!isFullDescriptionOpen)} activeOpacity={0.7} > @@ -94,10 +130,10 @@ const MetadataDetails: React.FC = ({ {isFullDescriptionOpen ? 'Show Less' : 'Show More'} - @@ -108,6 +144,29 @@ const MetadataDetails: React.FC = ({ }; const styles = StyleSheet.create({ + sourceSelectorContainer: { + paddingHorizontal: 16, + marginBottom: 16, + }, + sourceIndicator: { + fontSize: 12, + marginTop: 4, + fontStyle: 'italic', + }, + loadingContainer: { + flexDirection: 'row', + alignItems: 'center', + justifyContent: 'center', + paddingVertical: 12, + marginBottom: 8, + }, + loadingText: { + marginLeft: 8, + fontSize: 14, + }, + dimmed: { + opacity: 0.6, + }, metaInfo: { flexDirection: 'row', alignItems: 'center', diff --git a/src/components/metadata/MetadataSourceSelector.tsx b/src/components/metadata/MetadataSourceSelector.tsx new file mode 100644 index 00000000..9af4824b --- /dev/null +++ b/src/components/metadata/MetadataSourceSelector.tsx @@ -0,0 +1,410 @@ +import React, { useState, useEffect, useCallback, useMemo } from 'react'; +import { + View, + Text, + StyleSheet, + TouchableOpacity, + Modal, + ScrollView, + ActivityIndicator, +} from 'react-native'; +import { MaterialIcons } from '@expo/vector-icons'; +import { useTheme } from '../../contexts/ThemeContext'; +import { stremioService } from '../../services/stremioService'; +import { tmdbService } from '../../services/tmdbService'; +import { catalogService } from '../../services/catalogService'; +import { logger } from '../../utils/logger'; + +interface MetadataSource { + id: string; + name: string; + type: 'addon' | 'tmdb'; + hasMetaSupport?: boolean; + icon?: string; +} + +interface MetadataSourceSelectorProps { + currentSource?: string; + contentId: string; + contentType: string; + onSourceChange: (sourceId: string, sourceType: 'addon' | 'tmdb') => void; + disabled?: boolean; +} + +const MetadataSourceSelector: React.FC = ({ + currentSource, + contentId, + contentType, + onSourceChange, + disabled = false, +}) => { + const { currentTheme } = useTheme(); + const [isVisible, setIsVisible] = useState(false); + const [sources, setSources] = useState([]); + const [loading, setLoading] = useState(false); + const [selectedSource, setSelectedSource] = useState(currentSource || 'auto'); + + // Load available metadata sources + const loadMetadataSources = useCallback(async () => { + setLoading(true); + try { + const sources: MetadataSource[] = []; + + // Add auto-select option + sources.push({ + id: 'auto', + name: 'Auto (Best Available)', + type: 'addon', + icon: 'auto-fix-high', + }); + + // Add TMDB as a source + sources.push({ + id: 'tmdb', + name: 'The Movie Database (TMDB)', + type: 'tmdb', + icon: 'movie', + }); + + // Get installed addons with meta support + const addons = await stremioService.getInstalledAddonsAsync(); + logger.log(`[MetadataSourceSelector] Checking ${addons.length} installed addons for meta support`); + + for (const addon of addons) { + logger.log(`[MetadataSourceSelector] Checking addon: ${addon.name} (${addon.id})`); + logger.log(`[MetadataSourceSelector] Addon resources:`, addon.resources); + + // Check if addon supports meta resource + let hasMetaSupport = false; + + if (addon.resources) { + // Handle both array of strings and array of objects + hasMetaSupport = addon.resources.some((resource: any) => { + if (typeof resource === 'string') { + // Simple string format like ["catalog", "meta"] + return resource === 'meta'; + } else if (resource && typeof resource === 'object') { + // Object format like { name: 'meta', types: ['movie', 'series'] } + const supportsType = !resource.types || resource.types.includes(contentType); + logger.log(`[MetadataSourceSelector] Resource ${resource.name}: types=${resource.types}, supportsType=${supportsType}`); + return resource.name === 'meta' && supportsType; + } + return false; + }); + } + + logger.log(`[MetadataSourceSelector] Addon ${addon.name} has meta support: ${hasMetaSupport}`); + + if (hasMetaSupport) { + sources.push({ + id: addon.id, + name: addon.name, + type: 'addon', + hasMetaSupport: true, + icon: 'extension', + }); + logger.log(`[MetadataSourceSelector] Added addon ${addon.name} as metadata source`); + } + } + + // Sort sources: auto first, then TMDB, then addons alphabetically + sources.sort((a, b) => { + if (a.id === 'auto') return -1; + if (b.id === 'auto') return 1; + if (a.id === 'tmdb') return -1; + if (b.id === 'tmdb') return 1; + return a.name.localeCompare(b.name); + }); + + // Always include at least auto and TMDB for comparison + if (sources.length < 2) { + logger.warn('[MetadataSourceSelector] Less than 2 sources found, this should not happen'); + } + + setSources(sources); + logger.log(`[MetadataSourceSelector] Found ${sources.length} metadata sources:`, sources.map(s => s.name)); + } catch (error) { + logger.error('[MetadataSourceSelector] Failed to load metadata sources:', error); + } finally { + setLoading(false); + } + }, [contentType]); + + // Load sources on mount + useEffect(() => { + loadMetadataSources(); + }, [loadMetadataSources]); + + // Update selected source when currentSource prop changes + useEffect(() => { + if (currentSource) { + setSelectedSource(currentSource); + } + }, [currentSource]); + + const handleSourceSelect = useCallback((source: MetadataSource) => { + setSelectedSource(source.id); + setIsVisible(false); + onSourceChange(source.id, source.type); + }, [onSourceChange]); + + const currentSourceName = useMemo(() => { + const source = sources.find(s => s.id === selectedSource); + return source?.name || 'Auto (Best Available)'; + }, [sources, selectedSource]); + + const getSourceIcon = useCallback((source: MetadataSource) => { + switch (source.icon) { + case 'auto-fix-high': + return 'auto-fix-high'; + case 'movie': + return 'movie'; + case 'extension': + return 'extension'; + default: + return 'info'; + } + }, []); + + // Always show if we have sources and onSourceChange callback + if (sources.length === 0 || !onSourceChange) { + console.log('[MetadataSourceSelector] Not showing selector:', { sourcesLength: sources.length, hasCallback: !!onSourceChange }); + return null; + } + + console.log('[MetadataSourceSelector] Showing selector with sources:', sources.map(s => s.name)); + + return ( + <> + + + Metadata Source + + !disabled && setIsVisible(true)} + disabled={disabled} + > + + s.id === selectedSource) || sources[0])} + size={20} + color={currentTheme.colors.text} + /> + + {currentSourceName} + + + + + + + setIsVisible(false)} + > + + + + + Select Metadata Source + + setIsVisible(false)} + style={styles.closeButton} + > + + + + + {loading ? ( + + + + Loading sources... + + + ) : ( + + {sources.map((source) => ( + handleSourceSelect(source)} + > + + + + + {source.name} + + {source.type === 'addon' && source.hasMetaSupport && ( + + Stremio Addon with metadata support + + )} + {source.type === 'tmdb' && ( + + Comprehensive movie and TV database + + )} + {source.id === 'auto' && ( + + Automatically selects the best available source + + )} + + + {selectedSource === source.id && ( + + )} + + ))} + + )} + + + + + ); +}; + +const styles = StyleSheet.create({ + container: { + marginBottom: 16, + }, + label: { + fontSize: 14, + fontWeight: '600', + marginBottom: 8, + }, + selector: { + flexDirection: 'row', + alignItems: 'center', + justifyContent: 'space-between', + paddingHorizontal: 12, + paddingVertical: 10, + borderRadius: 8, + borderWidth: 1, + }, + disabled: { + opacity: 0.5, + }, + selectorContent: { + flexDirection: 'row', + alignItems: 'center', + flex: 1, + }, + selectorText: { + fontSize: 14, + fontWeight: '500', + marginLeft: 8, + flex: 1, + }, + modalOverlay: { + flex: 1, + backgroundColor: 'rgba(0, 0, 0, 0.5)', + justifyContent: 'center', + alignItems: 'center', + padding: 20, + }, + modalContent: { + width: '100%', + maxWidth: 400, + maxHeight: '80%', + borderRadius: 12, + overflow: 'hidden', + }, + modalHeader: { + flexDirection: 'row', + alignItems: 'center', + justifyContent: 'space-between', + padding: 16, + borderBottomWidth: 1, + borderBottomColor: 'rgba(255, 255, 255, 0.1)', + }, + modalTitle: { + fontSize: 18, + fontWeight: '600', + }, + closeButton: { + padding: 4, + }, + loadingContainer: { + flexDirection: 'row', + alignItems: 'center', + justifyContent: 'center', + padding: 32, + }, + loadingText: { + marginLeft: 8, + fontSize: 14, + }, + sourcesList: { + maxHeight: 400, + }, + sourceItem: { + flexDirection: 'row', + alignItems: 'center', + justifyContent: 'space-between', + padding: 16, + borderBottomWidth: 1, + }, + sourceContent: { + flexDirection: 'row', + alignItems: 'center', + flex: 1, + }, + sourceInfo: { + marginLeft: 12, + flex: 1, + }, + sourceName: { + fontSize: 16, + fontWeight: '500', + marginBottom: 2, + }, + sourceDescription: { + fontSize: 12, + lineHeight: 16, + }, +}); + +export default MetadataSourceSelector; \ No newline at end of file diff --git a/src/screens/MetadataScreen.tsx b/src/screens/MetadataScreen.tsx index ca330e40..c60ad0ca 100644 --- a/src/screens/MetadataScreen.tsx +++ b/src/screens/MetadataScreen.tsx @@ -49,6 +49,8 @@ import { useMetadataAnimations } from '../hooks/useMetadataAnimations'; import { useMetadataAssets } from '../hooks/useMetadataAssets'; import { useWatchProgress } from '../hooks/useWatchProgress'; import { TraktService, TraktPlaybackItem } from '../services/traktService'; +import { tmdbService } from '../services/tmdbService'; +import { catalogService } from '../services/catalogService'; const { height } = Dimensions.get('window'); @@ -76,6 +78,8 @@ const MetadataScreen: React.FC = () => { const [selectedCastMember, setSelectedCastMember] = useState(null); const [shouldLoadSecondaryData, setShouldLoadSecondaryData] = useState(false); const [isScreenFocused, setIsScreenFocused] = useState(true); + const [currentMetadataSource, setCurrentMetadataSource] = useState(addonId || 'auto'); + const [loadingMetadataSource, setLoadingMetadataSource] = useState(false); const transitionOpacity = useSharedValue(1); const interactionComplete = useRef(false); @@ -463,6 +467,71 @@ const MetadataScreen: React.FC = () => { setShowCastModal(true); }, [isScreenFocused]); + const handleMetadataSourceChange = useCallback(async (sourceId: string, sourceType: 'addon' | 'tmdb') => { + if (!isScreenFocused) return; + + setCurrentMetadataSource(sourceId); + setLoadingMetadataSource(true); + + // Reload metadata with the new source + try { + let newMetadata = null; + + if (sourceType === 'tmdb') { + // Load from TMDB + if (id.startsWith('tt')) { + // Convert IMDB ID to TMDB ID first + const tmdbId = await tmdbService.findTMDBIdByIMDB(id); + if (tmdbId) { + if (type === 'movie') { + const movieDetails = await tmdbService.getMovieDetails(tmdbId.toString()); + if (movieDetails) { + newMetadata = { + id: id, + type: 'movie', + name: movieDetails.title, + poster: tmdbService.getImageUrl(movieDetails.poster_path) || '', + banner: tmdbService.getImageUrl(movieDetails.backdrop_path) || '', + description: movieDetails.overview || '', + year: movieDetails.release_date ? parseInt(movieDetails.release_date.substring(0, 4)) : undefined, + genres: movieDetails.genres?.map((g: { name: string }) => g.name) || [], + inLibrary: metadata?.inLibrary || false, + }; + } + } else if (type === 'series') { + const showDetails = await tmdbService.getTVShowDetails(tmdbId); + if (showDetails) { + newMetadata = { + id: id, + type: 'series', + name: showDetails.name, + poster: tmdbService.getImageUrl(showDetails.poster_path) || '', + banner: tmdbService.getImageUrl(showDetails.backdrop_path) || '', + description: showDetails.overview || '', + year: showDetails.first_air_date ? parseInt(showDetails.first_air_date.substring(0, 4)) : undefined, + genres: showDetails.genres?.map((g: { name: string }) => g.name) || [], + inLibrary: metadata?.inLibrary || false, + }; + } + } + } + } + } else { + // Load from addon or auto + const addonIdToUse = sourceId === 'auto' ? undefined : sourceId; + newMetadata = await catalogService.getEnhancedContentDetails(type, id, addonIdToUse); + } + + if (newMetadata) { + setMetadata(newMetadata); + } + } catch (error) { + console.error('[MetadataScreen] Failed to reload metadata with new source:', error); + } finally { + setLoadingMetadataSource(false); + } + }, [isScreenFocused, id, type, metadata, tmdbService, catalogService, setMetadata]); + // Ultra-optimized animated styles - minimal calculations with conditional updates const containerStyle = useAnimatedStyle(() => ({ opacity: isScreenFocused ? animations.screenOpacity.value : 0.8, @@ -589,6 +658,10 @@ const MetadataScreen: React.FC = () => { metadata={metadata} imdbId={imdbId} type={type as 'movie' | 'series'} + contentId={id} + currentMetadataSource={currentMetadataSource} + onMetadataSourceChange={handleMetadataSourceChange} + loadingMetadata={loadingMetadataSource} renderRatings={() => imdbId && shouldLoadSecondaryData ? ( ) : null}