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>
This commit is contained in:
91
src/components/MetadataSelector/index.tsx
Normal file
91
src/components/MetadataSelector/index.tsx
Normal file
@@ -0,0 +1,91 @@
|
||||
import defineMessages from '@app/utils/defineMessages';
|
||||
import { useIntl } from 'react-intl';
|
||||
import Select, { type StylesConfig } from 'react-select';
|
||||
|
||||
enum MetadataProviderType {
|
||||
TMDB = 'tmdb',
|
||||
TVDB = 'tvdb',
|
||||
}
|
||||
|
||||
type MetadataProviderOptionType = {
|
||||
testId?: string;
|
||||
value: MetadataProviderType;
|
||||
label: string;
|
||||
};
|
||||
|
||||
const messages = defineMessages('components.MetadataSelector', {
|
||||
tmdbLabel: 'The Movie Database (TMDB)',
|
||||
tvdbLabel: 'TheTVDB',
|
||||
selectMetdataProvider: 'Select a metadata provider',
|
||||
});
|
||||
|
||||
interface MetadataSelectorProps {
|
||||
testId: string;
|
||||
value: MetadataProviderType;
|
||||
onChange: (value: MetadataProviderType) => void;
|
||||
isDisabled?: boolean;
|
||||
}
|
||||
|
||||
const MetadataSelector = ({
|
||||
testId = 'metadata-provider-selector',
|
||||
value,
|
||||
onChange,
|
||||
isDisabled = false,
|
||||
}: MetadataSelectorProps) => {
|
||||
const intl = useIntl();
|
||||
|
||||
const metadataProviderOptions: MetadataProviderOptionType[] = [
|
||||
{
|
||||
testId: 'tmdb-option',
|
||||
value: MetadataProviderType.TMDB,
|
||||
label: intl.formatMessage(messages.tmdbLabel),
|
||||
},
|
||||
{
|
||||
testId: 'tvdb-option',
|
||||
value: MetadataProviderType.TVDB,
|
||||
label: intl.formatMessage(messages.tvdbLabel),
|
||||
},
|
||||
];
|
||||
|
||||
const customStyles: StylesConfig<MetadataProviderOptionType, false> = {
|
||||
option: (base) => ({
|
||||
...base,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
}),
|
||||
singleValue: (base) => ({
|
||||
...base,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
}),
|
||||
};
|
||||
|
||||
const formatOptionLabel = (option: MetadataProviderOptionType) => (
|
||||
<div className="flex items-center">
|
||||
<span data-testid={option.testId}>{option.label}</span>
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<div data-testid={testId}>
|
||||
<Select
|
||||
options={metadataProviderOptions}
|
||||
isDisabled={isDisabled}
|
||||
className="react-select-container"
|
||||
classNamePrefix="react-select"
|
||||
value={metadataProviderOptions.find((option) => option.value === value)}
|
||||
onChange={(selectedOption) => {
|
||||
if (selectedOption) {
|
||||
onChange(selectedOption.value);
|
||||
}
|
||||
}}
|
||||
placeholder={intl.formatMessage(messages.selectMetdataProvider)}
|
||||
styles={customStyles}
|
||||
formatOptionLabel={formatOptionLabel}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export { MetadataProviderType };
|
||||
export default MetadataSelector;
|
||||
@@ -18,6 +18,7 @@ const messages = defineMessages('components.Settings', {
|
||||
menuLogs: 'Logs',
|
||||
menuJobs: 'Jobs & Cache',
|
||||
menuAbout: 'About',
|
||||
menuMetadataProviders: 'Metadata Providers',
|
||||
});
|
||||
|
||||
type SettingsLayoutProps = {
|
||||
@@ -59,6 +60,11 @@ const SettingsLayout = ({ children }: SettingsLayoutProps) => {
|
||||
route: '/settings/network',
|
||||
regex: /^\/settings\/network/,
|
||||
},
|
||||
{
|
||||
text: intl.formatMessage(messages.menuMetadataProviders),
|
||||
route: '/settings/metadata',
|
||||
regex: /^\/settings\/metadata/,
|
||||
},
|
||||
{
|
||||
text: intl.formatMessage(messages.menuNotifications),
|
||||
route: '/settings/notifications/email',
|
||||
|
||||
476
src/components/Settings/SettingsMetadata.tsx
Normal file
476
src/components/Settings/SettingsMetadata.tsx
Normal file
@@ -0,0 +1,476 @@
|
||||
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, {
|
||||
MetadataProviderType,
|
||||
} 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 axios from 'axios';
|
||||
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', {
|
||||
metadataProviderSettings: 'Metadata Providers',
|
||||
general: 'General',
|
||||
settings: 'Settings',
|
||||
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',
|
||||
notTested: 'Not Tested',
|
||||
failed: 'Does not work',
|
||||
operational: 'Operational',
|
||||
providerStatus: 'Metadata Provider Status',
|
||||
chooseProvider: 'Choose metadata providers for different content types',
|
||||
metadataProviderSelection: 'Metadata Provider Selection',
|
||||
tmdbProviderDoesnotWork:
|
||||
'TMDB provider does not work, please select another metadata provider',
|
||||
tvdbProviderDoesnotWork:
|
||||
'TVDB provider does not work, please select another metadata provider',
|
||||
allChosenProvidersAreOperational:
|
||||
'All chosen metadata providers are operational',
|
||||
connectionTestFailed: 'Connection test failed',
|
||||
failedToSaveMetadataSettings: 'Failed to save metadata provider settings',
|
||||
metadataSettingsSaved: 'Metadata provider settings saved',
|
||||
});
|
||||
|
||||
type ProviderStatus = 'ok' | 'not tested' | 'failed';
|
||||
|
||||
interface ProviderResponse {
|
||||
tvdb: ProviderStatus;
|
||||
tmdb: ProviderStatus;
|
||||
}
|
||||
|
||||
interface MetadataValues {
|
||||
tv: MetadataProviderType;
|
||||
anime: MetadataProviderType;
|
||||
}
|
||||
|
||||
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',
|
||||
async (url: string) => {
|
||||
const response = await axios.get<{
|
||||
tv: MetadataProviderType;
|
||||
anime: MetadataProviderType;
|
||||
}>(url);
|
||||
|
||||
return {
|
||||
metadata: {
|
||||
tv: response.data.tv,
|
||||
anime: response.data.anime,
|
||||
},
|
||||
};
|
||||
}
|
||||
);
|
||||
|
||||
const testConnection = async (
|
||||
values: MetadataValues
|
||||
): Promise<ProviderResponse> => {
|
||||
const useTmdb =
|
||||
values.tv === MetadataProviderType.TMDB ||
|
||||
values.anime === MetadataProviderType.TMDB;
|
||||
const useTvdb =
|
||||
values.tv === MetadataProviderType.TVDB ||
|
||||
values.anime === MetadataProviderType.TVDB;
|
||||
|
||||
const testData = {
|
||||
tmdb: useTmdb,
|
||||
tvdb: useTvdb,
|
||||
};
|
||||
|
||||
try {
|
||||
const response = await axios.post<{
|
||||
success: boolean;
|
||||
tests: ProviderResponse;
|
||||
}>('/api/v1/settings/metadatas/test', testData);
|
||||
|
||||
const newStatus: ProviderResponse = {
|
||||
tmdb: useTmdb ? response.data.tests.tmdb : 'not tested',
|
||||
tvdb: useTvdb ? response.data.tests.tvdb : 'not tested',
|
||||
};
|
||||
|
||||
setProviderStatus(newStatus);
|
||||
return newStatus;
|
||||
} catch (error) {
|
||||
if (axios.isAxiosError(error) && error.response) {
|
||||
// If we receive an error response with a valid format
|
||||
const errorData = error.response.data as {
|
||||
success: boolean;
|
||||
tests: ProviderResponse;
|
||||
};
|
||||
|
||||
if (errorData.tests) {
|
||||
const newStatus: ProviderResponse = {
|
||||
tmdb: useTmdb ? errorData.tests.tmdb : 'not tested',
|
||||
tvdb: useTvdb ? errorData.tests.tvdb : 'not tested',
|
||||
};
|
||||
|
||||
setProviderStatus(newStatus);
|
||||
return newStatus;
|
||||
}
|
||||
}
|
||||
|
||||
// In case of error without usable data
|
||||
throw new Error('Failed to test connection');
|
||||
}
|
||||
};
|
||||
|
||||
const saveSettings = async (
|
||||
values: MetadataValues
|
||||
): Promise<MetadataSettings> => {
|
||||
try {
|
||||
const response = await axios.put<{
|
||||
success: boolean;
|
||||
tv: MetadataProviderType;
|
||||
anime: MetadataProviderType;
|
||||
tests?: {
|
||||
tvdb: ProviderStatus;
|
||||
tmdb: ProviderStatus;
|
||||
};
|
||||
}>('/api/v1/settings/metadatas', {
|
||||
tv: values.tv,
|
||||
anime: values.anime,
|
||||
});
|
||||
|
||||
// Update metadata provider status if available
|
||||
if (response.data.tests) {
|
||||
const mapStatusValue = (status: string): ProviderStatus => {
|
||||
if (status === 'ok') return 'ok';
|
||||
if (status === 'failed') return 'failed';
|
||||
return 'not tested';
|
||||
};
|
||||
|
||||
setProviderStatus({
|
||||
tmdb: mapStatusValue(response.data.tests.tmdb),
|
||||
tvdb: mapStatusValue(response.data.tests.tvdb),
|
||||
});
|
||||
}
|
||||
|
||||
// Adapt the response to the format expected by the component
|
||||
return {
|
||||
metadata: {
|
||||
tv: response.data.tv,
|
||||
anime: response.data.anime,
|
||||
},
|
||||
};
|
||||
} catch (error) {
|
||||
// Retrieve test data in case of error
|
||||
if (axios.isAxiosError(error) && error.response?.data) {
|
||||
const errorData = error.response.data as {
|
||||
success: boolean;
|
||||
tests?: {
|
||||
tvdb: string;
|
||||
tmdb: string;
|
||||
};
|
||||
};
|
||||
|
||||
// If test data is available in the error response
|
||||
if (errorData.tests) {
|
||||
const mapStatusValue = (status: string): ProviderStatus => {
|
||||
if (status === 'ok') return 'ok';
|
||||
if (status === 'failed') return 'failed';
|
||||
return 'not tested';
|
||||
};
|
||||
|
||||
// Update metadata provider status with error data
|
||||
setProviderStatus({
|
||||
tmdb: mapStatusValue(errorData.tests.tmdb),
|
||||
tvdb: mapStatusValue(errorData.tests.tvdb),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
throw new Error('Failed to save Metadata settings');
|
||||
}
|
||||
};
|
||||
|
||||
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: MetadataProviderType.TMDB,
|
||||
anime: MetadataProviderType.TMDB,
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<PageTitle
|
||||
title={[
|
||||
intl.formatMessage(messages.general),
|
||||
intl.formatMessage(globalMessages.settings),
|
||||
]}
|
||||
/>
|
||||
|
||||
<div className="mb-6">
|
||||
<h3 className="heading">
|
||||
{intl.formatMessage(messages.metadataProviderSettings)}
|
||||
</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-24">TheMovieDB:</span>
|
||||
<span
|
||||
className={`text-sm ${getStatusClass(providerStatus.tmdb)}`}
|
||||
data-testid="tmdb-status-container"
|
||||
>
|
||||
<Badge badgeType={getBadgeType(providerStatus.tmdb)}>
|
||||
{getStatusMessage(providerStatus.tmdb)}
|
||||
</Badge>
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex items-center">
|
||||
<span className="mr-2 w-24">TheTVDB:</span>
|
||||
<span
|
||||
className={`text-sm ${getStatusClass(providerStatus.tvdb)}`}
|
||||
data-testid="tvdb-status"
|
||||
>
|
||||
<Badge badgeType={getBadgeType(providerStatus.tvdb)}>
|
||||
{getStatusMessage(providerStatus.tvdb)}
|
||||
</Badge>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="section">
|
||||
<Formik
|
||||
initialValues={{ metadata: initialValues }}
|
||||
onSubmit={async (values) => {
|
||||
try {
|
||||
const result = await saveSettings(values.metadata);
|
||||
|
||||
if (data) {
|
||||
data.metadata = result.metadata;
|
||||
}
|
||||
|
||||
addToast(intl.formatMessage(messages.metadataSettingsSaved), {
|
||||
appearance: 'success',
|
||||
});
|
||||
} catch (e) {
|
||||
addToast(
|
||||
intl.formatMessage(messages.failedToSaveMetadataSettings),
|
||||
{
|
||||
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.metadataProviderSelection)}
|
||||
</h2>
|
||||
<p className="description">
|
||||
{intl.formatMessage(messages.chooseProvider)}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="form-row">
|
||||
<label
|
||||
htmlFor="tv-metadata-provider"
|
||||
className="checkbox-label"
|
||||
>
|
||||
<span className="mr-2">
|
||||
{intl.formatMessage(messages.seriesMetadataProvider)}
|
||||
</span>
|
||||
</label>
|
||||
<div className="form-input-area">
|
||||
<MetadataSelector
|
||||
testId="tv-metadata-provider-selector"
|
||||
value={values.metadata.tv}
|
||||
onChange={(value) => setFieldValue('metadata.tv', value)}
|
||||
isDisabled={isSubmitting}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="form-row">
|
||||
<label
|
||||
htmlFor="anime-metadata-provider"
|
||||
className="checkbox-label"
|
||||
>
|
||||
<span className="mr-2">
|
||||
{intl.formatMessage(messages.animeMetadataProvider)}
|
||||
</span>
|
||||
</label>
|
||||
<div className="form-input-area">
|
||||
<MetadataSelector
|
||||
testId="anime-metadata-provider-selector"
|
||||
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(
|
||||
intl.formatMessage(messages.connectionTestFailed),
|
||||
{
|
||||
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;
|
||||
@@ -60,7 +60,7 @@ const Season = ({ seasonNumber, tvId }: SeasonProps) => {
|
||||
<CachedImage
|
||||
type="tmdb"
|
||||
className="rounded-lg object-contain"
|
||||
src={`https://image.tmdb.org/t/p/original/${episode.stillPath}`}
|
||||
src={episode.stillPath}
|
||||
alt=""
|
||||
fill
|
||||
/>
|
||||
|
||||
@@ -35,6 +35,7 @@ import { sortCrewPriority } from '@app/utils/creditHelpers';
|
||||
import defineMessages from '@app/utils/defineMessages';
|
||||
import { refreshIntervalHelper } from '@app/utils/refreshIntervalHelper';
|
||||
import { Disclosure, Transition } from '@headlessui/react';
|
||||
import { ChevronDownIcon } from '@heroicons/react/24/outline';
|
||||
import {
|
||||
ArrowRightCircleIcon,
|
||||
CogIcon,
|
||||
@@ -44,8 +45,7 @@ import {
|
||||
MinusCircleIcon,
|
||||
PlayIcon,
|
||||
StarIcon,
|
||||
} from '@heroicons/react/24/outline';
|
||||
import { ChevronDownIcon } from '@heroicons/react/24/solid';
|
||||
} from '@heroicons/react/24/solid';
|
||||
import type { RTRating } from '@server/api/rating/rottentomatoes';
|
||||
import { ANIME_KEYWORD_ID } from '@server/api/themoviedb/constants';
|
||||
import { IssueStatus } from '@server/constants/issue';
|
||||
@@ -118,9 +118,7 @@ const TvDetails = ({ tv }: TvDetailsProps) => {
|
||||
const intl = useIntl();
|
||||
const { locale } = useLocale();
|
||||
const [showRequestModal, setShowRequestModal] = useState(false);
|
||||
const [showManager, setShowManager] = useState(
|
||||
router.query.manage == '1' ? true : false
|
||||
);
|
||||
const [showManager, setShowManager] = useState(router.query.manage == '1');
|
||||
const [showIssueModal, setShowIssueModal] = useState(false);
|
||||
const [isUpdating, setIsUpdating] = useState<boolean>(false);
|
||||
const [toggleWatchlist, setToggleWatchlist] = useState<boolean>(
|
||||
@@ -156,7 +154,7 @@ const TvDetails = ({ tv }: TvDetailsProps) => {
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
setShowManager(router.query.manage == '1' ? true : false);
|
||||
setShowManager(router.query.manage == '1');
|
||||
}, [router.query.manage]);
|
||||
|
||||
const closeBlacklistModal = useCallback(
|
||||
|
||||
Reference in New Issue
Block a user