diff --git a/src/hooks/useTraktIntegration.ts b/src/hooks/useTraktIntegration.ts index 425ac1cd..3b61d440 100644 --- a/src/hooks/useTraktIntegration.ts +++ b/src/hooks/useTraktIntegration.ts @@ -68,8 +68,8 @@ export function useTraktIntegration() { setIsLoading(true); try { const [movies, shows] = await Promise.all([ - traktService.getWatchedMovies(), - traktService.getWatchedShows() + traktService.getWatchedMoviesWithImages(), + traktService.getWatchedShowsWithImages() ]); setWatchedMovies(movies); setWatchedShows(shows); @@ -94,12 +94,12 @@ export function useTraktIntegration() { continueWatching, ratings ] = await Promise.all([ - traktService.getWatchlistMovies(), - traktService.getWatchlistShows(), - traktService.getCollectionMovies(), - traktService.getCollectionShows(), - traktService.getPlaybackProgress(), - traktService.getRatings() + traktService.getWatchlistMoviesWithImages(), + traktService.getWatchlistShowsWithImages(), + traktService.getCollectionMoviesWithImages(), + traktService.getCollectionShowsWithImages(), + traktService.getPlaybackProgressWithImages(), + traktService.getRatingsWithImages() ]); setWatchlistMovies(watchlistMovies); diff --git a/src/screens/LibraryScreen.tsx b/src/screens/LibraryScreen.tsx index 36513b8f..22f61a29 100644 --- a/src/screens/LibraryScreen.tsx +++ b/src/screens/LibraryScreen.tsx @@ -28,7 +28,7 @@ import { useSafeAreaInsets } from 'react-native-safe-area-context'; import { useTheme } from '../contexts/ThemeContext'; import { useTraktContext } from '../contexts/TraktContext'; import TraktIcon from '../../assets/rating-icons/trakt.svg'; -import { TMDBService } from '../services/tmdbService'; +import { traktService, TraktService } from '../services/traktService'; // Define interfaces for proper typing interface LibraryItem extends StreamingContent { @@ -246,7 +246,10 @@ const LibraryScreen = () => { return folders.filter(folder => folder.itemCount > 0); }, [traktAuthenticated, watchedMovies, watchedShows, watchlistMovies, watchlistShows, collectionMovies, collectionShows, continueWatching, ratedContent]); - // Prepare Trakt items with proper poster URLs + // State for poster URLs (since they're now async) + const [traktPostersMap, setTraktPostersMap] = useState>(new Map()); + + // Prepare Trakt items with placeholders, then load posters async const traktItems = useMemo(() => { if (!traktAuthenticated || (!watchedMovies?.length && !watchedShows?.length)) { return []; @@ -259,11 +262,14 @@ const LibraryScreen = () => { for (const watchedMovie of watchedMovies) { const movie = watchedMovie.movie; if (movie) { + const itemId = String(movie.ids.trakt); + const cachedPoster = traktPostersMap.get(itemId); + items.push({ - id: String(movie.ids.trakt), + id: itemId, name: movie.title, type: 'movie', - poster: 'https://via.placeholder.com/300x450/cccccc/666666?text=Loading...', + poster: cachedPoster || 'https://via.placeholder.com/300x450/cccccc/666666?text=Loading...', year: movie.year, lastWatched: new Date(watchedMovie.last_watched_at).toLocaleDateString(), plays: watchedMovie.plays, @@ -279,11 +285,14 @@ const LibraryScreen = () => { for (const watchedShow of watchedShows) { const show = watchedShow.show; if (show) { + const itemId = String(show.ids.trakt); + const cachedPoster = traktPostersMap.get(itemId); + items.push({ - id: String(show.ids.trakt), + id: itemId, name: show.title, type: 'series', - poster: 'https://via.placeholder.com/300x450/cccccc/666666?text=Loading...', + poster: cachedPoster || 'https://via.placeholder.com/300x450/cccccc/666666?text=Loading...', year: show.year, lastWatched: new Date(watchedShow.last_watched_at).toLocaleDateString(), plays: watchedShow.plays, @@ -300,69 +309,72 @@ const LibraryScreen = () => { const dateB = b.lastWatched ? new Date(b.lastWatched).getTime() : 0; return dateB - dateA; }); - }, [traktAuthenticated, watchedMovies, watchedShows]); + }, [traktAuthenticated, watchedMovies, watchedShows, traktPostersMap]); - // State for tracking poster URLs - const [traktPostersMap, setTraktPostersMap] = useState>(new Map()); - - // Effect to fetch poster URLs for Trakt items + // Effect to load cached poster URLs useEffect(() => { - const fetchTraktPosters = async () => { - if (!traktAuthenticated || traktItems.length === 0) return; + const loadCachedPosters = async () => { + if (!traktAuthenticated) return; - const tmdbService = TMDBService.getInstance(); + const postersToLoad = new Map(); - // Process items individually and update state as each poster is fetched - for (const item of traktItems) { - try { - // Get TMDB ID from the original Trakt data - let tmdbId: number | null = null; - - if (item.type === 'movie' && watchedMovies) { - const watchedMovie = watchedMovies.find(wm => wm.movie?.ids.trakt === item.traktId); - tmdbId = watchedMovie?.movie?.ids.tmdb || null; - } else if (item.type === 'series' && watchedShows) { - const watchedShow = watchedShows.find(ws => ws.show?.ids.trakt === item.traktId); - tmdbId = watchedShow?.show?.ids.tmdb || null; - } - - if (tmdbId) { - // Fetch details from TMDB to get poster path - let posterPath: string | null = null; - - if (item.type === 'movie') { - const movieDetails = await tmdbService.getMovieDetails(String(tmdbId)); - posterPath = movieDetails?.poster_path || null; - } else { - const showDetails = await tmdbService.getTVShowDetails(tmdbId); - posterPath = showDetails?.poster_path || null; - } - - if (posterPath) { - const fullPosterUrl = tmdbService.getImageUrl(posterPath, 'w500'); - if (fullPosterUrl) { - // Update state immediately for this item - setTraktPostersMap(prevMap => { - const newMap = new Map(prevMap); - newMap.set(item.id, fullPosterUrl); - return newMap; - }); - } + // Collect movies that need posters + if (watchedMovies) { + for (const watchedMovie of watchedMovies) { + const movie = watchedMovie.movie; + if (movie) { + const itemId = String(movie.ids.trakt); + if (!traktPostersMap.has(itemId)) { + postersToLoad.set(itemId, movie.images); } } - } catch (error) { - logger.error(`Failed to fetch poster for Trakt item ${item.id}:`, error); } } + + // Collect shows that need posters + if (watchedShows) { + for (const watchedShow of watchedShows) { + const show = watchedShow.show; + if (show) { + const itemId = String(show.ids.trakt); + if (!traktPostersMap.has(itemId)) { + postersToLoad.set(itemId, show.images); + } + } + } + } + + // Load posters in parallel + const posterPromises = Array.from(postersToLoad.entries()).map(async ([itemId, images]) => { + try { + const posterUrl = await TraktService.getTraktPosterUrl(images); + return { + itemId, + posterUrl: posterUrl || 'https://via.placeholder.com/300x450/cccccc/666666?text=No+Poster' + }; + } catch (error) { + logger.error(`Failed to get cached poster for ${itemId}:`, error); + return { + itemId, + posterUrl: 'https://via.placeholder.com/300x450/cccccc/666666?text=No+Poster' + }; + } + }); + + const results = await Promise.all(posterPromises); + + // Update state with new posters + setTraktPostersMap(prevMap => { + const newMap = new Map(prevMap); + results.forEach(({ itemId, posterUrl }) => { + newMap.set(itemId, posterUrl); + }); + return newMap; + }); }; - fetchTraktPosters(); - }, [traktItems, traktAuthenticated, watchedMovies, watchedShows]); - - // Log when posters map updates - useEffect(() => { - // Removed debugging logs - }, [traktPostersMap]); + loadCachedPosters(); + }, [traktAuthenticated, watchedMovies, watchedShows]); const itemWidth = (width - 48) / 2; // 2 items per row with padding @@ -503,7 +515,7 @@ const LibraryScreen = () => { ); const renderTraktItem = ({ item, customWidth }: { item: TraktDisplayItem; customWidth?: number }) => { - const posterUrl = traktPostersMap.get(item.id) || 'https://via.placeholder.com/300x450/ff0000/ffffff?text=No+Poster'; + const posterUrl = item.poster || 'https://via.placeholder.com/300x450/ff0000/ffffff?text=No+Poster'; const width = customWidth || itemWidth; return ( @@ -567,11 +579,14 @@ const LibraryScreen = () => { for (const watchedMovie of watchedMovies) { const movie = watchedMovie.movie; if (movie) { + const itemId = String(movie.ids.trakt); + const cachedPoster = traktPostersMap.get(itemId); + items.push({ - id: String(movie.ids.trakt), + id: itemId, name: movie.title, type: 'movie', - poster: 'https://via.placeholder.com/300x450/cccccc/666666?text=Loading...', + poster: cachedPoster || 'https://via.placeholder.com/300x450/cccccc/666666?text=Loading...', year: movie.year, lastWatched: new Date(watchedMovie.last_watched_at).toLocaleDateString(), plays: watchedMovie.plays, @@ -586,11 +601,14 @@ const LibraryScreen = () => { for (const watchedShow of watchedShows) { const show = watchedShow.show; if (show) { + const itemId = String(show.ids.trakt); + const cachedPoster = traktPostersMap.get(itemId); + items.push({ - id: String(show.ids.trakt), + id: itemId, name: show.title, type: 'series', - poster: 'https://via.placeholder.com/300x450/cccccc/666666?text=Loading...', + poster: cachedPoster || 'https://via.placeholder.com/300x450/cccccc/666666?text=Loading...', year: show.year, lastWatched: new Date(watchedShow.last_watched_at).toLocaleDateString(), plays: watchedShow.plays, @@ -607,22 +625,28 @@ const LibraryScreen = () => { if (continueWatching) { for (const item of continueWatching) { if (item.type === 'movie' && item.movie) { + const itemId = String(item.movie.ids.trakt); + const cachedPoster = traktPostersMap.get(itemId); + items.push({ - id: String(item.movie.ids.trakt), + id: itemId, name: item.movie.title, type: 'movie', - poster: 'https://via.placeholder.com/300x450/cccccc/666666?text=Loading...', + poster: cachedPoster || 'https://via.placeholder.com/300x450/cccccc/666666?text=Loading...', year: item.movie.year, lastWatched: new Date(item.paused_at).toLocaleDateString(), imdbId: item.movie.ids.imdb, traktId: item.movie.ids.trakt, }); } else if (item.type === 'episode' && item.show && item.episode) { + const itemId = String(item.show.ids.trakt); + const cachedPoster = traktPostersMap.get(itemId); + items.push({ id: `${item.show.ids.trakt}:${item.episode.season}:${item.episode.number}`, name: `${item.show.title} S${item.episode.season}E${item.episode.number}`, type: 'series', - poster: 'https://via.placeholder.com/300x450/cccccc/666666?text=Loading...', + poster: cachedPoster || 'https://via.placeholder.com/300x450/cccccc/666666?text=Loading...', year: item.show.year, lastWatched: new Date(item.paused_at).toLocaleDateString(), imdbId: item.show.ids.imdb, @@ -639,11 +663,15 @@ const LibraryScreen = () => { for (const watchlistMovie of watchlistMovies) { const movie = watchlistMovie.movie; if (movie) { + const itemId = String(movie.ids.trakt); + const posterUrl = TraktService.getTraktPosterUrl(movie.images) || + 'https://via.placeholder.com/300x450/cccccc/666666?text=No+Poster'; + items.push({ - id: String(movie.ids.trakt), + id: itemId, name: movie.title, type: 'movie', - poster: 'https://via.placeholder.com/300x450/cccccc/666666?text=Loading...', + poster: posterUrl, year: movie.year, lastWatched: new Date(watchlistMovie.listed_at).toLocaleDateString(), imdbId: movie.ids.imdb, @@ -657,11 +685,15 @@ const LibraryScreen = () => { for (const watchlistShow of watchlistShows) { const show = watchlistShow.show; if (show) { + const itemId = String(show.ids.trakt); + const posterUrl = TraktService.getTraktPosterUrl(show.images) || + 'https://via.placeholder.com/300x450/cccccc/666666?text=No+Poster'; + items.push({ - id: String(show.ids.trakt), + id: itemId, name: show.title, type: 'series', - poster: 'https://via.placeholder.com/300x450/cccccc/666666?text=Loading...', + poster: posterUrl, year: show.year, lastWatched: new Date(watchlistShow.listed_at).toLocaleDateString(), imdbId: show.ids.imdb, @@ -678,11 +710,15 @@ const LibraryScreen = () => { for (const collectionMovie of collectionMovies) { const movie = collectionMovie.movie; if (movie) { + const itemId = String(movie.ids.trakt); + const posterUrl = TraktService.getTraktPosterUrl(movie.images) || + 'https://via.placeholder.com/300x450/cccccc/666666?text=No+Poster'; + items.push({ - id: String(movie.ids.trakt), + id: itemId, name: movie.title, type: 'movie', - poster: 'https://via.placeholder.com/300x450/cccccc/666666?text=Loading...', + poster: posterUrl, year: movie.year, lastWatched: new Date(collectionMovie.collected_at).toLocaleDateString(), imdbId: movie.ids.imdb, @@ -696,11 +732,15 @@ const LibraryScreen = () => { for (const collectionShow of collectionShows) { const show = collectionShow.show; if (show) { + const itemId = String(show.ids.trakt); + const posterUrl = TraktService.getTraktPosterUrl(show.images) || + 'https://via.placeholder.com/300x450/cccccc/666666?text=No+Poster'; + items.push({ - id: String(show.ids.trakt), + id: itemId, name: show.title, type: 'series', - poster: 'https://via.placeholder.com/300x450/cccccc/666666?text=Loading...', + poster: posterUrl, year: show.year, lastWatched: new Date(collectionShow.collected_at).toLocaleDateString(), imdbId: show.ids.imdb, @@ -717,11 +757,15 @@ const LibraryScreen = () => { for (const ratedItem of ratedContent) { if (ratedItem.movie) { const movie = ratedItem.movie; + const itemId = String(movie.ids.trakt); + const posterUrl = TraktService.getTraktPosterUrl(movie.images) || + 'https://via.placeholder.com/300x450/cccccc/666666?text=No+Poster'; + items.push({ - id: String(movie.ids.trakt), + id: itemId, name: movie.title, type: 'movie', - poster: 'https://via.placeholder.com/300x450/cccccc/666666?text=Loading...', + poster: posterUrl, year: movie.year, lastWatched: new Date(ratedItem.rated_at).toLocaleDateString(), rating: ratedItem.rating, @@ -730,11 +774,15 @@ const LibraryScreen = () => { }); } else if (ratedItem.show) { const show = ratedItem.show; + const itemId = String(show.ids.trakt); + const posterUrl = TraktService.getTraktPosterUrl(show.images) || + 'https://via.placeholder.com/300x450/cccccc/666666?text=No+Poster'; + items.push({ - id: String(show.ids.trakt), + id: itemId, name: show.title, type: 'series', - poster: 'https://via.placeholder.com/300x450/cccccc/666666?text=Loading...', + poster: posterUrl, year: show.year, lastWatched: new Date(ratedItem.rated_at).toLocaleDateString(), rating: ratedItem.rating, @@ -753,7 +801,7 @@ const LibraryScreen = () => { const dateB = b.lastWatched ? new Date(b.lastWatched).getTime() : 0; return dateB - dateA; }); - }, [watchedMovies, watchedShows, watchlistMovies, watchlistShows, collectionMovies, collectionShows, continueWatching, ratedContent]); + }, [watchedMovies, watchedShows, watchlistMovies, watchlistShows, collectionMovies, collectionShows, continueWatching, ratedContent, traktPostersMap]); const renderTraktContent = () => { if (traktLoading) { diff --git a/src/services/imageCacheService.ts b/src/services/imageCacheService.ts new file mode 100644 index 00000000..e5a8bcac --- /dev/null +++ b/src/services/imageCacheService.ts @@ -0,0 +1,137 @@ +import { logger } from '../utils/logger'; + +interface CachedImage { + url: string; + localPath: string; + timestamp: number; + expiresAt: number; +} + +class ImageCacheService { + private cache = new Map(); + private readonly CACHE_DURATION = 24 * 60 * 60 * 1000; // 24 hours + private readonly MAX_CACHE_SIZE = 100; // Maximum number of cached images + + /** + * Get a cached image URL or cache the original if not present + */ + public async getCachedImageUrl(originalUrl: string): Promise { + if (!originalUrl || originalUrl.includes('placeholder')) { + return originalUrl; // Don't cache placeholder images + } + + // Check if we have a valid cached version + const cached = this.cache.get(originalUrl); + if (cached && cached.expiresAt > Date.now()) { + logger.log(`[ImageCache] Retrieved from cache: ${originalUrl}`); + return cached.localPath; + } + + try { + // For now, return the original URL but mark it as cached + // In a production app, you would implement actual local caching here + const cachedImage: CachedImage = { + url: originalUrl, + localPath: originalUrl, // In production, this would be a local file path + timestamp: Date.now(), + expiresAt: Date.now() + this.CACHE_DURATION, + }; + + this.cache.set(originalUrl, cachedImage); + this.enforceMaxCacheSize(); + + logger.log(`[ImageCache] ✅ NEW CACHE ENTRY: ${originalUrl} (Cache size: ${this.cache.size})`); + return cachedImage.localPath; + } catch (error) { + logger.error('[ImageCache] Failed to cache image:', error); + return originalUrl; // Fallback to original URL + } + } + + /** + * Check if an image is cached + */ + public isCached(url: string): boolean { + const cached = this.cache.get(url); + return cached !== undefined && cached.expiresAt > Date.now(); + } + + /** + * Log cache status (for debugging) + */ + public logCacheStatus(): void { + const stats = this.getCacheStats(); + logger.log(`[ImageCache] 📊 Cache Status: ${stats.size} total, ${stats.expired} expired`); + + // Log first 5 cached URLs for debugging + const entries = Array.from(this.cache.entries()).slice(0, 5); + entries.forEach(([url, cached]) => { + const isExpired = cached.expiresAt <= Date.now(); + const timeLeft = Math.max(0, cached.expiresAt - Date.now()) / 1000 / 60; // minutes + logger.log(`[ImageCache] - ${url.substring(0, 60)}... (${isExpired ? 'EXPIRED' : `${timeLeft.toFixed(1)}m left`})`); + }); + } + + /** + * Clear expired cache entries + */ + public clearExpiredCache(): void { + const now = Date.now(); + for (const [url, cached] of this.cache.entries()) { + if (cached.expiresAt <= now) { + this.cache.delete(url); + } + } + } + + /** + * Clear all cached images + */ + public clearAllCache(): void { + this.cache.clear(); + logger.log('[ImageCache] Cleared all cached images'); + } + + /** + * Get cache statistics + */ + public getCacheStats(): { size: number; expired: number } { + const now = Date.now(); + let expired = 0; + + for (const cached of this.cache.values()) { + if (cached.expiresAt <= now) { + expired++; + } + } + + return { + size: this.cache.size, + expired, + }; + } + + /** + * Enforce maximum cache size by removing oldest entries + */ + private enforceMaxCacheSize(): void { + if (this.cache.size <= this.MAX_CACHE_SIZE) { + return; + } + + // Convert to array and sort by timestamp (oldest first) + const entries = Array.from(this.cache.entries()).sort( + (a, b) => a[1].timestamp - b[1].timestamp + ); + + // Remove oldest entries + const toRemove = this.cache.size - this.MAX_CACHE_SIZE; + for (let i = 0; i < toRemove; i++) { + this.cache.delete(entries[i][0]); + } + + logger.log(`[ImageCache] Removed ${toRemove} old entries to enforce cache size limit`); + } +} + +export const imageCacheService = new ImageCacheService(); \ No newline at end of file diff --git a/src/services/traktService.ts b/src/services/traktService.ts index 7aba7f7a..071deecd 100644 --- a/src/services/traktService.ts +++ b/src/services/traktService.ts @@ -1,5 +1,6 @@ import AsyncStorage from '@react-native-async-storage/async-storage'; import { logger } from '../utils/logger'; +import { imageCacheService } from './imageCacheService'; // Storage keys export const TRAKT_ACCESS_TOKEN_KEY = 'trakt_access_token'; @@ -32,6 +33,7 @@ export interface TraktWatchedItem { imdb: string; tmdb: number; }; + images?: TraktImages; }; show?: { title: string; @@ -42,6 +44,7 @@ export interface TraktWatchedItem { imdb: string; tmdb: number; }; + images?: TraktImages; }; plays: number; last_watched_at: string; @@ -57,6 +60,7 @@ export interface TraktWatchlistItem { imdb: string; tmdb: number; }; + images?: TraktImages; }; show?: { title: string; @@ -67,6 +71,7 @@ export interface TraktWatchlistItem { imdb: string; tmdb: number; }; + images?: TraktImages; }; listed_at: string; } @@ -81,6 +86,7 @@ export interface TraktCollectionItem { imdb: string; tmdb: number; }; + images?: TraktImages; }; show?: { title: string; @@ -91,6 +97,7 @@ export interface TraktCollectionItem { imdb: string; tmdb: number; }; + images?: TraktImages; }; collected_at: string; } @@ -105,6 +112,7 @@ export interface TraktRatingItem { imdb: string; tmdb: number; }; + images?: TraktImages; }; show?: { title: string; @@ -115,11 +123,33 @@ export interface TraktRatingItem { imdb: string; tmdb: number; }; + images?: TraktImages; }; rating: number; rated_at: string; } +export interface TraktImages { + fanart?: string[]; + poster?: string[]; + logo?: string[]; + clearart?: string[]; + banner?: string[]; + thumb?: string[]; +} + +export interface TraktItemWithImages { + title: string; + year: number; + ids: { + trakt: number; + slug: string; + imdb: string; + tmdb: number; + }; + images?: TraktImages; +} + // New types for scrobbling export interface TraktPlaybackItem { progress: number; @@ -135,6 +165,7 @@ export interface TraktPlaybackItem { imdb: string; tmdb: number; }; + images?: TraktImages; }; episode?: { season: number; @@ -146,6 +177,7 @@ export interface TraktPlaybackItem { imdb?: string; tmdb?: number; }; + images?: TraktImages; }; show?: { title: string; @@ -157,6 +189,7 @@ export interface TraktPlaybackItem { imdb: string; tmdb: number; }; + images?: TraktImages; }; } @@ -687,6 +720,124 @@ export class TraktService { return this.apiRequest(endpoint); } + /** + * Get the user's watched movies with images + */ + public async getWatchedMoviesWithImages(): Promise { + return this.apiRequest('/sync/watched/movies?extended=images'); + } + + /** + * Get the user's watched shows with images + */ + public async getWatchedShowsWithImages(): Promise { + return this.apiRequest('/sync/watched/shows?extended=images'); + } + + /** + * Get the user's watchlist movies with images + */ + public async getWatchlistMoviesWithImages(): Promise { + return this.apiRequest('/sync/watchlist/movies?extended=images'); + } + + /** + * Get the user's watchlist shows with images + */ + public async getWatchlistShowsWithImages(): Promise { + return this.apiRequest('/sync/watchlist/shows?extended=images'); + } + + /** + * Get the user's collection movies with images + */ + public async getCollectionMoviesWithImages(): Promise { + return this.apiRequest('/sync/collection/movies?extended=images'); + } + + /** + * Get the user's collection shows with images + */ + public async getCollectionShowsWithImages(): Promise { + return this.apiRequest('/sync/collection/shows?extended=images'); + } + + /** + * Get the user's ratings with images + */ + public async getRatingsWithImages(type?: 'movies' | 'shows'): Promise { + const endpoint = type ? `/sync/ratings/${type}?extended=images` : '/sync/ratings?extended=images'; + return this.apiRequest(endpoint); + } + + /** + * Get playback progress with images + */ + public async getPlaybackProgressWithImages(type?: 'movies' | 'shows'): Promise { + try { + const endpoint = type ? `/sync/playback/${type}?extended=images` : '/sync/playback?extended=images'; + return this.apiRequest(endpoint); + } catch (error) { + logger.error('[TraktService] Failed to get playback progress with images:', error); + return []; + } + } + + /** + * Extract poster URL from Trakt images with basic caching + */ + public static getTraktPosterUrl(images?: TraktImages): string | null { + if (!images || !images.poster || images.poster.length === 0) { + return null; + } + + // Get the first poster and add https prefix + const posterPath = images.poster[0]; + const fullUrl = posterPath.startsWith('http') ? posterPath : `https://${posterPath}`; + + // Try to use cached version synchronously (basic cache check) + const isCached = imageCacheService.isCached(fullUrl); + if (isCached) { + logger.log(`[TraktService] 🎯 Using cached poster: ${fullUrl.substring(0, 60)}...`); + } else { + logger.log(`[TraktService] 📥 New poster URL: ${fullUrl.substring(0, 60)}...`); + // Queue for async caching + imageCacheService.getCachedImageUrl(fullUrl).catch(error => { + logger.error('[TraktService] Background caching failed:', error); + }); + } + + return fullUrl; + } + + /** + * Extract poster URL from Trakt images with async caching + */ + public static async getTraktPosterUrlCached(images?: TraktImages): Promise { + const url = this.getTraktPosterUrl(images); + if (!url) return null; + + try { + return await imageCacheService.getCachedImageUrl(url); + } catch (error) { + logger.error('[TraktService] Failed to cache image:', error); + return url; + } + } + + /** + * Extract fanart URL from Trakt images + */ + public static getTraktFanartUrl(images?: TraktImages): string | null { + if (!images || !images.fanart || images.fanart.length === 0) { + return null; + } + + // Get the first fanart and add https prefix + const fanartPath = images.fanart[0]; + return fanartPath.startsWith('http') ? fanartPath : `https://${fanartPath}`; + } + /** * Get trakt id from IMDb id */ @@ -1270,6 +1421,17 @@ export class TraktService { logger.error('[TraktService] DEBUG: Error fetching playback progress:', error); } } + /** + * Debug image cache status + */ + public static debugImageCache(): void { + try { + logger.log('[TraktService] === IMAGE CACHE DEBUG ==='); + imageCacheService.logCacheStatus(); + } catch (error) { + logger.error('[TraktService] Debug image cache failed:', error); + } + } } // Export a singleton instance