/* * Copyright (c) 2021 - 2024, Ludvig Lundgren and the autobrr contributors. * SPDX-License-Identifier: GPL-2.0-or-later */ import { Fragment, useRef, useState, ReactElement } from "react"; import { useMutation, useQueryClient } from "@tanstack/react-query"; import { Dialog, DialogPanel, DialogTitle, Transition, TransitionChild } from "@headlessui/react"; import { XMarkIcon } from "@heroicons/react/24/solid"; import { Form, Formik, useFormikContext } from "formik"; import { classNames, sleep } from "@utils"; import { DEBUG } from "@components/debug"; import { APIClient } from "@api/APIClient"; import { DownloadClientKeys } from "@api/query_keys"; import { DownloadClientAuthType, DownloadClientTypeOptions, DownloadRuleConditionOptions } from "@domain/constants"; import { toast } from "@components/hot-toast"; import Toast from "@components/notifications/Toast"; import { useToggle } from "@hooks/hooks"; import { DeleteModal } from "@components/modals"; import { NumberFieldWide, PasswordFieldWide, RadioFieldsetWide, SwitchGroupWide, TextFieldWide } from "@components/inputs"; import { DocsLink, ExternalLink } from "@components/ExternalLink"; import { SelectFieldBasic } from "@components/inputs/select_wide"; interface InitialValuesSettings { basic?: { auth: boolean; username: string; password: string; }; auth?: { enabled: boolean; type: string; username: string; password: string; }; rules?: { enabled?: boolean; ignore_slow_torrents?: boolean; ignore_slow_torrents_condition?: IgnoreTorrentsCondition; download_speed_threshold?: number; max_active_downloads?: number; }; external_download_client_id?: number; external_download_client?: string; } interface InitialValues { name: string; type: DownloadClientType; enabled: boolean; host: string; port: number; tls: boolean; tls_skip_verify: boolean; username: string; password: string; settings: InitialValuesSettings; } function FormFieldsDeluge() { const { values: { tls } } = useFormikContext(); return (

See guides for how to connect to Deluge for various server types in our docs.


Dedicated servers:

Shared seedbox providers:

} /> {tls && ( )} ); } function FormFieldsArr() { const { values: { settings } } = useFormikContext(); return (

See guides for how to connect to the *arr suite for various server types in our docs.


Dedicated servers:

Shared seedbox providers:

} /> {settings.basic?.auth === true && ( <> )} ); } function FormFieldsQbit() { const { values: { port, tls, settings } } = useFormikContext(); return (

See guides for how to connect to qBittorrent for various server types in our docs.


Dedicated servers:

Shared seedbox providers:

} /> {port > 0 && ( )} {tls && ( )} {settings.basic?.auth === true && ( <> )} ); } function FormFieldsPorla() { const { values: { tls, settings } } = useFormikContext(); return (
{tls && ( )} {settings.basic?.auth === true && ( <> )}
); } function FormFieldsRTorrent() { const { values: { tls, settings } } = useFormikContext(); return (

See guides for how to connect to rTorrent for various server types in our docs.


Dedicated servers:

Shared seedbox providers:

} /> {tls && ( )} {settings.auth?.enabled && ( <> This should in most cases be Basic Auth, but some providers use Digest Auth.

} /> )} ); } function FormFieldsTransmission() { const { values: { tls } } = useFormikContext(); return (

See guides for how to connect to Transmission for various server types in our docs.


Dedicated servers:

Shared seedbox providers:

} /> {tls && ( )} ); } function FormFieldsSabnzbd() { const { values: { port, tls, settings } } = useFormikContext(); return (

See our guides on how to connect to qBittorrent for various server types in our docs.


Dedicated servers:

Shared seedbox providers:

} /> {port > 0 && ( )} {tls && ( )} {/**/} {/**/} {settings.basic?.auth === true && ( <> )} ); } export interface componentMapType { [key: string]: ReactElement; } export const componentMap: componentMapType = { DELUGE_V1: , DELUGE_V2: , QBITTORRENT: , RTORRENT: , TRANSMISSION: , PORLA: , RADARR: , SONARR: , LIDARR: , WHISPARR: , READARR: , SABNZBD: }; function FormFieldsRulesBasic() { const { values: { settings } } = useFormikContext(); return (
Rules

Manage max downloads.

{settings && settings.rules?.enabled === true && (

Limit the amount of active downloads (0 is unlimited), to give the maximum amount of bandwidth and disk for the downloads.



See recommendations for various server types here:

} /> )}
); } function FormFieldsRulesArr() { // const { // values: { settings } // } = useFormikContext(); return (
Download Client

Override download client to use. Can also be overridden per Filter Action.

Specify what client the arr should use by default. Can be overridden per filter action.

} />

DEPRECATED: Use Client name field instead.

} /> ); } function FormFieldsRulesQbit() { const { values: { settings } } = useFormikContext(); return (
Rules

Manage max downloads etc.

{settings.rules?.enabled === true && ( <>

Limit the amount of active downloads (0 is unlimited), to give the maximum amount of bandwidth and disk for the downloads.



See recommendations for various server types here:

} /> {settings.rules?.ignore_slow_torrents === true && ( <> Choose whether to respect or ignore the Max active downloads setting before checking speed thresholds.

} /> )} )}
); } function FormFieldsRulesTransmission() { const { values: { settings } } = useFormikContext(); return (
Rules

Manage max downloads etc.

{settings.rules?.enabled === true && ( <>

Limit the amount of active downloads (0 is unlimited), to give the maximum amount of bandwidth and disk for the downloads.



See recommendations for various server types here:

} /> )}
); } export const rulesComponentMap: componentMapType = { DELUGE_V1: , DELUGE_V2: , QBITTORRENT: , PORLA: , TRANSMISSION: , RADARR: , SONARR: , LIDARR: , WHISPARR: , READARR: , }; interface formButtonsProps { isSuccessfulTest: boolean; isErrorTest: boolean; isTesting: boolean; cancelFn: () => void; testFn: (data: unknown) => void; values: unknown; type: "CREATE" | "UPDATE"; toggleDeleteModal?: () => void; } function DownloadClientFormButtons({ type, isSuccessfulTest, isErrorTest, isTesting, cancelFn, testFn, values, toggleDeleteModal }: formButtonsProps) { const test = () => { testFn(values); }; return (
{type === "UPDATE" && ( )}
); } interface formProps { isOpen: boolean; toggle: () => void; } export function DownloadClientAddForm({ isOpen, toggle }: formProps) { const [isTesting, setIsTesting] = useState(false); const [isSuccessfulTest, setIsSuccessfulTest] = useState(false); const [isErrorTest, setIsErrorTest] = useState(false); const queryClient = useQueryClient(); const addMutation = useMutation({ mutationFn: (client: DownloadClient) => APIClient.download_clients.create(client), onSuccess: () => { queryClient.invalidateQueries({ queryKey: DownloadClientKeys.lists() }); toast.custom((t) => ); toggle(); }, onError: () => { toast.custom((t) => ); } }); const onSubmit = (data: unknown) => addMutation.mutate(data as DownloadClient); const testClientMutation = useMutation({ mutationFn: (client: DownloadClient) => APIClient.download_clients.test(client), onMutate: () => { setIsTesting(true); setIsErrorTest(false); setIsSuccessfulTest(false); }, onSuccess: () => { sleep(1000) .then(() => { setIsTesting(false); setIsSuccessfulTest(true); }) .then(() => { sleep(2500).then(() => { setIsSuccessfulTest(false); }); }); }, onError: () => { console.log("not added"); setIsTesting(false); setIsErrorTest(true); sleep(2500).then(() => { setIsErrorTest(false); }); } }); const testClient = (data: unknown) => testClientMutation.mutate(data as DownloadClient); const initialValues: InitialValues = { name: "", type: "QBITTORRENT", enabled: true, host: "", port: 0, tls: false, tls_skip_verify: false, username: "", password: "", settings: {} }; return (
{({ handleSubmit, values }) => (
Add client

Add download client.

{componentMap[values.type]}
{rulesComponentMap[values.type]} )}
); } interface updateFormProps { isOpen: boolean; toggle: () => void; client: DownloadClient; } export function DownloadClientUpdateForm({ client, isOpen, toggle }: updateFormProps) { const [isTesting, setIsTesting] = useState(false); const [isSuccessfulTest, setIsSuccessfulTest] = useState(false); const [isErrorTest, setIsErrorTest] = useState(false); const [deleteModalIsOpen, toggleDeleteModal] = useToggle(false); const cancelButtonRef = useRef(null); const cancelModalButtonRef = useRef(null); const queryClient = useQueryClient(); const mutation = useMutation({ mutationFn: (client: DownloadClient) => APIClient.download_clients.update(client), onSuccess: () => { queryClient.invalidateQueries({ queryKey: DownloadClientKeys.lists() }); queryClient.invalidateQueries({ queryKey: DownloadClientKeys.detail(client.id) }); toast.custom((t) => ); toggle(); } }); const onSubmit = (data: unknown) => mutation.mutate(data as DownloadClient); const deleteMutation = useMutation({ mutationFn: (clientID: number) => APIClient.download_clients.delete(clientID), onSuccess: () => { queryClient.invalidateQueries({ queryKey: DownloadClientKeys.lists() }); queryClient.invalidateQueries({ queryKey: DownloadClientKeys.detail(client.id) }); toast.custom((t) => ); toggleDeleteModal(); } }); const deleteAction = () => deleteMutation.mutate(client.id); const testClientMutation = useMutation({ mutationFn: (client: DownloadClient) => APIClient.download_clients.test(client), onMutate: () => { setIsTesting(true); setIsErrorTest(false); setIsSuccessfulTest(false); }, onSuccess: () => { sleep(1000) .then(() => { setIsTesting(false); setIsSuccessfulTest(true); }) .then(() => { sleep(2500).then(() => { setIsSuccessfulTest(false); }); }); }, onError: () => { setIsTesting(false); setIsErrorTest(true); sleep(2500).then(() => { setIsErrorTest(false); }); } }); const testClient = (data: unknown) => testClientMutation.mutate(data as DownloadClient); const initialValues = { id: client.id, name: client.name, type: client.type, enabled: client.enabled, host: client.host, port: client.port, tls: client.tls, tls_skip_verify: client.tls_skip_verify, username: client.username, password: client.password, settings: client.settings }; return (
{({ handleSubmit, values }) => { return (
Edit client

Edit download client settings.

{componentMap[values.type]}
{rulesComponentMap[values.type]} ); }}
); }