From 7c3934be03e211652746816972e3b128fc6086d7 Mon Sep 17 00:00:00 2001 From: tapframe Date: Fri, 20 Jun 2025 00:23:43 +0530 Subject: [PATCH] Update Trakt integration to fetch watched, watchlist, and collection items with images This update enhances the Trakt integration by modifying the data fetching methods to include images for watched movies, shows, watchlist items, and collections. The useTraktIntegration hook and LibraryScreen have been updated accordingly to handle and display these images, improving the visual representation of content. Additionally, new interfaces have been introduced in the TraktService to support these changes, ensuring a more comprehensive user experience. --- src/hooks/useTraktIntegration.ts | 16 +-- src/screens/LibraryScreen.tsx | 208 ++++++++++++++++++------------ src/services/imageCacheService.ts | 137 ++++++++++++++++++++ src/services/traktService.ts | 162 +++++++++++++++++++++++ 4 files changed, 435 insertions(+), 88 deletions(-) create mode 100644 src/services/imageCacheService.ts 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