refactor(tvdb): replace indexer by metadata providers

This commit is contained in:
TOomaAh
2025-09-01 00:13:53 +02:00
parent 82d81fd1d1
commit d7655e520d
11 changed files with 91 additions and 70 deletions

View File

View File

@@ -16,8 +16,9 @@ describe('TVDB Integration', () => {
metadataSaveButton: '[data-testid="metadata-save-button"]',
tmdbStatus: '[data-testid="tmdb-status"]',
tvdbStatus: '[data-testid="tvdb-status"]',
tvIndexerSelector: '[data-testid="tv-indexer-selector"]',
animeIndexerSelector: '[data-testid="anime-indexer-selector"]',
tvMetadataProviderSelector: '[data-testid="tv-metadata-provider-selector"]',
animeMetadataProviderSelector:
'[data-testid="anime-metadata-provider-selector"]',
seasonSelector: '[data-testid="season-selector"]',
season1: 'Season 1',
season2: 'Season 2',
@@ -50,7 +51,7 @@ describe('TVDB Integration', () => {
req.body = customBody;
}).as('saveMetadata');
} else {
// Sinon, juste intercepter sans modifier
// Else just intercept without modifying body
cy.intercept('PUT', '/api/v1/settings/metadatas').as('saveMetadata');
}
@@ -69,10 +70,10 @@ describe('TVDB Integration', () => {
cy.contains('h3', 'Metadata Providers').should('be.visible');
// Configure TVDB as TV provider and test connection
cy.get('[data-testid="tv-indexer-selector"]').click();
cy.get(SELECTORS.tvMetadataProviderSelector).click();
// get id react-select-4-option-1
cy.get('[id^="react-select-4-option-"]').contains('TheTVDB').click();
cy.get('[class*="react-select__option"]').contains('TheTVDB').click();
// Test the connection
testAndVerifyMetadataConnection().then(({ response }) => {

View File

@@ -1,7 +1,7 @@
import type { TvShowProvider } from '@server/api/provider';
import TheMovieDb from '@server/api/themoviedb';
import Tvdb from '@server/api/tvdb';
import { getSettings, IndexerType } from '@server/lib/settings';
import { getSettings, MetadataProviderType } from '@server/lib/settings';
import logger from '@server/logger';
export const getMetadataProvider = async (
@@ -14,13 +14,16 @@ export const getMetadataProvider = async (
return new TheMovieDb();
}
if (mediaType == 'tv' && settings.metadataSettings.tv == IndexerType.TVDB) {
if (
mediaType == 'tv' &&
settings.metadataSettings.tv == MetadataProviderType.TVDB
) {
return await Tvdb.getInstance();
}
if (
mediaType == 'anime' &&
settings.metadataSettings.anime == IndexerType.TVDB
settings.metadataSettings.anime == MetadataProviderType.TVDB
) {
return await Tvdb.getInstance();
}

View File

@@ -219,14 +219,14 @@ class JellyfinScanner {
throw new Error('No ID provided');
}
const indexer = tvShow.keywords.results.some(
const metadataProvider = tvShow.keywords.results.some(
(keyword: TmdbKeyword) => keyword.id === ANIME_KEYWORD_ID
)
? await getMetadataProvider('anime')
: await getMetadataProvider('tv');
if (!(indexer instanceof TheMovieDb)) {
tvShow = await indexer.getTvShow({
if (!(metadataProvider instanceof TheMovieDb)) {
tvShow = await metadataProvider.getTvShow({
tvId: Number(tmdbId),
});
}

View File

@@ -276,14 +276,14 @@ class PlexScanner
throw new Error('No ID provided');
}
const indexer = tvShow.keywords.results.some(
const metadataProvider = tvShow.keywords.results.some(
(keyword: TmdbKeyword) => keyword.id === ANIME_KEYWORD_ID
)
? await getMetadataProvider('anime')
: await getMetadataProvider('tv');
if (!(indexer instanceof TheMovieDb)) {
tvShow = await indexer.getTvShow({
if (!(metadataProvider instanceof TheMovieDb)) {
tvShow = await metadataProvider.getTvShow({
tvId: Number(tmdbId),
});
}

View File

@@ -100,14 +100,14 @@ interface Quota {
quotaDays?: number;
}
export enum IndexerType {
export enum MetadataProviderType {
TMDB = 'tmdb',
TVDB = 'tvdb',
}
export interface MetadataSettings {
tv: IndexerType;
anime: IndexerType;
tv: MetadataProviderType;
anime: MetadataProviderType;
}
export interface ProxySettings {
@@ -422,8 +422,8 @@ class Settings {
},
tautulli: {},
metadataSettings: {
tv: IndexerType.TMDB,
anime: IndexerType.TMDB,
tv: MetadataProviderType.TMDB,
anime: MetadataProviderType.TMDB,
},
radarr: [],
sonarr: [],

View File

@@ -2,7 +2,7 @@ import TheMovieDb from '@server/api/themoviedb';
import Tvdb from '@server/api/tvdb';
import {
getSettings,
IndexerType,
MetadataProviderType,
type MetadataSettings,
} from '@server/lib/settings';
import logger from '@server/logger';
@@ -32,21 +32,27 @@ metadataRoutes.put('/', async (req, res) => {
let tmdbTest = -1;
try {
if (body.tv === IndexerType.TVDB || body.anime === IndexerType.TVDB) {
if (
body.tv === MetadataProviderType.TVDB ||
body.anime === MetadataProviderType.TVDB
) {
tvdbTest = 0;
const tvdb = await Tvdb.getInstance();
await tvdb.test();
tvdbTest = 1;
}
} catch (e) {
logger.error('Failed to test indexers', {
logger.error('Failed to test metadata provider', {
label: 'Metadata',
message: e.message,
});
}
try {
if (body.tv === IndexerType.TMDB || body.anime === IndexerType.TMDB) {
if (
body.tv === MetadataProviderType.TMDB ||
body.anime === MetadataProviderType.TMDB
) {
tmdbTest = 0;
const tmdb = new TheMovieDb();
await tmdb.getTvShow({ tvId: 1054 });

View File

@@ -21,12 +21,12 @@ tvRoutes.get('/:id', async (req, res, next) => {
const tmdbTv = await tmdb.getTvShow({
tvId: Number(req.params.id),
});
const indexer = tmdbTv.keywords.results.some(
const metadataProvider = tmdbTv.keywords.results.some(
(keyword: TmdbKeyword) => keyword.id === ANIME_KEYWORD_ID
)
? await getMetadataProvider('anime')
: await getMetadataProvider('tv');
const tv = await indexer.getTvShow({
const tv = await metadataProvider.getTvShow({
tvId: Number(req.params.id),
language: (req.query.language as string) ?? req.locale,
});
@@ -45,7 +45,7 @@ tvRoutes.get('/:id', async (req, res, next) => {
// TMDB issue where it doesnt fallback to English when no overview is available in requested locale.
if (!data.overview) {
const tvEnglish = await indexer.getTvShow({
const tvEnglish = await metadataProvider.getTvShow({
tvId: Number(req.params.id),
});
data.overview = tvEnglish.overview;
@@ -71,13 +71,13 @@ tvRoutes.get('/:id/season/:seasonNumber', async (req, res, next) => {
const tmdbTv = await tmdb.getTvShow({
tvId: Number(req.params.id),
});
const indexer = tmdbTv.keywords.results.some(
const metadataProvider = tmdbTv.keywords.results.some(
(keyword: TmdbKeyword) => keyword.id === ANIME_KEYWORD_ID
)
? await getMetadataProvider('anime')
: await getMetadataProvider('tv');
const season = await indexer.getTvSeason({
const season = await metadataProvider.getTvSeason({
tvId: Number(req.params.id),
seasonNumber: Number(req.params.seasonNumber),
});

View File

@@ -5,14 +5,14 @@ import React from 'react';
import { useIntl } from 'react-intl';
import Select, { type StylesConfig } from 'react-select';
enum IndexerType {
enum MetadataProviderType {
TMDB = 'tmdb',
TVDB = 'tvdb',
}
type IndexerOptionType = {
type MetadataProviderOptionType = {
testId?: string;
value: IndexerType;
value: MetadataProviderType;
label: string;
icon: React.ReactNode;
};
@@ -20,40 +20,40 @@ type IndexerOptionType = {
const messages = defineMessages('components.MetadataSelector', {
tmdbLabel: 'The Movie Database (TMDB)',
tvdbLabel: 'TheTVDB',
selectIndexer: 'Select a metadata provider',
selectMetdataProvider: 'Select a metadata provider',
});
interface MetadataSelectorProps {
testId: string;
value: IndexerType;
onChange: (value: IndexerType) => void;
value: MetadataProviderType;
onChange: (value: MetadataProviderType) => void;
isDisabled?: boolean;
}
const MetadataSelector = ({
testId = 'indexer-selector',
testId = 'metadata-provider-selector',
value,
onChange,
isDisabled = false,
}: MetadataSelectorProps) => {
const intl = useIntl();
const indexerOptions: IndexerOptionType[] = [
const metadataProviderOptions: MetadataProviderOptionType[] = [
{
testId: 'tmdb-option',
value: IndexerType.TMDB,
value: MetadataProviderType.TMDB,
label: intl.formatMessage(messages.tmdbLabel),
icon: <TmdbLogo />,
},
{
testId: 'tvdb-option',
value: IndexerType.TVDB,
value: MetadataProviderType.TVDB,
label: intl.formatMessage(messages.tvdbLabel),
icon: <TvdbLogo />,
},
];
const customStyles: StylesConfig<IndexerOptionType, false> = {
const customStyles: StylesConfig<MetadataProviderOptionType, false> = {
option: (base) => ({
...base,
display: 'flex',
@@ -66,7 +66,7 @@ const MetadataSelector = ({
}),
};
const formatOptionLabel = (option: IndexerOptionType) => (
const formatOptionLabel = (option: MetadataProviderOptionType) => (
<div className="flex items-center">
{option.icon}
<span data-testid={option.testId}>{option.label}</span>
@@ -76,17 +76,17 @@ const MetadataSelector = ({
return (
<div data-testid={testId}>
<Select
options={indexerOptions}
options={metadataProviderOptions}
isDisabled={isDisabled}
className="react-select-container"
classNamePrefix="react-select"
value={indexerOptions.find((option) => option.value === value)}
value={metadataProviderOptions.find((option) => option.value === value)}
onChange={(selectedOption) => {
if (selectedOption) {
onChange(selectedOption.value);
}
}}
placeholder={intl.formatMessage(messages.selectIndexer)}
placeholder={intl.formatMessage(messages.selectMetdataProvider)}
styles={customStyles}
formatOptionLabel={formatOptionLabel}
/>
@@ -94,5 +94,5 @@ const MetadataSelector = ({
);
};
export { IndexerType };
export { MetadataProviderType };
export default MetadataSelector;

View File

@@ -3,7 +3,7 @@ import Button from '@app/components/Common/Button';
import LoadingSpinner from '@app/components/Common/LoadingSpinner';
import PageTitle from '@app/components/Common/PageTitle';
import MetadataSelector, {
IndexerType,
MetadataProviderType,
} from '@app/components/MetadataSelector';
import globalMessages from '@app/i18n/globalMessages';
import defineMessages from '@app/utils/defineMessages';
@@ -16,10 +16,11 @@ import { useToasts } from 'react-toast-notifications';
import useSWR from 'swr';
const messages = defineMessages('components.Settings', {
metadataProviderSettings: 'Metadata Providers',
general: 'General',
settings: 'Settings',
seriesIndexer: 'Series metadata provider',
animeIndexer: 'Anime metadata provider',
seriesMetadataProvider: 'Series metadata provider',
animeMetadataProvider: 'Anime metadata provider',
metadataSettings: 'Settings for metadata provider',
clickTest:
'Click on the "Test" button to check connectivity with metadata providers',
@@ -28,7 +29,7 @@ const messages = defineMessages('components.Settings', {
operational: 'Operational',
providerStatus: 'Metadata Provider Status',
chooseProvider: 'Choose metadata providers for different content types',
indexerSelection: 'Metadata Provider Selection',
metadataProviderSelection: 'Metadata Provider Selection',
tmdbProviderDoesnotWork:
'TMDB provider does not work, please select another metadata provider',
tvdbProviderDoesnotWork:
@@ -48,8 +49,8 @@ interface ProviderResponse {
}
interface MetadataValues {
tv: IndexerType;
anime: IndexerType;
tv: MetadataProviderType;
anime: MetadataProviderType;
}
interface MetadataSettings {
@@ -72,8 +73,8 @@ const SettingsMetadata = () => {
'/api/v1/settings/metadatas',
async (url: string) => {
const response = await axios.get<{
tv: IndexerType;
anime: IndexerType;
tv: MetadataProviderType;
anime: MetadataProviderType;
}>(url);
return {
@@ -89,9 +90,11 @@ const SettingsMetadata = () => {
values: MetadataValues
): Promise<ProviderResponse> => {
const useTmdb =
values.tv === IndexerType.TMDB || values.anime === IndexerType.TMDB;
values.tv === MetadataProviderType.TMDB ||
values.anime === MetadataProviderType.TMDB;
const useTvdb =
values.tv === IndexerType.TVDB || values.anime === IndexerType.TVDB;
values.tv === MetadataProviderType.TVDB ||
values.anime === MetadataProviderType.TVDB;
const testData = {
tmdb: useTmdb,
@@ -141,8 +144,8 @@ const SettingsMetadata = () => {
try {
const response = await axios.put<{
success: boolean;
tv: IndexerType;
anime: IndexerType;
tv: MetadataProviderType;
anime: MetadataProviderType;
tests?: {
tvdb: ProviderStatus;
tmdb: ProviderStatus;
@@ -252,8 +255,8 @@ const SettingsMetadata = () => {
}
const initialValues: MetadataValues = data?.metadata || {
tv: IndexerType.TMDB,
anime: IndexerType.TMDB,
tv: MetadataProviderType.TMDB,
anime: MetadataProviderType.TMDB,
};
return (
@@ -266,7 +269,9 @@ const SettingsMetadata = () => {
/>
<div className="mb-6">
<h3 className="heading">Metadata</h3>
<h3 className="heading">
{intl.formatMessage(messages.metadataProviderSettings)}
</h3>
<p className="description">
{intl.formatMessage(messages.metadataSettings)}
</p>
@@ -331,7 +336,7 @@ const SettingsMetadata = () => {
<Form className="section" data-testid="settings-main-form">
<div className="mb-6">
<h2 className="heading">
{intl.formatMessage(messages.indexerSelection)}
{intl.formatMessage(messages.metadataProviderSelection)}
</h2>
<p className="description">
{intl.formatMessage(messages.chooseProvider)}
@@ -339,14 +344,17 @@ const SettingsMetadata = () => {
</div>
<div className="form-row">
<label htmlFor="tvIndexer" className="checkbox-label">
<label
htmlFor="tv-metadata-provider"
className="checkbox-label"
>
<span className="mr-2">
{intl.formatMessage(messages.seriesIndexer)}
{intl.formatMessage(messages.seriesMetadataProvider)}
</span>
</label>
<div className="form-input-area">
<MetadataSelector
testId="tv-indexer-selector"
testId="tv-metadata-provider-selector"
value={values.metadata.tv}
onChange={(value) => setFieldValue('metadata.tv', value)}
isDisabled={isSubmitting}
@@ -355,14 +363,17 @@ const SettingsMetadata = () => {
</div>
<div className="form-row">
<label htmlFor="animeIndexer" className="checkbox-label">
<label
htmlFor="anime-metadata-provider"
className="checkbox-label"
>
<span className="mr-2">
{intl.formatMessage(messages.animeIndexer)}
{intl.formatMessage(messages.animeMetadataProvider)}
</span>
</label>
<div className="form-input-area">
<MetadataSelector
testId="anime-indexer-selector"
testId="anime-metadata-provider-selector"
value={values.metadata.anime}
onChange={(value) =>
setFieldValue('metadata.anime', value)

View File

@@ -307,7 +307,7 @@
"components.ManageSlideOver.removearr4k": "Remove from 4K {arr}",
"components.ManageSlideOver.tvshow": "series",
"components.MediaSlider.ShowMoreCard.seemore": "See More",
"components.MetadataSelector.selectIndexer": "Select a metadata provider",
"components.MetadataSelector.selectMetdataProvider": "Select a metadata provider",
"components.MetadataSelector.tmdbLabel": "The Movie Database (TMDB)",
"components.MetadataSelector.tvdbLabel": "TheTVDB",
"components.MovieDetails.MovieCast.fullcast": "Full Cast",
@@ -1096,7 +1096,7 @@
"components.Settings.addsonarr": "Add Sonarr Server",
"components.Settings.advancedTooltip": "Incorrectly configuring this setting may result in broken functionality",
"components.Settings.allChosenProvidersAreOperational": "All chosen metadata providers are operational",
"components.Settings.animeIndexer": "Anime metadata provider",
"components.Settings.animeMetadataProvider": "Anime metadata provider",
"components.Settings.apiKey": "API key",
"components.Settings.blacklistedTagImportInstructions": "Paste blacklist tag configuration below.",
"components.Settings.blacklistedTagImportTitle": "Import Blacklisted Tag Configuration",
@@ -1123,7 +1123,6 @@
"components.Settings.general": "General",
"components.Settings.hostname": "Hostname or IP Address",
"components.Settings.importBlacklistedTagsTip": "Import blacklisted tag configuration",
"components.Settings.indexerSelection": "Metadata Provider Selection",
"components.Settings.invalidKeyword": "{keywordId} is not a TMDB keyword.",
"components.Settings.invalidurlerror": "Unable to connect to {mediaServerName} server.",
"components.Settings.is4k": "4K",
@@ -1157,6 +1156,7 @@
"components.Settings.menuPlexSettings": "Plex",
"components.Settings.menuServices": "Services",
"components.Settings.menuUsers": "Users",
"components.Settings.metadataProviderSelection": "Metadata Provider Selection",
"components.Settings.metadataSettings": "Settings for metadata provider",
"components.Settings.metadataSettingsSaved": "Metadata provider settings saved",
"components.Settings.no": "No",
@@ -1188,7 +1188,7 @@
"components.Settings.scanbackground": "Scanning will run in the background. You can continue the setup process in the meantime.",
"components.Settings.scanning": "Syncing…",
"components.Settings.searchKeywords": "Search keywords…",
"components.Settings.seriesIndexer": "Series metadata provider",
"components.Settings.seriesMetadataProvider": "Series metadata provider",
"components.Settings.serverLocal": "local",
"components.Settings.serverRemote": "remote",
"components.Settings.serverSecure": "secure",