Files
channels-seerr/src/components/Settings/SettingsMetadata.tsx

369 lines
12 KiB
TypeScript

import Badge from '@app/components/Common/Badge';
import Button from '@app/components/Common/Button';
import LoadingSpinner from '@app/components/Common/LoadingSpinner';
import PageTitle from '@app/components/Common/PageTitle';
import MetadataSelector, {
IndexerType,
} from '@app/components/MetadataSelector';
import globalMessages from '@app/i18n/globalMessages';
import defineMessages from '@app/utils/defineMessages';
import { ArrowDownOnSquareIcon, BeakerIcon } from '@heroicons/react/24/outline';
import { Form, Formik } from 'formik';
import { useState } from 'react';
import { useIntl } from 'react-intl';
import { useToasts } from 'react-toast-notifications';
import useSWR from 'swr';
const messages = defineMessages('components.Settings', {
general: 'General',
settings: 'Settings',
seriesIndexer: 'Series Indexer',
animeIndexer: 'Anime Indexer',
metadataSettings: 'Settings for metadata provider',
clickTest: 'Click on the "Test" button to check connectivity with providers',
notTested: 'Not Tested',
failed: 'Does not work',
operational: 'Operational',
providerStatus: 'Provider Status',
chooseProvider: 'Choose metadata providers for different content types',
indexerSelection: 'Provider Selection',
tmdbProviderDoesnotWork:
'TMDB provider does not work, please select another provider',
tvdbProviderDoesnotWork:
'TVDB provider does not work, please select another provider',
allChosenProvidersAreOperational: 'All chosen providers are operational',
});
type ProviderStatus = 'ok' | 'not tested' | 'failed';
interface ProviderResponse {
tvdb: ProviderStatus;
tmdb: ProviderStatus;
}
interface MetadataValues {
tv: IndexerType;
anime: IndexerType;
}
interface MetadataSettings {
metadata: MetadataValues;
}
const SettingsMetadata = () => {
const intl = useIntl();
const { addToast } = useToasts();
const [isTesting, setIsTesting] = useState(false);
const defaultStatus: ProviderResponse = {
tmdb: 'not tested',
tvdb: 'not tested',
};
const [providerStatus, setProviderStatus] =
useState<ProviderResponse>(defaultStatus);
const { data, error } = useSWR<MetadataSettings>(
'/api/v1/settings/metadatas'
);
const testConnection = async (
values: MetadataValues
): Promise<ProviderResponse> => {
const useTmdb =
values.tv === IndexerType.TMDB || values.anime === IndexerType.TMDB;
const useTvdb =
values.tv === IndexerType.TVDB || values.anime === IndexerType.TVDB;
const testData = {
tmdb: useTmdb,
tvdb: useTvdb,
};
const response = await fetch('/api/v1/settings/metadatas/test', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(testData),
});
if (!response.ok) {
throw new Error('Failed to test connection');
}
const body = (await response.json()) as ProviderResponse;
const newStatus: ProviderResponse = {
tmdb: useTmdb ? body.tmdb : 'not tested',
tvdb: useTvdb ? body.tvdb : 'not tested',
};
setProviderStatus(newStatus);
return newStatus;
};
const saveSettings = async (
values: MetadataValues
): Promise<MetadataSettings> => {
const response = await fetch('/api/v1/settings/metadatas', {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
anime: values.anime,
tv: values.tv,
}),
});
if (!response.ok) {
throw new Error('Failed to save Metadata settings');
}
return (await response.json()) as MetadataSettings;
};
const getStatusClass = (status: ProviderStatus): string => {
switch (status) {
case 'ok':
return 'text-green-500';
case 'not tested':
return 'text-yellow-500';
case 'failed':
return 'text-red-500';
}
};
const getStatusMessage = (status: ProviderStatus): string => {
switch (status) {
case 'ok':
return intl.formatMessage(messages.operational);
case 'not tested':
return intl.formatMessage(messages.notTested);
case 'failed':
return intl.formatMessage(messages.failed);
}
};
const getBadgeType = (
status: ProviderStatus
):
| 'default'
| 'primary'
| 'danger'
| 'warning'
| 'success'
| 'dark'
| 'light'
| undefined => {
switch (status) {
case 'ok':
return 'success';
case 'not tested':
return 'warning';
case 'failed':
return 'danger';
}
};
if (!data && !error) {
return <LoadingSpinner />;
}
const initialValues: MetadataValues = data?.metadata || {
tv: IndexerType.TMDB,
anime: IndexerType.TMDB,
};
return (
<>
<PageTitle
title={[
intl.formatMessage(messages.general),
intl.formatMessage(globalMessages.settings),
]}
/>
<div className="mb-6">
<h3 className="heading">Metadata</h3>
<p className="description">
{intl.formatMessage(messages.metadataSettings)}
</p>
</div>
<div className="mb-6 rounded-lg bg-gray-800 p-4">
<h4 className="mb-3 text-lg font-medium">
{intl.formatMessage(messages.providerStatus)}
</h4>
<div className="flex flex-col space-y-3">
<div className="flex items-center">
<span className="mr-2 w-12">TheMovieDB:</span>
<span className={`text-sm ${getStatusClass(providerStatus.tmdb)}`}>
<Badge badgeType={getBadgeType(providerStatus.tmdb)}>
{getStatusMessage(providerStatus.tmdb)}
</Badge>
</span>
</div>
<div className="flex items-center">
<span className="mr-2 w-12">TheTVDB:</span>
<span className={`text-sm ${getStatusClass(providerStatus.tvdb)}`}>
<Badge badgeType={getBadgeType(providerStatus.tvdb)}>
{getStatusMessage(providerStatus.tvdb)}
</Badge>
</span>
</div>
</div>
</div>
<div className="section">
<Formik
initialValues={{ metadata: initialValues }}
onSubmit={async (values) => {
try {
await saveSettings(values.metadata);
if (data) {
data.metadata = values.metadata;
}
addToast('Metadata settings saved', { appearance: 'success' });
} catch (e) {
addToast('Failed to save metadata settings', {
appearance: 'error',
});
}
}}
>
{({ isSubmitting, isValid, values, setFieldValue }) => {
return (
<Form className="section" data-testid="settings-main-form">
<div className="mb-6">
<h2 className="heading">
{intl.formatMessage(messages.indexerSelection)}
</h2>
<p className="description">
{intl.formatMessage(messages.chooseProvider)}
</p>
</div>
<div className="form-row">
<label htmlFor="tvIndexer" className="checkbox-label">
<span className="mr-2">
{intl.formatMessage(messages.seriesIndexer)}
</span>
</label>
<div className="form-input-area">
<MetadataSelector
value={values.metadata.tv}
onChange={(value) => setFieldValue('metadata.tv', value)}
isDisabled={isSubmitting}
/>
</div>
</div>
<div className="form-row">
<label htmlFor="animeIndexer" className="checkbox-label">
<span className="mr-2">
{intl.formatMessage(messages.animeIndexer)}
</span>
</label>
<div className="form-input-area">
<MetadataSelector
value={values.metadata.anime}
onChange={(value) =>
setFieldValue('metadata.anime', value)
}
isDisabled={isSubmitting}
/>
</div>
</div>
<div className="actions">
<div className="flex justify-end">
<span className="ml-3 inline-flex rounded-md shadow-sm">
<Button
buttonType="warning"
type="button"
disabled={isSubmitting || !isValid}
onClick={async () => {
setIsTesting(true);
try {
const resp = await testConnection(values.metadata);
if (resp.tvdb === 'failed') {
addToast(
intl.formatMessage(
messages.tvdbProviderDoesnotWork
),
{
appearance: 'error',
autoDismiss: true,
}
);
} else if (resp.tmdb === 'failed') {
addToast(
intl.formatMessage(
messages.tmdbProviderDoesnotWork
),
{
appearance: 'error',
autoDismiss: true,
}
);
} else {
addToast(
intl.formatMessage(
messages.allChosenProvidersAreOperational
),
{
appearance: 'success',
}
);
}
} catch (e) {
addToast('Connection test failed', {
appearance: 'error',
autoDismiss: true,
});
} finally {
setIsTesting(false);
}
}}
>
<BeakerIcon />
<span>
{isTesting
? intl.formatMessage(globalMessages.testing)
: intl.formatMessage(globalMessages.test)}
</span>
</Button>
</span>
<span className="ml-3 inline-flex rounded-md shadow-sm">
<Button
data-testid="metadata-save-button"
buttonType="primary"
type="submit"
disabled={isSubmitting || !isValid || isTesting}
>
<ArrowDownOnSquareIcon />
<span>
{isSubmitting
? intl.formatMessage(globalMessages.saving)
: intl.formatMessage(globalMessages.save)}
</span>
</Button>
</span>
</div>
</div>
</Form>
);
}}
</Formik>
</div>
</>
);
};
export default SettingsMetadata;