Compare commits

..

1 Commits

Author SHA1 Message Date
Gauthier
c355a77417 chore: update to Node.js v22 2024-11-06 15:41:49 +01:00
21 changed files with 4178 additions and 4788 deletions

View File

@@ -13,7 +13,7 @@ jobs:
name: Lint & Test Build name: Lint & Test Build
if: github.event_name == 'pull_request' if: github.event_name == 'pull_request'
runs-on: ubuntu-22.04 runs-on: ubuntu-22.04
container: node:20-alpine container: node:22-alpine
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v4 uses: actions/checkout@v4

View File

@@ -17,7 +17,7 @@ jobs:
- name: Set up Node.js - name: Set up Node.js
uses: actions/setup-node@v4 uses: actions/setup-node@v4
with: with:
node-version: 20 node-version: 22
- name: Pnpm Setup - name: Pnpm Setup
uses: pnpm/action-setup@v4 uses: pnpm/action-setup@v4
with: with:

View File

@@ -20,7 +20,7 @@ jobs:
- name: Set up Node.js - name: Set up Node.js
uses: actions/setup-node@v4 uses: actions/setup-node@v4
with: with:
node-version: 20 node-version: 22
- name: Pnpm Setup - name: Pnpm Setup
uses: pnpm/action-setup@v4 uses: pnpm/action-setup@v4

View File

@@ -16,7 +16,7 @@ jobs:
- name: Set up Node.js - name: Set up Node.js
uses: actions/setup-node@v4 uses: actions/setup-node@v4
with: with:
node-version: 20 node-version: 22
- name: Set up QEMU - name: Set up QEMU
uses: docker/setup-qemu-action@v3 uses: docker/setup-qemu-action@v3
- name: Set up Docker Buildx - name: Set up Docker Buildx

View File

@@ -20,7 +20,7 @@ jobs:
- name: Set up Node.js - name: Set up Node.js
uses: actions/setup-node@v4 uses: actions/setup-node@v4
with: with:
node-version: 20 node-version: 22
- name: Pnpm Setup - name: Pnpm Setup
uses: pnpm/action-setup@v4 uses: pnpm/action-setup@v4

View File

@@ -8,7 +8,7 @@ All help is welcome and greatly appreciated! If you would like to contribute to
- HTML/Typescript/Javascript editor - HTML/Typescript/Javascript editor
- [VSCode](https://code.visualstudio.com/) is recommended. Upon opening the project, a few extensions will be automatically recommended for install. - [VSCode](https://code.visualstudio.com/) is recommended. Upon opening the project, a few extensions will be automatically recommended for install.
- [NodeJS](https://nodejs.org/en/download/) (Node 20.x) - [NodeJS](https://nodejs.org/en/download/) (Node 22.x)
- [Pnpm](https://pnpm.io/cli/install) - [Pnpm](https://pnpm.io/cli/install)
- [Git](https://git-scm.com/downloads) - [Git](https://git-scm.com/downloads)

View File

@@ -1,4 +1,4 @@
FROM node:20-alpine AS BUILD_IMAGE FROM node:22-alpine AS BUILD_IMAGE
WORKDIR /app WORKDIR /app
@@ -36,7 +36,7 @@ RUN touch config/DOCKER
RUN echo "{\"commitTag\": \"${COMMIT_TAG}\"}" > committag.json RUN echo "{\"commitTag\": \"${COMMIT_TAG}\"}" > committag.json
FROM node:20-alpine FROM node:22-alpine
# Metadata for Github Package Registry # Metadata for Github Package Registry
LABEL org.opencontainers.image.source="https://github.com/Fallenbagel/jellyseerr" LABEL org.opencontainers.image.source="https://github.com/Fallenbagel/jellyseerr"

View File

@@ -1,4 +1,4 @@
FROM node:20-alpine FROM node:22-alpine
COPY . /app COPY . /app
WORKDIR /app WORKDIR /app

2
next-env.d.ts vendored
View File

@@ -2,4 +2,4 @@
/// <reference types="next/image-types/global" /> /// <reference types="next/image-types/global" />
// NOTE: This file should not be edited // NOTE: This file should not be edited
// see https://nextjs.org/docs/basic-features/typescript for more information. // see https://nextjs.org/docs/pages/building-your-application/configuring/typescript for more information.

View File

@@ -43,7 +43,6 @@
"@svgr/webpack": "6.5.1", "@svgr/webpack": "6.5.1",
"@tanem/react-nprogress": "5.0.30", "@tanem/react-nprogress": "5.0.30",
"ace-builds": "1.15.2", "ace-builds": "1.15.2",
"axios": "^1.7.7",
"bcrypt": "5.1.0", "bcrypt": "5.1.0",
"bowser": "2.11.0", "bowser": "2.11.0",
"connect-typeorm": "1.1.4", "connect-typeorm": "1.1.4",
@@ -169,7 +168,7 @@
"typescript": "4.9.5" "typescript": "4.9.5"
}, },
"engines": { "engines": {
"node": "^20.0.0", "node": "^22.0.0",
"pnpm": "^9.0.0" "pnpm": "^9.0.0"
}, },
"overrides": { "overrides": {

8056
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,5 @@
import type { AxiosInstance, AxiosRequestConfig } from 'axios'; import type { RateLimitOptions } from '@server/utils/rateLimit';
import axios from 'axios'; import rateLimit from '@server/utils/rateLimit';
// import rateLimit from 'axios-rate-limit';
import type NodeCache from 'node-cache'; import type NodeCache from 'node-cache';
// 5 minute default TTL (in seconds) // 5 minute default TTL (in seconds)
@@ -12,71 +11,101 @@ const DEFAULT_ROLLING_BUFFER = 10000;
interface ExternalAPIOptions { interface ExternalAPIOptions {
nodeCache?: NodeCache; nodeCache?: NodeCache;
headers?: Record<string, unknown>; headers?: Record<string, unknown>;
rateLimit?: { rateLimit?: RateLimitOptions;
maxRPS: number;
maxRequests: number;
};
} }
class ExternalAPI { class ExternalAPI {
protected axios: AxiosInstance; protected fetch: typeof fetch;
protected params: Record<string, string>;
protected defaultHeaders: { [key: string]: string };
private baseUrl: string; private baseUrl: string;
private cache?: NodeCache; private cache?: NodeCache;
constructor( constructor(
baseUrl: string, baseUrl: string,
params: Record<string, unknown>, params: Record<string, string> = {},
options: ExternalAPIOptions = {} options: ExternalAPIOptions = {}
) { ) {
this.axios = axios.create({ if (options.rateLimit) {
baseURL: baseUrl, this.fetch = rateLimit(fetch, options.rateLimit);
params, } else {
headers: { this.fetch = fetch;
'Content-Type': 'application/json', }
Accept: 'application/json',
...options.headers,
},
});
// if (options.rateLimit) { const url = new URL(baseUrl);
// this.axios = rateLimit(this.axios, {
// maxRequests: options.rateLimit.maxRequests, this.defaultHeaders = {
// maxRPS: options.rateLimit.maxRPS, 'Content-Type': 'application/json',
// }); Accept: 'application/json',
// } ...((url.username || url.password) && {
Authorization: `Basic ${Buffer.from(
`${url.username}:${url.password}`
).toString('base64')}`,
}),
...options.headers,
};
if (url.username || url.password) {
url.username = '';
url.password = '';
baseUrl = url.toString();
}
this.baseUrl = baseUrl; this.baseUrl = baseUrl;
this.params = params;
this.cache = options.nodeCache; this.cache = options.nodeCache;
} }
protected async get<T>( protected async get<T>(
endpoint: string, endpoint: string,
config?: AxiosRequestConfig, params?: Record<string, string>,
ttl?: number ttl?: number,
config?: RequestInit
): Promise<T> { ): Promise<T> {
const cacheKey = this.serializeCacheKey(endpoint, config?.params); const cacheKey = this.serializeCacheKey(endpoint, {
...this.params,
...params,
});
const cachedItem = this.cache?.get<T>(cacheKey); const cachedItem = this.cache?.get<T>(cacheKey);
if (cachedItem) { if (cachedItem) {
return cachedItem; return cachedItem;
} }
const response = await this.axios.get<T>(endpoint, config); const url = this.formatUrl(endpoint, params);
const response = await this.fetch(url, {
...config,
headers: {
...this.defaultHeaders,
...config?.headers,
},
});
if (!response.ok) {
const text = await response.text();
throw new Error(
`${response.status} ${response.statusText}${text ? ': ' + text : ''}`,
{
cause: response,
}
);
}
const data = await this.getDataFromResponse(response);
if (this.cache) { if (this.cache && ttl !== 0) {
this.cache.set(cacheKey, response.data, ttl ?? DEFAULT_TTL); this.cache.set(cacheKey, data, ttl ?? DEFAULT_TTL);
} }
return response.data; return data;
} }
protected async post<T>( protected async post<T>(
endpoint: string, endpoint: string,
data?: Record<string, unknown>, data?: Record<string, unknown>,
config?: AxiosRequestConfig, params?: Record<string, string>,
ttl?: number ttl?: number,
config?: RequestInit
): Promise<T> { ): Promise<T> {
const cacheKey = this.serializeCacheKey(endpoint, { const cacheKey = this.serializeCacheKey(endpoint, {
config: config?.params, config: { ...this.params, ...params },
data, data,
}); });
const cachedItem = this.cache?.get<T>(cacheKey); const cachedItem = this.cache?.get<T>(cacheKey);
@@ -84,23 +113,43 @@ class ExternalAPI {
return cachedItem; return cachedItem;
} }
const response = await this.axios.post<T>(endpoint, data, config); const url = this.formatUrl(endpoint, params);
const response = await this.fetch(url, {
method: 'POST',
...config,
headers: {
...this.defaultHeaders,
...config?.headers,
},
body: data ? JSON.stringify(data) : undefined,
});
if (!response.ok) {
const text = await response.text();
throw new Error(
`${response.status} ${response.statusText}${text ? ': ' + text : ''}`,
{
cause: response,
}
);
}
const resData = await this.getDataFromResponse(response);
if (this.cache) { if (this.cache && ttl !== 0) {
this.cache.set(cacheKey, response.data, ttl ?? DEFAULT_TTL); this.cache.set(cacheKey, resData, ttl ?? DEFAULT_TTL);
} }
return response.data; return resData;
} }
protected async put<T>( protected async put<T>(
endpoint: string, endpoint: string,
data: Record<string, unknown>, data: Record<string, unknown>,
config?: AxiosRequestConfig, params?: Record<string, string>,
ttl?: number ttl?: number,
config?: RequestInit
): Promise<T> { ): Promise<T> {
const cacheKey = this.serializeCacheKey(endpoint, { const cacheKey = this.serializeCacheKey(endpoint, {
config: config?.params, config: { ...this.params, ...params },
data, data,
}); });
const cachedItem = this.cache?.get<T>(cacheKey); const cachedItem = this.cache?.get<T>(cacheKey);
@@ -108,41 +157,73 @@ class ExternalAPI {
return cachedItem; return cachedItem;
} }
const response = await this.axios.put<T>(endpoint, data, config); const url = this.formatUrl(endpoint, params);
const response = await this.fetch(url, {
method: 'PUT',
...config,
headers: {
...this.defaultHeaders,
...config?.headers,
},
body: JSON.stringify(data),
});
if (!response.ok) {
const text = await response.text();
throw new Error(
`${response.status} ${response.statusText}${text ? ': ' + text : ''}`,
{
cause: response,
}
);
}
const resData = await this.getDataFromResponse(response);
if (this.cache) { if (this.cache && ttl !== 0) {
this.cache.set(cacheKey, response.data, ttl ?? DEFAULT_TTL); this.cache.set(cacheKey, resData, ttl ?? DEFAULT_TTL);
} }
return response.data; return resData;
} }
protected async delete<T>( protected async delete<T>(
endpoint: string, endpoint: string,
config?: AxiosRequestConfig, params?: Record<string, string>,
ttl?: number config?: RequestInit
): Promise<T> { ): Promise<T> {
const cacheKey = this.serializeCacheKey(endpoint, config?.params); const url = this.formatUrl(endpoint, params);
const cachedItem = this.cache?.get<T>(cacheKey); const response = await this.fetch(url, {
if (cachedItem) { method: 'DELETE',
return cachedItem; ...config,
headers: {
...this.defaultHeaders,
...config?.headers,
},
});
if (!response.ok) {
const text = await response.text();
throw new Error(
`${response.status} ${response.statusText}${text ? ': ' + text : ''}`,
{
cause: response,
}
);
} }
const data = await this.getDataFromResponse(response);
const response = await this.axios.delete<T>(endpoint, config); return data;
if (this.cache) {
this.cache.set(cacheKey, response.data, ttl ?? DEFAULT_TTL);
}
return response.data;
} }
protected async getRolling<T>( protected async getRolling<T>(
endpoint: string, endpoint: string,
config?: AxiosRequestConfig, params?: Record<string, string>,
ttl?: number ttl?: number,
config?: RequestInit,
overwriteBaseUrl?: string
): Promise<T> { ): Promise<T> {
const cacheKey = this.serializeCacheKey(endpoint, config?.params); const cacheKey = this.serializeCacheKey(endpoint, {
...this.params,
...params,
});
const cachedItem = this.cache?.get<T>(cacheKey); const cachedItem = this.cache?.get<T>(cacheKey);
if (cachedItem) { if (cachedItem) {
@@ -153,20 +234,78 @@ class ExternalAPI {
keyTtl - (ttl ?? DEFAULT_TTL) * 1000 < keyTtl - (ttl ?? DEFAULT_TTL) * 1000 <
Date.now() - DEFAULT_ROLLING_BUFFER Date.now() - DEFAULT_ROLLING_BUFFER
) { ) {
this.axios.get<T>(endpoint, config).then((response) => { const url = this.formatUrl(endpoint, params, overwriteBaseUrl);
this.cache?.set(cacheKey, response.data, ttl ?? DEFAULT_TTL); this.fetch(url, {
...config,
headers: {
...this.defaultHeaders,
...config?.headers,
},
}).then(async (response) => {
if (!response.ok) {
const text = await response.text();
throw new Error(
`${response.status} ${response.statusText}${
text ? ': ' + text : ''
}`,
{
cause: response,
}
);
}
const data = await this.getDataFromResponse(response);
this.cache?.set(cacheKey, data, ttl ?? DEFAULT_TTL);
}); });
} }
return cachedItem; return cachedItem;
} }
const response = await this.axios.get<T>(endpoint, config); const url = this.formatUrl(endpoint, params, overwriteBaseUrl);
const response = await this.fetch(url, {
...config,
headers: {
...this.defaultHeaders,
...config?.headers,
},
});
if (!response.ok) {
const text = await response.text();
throw new Error(
`${response.status} ${response.statusText}${text ? ': ' + text : ''}`,
{
cause: response,
}
);
}
const data = await this.getDataFromResponse(response);
if (this.cache) { if (this.cache) {
this.cache.set(cacheKey, response.data, ttl ?? DEFAULT_TTL); this.cache.set(cacheKey, data, ttl ?? DEFAULT_TTL);
} }
return response.data; return data;
}
private formatUrl(
endpoint: string,
params?: Record<string, string>,
overwriteBaseUrl?: string
): string {
const baseUrl = overwriteBaseUrl || this.baseUrl;
const href =
baseUrl +
(baseUrl.endsWith('/') ? '' : '/') +
(endpoint.startsWith('/') ? endpoint.slice(1) : endpoint);
const searchParams = new URLSearchParams({
...this.params,
...params,
});
return (
href +
(searchParams.toString().length
? '?' + searchParams.toString()
: searchParams.toString())
);
} }
private serializeCacheKey( private serializeCacheKey(
@@ -179,6 +318,29 @@ class ExternalAPI {
return `${this.baseUrl}${endpoint}${JSON.stringify(params)}`; return `${this.baseUrl}${endpoint}${JSON.stringify(params)}`;
} }
private async getDataFromResponse(response: Response) {
const contentType = response.headers.get('Content-Type');
if (contentType?.includes('application/json')) {
return await response.json();
} else if (
contentType?.includes('application/xml') ||
contentType?.includes('text/html') ||
contentType?.includes('text/plain')
) {
return await response.text();
} else {
try {
return await response.json();
} catch {
try {
return await response.blob();
} catch {
return null;
}
}
}
}
} }
export default ExternalAPI; export default ExternalAPI;

View File

@@ -1,6 +1,6 @@
import ExternalAPI from '@server/api/externalapi';
import cacheManager from '@server/lib/cache'; import cacheManager from '@server/lib/cache';
import logger from '@server/logger'; import logger from '@server/logger';
import ExternalAPI from './externalapi';
interface GitHubRelease { interface GitHubRelease {
url: string; url: string;
@@ -67,10 +67,6 @@ class GithubAPI extends ExternalAPI {
'https://api.github.com', 'https://api.github.com',
{}, {},
{ {
headers: {
'Content-Type': 'application/json',
Accept: 'application/json',
},
nodeCache: cacheManager.getCache('github').data, nodeCache: cacheManager.getCache('github').data,
} }
); );
@@ -85,9 +81,7 @@ class GithubAPI extends ExternalAPI {
const data = await this.get<GitHubRelease[]>( const data = await this.get<GitHubRelease[]>(
'/repos/fallenbagel/jellyseerr/releases', '/repos/fallenbagel/jellyseerr/releases',
{ {
params: { per_page: take.toString(),
per_page: take,
},
} }
); );
@@ -112,10 +106,8 @@ class GithubAPI extends ExternalAPI {
const data = await this.get<GithubCommit[]>( const data = await this.get<GithubCommit[]>(
'/repos/fallenbagel/jellyseerr/commits', '/repos/fallenbagel/jellyseerr/commits',
{ {
params: { per_page: take.toString(),
per_page: take, branch,
branch,
},
} }
); );

View File

@@ -129,6 +129,8 @@ class JellyfinAPI extends ExternalAPI {
Username, Username,
Pw: Password, Pw: Password,
}, },
{},
undefined,
{ headers } { headers }
); );
}; };
@@ -291,15 +293,13 @@ class JellyfinAPI extends ExternalAPI {
const libraryItemsResponse = await this.get<any>( const libraryItemsResponse = await this.get<any>(
`/Users/${this.userId}/Items`, `/Users/${this.userId}/Items`,
{ {
params: { SortBy: 'SortName',
SortBy: 'SortName', SortOrder: 'Ascending',
SortOrder: 'Ascending', IncludeItemTypes: 'Series,Movie,Others',
IncludeItemTypes: 'Series,Movie,Others', Recursive: 'true',
Recursive: 'true', StartIndex: '0',
StartIndex: '0', ParentId: id,
ParentId: id, collapseBoxSetItems: 'false',
collapseBoxSetItems: 'false',
},
} }
); );
@@ -321,10 +321,8 @@ class JellyfinAPI extends ExternalAPI {
const itemResponse = await this.get<any>( const itemResponse = await this.get<any>(
`/Users/${this.userId}/Items/Latest`, `/Users/${this.userId}/Items/Latest`,
{ {
params: { Limit: '12',
Limit: '12', ParentId: id,
ParentId: id,
},
} }
); );
@@ -386,9 +384,7 @@ class JellyfinAPI extends ExternalAPI {
const episodeResponse = await this.get<any>( const episodeResponse = await this.get<any>(
`/Shows/${seriesID}/Episodes`, `/Shows/${seriesID}/Episodes`,
{ {
params: { seasonId: seasonID,
seasonId: seasonID,
},
} }
); );

View File

@@ -1,9 +1,9 @@
import ExternalAPI from '@server/api/externalapi';
import type { PlexDevice } from '@server/interfaces/api/plexInterfaces'; import type { PlexDevice } from '@server/interfaces/api/plexInterfaces';
import cacheManager from '@server/lib/cache'; import cacheManager from '@server/lib/cache';
import { getSettings } from '@server/lib/settings'; import { getSettings } from '@server/lib/settings';
import logger from '@server/logger'; import logger from '@server/logger';
import xml2js from 'xml2js'; import xml2js from 'xml2js';
import ExternalAPI from './externalapi';
interface PlexAccountResponse { interface PlexAccountResponse {
user: PlexUser; user: PlexUser;
@@ -137,8 +137,6 @@ class PlexTvAPI extends ExternalAPI {
{ {
headers: { headers: {
'X-Plex-Token': authToken, 'X-Plex-Token': authToken,
'Content-Type': 'application/json',
Accept: 'application/json',
}, },
nodeCache: cacheManager.getCache('plextv').data, nodeCache: cacheManager.getCache('plextv').data,
} }
@@ -149,15 +147,11 @@ class PlexTvAPI extends ExternalAPI {
public async getDevices(): Promise<PlexDevice[]> { public async getDevices(): Promise<PlexDevice[]> {
try { try {
const devicesResp = await this.axios.get( const devicesResp = await this.get('/api/resources', {
'/api/resources?includeHttps=1', includeHttps: '1',
{ });
transformResponse: [],
responseType: 'text',
}
);
const parsedXml = await xml2js.parseStringPromise( const parsedXml = await xml2js.parseStringPromise(
devicesResp.data as DeviceResponse devicesResp as DeviceResponse
); );
return parsedXml?.MediaContainer?.Device?.map((pxml: DeviceResponse) => ({ return parsedXml?.MediaContainer?.Device?.map((pxml: DeviceResponse) => ({
name: pxml.$.name, name: pxml.$.name,
@@ -205,11 +199,11 @@ class PlexTvAPI extends ExternalAPI {
public async getUser(): Promise<PlexUser> { public async getUser(): Promise<PlexUser> {
try { try {
const account = await this.axios.get<PlexAccountResponse>( const account = await this.get<PlexAccountResponse>(
'/users/account.json' '/users/account.json'
); );
return account.data.user; return account.user;
} catch (e) { } catch (e) {
logger.error( logger.error(
`Something went wrong while getting the account from plex.tv: ${e.message}`, `Something went wrong while getting the account from plex.tv: ${e.message}`,
@@ -249,13 +243,10 @@ class PlexTvAPI extends ExternalAPI {
} }
public async getUsers(): Promise<UsersResponse> { public async getUsers(): Promise<UsersResponse> {
const response = await this.axios.get('/api/users', { const data = await this.get('/api/users');
transformResponse: [],
responseType: 'text',
});
const parsedXml = (await xml2js.parseStringPromise( const parsedXml = (await xml2js.parseStringPromise(
response.data data as string
)) as UsersResponse; )) as UsersResponse;
return parsedXml; return parsedXml;
} }
@@ -270,49 +261,49 @@ class PlexTvAPI extends ExternalAPI {
items: PlexWatchlistItem[]; items: PlexWatchlistItem[];
}> { }> {
try { try {
const response = await this.axios.get<WatchlistResponse>( const params = new URLSearchParams({
'/library/sections/watchlist/all', 'X-Plex-Container-Start': offset.toString(),
'X-Plex-Container-Size': size.toString(),
});
const response = await this.fetch(
`https://metadata.provider.plex.tv/library/sections/watchlist/all?${params.toString()}`,
{ {
params: { headers: this.defaultHeaders,
'X-Plex-Container-Start': offset,
'X-Plex-Container-Size': size,
},
baseURL: 'https://metadata.provider.plex.tv',
} }
); );
const data = (await response.json()) as WatchlistResponse;
const watchlistDetails = await Promise.all( const watchlistDetails = await Promise.all(
(response.data.MediaContainer.Metadata ?? []).map( (data.MediaContainer.Metadata ?? []).map(async (watchlistItem) => {
async (watchlistItem) => { const detailedResponse = await this.getRolling<MetadataResponse>(
const detailedResponse = await this.getRolling<MetadataResponse>( `/library/metadata/${watchlistItem.ratingKey}`,
`/library/metadata/${watchlistItem.ratingKey}`, {},
{ undefined,
baseURL: 'https://metadata.provider.plex.tv', {},
} 'https://metadata.provider.plex.tv'
); );
const metadata = detailedResponse.MediaContainer.Metadata[0]; const metadata = detailedResponse.MediaContainer.Metadata[0];
const tmdbString = metadata.Guid.find((guid) => const tmdbString = metadata.Guid.find((guid) =>
guid.id.startsWith('tmdb') guid.id.startsWith('tmdb')
); );
const tvdbString = metadata.Guid.find((guid) => const tvdbString = metadata.Guid.find((guid) =>
guid.id.startsWith('tvdb') guid.id.startsWith('tvdb')
); );
return { return {
ratingKey: metadata.ratingKey, ratingKey: metadata.ratingKey,
// This should always be set? But I guess it also cannot be? // This should always be set? But I guess it also cannot be?
// We will filter out the 0's afterwards // We will filter out the 0's afterwards
tmdbId: tmdbString ? Number(tmdbString.id.split('//')[1]) : 0, tmdbId: tmdbString ? Number(tmdbString.id.split('//')[1]) : 0,
tvdbId: tvdbString tvdbId: tvdbString
? Number(tvdbString.id.split('//')[1]) ? Number(tvdbString.id.split('//')[1])
: undefined, : undefined,
title: metadata.title, title: metadata.title,
type: metadata.type, type: metadata.type,
}; };
} })
)
); );
const filteredList = watchlistDetails.filter((detail) => detail.tmdbId); const filteredList = watchlistDetails.filter((detail) => detail.tmdbId);
@@ -320,7 +311,7 @@ class PlexTvAPI extends ExternalAPI {
return { return {
offset, offset,
size, size,
totalSize: response.data.MediaContainer.totalSize, totalSize: data.MediaContainer.totalSize,
items: filteredList, items: filteredList,
}; };
} catch (e) { } catch (e) {

View File

@@ -1,4 +1,4 @@
import ExternalAPI from './externalapi'; import ExternalAPI from '@server/api/externalapi';
interface PushoverSoundsResponse { interface PushoverSoundsResponse {
sounds: { sounds: {
@@ -26,24 +26,13 @@ export const mapSounds = (sounds: {
class PushoverAPI extends ExternalAPI { class PushoverAPI extends ExternalAPI {
constructor() { constructor() {
super( super('https://api.pushover.net/1');
'https://api.pushover.net/1',
{},
{
headers: {
'Content-Type': 'application/json',
Accept: 'application/json',
},
}
);
} }
public async getSounds(appToken: string): Promise<PushoverSound[]> { public async getSounds(appToken: string): Promise<PushoverSound[]> {
try { try {
const data = await this.get<PushoverSoundsResponse>('/sounds.json', { const data = await this.get<PushoverSoundsResponse>('/sounds.json', {
params: { token: appToken,
token: appToken,
},
}); });
return mapSounds(data.sounds); return mapSounds(data.sounds);

View File

@@ -160,9 +160,7 @@ class ServarrBase<QueueItemAppendT> extends ExternalAPI {
const data = await this.get<QueueResponse<QueueItemAppendT>>( const data = await this.get<QueueResponse<QueueItemAppendT>>(
`/queue`, `/queue`,
{ {
params: { includeEpisode: 'true',
includeEpisode: 'true',
},
}, },
0 0
); );

View File

@@ -58,9 +58,7 @@ class RadarrAPI extends ServarrBase<{ movieId: number }> {
public async getMovieByTmdbId(id: number): Promise<RadarrMovie> { public async getMovieByTmdbId(id: number): Promise<RadarrMovie> {
try { try {
const data = await this.get<RadarrMovie[]>('/movie/lookup', { const data = await this.get<RadarrMovie[]>('/movie/lookup', {
params: { term: `tmdb:${id}`,
term: `tmdb:${id}`,
},
}); });
if (!data[0]) { if (!data[0]) {
@@ -224,10 +222,8 @@ class RadarrAPI extends ServarrBase<{ movieId: number }> {
try { try {
const { id, title } = await this.getMovieByTmdbId(movieId); const { id, title } = await this.getMovieByTmdbId(movieId);
await this.delete(`/movie/${id}`, { await this.delete(`/movie/${id}`, {
params: { deleteFiles: 'true',
deleteFiles: 'true', addImportExclusion: 'false',
addImportExclusion: 'false',
},
}); });
logger.info(`[Radarr] Removed movie ${title}`); logger.info(`[Radarr] Removed movie ${title}`);
} catch (e) { } catch (e) {

View File

@@ -138,9 +138,7 @@ class SonarrAPI extends ServarrBase<{
public async getSeriesByTitle(title: string): Promise<SonarrSeries[]> { public async getSeriesByTitle(title: string): Promise<SonarrSeries[]> {
try { try {
const data = await this.get<SonarrSeries[]>('/series/lookup', { const data = await this.get<SonarrSeries[]>('/series/lookup', {
params: { term: title,
term: title,
},
}); });
if (!data[0]) { if (!data[0]) {
@@ -161,9 +159,7 @@ class SonarrAPI extends ServarrBase<{
public async getSeriesByTvdbId(id: number): Promise<SonarrSeries> { public async getSeriesByTvdbId(id: number): Promise<SonarrSeries> {
try { try {
const data = await this.get<SonarrSeries[]>('/series/lookup', { const data = await this.get<SonarrSeries[]>('/series/lookup', {
params: { term: `tvdb:${id}`,
term: `tvdb:${id}`,
},
}); });
if (!data[0]) { if (!data[0]) {
@@ -349,10 +345,8 @@ class SonarrAPI extends ServarrBase<{
try { try {
const { id, title } = await this.getSeriesByTvdbId(serieId); const { id, title } = await this.getSeriesByTvdbId(serieId);
await this.delete(`/series/${id}`, { await this.delete(`/series/${id}`, {
params: { deleteFiles: 'true',
deleteFiles: 'true', addImportExclusion: 'false',
addImportExclusion: 'false',
},
}); });
logger.info(`[Radarr] Removed serie ${title}`); logger.info(`[Radarr] Removed serie ${title}`);
} catch (e) { } catch (e) {

View File

@@ -1,8 +1,7 @@
import ExternalAPI from '@server/api/externalapi';
import type { User } from '@server/entity/User'; import type { User } from '@server/entity/User';
import type { TautulliSettings } from '@server/lib/settings'; import type { TautulliSettings } from '@server/lib/settings';
import logger from '@server/logger'; import logger from '@server/logger';
import type { AxiosInstance } from 'axios';
import axios from 'axios';
import { uniqWith } from 'lodash'; import { uniqWith } from 'lodash';
export interface TautulliHistoryRecord { export interface TautulliHistoryRecord {
@@ -113,25 +112,25 @@ interface TautulliInfoResponse {
}; };
} }
class TautulliAPI { class TautulliAPI extends ExternalAPI {
private axios: AxiosInstance;
constructor(settings: TautulliSettings) { constructor(settings: TautulliSettings) {
this.axios = axios.create({ super(
baseURL: `${settings.useSsl ? 'https' : 'http'}://${settings.hostname}:${ `${settings.useSsl ? 'https' : 'http'}://${settings.hostname}:${
settings.port settings.port
}${settings.urlBase ?? ''}`, }${settings.urlBase ?? ''}`,
params: { apikey: settings.apiKey }, {
}); apikey: settings.apiKey || '',
}
);
} }
public async getInfo(): Promise<TautulliInfo> { public async getInfo(): Promise<TautulliInfo> {
try { try {
return ( return (
await this.axios.get<TautulliInfoResponse>('/api/v2', { await this.get<TautulliInfoResponse>('/api/v2', {
params: { cmd: 'get_tautulli_info' }, cmd: 'get_tautulli_info',
}) })
).data.response.data; ).response.data;
} catch (e) { } catch (e) {
logger.error('Something went wrong fetching Tautulli server info', { logger.error('Something went wrong fetching Tautulli server info', {
label: 'Tautulli API', label: 'Tautulli API',
@@ -148,14 +147,12 @@ class TautulliAPI {
): Promise<TautulliWatchStats[]> { ): Promise<TautulliWatchStats[]> {
try { try {
return ( return (
await this.axios.get<TautulliWatchStatsResponse>('/api/v2', { await this.get<TautulliWatchStatsResponse>('/api/v2', {
params: { cmd: 'get_item_watch_time_stats',
cmd: 'get_item_watch_time_stats', rating_key: ratingKey,
rating_key: ratingKey, grouping: '1',
grouping: 1,
},
}) })
).data.response.data; ).response.data;
} catch (e) { } catch (e) {
logger.error( logger.error(
'Something went wrong fetching media watch stats from Tautulli', 'Something went wrong fetching media watch stats from Tautulli',
@@ -176,14 +173,12 @@ class TautulliAPI {
): Promise<TautulliWatchUser[]> { ): Promise<TautulliWatchUser[]> {
try { try {
return ( return (
await this.axios.get<TautulliWatchUsersResponse>('/api/v2', { await this.get<TautulliWatchUsersResponse>('/api/v2', {
params: { cmd: 'get_item_user_stats',
cmd: 'get_item_user_stats', rating_key: ratingKey,
rating_key: ratingKey, grouping: '1',
grouping: 1,
},
}) })
).data.response.data; ).response.data;
} catch (e) { } catch (e) {
logger.error( logger.error(
'Something went wrong fetching media watch users from Tautulli', 'Something went wrong fetching media watch users from Tautulli',
@@ -206,15 +201,13 @@ class TautulliAPI {
} }
return ( return (
await this.axios.get<TautulliWatchStatsResponse>('/api/v2', { await this.get<TautulliWatchStatsResponse>('/api/v2', {
params: { cmd: 'get_user_watch_time_stats',
cmd: 'get_user_watch_time_stats', user_id: user.plexId.toString(),
user_id: user.plexId, query_days: '0',
query_days: 0, grouping: '1',
grouping: 1,
},
}) })
).data.response.data[0]; ).response.data[0];
} catch (e) { } catch (e) {
logger.error( logger.error(
'Something went wrong fetching user watch stats from Tautulli', 'Something went wrong fetching user watch stats from Tautulli',
@@ -245,19 +238,17 @@ class TautulliAPI {
while (results.length < 20) { while (results.length < 20) {
const tautulliData = ( const tautulliData = (
await this.axios.get<TautulliHistoryResponse>('/api/v2', { await this.get<TautulliHistoryResponse>('/api/v2', {
params: { cmd: 'get_history',
cmd: 'get_history', grouping: '1',
grouping: 1, order_column: 'date',
order_column: 'date', order_dir: 'desc',
order_dir: 'desc', user_id: user.plexId.toString(),
user_id: user.plexId, media_type: 'movie,episode',
media_type: 'movie,episode', length: take.toString(),
length: take, start: start.toString(),
start,
},
}) })
).data.response.data.data; ).response.data.data;
if (!tautulliData.length) { if (!tautulliData.length) {
return results; return results;

View File

@@ -112,10 +112,10 @@ class TheMovieDb extends ExternalAPI {
}, },
{ {
nodeCache: cacheManager.getCache('tmdb').data, nodeCache: cacheManager.getCache('tmdb').data,
// rateLimit: { rateLimit: {
// maxRPS: 50, maxRPS: 50,
// id: 'tmdb', id: 'tmdb',
// }, },
} }
); );
this.region = region; this.region = region;
@@ -130,12 +130,10 @@ class TheMovieDb extends ExternalAPI {
}: SearchOptions): Promise<TmdbSearchMultiResponse> => { }: SearchOptions): Promise<TmdbSearchMultiResponse> => {
try { try {
const data = await this.get<TmdbSearchMultiResponse>('/search/multi', { const data = await this.get<TmdbSearchMultiResponse>('/search/multi', {
params: { query,
query, page: page.toString(),
page: page.toString(), include_adult: includeAdult ? 'true' : 'false',
include_adult: includeAdult ? 'true' : 'false', language,
language,
},
}); });
return data; return data;
@@ -158,13 +156,11 @@ class TheMovieDb extends ExternalAPI {
}: SingleSearchOptions): Promise<TmdbSearchMovieResponse> => { }: SingleSearchOptions): Promise<TmdbSearchMovieResponse> => {
try { try {
const data = await this.get<TmdbSearchMovieResponse>('/search/movie', { const data = await this.get<TmdbSearchMovieResponse>('/search/movie', {
params: { query,
query, page: page.toString(),
page: page.toString(), include_adult: includeAdult ? 'true' : 'false',
include_adult: includeAdult ? 'true' : 'false', language,
language, primary_release_year: year?.toString() || '',
primary_release_year: year?.toString() || '',
},
}); });
return data; return data;
@@ -187,13 +183,11 @@ class TheMovieDb extends ExternalAPI {
}: SingleSearchOptions): Promise<TmdbSearchTvResponse> => { }: SingleSearchOptions): Promise<TmdbSearchTvResponse> => {
try { try {
const data = await this.get<TmdbSearchTvResponse>('/search/tv', { const data = await this.get<TmdbSearchTvResponse>('/search/tv', {
params: { query,
query, page: page.toString(),
page: page.toString(), include_adult: includeAdult ? 'true' : 'false',
include_adult: includeAdult ? 'true' : 'false', language,
language, first_air_date_year: year?.toString() || '',
first_air_date_year: year?.toString() || '',
},
}); });
return data; return data;
@@ -216,9 +210,7 @@ class TheMovieDb extends ExternalAPI {
}): Promise<TmdbPersonDetails> => { }): Promise<TmdbPersonDetails> => {
try { try {
const data = await this.get<TmdbPersonDetails>(`/person/${personId}`, { const data = await this.get<TmdbPersonDetails>(`/person/${personId}`, {
params: { language,
language,
},
}); });
return data; return data;
@@ -238,9 +230,7 @@ class TheMovieDb extends ExternalAPI {
const data = await this.get<TmdbPersonCombinedCredits>( const data = await this.get<TmdbPersonCombinedCredits>(
`/person/${personId}/combined_credits`, `/person/${personId}/combined_credits`,
{ {
params: { language,
language,
},
} }
); );
@@ -263,11 +253,9 @@ class TheMovieDb extends ExternalAPI {
const data = await this.get<TmdbMovieDetails>( const data = await this.get<TmdbMovieDetails>(
`/movie/${movieId}`, `/movie/${movieId}`,
{ {
params: { language,
language, append_to_response:
append_to_response: 'credits,external_ids,videos,keywords,release_dates,watch/providers',
'credits,external_ids,videos,keywords,release_dates,watch/providers',
},
}, },
43200 43200
); );
@@ -289,11 +277,9 @@ class TheMovieDb extends ExternalAPI {
const data = await this.get<TmdbTvDetails>( const data = await this.get<TmdbTvDetails>(
`/tv/${tvId}`, `/tv/${tvId}`,
{ {
params: { language,
language, append_to_response:
append_to_response: 'aggregate_credits,credits,external_ids,keywords,videos,content_ratings,watch/providers',
'aggregate_credits,credits,external_ids,keywords,videos,content_ratings,watch/providers',
},
}, },
43200 43200
); );
@@ -317,10 +303,8 @@ class TheMovieDb extends ExternalAPI {
const data = await this.get<TmdbSeasonWithEpisodes>( const data = await this.get<TmdbSeasonWithEpisodes>(
`/tv/${tvId}/season/${seasonNumber}`, `/tv/${tvId}/season/${seasonNumber}`,
{ {
params: { language: language || '',
language: language || '', append_to_response: 'external_ids',
append_to_response: 'external_ids',
},
} }
); );
@@ -343,10 +327,8 @@ class TheMovieDb extends ExternalAPI {
const data = await this.get<TmdbSearchMovieResponse>( const data = await this.get<TmdbSearchMovieResponse>(
`/movie/${movieId}/recommendations`, `/movie/${movieId}/recommendations`,
{ {
params: { page: page.toString(),
page: page.toString(), language,
language,
},
} }
); );
@@ -369,10 +351,8 @@ class TheMovieDb extends ExternalAPI {
const data = await this.get<TmdbSearchMovieResponse>( const data = await this.get<TmdbSearchMovieResponse>(
`/movie/${movieId}/similar`, `/movie/${movieId}/similar`,
{ {
params: { page: page.toString(),
page: page.toString(), language,
language,
},
} }
); );
@@ -395,10 +375,8 @@ class TheMovieDb extends ExternalAPI {
const data = await this.get<TmdbSearchMovieResponse>( const data = await this.get<TmdbSearchMovieResponse>(
`/keyword/${keywordId}/movies`, `/keyword/${keywordId}/movies`,
{ {
params: { page: page.toString(),
page: page.toString(), language,
language,
},
} }
); );
@@ -421,10 +399,8 @@ class TheMovieDb extends ExternalAPI {
const data = await this.get<TmdbSearchTvResponse>( const data = await this.get<TmdbSearchTvResponse>(
`/tv/${tvId}/recommendations`, `/tv/${tvId}/recommendations`,
{ {
params: { page: page.toString(),
page: page.toString(), language,
language,
},
} }
); );
@@ -447,10 +423,8 @@ class TheMovieDb extends ExternalAPI {
}): Promise<TmdbSearchTvResponse> { }): Promise<TmdbSearchTvResponse> {
try { try {
const data = await this.get<TmdbSearchTvResponse>(`/tv/${tvId}/similar`, { const data = await this.get<TmdbSearchTvResponse>(`/tv/${tvId}/similar`, {
params: { page: page.toString(),
page: page.toString(), language,
language,
},
}); });
return data; return data;
@@ -491,40 +465,38 @@ class TheMovieDb extends ExternalAPI {
.split('T')[0]; .split('T')[0];
const data = await this.get<TmdbSearchMovieResponse>('/discover/movie', { const data = await this.get<TmdbSearchMovieResponse>('/discover/movie', {
params: { sort_by: sortBy,
sort_by: sortBy, page: page.toString(),
page: page.toString(), include_adult: includeAdult ? 'true' : 'false',
include_adult: includeAdult ? 'true' : 'false', language,
language, region: this.region || '',
region: this.region || '', with_original_language:
with_original_language: originalLanguage && originalLanguage !== 'all'
originalLanguage && originalLanguage !== 'all' ? originalLanguage
? originalLanguage : originalLanguage === 'all'
: originalLanguage === 'all' ? ''
? '' : this.originalLanguage || '',
: this.originalLanguage || '', // Set our release date values, but check if one is set and not the other,
// Set our release date values, but check if one is set and not the other, // so we can force a past date or a future date. TMDB Requires both values if one is set!
// so we can force a past date or a future date. TMDB Requires both values if one is set! 'primary_release_date.gte':
'primary_release_date.gte': !primaryReleaseDateGte && primaryReleaseDateLte
!primaryReleaseDateGte && primaryReleaseDateLte ? defaultPastDate
? defaultPastDate : primaryReleaseDateGte || '',
: primaryReleaseDateGte || '', 'primary_release_date.lte':
'primary_release_date.lte': !primaryReleaseDateLte && primaryReleaseDateGte
!primaryReleaseDateLte && primaryReleaseDateGte ? defaultFutureDate
? defaultFutureDate : primaryReleaseDateLte || '',
: primaryReleaseDateLte || '', with_genres: genre || '',
with_genres: genre || '', with_companies: studio || '',
with_companies: studio || '', with_keywords: keywords || '',
with_keywords: keywords || '', 'with_runtime.gte': withRuntimeGte || '',
'with_runtime.gte': withRuntimeGte || '', 'with_runtime.lte': withRuntimeLte || '',
'with_runtime.lte': withRuntimeLte || '', 'vote_average.gte': voteAverageGte || '',
'vote_average.gte': voteAverageGte || '', 'vote_average.lte': voteAverageLte || '',
'vote_average.lte': voteAverageLte || '', 'vote_count.gte': voteCountGte || '',
'vote_count.gte': voteCountGte || '', 'vote_count.lte': voteCountLte || '',
'vote_count.lte': voteCountLte || '', watch_region: watchRegion || '',
watch_region: watchRegion || '', with_watch_providers: watchProviders || '',
with_watch_providers: watchProviders || '',
},
}); });
return data; return data;
@@ -566,43 +538,41 @@ class TheMovieDb extends ExternalAPI {
.split('T')[0]; .split('T')[0];
const data = await this.get<TmdbSearchTvResponse>('/discover/tv', { const data = await this.get<TmdbSearchTvResponse>('/discover/tv', {
params: { sort_by: sortBy,
sort_by: sortBy, page: page.toString(),
page: page.toString(), language,
language, region: this.region || '',
region: this.region || '', // Set our release date values, but check if one is set and not the other,
// Set our release date values, but check if one is set and not the other, // so we can force a past date or a future date. TMDB Requires both values if one is set!
// so we can force a past date or a future date. TMDB Requires both values if one is set! 'first_air_date.gte':
'first_air_date.gte': !firstAirDateGte && firstAirDateLte
!firstAirDateGte && firstAirDateLte ? defaultPastDate
? defaultPastDate : firstAirDateGte || '',
: firstAirDateGte || '', 'first_air_date.lte':
'first_air_date.lte': !firstAirDateLte && firstAirDateGte
!firstAirDateLte && firstAirDateGte ? defaultFutureDate
? defaultFutureDate : firstAirDateLte || '',
: firstAirDateLte || '', with_original_language:
with_original_language: originalLanguage && originalLanguage !== 'all'
originalLanguage && originalLanguage !== 'all' ? originalLanguage
? originalLanguage : originalLanguage === 'all'
: originalLanguage === 'all' ? ''
? '' : this.originalLanguage || '',
: this.originalLanguage || '', include_null_first_air_dates: includeEmptyReleaseDate
include_null_first_air_dates: includeEmptyReleaseDate ? 'true'
? 'true' : 'false',
: 'false', with_genres: genre || '',
with_genres: genre || '', with_networks: network?.toString() || '',
with_networks: network?.toString() || '', with_keywords: keywords || '',
with_keywords: keywords || '', 'with_runtime.gte': withRuntimeGte || '',
'with_runtime.gte': withRuntimeGte || '', 'with_runtime.lte': withRuntimeLte || '',
'with_runtime.lte': withRuntimeLte || '', 'vote_average.gte': voteAverageGte || '',
'vote_average.gte': voteAverageGte || '', 'vote_average.lte': voteAverageLte || '',
'vote_average.lte': voteAverageLte || '', 'vote_count.gte': voteCountGte || '',
'vote_count.gte': voteCountGte || '', 'vote_count.lte': voteCountLte || '',
'vote_count.lte': voteCountLte || '', with_watch_providers: watchProviders || '',
with_watch_providers: watchProviders || '', watch_region: watchRegion || '',
watch_region: watchRegion || '', with_status: withStatus || '',
with_status: withStatus || '',
},
}); });
return data; return data;
@@ -622,12 +592,10 @@ class TheMovieDb extends ExternalAPI {
const data = await this.get<TmdbUpcomingMoviesResponse>( const data = await this.get<TmdbUpcomingMoviesResponse>(
'/movie/upcoming', '/movie/upcoming',
{ {
params: { page: page.toString(),
page: page.toString(), language,
language, region: this.region || '',
region: this.region || '', originalLanguage: this.originalLanguage || '',
originalLanguage: this.originalLanguage || '',
},
} }
); );
@@ -650,11 +618,9 @@ class TheMovieDb extends ExternalAPI {
const data = await this.get<TmdbSearchMultiResponse>( const data = await this.get<TmdbSearchMultiResponse>(
`/trending/all/${timeWindow}`, `/trending/all/${timeWindow}`,
{ {
params: { page: page.toString(),
page: page.toString(), language,
language, region: this.region || '',
region: this.region || '',
},
} }
); );
@@ -675,9 +641,7 @@ class TheMovieDb extends ExternalAPI {
const data = await this.get<TmdbSearchMovieResponse>( const data = await this.get<TmdbSearchMovieResponse>(
`/trending/movie/${timeWindow}`, `/trending/movie/${timeWindow}`,
{ {
params: { page: page.toString(),
page: page.toString(),
},
} }
); );
@@ -698,9 +662,7 @@ class TheMovieDb extends ExternalAPI {
const data = await this.get<TmdbSearchTvResponse>( const data = await this.get<TmdbSearchTvResponse>(
`/trending/tv/${timeWindow}`, `/trending/tv/${timeWindow}`,
{ {
params: { page: page.toString(),
page: page.toString(),
},
} }
); );
@@ -729,10 +691,8 @@ class TheMovieDb extends ExternalAPI {
const data = await this.get<TmdbExternalIdResponse>( const data = await this.get<TmdbExternalIdResponse>(
`/find/${externalId}`, `/find/${externalId}`,
{ {
params: { external_source: type === 'imdb' ? 'imdb_id' : 'tvdb_id',
external_source: type === 'imdb' ? 'imdb_id' : 'tvdb_id', language,
language,
},
} }
); );
@@ -822,9 +782,7 @@ class TheMovieDb extends ExternalAPI {
const data = await this.get<TmdbCollection>( const data = await this.get<TmdbCollection>(
`/collection/${collectionId}`, `/collection/${collectionId}`,
{ {
params: { language,
language,
},
} }
); );
@@ -897,9 +855,7 @@ class TheMovieDb extends ExternalAPI {
const data = await this.get<TmdbGenresResult>( const data = await this.get<TmdbGenresResult>(
'/genre/movie/list', '/genre/movie/list',
{ {
params: { language,
language,
},
}, },
86400 // 24 hours 86400 // 24 hours
); );
@@ -911,9 +867,7 @@ class TheMovieDb extends ExternalAPI {
const englishData = await this.get<TmdbGenresResult>( const englishData = await this.get<TmdbGenresResult>(
'/genre/movie/list', '/genre/movie/list',
{ {
params: { language: 'en',
language: 'en',
},
}, },
86400 // 24 hours 86400 // 24 hours
); );
@@ -948,9 +902,7 @@ class TheMovieDb extends ExternalAPI {
const data = await this.get<TmdbGenresResult>( const data = await this.get<TmdbGenresResult>(
'/genre/tv/list', '/genre/tv/list',
{ {
params: { language,
language,
},
}, },
86400 // 24 hours 86400 // 24 hours
); );
@@ -962,9 +914,7 @@ class TheMovieDb extends ExternalAPI {
const englishData = await this.get<TmdbGenresResult>( const englishData = await this.get<TmdbGenresResult>(
'/genre/tv/list', '/genre/tv/list',
{ {
params: { language: 'en',
language: 'en',
},
}, },
86400 // 24 hours 86400 // 24 hours
); );
@@ -1019,10 +969,8 @@ class TheMovieDb extends ExternalAPI {
const data = await this.get<TmdbKeywordSearchResponse>( const data = await this.get<TmdbKeywordSearchResponse>(
'/search/keyword', '/search/keyword',
{ {
params: { query,
query, page: page.toString(),
page: page.toString(),
},
}, },
86400 // 24 hours 86400 // 24 hours
); );
@@ -1044,10 +992,8 @@ class TheMovieDb extends ExternalAPI {
const data = await this.get<TmdbCompanySearchResponse>( const data = await this.get<TmdbCompanySearchResponse>(
'/search/company', '/search/company',
{ {
params: { query,
query, page: page.toString(),
page: page.toString(),
},
}, },
86400 // 24 hours 86400 // 24 hours
); );
@@ -1067,9 +1013,7 @@ class TheMovieDb extends ExternalAPI {
const data = await this.get<{ results: TmdbWatchProviderRegion[] }>( const data = await this.get<{ results: TmdbWatchProviderRegion[] }>(
'/watch/providers/regions', '/watch/providers/regions',
{ {
params: { language: language ? this.originalLanguage || '' : '',
language: language ? this.originalLanguage || '' : '',
},
}, },
86400 // 24 hours 86400 // 24 hours
); );
@@ -1093,10 +1037,8 @@ class TheMovieDb extends ExternalAPI {
const data = await this.get<{ results: TmdbWatchProviderDetails[] }>( const data = await this.get<{ results: TmdbWatchProviderDetails[] }>(
'/watch/providers/movie', '/watch/providers/movie',
{ {
params: { language: language ? this.originalLanguage || '' : '',
language: language ? this.originalLanguage || '' : '', watch_region: watchRegion,
watch_region: watchRegion,
},
}, },
86400 // 24 hours 86400 // 24 hours
); );
@@ -1120,10 +1062,8 @@ class TheMovieDb extends ExternalAPI {
const data = await this.get<{ results: TmdbWatchProviderDetails[] }>( const data = await this.get<{ results: TmdbWatchProviderDetails[] }>(
'/watch/providers/tv', '/watch/providers/tv',
{ {
params: { language: language ? this.originalLanguage || '' : '',
language: language ? this.originalLanguage || '' : '', watch_region: watchRegion,
watch_region: watchRegion,
},
}, },
86400 // 24 hours 86400 // 24 hours
); );