import cache from '../cache.js'; import config from '../config.js'; export default class Tmdb { static id = 'tmdb'; static name = 'The Movie Database'; #cleanTmdbId(id) { return id.replace(/^tmdb-/, ''); } #getSearchTitle(title) { // Special handling for UFC events const ufcMatch = title.match(/UFC Fight Night (\d+):/i) || title.match(/UFC (\d+):/i); if (ufcMatch) { return `UFC ${ufcMatch[1]}`; } return title; } async getMovieById(id, language){ if (id.startsWith('tmdb-')) { try { const cleanId = this.#cleanTmdbId(id); const movie = await this.#request('GET', `/3/movie/${cleanId}`, { query: { language: language || 'en-US' } }, { key: `movie:${cleanId}:${language || '-'}`, ttl: 3600*3 }); return { name: this.#getSearchTitle(language ? movie.title || movie.original_title : movie.original_title || movie.title), originalName: language ? movie.title || movie.original_title : movie.original_title || movie.title, year: parseInt(`${movie.release_date}`.split('-').shift()), imdb_id: movie.imdb_id || id, type: 'movie', stremioId: id, id, }; } catch (err) { console.log(`Failed to fetch movie directly with TMDB ID ${id}:`, err.message); } } // Fallback to IMDb lookup const searchId = await this.#request('GET', `/3/find/${id}`, { query: { external_source: 'imdb_id', language: language || 'en-US' } }, { key: `searchId:${id}:${language || '-'}`, ttl: 3600*3 }); if (!searchId.movie_results?.[0]) { throw new Error(`Movie not found: ${id}`); } const meta = searchId.movie_results[0]; return { name: this.#getSearchTitle(language ? meta.title || meta.original_title : meta.original_title || meta.title), originalName: language ? meta.title || meta.original_title : meta.original_title || meta.title, year: parseInt(`${meta.release_date}`.split('-').shift()), imdb_id: id, type: 'movie', stremioId: id, id, }; } async getEpisodeById(id, season, episode, language){ if (id.startsWith('tmdb-')) { try { const cleanId = this.#cleanTmdbId(id); const show = await this.#request('GET', `/3/tv/${cleanId}`, { query: { language: language || 'en-US' } }, { key: `tv:${cleanId}:${language || '-'}`, ttl: 3600*3 }); const episodes = []; show.seasons.forEach(s => { for(let e = 1; e <= s.episode_count; e++){ episodes.push({ season: s.season_number, episode: e, stremioId: `${id}:${s.season_number}:${e}` }); } }); return { name: this.#getSearchTitle(language ? show.name || show.original_name : show.original_name || show.name), originalName: language ? show.name || show.original_name : show.original_name || show.name, year: parseInt(`${show.first_air_date}`.split('-').shift()), imdb_id: show.external_ids?.imdb_id || id, type: 'series', stremioId: `${id}:${season}:${episode}`, id, season, episode, episodes }; } catch (err) { console.log(`Failed to fetch show directly with TMDB ID ${id}:`, err.message); } } const searchId = await this.#request('GET', `/3/find/${id}`, { query: { external_source: 'imdb_id' } }, { key: `searchId:${id}`, ttl: 3600*3 }); if (!searchId.tv_results?.[0]) { throw new Error(`TV series not found: ${id}`); } const meta = await this.#request('GET', `/3/tv/${searchId.tv_results[0].id}`, { query: { language: language || 'en-US' } }, { key: `${id}:${language}`, ttl: 3600*3 }); const episodes = []; meta.seasons.forEach(s => { for(let e = 1; e <= s.episode_count; e++){ episodes.push({ season: s.season_number, episode: e, stremioId: `${id}:${s.season_number}:${e}` }); } }); return { name: this.#getSearchTitle(language ? meta.name || meta.original_name : meta.original_name || meta.name), originalName: language ? meta.name || meta.original_name : meta.original_name || meta.name, year: parseInt(`${meta.first_air_date}`.split('-').shift()), imdb_id: id, type: 'series', stremioId: `${id}:${season}:${episode}`, id, season, episode, episodes }; } async getLanguages(){ return [{value: '', label: '🌎Original (Recommended)'}].concat( ...config.languages .map(language => ({value: language.iso639, label: language.label})) .filter(language => language.value) ); } async #request(method, path, opts = {}, cacheOpts = {}) { const apiKey = config.tmdbAccessToken; // Normalize cache options cacheOpts = { key: '', ttl: 0, ...cacheOpts }; // Check cache first if (cacheOpts.key) { const cached = await cache.get(`tmdb:${cacheOpts.key}`); if (cached) return cached; } // Clean up the path - remove any trailing slashes path = path.replace(/\/+$/, ''); // Prepare query parameters including API key const queryParams = new URLSearchParams({ api_key: apiKey, ...(opts.query || {}) }); // Build the complete URL const url = `https://api.themoviedb.org${path}?${queryParams}`; // Prepare request options const requestOpts = { method, headers: { 'Accept': 'application/json', 'Content-Type': 'application/json;charset=utf-8', ...opts.headers } }; // Debug log the full URL console.log('TMDB Request URL:', url); console.log('TMDB Request Headers:', requestOpts.headers); try { const res = await fetch(url, requestOpts); const data = await res.json(); // Debug log the response console.log('TMDB Response Status:', res.status); console.log('TMDB Response Data:', JSON.stringify(data, null, 2)); if (!res.ok) { console.error('TMDB API Error:', { status: res.status, url: url, headers: requestOpts.headers, response: data }); throw new Error(`TMDB API error: ${data.status_message || 'Unknown error'}`); } // Cache successful response if needed if (cacheOpts.key && cacheOpts.ttl > 0) { await cache.set(`tmdb:${cacheOpts.key}`, data, {ttl: cacheOpts.ttl}); } return data; } catch (error) { console.error('TMDB Request failed:', error); throw error; } } }