feat(cache): external API cache (#786)

pull/793/head
sct 3 years ago committed by GitHub
parent b239598e64
commit 20289b5960
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -36,6 +36,7 @@
"intl": "^1.2.5",
"lodash": "^4.17.20",
"next": "10.0.3",
"node-cache": "^5.1.2",
"node-schedule": "^1.3.3",
"nodemailer": "^6.4.17",
"nookies": "^2.5.2",

@ -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;

@ -0,0 +1 @@
export const ANIME_KEYWORD_ID = 210024;

@ -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;

@ -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 { MediaStatus, MediaRequestStatus, MediaType } from '../constants/media';
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 logger from '../logger';
import SeasonRequest from './SeasonRequest';

@ -1,10 +1,11 @@
import { getRepository } from 'typeorm';
import { User } from '../../entity/User';
import PlexAPI, { PlexLibraryItem, PlexMetadata } from '../../api/plexapi';
import TheMovieDb, {
import TheMovieDb from '../../api/themoviedb';
import {
TmdbMovieDetails,
TmdbTvDetails,
} from '../../api/themoviedb';
} from '../../api/themoviedb/interfaces';
import Media from '../../entity/Media';
import { MediaStatus, MediaType } from '../../constants/media';
import logger from '../../logger';

@ -2,7 +2,8 @@ import { uniqWith } from 'lodash';
import { getRepository } from 'typeorm';
import { v4 as uuid } from 'uuid';
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 Media from '../../entity/Media';
import Season from '../../entity/Season';

@ -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 Media from '../entity/Media';
import { mapMovieResult, MovieResult } from './Search';

@ -1,4 +1,4 @@
import { TmdbMovieDetails } from '../api/themoviedb';
import type { TmdbMovieDetails } from '../api/themoviedb/interfaces';
import {
ProductionCompany,
Genre,

@ -1,8 +1,8 @@
import {
import type {
TmdbPersonCreditCast,
TmdbPersonCreditCrew,
TmdbPersonDetail,
} from '../api/themoviedb';
} from '../api/themoviedb/interfaces';
import Media from '../entity/Media';
export interface PersonDetail {

@ -2,7 +2,7 @@ import type {
TmdbMovieResult,
TmdbPersonResult,
TmdbTvResult,
} from '../api/themoviedb';
} from '../api/themoviedb/interfaces';
import { MediaType as MainMediaType } from '../constants/media';
import Media from '../entity/Media';

@ -10,12 +10,12 @@ import {
Keyword,
mapVideos,
} from './common';
import {
import type {
TmdbTvEpisodeResult,
TmdbTvSeasonResult,
TmdbTvDetails,
TmdbSeasonWithEpisodes,
} from '../api/themoviedb';
} from '../api/themoviedb/interfaces';
import type Media from '../entity/Media';
import { Video } from './Movie';

@ -1,11 +1,11 @@
import {
import type {
TmdbCreditCast,
TmdbAggregateCreditCast,
TmdbCreditCrew,
TmdbExternalIds,
TmdbVideo,
TmdbVideoResult,
} from '../api/themoviedb';
} from '../api/themoviedb/interfaces';
import { Video } from '../models/Movie';

@ -2,7 +2,7 @@ import type {
TmdbMovieResult,
TmdbTvResult,
TmdbPersonResult,
} from '../api/themoviedb';
} from '../api/themoviedb/interfaces';
export const isMovie = (
movie: TmdbMovieResult | TmdbTvResult | TmdbPersonResult

@ -6,7 +6,7 @@ import { defineMessages, useIntl } from 'react-intl';
import { MediaRequest } from '../../../server/entity/MediaRequest';
import useSWR from 'swr';
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 {
MediaStatus,

@ -28,7 +28,7 @@ import RTAudFresh from '../../assets/rt_aud_fresh.svg';
import RTAudRotten from '../../assets/rt_aud_rotten.svg';
import type { RTRating } from '../../../server/api/rottentomatoes';
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 { sortCrewPriority } from '../../utils/creditHelpers';
import { Crew } from '../../../server/models/common';

@ -4015,6 +4015,11 @@ clone-response@^1.0.2:
dependencies:
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:
version "1.0.4"
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"
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:
version "1.10.0"
resolved "https://registry.yarnpkg.com/node-emoji/-/node-emoji-1.10.0.tgz#8886abd25d9c7bb61802a658523d1f8d2a89b2da"

Loading…
Cancel
Save