diff --git a/package.json b/package.json index f6af746db..76d4936b8 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 8b68e8b5e..614cc501b 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -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: {} diff --git a/server/api/externalapi.ts b/server/api/externalapi.ts index 0dc1f967d..75dec5eaf 100644 --- a/server/api/externalapi.ts +++ b/server/api/externalapi.ts @@ -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; - rateLimit?: RateLimitOptions; + rateLimit?: { + maxRPS: number; + maxRequests: number; + }; } class ExternalAPI { - protected fetch: typeof fetch; - protected params: Record; - protected defaultHeaders: { [key: string]: string }; + protected axios: AxiosInstance; private baseUrl: string; private cache?: NodeCache; constructor( baseUrl: string, - params: Record = {}, + params: Record, 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( endpoint: string, - params?: Record, - ttl?: number, - config?: RequestInit + config?: AxiosRequestConfig, + ttl?: number ): Promise { - const cacheKey = this.serializeCacheKey(endpoint, { - ...this.params, - ...params, - }); + const cacheKey = this.serializeCacheKey(endpoint, config?.params); const cachedItem = this.cache?.get(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(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( endpoint: string, data?: Record, - params?: Record, - ttl?: number, - config?: RequestInit + config?: AxiosRequestConfig, + ttl?: number ): Promise { const cacheKey = this.serializeCacheKey(endpoint, { - config: { ...this.params, ...params }, + config: config?.params, data, }); const cachedItem = this.cache?.get(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(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( endpoint: string, data: Record, - params?: Record, - ttl?: number, - config?: RequestInit + config?: AxiosRequestConfig, + ttl?: number ): Promise { const cacheKey = this.serializeCacheKey(endpoint, { - config: { ...this.params, ...params }, + config: config?.params, data, }); const cachedItem = this.cache?.get(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(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( endpoint: string, - params?: Record, - config?: RequestInit + config?: AxiosRequestConfig, + ttl?: number ): Promise { - 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(cacheKey); + if (cachedItem) { + return cachedItem; + } + + const response = await this.axios.delete(endpoint, config); + + if (this.cache) { + this.cache.set(cacheKey, response.data, ttl ?? DEFAULT_TTL); } - const data = await this.getDataFromResponse(response); - return data; + return response.data; } protected async getRolling( endpoint: string, - params?: Record, - ttl?: number, - config?: RequestInit, - overwriteBaseUrl?: string + config?: AxiosRequestConfig, + ttl?: number ): Promise { - const cacheKey = this.serializeCacheKey(endpoint, { - ...this.params, - ...params, - }); + const cacheKey = this.serializeCacheKey(endpoint, config?.params); const cachedItem = this.cache?.get(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(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(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, - 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; diff --git a/server/api/github.ts b/server/api/github.ts index c2c3fe6a5..86539903b 100644 --- a/server/api/github.ts +++ b/server/api/github.ts @@ -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( '/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( '/repos/fallenbagel/jellyseerr/commits', { - per_page: take.toString(), - branch, + params: { + per_page: take, + branch, + }, } ); diff --git a/server/api/jellyfin.ts b/server/api/jellyfin.ts index 7b45cdaf7..b55f27f73 100644 --- a/server/api/jellyfin.ts +++ b/server/api/jellyfin.ts @@ -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( `/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( `/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( `/Shows/${seriesID}/Episodes`, { - seasonId: seasonID, + params: { + seasonId: seasonID, + }, } ); diff --git a/server/api/plextv.ts b/server/api/plextv.ts index 27bed1962..704926895 100644 --- a/server/api/plextv.ts +++ b/server/api/plextv.ts @@ -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 { 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 { try { - const account = await this.get( + const account = await this.axios.get( '/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 { - 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( + '/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( - `/library/metadata/${watchlistItem.ratingKey}`, - {}, - undefined, - {}, - 'https://metadata.provider.plex.tv' - ); - - 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') - ); - - 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, - }; - }) + (response.data.MediaContainer.Metadata ?? []).map( + async (watchlistItem) => { + const detailedResponse = await this.getRolling( + `/library/metadata/${watchlistItem.ratingKey}`, + { + baseURL: 'https://metadata.provider.plex.tv', + } + ); + + 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') + ); + + 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) { diff --git a/server/api/pushover.ts b/server/api/pushover.ts index 31d0639f8..41754368d 100644 --- a/server/api/pushover.ts +++ b/server/api/pushover.ts @@ -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 { try { const data = await this.get('/sounds.json', { - token: appToken, + params: { + token: appToken, + }, }); return mapSounds(data.sounds); diff --git a/server/api/servarr/base.ts b/server/api/servarr/base.ts index 8b0d5ca09..f57ad7c43 100644 --- a/server/api/servarr/base.ts +++ b/server/api/servarr/base.ts @@ -160,7 +160,9 @@ class ServarrBase extends ExternalAPI { const data = await this.get>( `/queue`, { - includeEpisode: 'true', + params: { + includeEpisode: 'true', + }, }, 0 ); diff --git a/server/api/servarr/radarr.ts b/server/api/servarr/radarr.ts index 51d300374..050016183 100644 --- a/server/api/servarr/radarr.ts +++ b/server/api/servarr/radarr.ts @@ -58,7 +58,9 @@ class RadarrAPI extends ServarrBase<{ movieId: number }> { public async getMovieByTmdbId(id: number): Promise { try { const data = await this.get('/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) { diff --git a/server/api/servarr/sonarr.ts b/server/api/servarr/sonarr.ts index 8ae054edb..bbe047e22 100644 --- a/server/api/servarr/sonarr.ts +++ b/server/api/servarr/sonarr.ts @@ -138,7 +138,9 @@ class SonarrAPI extends ServarrBase<{ public async getSeriesByTitle(title: string): Promise { try { const data = await this.get('/series/lookup', { - term: title, + params: { + term: title, + }, }); if (!data[0]) { @@ -159,7 +161,9 @@ class SonarrAPI extends ServarrBase<{ public async getSeriesByTvdbId(id: number): Promise { try { const data = await this.get('/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) { diff --git a/server/api/tautulli.ts b/server/api/tautulli.ts index a7e667033..0e5e07071 100644 --- a/server/api/tautulli.ts +++ b/server/api/tautulli.ts @@ -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 { try { return ( - await this.get('/api/v2', { - cmd: 'get_tautulli_info', + await this.axios.get('/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 { try { return ( - await this.get('/api/v2', { - cmd: 'get_item_watch_time_stats', - rating_key: ratingKey, - grouping: '1', + await this.axios.get('/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 { try { return ( - await this.get('/api/v2', { - cmd: 'get_item_user_stats', - rating_key: ratingKey, - grouping: '1', + await this.axios.get('/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('/api/v2', { - cmd: 'get_user_watch_time_stats', - user_id: user.plexId.toString(), - query_days: '0', - grouping: '1', + await this.axios.get('/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('/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('/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; diff --git a/server/api/themoviedb/index.ts b/server/api/themoviedb/index.ts index 6f13ec08a..3fda2c6ff 100644 --- a/server/api/themoviedb/index.ts +++ b/server/api/themoviedb/index.ts @@ -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 => { try { const data = await this.get('/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 => { try { const data = await this.get('/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 => { try { const data = await this.get('/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 => { try { const data = await this.get(`/person/${personId}`, { - language, + params: { + language, + }, }); return data; @@ -230,7 +238,9 @@ class TheMovieDb extends ExternalAPI { const data = await this.get( `/person/${personId}/combined_credits`, { - language, + params: { + language, + }, } ); @@ -253,9 +263,11 @@ class TheMovieDb extends ExternalAPI { const data = await this.get( `/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( `/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( `/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( `/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( `/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( `/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( `/tv/${tvId}/recommendations`, { - page: page.toString(), - language, + params: { + page: page.toString(), + language, + }, } ); @@ -423,8 +447,10 @@ class TheMovieDb extends ExternalAPI { }): Promise { try { const data = await this.get(`/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('/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('/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( '/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( `/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( `/trending/movie/${timeWindow}`, { - page: page.toString(), + params: { + page: page.toString(), + }, } ); @@ -662,7 +698,9 @@ class TheMovieDb extends ExternalAPI { const data = await this.get( `/trending/tv/${timeWindow}`, { - page: page.toString(), + params: { + page: page.toString(), + }, } ); @@ -691,8 +729,10 @@ class TheMovieDb extends ExternalAPI { const data = await this.get( `/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( `/collection/${collectionId}`, { - language, + params: { + language, + }, } ); @@ -855,7 +897,9 @@ class TheMovieDb extends ExternalAPI { const data = await this.get( '/genre/movie/list', { - language, + params: { + language, + }, }, 86400 // 24 hours ); @@ -867,7 +911,9 @@ class TheMovieDb extends ExternalAPI { const englishData = await this.get( '/genre/movie/list', { - language: 'en', + params: { + language: 'en', + }, }, 86400 // 24 hours ); @@ -902,7 +948,9 @@ class TheMovieDb extends ExternalAPI { const data = await this.get( '/genre/tv/list', { - language, + params: { + language, + }, }, 86400 // 24 hours ); @@ -914,7 +962,9 @@ class TheMovieDb extends ExternalAPI { const englishData = await this.get( '/genre/tv/list', { - language: 'en', + params: { + language: 'en', + }, }, 86400 // 24 hours ); @@ -969,8 +1019,10 @@ class TheMovieDb extends ExternalAPI { const data = await this.get( '/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( '/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 );