import BlacklistedTagsSelector from '@app/components/BlacklistedTagsSelector'; import Button from '@app/components/Common/Button'; import LoadingSpinner from '@app/components/Common/LoadingSpinner'; import PageTitle from '@app/components/Common/PageTitle'; import SensitiveInput from '@app/components/Common/SensitiveInput'; import LanguageSelector from '@app/components/LanguageSelector'; import RegionSelector from '@app/components/RegionSelector'; import CopyButton from '@app/components/Settings/CopyButton'; import SettingsBadge from '@app/components/Settings/SettingsBadge'; import { availableLanguages } from '@app/context/LanguageContext'; import useLocale from '@app/hooks/useLocale'; import { Permission, useUser } from '@app/hooks/useUser'; import globalMessages from '@app/i18n/globalMessages'; import defineMessages from '@app/utils/defineMessages'; import { isValidURL } from '@app/utils/urlValidationHelper'; import { ArrowDownOnSquareIcon } from '@heroicons/react/24/outline'; import { ArrowPathIcon } from '@heroicons/react/24/solid'; import type { UserSettingsGeneralResponse } from '@server/interfaces/api/userSettingsInterfaces'; import type { MainSettings } from '@server/lib/settings'; import type { AvailableLocale } from '@server/types/languages'; import axios from 'axios'; import { Field, Form, Formik } from 'formik'; import { useIntl } from 'react-intl'; import { useToasts } from 'react-toast-notifications'; import useSWR, { mutate } from 'swr'; import * as Yup from 'yup'; const messages = defineMessages('components.Settings.SettingsMain', { general: 'General', generalsettings: 'General Settings', generalsettingsDescription: 'Configure global and default settings for Seerr.', apikey: 'API Key', apikeyCopied: 'Copied API key to clipboard.', applicationTitle: 'Application Title', applicationurl: 'Application URL', discoverRegion: 'Discover Region', discoverRegionTip: 'Filter content by regional availability', originallanguage: 'Discover Language', originallanguageTip: 'Filter content by original language', blacklistedTags: 'Blacklist Content with Tags', blacklistedTagsTip: 'Automatically add content with tags to the blacklist using the "Process Blacklisted Tags" job', blacklistedTagsLimit: 'Limit Content Blacklisted per Tag', blacklistedTagsLimitTip: 'The "Process Blacklisted Tags" job will blacklist this many pages into each sort. Larger numbers will create a more accurate blacklist, but use more space.', streamingRegion: 'Streaming Region', streamingRegionTip: 'Show streaming sites by regional availability', hideBlacklisted: 'Hide Blacklisted Items', hideBlacklistedTip: 'Hide blacklisted items from discover pages for all users with the "Manage Blacklist" permission', toastApiKeySuccess: 'New API key generated successfully!', toastApiKeyFailure: 'Something went wrong while generating a new API key.', toastSettingsSuccess: 'Settings saved successfully!', toastSettingsFailure: 'Something went wrong while saving settings.', hideAvailable: 'Hide Available Media', hideAvailableTip: 'Hide available media from the discover pages but not search results', cacheImages: 'Enable Image Caching', cacheImagesTip: 'Cache externally sourced images (requires a significant amount of disk space)', validationApplicationTitle: 'You must provide an application title', validationApplicationUrl: 'You must provide a valid URL', validationApplicationUrlTrailingSlash: 'URL must not end in a trailing slash', partialRequestsEnabled: 'Allow Partial Series Requests', enableSpecialEpisodes: 'Allow Special Episodes Requests', locale: 'Display Language', youtubeUrl: 'YouTube URL', youtubeUrlTip: 'Base URL for YouTube videos if a self-hosted YouTube instance is used.', validationUrl: 'You must provide a valid URL', validationUrlTrailingSlash: 'URL must not end in a trailing slash', }); const SettingsMain = () => { const { addToast } = useToasts(); const { user: currentUser, hasPermission: userHasPermission } = useUser(); const intl = useIntl(); const { setLocale } = useLocale(); const { data, error, mutate: revalidate, } = useSWR('/api/v1/settings/main'); const { data: userData } = useSWR( currentUser ? `/api/v1/user/${currentUser.id}/settings/main` : null ); const MainSettingsSchema = Yup.object().shape({ applicationTitle: Yup.string().required( intl.formatMessage(messages.validationApplicationTitle) ), applicationUrl: Yup.string() .test( 'valid-url', intl.formatMessage(messages.validationApplicationUrl), isValidURL ) .test( 'no-trailing-slash', intl.formatMessage(messages.validationApplicationUrlTrailingSlash), (value) => !value || !value.endsWith('/') ), blacklistedTagsLimit: Yup.number() .test( 'positive', 'Number must be greater than 0.', (value) => (value ?? 0) >= 0 ) .test( 'lte-250', 'Number must be less than or equal to 250.', (value) => (value ?? 0) <= 250 ), youtubeUrl: Yup.string() .url(intl.formatMessage(messages.validationUrl)) .test( 'no-trailing-slash', intl.formatMessage(messages.validationUrlTrailingSlash), (value) => !value || !value.endsWith('/') ), }); const regenerate = async () => { try { await axios.post('/api/v1/settings/main/regenerate'); revalidate(); addToast(intl.formatMessage(messages.toastApiKeySuccess), { autoDismiss: true, appearance: 'success', }); } catch (e) { addToast(intl.formatMessage(messages.toastApiKeyFailure), { autoDismiss: true, appearance: 'error', }); } }; if (!data && !error) { return ; } return ( <>

{intl.formatMessage(messages.generalsettings)}

{intl.formatMessage(messages.generalsettingsDescription)}

{ try { await axios.post('/api/v1/settings/main', { applicationTitle: values.applicationTitle, applicationUrl: values.applicationUrl, hideAvailable: values.hideAvailable, hideBlacklisted: values.hideBlacklisted, locale: values.locale, discoverRegion: values.discoverRegion, streamingRegion: values.streamingRegion, originalLanguage: values.originalLanguage, blacklistedTags: values.blacklistedTags, blacklistedTagsLimit: values.blacklistedTagsLimit, partialRequestsEnabled: values.partialRequestsEnabled, enableSpecialEpisodes: values.enableSpecialEpisodes, cacheImages: values.cacheImages, youtubeUrl: values.youtubeUrl, }); mutate('/api/v1/settings/public'); mutate('/api/v1/status'); if (setLocale) { setLocale( (userData?.locale ? userData.locale : values.locale) as AvailableLocale ); } addToast(intl.formatMessage(messages.toastSettingsSuccess), { autoDismiss: true, appearance: 'success', }); } catch (e) { addToast(intl.formatMessage(messages.toastSettingsFailure), { autoDismiss: true, appearance: 'error', }); } finally { revalidate(); } }} > {({ errors, touched, isSubmitting, isValid, values, setFieldValue, }) => { return (
{userHasPermission(Permission.ADMIN) && (
)}
{errors.applicationTitle && touched.applicationTitle && typeof errors.applicationTitle === 'string' && (
{errors.applicationTitle}
)}
{errors.applicationUrl && touched.applicationUrl && typeof errors.applicationUrl === 'string' && (
{errors.applicationUrl}
)}
{ setFieldValue('cacheImages', !values.cacheImages); }} />
{( Object.keys( availableLanguages ) as (keyof typeof availableLanguages)[] ).map((key) => ( ))}
{errors.blacklistedTagsLimit && touched.blacklistedTagsLimit && typeof errors.blacklistedTagsLimit === 'string' && (
{errors.blacklistedTagsLimit}
)}
{ setFieldValue('hideAvailable', !values.hideAvailable); }} />
{ setFieldValue( 'hideBlacklisted', !values.hideBlacklisted ); }} />
{ setFieldValue( 'partialRequestsEnabled', !values.partialRequestsEnabled ); }} />
{ setFieldValue( 'enableSpecialEpisodes', !values.enableSpecialEpisodes ); }} />
{errors.youtubeUrl && touched.youtubeUrl && typeof errors.youtubeUrl === 'string' && (
{errors.youtubeUrl}
)}
); }}
); }; export default SettingsMain;