diff --git a/src/components/metadata/SeriesContent.tsx b/src/components/metadata/SeriesContent.tsx index 555d7f9..9d20443 100644 --- a/src/components/metadata/SeriesContent.tsx +++ b/src/components/metadata/SeriesContent.tsx @@ -936,12 +936,12 @@ const styles = StyleSheet.create({ // Vertical Layout Styles episodeListContentVertical: { - paddingBottom: 20, + paddingBottom: 8, paddingHorizontal: 16, }, episodeListContentVerticalTablet: { paddingHorizontal: 16, - paddingBottom: 20, + paddingBottom: 8, }, episodeGridVertical: { flexDirection: 'row', diff --git a/src/hooks/useMetadata.ts b/src/hooks/useMetadata.ts index de7c5b1..16f84b7 100644 --- a/src/hooks/useMetadata.ts +++ b/src/hooks/useMetadata.ts @@ -1692,6 +1692,7 @@ export const useMetadata = ({ id, type, addonId }: UseMetadataProps): UseMetadat if (__DEV__) console.log('[useMetadata] fetched certification via TMDB id (extract path)', { type, fetchedTmdbId, certification }); setMetadata(prev => prev ? { ...prev, + tmdbId: fetchedTmdbId, certification } : null); } else { @@ -1755,7 +1756,7 @@ export const useMetadata = ({ id, type, addonId }: UseMetadataProps): UseMetadat const cert = await tmdbSvc.getCertification(type, tmdbId); if (cert) { if (__DEV__) console.log('[useMetadata] fetched certification (attach path)', { type, tmdbId, cert }); - setMetadata(prev => prev ? { ...prev, certification: cert } : prev); + setMetadata(prev => prev ? { ...prev, tmdbId, certification: cert } : prev); } else { if (__DEV__) console.warn('[useMetadata] TMDB returned no certification (attach path)', { type, tmdbId }); } @@ -1814,6 +1815,7 @@ export const useMetadata = ({ id, type, addonId }: UseMetadataProps): UseMetadat // Update metadata with TV details setMetadata((prev: any) => ({ ...prev, + tmdbId, tvDetails })); } @@ -1849,6 +1851,7 @@ export const useMetadata = ({ id, type, addonId }: UseMetadataProps): UseMetadat // Update metadata with movie details setMetadata((prev: any) => ({ ...prev, + tmdbId, movieDetails: movieDetailsObj })); } diff --git a/src/hooks/useMetadataAssets.ts b/src/hooks/useMetadataAssets.ts index 052632c..f938f67 100644 --- a/src/hooks/useMetadataAssets.ts +++ b/src/hooks/useMetadataAssets.ts @@ -103,6 +103,17 @@ export const useMetadataAssets = ( // Optimized logo fetching useEffect(() => { const logoPreference = settings.logoSourcePreference || 'tmdb'; + + if (__DEV__) { + console.log('[useMetadataAssets] Logo fetch triggered:', { + id, + type, + logoPreference, + hasImdbId: !!imdbId, + tmdbEnrichmentEnabled: settings.enrichMetadataWithTMDB, + logoFetchInProgress: logoFetchInProgress.current + }); + } const currentLogoUrl = metadata?.logo; let shouldFetchLogo = false; @@ -141,12 +152,12 @@ export const useMetadataAssets = ( try { const preferredLanguage = settings.tmdbLanguagePreference || 'en'; - + if (logoPreference === 'tmdb') { // TMDB path - optimized flow let tmdbId: string | null = null; let contentType = type === 'series' ? 'tv' : 'movie'; - + // Extract or find TMDB ID in one step if (id.startsWith('tmdb:')) { tmdbId = id.split(':')[1]; @@ -157,9 +168,11 @@ export const useMetadataAssets = ( if (foundId) { tmdbId = String(foundId); setFoundTmdbId(tmdbId); // Save for banner fetching + } else if (__DEV__) { + console.log('[useMetadataAssets] Could not find TMDB ID for IMDB:', imdbId); } } catch (error) { - // Handle error silently + if (__DEV__) console.error('[useMetadataAssets] Error finding TMDB ID:', error); } } else { const parsedId = parseInt(id, 10); @@ -173,15 +186,39 @@ export const useMetadataAssets = ( // Direct fetch - avoid multiple service calls const tmdbService = TMDBService.getInstance(); const logoUrl = await tmdbService.getContentLogo(contentType as 'tv' | 'movie', tmdbId, preferredLanguage); - + + if (__DEV__) { + console.log('[useMetadataAssets] Logo fetch result:', { + contentType, + tmdbId, + preferredLanguage, + logoUrl, + logoPreference + }); + } + if (logoUrl) { // Preload the image FastImage.preload([{ uri: logoUrl }]); - + setMetadata((prevMetadata: any) => ({ ...prevMetadata!, logo: logoUrl })); + } else { + // TMDB logo not found, try to restore addon logo if it exists + if (currentLogoUrl && !isTmdbUrl(currentLogoUrl)) { + if (__DEV__) console.log('[useMetadataAssets] Restoring addon logo after TMDB logo not found'); + setMetadata((prevMetadata: any) => ({ ...prevMetadata!, logo: currentLogoUrl })); + } else if (__DEV__) { + console.log('[useMetadataAssets] No logo found for TMDB ID:', tmdbId); + } } } catch (error) { - // Handle error silently + // TMDB logo fetch failed, try to restore addon logo if it exists + if (currentLogoUrl && !isTmdbUrl(currentLogoUrl)) { + if (__DEV__) console.log('[useMetadataAssets] Restoring addon logo after TMDB fetch error'); + setMetadata((prevMetadata: any) => ({ ...prevMetadata!, logo: currentLogoUrl })); + } else if (__DEV__) { + console.error('[useMetadataAssets] Logo fetch error:', error); + } } } } diff --git a/src/navigation/AppNavigator.tsx b/src/navigation/AppNavigator.tsx index 6b0f235..ca05e52 100644 --- a/src/navigation/AppNavigator.tsx +++ b/src/navigation/AppNavigator.tsx @@ -51,6 +51,7 @@ import CastMoviesScreen from '../screens/CastMoviesScreen'; import UpdateScreen from '../screens/UpdateScreen'; import AISettingsScreen from '../screens/AISettingsScreen'; import AIChatScreen from '../screens/AIChatScreen'; +import BackdropGalleryScreen from '../screens/BackdropGalleryScreen'; import BackupScreen from '../screens/BackupScreen'; // Stack navigator types @@ -153,6 +154,11 @@ export type RootStackParamList = { episodeNumber?: number; title: string; }; + BackdropGallery: { + tmdbId: number; + type: 'movie' | 'tv'; + title: string; + }; }; export type RootStackNavigationProp = NativeStackNavigationProp; @@ -1328,8 +1334,8 @@ const InnerNavigator = ({ initialRouteName }: { initialRouteName?: keyof RootSta }, }} /> - + diff --git a/src/screens/BackdropGalleryScreen.tsx b/src/screens/BackdropGalleryScreen.tsx new file mode 100644 index 0000000..b80e2a4 --- /dev/null +++ b/src/screens/BackdropGalleryScreen.tsx @@ -0,0 +1,254 @@ +import React, { useState, useEffect } from 'react'; +import { + View, + Text, + StyleSheet, + FlatList, + TouchableOpacity, + Dimensions, + ActivityIndicator, + StatusBar, +} from 'react-native'; +import { useRoute, useNavigation } from '@react-navigation/native'; +import { SafeAreaView } from 'react-native-safe-area-context'; +import FastImage from '@d11/react-native-fast-image'; +import { MaterialIcons } from '@expo/vector-icons'; +import { TMDBService } from '../services/tmdbService'; + +const { width } = Dimensions.get('window'); +const BACKDROP_WIDTH = width * 0.9; +const BACKDROP_HEIGHT = (BACKDROP_WIDTH * 9) / 16; // 16:9 aspect ratio + +interface BackdropItem { + file_path: string; + width: number; + height: number; + aspect_ratio: number; +} + +interface RouteParams { + tmdbId: number; + type: 'movie' | 'tv'; + title: string; +} + +const BackdropGalleryScreen: React.FC = () => { + const route = useRoute(); + const navigation = useNavigation(); + const { tmdbId, type, title } = route.params as RouteParams; + + const [backdrops, setBackdrops] = useState([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + + useEffect(() => { + const fetchBackdrops = async () => { + try { + setLoading(true); + const tmdbService = TMDBService.getInstance(); + + let images; + if (type === 'movie') { + images = await tmdbService.getMovieImagesFull(tmdbId); + } else { + images = await tmdbService.getTvShowImagesFull(tmdbId); + } + + if (__DEV__) { + console.log('[BackdropGallery] TMDB response:', { + tmdbId, + type, + hasImages: !!images, + backdropsCount: images?.backdrops?.length || 0, + images + }); + } + + if (images && images.backdrops && images.backdrops.length > 0) { + setBackdrops(images.backdrops); + } else { + setError('No backdrops found'); + } + } catch (err) { + setError('Failed to load backdrops'); + console.error('Backdrop fetch error:', err); + } finally { + setLoading(false); + } + }; + + if (tmdbId) { + fetchBackdrops(); + } + }, [tmdbId, type]); + + const renderBackdrop = ({ item, index }: { item: BackdropItem; index: number }) => { + const imageUrl = `https://image.tmdb.org/t/p/w1280${item.file_path}`; + + return ( + + + + + {item.width} × {item.height} + + + {item.aspect_ratio.toFixed(2)}:1 + + + + ); + }; + + const renderHeader = () => ( + + navigation.goBack()} + > + + + + + {title} + + + {backdrops.length} Backdrop{backdrops.length !== 1 ? 's' : ''} + + + + ); + + if (loading) { + return ( + + + {renderHeader()} + + + Loading backdrops... + + + ); + } + + if (error || backdrops.length === 0) { + return ( + + + {renderHeader()} + + + + {error || 'No backdrops available'} + + + + ); + } + + return ( + + + {renderHeader()} + `${item.file_path}-${index}`} + renderItem={renderBackdrop} + contentContainerStyle={styles.listContainer} + showsVerticalScrollIndicator={false} + /> + + ); +}; + +const styles = StyleSheet.create({ + container: { + flex: 1, + backgroundColor: '#000', + }, + header: { + flexDirection: 'row', + alignItems: 'center', + paddingHorizontal: 16, + paddingVertical: 12, + borderBottomWidth: 1, + borderBottomColor: 'rgba(255,255,255,0.1)', + }, + backButton: { + padding: 8, + marginRight: 12, + }, + titleContainer: { + flex: 1, + }, + title: { + fontSize: 18, + fontWeight: '700', + color: '#fff', + marginBottom: 2, + }, + subtitle: { + fontSize: 14, + color: 'rgba(255,255,255,0.7)', + }, + listContainer: { + padding: 16, + }, + backdropContainer: { + marginBottom: 20, + borderRadius: 12, + overflow: 'hidden', + backgroundColor: 'rgba(255,255,255,0.05)', + }, + backdropImage: { + width: BACKDROP_WIDTH, + height: BACKDROP_HEIGHT, + alignSelf: 'center', + }, + backdropInfo: { + flexDirection: 'row', + justifyContent: 'space-between', + alignItems: 'center', + padding: 12, + backgroundColor: 'rgba(0,0,0,0.7)', + }, + backdropResolution: { + fontSize: 12, + color: '#fff', + opacity: 0.8, + }, + backdropAspect: { + fontSize: 12, + color: '#fff', + opacity: 0.8, + }, + loadingContainer: { + flex: 1, + justifyContent: 'center', + alignItems: 'center', + }, + loadingText: { + marginTop: 12, + fontSize: 16, + color: 'rgba(255,255,255,0.7)', + }, + errorContainer: { + flex: 1, + justifyContent: 'center', + alignItems: 'center', + padding: 32, + }, + errorText: { + marginTop: 16, + fontSize: 16, + color: 'rgba(255,255,255,0.7)', + textAlign: 'center', + }, +}); + +export default BackdropGalleryScreen; diff --git a/src/screens/MetadataScreen.tsx b/src/screens/MetadataScreen.tsx index ab0fafe..dc587d2 100644 --- a/src/screens/MetadataScreen.tsx +++ b/src/screens/MetadataScreen.tsx @@ -995,6 +995,24 @@ const MetadataScreen: React.FC = () => { )} + {/* Backdrop Gallery section - shown after details for movies/TV when TMDB ID is available */} + {shouldLoadSecondaryData && metadata?.tmdbId && ( + + navigation.navigate('BackdropGallery' as any, { + tmdbId: metadata.tmdbId, + type: Object.keys(groupedEpisodes).length > 0 ? 'tv' : 'movie', + title: metadata.name || 'Gallery' + })} + > + + Backdrop Gallery + + + + )} + {/* Recommendations Section with skeleton when loading - Lazy loaded */} {type === 'movie' && shouldLoadSecondaryData && ( { + try { + const response = await axios.get(`${BASE_URL}/movie/${movieId}/images`, { + headers: await this.getHeaders(), + params: await this.getParams({ + include_image_language: `en,null` + }), + }); + + return response.data; + } catch (error) { + return null; + } + } + + /** + * Get movie images (logos only) by TMDB ID - legacy method */ async getMovieImages(movieId: number | string, preferredLanguage: string = 'en'): Promise { try { @@ -697,7 +715,25 @@ export class TMDBService { } /** - * Get TV show images (logos, posters, backdrops) by TMDB ID + * Get TV show images (logos, posters, backdrops) by TMDB ID - returns full images object + */ + async getTvShowImagesFull(showId: number | string): Promise { + try { + const response = await axios.get(`${BASE_URL}/tv/${showId}/images`, { + headers: await this.getHeaders(), + params: await this.getParams({ + include_image_language: `en,null` + }), + }); + + return response.data; + } catch (error) { + return null; + } + } + + /** + * Get TV show images (logos only) by TMDB ID - legacy method */ async getTvShowImages(showId: number | string, preferredLanguage: string = 'en'): Promise { try {