commit
12c5bbd778
@ -1,15 +1,23 @@
|
||||
node_modules
|
||||
.next
|
||||
**/*.md
|
||||
**/.gitkeep
|
||||
**/.vscode
|
||||
.all-contributorsrc
|
||||
.dockerignore
|
||||
.editorconfig
|
||||
.eslintrc.js
|
||||
.git
|
||||
.gitbook.yaml
|
||||
.gitconfig
|
||||
.gitignore
|
||||
.github
|
||||
.all-contributorsrc
|
||||
.editorconfig
|
||||
.next
|
||||
.prettierignore
|
||||
**/README.md
|
||||
**/.vscode
|
||||
config/db/db.sqlite3
|
||||
config/db/logs/overseerr.log
|
||||
Dockerfil**
|
||||
**.md
|
||||
Dockerfile*
|
||||
docker-compose.yml
|
||||
docs
|
||||
LICENSE
|
||||
node_modules
|
||||
snap
|
||||
stylelint.config.js
|
||||
|
@ -0,0 +1,23 @@
|
||||
name: Deploy API Docs
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- develop
|
||||
|
||||
jobs:
|
||||
deploy:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Generate Swagger UI
|
||||
uses: Legion2/swagger-ui-action@v1
|
||||
with:
|
||||
output: swagger-ui
|
||||
spec-file: overseerr-api.yml
|
||||
- name: Deploy to GitHub Pages
|
||||
uses: peaceiris/actions-gh-pages@v3
|
||||
with:
|
||||
github_token: ${{ secrets.GITHUB_TOKEN }}
|
||||
publish_dir: swagger-ui
|
||||
cname: api-docs.overseerr.dev
|
@ -0,0 +1,9 @@
|
||||
{
|
||||
"formats": {
|
||||
"openapi": {
|
||||
"rootDir": ".",
|
||||
"include": ["**"]
|
||||
}
|
||||
},
|
||||
"exclude": ["docs"]
|
||||
}
|
Before Width: | Height: | Size: 8.1 KiB |
After Width: | Height: | Size: 1023 B |
Before Width: | Height: | Size: 20 KiB |
After Width: | Height: | Size: 2.3 KiB |
@ -0,0 +1,102 @@
|
||||
import axios, { AxiosInstance, AxiosRequestConfig } from 'axios';
|
||||
import NodeCache from 'node-cache';
|
||||
|
||||
// 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;
|
||||
|
||||
// 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,934 +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 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;
|
||||
credits: {
|
||||
cast: TmdbCreditCast[];
|
||||
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: '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',
|
||||
},
|
||||
},
|
||||
43200
|
||||
);
|
||||
|
||||
return data;
|
||||
} catch (e) {
|
||||
throw new Error(`[TMDB] Failed to fetch movie details: ${e.message}`);
|
||||
}
|
||||
};
|
||||
|
||||
public getTvShow = async ({
|
||||
tvId,
|
||||
language = 'en',
|
||||
}: {
|
||||
tvId: number;
|
||||
language?: string;
|
||||
}): Promise<TmdbTvDetails> => {
|
||||
try {
|
||||
const data = await this.get<TmdbTvDetails>(
|
||||
`/tv/${tvId}`,
|
||||
{
|
||||
params: {
|
||||
language,
|
||||
append_to_response:
|
||||
'aggregate_credits,credits,external_ids,keywords,videos',
|
||||
},
|
||||
},
|
||||
43200
|
||||
);
|
||||
|
||||
return data;
|
||||
} catch (e) {
|
||||
throw new Error(`[TMDB] Failed to fetch tv show details: ${e.message}`);
|
||||
}
|
||||
};
|
||||
|
||||
public getTvSeason = async ({
|
||||
tvId,
|
||||
seasonNumber,
|
||||
language,
|
||||
}: {
|
||||
tvId: number;
|
||||
seasonNumber: number;
|
||||
language?: string;
|
||||
}): Promise<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[];
|
||||
}
|
@ -0,0 +1,60 @@
|
||||
import NodeCache from 'node-cache';
|
||||
|
||||
export type AvailableCacheIds = 'tmdb' | 'radarr' | 'sonarr' | 'rt';
|
||||
|
||||
const DEFAULT_TTL = 300;
|
||||
const DEFAULT_CHECK_PERIOD = 120;
|
||||
|
||||
class Cache {
|
||||
public id: AvailableCacheIds;
|
||||
public data: NodeCache;
|
||||
public name: string;
|
||||
|
||||
constructor(
|
||||
id: AvailableCacheIds,
|
||||
name: string,
|
||||
options: { stdTtl?: number; checkPeriod?: number } = {}
|
||||
) {
|
||||
this.id = id;
|
||||
this.name = name;
|
||||
this.data = new NodeCache({
|
||||
stdTTL: options.stdTtl ?? DEFAULT_TTL,
|
||||
checkperiod: options.checkPeriod ?? DEFAULT_CHECK_PERIOD,
|
||||
});
|
||||
}
|
||||
|
||||
public getStats() {
|
||||
return this.data.getStats();
|
||||
}
|
||||
|
||||
public flush(): void {
|
||||
this.data.flushAll();
|
||||
}
|
||||
}
|
||||
|
||||
class CacheManager {
|
||||
private availableCaches: Record<AvailableCacheIds, Cache> = {
|
||||
tmdb: new Cache('tmdb', 'TMDb API', {
|
||||
stdTtl: 21600,
|
||||
checkPeriod: 60 * 30,
|
||||
}),
|
||||
radarr: new Cache('radarr', 'Radarr API'),
|
||||
sonarr: new Cache('sonarr', 'Sonarr API'),
|
||||
rt: new Cache('rt', 'Rotten Tomatoes API', {
|
||||
stdTtl: 43200,
|
||||
checkPeriod: 60 * 30,
|
||||
}),
|
||||
};
|
||||
|
||||
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,224 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html
|
||||
lang="en"
|
||||
xmlns:v="urn:schemas-microsoft-com:vml"
|
||||
xmlns:o="urn:schemas-microsoft-com:office:office"
|
||||
>
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="x-apple-disable-message-reformatting" />
|
||||
<meta http-equiv="x-ua-compatible" content="ie=edge" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<meta
|
||||
name="format-detection"
|
||||
content="telephone=no, date=no, address=no, email=no"
|
||||
/>
|
||||
<link
|
||||
href="https://fonts.googleapis.com/css?family=Nunito+Sans:400,700&amp;display=swap"
|
||||
rel="stylesheet"
|
||||
media="screen"
|
||||
/>
|
||||
<!--[if mso]>
|
||||
<xml
|
||||
><o:OfficeDocumentSettings
|
||||
><o:PixelsPerInch>96</o:PixelsPerInch></o:OfficeDocumentSettings
|
||||
></xml
|
||||
>
|
||||
<style>
|
||||
td,
|
||||
th,
|
||||
div,
|
||||
p,
|
||||
a,
|
||||
h1,
|
||||
h2,
|
||||
h3,
|
||||
h4,
|
||||
h5,
|
||||
h6 {
|
||||
font-family: 'Segoe UI', sans-serif;
|
||||
mso-line-height-rule: exactly;
|
||||
}
|
||||
</style>
|
||||
<![endif]-->
|
||||
<style>
|
||||
@media (max-width: 600px) {
|
||||
.sm-w-full {
|
||||
width: 100% !important;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body
|
||||
style="
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
width: 100%;
|
||||
word-break: break-word;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
background-color: #f2f4f6;
|
||||
"
|
||||
>
|
||||
<div role="article" aria-roledescription="email" aria-label="" lang="en">
|
||||
<table
|
||||
style="
|
||||
background-color: #f2f4f6;
|
||||
font-family: 'Nunito Sans', -apple-system, 'Segoe UI', sans-serif;
|
||||
width: 100%;
|
||||
"
|
||||
width="100%"
|
||||
bgcolor="#f2f4f6"
|
||||
cellpadding="0"
|
||||
cellspacing="0"
|
||||
role="presentation"
|
||||
>
|
||||
<tr>
|
||||
<td align="center">
|
||||
<table
|
||||
style="width: 100%"
|
||||
width="100%"
|
||||
cellpadding="0"
|
||||
cellspacing="0"
|
||||
role="presentation"
|
||||
>
|
||||
<tr>
|
||||
<td
|
||||
align="center"
|
||||
style="
|
||||
font-size: 16px;
|
||||
padding-top: 25px;
|
||||
padding-bottom: 25px;
|
||||
text-align: center;
|
||||
"
|
||||
>
|
||||
<a
|
||||
href="https://example.com"
|
||||
style="
|
||||
text-shadow: 0 1px 0 #ffffff;
|
||||
font-weight: 700;
|
||||
font-size: 16px;
|
||||
color: #a8aaaf;
|
||||
text-decoration: none;
|
||||
"
|
||||
>
|
||||
Overseerr
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="width: 100%" width="100%">
|
||||
<table
|
||||
align="center"
|
||||
class="sm-w-full"
|
||||
style="
|
||||
background-color: #ffffff;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
width: 570px;
|
||||
"
|
||||
width="570"
|
||||
bgcolor="#ffffff"
|
||||
cellpadding="0"
|
||||
cellspacing="0"
|
||||
role="presentation"
|
||||
>
|
||||
<tr>
|
||||
<td style="padding: 45px">
|
||||
<div style="font-size: 16px">
|
||||
{{body}}
|
||||
<br />
|
||||
<br />
|
||||
<p style="margin-top: 4px; text-align: center">
|
||||
{{media_name}
|
||||
</p>
|
||||
<table
|
||||
cellpadding="0"
|
||||
cellspacing="0"
|
||||
role="presentation"
|
||||
>
|
||||
<tr>
|
||||
<td>
|
||||
<table
|
||||
cellpadding="0"
|
||||
cellspacing="0"
|
||||
role="presentation"
|
||||
>
|
||||
<img src="{{image_url}}" alt="" />
|
||||
<p></p>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<p
|
||||
style="
|
||||
font-size: 16px;
|
||||
line-height: 24px;
|
||||
margin-top: 6px;
|
||||
margin-bottom: 20px;
|
||||
color: #51545e;
|
||||
"
|
||||
>
|
||||
Requested by {{requester_name}} at {{timestamp}}
|
||||
</p>
|
||||
<p
|
||||
style="
|
||||
font-size: 13px;
|
||||
line-height: 24px;
|
||||
margin-top: 6px;
|
||||
margin-bottom: 20px;
|
||||
color: #51545e;
|
||||
"
|
||||
>
|
||||
<a href="{{action_url}}" style="color: #3869d4"
|
||||
>Open detail page</a
|
||||
>
|
||||
</p>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
<tr>
|
||||
<td>
|
||||
<table
|
||||
align="center"
|
||||
class="sm-w-full"
|
||||
style="
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
text-align: center;
|
||||
width: 570px;
|
||||
"
|
||||
width="570"
|
||||
cellpadding="0"
|
||||
cellspacing="0"
|
||||
role="presentation"
|
||||
>
|
||||
<tr>
|
||||
<td align="center" style="font-size: 16px; padding: 45px">
|
||||
<p
|
||||
style="
|
||||
font-size: 13px;
|
||||
line-height: 24px;
|
||||
margin-top: 6px;
|
||||
margin-bottom: 20px;
|
||||
text-align: center;
|
||||
color: #a8aaaf;
|
||||
"
|
||||
>
|
||||
Overseerr.
|
||||
</p>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</body>
|
||||
</html>
|
@ -1 +1 @@
|
||||
= `${requestType}: ${mediaName} - Overseerr`
|
||||
= `${requestType}: ${mediaName} - ${applicationTitle}`
|
||||
|
@ -1 +1 @@
|
||||
= `Password reset - Overseerr`
|
||||
= `Password Reset - ${applicationTitle}`
|
||||
|
@ -1 +1 @@
|
||||
= `Test Notification - Overseerr`
|
||||
= `Test Notification - ${applicationTitle}`
|
||||
|
@ -0,0 +1,16 @@
|
||||
import { existsSync } from 'fs';
|
||||
import path from 'path';
|
||||
|
||||
const CONFIG_PATH = process.env.CONFIG_DIRECTORY
|
||||
? process.env.CONFIG_DIRECTORY
|
||||
: path.join(__dirname, '../../config');
|
||||
|
||||
const DOCKER_PATH = `${CONFIG_PATH}/DOCKER`;
|
||||
|
||||
export const appDataStatus = (): boolean => {
|
||||
return !existsSync(DOCKER_PATH);
|
||||
};
|
||||
|
||||
export const appDataPath = (): string => {
|
||||
return CONFIG_PATH;
|
||||
};
|
After Width: | Height: | Size: 4.4 KiB |
@ -0,0 +1,42 @@
|
||||
import React from 'react';
|
||||
import { defineMessages, useIntl } from 'react-intl';
|
||||
import useSWR from 'swr';
|
||||
import Alert from '../Common/Alert';
|
||||
|
||||
const messages = defineMessages({
|
||||
dockerVolumeMissing: 'Docker Volume Mount Missing',
|
||||
dockerVolumeMissingDescription:
|
||||
'The <code>{appDataPath}</code> volume mount was not configured properly. All data will be cleared when the container is stopped or restarted.',
|
||||
});
|
||||
|
||||
const AppDataWarning: React.FC = () => {
|
||||
const intl = useIntl();
|
||||
const { data, error } = useSWR<{ appData: boolean; appDataPath: string }>(
|
||||
'/api/v1/status/appdata'
|
||||
);
|
||||
|
||||
if (!data && !error) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!data) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{!data.appData && (
|
||||
<Alert title={intl.formatMessage(messages.dockerVolumeMissing)}>
|
||||
{intl.formatMessage(messages.dockerVolumeMissingDescription, {
|
||||
code: function code(msg) {
|
||||
return <code className="bg-opacity-50">{msg}</code>;
|
||||
},
|
||||
appDataPath: data.appDataPath,
|
||||
})}
|
||||
</Alert>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default AppDataWarning;
|
@ -0,0 +1,22 @@
|
||||
import React from 'react';
|
||||
import useSettings from '../../../hooks/useSettings';
|
||||
import Head from 'next/head';
|
||||
|
||||
interface PageTitleProps {
|
||||
title: string | (string | undefined)[];
|
||||
}
|
||||
|
||||
const PageTitle: React.FC<PageTitleProps> = ({ title }) => {
|
||||
const settings = useSettings();
|
||||
|
||||
return (
|
||||
<Head>
|
||||
<title>
|
||||
{Array.isArray(title) ? title.filter(Boolean).join(' - ') : title} -{' '}
|
||||
{settings.currentSettings.applicationTitle}
|
||||
</title>
|
||||
</Head>
|
||||
);
|
||||
};
|
||||
|
||||
export default PageTitle;
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in new issue