/*
* Copyright (c) 2021 - 2025, 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";
import { AddFormProps, UpdateFormProps } from "@forms/_shared";
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" && (
Remove
)}
testClient(values)}
onClick={test}
>
{isTesting ? (
) : isSuccessfulTest ? (
"OK!"
) : isErrorTest ? (
"ERROR"
) : (
"Test"
)}
Cancel
{type === "CREATE" ? "Create" : "Save"}
);
}
export function DownloadClientAddForm({ isOpen, toggle }: AddFormProps) {
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 }) => (
)}
);
}
export function DownloadClientUpdateForm({ isOpen, toggle, data: client}: 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 (
);
}}
);
}