diff --git a/.env.example b/.env.example index baa65525..c088715d 100644 --- a/.env.example +++ b/.env.example @@ -3,6 +3,12 @@ EXPO_PUBLIC_SUPABASE_URL=your_supabase_project_url EXPO_PUBLIC_SUPABASE_ANON_KEY=your_supabase_anon_key +# Remote cache for TMDB (optional) +# Set to true to use local/remote cache server, and provide URL +EXPO_PUBLIC_USE_REMOTE_CACHE=false +EXPO_PUBLIC_CACHE_SERVER_URL=http://localhost:5173 +EXPO_PUBLIC_DISABLE_LOCAL_CACHE=false + # MovieBox (MoviesMod) Keys EXPO_PUBLIC_MOVIEBOX_PRIMARY_KEY=your_moviebox_primary_key EXPO_PUBLIC_MOVIEBOX_TMDB_API_KEY=your_tmdb_api_key_for_moviebox diff --git a/.gitignore b/.gitignore index 1ce445fb..91b38442 100644 --- a/.gitignore +++ b/.gitignore @@ -76,3 +76,4 @@ bottomnav.md mmkv.md src/services/tmdbService.ts fix-android-scroll-lag-summary.md +server/cache-server \ No newline at end of file diff --git a/src/components/metadata/SeriesContent.tsx b/src/components/metadata/SeriesContent.tsx index 3a6b3e42..2b266b9b 100644 --- a/src/components/metadata/SeriesContent.tsx +++ b/src/components/metadata/SeriesContent.tsx @@ -499,6 +499,13 @@ export const SeriesContent: React.FC = ({ + // Helper function to get IMDb rating for an episode - O(1) lookup using map + const getIMDbRating = useCallback((seasonNumber: number, episodeNumber: number): number | null => { + const key = `${seasonNumber}:${episodeNumber}`; + const rating = imdbRatingsMap[key]; + return rating ?? null; + }, [imdbRatingsMap]); + if (loadingSeasons) { return ( @@ -717,13 +724,6 @@ export const SeriesContent: React.FC = ({ ); }; - // Helper function to get IMDb rating for an episode - O(1) lookup using map - const getIMDbRating = useCallback((seasonNumber: number, episodeNumber: number): number | null => { - const key = `${seasonNumber}:${episodeNumber}`; - const rating = imdbRatingsMap[key]; - return rating ?? null; - }, [imdbRatingsMap]); - // Vertical layout episode card (traditional) const renderVerticalEpisodeCard = (episode: Episode) => { // Resolve episode image with addon-first logic @@ -1579,6 +1579,7 @@ const styles = StyleSheet.create({ flexDirection: 'row', alignItems: 'center', // chip background removed + minWidth: 52, // reserve space so following items (rating) don't shift }, runtimeText: { fontSize: 13, diff --git a/src/screens/.ShowRatingsScreen.tsx.swp b/src/screens/.ShowRatingsScreen.tsx.swp deleted file mode 100644 index 66357af5..00000000 Binary files a/src/screens/.ShowRatingsScreen.tsx.swp and /dev/null differ diff --git a/src/services/tmdbService.ts b/src/services/tmdbService.ts index 3940cb9e..99ba7033 100644 --- a/src/services/tmdbService.ts +++ b/src/services/tmdbService.ts @@ -7,6 +7,12 @@ const DEFAULT_API_KEY = 'd131017ccc6e5462a81c9304d21476de'; const BASE_URL = 'https://api.themoviedb.org/3'; const TMDB_API_KEY_STORAGE_KEY = 'tmdb_api_key'; const USE_CUSTOM_TMDB_API_KEY = 'use_custom_tmdb_api_key'; +// Remote cache configuration +const REMOTE_CACHE_URL = process.env.EXPO_PUBLIC_CACHE_SERVER_URL; +const USE_REMOTE_CACHE = process.env.EXPO_PUBLIC_USE_REMOTE_CACHE === 'true'; +const REMOTE_CACHE_NAMESPACE = 'tmdb'; +// Allow temporarily disabling local MMKV cache (read/write) +const DISABLE_LOCAL_CACHE = process.env.EXPO_PUBLIC_DISABLE_LOCAL_CACHE === 'true'; // Cache configuration const TMDB_CACHE_PREFIX = 'tmdb_cache_'; @@ -134,6 +140,52 @@ export class TMDBService { this.loadApiKey(); } + /** + * Remote cache helpers + */ + private async remoteGetCachedData(key: string): Promise { + if (!USE_REMOTE_CACHE || !REMOTE_CACHE_URL) return null; + try { + const url = `${REMOTE_CACHE_URL}/cache/${REMOTE_CACHE_NAMESPACE}/${encodeURIComponent(key)}`; + const response = await axios.get(url, { headers: { 'Content-Type': 'application/json' } }); + const payload = response.data; + if (payload && Object.prototype.hasOwnProperty.call(payload, 'data')) { + // Warm local cache for faster subsequent reads (skip if disabled) + if (!DISABLE_LOCAL_CACHE) { + this.setCachedData(key, payload.data); + } + logger.log(`[TMDB Remote Cache] ✅ HIT: ${key}`); + return payload.data as T; + } + return null; + } catch (_) { + logger.log(`[TMDB Remote Cache] ❌ MISS: ${key}`); + return null; + } + } + + private async remoteSetCachedData(key: string, data: any): Promise { + if (!USE_REMOTE_CACHE || !REMOTE_CACHE_URL) return; + try { + const url = `${REMOTE_CACHE_URL}/cache/${REMOTE_CACHE_NAMESPACE}/${encodeURIComponent(key)}`; + await axios.put(url, { data, ttlMs: CACHE_TTL_MS }, { headers: { 'Content-Type': 'application/json' } }); + logger.log(`[TMDB Remote Cache] 💾 STORED: ${key}`); + } catch (_) { + // best-effort only + } + } + + private async remoteClearAllCache(): Promise { + if (!USE_REMOTE_CACHE || !REMOTE_CACHE_URL) return; + try { + const url = `${REMOTE_CACHE_URL}/cache/${REMOTE_CACHE_NAMESPACE}/clear`; + await axios.post(url, {}, { headers: { 'Content-Type': 'application/json' } }); + logger.log(`[TMDB Remote Cache] 🗑️ CLEARED namespace ${REMOTE_CACHE_NAMESPACE}`); + } catch (_) { + // ignore + } + } + /** * Generate a unique cache key from endpoint and parameters */ @@ -154,6 +206,10 @@ export class TMDBService { * Retrieve cached data if not expired */ private getCachedData(key: string): T | null { + if (DISABLE_LOCAL_CACHE) { + logger.log(`[TMDB Cache] 🚫 LOCAL DISABLED: ${key}`); + return null; + } try { const cachedStr = mmkvStorage.getString(key); if (!cachedStr) { @@ -180,6 +236,19 @@ export class TMDBService { } } + private async getFromCacheOrRemote(key: string): Promise { + // Local-first: serve from MMKV if present; else try remote and warm local + if (!DISABLE_LOCAL_CACHE) { + const local = this.getCachedData(key); + if (local !== null) return local; + } + if (USE_REMOTE_CACHE && REMOTE_CACHE_URL) { + const remote = await this.remoteGetCachedData(key); + if (remote !== null) return remote; + } + return null; + } + /** * Store data in cache with timestamp * Only called after successful API responses - never caches error responses @@ -193,12 +262,19 @@ export class TMDBService { } try { + if (!DISABLE_LOCAL_CACHE) { const cacheEntry = { data, timestamp: Date.now() }; mmkvStorage.setString(key, JSON.stringify(cacheEntry)); logger.log(`[TMDB Cache] 💾 STORED: ${key}`); + } else { + logger.log(`[TMDB Cache] ⛔ LOCAL WRITE SKIPPED: ${key}`); + } + // Best-effort remote write + // eslint-disable-next-line @typescript-eslint/no-floating-promises + this.remoteSetCachedData(key, data); } catch (error) { // Ignore cache errors } @@ -285,8 +361,8 @@ export class TMDBService { async searchTVShow(query: string): Promise { const cacheKey = this.generateCacheKey('search_tv', { query }); - // Check cache - const cached = this.getCachedData(cacheKey); + // Check cache (local or remote) + const cached = await this.getFromCacheOrRemote(cacheKey); if (cached !== null) return cached; logger.log(`[TMDB API] 🌐 FETCHING: searchTVShow("${query}")`); @@ -314,8 +390,8 @@ export class TMDBService { async getTVShowDetails(tmdbId: number, language: string = 'en'): Promise { const cacheKey = this.generateCacheKey(`tv_${tmdbId}`, { language }); - // Check cache - const cached = this.getCachedData(cacheKey); + // Check cache (local or remote) + const cached = await this.getFromCacheOrRemote(cacheKey); if (cached !== null) return cached; try { @@ -344,8 +420,8 @@ export class TMDBService { ): Promise<{ imdb_id: string | null } | null> { const cacheKey = this.generateCacheKey(`tv_${tmdbId}_season_${seasonNumber}_episode_${episodeNumber}_external_ids`); - // Check cache - const cached = this.getCachedData<{ imdb_id: string | null }>(cacheKey); + // Check cache (local or remote) + const cached = await this.getFromCacheOrRemote<{ imdb_id: string | null }>(cacheKey); if (cached !== null) return cached; try { @@ -416,8 +492,8 @@ export class TMDBService { const cacheKey = this.generateCacheKey(`imdb_ratings_${tmdbId}`); - // Check cache - const cached = this.getCachedData(cacheKey); + // Check cache (local or remote) + const cached = await this.getFromCacheOrRemote(cacheKey); if (cached !== null) return cached; const apiUrl = `${IMDB_RATINGS_API_BASE_URL}/api/shows/${tmdbId}/season-ratings`; @@ -450,8 +526,8 @@ export class TMDBService { async getSeasonDetails(tmdbId: number, seasonNumber: number, showName?: string, language: string = 'en-US'): Promise { const cacheKey = this.generateCacheKey(`tv_${tmdbId}_season_${seasonNumber}`, { language, showName }); - // Check cache - const cached = this.getCachedData(cacheKey); + // Check cache (local or remote) + const cached = await this.getFromCacheOrRemote(cacheKey); if (cached !== null) return cached; try { @@ -481,8 +557,8 @@ export class TMDBService { ): Promise { const cacheKey = this.generateCacheKey(`tv_${tmdbId}_season_${seasonNumber}_episode_${episodeNumber}`, { language }); - // Check cache - const cached = this.getCachedData(cacheKey); + // Check cache (local or remote) + const cached = await this.getFromCacheOrRemote(cacheKey); if (cached !== null) return cached; try { @@ -528,8 +604,8 @@ export class TMDBService { async findTMDBIdByIMDB(imdbId: string): Promise { const cacheKey = this.generateCacheKey('find_imdb', { imdbId }); - // Check cache - const cached = this.getCachedData(cacheKey); + // Check cache (local or remote) + const cached = await this.getFromCacheOrRemote(cacheKey); if (cached !== null) return cached; try { @@ -654,8 +730,8 @@ export class TMDBService { async getCredits(tmdbId: number, type: string) { const cacheKey = this.generateCacheKey(`${type}_${tmdbId}_credits`); - // Check cache - const cached = this.getCachedData<{ cast: any[]; crew: any[] }>(cacheKey); + // Check cache (local or remote) + const cached = await this.getFromCacheOrRemote<{ cast: any[]; crew: any[] }>(cacheKey); if (cached !== null) return cached; try { @@ -679,8 +755,8 @@ export class TMDBService { async getPersonDetails(personId: number) { const cacheKey = this.generateCacheKey(`person_${personId}`); - // Check cache - const cached = this.getCachedData(cacheKey); + // Check cache (local or remote) + const cached = await this.getFromCacheOrRemote(cacheKey); if (cached !== null) return cached; try { @@ -704,8 +780,8 @@ export class TMDBService { async getPersonMovieCredits(personId: number) { const cacheKey = this.generateCacheKey(`person_${personId}_movie_credits`); - // Check cache - const cached = this.getCachedData(cacheKey); + // Check cache (local or remote) + const cached = await this.getFromCacheOrRemote(cacheKey); if (cached !== null) return cached; try { @@ -729,8 +805,8 @@ export class TMDBService { async getPersonTvCredits(personId: number) { const cacheKey = this.generateCacheKey(`person_${personId}_tv_credits`); - // Check cache - const cached = this.getCachedData(cacheKey); + // Check cache (local or remote) + const cached = await this.getFromCacheOrRemote(cacheKey); if (cached !== null) return cached; try { @@ -754,8 +830,8 @@ export class TMDBService { async getPersonCombinedCredits(personId: number) { const cacheKey = this.generateCacheKey(`person_${personId}_combined_credits`); - // Check cache - const cached = this.getCachedData(cacheKey); + // Check cache (local or remote) + const cached = await this.getFromCacheOrRemote(cacheKey); if (cached !== null) return cached; try { @@ -779,8 +855,8 @@ export class TMDBService { async getShowExternalIds(tmdbId: number): Promise<{ imdb_id: string | null } | null> { const cacheKey = this.generateCacheKey(`tv_${tmdbId}_external_ids`); - // Check cache - const cached = this.getCachedData<{ imdb_id: string | null }>(cacheKey); + // Check cache (local or remote) + const cached = await this.getFromCacheOrRemote<{ imdb_id: string | null }>(cacheKey); if (cached !== null) return cached; try { @@ -806,8 +882,8 @@ export class TMDBService { const cacheKey = this.generateCacheKey(`${type}_${tmdbId}_recommendations`, { language }); - // Check cache - const cached = this.getCachedData(cacheKey); + // Check cache (local or remote) + const cached = await this.getFromCacheOrRemote(cacheKey); if (cached !== null) return cached; try { @@ -826,8 +902,8 @@ export class TMDBService { async searchMulti(query: string): Promise { const cacheKey = this.generateCacheKey('search_multi', { query }); - // Check cache - const cached = this.getCachedData(cacheKey); + // Check cache (local or remote) + const cached = await this.getFromCacheOrRemote(cacheKey); if (cached !== null) return cached; try { @@ -854,8 +930,8 @@ export class TMDBService { async getMovieDetails(movieId: string, language: string = 'en'): Promise { const cacheKey = this.generateCacheKey(`movie_${movieId}`, { language }); - // Check cache - const cached = this.getCachedData(cacheKey); + // Check cache (local or remote) + const cached = await this.getFromCacheOrRemote(cacheKey); if (cached !== null) return cached; try { @@ -880,8 +956,8 @@ export class TMDBService { async getCollectionDetails(collectionId: number, language: string = 'en'): Promise { const cacheKey = this.generateCacheKey(`collection_${collectionId}`, { language }); - // Check cache - const cached = this.getCachedData(cacheKey); + // Check cache (local or remote) + const cached = await this.getFromCacheOrRemote(cacheKey); if (cached !== null) return cached; try { @@ -905,8 +981,8 @@ export class TMDBService { async getCollectionImages(collectionId: number, language: string = 'en'): Promise { const cacheKey = this.generateCacheKey(`collection_${collectionId}_images`, { language }); - // Check cache - const cached = this.getCachedData(cacheKey); + // Check cache (local or remote) + const cached = await this.getFromCacheOrRemote(cacheKey); if (cached !== null) return cached; try { @@ -931,8 +1007,8 @@ export class TMDBService { async getMovieImagesFull(movieId: number | string, language: string = 'en'): Promise { const cacheKey = this.generateCacheKey(`movie_${movieId}_images_full`, { language }); - // Check cache - const cached = this.getCachedData(cacheKey); + // Check cache (local or remote) + const cached = await this.getFromCacheOrRemote(cacheKey); if (cached !== null) { return cached; } @@ -1086,8 +1162,8 @@ export class TMDBService { async getTvShowImagesFull(showId: number | string, language: string = 'en'): Promise { const cacheKey = this.generateCacheKey(`tv_${showId}_images_full`, { language }); - // Check cache - const cached = this.getCachedData(cacheKey); + // Check cache (local or remote) + const cached = await this.getFromCacheOrRemote(cacheKey); if (cached !== null) return cached; try { @@ -1112,8 +1188,8 @@ export class TMDBService { async getTvShowImages(showId: number | string, preferredLanguage: string = 'en'): Promise { const cacheKey = this.generateCacheKey(`tv_${showId}_logo`, { preferredLanguage }); - // Check cache - const cached = this.getCachedData(cacheKey); + // Check cache (local or remote) + const cached = await this.getFromCacheOrRemote(cacheKey); if (cached !== null) return cached; try { @@ -1330,8 +1406,8 @@ export class TMDBService { async getTrending(type: 'movie' | 'tv', timeWindow: 'day' | 'week'): Promise { const cacheKey = this.generateCacheKey(`trending_${type}_${timeWindow}`); - // Check cache - const cached = this.getCachedData(cacheKey); + // Check cache (local or remote) + const cached = await this.getFromCacheOrRemote(cacheKey); if (cached !== null) return cached; try { @@ -1379,8 +1455,8 @@ export class TMDBService { async getPopular(type: 'movie' | 'tv', page: number = 1): Promise { const cacheKey = this.generateCacheKey(`popular_${type}`, { page }); - // Check cache - const cached = this.getCachedData(cacheKey); + // Check cache (local or remote) + const cached = await this.getFromCacheOrRemote(cacheKey); if (cached !== null) return cached; try { @@ -1429,8 +1505,8 @@ export class TMDBService { async getUpcoming(type: 'movie' | 'tv', page: number = 1): Promise { const cacheKey = this.generateCacheKey(`upcoming_${type}`, { page }); - // Check cache - const cached = this.getCachedData(cacheKey); + // Check cache (local or remote) + const cached = await this.getFromCacheOrRemote(cacheKey); if (cached !== null) return cached; try { @@ -1482,8 +1558,8 @@ export class TMDBService { async getNowPlaying(page: number = 1, region: string = 'US'): Promise { const cacheKey = this.generateCacheKey('now_playing', { page, region }); - // Check cache - const cached = this.getCachedData(cacheKey); + // Check cache (local or remote) + const cached = await this.getFromCacheOrRemote(cacheKey); if (cached !== null) return cached; try { @@ -1531,8 +1607,8 @@ export class TMDBService { async getMovieGenres(): Promise<{ id: number; name: string }[]> { const cacheKey = this.generateCacheKey('genres_movie'); - // Check cache - const cached = this.getCachedData<{ id: number; name: string }[]>(cacheKey); + // Check cache (local or remote) + const cached = await this.getFromCacheOrRemote<{ id: number; name: string }[]>(cacheKey); if (cached !== null) return cached; try { @@ -1556,8 +1632,8 @@ export class TMDBService { async getTvGenres(): Promise<{ id: number; name: string }[]> { const cacheKey = this.generateCacheKey('genres_tv'); - // Check cache - const cached = this.getCachedData<{ id: number; name: string }[]>(cacheKey); + // Check cache (local or remote) + const cached = await this.getFromCacheOrRemote<{ id: number; name: string }[]>(cacheKey); if (cached !== null) return cached; try { @@ -1584,8 +1660,8 @@ export class TMDBService { async discoverByGenre(type: 'movie' | 'tv', genreName: string, page: number = 1): Promise { const cacheKey = this.generateCacheKey(`discover_${type}`, { genreName, page }); - // Check cache - const cached = this.getCachedData(cacheKey); + // Check cache (local or remote) + const cached = await this.getFromCacheOrRemote(cacheKey); if (cached !== null) return cached; try {