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.
This commit is contained in:
tapframe 2025-06-20 00:23:43 +05:30
parent 237c1fae3d
commit 7c3934be03
4 changed files with 435 additions and 88 deletions

View file

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

View file

@ -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<Map<string, string>>(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<Map<string, string>>(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<string, any>();
// 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) {

View file

@ -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<string, CachedImage>();
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<string> {
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();

View file

@ -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<TraktRatingItem[]>(endpoint);
}
/**
* Get the user's watched movies with images
*/
public async getWatchedMoviesWithImages(): Promise<TraktWatchedItem[]> {
return this.apiRequest<TraktWatchedItem[]>('/sync/watched/movies?extended=images');
}
/**
* Get the user's watched shows with images
*/
public async getWatchedShowsWithImages(): Promise<TraktWatchedItem[]> {
return this.apiRequest<TraktWatchedItem[]>('/sync/watched/shows?extended=images');
}
/**
* Get the user's watchlist movies with images
*/
public async getWatchlistMoviesWithImages(): Promise<TraktWatchlistItem[]> {
return this.apiRequest<TraktWatchlistItem[]>('/sync/watchlist/movies?extended=images');
}
/**
* Get the user's watchlist shows with images
*/
public async getWatchlistShowsWithImages(): Promise<TraktWatchlistItem[]> {
return this.apiRequest<TraktWatchlistItem[]>('/sync/watchlist/shows?extended=images');
}
/**
* Get the user's collection movies with images
*/
public async getCollectionMoviesWithImages(): Promise<TraktCollectionItem[]> {
return this.apiRequest<TraktCollectionItem[]>('/sync/collection/movies?extended=images');
}
/**
* Get the user's collection shows with images
*/
public async getCollectionShowsWithImages(): Promise<TraktCollectionItem[]> {
return this.apiRequest<TraktCollectionItem[]>('/sync/collection/shows?extended=images');
}
/**
* Get the user's ratings with images
*/
public async getRatingsWithImages(type?: 'movies' | 'shows'): Promise<TraktRatingItem[]> {
const endpoint = type ? `/sync/ratings/${type}?extended=images` : '/sync/ratings?extended=images';
return this.apiRequest<TraktRatingItem[]>(endpoint);
}
/**
* Get playback progress with images
*/
public async getPlaybackProgressWithImages(type?: 'movies' | 'shows'): Promise<TraktPlaybackItem[]> {
try {
const endpoint = type ? `/sync/playback/${type}?extended=images` : '/sync/playback?extended=images';
return this.apiRequest<TraktPlaybackItem[]>(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<string | null> {
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