From 4b0652d7ba2b834e7c06250910b7a3261199d825 Mon Sep 17 00:00:00 2001 From: TOomaAh Date: Wed, 19 Mar 2025 19:43:29 +0100 Subject: [PATCH] refactor(metadata): rewrite metadata settings --- jellyseerr-api.yml | 25 +- server/api/metadata.ts | 6 +- server/api/tvdb/index.ts | 16 +- server/lib/settings/index.ts | 50 +-- server/routes/settings/metadata.ts | 141 +++++++- src/components/MetadataSelector/index.ts | 0 src/components/MetadataSelector/index.tsx | 94 +++++ src/components/Settings/SettingsMetadata.tsx | 344 ++++++++++++------- 8 files changed, 456 insertions(+), 220 deletions(-) delete mode 100644 src/components/MetadataSelector/index.ts create mode 100644 src/components/MetadataSelector/index.tsx diff --git a/jellyseerr-api.yml b/jellyseerr-api.yml index 56887e11..752b85f8 100644 --- a/jellyseerr-api.yml +++ b/jellyseerr-api.yml @@ -533,18 +533,6 @@ components: type: string enum: [tvdb, tmdb] example: 'tvdb' - providers: - type: object - properties: - tvdb: - type: object - properties: - apiKey: - type: string - example: '123456789' - pin: - type: string - example: '1234' TautulliSettings: type: object properties: @@ -2631,6 +2619,19 @@ paths: description: Tests if the TVDB configuration is valid. Returns a list of available languages on success. tags: - settings + requestBody: + required: true + content: + application/json: + schema: + type: object + properties: + tmdb: + type: boolean + example: true + tvdb: + type: boolean + example: true responses: '200': description: Succesfully connected to TVDB diff --git a/server/api/metadata.ts b/server/api/metadata.ts index b9e33858..cd578da4 100644 --- a/server/api/metadata.ts +++ b/server/api/metadata.ts @@ -10,17 +10,17 @@ export const getMetadataProvider = async ( try { const settings = await getSettings(); - if (!settings.tvdb.apiKey || mediaType == 'movie') { + if (mediaType == 'movie') { return new TheMovieDb(); } - if (mediaType == 'tv' && settings.metadataType.tv == IndexerType.TVDB) { + if (mediaType == 'tv' && settings.metadataSettings.tv == IndexerType.TVDB) { return await Tvdb.getInstance(); } if ( mediaType == 'anime' && - settings.metadataType.anime == IndexerType.TVDB + settings.metadataSettings.anime == IndexerType.TVDB ) { return await Tvdb.getInstance(); } diff --git a/server/api/tvdb/index.ts b/server/api/tvdb/index.ts index ffcab177..1298a555 100644 --- a/server/api/tvdb/index.ts +++ b/server/api/tvdb/index.ts @@ -14,7 +14,6 @@ import type { TvdbTvDetails, } from '@server/api/tvdb/interfaces'; import cacheManager, { type AvailableCacheIds } from '@server/lib/cache'; -import { getSettings } from '@server/lib/settings'; import logger from '@server/logger'; interface TvdbConfig { @@ -45,12 +44,12 @@ class Tvdb extends ExternalAPI implements TvShowIndexer { private apiKey?: string; private pin?: string; - constructor(apiKey: string, pin?: string) { + constructor(pin?: string) { const finalConfig = { ...DEFAULT_CONFIG }; super( finalConfig.baseUrl, { - apiKey: apiKey, + apiKey: '', }, { nodeCache: cacheManager.getCache(finalConfig.cachePrefix).data, @@ -60,28 +59,19 @@ class Tvdb extends ExternalAPI implements TvShowIndexer { }, } ); - this.apiKey = apiKey; this.pin = pin; this.tmdb = new TheMovieDb(); } public static async getInstance(): Promise { if (!this.instance) { - const settings = await getSettings(); - - if (!settings.tvdb.apiKey) { - throw new Error('TVDB API key is not set'); - } - try { - this.instance = new Tvdb(settings.tvdb.apiKey, settings.tvdb.pin); + this.instance = new Tvdb(); await this.instance.login(); } catch (error) { logger.error(`Failed to login to TVDB: ${error.message}`); throw new Error('TVDB API key is not set'); } - - this.instance = new Tvdb(settings.tvdb.apiKey, settings.tvdb.pin); } return this.instance; diff --git a/server/lib/settings/index.ts b/server/lib/settings/index.ts index c44810c0..fbc19041 100644 --- a/server/lib/settings/index.ts +++ b/server/lib/settings/index.ts @@ -101,24 +101,11 @@ interface Quota { } export enum IndexerType { - TMDB, - TVDB, + TMDB = 'tmdb', + TVDB = 'tvdb', } export interface MetadataSettings { - settings: MetadataTypeSettings; - providers: ProviderSettings; -} - -export interface TvdbSettings { - apiKey?: string; - pin?: string; -} - -export interface ProviderSettings { - tvdb: TvdbSettings; -} -export interface MetadataTypeSettings { tv: IndexerType; anime: IndexerType; } @@ -373,7 +360,6 @@ export interface AllSettings { notifications: NotificationSettings; jobs: Record; network: NetworkSettings; - tvdb: TvdbSettings; metadataSettings: MetadataSettings; } @@ -436,20 +422,8 @@ class Settings { }, tautulli: {}, metadataSettings: { - settings: { - tv: IndexerType.TMDB, - anime: IndexerType.TVDB, - }, - providers: { - tvdb: { - apiKey: '', - pin: '', - }, - }, - }, - tvdb: { - apiKey: '', - pin: '', + tv: IndexerType.TMDB, + anime: IndexerType.TVDB, }, radarr: [], sonarr: [], @@ -653,22 +627,6 @@ class Settings { this.data.metadataSettings = data; } - get tvdb(): TvdbSettings { - return this.data.tvdb; - } - - set tvdb(data: TvdbSettings) { - this.data.tvdb = data; - } - - get metadataType(): MetadataTypeSettings { - return this.data.metadataSettings.settings; - } - - set metadataType(data: MetadataTypeSettings) { - this.data.metadataSettings.settings = data; - } - get radarr(): RadarrSettings[] { return this.data.radarr; } diff --git a/server/routes/settings/metadata.ts b/server/routes/settings/metadata.ts index 3b5190dc..9c43a0f8 100644 --- a/server/routes/settings/metadata.ts +++ b/server/routes/settings/metadata.ts @@ -1,5 +1,10 @@ +import TheMovieDb from '@server/api/themoviedb'; import Tvdb from '@server/api/tvdb'; -import { getSettings, type MetadataSettings } from '@server/lib/settings'; +import { + getSettings, + IndexerType, + type MetadataSettings, +} from '@server/lib/settings'; import logger from '@server/logger'; import { Router } from 'express'; @@ -8,39 +13,137 @@ const metadataRoutes = Router(); metadataRoutes.get('/', (_req, res) => { const settings = getSettings(); - res.status(200).json(settings.metadataSettings); + res.status(200).json({ + metadata: { + tv: settings.metadataSettings.tv === IndexerType.TMDB ? 'tmdb' : 'tvdb', + anime: + settings.metadataSettings.anime === IndexerType.TMDB ? 'tmdb' : 'tvdb', + }, + }); }); -metadataRoutes.put('/', (req, res) => { +metadataRoutes.put('/', async (req, res) => { const settings = getSettings(); const body = req.body as MetadataSettings; - settings.metadataSettings = { - providers: body.providers, - settings: body.settings, - }; - settings.save(); + // test indexers + let tvdbTest = -1; + let tmdbTest = -1; - return res.status(200).json({ - tvdb: settings.tvdb, + try { + if (body.tv === IndexerType.TVDB || body.anime === IndexerType.TVDB) { + tvdbTest = 0; + const tvdb = await Tvdb.getInstance(); + await tvdb.test(); + tvdbTest = 1; + } + } catch (e) { + logger.error('Failed to test indexers', { + label: 'Metadata', + message: e.message, + }); + } + + try { + if (body.tv === IndexerType.TMDB || body.anime === IndexerType.TMDB) { + tmdbTest = 0; + const tmdb = new TheMovieDb(); + await tmdb.getTvShow({ tvId: 1054 }); + tmdbTest = 1; + } + } catch (e) { + logger.error('Failed to test indexers', { + label: 'Metadata', + message: e.message, + }); + } + + logger.info('Updated metadata settings', { + label: 'Metadata', + body: body, + tmdb: tmdbTest, + tvdb: tvdbTest, + tv: body.tv, + anime: body.anime, + }); + + if (tvdbTest === 0 || tmdbTest === 0) { + return res.status(500).json({ + tvdb: tvdbTest === 1 ? 'ok' : 'failed', + tmdb: tmdbTest === 1 ? 'ok' : 'failed', + }); + } + + settings.metadataSettings = { + tv: body.tv, + anime: body.anime, + }; + await settings.save(); + + res.status(200).json({ + tv: settings.metadataSettings.tv === IndexerType.TMDB ? 'tmdb' : 'tvdb', + anime: + settings.metadataSettings.anime === IndexerType.TMDB ? 'tmdb' : 'tvdb', }); }); -metadataRoutes.post('/test', async (req, res, next) => { - try { - const tvdb = await Tvdb.getInstance(); - await tvdb.test(); +metadataRoutes.post('/test', async (req, res) => { + let tvdbTest = -1; + let tmdbTest = -1; - // TODO: add tmdb test - return res.status(200).json({ tvdb: true }); + try { + const body = req.body as { tmdb: boolean; tvdb: boolean }; + + try { + if (body.tmdb) { + tmdbTest = 0; + const tmdb = new TheMovieDb(); + await tmdb.getTvShow({ tvId: 1054 }); + tmdbTest = 1; + } + } catch (e) { + logger.error('Failed to test indexers', { + label: 'Metadata', + message: e.message, + }); + } + + try { + if (body.tvdb) { + tvdbTest = 0; + const tvdb = await Tvdb.getInstance(); + await tvdb.test(); + tvdbTest = 1; + } + } catch (e) { + logger.error('Failed to test indexers', { + label: 'Metadata', + message: e.message, + }); + } + + const response = { + tmdb: tmdbTest === -1 ? 'not tested' : tmdbTest === 0 ? 'failed' : 'ok', + tvdb: tvdbTest === -1 ? 'not tested' : tvdbTest === 0 ? 'failed' : 'ok', + }; + + return res.status(200).json(response); } catch (e) { - logger.error('Failed to test Tvdb', { - label: 'Tvdb', + logger.error('Failed to test indexers', { + label: 'Metadata', message: e.message, }); - return next({ status: 500, message: 'Failed to connect to Tvdb' }); + // if tmdbTest != -1 (tested) and tmdbTest === 1 (ok) then fail + // if tvdbTest != -1 (tested) and tvdbTest === 1 (ok) then fail + // if test === -1 = 'not tested' if test === 0 = 'failed' if test === 1 = 'ok' + const response = { + tmdb: tmdbTest === -1 ? 'not tested' : tmdbTest === 0 ? 'failed' : 'ok', + tvdb: tvdbTest === -1 ? 'not tested' : tvdbTest === 0 ? 'failed' : 'ok', + }; + + return res.status(500).json(response); } }); diff --git a/src/components/MetadataSelector/index.ts b/src/components/MetadataSelector/index.ts deleted file mode 100644 index e69de29b..00000000 diff --git a/src/components/MetadataSelector/index.tsx b/src/components/MetadataSelector/index.tsx new file mode 100644 index 00000000..1e3fe683 --- /dev/null +++ b/src/components/MetadataSelector/index.tsx @@ -0,0 +1,94 @@ +import TmdbLogo from '@app/assets/services/tmdb.svg'; +import TvdbLogo from '@app/assets/services/tvdb.svg'; +import defineMessages from '@app/utils/defineMessages'; +import React from 'react'; +import { useIntl } from 'react-intl'; +import Select, { type StylesConfig } from 'react-select'; + +enum IndexerType { + TMDB = 'tmdb', + TVDB = 'tvdb', +} + +type IndexerOptionType = { + value: IndexerType; + label: string; + icon: React.ReactNode; +}; + +const messages = defineMessages('components.MetadataSelector', { + tmdbLabel: 'TMDB', + tvdbLabel: 'TVDB', + selectIndexer: 'Select an indexer', +}); + +interface MetadataSelectorProps { + value: IndexerType; + onChange: (value: IndexerType) => void; + isDisabled?: boolean; +} + +const MetadataSelector = ({ + value, + onChange, + isDisabled = false, +}: MetadataSelectorProps) => { + const intl = useIntl(); + + // Options pour les sélecteurs d'indexeurs avec leurs logos + const indexerOptions: IndexerOptionType[] = [ + { + value: IndexerType.TMDB, + label: intl.formatMessage(messages.tmdbLabel), + icon: , + }, + { + value: IndexerType.TVDB, + label: intl.formatMessage(messages.tvdbLabel), + icon: , + }, + ]; + + // Style personnalisé pour inclure les icônes + const customStyles: StylesConfig = { + option: (base) => ({ + ...base, + display: 'flex', + alignItems: 'center', + }), + singleValue: (base) => ({ + ...base, + display: 'flex', + alignItems: 'center', + }), + }; + + // Format personnalisé pour les options avec logo + const formatOptionLabel = (option: IndexerOptionType) => ( +
+ {option.icon} + {option.label} +
+ ); + + return ( +