From e0333527528291e548f34dae2407aa336da6224b Mon Sep 17 00:00:00 2001 From: tapframe Date: Sun, 2 Nov 2025 22:11:39 +0530 Subject: [PATCH] new tmdb ratings api --- ios/Nuvio.xcodeproj/project.pbxproj | 6 +- ios/Nuvio/Info.plist | 194 +++++++++++++------------ ios/Nuvio/NuvioRelease.entitlements | 9 +- src/screens/.ShowRatingsScreen.tsx.swp | Bin 0 -> 16384 bytes src/screens/ShowRatingsScreen.tsx | 73 +++++----- src/services/tmdbService.ts | 96 +++++++----- 6 files changed, 207 insertions(+), 171 deletions(-) create mode 100644 src/screens/.ShowRatingsScreen.tsx.swp diff --git a/ios/Nuvio.xcodeproj/project.pbxproj b/ios/Nuvio.xcodeproj/project.pbxproj index 678af2b..64b7e1c 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.nuviohub.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/ios/Nuvio/Info.plist b/ios/Nuvio/Info.plist index 3336aee..2e04327 100644 --- a/ios/Nuvio/Info.plist +++ b/ios/Nuvio/Info.plist @@ -1,99 +1,101 @@ - - CADisableMinimumFrameDurationOnPhone - - CFBundleDevelopmentRegion - $(DEVELOPMENT_LANGUAGE) - CFBundleDisplayName - Nuvio - CFBundleExecutable - $(EXECUTABLE_NAME) - CFBundleIdentifier - $(PRODUCT_BUNDLE_IDENTIFIER) - CFBundleInfoDictionaryVersion - 6.0 - CFBundleName - $(PRODUCT_NAME) - CFBundlePackageType - $(PRODUCT_BUNDLE_PACKAGE_TYPE) - CFBundleShortVersionString - 1.2.7 - CFBundleSignature - ???? - CFBundleURLTypes - - - CFBundleURLSchemes - - nuvio - com.nuvio.app - - - - CFBundleURLSchemes - - exp+nuvio - - - - CFBundleVersion - 22 - LSMinimumSystemVersion - 12.0 - LSRequiresIPhoneOS - - LSSupportsOpeningDocumentsInPlace - - NSAppTransportSecurity - - NSAllowsArbitraryLoads - - - NSBonjourServices - - _http._tcp - - NSLocalNetworkUsageDescription - Allow $(PRODUCT_NAME) to access your local network - RCTNewArchEnabled - - RCTRootViewBackgroundColor - 4278322180 - UIBackgroundModes - - audio - - UIFileSharingEnabled - - UILaunchStoryboardName - SplashScreen - UIRequiredDeviceCapabilities - - arm64 - - UIRequiresFullScreen - - UIStatusBarStyle - UIStatusBarStyleDefault - UISupportedInterfaceOrientations - - UIInterfaceOrientationPortrait - UIInterfaceOrientationPortraitUpsideDown - UIInterfaceOrientationLandscapeLeft - UIInterfaceOrientationLandscapeRight - - UISupportedInterfaceOrientations~ipad - - UIInterfaceOrientationPortrait - UIInterfaceOrientationPortraitUpsideDown - UIInterfaceOrientationLandscapeLeft - UIInterfaceOrientationLandscapeRight - - UIUserInterfaceStyle - Dark - UIViewControllerBasedStatusBarAppearance - - - + + CADisableMinimumFrameDurationOnPhone + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleDisplayName + Nuvio + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + $(PRODUCT_BUNDLE_PACKAGE_TYPE) + CFBundleShortVersionString + 1.2.7 + CFBundleSignature + ???? + CFBundleURLTypes + + + CFBundleURLSchemes + + nuvio + com.nuvio.app + + + + CFBundleURLSchemes + + exp+nuvio + + + + CFBundleVersion + 22 + LSMinimumSystemVersion + 12.0 + LSRequiresIPhoneOS + + LSSupportsOpeningDocumentsInPlace + + NSAppTransportSecurity + + NSAllowsArbitraryLoads + + + NSBonjourServices + + _http._tcp + + NSLocalNetworkUsageDescription + Allow $(PRODUCT_NAME) to access your local network + NSMicrophoneUsageDescription + This app does not require microphone access. + RCTNewArchEnabled + + RCTRootViewBackgroundColor + 4278322180 + UIBackgroundModes + + audio + + UIFileSharingEnabled + + UILaunchStoryboardName + SplashScreen + UIRequiredDeviceCapabilities + + arm64 + + UIRequiresFullScreen + + UIStatusBarStyle + UIStatusBarStyleDefault + UISupportedInterfaceOrientations + + UIInterfaceOrientationPortrait + UIInterfaceOrientationPortraitUpsideDown + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UISupportedInterfaceOrientations~ipad + + UIInterfaceOrientationPortrait + UIInterfaceOrientationPortraitUpsideDown + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UIUserInterfaceStyle + Dark + UIViewControllerBasedStatusBarAppearance + + + \ No newline at end of file diff --git a/ios/Nuvio/NuvioRelease.entitlements b/ios/Nuvio/NuvioRelease.entitlements index 0c67376..a0bc443 100644 --- a/ios/Nuvio/NuvioRelease.entitlements +++ b/ios/Nuvio/NuvioRelease.entitlements @@ -1,5 +1,10 @@ - - + + aps-environment + development + com.apple.developer.associated-domains + + + \ No newline at end of file diff --git a/src/screens/.ShowRatingsScreen.tsx.swp b/src/screens/.ShowRatingsScreen.tsx.swp new file mode 100644 index 0000000000000000000000000000000000000000..66357af5455c32c989b52965c0288105ba6160a9 GIT binary patch literal 16384 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 literal 0 HcmV?d00001 diff --git a/src/screens/ShowRatingsScreen.tsx b/src/screens/ShowRatingsScreen.tsx index e6fe5be..ac3e2de 100644 --- a/src/screens/ShowRatingsScreen.tsx +++ b/src/screens/ShowRatingsScreen.tsx @@ -28,7 +28,7 @@ if (Platform.OS === 'ios') { liquidGlassAvailable = false; } } -import { TMDBService, TMDBShow as Show, TMDBSeason, TMDBEpisode } from '../services/tmdbService'; +import { TMDBService, TMDBShow as Show, TMDBSeason, TMDBEpisode, IMDbRatings } from '../services/tmdbService'; import { RouteProp } from '@react-navigation/native'; import MaterialIcons from 'react-native-vector-icons/MaterialIcons'; import axios from 'axios'; @@ -78,17 +78,17 @@ const getRatingColor = (rating: number): string => { }; // Memoized components -const RatingCell = memo(({ episode, ratingSource, getTVMazeRating, isCurrentSeason, theme }: { +const RatingCell = memo(({ episode, ratingSource, getTVMazeRating, getIMDbRating, theme }: { episode: TMDBEpisode; ratingSource: RatingSource; getTVMazeRating: (seasonNumber: number, episodeNumber: number) => number | null; - isCurrentSeason: (episode: TMDBEpisode) => boolean; + getIMDbRating: (seasonNumber: number, episodeNumber: number) => number | null; theme: any; }) => { const getRatingForSource = useCallback((episode: TMDBEpisode): number | null => { switch (ratingSource) { case 'imdb': - return episode.imdb_rating || null; + return getIMDbRating(episode.season_number, episode.episode_number); case 'tmdb': return episode.vote_average || null; case 'tvmaze': @@ -96,23 +96,25 @@ const RatingCell = memo(({ episode, ratingSource, getTVMazeRating, isCurrentSeas default: return null; } - }, [ratingSource, getTVMazeRating]); + }, [ratingSource, getTVMazeRating, getIMDbRating]); const isRatingPotentiallyInaccurate = useCallback((episode: TMDBEpisode): boolean => { const rating = getRatingForSource(episode); if (!rating) return false; - if (ratingSource === 'tmdb' && episode.imdb_rating) { - const difference = Math.abs(rating - episode.imdb_rating); - return difference >= 2; + if (ratingSource === 'tmdb') { + const imdbRating = getIMDbRating(episode.season_number, episode.episode_number); + if (imdbRating) { + const difference = Math.abs(rating - imdbRating); + return difference >= 2; + } } return false; - }, [getRatingForSource, ratingSource]); + }, [getRatingForSource, ratingSource, getIMDbRating]); const rating = getRatingForSource(episode); const isInaccurate = isRatingPotentiallyInaccurate(episode); - const isCurrent = isCurrentSeason(episode); if (!rating) { if (!episode.air_date || new Date(episode.air_date) > new Date()) { @@ -135,16 +137,15 @@ const RatingCell = memo(({ episode, ratingSource, getTVMazeRating, isCurrentSeas styles.ratingCell, { backgroundColor: getRatingColor(rating), - opacity: isCurrent ? 0.7 : 1, } ]}> {rating.toFixed(1)} - {(isInaccurate || isCurrent) && ( + {isInaccurate && ( )} @@ -160,7 +161,7 @@ const RatingSourceToggle = memo(({ ratingSource, setRatingSource, theme }: { Rating Source: - {['imdb', 'tmdb', 'tvmaze'].map((source) => { + {['tmdb', 'imdb', 'tvmaze'].map((source) => { const isActive = ratingSource === source; return ( { const [show, setShow] = useState(null); const [seasons, setSeasons] = useState([]); const [tvmazeEpisodes, setTvmazeEpisodes] = useState([]); + const [imdbRatings, setImdbRatings] = useState([]); const [loading, setLoading] = useState(true); const [loadingSeasons, setLoadingSeasons] = useState(false); const [loadedSeasons, setLoadedSeasons] = useState([]); - const [ratingSource, setRatingSource] = useState('tmdb'); + const [ratingSource, setRatingSource] = useState('imdb'); const [visibleSeasonRange, setVisibleSeasonRange] = useState({ start: 0, end: 8 }); const [loadingProgress, setLoadingProgress] = useState(0); const ratingsCache = useRef<{[key: string]: number | null}>({}); @@ -316,6 +318,12 @@ const ShowRatingsScreen = ({ route }: Props) => { if (showData) { setShow(showData); + // Fetch IMDb ratings for all seasons + const imdbRatingsData = await tmdb.getIMDbRatings(showId); + if (imdbRatingsData) { + setImdbRatings(imdbRatingsData); + } + // Get external IDs to fetch TVMaze data const externalIds = await tmdb.getShowExternalIds(showId); if (externalIds?.imdb_id) { @@ -347,19 +355,22 @@ const ShowRatingsScreen = ({ route }: Props) => { return episode?.rating?.average || null; }, [tvmazeEpisodes]); - const isCurrentSeason = useCallback((episode: TMDBEpisode): boolean => { - if (!seasons.length || !episode.air_date) return false; + const getIMDbRating = useCallback((seasonNumber: number, episodeNumber: number): number | null => { + // Flatten all episodes from all seasons and find the matching one + for (const season of imdbRatings) { + if (!season.episodes) continue; + + const episode = season.episodes.find( + ep => ep.season_number === seasonNumber && ep.episode_number === episodeNumber + ); + + if (episode) { + return episode.vote_average || null; + } + } - const latestSeasonNumber = Math.max(...seasons.map(s => s.season_number)); - if (episode.season_number !== latestSeasonNumber) return false; - - const now = new Date(); - const airDate = new Date(episode.air_date); - const monthsDiff = (now.getFullYear() - airDate.getFullYear()) * 12 + - (now.getMonth() - airDate.getMonth()); - - return monthsDiff <= 6; - }, [seasons]); + return null; + }, [imdbRatings]); if (loading) { return ( @@ -459,10 +470,6 @@ const ShowRatingsScreen = ({ route }: Props) => { Rating differs significantly from IMDb - - - Current season (ratings may change) - @@ -535,7 +542,7 @@ const ShowRatingsScreen = ({ route }: Props) => { episode={season.episodes[episodeIndex]} ratingSource={ratingSource} getTVMazeRating={getTVMazeRating} - isCurrentSeason={isCurrentSeason} + getIMDbRating={getIMDbRating} theme={currentTheme} /> } diff --git a/src/services/tmdbService.ts b/src/services/tmdbService.ts index c0fc452..3940cb9 100644 --- a/src/services/tmdbService.ts +++ b/src/services/tmdbService.ts @@ -108,6 +108,21 @@ export interface TMDBCollectionPart { popularity: number; } +// Types for IMDb ratings API responses +export interface IMDbRatingEpisode { + vote_average: number; + episode_number: number; + name: string; + season_number: number; + tconst: string; +} + +export interface IMDbRatingSeason { + episodes: IMDbRatingEpisode[]; +} + +export type IMDbRatings = IMDbRatingSeason[]; + export class TMDBService { private static instance: TMDBService; private static ratingCache: Map = new Map(); @@ -351,6 +366,7 @@ export class TMDBService { /** * Get IMDb rating for an episode using OMDB API with caching + * @deprecated This method is deprecated. Use getIMDbRatings instead for better accuracy and performance. */ async getIMDbRating(showName: string, seasonNumber: number, episodeNumber: number): Promise { const cacheKey = this.generateRatingCacheKey(showName, seasonNumber, episodeNumber); @@ -387,7 +403,49 @@ export class TMDBService { } /** - * Get season details including all episodes with IMDb ratings + * Get IMDb ratings for all seasons and episodes + * This replaces the OMDB API approach and provides more accurate ratings + */ + async getIMDbRatings(tmdbId: number): Promise { + const IMDB_RATINGS_API_BASE_URL = process.env.EXPO_PUBLIC_IMDB_RATINGS_API_BASE_URL; + + if (!IMDB_RATINGS_API_BASE_URL) { + logger.error('[TMDB API] Missing EXPO_PUBLIC_IMDB_RATINGS_API_BASE_URL environment variable'); + return null; + } + + const cacheKey = this.generateCacheKey(`imdb_ratings_${tmdbId}`); + + // Check cache + const cached = this.getCachedData(cacheKey); + if (cached !== null) return cached; + + const apiUrl = `${IMDB_RATINGS_API_BASE_URL}/api/shows/${tmdbId}/season-ratings`; + + logger.log(`[TMDB API] 🌐 FETCHING: getIMDbRatings(${tmdbId})`); + try { + const response = await axios.get(apiUrl, { + headers: { + 'Content-Type': 'application/json', + }, + }); + + const data = response.data; + if (data && Array.isArray(data)) { + this.setCachedData(cacheKey, data); + return data; + } + + return null; + } catch (error) { + logger.error('[TMDB API] Error fetching IMDb ratings:', error); + return null; + } + } + + /** + * Get season details including all episodes + * Note: IMDb ratings are now fetched separately via getIMDbRatings() for better accuracy */ async getSeasonDetails(tmdbId: number, seasonNumber: number, showName?: string, language: string = 'en-US'): Promise { const cacheKey = this.generateCacheKey(`tv_${tmdbId}_season_${seasonNumber}`, { language, showName }); @@ -405,42 +463,6 @@ export class TMDBService { }); const season = response.data; - - // If show name is provided, fetch IMDb ratings for each episode in batches - if (showName) { - // Process episodes in batches of 5 to avoid rate limiting - const batchSize = 5; - const episodes = [...season.episodes]; - const episodesWithRatings = []; - - for (let i = 0; i < episodes.length; i += batchSize) { - const batch = episodes.slice(i, i + batchSize); - const batchPromises = batch.map(async (episode: TMDBEpisode) => { - const imdbRating = await this.getIMDbRating( - showName, - episode.season_number, - episode.episode_number - ); - - return { - ...episode, - imdb_rating: imdbRating - }; - }); - - const batchResults = await Promise.all(batchPromises); - episodesWithRatings.push(...batchResults); - } - - const result = { - ...season, - episodes: episodesWithRatings, - }; - - this.setCachedData(cacheKey, result); - return result; - } - this.setCachedData(cacheKey, season); return season; } catch (error) {