|
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) { |
|
|
|
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); |
|
} |
|
} |
|
|
|
|
|
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; |
|
|
|
|
|
cacheOpts = { |
|
key: '', |
|
ttl: 0, |
|
...cacheOpts |
|
}; |
|
|
|
|
|
if (cacheOpts.key) { |
|
const cached = await cache.get(`tmdb:${cacheOpts.key}`); |
|
if (cached) return cached; |
|
} |
|
|
|
|
|
path = path.replace(/\/+$/, ''); |
|
|
|
|
|
const queryParams = new URLSearchParams({ |
|
api_key: apiKey, |
|
...(opts.query || {}) |
|
}); |
|
|
|
|
|
const url = `https://api.themoviedb.org${path}?${queryParams}`; |
|
|
|
|
|
const requestOpts = { |
|
method, |
|
headers: { |
|
'Accept': 'application/json', |
|
'Content-Type': 'application/json;charset=utf-8', |
|
...opts.headers |
|
} |
|
}; |
|
|
|
|
|
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(); |
|
|
|
|
|
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'}`); |
|
} |
|
|
|
|
|
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; |
|
} |
|
} |
|
} |