feat(cache): external API cache (#786)
This commit is contained in:
@@ -36,6 +36,7 @@
|
|||||||
"intl": "^1.2.5",
|
"intl": "^1.2.5",
|
||||||
"lodash": "^4.17.20",
|
"lodash": "^4.17.20",
|
||||||
"next": "10.0.3",
|
"next": "10.0.3",
|
||||||
|
"node-cache": "^5.1.2",
|
||||||
"node-schedule": "^1.3.3",
|
"node-schedule": "^1.3.3",
|
||||||
"nodemailer": "^6.4.17",
|
"nodemailer": "^6.4.17",
|
||||||
"nookies": "^2.5.2",
|
"nookies": "^2.5.2",
|
||||||
|
|||||||
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 { RadarrSettings } from '../lib/settings';
|
||||||
import logger from '../logger';
|
import logger from '../logger';
|
||||||
|
import ExternalAPI from './externalapi';
|
||||||
|
|
||||||
interface RadarrMovieOptions {
|
interface RadarrMovieOptions {
|
||||||
title: string;
|
title: string;
|
||||||
@@ -73,21 +74,23 @@ interface QueueResponse {
|
|||||||
records: QueueItem[];
|
records: QueueItem[];
|
||||||
}
|
}
|
||||||
|
|
||||||
class RadarrAPI {
|
class RadarrAPI extends ExternalAPI {
|
||||||
static buildRadarrUrl(radarrSettings: RadarrSettings, path?: string): string {
|
static buildRadarrUrl(radarrSettings: RadarrSettings, path?: string): string {
|
||||||
return `${radarrSettings.useSsl ? 'https' : 'http'}://${
|
return `${radarrSettings.useSsl ? 'https' : 'http'}://${
|
||||||
radarrSettings.hostname
|
radarrSettings.hostname
|
||||||
}:${radarrSettings.port}${radarrSettings.baseUrl ?? ''}${path}`;
|
}:${radarrSettings.port}${radarrSettings.baseUrl ?? ''}${path}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
private axios: AxiosInstance;
|
|
||||||
constructor({ url, apiKey }: { url: string; apiKey: string }) {
|
constructor({ url, apiKey }: { url: string; apiKey: string }) {
|
||||||
this.axios = Axios.create({
|
super(
|
||||||
baseURL: url,
|
url,
|
||||||
params: {
|
{
|
||||||
apikey: apiKey,
|
apikey: apiKey,
|
||||||
},
|
},
|
||||||
});
|
{
|
||||||
|
nodeCache: cacheManager.getCache('radarr').data,
|
||||||
|
}
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
public getMovies = async (): Promise<RadarrMovie[]> => {
|
public getMovies = async (): Promise<RadarrMovie[]> => {
|
||||||
@@ -238,9 +241,13 @@ class RadarrAPI {
|
|||||||
|
|
||||||
public getProfiles = async (): Promise<RadarrProfile[]> => {
|
public getProfiles = async (): Promise<RadarrProfile[]> => {
|
||||||
try {
|
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) {
|
} catch (e) {
|
||||||
throw new Error(`[Radarr] Failed to retrieve profiles: ${e.message}`);
|
throw new Error(`[Radarr] Failed to retrieve profiles: ${e.message}`);
|
||||||
}
|
}
|
||||||
@@ -248,9 +255,13 @@ class RadarrAPI {
|
|||||||
|
|
||||||
public getRootFolders = async (): Promise<RadarrRootFolder[]> => {
|
public getRootFolders = async (): Promise<RadarrRootFolder[]> => {
|
||||||
try {
|
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) {
|
} catch (e) {
|
||||||
throw new Error(`[Radarr] Failed to retrieve root folders: ${e.message}`);
|
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 {
|
interface RTMovieOldSearchResult {
|
||||||
id: number;
|
id: number;
|
||||||
@@ -55,17 +56,19 @@ export interface RTRating {
|
|||||||
* Unfortunately, we need to do it by searching for the movie name, so it's
|
* Unfortunately, we need to do it by searching for the movie name, so it's
|
||||||
* not always accurate.
|
* not always accurate.
|
||||||
*/
|
*/
|
||||||
class RottenTomatoes {
|
class RottenTomatoes extends ExternalAPI {
|
||||||
private axios: AxiosInstance;
|
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
this.axios = axios.create({
|
super(
|
||||||
baseURL: 'https://www.rottentomatoes.com/api/private',
|
'https://www.rottentomatoes.com/api/private',
|
||||||
|
{},
|
||||||
|
{
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
Accept: 'application/json',
|
Accept: 'application/json',
|
||||||
},
|
},
|
||||||
});
|
nodeCache: cacheManager.getCache('rt').data,
|
||||||
|
}
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -85,33 +88,30 @@ class RottenTomatoes {
|
|||||||
year: number
|
year: number
|
||||||
): Promise<RTRating | null> {
|
): Promise<RTRating | null> {
|
||||||
try {
|
try {
|
||||||
const response = await this.axios.get<RTMovieSearchResponse>(
|
const data = await this.get<RTMovieSearchResponse>('/v1.0/movies', {
|
||||||
'/v1.0/movies',
|
|
||||||
{
|
|
||||||
params: { q: name },
|
params: { q: name },
|
||||||
}
|
});
|
||||||
);
|
|
||||||
|
|
||||||
// First, attempt to match exact name and year
|
// 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
|
(movie) => movie.year === year && movie.title === name
|
||||||
);
|
);
|
||||||
|
|
||||||
// If we don't find a movie, try to match partial name and year
|
// If we don't find a movie, try to match partial name and year
|
||||||
if (!movie) {
|
if (!movie) {
|
||||||
movie = response.data.movies.find(
|
movie = data.movies.find(
|
||||||
(movie) => movie.year === year && movie.title.includes(name)
|
(movie) => movie.year === year && movie.title.includes(name)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// If we still dont find a movie, try to match just on year
|
// If we still dont find a movie, try to match just on year
|
||||||
if (!movie) {
|
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
|
// One last try, try exact name match only
|
||||||
if (!movie) {
|
if (!movie) {
|
||||||
movie = response.data.movies.find((movie) => movie.title === name);
|
movie = data.movies.find((movie) => movie.title === name);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!movie) {
|
if (!movie) {
|
||||||
@@ -139,19 +139,14 @@ class RottenTomatoes {
|
|||||||
year?: number
|
year?: number
|
||||||
): Promise<RTRating | null> {
|
): Promise<RTRating | null> {
|
||||||
try {
|
try {
|
||||||
const response = await this.axios.get<RTMultiSearchResponse>(
|
const data = await this.get<RTMultiSearchResponse>('/v2.0/search/', {
|
||||||
'/v2.0/search/',
|
|
||||||
{
|
|
||||||
params: { q: name, limit: 10 },
|
params: { q: name, limit: 10 },
|
||||||
}
|
});
|
||||||
);
|
|
||||||
|
|
||||||
let tvshow: RTTvSearchResult | undefined = response.data.tvSeries[0];
|
let tvshow: RTTvSearchResult | undefined = data.tvSeries[0];
|
||||||
|
|
||||||
if (year) {
|
if (year) {
|
||||||
tvshow = response.data.tvSeries.find(
|
tvshow = data.tvSeries.find((series) => series.startYear === year);
|
||||||
(series) => series.startYear === year
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!tvshow) {
|
if (!tvshow) {
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import Axios, { AxiosInstance } from 'axios';
|
import cacheManager from '../lib/cache';
|
||||||
import { SonarrSettings } from '../lib/settings';
|
import { SonarrSettings } from '../lib/settings';
|
||||||
import logger from '../logger';
|
import logger from '../logger';
|
||||||
|
import ExternalAPI from './externalapi';
|
||||||
|
|
||||||
interface SonarrSeason {
|
interface SonarrSeason {
|
||||||
seasonNumber: number;
|
seasonNumber: number;
|
||||||
@@ -119,21 +120,23 @@ interface AddSeriesOptions {
|
|||||||
searchNow?: boolean;
|
searchNow?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
class SonarrAPI {
|
class SonarrAPI extends ExternalAPI {
|
||||||
static buildSonarrUrl(sonarrSettings: SonarrSettings, path?: string): string {
|
static buildSonarrUrl(sonarrSettings: SonarrSettings, path?: string): string {
|
||||||
return `${sonarrSettings.useSsl ? 'https' : 'http'}://${
|
return `${sonarrSettings.useSsl ? 'https' : 'http'}://${
|
||||||
sonarrSettings.hostname
|
sonarrSettings.hostname
|
||||||
}:${sonarrSettings.port}${sonarrSettings.baseUrl ?? ''}${path}`;
|
}:${sonarrSettings.port}${sonarrSettings.baseUrl ?? ''}${path}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
private axios: AxiosInstance;
|
|
||||||
constructor({ url, apiKey }: { url: string; apiKey: string }) {
|
constructor({ url, apiKey }: { url: string; apiKey: string }) {
|
||||||
this.axios = Axios.create({
|
super(
|
||||||
baseURL: url,
|
url,
|
||||||
params: {
|
{
|
||||||
apikey: apiKey,
|
apikey: apiKey,
|
||||||
},
|
},
|
||||||
});
|
{
|
||||||
|
nodeCache: cacheManager.getCache('sonarr').data,
|
||||||
|
}
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async getSeries(): Promise<SonarrSeries[]> {
|
public async getSeries(): Promise<SonarrSeries[]> {
|
||||||
@@ -280,9 +283,13 @@ class SonarrAPI {
|
|||||||
|
|
||||||
public async getProfiles(): Promise<SonarrProfile[]> {
|
public async getProfiles(): Promise<SonarrProfile[]> {
|
||||||
try {
|
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) {
|
} catch (e) {
|
||||||
logger.error('Something went wrong while retrieving Sonarr profiles.', {
|
logger.error('Something went wrong while retrieving Sonarr profiles.', {
|
||||||
label: 'Sonarr API',
|
label: 'Sonarr API',
|
||||||
@@ -294,9 +301,13 @@ class SonarrAPI {
|
|||||||
|
|
||||||
public async getRootFolders(): Promise<SonarrRootFolder[]> {
|
public async getRootFolders(): Promise<SonarrRootFolder[]> {
|
||||||
try {
|
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) {
|
} catch (e) {
|
||||||
logger.error(
|
logger.error(
|
||||||
'Something went wrong while retrieving Sonarr root folders.',
|
'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[];
|
||||||
|
}
|
||||||
@@ -15,7 +15,8 @@ import { User } from './User';
|
|||||||
import Media from './Media';
|
import Media from './Media';
|
||||||
import { MediaStatus, MediaRequestStatus, MediaType } from '../constants/media';
|
import { MediaStatus, MediaRequestStatus, MediaType } from '../constants/media';
|
||||||
import { getSettings } from '../lib/settings';
|
import { getSettings } from '../lib/settings';
|
||||||
import TheMovieDb, { ANIME_KEYWORD_ID } from '../api/themoviedb';
|
import TheMovieDb from '../api/themoviedb';
|
||||||
|
import { ANIME_KEYWORD_ID } from '../api/themoviedb/constants';
|
||||||
import RadarrAPI from '../api/radarr';
|
import RadarrAPI from '../api/radarr';
|
||||||
import logger from '../logger';
|
import logger from '../logger';
|
||||||
import SeasonRequest from './SeasonRequest';
|
import SeasonRequest from './SeasonRequest';
|
||||||
|
|||||||
@@ -1,10 +1,11 @@
|
|||||||
import { getRepository } from 'typeorm';
|
import { getRepository } from 'typeorm';
|
||||||
import { User } from '../../entity/User';
|
import { User } from '../../entity/User';
|
||||||
import PlexAPI, { PlexLibraryItem, PlexMetadata } from '../../api/plexapi';
|
import PlexAPI, { PlexLibraryItem, PlexMetadata } from '../../api/plexapi';
|
||||||
import TheMovieDb, {
|
import TheMovieDb from '../../api/themoviedb';
|
||||||
|
import {
|
||||||
TmdbMovieDetails,
|
TmdbMovieDetails,
|
||||||
TmdbTvDetails,
|
TmdbTvDetails,
|
||||||
} from '../../api/themoviedb';
|
} from '../../api/themoviedb/interfaces';
|
||||||
import Media from '../../entity/Media';
|
import Media from '../../entity/Media';
|
||||||
import { MediaStatus, MediaType } from '../../constants/media';
|
import { MediaStatus, MediaType } from '../../constants/media';
|
||||||
import logger from '../../logger';
|
import logger from '../../logger';
|
||||||
|
|||||||
@@ -2,7 +2,8 @@ import { uniqWith } from 'lodash';
|
|||||||
import { getRepository } from 'typeorm';
|
import { getRepository } from 'typeorm';
|
||||||
import { v4 as uuid } from 'uuid';
|
import { v4 as uuid } from 'uuid';
|
||||||
import SonarrAPI, { SonarrSeries } from '../../api/sonarr';
|
import SonarrAPI, { SonarrSeries } from '../../api/sonarr';
|
||||||
import TheMovieDb, { TmdbTvDetails } from '../../api/themoviedb';
|
import TheMovieDb from '../../api/themoviedb';
|
||||||
|
import { TmdbTvDetails } from '../../api/themoviedb/interfaces';
|
||||||
import { MediaStatus, MediaType } from '../../constants/media';
|
import { MediaStatus, MediaType } from '../../constants/media';
|
||||||
import Media from '../../entity/Media';
|
import Media from '../../entity/Media';
|
||||||
import Season from '../../entity/Season';
|
import Season from '../../entity/Season';
|
||||||
|
|||||||
56
server/lib/cache.ts
Normal file
56
server/lib/cache.ts
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
import NodeCache from 'node-cache';
|
||||||
|
|
||||||
|
type AvailableCacheIds = 'tmdb' | 'radarr' | 'sonarr' | 'rt';
|
||||||
|
|
||||||
|
interface Cache {
|
||||||
|
id: AvailableCacheIds;
|
||||||
|
data: NodeCache;
|
||||||
|
}
|
||||||
|
|
||||||
|
const DEFAULT_TTL = 300;
|
||||||
|
const DEFAULT_CHECK_PERIOD = 120;
|
||||||
|
|
||||||
|
class CacheManager {
|
||||||
|
private availableCaches: Record<AvailableCacheIds, Cache> = {
|
||||||
|
tmdb: {
|
||||||
|
id: 'tmdb',
|
||||||
|
data: new NodeCache({
|
||||||
|
stdTTL: DEFAULT_TTL,
|
||||||
|
checkperiod: DEFAULT_CHECK_PERIOD,
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
radarr: {
|
||||||
|
id: 'radarr',
|
||||||
|
data: new NodeCache({
|
||||||
|
stdTTL: DEFAULT_TTL,
|
||||||
|
checkperiod: DEFAULT_CHECK_PERIOD,
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
sonarr: {
|
||||||
|
id: 'sonarr',
|
||||||
|
data: new NodeCache({
|
||||||
|
stdTTL: DEFAULT_TTL,
|
||||||
|
checkperiod: DEFAULT_CHECK_PERIOD,
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
rt: {
|
||||||
|
id: 'rt',
|
||||||
|
data: new NodeCache({
|
||||||
|
stdTTL: 21600, // 12 hours TTL
|
||||||
|
checkperiod: 60 * 30, // 30 minutes check period
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
public getCache(id: AvailableCacheIds): Cache {
|
||||||
|
return this.availableCaches[id];
|
||||||
|
}
|
||||||
|
|
||||||
|
public getAllCaches(): Record<string, Cache> {
|
||||||
|
return this.availableCaches;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const cacheManager = new CacheManager();
|
||||||
|
|
||||||
|
export default cacheManager;
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import { TmdbCollection } from '../api/themoviedb';
|
import type { TmdbCollection } from '../api/themoviedb/interfaces';
|
||||||
import { MediaType } from '../constants/media';
|
import { MediaType } from '../constants/media';
|
||||||
import Media from '../entity/Media';
|
import Media from '../entity/Media';
|
||||||
import { mapMovieResult, MovieResult } from './Search';
|
import { mapMovieResult, MovieResult } from './Search';
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { TmdbMovieDetails } from '../api/themoviedb';
|
import type { TmdbMovieDetails } from '../api/themoviedb/interfaces';
|
||||||
import {
|
import {
|
||||||
ProductionCompany,
|
ProductionCompany,
|
||||||
Genre,
|
Genre,
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import {
|
import type {
|
||||||
TmdbPersonCreditCast,
|
TmdbPersonCreditCast,
|
||||||
TmdbPersonCreditCrew,
|
TmdbPersonCreditCrew,
|
||||||
TmdbPersonDetail,
|
TmdbPersonDetail,
|
||||||
} from '../api/themoviedb';
|
} from '../api/themoviedb/interfaces';
|
||||||
import Media from '../entity/Media';
|
import Media from '../entity/Media';
|
||||||
|
|
||||||
export interface PersonDetail {
|
export interface PersonDetail {
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import type {
|
|||||||
TmdbMovieResult,
|
TmdbMovieResult,
|
||||||
TmdbPersonResult,
|
TmdbPersonResult,
|
||||||
TmdbTvResult,
|
TmdbTvResult,
|
||||||
} from '../api/themoviedb';
|
} from '../api/themoviedb/interfaces';
|
||||||
import { MediaType as MainMediaType } from '../constants/media';
|
import { MediaType as MainMediaType } from '../constants/media';
|
||||||
import Media from '../entity/Media';
|
import Media from '../entity/Media';
|
||||||
|
|
||||||
|
|||||||
@@ -10,12 +10,12 @@ import {
|
|||||||
Keyword,
|
Keyword,
|
||||||
mapVideos,
|
mapVideos,
|
||||||
} from './common';
|
} from './common';
|
||||||
import {
|
import type {
|
||||||
TmdbTvEpisodeResult,
|
TmdbTvEpisodeResult,
|
||||||
TmdbTvSeasonResult,
|
TmdbTvSeasonResult,
|
||||||
TmdbTvDetails,
|
TmdbTvDetails,
|
||||||
TmdbSeasonWithEpisodes,
|
TmdbSeasonWithEpisodes,
|
||||||
} from '../api/themoviedb';
|
} from '../api/themoviedb/interfaces';
|
||||||
import type Media from '../entity/Media';
|
import type Media from '../entity/Media';
|
||||||
import { Video } from './Movie';
|
import { Video } from './Movie';
|
||||||
|
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
import {
|
import type {
|
||||||
TmdbCreditCast,
|
TmdbCreditCast,
|
||||||
TmdbAggregateCreditCast,
|
TmdbAggregateCreditCast,
|
||||||
TmdbCreditCrew,
|
TmdbCreditCrew,
|
||||||
TmdbExternalIds,
|
TmdbExternalIds,
|
||||||
TmdbVideo,
|
TmdbVideo,
|
||||||
TmdbVideoResult,
|
TmdbVideoResult,
|
||||||
} from '../api/themoviedb';
|
} from '../api/themoviedb/interfaces';
|
||||||
|
|
||||||
import { Video } from '../models/Movie';
|
import { Video } from '../models/Movie';
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import type {
|
|||||||
TmdbMovieResult,
|
TmdbMovieResult,
|
||||||
TmdbTvResult,
|
TmdbTvResult,
|
||||||
TmdbPersonResult,
|
TmdbPersonResult,
|
||||||
} from '../api/themoviedb';
|
} from '../api/themoviedb/interfaces';
|
||||||
|
|
||||||
export const isMovie = (
|
export const isMovie = (
|
||||||
movie: TmdbMovieResult | TmdbTvResult | TmdbPersonResult
|
movie: TmdbMovieResult | TmdbTvResult | TmdbPersonResult
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import { defineMessages, useIntl } from 'react-intl';
|
|||||||
import { MediaRequest } from '../../../server/entity/MediaRequest';
|
import { MediaRequest } from '../../../server/entity/MediaRequest';
|
||||||
import useSWR from 'swr';
|
import useSWR from 'swr';
|
||||||
import { useToasts } from 'react-toast-notifications';
|
import { useToasts } from 'react-toast-notifications';
|
||||||
import { ANIME_KEYWORD_ID } from '../../../server/api/themoviedb';
|
import { ANIME_KEYWORD_ID } from '../../../server/api/themoviedb/constants';
|
||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
import {
|
import {
|
||||||
MediaStatus,
|
MediaStatus,
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ import RTAudFresh from '../../assets/rt_aud_fresh.svg';
|
|||||||
import RTAudRotten from '../../assets/rt_aud_rotten.svg';
|
import RTAudRotten from '../../assets/rt_aud_rotten.svg';
|
||||||
import type { RTRating } from '../../../server/api/rottentomatoes';
|
import type { RTRating } from '../../../server/api/rottentomatoes';
|
||||||
import Head from 'next/head';
|
import Head from 'next/head';
|
||||||
import { ANIME_KEYWORD_ID } from '../../../server/api/themoviedb';
|
import { ANIME_KEYWORD_ID } from '../../../server/api/themoviedb/constants';
|
||||||
import ExternalLinkBlock from '../ExternalLinkBlock';
|
import ExternalLinkBlock from '../ExternalLinkBlock';
|
||||||
import { sortCrewPriority } from '../../utils/creditHelpers';
|
import { sortCrewPriority } from '../../utils/creditHelpers';
|
||||||
import { Crew } from '../../../server/models/common';
|
import { Crew } from '../../../server/models/common';
|
||||||
|
|||||||
12
yarn.lock
12
yarn.lock
@@ -4015,6 +4015,11 @@ clone-response@^1.0.2:
|
|||||||
dependencies:
|
dependencies:
|
||||||
mimic-response "^1.0.0"
|
mimic-response "^1.0.0"
|
||||||
|
|
||||||
|
clone@2.x:
|
||||||
|
version "2.1.2"
|
||||||
|
resolved "https://registry.yarnpkg.com/clone/-/clone-2.1.2.tgz#1b7f4b9f591f1e8f83670401600345a02887435f"
|
||||||
|
integrity sha1-G39Ln1kfHo+DZwQBYANFoCiHQ18=
|
||||||
|
|
||||||
clone@^1.0.2:
|
clone@^1.0.2:
|
||||||
version "1.0.4"
|
version "1.0.4"
|
||||||
resolved "https://registry.yarnpkg.com/clone/-/clone-1.0.4.tgz#da309cc263df15994c688ca902179ca3c7cd7c7e"
|
resolved "https://registry.yarnpkg.com/clone/-/clone-1.0.4.tgz#da309cc263df15994c688ca902179ca3c7cd7c7e"
|
||||||
@@ -9469,6 +9474,13 @@ node-addon-api@^3.0.2:
|
|||||||
resolved "https://registry.yarnpkg.com/node-addon-api/-/node-addon-api-3.0.2.tgz#04bc7b83fd845ba785bb6eae25bc857e1ef75681"
|
resolved "https://registry.yarnpkg.com/node-addon-api/-/node-addon-api-3.0.2.tgz#04bc7b83fd845ba785bb6eae25bc857e1ef75681"
|
||||||
integrity sha512-+D4s2HCnxPd5PjjI0STKwncjXTUKKqm74MDMz9OPXavjsGmjkvwgLtA5yoxJUdmpj52+2u+RrXgPipahKczMKg==
|
integrity sha512-+D4s2HCnxPd5PjjI0STKwncjXTUKKqm74MDMz9OPXavjsGmjkvwgLtA5yoxJUdmpj52+2u+RrXgPipahKczMKg==
|
||||||
|
|
||||||
|
node-cache@^5.1.2:
|
||||||
|
version "5.1.2"
|
||||||
|
resolved "https://registry.yarnpkg.com/node-cache/-/node-cache-5.1.2.tgz#f264dc2ccad0a780e76253a694e9fd0ed19c398d"
|
||||||
|
integrity sha512-t1QzWwnk4sjLWaQAS8CHgOJ+RAfmHpxFWmc36IWTiWHQfs0w5JDMBS1b1ZxQteo0vVVuWJvIUKHDkkeK7vIGCg==
|
||||||
|
dependencies:
|
||||||
|
clone "2.x"
|
||||||
|
|
||||||
node-emoji@^1.10.0, node-emoji@^1.8.1:
|
node-emoji@^1.10.0, node-emoji@^1.8.1:
|
||||||
version "1.10.0"
|
version "1.10.0"
|
||||||
resolved "https://registry.yarnpkg.com/node-emoji/-/node-emoji-1.10.0.tgz#8886abd25d9c7bb61802a658523d1f8d2a89b2da"
|
resolved "https://registry.yarnpkg.com/node-emoji/-/node-emoji-1.10.0.tgz#8886abd25d9c7bb61802a658523d1f8d2a89b2da"
|
||||||
|
|||||||
Reference in New Issue
Block a user