NuvioStreaming/src/services/mdblistService.ts
2025-10-26 12:42:34 +05:30

243 lines
9.4 KiB
TypeScript

import { mmkvStorage } from './mmkvStorage';
import { logger } from '../utils/logger';
import {
MDBLIST_API_KEY_STORAGE_KEY,
MDBLIST_ENABLED_STORAGE_KEY,
isMDBListEnabled
} from '../screens/MDBListSettingsScreen';
export interface MDBListRatings {
trakt?: number;
imdb?: number;
tmdb?: number;
letterboxd?: number;
tomatoes?: number;
audience?: number;
metacritic?: number;
}
export class MDBListService {
private static instance: MDBListService;
private apiKey: string | null = null;
private enabled: boolean = true;
private apiKeyErrorCount: number = 0; // Add counter for API key errors
private lastApiKeyErrorTime: number = 0; // To track when last error occurred
private ratingsCache: Map<string, MDBListRatings | null> = new Map(); // Cache for ratings - null values represent known "not found" results
private constructor() {
logger.log('[MDBListService] Service initialized');
}
static getInstance(): MDBListService {
if (!MDBListService.instance) {
MDBListService.instance = new MDBListService();
}
return MDBListService.instance;
}
async initialize(): Promise<void> {
try {
// First check if MDBList is enabled
const enabledSetting = await mmkvStorage.getItem(MDBLIST_ENABLED_STORAGE_KEY);
const wasEnabled = this.enabled;
this.enabled = enabledSetting === null || enabledSetting === 'true';
logger.log('[MDBListService] MDBList enabled:', this.enabled);
// Clear cache if enabled state changed
if (wasEnabled !== this.enabled) {
this.clearCache();
logger.log('[MDBListService] Cache cleared due to enabled state change');
}
if (!this.enabled) {
logger.log('[MDBListService] MDBList is disabled, skipping API key loading');
this.apiKey = null;
return;
}
const newApiKey = await mmkvStorage.getItem(MDBLIST_API_KEY_STORAGE_KEY);
// Reset error counter when API key changes
if (newApiKey !== this.apiKey) {
this.apiKeyErrorCount = 0;
this.lastApiKeyErrorTime = 0;
// Clear the cache when API key changes
this.clearCache();
logger.log('[MDBListService] Cache cleared due to API key change');
}
this.apiKey = newApiKey;
logger.log('[MDBListService] Initialized with API key:', this.apiKey ? 'Present' : 'Not found');
} catch (error) {
logger.error('[MDBListService] Failed to load settings:', error);
this.apiKey = null;
this.enabled = true; // Default to enabled on error
}
}
async getRatings(imdbId: string, mediaType: 'movie' | 'show'): Promise<MDBListRatings | null> {
logger.log(`[MDBListService] Fetching ratings for ${mediaType} with IMDB ID:`, imdbId);
// Create cache key
const cacheKey = `${mediaType}:${imdbId}`;
// Check cache first - including null values which mean "no ratings available"
if (this.ratingsCache.has(cacheKey)) {
const cachedRatings = this.ratingsCache.get(cacheKey);
logger.log(`[MDBListService] Retrieved ${cachedRatings ? 'ratings' : 'negative result'} from cache for ${mediaType}:`, imdbId);
// TypeScript knows cachedRatings can't be undefined here since we checked with .has()
return cachedRatings as MDBListRatings | null;
}
// Check if MDBList is enabled before doing anything else
if (!this.enabled) {
// Try to refresh enabled status in case it was changed
try {
const enabledSetting = await mmkvStorage.getItem(MDBLIST_ENABLED_STORAGE_KEY);
this.enabled = enabledSetting === null || enabledSetting === 'true';
} catch (error) {
// Ignore error and keep current state
}
if (!this.enabled) {
logger.log('[MDBListService] MDBList is disabled, not fetching ratings');
return null;
}
}
if (!this.apiKey) {
logger.log('[MDBListService] No API key found, attempting to initialize');
await this.initialize();
if (!this.apiKey || !this.enabled) {
const reason = !this.enabled ? 'MDBList is disabled' : 'No API key found';
logger.warn(`[MDBListService] ${reason}`);
return null;
}
}
try {
const ratings: MDBListRatings = {};
const ratingTypes = ['trakt', 'imdb', 'tmdb', 'letterboxd', 'tomatoes', 'audience', 'metacritic'];
logger.log(`[MDBListService] Starting to fetch ${ratingTypes.length} different rating types in parallel`);
const formattedImdbId = imdbId.startsWith('tt') ? imdbId : `tt${imdbId}`;
if (!/^tt\d+$/.test(formattedImdbId)) {
logger.error('[MDBListService] Invalid IMDB ID format:', formattedImdbId);
return null;
}
logger.log(`[MDBListService] Using formatted IMDB ID:`, formattedImdbId);
// Create an array of fetch promises
const fetchPromises = ratingTypes.map(async (ratingType) => {
try {
// API Key in URL query parameter
const url = `https://api.mdblist.com/rating/${mediaType}/${ratingType}?apikey=${this.apiKey}`;
logger.log(`[MDBListService] Fetching ${ratingType} rating from:`, url);
// Body contains only ids and provider
const body = {
ids: [formattedImdbId],
provider: 'imdb'
};
logger.log(`[MDBListService] Request body:`, body);
const response = await fetch(url, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(body)
});
logger.log(`[MDBListService] ${ratingType} response status:`, response.status);
if (response.ok) {
const data = await response.json();
logger.log(`[MDBListService] ${ratingType} response data:`, data);
if (data.ratings?.[0]?.rating) {
ratings[ratingType as keyof MDBListRatings] = data.ratings[0].rating;
logger.log(`[MDBListService] Added ${ratingType} rating:`, data.ratings[0].rating);
return { type: ratingType, rating: data.ratings[0].rating };
} else {
logger.warn(`[MDBListService] No ${ratingType} rating found in response`);
return null;
}
} else {
// Log specific error for invalid API key
if (response.status === 403) {
const errorText = await response.text();
try {
const errorJson = JSON.parse(errorText);
if (errorJson.error === "Invalid API key") {
// Only log the error every 5 requests or if more than 10 minutes have passed
const now = Date.now();
this.apiKeyErrorCount++;
if (this.apiKeyErrorCount === 1 || this.apiKeyErrorCount % 5 === 0 || now - this.lastApiKeyErrorTime > 600000) {
logger.error('[MDBListService] API Key rejected by server:', this.apiKey);
this.lastApiKeyErrorTime = now;
}
} else {
logger.warn(`[MDBListService] 403 Forbidden, but not invalid key error:`, errorJson);
}
} catch (parseError) {
logger.warn(`[MDBListService] 403 Forbidden, non-JSON response:`, errorText);
}
} else {
logger.warn(`[MDBListService] Failed to fetch ${ratingType} rating. Status:`, response.status);
const errorText = await response.text();
logger.warn(`[MDBListService] Error response:`, errorText);
}
return null;
}
} catch (error) {
logger.error(`[MDBListService] Error fetching ${ratingType} rating:`, error);
return null;
}
});
// Execute all fetch promises in parallel
const results = await Promise.all(fetchPromises);
// Process results
results.forEach(result => {
if (result) {
ratings[result.type as keyof MDBListRatings] = result.rating;
}
});
const ratingCount = Object.keys(ratings).length;
logger.log(`[MDBListService] Fetched ${ratingCount} ratings successfully:`, ratings);
// Store in cache even if we got no ratings - this prevents repeated API calls for content with no ratings
const result = ratingCount > 0 ? ratings : null;
this.ratingsCache.set(cacheKey, result);
logger.log(`[MDBListService] Stored ${result ? 'ratings' : 'negative result'} in cache for ${mediaType}:`, imdbId);
return result;
} catch (error) {
logger.error('[MDBListService] Error fetching MDBList ratings:', error);
return null;
}
}
// Method to clear the cache
clearCache(): void {
this.ratingsCache.clear();
logger.log('[MDBListService] Cache cleared');
}
// Method to invalidate a specific cache entry
invalidateCache(imdbId: string, mediaType: 'movie' | 'show'): void {
const cacheKey = `${mediaType}:${imdbId}`;
const hadEntry = this.ratingsCache.delete(cacheKey);
logger.log(`[MDBListService] Cache entry ${hadEntry ? 'invalidated' : 'not found'} for ${mediaType}:`, imdbId);
}
// Method to check if a rating is in cache
isCached(imdbId: string, mediaType: 'movie' | 'show'): boolean {
const cacheKey = `${mediaType}:${imdbId}`;
return this.ratingsCache.has(cacheKey);
}
}
export const mdblistService = MDBListService.getInstance();