new tmdb ratings api

This commit is contained in:
tapframe 2025-11-02 22:11:39 +05:30
parent 96f79f7c72
commit e033352752
6 changed files with 207 additions and 171 deletions

View file

@ -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";

View file

@ -1,99 +1,101 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CADisableMinimumFrameDurationOnPhone</key>
<true/>
<key>CFBundleDevelopmentRegion</key>
<string>$(DEVELOPMENT_LANGUAGE)</string>
<key>CFBundleDisplayName</key>
<string>Nuvio</string>
<key>CFBundleExecutable</key>
<string>$(EXECUTABLE_NAME)</string>
<key>CFBundleIdentifier</key>
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>$(PRODUCT_NAME)</string>
<key>CFBundlePackageType</key>
<string>$(PRODUCT_BUNDLE_PACKAGE_TYPE)</string>
<key>CFBundleShortVersionString</key>
<string>1.2.7</string>
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleURLTypes</key>
<array>
<dict>
<key>CFBundleURLSchemes</key>
<array>
<string>nuvio</string>
<string>com.nuvio.app</string>
</array>
</dict>
<dict>
<key>CFBundleURLSchemes</key>
<array>
<string>exp+nuvio</string>
</array>
</dict>
</array>
<key>CFBundleVersion</key>
<string>22</string>
<key>LSMinimumSystemVersion</key>
<string>12.0</string>
<key>LSRequiresIPhoneOS</key>
<true/>
<key>LSSupportsOpeningDocumentsInPlace</key>
<true/>
<key>NSAppTransportSecurity</key>
<dict>
<key>NSAllowsArbitraryLoads</key>
<true/>
</dict>
<key>NSBonjourServices</key>
<array>
<string>_http._tcp</string>
</array>
<key>NSLocalNetworkUsageDescription</key>
<string>Allow $(PRODUCT_NAME) to access your local network</string>
<key>RCTNewArchEnabled</key>
<true/>
<key>RCTRootViewBackgroundColor</key>
<integer>4278322180</integer>
<key>UIBackgroundModes</key>
<array>
<string>audio</string>
</array>
<key>UIFileSharingEnabled</key>
<true/>
<key>UILaunchStoryboardName</key>
<string>SplashScreen</string>
<key>UIRequiredDeviceCapabilities</key>
<array>
<string>arm64</string>
</array>
<key>UIRequiresFullScreen</key>
<true/>
<key>UIStatusBarStyle</key>
<string>UIStatusBarStyleDefault</string>
<key>UISupportedInterfaceOrientations</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationPortraitUpsideDown</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
<key>UISupportedInterfaceOrientations~ipad</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationPortraitUpsideDown</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
<key>UIUserInterfaceStyle</key>
<string>Dark</string>
<key>UIViewControllerBasedStatusBarAppearance</key>
<false/>
</dict>
</plist>
<dict>
<key>CADisableMinimumFrameDurationOnPhone</key>
<true/>
<key>CFBundleDevelopmentRegion</key>
<string>$(DEVELOPMENT_LANGUAGE)</string>
<key>CFBundleDisplayName</key>
<string>Nuvio</string>
<key>CFBundleExecutable</key>
<string>$(EXECUTABLE_NAME)</string>
<key>CFBundleIdentifier</key>
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>$(PRODUCT_NAME)</string>
<key>CFBundlePackageType</key>
<string>$(PRODUCT_BUNDLE_PACKAGE_TYPE)</string>
<key>CFBundleShortVersionString</key>
<string>1.2.7</string>
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleURLTypes</key>
<array>
<dict>
<key>CFBundleURLSchemes</key>
<array>
<string>nuvio</string>
<string>com.nuvio.app</string>
</array>
</dict>
<dict>
<key>CFBundleURLSchemes</key>
<array>
<string>exp+nuvio</string>
</array>
</dict>
</array>
<key>CFBundleVersion</key>
<string>22</string>
<key>LSMinimumSystemVersion</key>
<string>12.0</string>
<key>LSRequiresIPhoneOS</key>
<true/>
<key>LSSupportsOpeningDocumentsInPlace</key>
<true/>
<key>NSAppTransportSecurity</key>
<dict>
<key>NSAllowsArbitraryLoads</key>
<true/>
</dict>
<key>NSBonjourServices</key>
<array>
<string>_http._tcp</string>
</array>
<key>NSLocalNetworkUsageDescription</key>
<string>Allow $(PRODUCT_NAME) to access your local network</string>
<key>NSMicrophoneUsageDescription</key>
<string>This app does not require microphone access.</string>
<key>RCTNewArchEnabled</key>
<true/>
<key>RCTRootViewBackgroundColor</key>
<integer>4278322180</integer>
<key>UIBackgroundModes</key>
<array>
<string>audio</string>
</array>
<key>UIFileSharingEnabled</key>
<true/>
<key>UILaunchStoryboardName</key>
<string>SplashScreen</string>
<key>UIRequiredDeviceCapabilities</key>
<array>
<string>arm64</string>
</array>
<key>UIRequiresFullScreen</key>
<true/>
<key>UIStatusBarStyle</key>
<string>UIStatusBarStyleDefault</string>
<key>UISupportedInterfaceOrientations</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationPortraitUpsideDown</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
<key>UISupportedInterfaceOrientations~ipad</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationPortraitUpsideDown</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
<key>UIUserInterfaceStyle</key>
<string>Dark</string>
<key>UIViewControllerBasedStatusBarAppearance</key>
<false/>
</dict>
</plist>

View file

@ -1,5 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict/>
</plist>
<dict>
<key>aps-environment</key>
<string>development</string>
<key>com.apple.developer.associated-domains</key>
<array/>
</dict>
</plist>

Binary file not shown.

View file

@ -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,
}
]}>
<Text style={styles.ratingText}>{rating.toFixed(1)}</Text>
</Animated.View>
{(isInaccurate || isCurrent) && (
{isInaccurate && (
<MaterialIcons
name={isCurrent ? "schedule" : "warning"}
name="warning"
size={12}
color={isCurrent ? theme.colors.primary : theme.colors.warning}
color={theme.colors.warning}
style={styles.warningIcon}
/>
)}
@ -160,7 +161,7 @@ const RatingSourceToggle = memo(({ ratingSource, setRatingSource, theme }: {
<View style={styles.ratingSourceContainer}>
<Text style={[styles.sectionTitle, { color: theme.colors.white }]}>Rating Source:</Text>
<View style={styles.ratingSourceButtons}>
{['imdb', 'tmdb', 'tvmaze'].map((source) => {
{['tmdb', 'imdb', 'tvmaze'].map((source) => {
const isActive = ratingSource === source;
return (
<TouchableOpacity
@ -217,10 +218,11 @@ const ShowRatingsScreen = ({ route }: Props) => {
const [show, setShow] = useState<Show | null>(null);
const [seasons, setSeasons] = useState<TMDBSeason[]>([]);
const [tvmazeEpisodes, setTvmazeEpisodes] = useState<TVMazeEpisode[]>([]);
const [imdbRatings, setImdbRatings] = useState<IMDbRatings>([]);
const [loading, setLoading] = useState(true);
const [loadingSeasons, setLoadingSeasons] = useState(false);
const [loadedSeasons, setLoadedSeasons] = useState<number[]>([]);
const [ratingSource, setRatingSource] = useState<RatingSource>('tmdb');
const [ratingSource, setRatingSource] = useState<RatingSource>('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) => {
<MaterialIcons name="warning" size={14} color={colors.warning} />
<Text style={[styles.warningText, { color: colors.lightGray }]}>Rating differs significantly from IMDb</Text>
</View>
<View style={styles.warningLegend}>
<MaterialIcons name="schedule" size={14} color={colors.primary} />
<Text style={[styles.warningText, { color: colors.lightGray }]}>Current season (ratings may change)</Text>
</View>
</View>
</View>
</Animated.View>
@ -535,7 +542,7 @@ const ShowRatingsScreen = ({ route }: Props) => {
episode={season.episodes[episodeIndex]}
ratingSource={ratingSource}
getTVMazeRating={getTVMazeRating}
isCurrentSeason={isCurrentSeason}
getIMDbRating={getIMDbRating}
theme={currentTheme}
/>
}

View file

@ -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<string, number | null> = 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<number | null> {
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<IMDbRatings | null> {
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<IMDbRatings>(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<TMDBSeason | null> {
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) {