import ExternalAPI from '@server/api/externalapi'; import cacheManager from '@server/lib/cache'; import { sortBy } from 'lodash'; import type { TmdbCollection, TmdbExternalIdResponse, TmdbGenre, TmdbGenresResult, TmdbLanguage, TmdbMovieDetails, TmdbNetwork, TmdbPersonCombinedCredits, TmdbPersonDetails, TmdbProductionCompany, TmdbRegion, TmdbSearchMovieResponse, TmdbSearchMultiResponse, TmdbSearchTvResponse, TmdbSeasonWithEpisodes, TmdbTvDetails, TmdbUpcomingMoviesResponse, } from './interfaces'; interface SearchOptions { query: string; page?: number; includeAdult?: boolean; language?: string; } interface SingleSearchOptions extends SearchOptions { year?: number; } interface DiscoverMovieOptions { page?: number; includeAdult?: boolean; language?: string; primaryReleaseDateGte?: string; primaryReleaseDateLte?: string; originalLanguage?: string; genre?: number; studio?: number; sortBy?: | 'popularity.asc' | 'popularity.desc' | 'release_date.asc' | 'release_date.desc' | 'revenue.asc' | 'revenue.desc' | 'primary_release_date.asc' | 'primary_release_date.desc' | 'original_title.asc' | 'original_title.desc' | 'vote_average.asc' | 'vote_average.desc' | 'vote_count.asc' | 'vote_count.desc'; } interface DiscoverTvOptions { page?: number; language?: string; firstAirDateGte?: string; firstAirDateLte?: string; includeEmptyReleaseDate?: boolean; originalLanguage?: string; genre?: number; network?: number; sortBy?: | 'popularity.asc' | 'popularity.desc' | 'vote_average.asc' | 'vote_average.desc' | 'vote_count.asc' | 'vote_count.desc' | 'first_air_date.asc' | 'first_air_date.desc'; } class TheMovieDb extends ExternalAPI { private region?: string; private originalLanguage?: string; constructor({ region, originalLanguage, }: { region?: string; originalLanguage?: string } = {}) { super( 'https://api.themoviedb.org/3', { api_key: 'db55323b8d3e4154498498a75642b381', }, { nodeCache: cacheManager.getCache('tmdb').data, rateLimit: { maxRequests: 20, maxRPS: 50, }, } ); this.region = region; this.originalLanguage = originalLanguage; } public searchMulti = async ({ query, page = 1, includeAdult = false, language = 'en', }: SearchOptions): Promise => { try { const data = await this.get('/search/multi', { params: { query, page, include_adult: includeAdult, language }, }); return data; } catch (e) { return { page: 1, results: [], total_pages: 1, total_results: 0, }; } }; public searchMovies = async ({ query, page = 1, includeAdult = false, language = 'en', year, }: SingleSearchOptions): Promise => { try { const data = await this.get('/search/movie', { params: { query, page, include_adult: includeAdult, language, primary_release_year: year, }, }); return data; } catch (e) { return { page: 1, results: [], total_pages: 1, total_results: 0, }; } }; public searchTvShows = async ({ query, page = 1, includeAdult = false, language = 'en', year, }: SingleSearchOptions): Promise => { try { const data = await this.get('/search/tv', { params: { query, page, include_adult: includeAdult, language, first_air_date_year: year, }, }); return data; } catch (e) { return { page: 1, results: [], total_pages: 1, total_results: 0, }; } }; public getPerson = async ({ personId, language = 'en', }: { personId: number; language?: string; }): Promise => { try { const data = await this.get(`/person/${personId}`, { params: { language }, }); return data; } catch (e) { throw new Error(`[TMDB] Failed to fetch person details: ${e.message}`); } }; public getPersonCombinedCredits = async ({ personId, language = 'en', }: { personId: number; language?: string; }): Promise => { try { const data = await this.get( `/person/${personId}/combined_credits`, { params: { language }, } ); return data; } catch (e) { throw new Error( `[TMDB] Failed to fetch person combined credits: ${e.message}` ); } }; public getMovie = async ({ movieId, language = 'en', }: { movieId: number; language?: string; }): Promise => { try { const data = await this.get( `/movie/${movieId}`, { params: { language, append_to_response: 'credits,external_ids,videos,release_dates,watch/providers', }, }, 43200 ); return data; } catch (e) { throw new Error(`[TMDB] Failed to fetch movie details: ${e.message}`); } }; public getTvShow = async ({ tvId, language = 'en', }: { tvId: number; language?: string; }): Promise => { try { const data = await this.get( `/tv/${tvId}`, { params: { language, append_to_response: 'aggregate_credits,credits,external_ids,keywords,videos,content_ratings,watch/providers', }, }, 43200 ); return data; } catch (e) { throw new Error(`[TMDB] Failed to fetch TV show details: ${e.message}`); } }; public getTvSeason = async ({ tvId, seasonNumber, language, }: { tvId: number; seasonNumber: number; language?: string; }): Promise => { try { const data = await this.get( `/tv/${tvId}/season/${seasonNumber}`, { params: { language, append_to_response: 'external_ids', }, } ); return data; } catch (e) { throw new Error(`[TMDB] Failed to fetch TV show details: ${e.message}`); } }; public async getMovieRecommendations({ movieId, page = 1, language = 'en', }: { movieId: number; page?: number; language?: string; }): Promise { try { const data = await this.get( `/movie/${movieId}/recommendations`, { params: { page, language, }, } ); return data; } catch (e) { throw new Error(`[TMDB] Failed to fetch discover movies: ${e.message}`); } } public async getMovieSimilar({ movieId, page = 1, language = 'en', }: { movieId: number; page?: number; language?: string; }): Promise { try { const data = await this.get( `/movie/${movieId}/similar`, { params: { page, language, }, } ); return data; } catch (e) { throw new Error(`[TMDB] Failed to fetch discover movies: ${e.message}`); } } public async getMoviesByKeyword({ keywordId, page = 1, language = 'en', }: { keywordId: number; page?: number; language?: string; }): Promise { try { const data = await this.get( `/keyword/${keywordId}/movies`, { params: { page, language, }, } ); return data; } catch (e) { throw new Error(`[TMDB] Failed to fetch movies by keyword: ${e.message}`); } } public async getTvRecommendations({ tvId, page = 1, language = 'en', }: { tvId: number; page?: number; language?: string; }): Promise { try { const data = await this.get( `/tv/${tvId}/recommendations`, { params: { page, language, }, } ); return data; } catch (e) { throw new Error( `[TMDB] Failed to fetch TV recommendations: ${e.message}` ); } } public async getTvSimilar({ tvId, page = 1, language = 'en', }: { tvId: number; page?: number; language?: string; }): Promise { try { const data = await this.get(`/tv/${tvId}/similar`, { params: { page, language, }, }); return data; } catch (e) { throw new Error(`[TMDB] Failed to fetch TV similar: ${e.message}`); } } public getDiscoverMovies = async ({ sortBy = 'popularity.desc', page = 1, includeAdult = false, language = 'en', primaryReleaseDateGte, primaryReleaseDateLte, originalLanguage, genre, studio, }: DiscoverMovieOptions = {}): Promise => { try { const data = await this.get('/discover/movie', { params: { sort_by: sortBy, page, include_adult: includeAdult, language, region: this.region, with_original_language: originalLanguage ?? this.originalLanguage, 'primary_release_date.gte': primaryReleaseDateGte, 'primary_release_date.lte': primaryReleaseDateLte, with_genres: genre, with_companies: studio, }, }); return data; } catch (e) { throw new Error(`[TMDB] Failed to fetch discover movies: ${e.message}`); } }; public getDiscoverTv = async ({ sortBy = 'popularity.desc', page = 1, language = 'en', firstAirDateGte, firstAirDateLte, includeEmptyReleaseDate = false, originalLanguage, genre, network, }: DiscoverTvOptions = {}): Promise => { try { const data = await this.get('/discover/tv', { params: { sort_by: sortBy, page, language, region: this.region, 'first_air_date.gte': firstAirDateGte, 'first_air_date.lte': firstAirDateLte, with_original_language: originalLanguage ?? this.originalLanguage, include_null_first_air_dates: includeEmptyReleaseDate, with_genres: genre, with_networks: network, }, }); return data; } catch (e) { throw new Error(`[TMDB] Failed to fetch discover TV: ${e.message}`); } }; public getUpcomingMovies = async ({ page = 1, language = 'en', }: { page: number; language: string; }): Promise => { try { const data = await this.get( '/movie/upcoming', { params: { page, language, region: this.region, originalLanguage: this.originalLanguage, }, } ); return data; } catch (e) { throw new Error(`[TMDB] Failed to fetch upcoming movies: ${e.message}`); } }; public getAllTrending = async ({ page = 1, timeWindow = 'day', language = 'en', }: { page?: number; timeWindow?: 'day' | 'week'; language?: string; } = {}): Promise => { try { const data = await this.get( `/trending/all/${timeWindow}`, { params: { page, language, region: this.region, }, } ); return data; } catch (e) { throw new Error(`[TMDB] Failed to fetch all trending: ${e.message}`); } }; public getMovieTrending = async ({ page = 1, timeWindow = 'day', }: { page?: number; timeWindow?: 'day' | 'week'; } = {}): Promise => { try { const data = await this.get( `/trending/movie/${timeWindow}`, { params: { page, }, } ); return data; } catch (e) { throw new Error(`[TMDB] Failed to fetch all trending: ${e.message}`); } }; public getTvTrending = async ({ page = 1, timeWindow = 'day', }: { page?: number; timeWindow?: 'day' | 'week'; } = {}): Promise => { try { const data = await this.get( `/trending/tv/${timeWindow}`, { params: { page, }, } ); return data; } catch (e) { throw new Error(`[TMDB] Failed to fetch all trending: ${e.message}`); } }; public async getByExternalId({ externalId, type, language = 'en', }: | { externalId: string; type: 'imdb'; language?: string; } | { externalId: number; type: 'tvdb'; language?: string; }): Promise { try { const data = await this.get( `/find/${externalId}`, { params: { external_source: type === 'imdb' ? 'imdb_id' : 'tvdb_id', language, }, } ); return data; } catch (e) { throw new Error(`[TMDB] Failed to find by external ID: ${e.message}`); } } public async getMediaByImdbId({ imdbId, language = 'en', }: { imdbId: string; language?: string; }): Promise { try { const extResponse = await this.getByExternalId({ externalId: imdbId, type: 'imdb', }); if (extResponse.movie_results[0]) { const movie = await this.getMovie({ movieId: extResponse.movie_results[0].id, language, }); return movie; } if (extResponse.tv_results[0]) { const tvshow = await this.getTvShow({ tvId: extResponse.tv_results[0].id, language, }); return tvshow; } throw new Error(`No movie or show returned from API for ID ${imdbId}`); } catch (e) { throw new Error( `[TMDB] Failed to find media using external IMDb ID: ${e.message}` ); } } public async getShowByTvdbId({ tvdbId, language = 'en', }: { tvdbId: number; language?: string; }): Promise { try { const extResponse = await this.getByExternalId({ externalId: tvdbId, type: 'tvdb', }); if (extResponse.tv_results[0]) { const tvshow = await this.getTvShow({ tvId: extResponse.tv_results[0].id, language, }); return tvshow; } throw new Error(`No show returned from API for ID ${tvdbId}`); } catch (e) { throw new Error( `[TMDB] Failed to get TV show using the external TVDB ID: ${e.message}` ); } } public async getCollection({ collectionId, language = 'en', }: { collectionId: number; language?: string; }): Promise { try { const data = await this.get( `/collection/${collectionId}`, { params: { language, }, } ); return data; } catch (e) { throw new Error(`[TMDB] Failed to fetch collection: ${e.message}`); } } public async getRegions(): Promise { try { const data = await this.get( '/configuration/countries', {}, 86400 // 24 hours ); const regions = sortBy(data, 'english_name'); return regions; } catch (e) { throw new Error(`[TMDB] Failed to fetch countries: ${e.message}`); } } public async getLanguages(): Promise { try { const data = await this.get( '/configuration/languages', {}, 86400 // 24 hours ); const languages = sortBy(data, 'english_name'); return languages; } catch (e) { throw new Error(`[TMDB] Failed to fetch langauges: ${e.message}`); } } public async getStudio(studioId: number): Promise { try { const data = await this.get( `/company/${studioId}` ); return data; } catch (e) { throw new Error(`[TMDB] Failed to fetch movie studio: ${e.message}`); } } public async getNetwork(networkId: number): Promise { try { const data = await this.get(`/network/${networkId}`); return data; } catch (e) { throw new Error(`[TMDB] Failed to fetch TV network: ${e.message}`); } } public async getMovieGenres({ language = 'en', }: { language?: string; } = {}): Promise { try { const data = await this.get( '/genre/movie/list', { params: { language, }, }, 86400 // 24 hours ); if ( !language.startsWith('en') && data.genres.some((genre) => !genre.name) ) { const englishData = await this.get( '/genre/movie/list', { params: { language: 'en', }, }, 86400 // 24 hours ); data.genres .filter((genre) => !genre.name) .forEach((genre) => { genre.name = englishData.genres.find( (englishGenre) => englishGenre.id === genre.id )?.name ?? ''; }); } const movieGenres = sortBy( data.genres.filter((genre) => genre.name), 'name' ); return movieGenres; } catch (e) { throw new Error(`[TMDB] Failed to fetch movie genres: ${e.message}`); } } public async getTvGenres({ language = 'en', }: { language?: string; } = {}): Promise { try { const data = await this.get( '/genre/tv/list', { params: { language, }, }, 86400 // 24 hours ); if ( !language.startsWith('en') && data.genres.some((genre) => !genre.name) ) { const englishData = await this.get( '/genre/tv/list', { params: { language: 'en', }, }, 86400 // 24 hours ); data.genres .filter((genre) => !genre.name) .forEach((genre) => { genre.name = englishData.genres.find( (englishGenre) => englishGenre.id === genre.id )?.name ?? ''; }); } const tvGenres = sortBy( data.genres.filter((genre) => genre.name), 'name' ); return tvGenres; } catch (e) { throw new Error(`[TMDB] Failed to fetch TV genres: ${e.message}`); } } } export default TheMovieDb;