diff --git a/overseerr-api.yml b/overseerr-api.yml
index dfbbfd084..4e04b6788 100644
--- a/overseerr-api.yml
+++ b/overseerr-api.yml
@@ -1932,6 +1932,11 @@ components:
type: string
native_name:
type: string
+ OverrideRule:
+ type: object
+ properties:
+ id:
+ type: string
securitySchemes:
cookieAuth:
type: apiKey
@@ -6956,6 +6961,68 @@ paths:
type: array
items:
$ref: '#/components/schemas/WatchProviderDetails'
+ /overrideRule:
+ get:
+ summary: Get override rules
+ description: Returns a list of all override rules with their conditions and settings
+ tags:
+ - overriderule
+ responses:
+ '200':
+ description: Override rules returned
+ content:
+ application/json:
+ schema:
+ type: array
+ items:
+ $ref: '#/components/schemas/OverrideRule'
+ post:
+ summary: Create override rule
+ description: Creates a new Override Rule from the request body.
+ tags:
+ - overriderule
+ responses:
+ '200':
+ description: 'Values were successfully created'
+ content:
+ application/json:
+ schema:
+ type: array
+ items:
+ $ref: '#/components/schemas/OverrideRule'
+ /overrideRule/{ruleId}:
+ put:
+ summary: Update override rule
+ description: Updates an Override Rule from the request body.
+ tags:
+ - overriderule
+ responses:
+ '200':
+ description: 'Values were successfully updated'
+ content:
+ application/json:
+ schema:
+ type: array
+ items:
+ $ref: '#/components/schemas/OverrideRule'
+ delete:
+ summary: Delete override rule by ID
+ description: Deletes the override rule with the provided ruleId.
+ tags:
+ - overriderule
+ parameters:
+ - in: path
+ name: ruleId
+ required: true
+ schema:
+ type: number
+ responses:
+ '200':
+ description: Override rule successfully deleted
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/OverrideRule'
security:
- cookieAuth: []
- apiKey: []
diff --git a/server/entity/OverrideRule.ts b/server/entity/OverrideRule.ts
new file mode 100644
index 000000000..394cc4293
--- /dev/null
+++ b/server/entity/OverrideRule.ts
@@ -0,0 +1,53 @@
+import {
+ Column,
+ CreateDateColumn,
+ Entity,
+ JoinTable,
+ ManyToMany,
+ PrimaryGeneratedColumn,
+ UpdateDateColumn,
+} from 'typeorm';
+import { User } from './User';
+
+@Entity()
+class OverrideRule {
+ @PrimaryGeneratedColumn()
+ public id: number;
+
+ @Column({ type: 'int', nullable: true })
+ public radarrServiceId?: number;
+
+ @Column({ type: 'int', nullable: true })
+ public sonarrServiceId?: number;
+
+ @Column({ nullable: true })
+ public genre?: string;
+
+ @Column({ nullable: true })
+ public language?: string;
+
+ @ManyToMany(() => User)
+ @JoinTable()
+ public users: User[];
+
+ @Column({ type: 'int', nullable: true })
+ public profileId?: number;
+
+ @Column({ nullable: true })
+ public rootFolder?: string;
+
+ @Column({ type: 'simple-array', nullable: true })
+ public tags?: number[];
+
+ @CreateDateColumn()
+ public createdAt: Date;
+
+ @UpdateDateColumn()
+ public updatedAt: Date;
+
+ constructor(init?: Partial) {
+ Object.assign(this, init);
+ }
+}
+
+export default OverrideRule;
diff --git a/server/interfaces/api/overrideRuleInterfaces.ts b/server/interfaces/api/overrideRuleInterfaces.ts
new file mode 100644
index 000000000..5ae61a684
--- /dev/null
+++ b/server/interfaces/api/overrideRuleInterfaces.ts
@@ -0,0 +1,3 @@
+import type OverrideRule from '@server/entity/OverrideRule';
+
+export type OverrideRuleResultsResponse = OverrideRule[];
diff --git a/server/lib/settings/index.ts b/server/lib/settings/index.ts
index 425fc1389..1417fbd8f 100644
--- a/server/lib/settings/index.ts
+++ b/server/lib/settings/index.ts
@@ -76,6 +76,7 @@ export interface DVRSettings {
syncEnabled: boolean;
preventSearch: boolean;
tagRequests: boolean;
+ overrideRule: number[];
}
export interface RadarrSettings extends DVRSettings {
diff --git a/server/routes/index.ts b/server/routes/index.ts
index 120e2e86b..f064e6031 100644
--- a/server/routes/index.ts
+++ b/server/routes/index.ts
@@ -15,6 +15,7 @@ import { checkUser, isAuthenticated } from '@server/middleware/auth';
import { mapWatchProviderDetails } from '@server/models/common';
import { mapProductionCompany } from '@server/models/Movie';
import { mapNetwork } from '@server/models/Tv';
+import overrideRuleRoutes from '@server/routes/overrideRule';
import settingsRoutes from '@server/routes/settings';
import watchlistRoutes from '@server/routes/watchlist';
import {
@@ -160,6 +161,11 @@ router.use('/service', isAuthenticated(), serviceRoutes);
router.use('/issue', isAuthenticated(), issueRoutes);
router.use('/issueComment', isAuthenticated(), issueCommentRoutes);
router.use('/auth', authRoutes);
+router.use(
+ '/overrideRule',
+ isAuthenticated(Permission.ADMIN),
+ overrideRuleRoutes
+);
router.get('/regions', isAuthenticated(), async (req, res, next) => {
const tmdb = new TheMovieDb();
diff --git a/server/routes/overrideRule.ts b/server/routes/overrideRule.ts
new file mode 100644
index 000000000..6662189d4
--- /dev/null
+++ b/server/routes/overrideRule.ts
@@ -0,0 +1,102 @@
+import { getRepository } from '@server/datasource';
+import OverrideRule from '@server/entity/OverrideRule';
+import type { OverrideRuleResultsResponse } from '@server/interfaces/api/overrideRuleInterfaces';
+import { Permission } from '@server/lib/permissions';
+import { isAuthenticated } from '@server/middleware/auth';
+import { Router } from 'express';
+
+const overrideRuleRoutes = Router();
+
+overrideRuleRoutes.get(
+ '/',
+ isAuthenticated(Permission.ADMIN),
+ async (req, res, next) => {
+ const overrideRuleRepository = getRepository(OverrideRule);
+
+ try {
+ const rules = await overrideRuleRepository.find({});
+
+ return res.status(200).json(rules as OverrideRuleResultsResponse);
+ } catch (e) {
+ next({ status: 404, message: e.message });
+ }
+ }
+);
+
+overrideRuleRoutes.post<
+ Record,
+ OverrideRule,
+ {
+ genre?: string;
+ language?: string;
+ profileId?: number;
+ rootFolder?: string;
+ tags?: number[];
+ radarrServiceId?: number;
+ sonarrServiceId?: number;
+ }
+>('/', isAuthenticated(Permission.ADMIN), async (req, res, next) => {
+ const overrideRuleRepository = getRepository(OverrideRule);
+
+ try {
+ const rule = new OverrideRule({
+ genre: req.body.genre,
+ language: req.body.language,
+ profileId: req.body.profileId,
+ rootFolder: req.body.rootFolder,
+ tags: req.body.tags,
+ radarrServiceId: req.body.radarrServiceId,
+ sonarrServiceId: req.body.sonarrServiceId,
+ });
+
+ const newIssue = await overrideRuleRepository.save(rule);
+
+ return res.status(200).json(newIssue);
+ } catch (e) {
+ next({ status: 404, message: e.message });
+ }
+});
+
+overrideRuleRoutes.put<
+ { ruleId: string },
+ OverrideRule,
+ {
+ genre?: string;
+ language?: string;
+ profileId?: number;
+ rootFolder?: string;
+ tags?: number[];
+ radarrServiceId?: number;
+ sonarrServiceId?: number;
+ }
+>('/:ruleId', isAuthenticated(Permission.ADMIN), async (req, res, next) => {
+ const overrideRuleRepository = getRepository(OverrideRule);
+
+ try {
+ const rule = await overrideRuleRepository.findOne({
+ where: {
+ id: Number(req.params.ruleId),
+ },
+ });
+
+ if (!rule) {
+ return next({ status: 404, message: 'Override Rule not found.' });
+ }
+
+ rule.genre = req.body.genre;
+ rule.language = req.body.language;
+ rule.profileId = req.body.profileId;
+ rule.rootFolder = req.body.rootFolder;
+ rule.tags = req.body.tags;
+ rule.radarrServiceId = req.body.radarrServiceId;
+ rule.sonarrServiceId = req.body.sonarrServiceId;
+
+ const newIssue = await overrideRuleRepository.save(rule);
+
+ return res.status(200).json(newIssue);
+ } catch (e) {
+ next({ status: 404, message: e.message });
+ }
+});
+
+export default overrideRuleRoutes;
diff --git a/src/components/Common/Modal/index.tsx b/src/components/Common/Modal/index.tsx
index 4930bd85d..8cebf06f7 100644
--- a/src/components/Common/Modal/index.tsx
+++ b/src/components/Common/Modal/index.tsx
@@ -7,7 +7,7 @@ import { useLockBodyScroll } from '@app/hooks/useLockBodyScroll';
import globalMessages from '@app/i18n/globalMessages';
import { Transition } from '@headlessui/react';
import type { MouseEvent } from 'react';
-import React, { Fragment, useRef } from 'react';
+import React, { Fragment, useEffect, useRef } from 'react';
import ReactDOM from 'react-dom';
import { useIntl } from 'react-intl';
@@ -66,8 +66,12 @@ const Modal = React.forwardRef(
) => {
const intl = useIntl();
const modalRef = useRef(null);
+ const backgroundClickableRef = useRef(backgroundClickable); // This ref is used to detect state change inside the useClickOutside hook
+ useEffect(() => {
+ backgroundClickableRef.current = backgroundClickable;
+ }, [backgroundClickable]);
useClickOutside(modalRef, () => {
- if (onCancel && backgroundClickable) {
+ if (onCancel && backgroundClickableRef.current) {
onCancel();
}
});
diff --git a/src/components/Settings/OverrideRuleModal.tsx b/src/components/Settings/OverrideRuleModal.tsx
new file mode 100644
index 000000000..3220e87cc
--- /dev/null
+++ b/src/components/Settings/OverrideRuleModal.tsx
@@ -0,0 +1,317 @@
+import Modal from '@app/components/Common/Modal';
+import LanguageSelector from '@app/components/LanguageSelector';
+import { GenreSelector } from '@app/components/Selector';
+import type { DVRTestResponse } from '@app/components/Settings/SettingsServices';
+import useSettings from '@app/hooks/useSettings';
+import globalMessages from '@app/i18n/globalMessages';
+import defineMessages from '@app/utils/defineMessages';
+import { Transition } from '@headlessui/react';
+import type OverrideRule from '@server/entity/OverrideRule';
+import { Field, Formik } from 'formik';
+import { useIntl } from 'react-intl';
+import Select from 'react-select';
+import { useToasts } from 'react-toast-notifications';
+
+const messages = defineMessages('components.Settings.RadarrModal', {
+ createrule: 'New Override Rule',
+ editrule: 'Edit Override Rule',
+ create: 'Create rule',
+ rootfolder: 'Root Folder',
+ selectRootFolder: 'Select root folder',
+ qualityprofile: 'Quality Profile',
+ selectQualityProfile: 'Select quality profile',
+ tags: 'Tags',
+ notagoptions: 'No tags.',
+ selecttags: 'Select tags',
+ ruleCreated: 'Override rule created successfully!',
+ ruleUpdated: 'Override rule updated successfully!',
+});
+
+type OptionType = {
+ value: number;
+ label: string;
+};
+
+interface OverrideRuleModalProps {
+ rule: OverrideRule | null;
+ onClose: () => void;
+ testResponse: DVRTestResponse;
+ radarrId?: number;
+ sonarrId?: number;
+}
+
+const OverrideRuleModal = ({
+ onClose,
+ rule,
+ testResponse,
+ radarrId,
+ sonarrId,
+}: OverrideRuleModalProps) => {
+ const intl = useIntl();
+ const { addToast } = useToasts();
+ const { currentSettings } = useSettings();
+
+ return (
+
+ {
+ try {
+ const submission = {
+ genre: values.genre || null,
+ language: values.language || null,
+ profileId: Number(values.profileId) || null,
+ rootFolder: values.rootFolder || null,
+ tags: values.tags || null,
+ radarrServiceId: radarrId,
+ sonarrServiceId: sonarrId,
+ };
+ if (!rule) {
+ const res = await fetch('/api/v1/overrideRule', {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ },
+ body: JSON.stringify(submission),
+ });
+ if (!res.ok) throw new Error();
+ addToast(intl.formatMessage(messages.ruleCreated), {
+ appearance: 'success',
+ autoDismiss: true,
+ });
+ } else {
+ const res = await fetch(`/api/v1/overrideRule/${rule.id}`, {
+ method: 'PUT',
+ headers: {
+ 'Content-Type': 'application/json',
+ },
+ body: JSON.stringify(submission),
+ });
+ if (!res.ok) throw new Error();
+ addToast(intl.formatMessage(messages.ruleUpdated), {
+ appearance: 'success',
+ autoDismiss: true,
+ });
+ }
+ onClose();
+ } catch (e) {
+ // set error here
+ }
+ }}
+ >
+ {({
+ errors,
+ touched,
+ values,
+ handleSubmit,
+ setFieldValue,
+ isSubmitting,
+ isValid,
+ }) => {
+ return (
+ handleSubmit()}
+ title={
+ !rule
+ ? intl.formatMessage(messages.createrule)
+ : intl.formatMessage(messages.editrule)
+ }
+ >
+
+
+ Condition
+
+
+
+
+
+ {
+ setFieldValue(
+ 'genre',
+ genres?.map((v) => v.value).join(',')
+ );
+ }}
+ />
+
+ {errors.genre &&
+ touched.genre &&
+ typeof errors.genre === 'string' && (
+
{errors.genre}
+ )}
+
+
+
+
+
+
+ {
+ setFieldValue('language', value);
+ }}
+ />
+
+ {errors.genre &&
+ touched.genre &&
+ typeof errors.genre === 'string' && (
+
{errors.genre}
+ )}
+
+
+
+ Settings
+
+
+
+
+
+
+
+ {testResponse.rootFolders.length > 0 &&
+ testResponse.rootFolders.map((folder) => (
+
+ ))}
+
+
+ {errors.rootFolder &&
+ touched.rootFolder &&
+ typeof errors.rootFolder === 'string' && (
+
{errors.rootFolder}
+ )}
+
+
+
+
+
+
+
+
+ {testResponse.profiles.length > 0 &&
+ testResponse.profiles.map((profile) => (
+
+ ))}
+
+
+ {errors.profileId &&
+ touched.profileId &&
+ typeof errors.profileId === 'string' && (
+
{errors.profileId}
+ )}
+
+
+
+
+
+
+
+
+
+ );
+ }}
+
+
+ );
+};
+
+export default OverrideRuleModal;
diff --git a/src/components/Settings/RadarrModal/index.tsx b/src/components/Settings/RadarrModal/index.tsx
index 51c74ba89..fbd97f39b 100644
--- a/src/components/Settings/RadarrModal/index.tsx
+++ b/src/components/Settings/RadarrModal/index.tsx
@@ -1,14 +1,21 @@
+import Button from '@app/components/Common/Button';
import Modal from '@app/components/Common/Modal';
import SensitiveInput from '@app/components/Common/SensitiveInput';
+import type { DVRTestResponse } from '@app/components/Settings/SettingsServices';
import globalMessages from '@app/i18n/globalMessages';
import defineMessages from '@app/utils/defineMessages';
import { Transition } from '@headlessui/react';
-import type { RadarrSettings } from '@server/lib/settings';
+import { PlusIcon } from '@heroicons/react/24/solid';
+import type { TmdbGenre } from '@server/api/themoviedb/interfaces';
+import type OverrideRule from '@server/entity/OverrideRule';
+import type { OverrideRuleResultsResponse } from '@server/interfaces/api/overrideRuleInterfaces';
+import type { Language, RadarrSettings } from '@server/lib/settings';
import { Field, Formik } from 'formik';
import { useCallback, useEffect, useRef, useState } from 'react';
import { useIntl } from 'react-intl';
import Select from 'react-select';
import { useToasts } from 'react-toast-notifications';
+import useSWR from 'swr';
import * as Yup from 'yup';
type OptionType = {
@@ -69,41 +76,46 @@ const messages = defineMessages('components.Settings.RadarrModal', {
announced: 'Announced',
inCinemas: 'In Cinemas',
released: 'Released',
+ overrideRules: 'Override Rules',
+ addrule: 'New Override Rule',
});
-interface TestResponse {
- profiles: {
- id: number;
- name: string;
- }[];
- rootFolders: {
- id: number;
- path: string;
- }[];
- tags: {
- id: number;
- label: string;
- }[];
- urlBase?: string;
-}
-
interface RadarrModalProps {
radarr: RadarrSettings | null;
- onClose: () => void;
+ onClose?: () => void;
onSave: () => void;
+ overrideRuleModal: { open: boolean; rule: OverrideRule | null };
+ setOverrideRuleModal: ({
+ open,
+ rule,
+ testResponse,
+ }: {
+ open: boolean;
+ rule: OverrideRule | null;
+ testResponse: DVRTestResponse;
+ }) => void;
}
-const RadarrModal = ({ onClose, radarr, onSave }: RadarrModalProps) => {
+const RadarrModal = ({
+ onClose,
+ radarr,
+ onSave,
+ overrideRuleModal,
+ setOverrideRuleModal,
+}: RadarrModalProps) => {
const intl = useIntl();
+ const { data: rules, mutate: revalidate } =
+ useSWR('/api/v1/overrideRule');
const initialLoad = useRef(false);
const { addToast } = useToasts();
const [isValidated, setIsValidated] = useState(radarr ? true : false);
const [isTesting, setIsTesting] = useState(false);
- const [testResponse, setTestResponse] = useState({
+ const [testResponse, setTestResponse] = useState({
profiles: [],
rootFolders: [],
tags: [],
});
+
const RadarrSettingsSchema = Yup.object().shape({
name: Yup.string().required(
intl.formatMessage(messages.validationNameRequired)
@@ -220,6 +232,10 @@ const RadarrModal = ({ onClose, radarr, onSave }: RadarrModalProps) => {
}
}, [radarr, testConnection]);
+ useEffect(() => {
+ revalidate();
+ }, [overrideRuleModal, revalidate]);
+
return (
{
values.is4k ? messages.edit4kradarr : messages.editradarr
)
}
+ backgroundClickable={!overrideRuleModal.open}
>
@@ -753,6 +770,37 @@ const RadarrModal = ({ onClose, radarr, onSave }: RadarrModalProps) => {
+
+ {intl.formatMessage(messages.overrideRules)}
+
+
+ {rules && (
+
+ )}
+ -
+
+
+
+
+
);
}}
@@ -761,4 +809,113 @@ const RadarrModal = ({ onClose, radarr, onSave }: RadarrModalProps) => {
);
};
+interface OverrideRulesProps {
+ rules: OverrideRule[];
+ setOverrideRuleModal: ({
+ open,
+ rule,
+ testResponse,
+ }: {
+ open: boolean;
+ rule: OverrideRule | null;
+ testResponse: DVRTestResponse;
+ }) => void;
+ testResponse: DVRTestResponse;
+ radarr: RadarrSettings | null;
+}
+const OverrideRules = ({
+ rules,
+ setOverrideRuleModal,
+ testResponse,
+ radarr,
+}: OverrideRulesProps) => {
+ const intl = useIntl();
+ const { data: languages } = useSWR('/api/v1/languages');
+ const { data: genres } = useSWR('/api/v1/genres/movie');
+
+ return rules
+ ?.filter(
+ (rule) =>
+ rule.radarrServiceId !== null && rule.radarrServiceId === radarr?.id
+ )
+ .map((rule) => (
+
+ ));
+};
+
export default RadarrModal;
diff --git a/src/components/Settings/SettingsServices.tsx b/src/components/Settings/SettingsServices.tsx
index 63cd463f7..06e627c32 100644
--- a/src/components/Settings/SettingsServices.tsx
+++ b/src/components/Settings/SettingsServices.tsx
@@ -6,12 +6,14 @@ import Button from '@app/components/Common/Button';
import LoadingSpinner from '@app/components/Common/LoadingSpinner';
import Modal from '@app/components/Common/Modal';
import PageTitle from '@app/components/Common/PageTitle';
+import OverrideRuleModal from '@app/components/Settings/OverrideRuleModal';
import RadarrModal from '@app/components/Settings/RadarrModal';
import SonarrModal from '@app/components/Settings/SonarrModal';
import globalMessages from '@app/i18n/globalMessages';
import defineMessages from '@app/utils/defineMessages';
import { Transition } from '@headlessui/react';
import { PencilIcon, PlusIcon, TrashIcon } from '@heroicons/react/24/solid';
+import type OverrideRule from '@server/entity/OverrideRule';
import type { RadarrSettings, SonarrSettings } from '@server/lib/settings';
import { Fragment, useState } from 'react';
import { useIntl } from 'react-intl';
@@ -57,6 +59,22 @@ interface ServerInstanceProps {
onDelete: () => void;
}
+export interface DVRTestResponse {
+ profiles: {
+ id: number;
+ name: string;
+ }[];
+ rootFolders: {
+ id: number;
+ path: string;
+ }[];
+ tags: {
+ id: number;
+ label: string;
+ }[];
+ urlBase?: string;
+}
+
const ServerInstance = ({
name,
hostname,
@@ -193,6 +211,15 @@ const SettingsServices = () => {
type: 'radarr',
serverId: null,
});
+ const [overrideRuleModal, setOverrideRuleModal] = useState<{
+ open: boolean;
+ rule: OverrideRule | null;
+ testResponse: DVRTestResponse | null;
+ }>({
+ open: false,
+ rule: null,
+ testResponse: null,
+ });
const deleteServer = async () => {
const res = await fetch(
@@ -227,15 +254,35 @@ const SettingsServices = () => {
})}
+ {overrideRuleModal.open && overrideRuleModal.testResponse && (
+
+ setOverrideRuleModal({
+ open: false,
+ rule: null,
+ testResponse: null,
+ })
+ }
+ testResponse={overrideRuleModal.testResponse}
+ radarrId={editRadarrModal.radarr?.id}
+ sonarrId={editSonarrModal.sonarr?.id}
+ />
+ )}
{editRadarrModal.open && (
setEditRadarrModal({ open: false, radarr: null })}
+ onClose={() => {
+ if (!overrideRuleModal.open)
+ setEditRadarrModal({ open: false, radarr: null });
+ }}
onSave={() => {
revalidateRadarr();
mutate('/api/v1/settings/public');
setEditRadarrModal({ open: false, radarr: null });
}}
+ overrideRuleModal={overrideRuleModal}
+ setOverrideRuleModal={setOverrideRuleModal}
/>
)}
{editSonarrModal.open && (
diff --git a/src/components/Settings/SonarrModal/index.tsx b/src/components/Settings/SonarrModal/index.tsx
index 7890d4b5c..c0bdd75d0 100644
--- a/src/components/Settings/SonarrModal/index.tsx
+++ b/src/components/Settings/SonarrModal/index.tsx
@@ -1,5 +1,6 @@
import Modal from '@app/components/Common/Modal';
import SensitiveInput from '@app/components/Common/SensitiveInput';
+import type { DVRTestResponse } from '@app/components/Settings/SettingsServices';
import globalMessages from '@app/i18n/globalMessages';
import defineMessages from '@app/utils/defineMessages';
import { Transition } from '@headlessui/react';
@@ -77,26 +78,11 @@ const messages = defineMessages('components.Settings.SonarrModal', {
selecttags: 'Select tags',
});
-interface TestResponse {
- profiles: {
+interface SonarrTestResponse extends DVRTestResponse {
+ languageProfiles: {
id: number;
name: string;
- }[];
- rootFolders: {
- id: number;
- path: string;
- }[];
- languageProfiles:
- | {
- id: number;
- name: string;
- }[]
- | null;
- tags: {
- id: number;
- label: string;
- }[];
- urlBase?: string;
+ }[] | null;
}
interface SonarrModalProps {
@@ -111,7 +97,7 @@ const SonarrModal = ({ onClose, sonarr, onSave }: SonarrModalProps) => {
const { addToast } = useToasts();
const [isValidated, setIsValidated] = useState(sonarr ? true : false);
const [isTesting, setIsTesting] = useState(false);
- const [testResponse, setTestResponse] = useState({
+ const [testResponse, setTestResponse] = useState({
profiles: [],
rootFolders: [],
languageProfiles: null,
@@ -197,7 +183,7 @@ const SonarrModal = ({ onClose, sonarr, onSave }: SonarrModalProps) => {
}),
});
if (!res.ok) throw new Error();
- const data: TestResponse = await res.json();
+ const data: SonarrTestResponse = await res.json();
setIsValidated(true);
setTestResponse(data);