no1b4me's picture
Upload 38 files
1fa2c88 verified
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;
}
}
}