Compare commits

...

1 Commits

Author SHA1 Message Date
Gauthier
b133d5a8a6 refactor: try to switch externalapi to axios to fix performance issue 2024-11-06 21:16:47 +01:00
12 changed files with 459 additions and 467 deletions

View File

@@ -43,6 +43,7 @@
"@svgr/webpack": "6.5.1",
"@tanem/react-nprogress": "5.0.30",
"ace-builds": "1.15.2",
"axios": "^1.7.7",
"bcrypt": "5.1.0",
"bowser": "2.11.0",
"connect-typeorm": "1.1.4",

40
pnpm-lock.yaml generated
View File

@@ -41,6 +41,9 @@ importers:
ace-builds:
specifier: 1.15.2
version: 1.15.2
axios:
specifier: ^1.7.7
version: 1.7.7
bcrypt:
specifier: 5.1.0
version: 5.1.0(encoding@0.1.13)
@@ -3402,6 +3405,9 @@ packages:
resolution: {integrity: sha512-QbUdXJVTpvUTHU7871ppZkdOLBeGUKBQWHkHrvN2V9IQWGMt61zf3B45BtzjxEJzYuj0JBjBZP/hmYS/R9pmAw==}
engines: {node: '>=4'}
axios@1.7.7:
resolution: {integrity: sha512-S4kL7XrjgBmvdGut0sN3yJxqYzrDOnivkBiN0OFs6hLiUam3UPvswUo0kqGyhqUZGEOytHyumEdXsAkgCOUf3Q==}
axobject-query@3.1.1:
resolution: {integrity: sha512-goKlv8DZrK9hUh975fnHzhNIO4jUnFCfv/dszV5VwUGDFjI6vQ2VwoyjYjYNEbBE8AH87TduWP5uyDR1D+Iteg==}
@@ -5022,6 +5028,15 @@ packages:
fn.name@1.1.0:
resolution: {integrity: sha512-GRnmB5gPyJpAhTQdSZTSp9uaPSvl09KoYcMQtsB9rQoOmzs9dH6ffeccH+Z+cv6P68Hu5bC6JjRh4Ah/mHSNRw==}
follow-redirects@1.15.9:
resolution: {integrity: sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==}
engines: {node: '>=4.0'}
peerDependencies:
debug: '*'
peerDependenciesMeta:
debug:
optional: true
for-each@0.3.3:
resolution: {integrity: sha512-jqYfLp7mo9vIyQf8ykW2v7A+2N4QjeCeI5+Dz9XraiO1ign81wjiH7Fb9vSOWvQfNtmSa4H2RoQTrrXivdUZmw==}
@@ -5036,6 +5051,10 @@ packages:
resolution: {integrity: sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ==}
engines: {node: '>= 0.12'}
form-data@4.0.1:
resolution: {integrity: sha512-tzN8e4TX8+kkxGPK8D5u0FNmjPUjw3lwC9lSLxxoB/+GtsJG91CO8bSWy73APlgAZzZbXEYZJuxjkHH2w+Ezhw==}
engines: {node: '>= 6'}
formik@2.4.6:
resolution: {integrity: sha512-A+2EI7U7aG296q2TLGvNapDNTZp1khVt5Vk0Q/fyfSROss0V/V6+txt2aJnwEos44IxTCW/LYAi/zgWzlevj+g==}
peerDependencies:
@@ -7384,6 +7403,9 @@ packages:
proxy-from-env@1.0.0:
resolution: {integrity: sha512-F2JHgJQ1iqwnHDcQjVBsq3n/uoaFL+iPW/eAeL7kVxy/2RrWaN4WroKjjvbsoRtv0ftelNyC01bjRhn/bhcf4A==}
proxy-from-env@1.1.0:
resolution: {integrity: sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==}
psl@1.9.0:
resolution: {integrity: sha512-E/ZsdU4HLs/68gYzgGTkMicWTLPdAftJLfJFlLUAAKZGkStNU72sZjT66SnMDVOfOWY/YAoiD7Jxa9iHvngcag==}
@@ -13263,6 +13285,14 @@ snapshots:
axe-core@4.9.1: {}
axios@1.7.7:
dependencies:
follow-redirects: 1.15.9
form-data: 4.0.1
proxy-from-env: 1.1.0
transitivePeerDependencies:
- debug
axobject-query@3.1.1:
dependencies:
deep-equal: 2.2.3
@@ -15278,6 +15308,8 @@ snapshots:
fn.name@1.1.0: {}
follow-redirects@1.15.9: {}
for-each@0.3.3:
dependencies:
is-callable: 1.2.7
@@ -15295,6 +15327,12 @@ snapshots:
combined-stream: 1.0.8
mime-types: 2.1.35
form-data@4.0.1:
dependencies:
asynckit: 0.4.0
combined-stream: 1.0.8
mime-types: 2.1.35
formik@2.4.6(react@18.3.1):
dependencies:
'@types/hoist-non-react-statics': 3.3.5
@@ -17842,6 +17880,8 @@ snapshots:
proxy-from-env@1.0.0: {}
proxy-from-env@1.1.0: {}
psl@1.9.0: {}
pstree.remy@1.1.8: {}

View File

@@ -1,5 +1,6 @@
import type { RateLimitOptions } from '@server/utils/rateLimit';
import rateLimit from '@server/utils/rateLimit';
import type { AxiosInstance, AxiosRequestConfig } from 'axios';
import axios from 'axios';
// import rateLimit from 'axios-rate-limit';
import type NodeCache from 'node-cache';
// 5 minute default TTL (in seconds)
@@ -11,101 +12,71 @@ const DEFAULT_ROLLING_BUFFER = 10000;
interface ExternalAPIOptions {
nodeCache?: NodeCache;
headers?: Record<string, unknown>;
rateLimit?: RateLimitOptions;
rateLimit?: {
maxRPS: number;
maxRequests: number;
};
}
class ExternalAPI {
protected fetch: typeof fetch;
protected params: Record<string, string>;
protected defaultHeaders: { [key: string]: string };
protected axios: AxiosInstance;
private baseUrl: string;
private cache?: NodeCache;
constructor(
baseUrl: string,
params: Record<string, string> = {},
params: Record<string, unknown>,
options: ExternalAPIOptions = {}
) {
if (options.rateLimit) {
this.fetch = rateLimit(fetch, options.rateLimit);
} else {
this.fetch = fetch;
}
this.axios = axios.create({
baseURL: baseUrl,
params,
headers: {
'Content-Type': 'application/json',
Accept: 'application/json',
...options.headers,
},
});
const url = new URL(baseUrl);
this.defaultHeaders = {
'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();
}
// if (options.rateLimit) {
// this.axios = rateLimit(this.axios, {
// maxRequests: options.rateLimit.maxRequests,
// maxRPS: options.rateLimit.maxRPS,
// });
// }
this.baseUrl = baseUrl;
this.params = params;
this.cache = options.nodeCache;
}
protected async get<T>(
endpoint: string,
params?: Record<string, string>,
ttl?: number,
config?: RequestInit
config?: AxiosRequestConfig,
ttl?: number
): Promise<T> {
const cacheKey = this.serializeCacheKey(endpoint, {
...this.params,
...params,
});
const cacheKey = this.serializeCacheKey(endpoint, config?.params);
const cachedItem = this.cache?.get<T>(cacheKey);
if (cachedItem) {
return cachedItem;
}
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);
const response = await this.axios.get<T>(endpoint, config);
if (this.cache && ttl !== 0) {
this.cache.set(cacheKey, data, ttl ?? DEFAULT_TTL);
if (this.cache) {
this.cache.set(cacheKey, response.data, ttl ?? DEFAULT_TTL);
}
return data;
return response.data;
}
protected async post<T>(
endpoint: string,
data?: Record<string, unknown>,
params?: Record<string, string>,
ttl?: number,
config?: RequestInit
config?: AxiosRequestConfig,
ttl?: number
): Promise<T> {
const cacheKey = this.serializeCacheKey(endpoint, {
config: { ...this.params, ...params },
config: config?.params,
data,
});
const cachedItem = this.cache?.get<T>(cacheKey);
@@ -113,43 +84,23 @@ class ExternalAPI {
return cachedItem;
}
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);
const response = await this.axios.post<T>(endpoint, data, config);
if (this.cache && ttl !== 0) {
this.cache.set(cacheKey, resData, ttl ?? DEFAULT_TTL);
if (this.cache) {
this.cache.set(cacheKey, response.data, ttl ?? DEFAULT_TTL);
}
return resData;
return response.data;
}
protected async put<T>(
endpoint: string,
data: Record<string, unknown>,
params?: Record<string, string>,
ttl?: number,
config?: RequestInit
config?: AxiosRequestConfig,
ttl?: number
): Promise<T> {
const cacheKey = this.serializeCacheKey(endpoint, {
config: { ...this.params, ...params },
config: config?.params,
data,
});
const cachedItem = this.cache?.get<T>(cacheKey);
@@ -157,73 +108,41 @@ class ExternalAPI {
return cachedItem;
}
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);
const response = await this.axios.put<T>(endpoint, data, config);
if (this.cache && ttl !== 0) {
this.cache.set(cacheKey, resData, ttl ?? DEFAULT_TTL);
if (this.cache) {
this.cache.set(cacheKey, response.data, ttl ?? DEFAULT_TTL);
}
return resData;
return response.data;
}
protected async delete<T>(
endpoint: string,
params?: Record<string, string>,
config?: RequestInit
config?: AxiosRequestConfig,
ttl?: number
): Promise<T> {
const url = this.formatUrl(endpoint, params);
const response = await this.fetch(url, {
method: 'DELETE',
...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 cacheKey = this.serializeCacheKey(endpoint, config?.params);
const cachedItem = this.cache?.get<T>(cacheKey);
if (cachedItem) {
return cachedItem;
}
const data = await this.getDataFromResponse(response);
return data;
const response = await this.axios.delete<T>(endpoint, config);
if (this.cache) {
this.cache.set(cacheKey, response.data, ttl ?? DEFAULT_TTL);
}
return response.data;
}
protected async getRolling<T>(
endpoint: string,
params?: Record<string, string>,
ttl?: number,
config?: RequestInit,
overwriteBaseUrl?: string
config?: AxiosRequestConfig,
ttl?: number
): Promise<T> {
const cacheKey = this.serializeCacheKey(endpoint, {
...this.params,
...params,
});
const cacheKey = this.serializeCacheKey(endpoint, config?.params);
const cachedItem = this.cache?.get<T>(cacheKey);
if (cachedItem) {
@@ -234,78 +153,20 @@ class ExternalAPI {
keyTtl - (ttl ?? DEFAULT_TTL) * 1000 <
Date.now() - DEFAULT_ROLLING_BUFFER
) {
const url = this.formatUrl(endpoint, params, overwriteBaseUrl);
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);
this.axios.get<T>(endpoint, config).then((response) => {
this.cache?.set(cacheKey, response.data, ttl ?? DEFAULT_TTL);
});
}
return cachedItem;
}
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);
const response = await this.axios.get<T>(endpoint, config);
if (this.cache) {
this.cache.set(cacheKey, data, ttl ?? DEFAULT_TTL);
this.cache.set(cacheKey, response.data, ttl ?? DEFAULT_TTL);
}
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())
);
return response.data;
}
private serializeCacheKey(
@@ -318,29 +179,6 @@ class ExternalAPI {
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;

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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