Files
jellyseerr/server/api/externalapi.ts
THOMAS B 22b2824441 feat: add tvdb indexer (#899)
* feat(tvdb): get tv seasons/episodes with tvdb

* fix: fix rate limiter index tvdb indexer

* fix(usersettings): remove unused column tvdbtoken

* refactor(tvdb): replace tvdb api by skyhook

* fix: error during get episodes

* fix: error if tmdb poster is null

* refactor: clean tvdb indexer code

* fix: wrong language with tmdb indexer

* style: replace avalaible to available

* style: tvdb.login to tvdb.test

* fix(test): fix  discover test

* fix(test): wrong url tv-details

* test(tvdb): add tvdb tests

* style(tvdb): rename pokemon to correct tv show

* refactor(indexer): remove unused getSeasonIdentifier method

* refactor(settings): replace tvdb object to boolean type

* refactor(tmdb): reduce still path condition

* test(tvdb): change 'use' to 'tvdb' condition check

* fix(tmdb): fix build

fix build after rebase

* fix(build): revert package.json

* fix(tvdb): ensure that seasons contain data

* refactor(swagger): fix /tvdb/test response

* fix(scanner): add tvdb indexer for scanner

* refactor(tvdb): remove skyhook api

* refactor(tvdb): use tvdb api

* fix(tvdb): rename tvdb to medatada

* refactor(medata): add tvdb settings

* refactor(metadata): rewrite metadata settings

* refactor(metadata): refactor metadata routes

* refactor(metadata): remove french comments

* refactor(metadata): refactor tvdb api calls

* style(prettier): run prettier

* fix(scanner): fix jellyfin scanner with tvdb provider

* fix(scanner): fix plex scanner tvdb provider

* style(provider): change provider name in info section

* style(provider): full provider name in select

* style(provider): remove french comment

* fix(tests): fix all cypress tests

* refactor(tvdb): fix apikey

* refactor(tmdb): apply prettier

* refactor(tvdb): remove logger info

* feat(metadata): replace fetch with axios for API calls

* feat(provider): replace indexer by provider

* fix(tests): fix cypress test

* chore: add project-wide apikey for tvdb

* chore: add correct application-wide key

* fix(test): fix test with default provider tmdb anime

* style(cypress): fix anime name variable

* chore(i18n): remove french translation + apply i18n:extract

* style(wording): standardize naming to "Metadata Provider" in UI text

* docs(comments): translate from French to English

* refactor(tvdb): remove unnecessary try/catch block

* feat(i18n): add missing translations

* fix(scanner): correct metadata provider ID from Tmdb to Tvdb

* style(settings): clarify navigation label from "Metadata" to "Metadata Providers"

* style(logs): update error log label from "Metadata" to "MetadataProvider"

* refactor(tvdb): replace indexer by metadata providers

* refactor(settings): remove metadata providers logo

* fix(config): restore missing config/db/.gitkeep file

---------

Co-authored-by: TOomaAh <ubuntu@PC>
Co-authored-by: fallenbagel <98979876+Fallenbagel@users.noreply.github.com>
2025-09-02 22:40:47 +02:00

157 lines
3.8 KiB
TypeScript

import { requestInterceptorFunction } from '@server/utils/customProxyAgent';
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)
const DEFAULT_TTL = 300;
// 10 seconds default rolling buffer (in ms)
const DEFAULT_ROLLING_BUFFER = 10000;
export interface ExternalAPIOptions {
nodeCache?: NodeCache;
headers?: Record<string, unknown>;
rateLimit?: {
maxRPS: number;
maxRequests: number;
};
}
class ExternalAPI {
protected axios: AxiosInstance;
private baseUrl: string;
private cache?: NodeCache;
constructor(
baseUrl: string,
params: Record<string, unknown>,
options: ExternalAPIOptions = {}
) {
this.axios = axios.create({
baseURL: baseUrl,
params,
headers: {
'Content-Type': 'application/json',
Accept: 'application/json',
...options.headers,
},
});
this.axios.interceptors.request.use(requestInterceptorFunction);
if (options.rateLimit) {
this.axios = rateLimit(this.axios, {
maxRequests: options.rateLimit.maxRequests,
maxRPS: options.rateLimit.maxRPS,
});
}
this.baseUrl = baseUrl;
this.cache = options.nodeCache;
}
protected async get<T>(
endpoint: string,
config?: AxiosRequestConfig,
ttl?: number
): Promise<T> {
const cacheKey = this.serializeCacheKey(endpoint, {
...config?.params,
headers: config?.headers,
});
const cachedItem = this.cache?.get<T>(cacheKey);
if (cachedItem) {
return cachedItem;
}
const response = await this.axios.get<T>(endpoint, config);
if (this.cache && ttl !== 0) {
this.cache.set(cacheKey, response.data, ttl ?? DEFAULT_TTL);
}
return response.data;
}
protected async post<T>(
endpoint: string,
data?: Record<string, unknown>,
config?: AxiosRequestConfig,
ttl?: number
): Promise<T> {
const cacheKey = this.serializeCacheKey(endpoint, {
config: config?.params,
...(data ? { data } : {}),
});
const cachedItem = this.cache?.get<T>(cacheKey);
if (cachedItem) {
return cachedItem;
}
const response = await this.axios.post<T>(endpoint, data, config);
if (this.cache && ttl !== 0) {
this.cache.set(cacheKey, response.data, ttl ?? DEFAULT_TTL);
}
return response.data;
}
protected async getRolling<T>(
endpoint: string,
config?: AxiosRequestConfig,
ttl?: number
): Promise<T> {
const cacheKey = this.serializeCacheKey(endpoint, {
...config?.params,
headers: config?.headers,
});
const cachedItem = this.cache?.get<T>(cacheKey);
if (cachedItem) {
const keyTtl = this.cache?.getTtl(cacheKey) ?? 0;
// If the item has passed our rolling check, fetch again in background
if (
keyTtl - (ttl ?? DEFAULT_TTL) * 1000 <
Date.now() - DEFAULT_ROLLING_BUFFER
) {
this.axios.get<T>(endpoint, config).then((response) => {
this.cache?.set(cacheKey, response.data, ttl ?? DEFAULT_TTL);
});
}
return cachedItem;
}
const response = await this.axios.get<T>(endpoint, config);
if (this.cache && ttl !== 0) {
this.cache.set(cacheKey, response.data, ttl ?? DEFAULT_TTL);
}
return response.data;
}
protected removeCache(endpoint: string, options?: Record<string, unknown>) {
const cacheKey = this.serializeCacheKey(endpoint, {
...options,
});
this.cache?.del(cacheKey);
}
private serializeCacheKey(
endpoint: string,
options?: Record<string, unknown>
) {
if (!options) {
return `${this.baseUrl}${endpoint}`;
}
return `${this.baseUrl}${endpoint}${JSON.stringify(options)}`;
}
}
export default ExternalAPI;