diff --git a/ios/Nuvio.xcodeproj/project.pbxproj b/ios/Nuvio.xcodeproj/project.pbxproj
index 678af2ba..64b7e1c7 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 3336aee6..2e043270 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 0c67376e..a0bc443f 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 00000000..66357af5
Binary files /dev/null and b/src/screens/.ShowRatingsScreen.tsx.swp differ
diff --git a/src/screens/ShowRatingsScreen.tsx b/src/screens/ShowRatingsScreen.tsx
index e6fe5be6..ac3e2de2 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 c0fc4529..3940cb9e 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) {