diff --git a/ios/Nuvio.xcodeproj/project.pbxproj b/ios/Nuvio.xcodeproj/project.pbxproj index 64b7e1c7..3acee97e 100644 --- a/ios/Nuvio.xcodeproj/project.pbxproj +++ b/ios/Nuvio.xcodeproj/project.pbxproj @@ -459,7 +459,7 @@ ); OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_DEBUG"; PRODUCT_BUNDLE_IDENTIFIER = com.nuvio.app; - PRODUCT_NAME = "Nuvio"; + PRODUCT_NAME = Nuvio; SWIFT_OBJC_BRIDGING_HEADER = "Nuvio/Nuvio-Bridging-Header.h"; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; SWIFT_VERSION = 5.0; @@ -490,8 +490,8 @@ "-lc++", ); OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_RELEASE"; - PRODUCT_BUNDLE_IDENTIFIER = "com.nuvio.app"; - PRODUCT_NAME = "Nuvio"; + PRODUCT_BUNDLE_IDENTIFIER = com.nuvio.app; + PRODUCT_NAME = Nuvio; SWIFT_OBJC_BRIDGING_HEADER = "Nuvio/Nuvio-Bridging-Header.h"; SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2"; diff --git a/src/components/metadata/SeriesContent.tsx b/src/components/metadata/SeriesContent.tsx index 6741e5e0..4b5b061a 100644 --- a/src/components/metadata/SeriesContent.tsx +++ b/src/components/metadata/SeriesContent.tsx @@ -338,6 +338,8 @@ export const SeriesContent: React.FC = ({ const hydrateFromTmdb = async () => { try { if (!metadata?.id || !selectedSeason) return; + // Respect settings: skip TMDB enrichment when disabled + if (!settings?.enrichMetadataWithTMDB) return; const currentSeasonEpisodes = groupedEpisodes[selectedSeason] || []; if (currentSeasonEpisodes.length === 0) return; @@ -375,7 +377,7 @@ export const SeriesContent: React.FC = ({ }; hydrateFromTmdb(); - }, [metadata?.id, selectedSeason, groupedEpisodes]); + }, [metadata?.id, selectedSeason, groupedEpisodes, settings?.enrichMetadataWithTMDB]); // Enable item animations shortly after mount to avoid initial overlap/glitch useEffect(() => { @@ -538,7 +540,7 @@ export const SeriesContent: React.FC = ({ // Get season poster URL (needed for both views) let seasonPoster = DEFAULT_PLACEHOLDER; if (seasonEpisodes[0]?.season_poster_path) { - const tmdbUrl = tmdbService.getImageUrl(seasonEpisodes[0].season_poster_path, 'w500'); + const tmdbUrl = tmdbService.getImageUrl(seasonEpisodes[0].season_poster_path, 'original'); if (tmdbUrl) seasonPoster = tmdbUrl; } else if (metadata?.poster) { seasonPoster = metadata.poster; @@ -653,18 +655,32 @@ export const SeriesContent: React.FC = ({ // Vertical layout episode card (traditional) const renderVerticalEpisodeCard = (episode: Episode) => { - let episodeImage = EPISODE_PLACEHOLDER; - if (episode.still_path) { - // Check if still_path is already a full URL - if (episode.still_path.startsWith('http')) { - episodeImage = episode.still_path; - } else { - const tmdbUrl = tmdbService.getImageUrl(episode.still_path, 'w500'); - if (tmdbUrl) episodeImage = tmdbUrl; + // Resolve episode image with addon-first logic + const resolveEpisodeImage = (): string => { + const candidates: Array = [ + // Add-on common fields + (episode as any).thumbnail, + (episode as any).image, + (episode as any).thumb, + (episode as any)?.images?.still, + episode.still_path, + ]; + + for (const cand of candidates) { + if (!cand) continue; + if (typeof cand === 'string' && (cand.startsWith('http://') || cand.startsWith('https://'))) { + return cand; + } + // TMDB relative paths only when enrichment is enabled + if (typeof cand === 'string' && cand.startsWith('/') && settings?.enrichMetadataWithTMDB) { + const tmdbUrl = tmdbService.getImageUrl(cand, 'original'); + if (tmdbUrl) return tmdbUrl; + } } - } else if (metadata?.poster) { - episodeImage = metadata.poster; - } + return metadata?.poster || EPISODE_PLACEHOLDER; + }; + + let episodeImage = resolveEpisodeImage(); const episodeNumber = typeof episode.episode_number === 'number' ? episode.episode_number.toString() : ''; const seasonNumber = typeof episode.season_number === 'number' ? episode.season_number.toString() : ''; @@ -695,7 +711,7 @@ export const SeriesContent: React.FC = ({ const effectiveVote = (tmdbOverride?.vote_average ?? episode.vote_average) || 0; const effectiveRuntime = tmdbOverride?.runtime ?? (episode as any).runtime; if (!episode.still_path && tmdbOverride?.still_path) { - const tmdbUrl = tmdbService.getImageUrl(tmdbOverride.still_path, 'w500'); + const tmdbUrl = tmdbService.getImageUrl(tmdbOverride.still_path, 'original'); if (tmdbUrl) episodeImage = tmdbUrl; } const progress = episodeProgress[episodeId]; @@ -869,7 +885,7 @@ export const SeriesContent: React.FC = ({ )} - = ({ lineHeight: isTV ? 22 : isLargeTablet ? 20 : isTablet ? 20 : 18 } ]} numberOfLines={isLargeScreen ? 4 : isTablet ? 3 : 2}> - {episode.overview || 'No description available'} + {(episode.overview || (episode as any).description || (episode as any).plot || (episode as any).synopsis || 'No description available')} @@ -886,18 +902,29 @@ export const SeriesContent: React.FC = ({ // Horizontal layout episode card (Netflix-style) const renderHorizontalEpisodeCard = (episode: Episode) => { - let episodeImage = EPISODE_PLACEHOLDER; - if (episode.still_path) { - // Check if still_path is already a full URL - if (episode.still_path.startsWith('http')) { - episodeImage = episode.still_path; - } else { - const tmdbUrl = tmdbService.getImageUrl(episode.still_path, 'w500'); - if (tmdbUrl) episodeImage = tmdbUrl; + const resolveEpisodeImage = (): string => { + const candidates: Array = [ + (episode as any).thumbnail, + (episode as any).image, + (episode as any).thumb, + (episode as any)?.images?.still, + episode.still_path, + ]; + + for (const cand of candidates) { + if (!cand) continue; + if (typeof cand === 'string' && (cand.startsWith('http://') || cand.startsWith('https://'))) { + return cand; + } + if (typeof cand === 'string' && cand.startsWith('/') && settings?.enrichMetadataWithTMDB) { + const tmdbUrl = tmdbService.getImageUrl(cand, 'original'); + if (tmdbUrl) return tmdbUrl; + } } - } else if (metadata?.poster) { - episodeImage = metadata.poster; - } + return metadata?.poster || EPISODE_PLACEHOLDER; + }; + + let episodeImage = resolveEpisodeImage(); const episodeNumber = typeof episode.episode_number === 'number' ? episode.episode_number.toString() : ''; const seasonNumber = typeof episode.season_number === 'number' ? episode.season_number.toString() : ''; @@ -1006,7 +1033,7 @@ export const SeriesContent: React.FC = ({ {/* Episode Description */} - = ({ opacity: isTV ? 0.95 : isLargeTablet ? 0.9 : isTablet ? 0.9 : 0.9 } ]} numberOfLines={isLargeScreen ? 4 : 3}> - {episode.overview || 'No description available'} + {(episode.overview || (episode as any).description || (episode as any).plot || (episode as any).synopsis || 'No description available')} {/* Metadata Row */} diff --git a/src/hooks/useMetadata.ts b/src/hooks/useMetadata.ts index 4f38ef7a..5934ceb6 100644 --- a/src/hooks/useMetadata.ts +++ b/src/hooks/useMetadata.ts @@ -1068,15 +1068,31 @@ export const useMetadata = ({ id, type, addonId }: UseMetadataProps): UseMetadat groupedAddonEpisodes[seasonNumber] = []; } + // Resolve image and description dynamically from arbitrary addons + const imageCandidate = ( + video.thumbnail || + video.image || + video.thumb || + (video.images && video.images.still) || + null + ); + const descriptionCandidate = ( + video.overview || + video.description || + video.plot || + video.synopsis || + '' + ); + // 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 || '', + overview: descriptionCandidate, 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, + still_path: imageCandidate, vote_average: parseFloat(video.rating) || 0, runtime: undefined, episodeString: `S${seasonNumber.toString().padStart(2, '0')}E${episodeNumber.toString().padStart(2, '0')}`, diff --git a/src/screens/MetadataScreen.tsx b/src/screens/MetadataScreen.tsx index 675a5f3c..8046ee05 100644 --- a/src/screens/MetadataScreen.tsx +++ b/src/screens/MetadataScreen.tsx @@ -842,7 +842,7 @@ const MetadataScreen: React.FC = () => { return ( @@ -913,7 +913,7 @@ const MetadataScreen: React.FC = () => {