mirror of
https://github.com/sct/overseerr.git
synced 2025-09-17 17:24:35 +02:00
feat(cache): external API cache (#786)
This commit is contained in:
106
server/api/externalapi.ts
Normal file
106
server/api/externalapi.ts
Normal file
@@ -0,0 +1,106 @@
|
||||
import axios, { AxiosInstance, AxiosRequestConfig } from 'axios';
|
||||
import NodeCache from 'node-cache';
|
||||
import logger from '../logger';
|
||||
|
||||
// 5 minute default TTL (in seconds)
|
||||
const DEFAULT_TTL = 300;
|
||||
|
||||
// 10 seconds default rolling buffer (in ms)
|
||||
const DEFAULT_ROLLING_BUFFER = 10000;
|
||||
|
||||
interface ExternalAPIOptions {
|
||||
nodeCache?: NodeCache;
|
||||
headers?: Record<string, unknown>;
|
||||
}
|
||||
|
||||
class ExternalAPI {
|
||||
protected axios: AxiosInstance;
|
||||
private baseUrl: string;
|
||||
private cache?: NodeCache;
|
||||
|
||||
constructor(
|
||||
baseUrl: string,
|
||||
params: Record<string, unknown>,
|
||||
options: ExternalAPIOptions = {}
|
||||
) {
|
||||
this.axios = axios.create({
|
||||
baseURL: baseUrl,
|
||||
params,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Accept: 'application/json',
|
||||
...options.headers,
|
||||
},
|
||||
});
|
||||
this.baseUrl = baseUrl;
|
||||
this.cache = options.nodeCache;
|
||||
}
|
||||
|
||||
protected async get<T>(
|
||||
endpoint: string,
|
||||
config?: AxiosRequestConfig,
|
||||
ttl?: number
|
||||
): Promise<T> {
|
||||
const cacheKey = this.serializeCacheKey(endpoint, config?.params);
|
||||
const cachedItem = this.cache?.get<T>(cacheKey);
|
||||
if (cachedItem) {
|
||||
return cachedItem;
|
||||
}
|
||||
|
||||
const response = await this.axios.get<T>(endpoint, config);
|
||||
|
||||
if (this.cache) {
|
||||
this.cache.set(cacheKey, response.data, ttl ?? DEFAULT_TTL);
|
||||
}
|
||||
|
||||
return response.data;
|
||||
}
|
||||
|
||||
protected async getRolling<T>(
|
||||
endpoint: string,
|
||||
config?: AxiosRequestConfig,
|
||||
ttl?: number
|
||||
): Promise<T> {
|
||||
const cacheKey = this.serializeCacheKey(endpoint, config?.params);
|
||||
const cachedItem = this.cache?.get<T>(cacheKey);
|
||||
|
||||
if (cachedItem) {
|
||||
const keyTtl = this.cache?.getTtl(cacheKey) ?? 0;
|
||||
logger.debug(`Loaded item from cache: ${cacheKey}`, {
|
||||
keyTtl,
|
||||
});
|
||||
|
||||
// If the item has passed our rolling check, fetch again in background
|
||||
if (
|
||||
keyTtl - (ttl ?? DEFAULT_TTL) * 1000 <
|
||||
Date.now() - DEFAULT_ROLLING_BUFFER
|
||||
) {
|
||||
this.axios.get<T>(endpoint, config).then((response) => {
|
||||
this.cache?.set(cacheKey, response.data, ttl ?? DEFAULT_TTL);
|
||||
});
|
||||
}
|
||||
return cachedItem;
|
||||
}
|
||||
|
||||
const response = await this.axios.get<T>(endpoint, config);
|
||||
|
||||
if (this.cache) {
|
||||
this.cache.set(cacheKey, response.data, ttl ?? DEFAULT_TTL);
|
||||
}
|
||||
|
||||
return response.data;
|
||||
}
|
||||
|
||||
private serializeCacheKey(
|
||||
endpoint: string,
|
||||
params?: Record<string, unknown>
|
||||
) {
|
||||
if (!params) {
|
||||
return `${this.baseUrl}${endpoint}`;
|
||||
}
|
||||
|
||||
return `${this.baseUrl}${endpoint}${JSON.stringify(params)}`;
|
||||
}
|
||||
}
|
||||
|
||||
export default ExternalAPI;
|
@@ -1,6 +1,7 @@
|
||||
import Axios, { AxiosInstance } from 'axios';
|
||||
import cacheManager from '../lib/cache';
|
||||
import { RadarrSettings } from '../lib/settings';
|
||||
import logger from '../logger';
|
||||
import ExternalAPI from './externalapi';
|
||||
|
||||
interface RadarrMovieOptions {
|
||||
title: string;
|
||||
@@ -73,21 +74,23 @@ interface QueueResponse {
|
||||
records: QueueItem[];
|
||||
}
|
||||
|
||||
class RadarrAPI {
|
||||
class RadarrAPI extends ExternalAPI {
|
||||
static buildRadarrUrl(radarrSettings: RadarrSettings, path?: string): string {
|
||||
return `${radarrSettings.useSsl ? 'https' : 'http'}://${
|
||||
radarrSettings.hostname
|
||||
}:${radarrSettings.port}${radarrSettings.baseUrl ?? ''}${path}`;
|
||||
}
|
||||
|
||||
private axios: AxiosInstance;
|
||||
constructor({ url, apiKey }: { url: string; apiKey: string }) {
|
||||
this.axios = Axios.create({
|
||||
baseURL: url,
|
||||
params: {
|
||||
super(
|
||||
url,
|
||||
{
|
||||
apikey: apiKey,
|
||||
},
|
||||
});
|
||||
{
|
||||
nodeCache: cacheManager.getCache('radarr').data,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
public getMovies = async (): Promise<RadarrMovie[]> => {
|
||||
@@ -238,9 +241,13 @@ class RadarrAPI {
|
||||
|
||||
public getProfiles = async (): Promise<RadarrProfile[]> => {
|
||||
try {
|
||||
const response = await this.axios.get<RadarrProfile[]>(`/profile`);
|
||||
const data = await this.getRolling<RadarrProfile[]>(
|
||||
`/profile`,
|
||||
undefined,
|
||||
3600
|
||||
);
|
||||
|
||||
return response.data;
|
||||
return data;
|
||||
} catch (e) {
|
||||
throw new Error(`[Radarr] Failed to retrieve profiles: ${e.message}`);
|
||||
}
|
||||
@@ -248,9 +255,13 @@ class RadarrAPI {
|
||||
|
||||
public getRootFolders = async (): Promise<RadarrRootFolder[]> => {
|
||||
try {
|
||||
const response = await this.axios.get<RadarrRootFolder[]>(`/rootfolder`);
|
||||
const data = await this.getRolling<RadarrRootFolder[]>(
|
||||
`/rootfolder`,
|
||||
undefined,
|
||||
3600
|
||||
);
|
||||
|
||||
return response.data;
|
||||
return data;
|
||||
} catch (e) {
|
||||
throw new Error(`[Radarr] Failed to retrieve root folders: ${e.message}`);
|
||||
}
|
||||
|
@@ -1,4 +1,5 @@
|
||||
import axios, { AxiosInstance } from 'axios';
|
||||
import cacheManager from '../lib/cache';
|
||||
import ExternalAPI from './externalapi';
|
||||
|
||||
interface RTMovieOldSearchResult {
|
||||
id: number;
|
||||
@@ -55,17 +56,19 @@ export interface RTRating {
|
||||
* Unfortunately, we need to do it by searching for the movie name, so it's
|
||||
* not always accurate.
|
||||
*/
|
||||
class RottenTomatoes {
|
||||
private axios: AxiosInstance;
|
||||
|
||||
class RottenTomatoes extends ExternalAPI {
|
||||
constructor() {
|
||||
this.axios = axios.create({
|
||||
baseURL: 'https://www.rottentomatoes.com/api/private',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Accept: 'application/json',
|
||||
},
|
||||
});
|
||||
super(
|
||||
'https://www.rottentomatoes.com/api/private',
|
||||
{},
|
||||
{
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Accept: 'application/json',
|
||||
},
|
||||
nodeCache: cacheManager.getCache('rt').data,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -85,33 +88,30 @@ class RottenTomatoes {
|
||||
year: number
|
||||
): Promise<RTRating | null> {
|
||||
try {
|
||||
const response = await this.axios.get<RTMovieSearchResponse>(
|
||||
'/v1.0/movies',
|
||||
{
|
||||
params: { q: name },
|
||||
}
|
||||
);
|
||||
const data = await this.get<RTMovieSearchResponse>('/v1.0/movies', {
|
||||
params: { q: name },
|
||||
});
|
||||
|
||||
// First, attempt to match exact name and year
|
||||
let movie = response.data.movies.find(
|
||||
let movie = data.movies.find(
|
||||
(movie) => movie.year === year && movie.title === name
|
||||
);
|
||||
|
||||
// If we don't find a movie, try to match partial name and year
|
||||
if (!movie) {
|
||||
movie = response.data.movies.find(
|
||||
movie = data.movies.find(
|
||||
(movie) => movie.year === year && movie.title.includes(name)
|
||||
);
|
||||
}
|
||||
|
||||
// If we still dont find a movie, try to match just on year
|
||||
if (!movie) {
|
||||
movie = response.data.movies.find((movie) => movie.year === year);
|
||||
movie = data.movies.find((movie) => movie.year === year);
|
||||
}
|
||||
|
||||
// One last try, try exact name match only
|
||||
if (!movie) {
|
||||
movie = response.data.movies.find((movie) => movie.title === name);
|
||||
movie = data.movies.find((movie) => movie.title === name);
|
||||
}
|
||||
|
||||
if (!movie) {
|
||||
@@ -139,19 +139,14 @@ class RottenTomatoes {
|
||||
year?: number
|
||||
): Promise<RTRating | null> {
|
||||
try {
|
||||
const response = await this.axios.get<RTMultiSearchResponse>(
|
||||
'/v2.0/search/',
|
||||
{
|
||||
params: { q: name, limit: 10 },
|
||||
}
|
||||
);
|
||||
const data = await this.get<RTMultiSearchResponse>('/v2.0/search/', {
|
||||
params: { q: name, limit: 10 },
|
||||
});
|
||||
|
||||
let tvshow: RTTvSearchResult | undefined = response.data.tvSeries[0];
|
||||
let tvshow: RTTvSearchResult | undefined = data.tvSeries[0];
|
||||
|
||||
if (year) {
|
||||
tvshow = response.data.tvSeries.find(
|
||||
(series) => series.startYear === year
|
||||
);
|
||||
tvshow = data.tvSeries.find((series) => series.startYear === year);
|
||||
}
|
||||
|
||||
if (!tvshow) {
|
||||
|
@@ -1,6 +1,7 @@
|
||||
import Axios, { AxiosInstance } from 'axios';
|
||||
import cacheManager from '../lib/cache';
|
||||
import { SonarrSettings } from '../lib/settings';
|
||||
import logger from '../logger';
|
||||
import ExternalAPI from './externalapi';
|
||||
|
||||
interface SonarrSeason {
|
||||
seasonNumber: number;
|
||||
@@ -119,21 +120,23 @@ interface AddSeriesOptions {
|
||||
searchNow?: boolean;
|
||||
}
|
||||
|
||||
class SonarrAPI {
|
||||
class SonarrAPI extends ExternalAPI {
|
||||
static buildSonarrUrl(sonarrSettings: SonarrSettings, path?: string): string {
|
||||
return `${sonarrSettings.useSsl ? 'https' : 'http'}://${
|
||||
sonarrSettings.hostname
|
||||
}:${sonarrSettings.port}${sonarrSettings.baseUrl ?? ''}${path}`;
|
||||
}
|
||||
|
||||
private axios: AxiosInstance;
|
||||
constructor({ url, apiKey }: { url: string; apiKey: string }) {
|
||||
this.axios = Axios.create({
|
||||
baseURL: url,
|
||||
params: {
|
||||
super(
|
||||
url,
|
||||
{
|
||||
apikey: apiKey,
|
||||
},
|
||||
});
|
||||
{
|
||||
nodeCache: cacheManager.getCache('sonarr').data,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
public async getSeries(): Promise<SonarrSeries[]> {
|
||||
@@ -280,9 +283,13 @@ class SonarrAPI {
|
||||
|
||||
public async getProfiles(): Promise<SonarrProfile[]> {
|
||||
try {
|
||||
const response = await this.axios.get<SonarrProfile[]>('/profile');
|
||||
const data = await this.getRolling<SonarrProfile[]>(
|
||||
'/profile',
|
||||
undefined,
|
||||
3600
|
||||
);
|
||||
|
||||
return response.data;
|
||||
return data;
|
||||
} catch (e) {
|
||||
logger.error('Something went wrong while retrieving Sonarr profiles.', {
|
||||
label: 'Sonarr API',
|
||||
@@ -294,9 +301,13 @@ class SonarrAPI {
|
||||
|
||||
public async getRootFolders(): Promise<SonarrRootFolder[]> {
|
||||
try {
|
||||
const response = await this.axios.get<SonarrRootFolder[]>('/rootfolder');
|
||||
const data = await this.getRolling<SonarrRootFolder[]>(
|
||||
'/rootfolder',
|
||||
undefined,
|
||||
3600
|
||||
);
|
||||
|
||||
return response.data;
|
||||
return data;
|
||||
} catch (e) {
|
||||
logger.error(
|
||||
'Something went wrong while retrieving Sonarr root folders.',
|
||||
|
@@ -1,945 +0,0 @@
|
||||
import axios, { AxiosInstance } from 'axios';
|
||||
|
||||
export const ANIME_KEYWORD_ID = 210024;
|
||||
|
||||
interface SearchOptions {
|
||||
query: string;
|
||||
page?: number;
|
||||
includeAdult?: boolean;
|
||||
language?: string;
|
||||
}
|
||||
|
||||
interface DiscoverMovieOptions {
|
||||
page?: number;
|
||||
includeAdult?: boolean;
|
||||
language?: string;
|
||||
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;
|
||||
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';
|
||||
}
|
||||
|
||||
interface TmdbMediaResult {
|
||||
id: number;
|
||||
media_type: string;
|
||||
popularity: number;
|
||||
poster_path?: string;
|
||||
backdrop_path?: string;
|
||||
vote_count: number;
|
||||
vote_average: number;
|
||||
genre_ids: number[];
|
||||
overview: string;
|
||||
original_language: string;
|
||||
}
|
||||
|
||||
export interface TmdbMovieResult extends TmdbMediaResult {
|
||||
media_type: 'movie';
|
||||
title: string;
|
||||
original_title: string;
|
||||
release_date: string;
|
||||
adult: boolean;
|
||||
video: boolean;
|
||||
}
|
||||
|
||||
export interface TmdbTvResult extends TmdbMediaResult {
|
||||
media_type: 'tv';
|
||||
name: string;
|
||||
original_name: string;
|
||||
origin_country: string[];
|
||||
first_air_date: string;
|
||||
}
|
||||
|
||||
export interface TmdbPersonResult {
|
||||
id: number;
|
||||
name: string;
|
||||
popularity: number;
|
||||
profile_path?: string;
|
||||
adult: boolean;
|
||||
media_type: 'person';
|
||||
known_for: (TmdbMovieResult | TmdbTvResult)[];
|
||||
}
|
||||
|
||||
interface TmdbPaginatedResponse {
|
||||
page: number;
|
||||
total_results: number;
|
||||
total_pages: number;
|
||||
}
|
||||
|
||||
interface TmdbSearchMultiResponse extends TmdbPaginatedResponse {
|
||||
results: (TmdbMovieResult | TmdbTvResult | TmdbPersonResult)[];
|
||||
}
|
||||
|
||||
interface TmdbSearchMovieResponse extends TmdbPaginatedResponse {
|
||||
results: TmdbMovieResult[];
|
||||
}
|
||||
|
||||
interface TmdbSearchTvResponse extends TmdbPaginatedResponse {
|
||||
results: TmdbTvResult[];
|
||||
}
|
||||
|
||||
interface TmdbUpcomingMoviesResponse extends TmdbPaginatedResponse {
|
||||
dates: {
|
||||
maximum: string;
|
||||
minimum: string;
|
||||
};
|
||||
results: TmdbMovieResult[];
|
||||
}
|
||||
|
||||
interface TmdbExternalIdResponse {
|
||||
movie_results: TmdbMovieResult[];
|
||||
tv_results: TmdbTvResult[];
|
||||
}
|
||||
|
||||
export interface TmdbCreditCast {
|
||||
cast_id: number;
|
||||
character: string;
|
||||
credit_id: string;
|
||||
gender?: number;
|
||||
id: number;
|
||||
name: string;
|
||||
order: number;
|
||||
profile_path?: string;
|
||||
}
|
||||
|
||||
export interface TmdbAggregateCreditCast extends TmdbCreditCast {
|
||||
roles: {
|
||||
credit_id: string;
|
||||
character: string;
|
||||
episode_count: number;
|
||||
}[];
|
||||
}
|
||||
|
||||
export interface TmdbCreditCrew {
|
||||
credit_id: string;
|
||||
gender?: number;
|
||||
id: number;
|
||||
name: string;
|
||||
profile_path?: string;
|
||||
job: string;
|
||||
department: string;
|
||||
}
|
||||
|
||||
export interface TmdbExternalIds {
|
||||
imdb_id?: string;
|
||||
freebase_mid?: string;
|
||||
freebase_id?: string;
|
||||
tvdb_id?: number;
|
||||
tvrage_id?: string;
|
||||
facebook_id?: string;
|
||||
instagram_id?: string;
|
||||
twitter_id?: string;
|
||||
}
|
||||
|
||||
export interface TmdbMovieDetails {
|
||||
id: number;
|
||||
imdb_id?: string;
|
||||
adult: boolean;
|
||||
backdrop_path?: string;
|
||||
poster_path?: string;
|
||||
budget: number;
|
||||
genres: {
|
||||
id: number;
|
||||
name: string;
|
||||
}[];
|
||||
homepage?: string;
|
||||
original_language: string;
|
||||
original_title: string;
|
||||
overview?: string;
|
||||
popularity: number;
|
||||
production_companies: {
|
||||
id: number;
|
||||
name: string;
|
||||
logo_path?: string;
|
||||
origin_country: string;
|
||||
}[];
|
||||
production_countries: {
|
||||
iso_3166_1: string;
|
||||
name: string;
|
||||
}[];
|
||||
release_date: string;
|
||||
revenue: number;
|
||||
runtime?: number;
|
||||
spoken_languages: {
|
||||
iso_639_1: string;
|
||||
name: string;
|
||||
}[];
|
||||
status: string;
|
||||
tagline?: string;
|
||||
title: string;
|
||||
video: boolean;
|
||||
vote_average: number;
|
||||
vote_count: number;
|
||||
credits: {
|
||||
cast: TmdbCreditCast[];
|
||||
crew: TmdbCreditCrew[];
|
||||
};
|
||||
belongs_to_collection?: {
|
||||
id: number;
|
||||
name: string;
|
||||
poster_path?: string;
|
||||
backdrop_path?: string;
|
||||
};
|
||||
external_ids: TmdbExternalIds;
|
||||
videos: TmdbVideoResult;
|
||||
}
|
||||
|
||||
export interface TmdbVideo {
|
||||
id: string;
|
||||
key: string;
|
||||
name: string;
|
||||
site: 'YouTube';
|
||||
size: number;
|
||||
type:
|
||||
| 'Clip'
|
||||
| 'Teaser'
|
||||
| 'Trailer'
|
||||
| 'Featurette'
|
||||
| 'Opening Credits'
|
||||
| 'Behind the Scenes'
|
||||
| 'Bloopers';
|
||||
}
|
||||
|
||||
export interface TmdbTvEpisodeResult {
|
||||
id: number;
|
||||
air_date: string;
|
||||
episode_number: number;
|
||||
name: string;
|
||||
overview: string;
|
||||
production_code: string;
|
||||
season_number: number;
|
||||
show_id: number;
|
||||
still_path: string;
|
||||
vote_average: number;
|
||||
vote_cuont: number;
|
||||
}
|
||||
|
||||
export interface TmdbTvSeasonResult {
|
||||
id: number;
|
||||
air_date: string;
|
||||
episode_count: number;
|
||||
name: string;
|
||||
overview: string;
|
||||
poster_path?: string;
|
||||
season_number: number;
|
||||
}
|
||||
|
||||
export interface TmdbTvDetails {
|
||||
id: number;
|
||||
backdrop_path?: string;
|
||||
created_by: {
|
||||
id: number;
|
||||
credit_id: string;
|
||||
name: string;
|
||||
gender: number;
|
||||
profile_path?: string;
|
||||
}[];
|
||||
episode_run_time: number[];
|
||||
first_air_date: string;
|
||||
genres: {
|
||||
id: number;
|
||||
name: string;
|
||||
}[];
|
||||
homepage: string;
|
||||
in_production: boolean;
|
||||
languages: string[];
|
||||
last_air_date: string;
|
||||
last_episode_to_air?: TmdbTvEpisodeResult;
|
||||
name: string;
|
||||
next_episode_to_air?: TmdbTvEpisodeResult;
|
||||
networks: {
|
||||
id: number;
|
||||
name: string;
|
||||
logo_path: string;
|
||||
origin_country: string;
|
||||
}[];
|
||||
number_of_episodes: number;
|
||||
number_of_seasons: number;
|
||||
origin_country: string[];
|
||||
original_language: string;
|
||||
original_name: string;
|
||||
overview: string;
|
||||
popularity: number;
|
||||
poster_path?: string;
|
||||
production_companies: {
|
||||
id: number;
|
||||
logo_path?: string;
|
||||
name: string;
|
||||
origin_country: string;
|
||||
}[];
|
||||
spoken_languages: {
|
||||
english_name: string;
|
||||
iso_639_1: string;
|
||||
name: string;
|
||||
}[];
|
||||
seasons: TmdbTvSeasonResult[];
|
||||
status: string;
|
||||
type: string;
|
||||
vote_average: number;
|
||||
vote_count: number;
|
||||
aggregate_credits: {
|
||||
cast: TmdbAggregateCreditCast[];
|
||||
};
|
||||
credits: {
|
||||
crew: TmdbCreditCrew[];
|
||||
};
|
||||
external_ids: TmdbExternalIds;
|
||||
keywords: {
|
||||
results: TmdbKeyword[];
|
||||
};
|
||||
videos: TmdbVideoResult;
|
||||
}
|
||||
|
||||
export interface TmdbVideoResult {
|
||||
results: TmdbVideo[];
|
||||
}
|
||||
|
||||
export interface TmdbKeyword {
|
||||
id: number;
|
||||
name: string;
|
||||
}
|
||||
|
||||
export interface TmdbPersonDetail {
|
||||
id: number;
|
||||
name: string;
|
||||
deathday: string;
|
||||
known_for_department: string;
|
||||
also_known_as?: string[];
|
||||
gender: number;
|
||||
biography: string;
|
||||
popularity: string;
|
||||
place_of_birth?: string;
|
||||
profile_path?: string;
|
||||
adult: boolean;
|
||||
imdb_id?: string;
|
||||
homepage?: string;
|
||||
}
|
||||
|
||||
export interface TmdbPersonCredit {
|
||||
id: number;
|
||||
original_language: string;
|
||||
episode_count: number;
|
||||
overview: string;
|
||||
origin_country: string[];
|
||||
original_name: string;
|
||||
vote_count: number;
|
||||
name: string;
|
||||
media_type?: string;
|
||||
popularity: number;
|
||||
credit_id: string;
|
||||
backdrop_path?: string;
|
||||
first_air_date: string;
|
||||
vote_average: number;
|
||||
genre_ids?: number[];
|
||||
poster_path?: string;
|
||||
original_title: string;
|
||||
video?: boolean;
|
||||
title: string;
|
||||
adult: boolean;
|
||||
release_date: string;
|
||||
}
|
||||
export interface TmdbPersonCreditCast extends TmdbPersonCredit {
|
||||
character: string;
|
||||
}
|
||||
|
||||
export interface TmdbPersonCreditCrew extends TmdbPersonCredit {
|
||||
department: string;
|
||||
job: string;
|
||||
}
|
||||
|
||||
export interface TmdbPersonCombinedCredits {
|
||||
id: number;
|
||||
cast: TmdbPersonCreditCast[];
|
||||
crew: TmdbPersonCreditCrew[];
|
||||
}
|
||||
|
||||
export interface TmdbSeasonWithEpisodes extends TmdbTvSeasonResult {
|
||||
episodes: TmdbTvEpisodeResult[];
|
||||
external_ids: TmdbExternalIds;
|
||||
}
|
||||
|
||||
export interface TmdbCollection {
|
||||
id: number;
|
||||
name: string;
|
||||
overview?: string;
|
||||
poster_path?: string;
|
||||
backdrop_path?: string;
|
||||
parts: TmdbMovieResult[];
|
||||
}
|
||||
|
||||
class TheMovieDb {
|
||||
private apiKey = 'db55323b8d3e4154498498a75642b381';
|
||||
private axios: AxiosInstance;
|
||||
|
||||
constructor() {
|
||||
this.axios = axios.create({
|
||||
baseURL: 'https://api.themoviedb.org/3',
|
||||
params: {
|
||||
api_key: this.apiKey,
|
||||
},
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Accept: 'application/json',
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
public searchMulti = async ({
|
||||
query,
|
||||
page = 1,
|
||||
includeAdult = false,
|
||||
language = 'en-US',
|
||||
}: SearchOptions): Promise<TmdbSearchMultiResponse> => {
|
||||
try {
|
||||
const response = await this.axios.get('/search/multi', {
|
||||
params: { query, page, include_adult: includeAdult, language },
|
||||
});
|
||||
|
||||
return response.data;
|
||||
} catch (e) {
|
||||
return {
|
||||
page: 1,
|
||||
results: [],
|
||||
total_pages: 1,
|
||||
total_results: 0,
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
public getPerson = async ({
|
||||
personId,
|
||||
language = 'en-US',
|
||||
}: {
|
||||
personId: number;
|
||||
language?: string;
|
||||
}): Promise<TmdbPersonDetail> => {
|
||||
try {
|
||||
const response = await this.axios.get<TmdbPersonDetail>(
|
||||
`/person/${personId}`,
|
||||
{
|
||||
params: { language },
|
||||
}
|
||||
);
|
||||
|
||||
return response.data;
|
||||
} catch (e) {
|
||||
throw new Error(`[TMDB] Failed to fetch person details: ${e.message}`);
|
||||
}
|
||||
};
|
||||
|
||||
public getPersonCombinedCredits = async ({
|
||||
personId,
|
||||
language = 'en-US',
|
||||
}: {
|
||||
personId: number;
|
||||
language?: string;
|
||||
}): Promise<TmdbPersonCombinedCredits> => {
|
||||
try {
|
||||
const response = await this.axios.get<TmdbPersonCombinedCredits>(
|
||||
`/person/${personId}/combined_credits`,
|
||||
{
|
||||
params: { language },
|
||||
}
|
||||
);
|
||||
|
||||
return response.data;
|
||||
} catch (e) {
|
||||
throw new Error(
|
||||
`[TMDB] Failed to fetch person combined credits: ${e.message}`
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
public getMovie = async ({
|
||||
movieId,
|
||||
language = 'en-US',
|
||||
}: {
|
||||
movieId: number;
|
||||
language?: string;
|
||||
}): Promise<TmdbMovieDetails> => {
|
||||
try {
|
||||
const response = await this.axios.get<TmdbMovieDetails>(
|
||||
`/movie/${movieId}`,
|
||||
{
|
||||
params: {
|
||||
language,
|
||||
append_to_response: 'credits,external_ids,videos',
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
return response.data;
|
||||
} catch (e) {
|
||||
throw new Error(`[TMDB] Failed to fetch movie details: ${e.message}`);
|
||||
}
|
||||
};
|
||||
|
||||
public getTvShow = async ({
|
||||
tvId,
|
||||
language = 'en-US',
|
||||
}: {
|
||||
tvId: number;
|
||||
language?: string;
|
||||
}): Promise<TmdbTvDetails> => {
|
||||
try {
|
||||
const response = await this.axios.get<TmdbTvDetails>(`/tv/${tvId}`, {
|
||||
params: {
|
||||
language,
|
||||
append_to_response:
|
||||
'aggregate_credits,credits,external_ids,keywords,videos',
|
||||
},
|
||||
});
|
||||
|
||||
return response.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<TmdbSeasonWithEpisodes> => {
|
||||
try {
|
||||
const response = await this.axios.get<TmdbSeasonWithEpisodes>(
|
||||
`/tv/${tvId}/season/${seasonNumber}`,
|
||||
{
|
||||
params: {
|
||||
language,
|
||||
append_to_response: 'external_ids',
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
return response.data;
|
||||
} catch (e) {
|
||||
throw new Error(`[TMDB] Failed to fetch tv show details: ${e.message}`);
|
||||
}
|
||||
};
|
||||
|
||||
public async getMovieRecommendations({
|
||||
movieId,
|
||||
page = 1,
|
||||
language = 'en-US',
|
||||
}: {
|
||||
movieId: number;
|
||||
page?: number;
|
||||
language?: string;
|
||||
}): Promise<TmdbSearchMovieResponse> {
|
||||
try {
|
||||
const response = await this.axios.get<TmdbSearchMovieResponse>(
|
||||
`/movie/${movieId}/recommendations`,
|
||||
{
|
||||
params: {
|
||||
page,
|
||||
language,
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
return response.data;
|
||||
} catch (e) {
|
||||
throw new Error(`[TMDB] Failed to fetch discover movies: ${e.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
public async getMovieSimilar({
|
||||
movieId,
|
||||
page = 1,
|
||||
language = 'en-US',
|
||||
}: {
|
||||
movieId: number;
|
||||
page?: number;
|
||||
language?: string;
|
||||
}): Promise<TmdbSearchMovieResponse> {
|
||||
try {
|
||||
const response = await this.axios.get<TmdbSearchMovieResponse>(
|
||||
`/movie/${movieId}/similar`,
|
||||
{
|
||||
params: {
|
||||
page,
|
||||
language,
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
return response.data;
|
||||
} catch (e) {
|
||||
throw new Error(`[TMDB] Failed to fetch discover movies: ${e.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
public async getMoviesByKeyword({
|
||||
keywordId,
|
||||
page = 1,
|
||||
language = 'en-US',
|
||||
}: {
|
||||
keywordId: number;
|
||||
page?: number;
|
||||
language?: string;
|
||||
}): Promise<TmdbSearchMovieResponse> {
|
||||
try {
|
||||
const response = await this.axios.get<TmdbSearchMovieResponse>(
|
||||
`/keyword/${keywordId}/movies`,
|
||||
{
|
||||
params: {
|
||||
page,
|
||||
language,
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
return response.data;
|
||||
} catch (e) {
|
||||
throw new Error(`[TMDB] Failed to fetch movies by keyword: ${e.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
public async getTvRecommendations({
|
||||
tvId,
|
||||
page = 1,
|
||||
language = 'en-US',
|
||||
}: {
|
||||
tvId: number;
|
||||
page?: number;
|
||||
language?: string;
|
||||
}): Promise<TmdbSearchTvResponse> {
|
||||
try {
|
||||
const response = await this.axios.get<TmdbSearchTvResponse>(
|
||||
`/tv/${tvId}/recommendations`,
|
||||
{
|
||||
params: {
|
||||
page,
|
||||
language,
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
return response.data;
|
||||
} catch (e) {
|
||||
throw new Error(
|
||||
`[TMDB] Failed to fetch tv recommendations: ${e.message}`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
public async getTvSimilar({
|
||||
tvId,
|
||||
page = 1,
|
||||
language = 'en-US',
|
||||
}: {
|
||||
tvId: number;
|
||||
page?: number;
|
||||
language?: string;
|
||||
}): Promise<TmdbSearchTvResponse> {
|
||||
try {
|
||||
const response = await this.axios.get<TmdbSearchTvResponse>(
|
||||
`/tv/${tvId}/similar`,
|
||||
{
|
||||
params: {
|
||||
page,
|
||||
language,
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
return response.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-US',
|
||||
}: DiscoverMovieOptions = {}): Promise<TmdbSearchMovieResponse> => {
|
||||
try {
|
||||
const response = await this.axios.get<TmdbSearchMovieResponse>(
|
||||
'/discover/movie',
|
||||
{
|
||||
params: {
|
||||
sort_by: sortBy,
|
||||
page,
|
||||
include_adult: includeAdult,
|
||||
language,
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
return response.data;
|
||||
} catch (e) {
|
||||
throw new Error(`[TMDB] Failed to fetch discover movies: ${e.message}`);
|
||||
}
|
||||
};
|
||||
|
||||
public getDiscoverTv = async ({
|
||||
sortBy = 'popularity.desc',
|
||||
page = 1,
|
||||
language = 'en-US',
|
||||
}: DiscoverTvOptions = {}): Promise<TmdbSearchTvResponse> => {
|
||||
try {
|
||||
const response = await this.axios.get<TmdbSearchTvResponse>(
|
||||
'/discover/tv',
|
||||
{
|
||||
params: {
|
||||
sort_by: sortBy,
|
||||
page,
|
||||
language,
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
return response.data;
|
||||
} catch (e) {
|
||||
throw new Error(`[TMDB] Failed to fetch discover tv: ${e.message}`);
|
||||
}
|
||||
};
|
||||
|
||||
public getUpcomingMovies = async ({
|
||||
page = 1,
|
||||
language = 'en-US',
|
||||
}: {
|
||||
page: number;
|
||||
language: string;
|
||||
}): Promise<TmdbUpcomingMoviesResponse> => {
|
||||
try {
|
||||
const response = await this.axios.get<TmdbUpcomingMoviesResponse>(
|
||||
'/movie/upcoming',
|
||||
{
|
||||
params: {
|
||||
page,
|
||||
language,
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
return response.data;
|
||||
} catch (e) {
|
||||
throw new Error(`[TMDB] Failed to fetch upcoming movies: ${e.message}`);
|
||||
}
|
||||
};
|
||||
|
||||
public getAllTrending = async ({
|
||||
page = 1,
|
||||
timeWindow = 'day',
|
||||
language = 'en-US',
|
||||
}: {
|
||||
page?: number;
|
||||
timeWindow?: 'day' | 'week';
|
||||
language?: string;
|
||||
} = {}): Promise<TmdbSearchMultiResponse> => {
|
||||
try {
|
||||
const response = await this.axios.get<TmdbSearchMultiResponse>(
|
||||
`/trending/all/${timeWindow}`,
|
||||
{
|
||||
params: {
|
||||
page,
|
||||
language,
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
return response.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<TmdbSearchMovieResponse> => {
|
||||
try {
|
||||
const response = await this.axios.get<TmdbSearchMovieResponse>(
|
||||
`/trending/movie/${timeWindow}`,
|
||||
{
|
||||
params: {
|
||||
page,
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
return response.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<TmdbSearchTvResponse> => {
|
||||
try {
|
||||
const response = await this.axios.get<TmdbSearchTvResponse>(
|
||||
`/trending/tv/${timeWindow}`,
|
||||
{
|
||||
params: {
|
||||
page,
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
return response.data;
|
||||
} catch (e) {
|
||||
throw new Error(`[TMDB] Failed to fetch all trending: ${e.message}`);
|
||||
}
|
||||
};
|
||||
|
||||
public async getByExternalId({
|
||||
externalId,
|
||||
type,
|
||||
language = 'en-US',
|
||||
}:
|
||||
| {
|
||||
externalId: string;
|
||||
type: 'imdb';
|
||||
language?: string;
|
||||
}
|
||||
| {
|
||||
externalId: number;
|
||||
type: 'tvdb';
|
||||
language?: string;
|
||||
}): Promise<TmdbExternalIdResponse> {
|
||||
try {
|
||||
const response = await this.axios.get<TmdbExternalIdResponse>(
|
||||
`/find/${externalId}`,
|
||||
{
|
||||
params: {
|
||||
external_source: type === 'imdb' ? 'imdb_id' : 'tvdb_id',
|
||||
language,
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
return response.data;
|
||||
} catch (e) {
|
||||
throw new Error(`[TMDB] Failed to find by external ID: ${e.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
public async getMovieByImdbId({
|
||||
imdbId,
|
||||
language = 'en-US',
|
||||
}: {
|
||||
imdbId: string;
|
||||
language?: string;
|
||||
}): Promise<TmdbMovieDetails> {
|
||||
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;
|
||||
}
|
||||
|
||||
throw new Error(
|
||||
'[TMDB] Failed to find a title with the provided IMDB id'
|
||||
);
|
||||
} catch (e) {
|
||||
throw new Error(
|
||||
`[TMDB] Failed to get movie by external imdb ID: ${e.message}`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
public async getShowByTvdbId({
|
||||
tvdbId,
|
||||
language = 'en-US',
|
||||
}: {
|
||||
tvdbId: number;
|
||||
language?: string;
|
||||
}): Promise<TmdbTvDetails> {
|
||||
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(
|
||||
`[TMDB] Failed to find a TV show with the provided TVDB 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-US',
|
||||
}: {
|
||||
collectionId: number;
|
||||
language?: string;
|
||||
}): Promise<TmdbCollection> {
|
||||
try {
|
||||
const response = await this.axios.get<TmdbCollection>(
|
||||
`/collection/${collectionId}`,
|
||||
{
|
||||
params: {
|
||||
language,
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
return response.data;
|
||||
} catch (e) {
|
||||
throw new Error(`[TMDB] Failed to fetch collection: ${e.message}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default TheMovieDb;
|
1
server/api/themoviedb/constants.ts
Normal file
1
server/api/themoviedb/constants.ts
Normal file
@@ -0,0 +1 @@
|
||||
export const ANIME_KEYWORD_ID = 210024;
|
599
server/api/themoviedb/index.ts
Normal file
599
server/api/themoviedb/index.ts
Normal file
@@ -0,0 +1,599 @@
|
||||
import cacheManager from '../../lib/cache';
|
||||
import ExternalAPI from '../externalapi';
|
||||
import {
|
||||
TmdbCollection,
|
||||
TmdbExternalIdResponse,
|
||||
TmdbMovieDetails,
|
||||
TmdbPersonCombinedCredits,
|
||||
TmdbPersonDetail,
|
||||
TmdbSearchMovieResponse,
|
||||
TmdbSearchMultiResponse,
|
||||
TmdbSearchTvResponse,
|
||||
TmdbSeasonWithEpisodes,
|
||||
TmdbTvDetails,
|
||||
TmdbUpcomingMoviesResponse,
|
||||
} from './interfaces';
|
||||
|
||||
interface SearchOptions {
|
||||
query: string;
|
||||
page?: number;
|
||||
includeAdult?: boolean;
|
||||
language?: string;
|
||||
}
|
||||
|
||||
interface DiscoverMovieOptions {
|
||||
page?: number;
|
||||
includeAdult?: boolean;
|
||||
language?: string;
|
||||
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;
|
||||
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 {
|
||||
constructor() {
|
||||
super(
|
||||
'https://api.themoviedb.org/3',
|
||||
{
|
||||
api_key: 'db55323b8d3e4154498498a75642b381',
|
||||
},
|
||||
{
|
||||
nodeCache: cacheManager.getCache('tmdb').data,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
public searchMulti = async ({
|
||||
query,
|
||||
page = 1,
|
||||
includeAdult = false,
|
||||
language = 'en',
|
||||
}: SearchOptions): Promise<TmdbSearchMultiResponse> => {
|
||||
try {
|
||||
const data = await this.get<TmdbSearchMultiResponse>('/search/multi', {
|
||||
params: { query, page, include_adult: includeAdult, language },
|
||||
});
|
||||
|
||||
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<TmdbPersonDetail> => {
|
||||
try {
|
||||
const data = await this.get<TmdbPersonDetail>(`/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<TmdbPersonCombinedCredits> => {
|
||||
try {
|
||||
const data = await this.get<TmdbPersonCombinedCredits>(
|
||||
`/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<TmdbMovieDetails> => {
|
||||
try {
|
||||
const data = await this.get<TmdbMovieDetails>(
|
||||
`/movie/${movieId}`,
|
||||
{
|
||||
params: {
|
||||
language,
|
||||
append_to_response: 'credits,external_ids,videos',
|
||||
},
|
||||
},
|
||||
900
|
||||
);
|
||||
|
||||
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<TmdbTvDetails> => {
|
||||
try {
|
||||
const data = await this.get<TmdbTvDetails>(
|
||||
`/tv/${tvId}`,
|
||||
{
|
||||
params: {
|
||||
language,
|
||||
append_to_response:
|
||||
'aggregate_credits,credits,external_ids,keywords,videos',
|
||||
},
|
||||
},
|
||||
900
|
||||
);
|
||||
|
||||
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<TmdbSeasonWithEpisodes> => {
|
||||
try {
|
||||
const data = await this.get<TmdbSeasonWithEpisodes>(
|
||||
`/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<TmdbSearchMovieResponse> {
|
||||
try {
|
||||
const data = await this.get<TmdbSearchMovieResponse>(
|
||||
`/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<TmdbSearchMovieResponse> {
|
||||
try {
|
||||
const data = await this.get<TmdbSearchMovieResponse>(
|
||||
`/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<TmdbSearchMovieResponse> {
|
||||
try {
|
||||
const data = await this.get<TmdbSearchMovieResponse>(
|
||||
`/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<TmdbSearchTvResponse> {
|
||||
try {
|
||||
const data = await this.get<TmdbSearchTvResponse>(
|
||||
`/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<TmdbSearchTvResponse> {
|
||||
try {
|
||||
const data = await this.get<TmdbSearchTvResponse>(`/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',
|
||||
}: DiscoverMovieOptions = {}): Promise<TmdbSearchMovieResponse> => {
|
||||
try {
|
||||
const data = await this.get<TmdbSearchMovieResponse>('/discover/movie', {
|
||||
params: {
|
||||
sort_by: sortBy,
|
||||
page,
|
||||
include_adult: includeAdult,
|
||||
language,
|
||||
},
|
||||
});
|
||||
|
||||
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',
|
||||
}: DiscoverTvOptions = {}): Promise<TmdbSearchTvResponse> => {
|
||||
try {
|
||||
const data = await this.get<TmdbSearchTvResponse>('/discover/tv', {
|
||||
params: {
|
||||
sort_by: sortBy,
|
||||
page,
|
||||
language,
|
||||
},
|
||||
});
|
||||
|
||||
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<TmdbUpcomingMoviesResponse> => {
|
||||
try {
|
||||
const data = await this.get<TmdbUpcomingMoviesResponse>(
|
||||
'/movie/upcoming',
|
||||
{
|
||||
params: {
|
||||
page,
|
||||
language,
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
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<TmdbSearchMultiResponse> => {
|
||||
try {
|
||||
const data = await this.get<TmdbSearchMultiResponse>(
|
||||
`/trending/all/${timeWindow}`,
|
||||
{
|
||||
params: {
|
||||
page,
|
||||
language,
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
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<TmdbSearchMovieResponse> => {
|
||||
try {
|
||||
const data = await this.get<TmdbSearchMovieResponse>(
|
||||
`/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<TmdbSearchTvResponse> => {
|
||||
try {
|
||||
const data = await this.get<TmdbSearchTvResponse>(
|
||||
`/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<TmdbExternalIdResponse> {
|
||||
try {
|
||||
const data = await this.get<TmdbExternalIdResponse>(
|
||||
`/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 getMovieByImdbId({
|
||||
imdbId,
|
||||
language = 'en',
|
||||
}: {
|
||||
imdbId: string;
|
||||
language?: string;
|
||||
}): Promise<TmdbMovieDetails> {
|
||||
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;
|
||||
}
|
||||
|
||||
throw new Error(
|
||||
'[TMDB] Failed to find a title with the provided IMDB id'
|
||||
);
|
||||
} catch (e) {
|
||||
throw new Error(
|
||||
`[TMDB] Failed to get movie by external imdb ID: ${e.message}`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
public async getShowByTvdbId({
|
||||
tvdbId,
|
||||
language = 'en',
|
||||
}: {
|
||||
tvdbId: number;
|
||||
language?: string;
|
||||
}): Promise<TmdbTvDetails> {
|
||||
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(
|
||||
`[TMDB] Failed to find a TV show with the provided TVDB 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<TmdbCollection> {
|
||||
try {
|
||||
const data = await this.get<TmdbCollection>(
|
||||
`/collection/${collectionId}`,
|
||||
{
|
||||
params: {
|
||||
language,
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
return data;
|
||||
} catch (e) {
|
||||
throw new Error(`[TMDB] Failed to fetch collection: ${e.message}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default TheMovieDb;
|
346
server/api/themoviedb/interfaces.ts
Normal file
346
server/api/themoviedb/interfaces.ts
Normal file
@@ -0,0 +1,346 @@
|
||||
interface TmdbMediaResult {
|
||||
id: number;
|
||||
media_type: string;
|
||||
popularity: number;
|
||||
poster_path?: string;
|
||||
backdrop_path?: string;
|
||||
vote_count: number;
|
||||
vote_average: number;
|
||||
genre_ids: number[];
|
||||
overview: string;
|
||||
original_language: string;
|
||||
}
|
||||
|
||||
export interface TmdbMovieResult extends TmdbMediaResult {
|
||||
media_type: 'movie';
|
||||
title: string;
|
||||
original_title: string;
|
||||
release_date: string;
|
||||
adult: boolean;
|
||||
video: boolean;
|
||||
}
|
||||
|
||||
export interface TmdbTvResult extends TmdbMediaResult {
|
||||
media_type: 'tv';
|
||||
name: string;
|
||||
original_name: string;
|
||||
origin_country: string[];
|
||||
first_air_date: string;
|
||||
}
|
||||
|
||||
export interface TmdbPersonResult {
|
||||
id: number;
|
||||
name: string;
|
||||
popularity: number;
|
||||
profile_path?: string;
|
||||
adult: boolean;
|
||||
media_type: 'person';
|
||||
known_for: (TmdbMovieResult | TmdbTvResult)[];
|
||||
}
|
||||
|
||||
interface TmdbPaginatedResponse {
|
||||
page: number;
|
||||
total_results: number;
|
||||
total_pages: number;
|
||||
}
|
||||
|
||||
export interface TmdbSearchMultiResponse extends TmdbPaginatedResponse {
|
||||
results: (TmdbMovieResult | TmdbTvResult | TmdbPersonResult)[];
|
||||
}
|
||||
|
||||
export interface TmdbSearchMovieResponse extends TmdbPaginatedResponse {
|
||||
results: TmdbMovieResult[];
|
||||
}
|
||||
|
||||
export interface TmdbSearchTvResponse extends TmdbPaginatedResponse {
|
||||
results: TmdbTvResult[];
|
||||
}
|
||||
|
||||
export interface TmdbUpcomingMoviesResponse extends TmdbPaginatedResponse {
|
||||
dates: {
|
||||
maximum: string;
|
||||
minimum: string;
|
||||
};
|
||||
results: TmdbMovieResult[];
|
||||
}
|
||||
|
||||
export interface TmdbExternalIdResponse {
|
||||
movie_results: TmdbMovieResult[];
|
||||
tv_results: TmdbTvResult[];
|
||||
}
|
||||
|
||||
export interface TmdbCreditCast {
|
||||
cast_id: number;
|
||||
character: string;
|
||||
credit_id: string;
|
||||
gender?: number;
|
||||
id: number;
|
||||
name: string;
|
||||
order: number;
|
||||
profile_path?: string;
|
||||
}
|
||||
|
||||
export interface TmdbAggregateCreditCast extends TmdbCreditCast {
|
||||
roles: {
|
||||
credit_id: string;
|
||||
character: string;
|
||||
episode_count: number;
|
||||
}[];
|
||||
}
|
||||
|
||||
export interface TmdbCreditCrew {
|
||||
credit_id: string;
|
||||
gender?: number;
|
||||
id: number;
|
||||
name: string;
|
||||
profile_path?: string;
|
||||
job: string;
|
||||
department: string;
|
||||
}
|
||||
|
||||
export interface TmdbExternalIds {
|
||||
imdb_id?: string;
|
||||
freebase_mid?: string;
|
||||
freebase_id?: string;
|
||||
tvdb_id?: number;
|
||||
tvrage_id?: string;
|
||||
facebook_id?: string;
|
||||
instagram_id?: string;
|
||||
twitter_id?: string;
|
||||
}
|
||||
|
||||
export interface TmdbMovieDetails {
|
||||
id: number;
|
||||
imdb_id?: string;
|
||||
adult: boolean;
|
||||
backdrop_path?: string;
|
||||
poster_path?: string;
|
||||
budget: number;
|
||||
genres: {
|
||||
id: number;
|
||||
name: string;
|
||||
}[];
|
||||
homepage?: string;
|
||||
original_language: string;
|
||||
original_title: string;
|
||||
overview?: string;
|
||||
popularity: number;
|
||||
production_companies: {
|
||||
id: number;
|
||||
name: string;
|
||||
logo_path?: string;
|
||||
origin_country: string;
|
||||
}[];
|
||||
production_countries: {
|
||||
iso_3166_1: string;
|
||||
name: string;
|
||||
}[];
|
||||
release_date: string;
|
||||
revenue: number;
|
||||
runtime?: number;
|
||||
spoken_languages: {
|
||||
iso_639_1: string;
|
||||
name: string;
|
||||
}[];
|
||||
status: string;
|
||||
tagline?: string;
|
||||
title: string;
|
||||
video: boolean;
|
||||
vote_average: number;
|
||||
vote_count: number;
|
||||
credits: {
|
||||
cast: TmdbCreditCast[];
|
||||
crew: TmdbCreditCrew[];
|
||||
};
|
||||
belongs_to_collection?: {
|
||||
id: number;
|
||||
name: string;
|
||||
poster_path?: string;
|
||||
backdrop_path?: string;
|
||||
};
|
||||
external_ids: TmdbExternalIds;
|
||||
videos: TmdbVideoResult;
|
||||
}
|
||||
|
||||
export interface TmdbVideo {
|
||||
id: string;
|
||||
key: string;
|
||||
name: string;
|
||||
site: 'YouTube';
|
||||
size: number;
|
||||
type:
|
||||
| 'Clip'
|
||||
| 'Teaser'
|
||||
| 'Trailer'
|
||||
| 'Featurette'
|
||||
| 'Opening Credits'
|
||||
| 'Behind the Scenes'
|
||||
| 'Bloopers';
|
||||
}
|
||||
|
||||
export interface TmdbTvEpisodeResult {
|
||||
id: number;
|
||||
air_date: string;
|
||||
episode_number: number;
|
||||
name: string;
|
||||
overview: string;
|
||||
production_code: string;
|
||||
season_number: number;
|
||||
show_id: number;
|
||||
still_path: string;
|
||||
vote_average: number;
|
||||
vote_cuont: number;
|
||||
}
|
||||
|
||||
export interface TmdbTvSeasonResult {
|
||||
id: number;
|
||||
air_date: string;
|
||||
episode_count: number;
|
||||
name: string;
|
||||
overview: string;
|
||||
poster_path?: string;
|
||||
season_number: number;
|
||||
}
|
||||
|
||||
export interface TmdbTvDetails {
|
||||
id: number;
|
||||
backdrop_path?: string;
|
||||
created_by: {
|
||||
id: number;
|
||||
credit_id: string;
|
||||
name: string;
|
||||
gender: number;
|
||||
profile_path?: string;
|
||||
}[];
|
||||
episode_run_time: number[];
|
||||
first_air_date: string;
|
||||
genres: {
|
||||
id: number;
|
||||
name: string;
|
||||
}[];
|
||||
homepage: string;
|
||||
in_production: boolean;
|
||||
languages: string[];
|
||||
last_air_date: string;
|
||||
last_episode_to_air?: TmdbTvEpisodeResult;
|
||||
name: string;
|
||||
next_episode_to_air?: TmdbTvEpisodeResult;
|
||||
networks: {
|
||||
id: number;
|
||||
name: string;
|
||||
logo_path: string;
|
||||
origin_country: string;
|
||||
}[];
|
||||
number_of_episodes: number;
|
||||
number_of_seasons: number;
|
||||
origin_country: string[];
|
||||
original_language: string;
|
||||
original_name: string;
|
||||
overview: string;
|
||||
popularity: number;
|
||||
poster_path?: string;
|
||||
production_companies: {
|
||||
id: number;
|
||||
logo_path?: string;
|
||||
name: string;
|
||||
origin_country: string;
|
||||
}[];
|
||||
spoken_languages: {
|
||||
english_name: string;
|
||||
iso_639_1: string;
|
||||
name: string;
|
||||
}[];
|
||||
seasons: TmdbTvSeasonResult[];
|
||||
status: string;
|
||||
type: string;
|
||||
vote_average: number;
|
||||
vote_count: number;
|
||||
aggregate_credits: {
|
||||
cast: TmdbAggregateCreditCast[];
|
||||
};
|
||||
credits: {
|
||||
crew: TmdbCreditCrew[];
|
||||
};
|
||||
external_ids: TmdbExternalIds;
|
||||
keywords: {
|
||||
results: TmdbKeyword[];
|
||||
};
|
||||
videos: TmdbVideoResult;
|
||||
}
|
||||
|
||||
export interface TmdbVideoResult {
|
||||
results: TmdbVideo[];
|
||||
}
|
||||
|
||||
export interface TmdbKeyword {
|
||||
id: number;
|
||||
name: string;
|
||||
}
|
||||
|
||||
export interface TmdbPersonDetail {
|
||||
id: number;
|
||||
name: string;
|
||||
deathday: string;
|
||||
known_for_department: string;
|
||||
also_known_as?: string[];
|
||||
gender: number;
|
||||
biography: string;
|
||||
popularity: string;
|
||||
place_of_birth?: string;
|
||||
profile_path?: string;
|
||||
adult: boolean;
|
||||
imdb_id?: string;
|
||||
homepage?: string;
|
||||
}
|
||||
|
||||
export interface TmdbPersonCredit {
|
||||
id: number;
|
||||
original_language: string;
|
||||
episode_count: number;
|
||||
overview: string;
|
||||
origin_country: string[];
|
||||
original_name: string;
|
||||
vote_count: number;
|
||||
name: string;
|
||||
media_type?: string;
|
||||
popularity: number;
|
||||
credit_id: string;
|
||||
backdrop_path?: string;
|
||||
first_air_date: string;
|
||||
vote_average: number;
|
||||
genre_ids?: number[];
|
||||
poster_path?: string;
|
||||
original_title: string;
|
||||
video?: boolean;
|
||||
title: string;
|
||||
adult: boolean;
|
||||
release_date: string;
|
||||
}
|
||||
export interface TmdbPersonCreditCast extends TmdbPersonCredit {
|
||||
character: string;
|
||||
}
|
||||
|
||||
export interface TmdbPersonCreditCrew extends TmdbPersonCredit {
|
||||
department: string;
|
||||
job: string;
|
||||
}
|
||||
|
||||
export interface TmdbPersonCombinedCredits {
|
||||
id: number;
|
||||
cast: TmdbPersonCreditCast[];
|
||||
crew: TmdbPersonCreditCrew[];
|
||||
}
|
||||
|
||||
export interface TmdbSeasonWithEpisodes extends TmdbTvSeasonResult {
|
||||
episodes: TmdbTvEpisodeResult[];
|
||||
external_ids: TmdbExternalIds;
|
||||
}
|
||||
|
||||
export interface TmdbCollection {
|
||||
id: number;
|
||||
name: string;
|
||||
overview?: string;
|
||||
poster_path?: string;
|
||||
backdrop_path?: string;
|
||||
parts: TmdbMovieResult[];
|
||||
}
|
Reference in New Issue
Block a user