mirror of
https://github.com/idanoo/autobrr
synced 2025-07-23 08:49:13 +00:00
Feature: Deluge download client (#12)
* chore: add go-libdeluge package * feat: implement deluge v1 and v2 clients * feat(web): handle add and update deluge clients * chore: temp remove releaseinfo parser
This commit is contained in:
parent
eb5b040eeb
commit
0c4aaa29b0
19 changed files with 493 additions and 122 deletions
|
@ -10,19 +10,7 @@ import {TextField} from "./inputs";
|
|||
import DEBUG from "./debug";
|
||||
import APIClient from "../api/APIClient";
|
||||
import {queryClient} from "../App";
|
||||
|
||||
interface radioFieldsetOption {
|
||||
label: string;
|
||||
value: string;
|
||||
}
|
||||
|
||||
const actionTypeOptions: radioFieldsetOption[] = [
|
||||
{label: "Test", value: "TEST"},
|
||||
{label: "Watch dir", value: "WATCH_FOLDER"},
|
||||
{label: "Exec", value: "EXEC"},
|
||||
{label: "qBittorrent", value: "QBITTORRENT"},
|
||||
{label: "Deluge", value: "DELUGE"},
|
||||
];
|
||||
import {ActionTypeNameMap, ActionTypeOptions, DownloadClientTypeNameMap} from "../domain/constants";
|
||||
|
||||
interface FilterListProps {
|
||||
actions: Action[];
|
||||
|
@ -262,7 +250,8 @@ function ListItem({action, clients, filterID, idx}: ListItemProps) {
|
|||
</div>
|
||||
</div>
|
||||
)
|
||||
case "DELUGE":
|
||||
case "DELUGE_V1":
|
||||
case "DELUGE_V2":
|
||||
return (
|
||||
<div>
|
||||
<div className="mt-6 grid grid-cols-12 gap-6">
|
||||
|
@ -426,7 +415,7 @@ function ListItem({action, clients, filterID, idx}: ListItemProps) {
|
|||
</div>
|
||||
<div className="mt-4 flex-shrink-0 sm:mt-0 sm:ml-5">
|
||||
<div className="flex overflow-hidden -space-x-1">
|
||||
<span className="text-sm font-normal text-gray-500">{action.type}</span>
|
||||
<span className="text-sm font-normal text-gray-500">{ActionTypeNameMap[action.type]}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -561,7 +550,7 @@ function ListItem({action, clients, filterID, idx}: ListItemProps) {
|
|||
<Listbox.Button
|
||||
className="bg-white relative w-full border border-gray-300 rounded-md shadow-sm pl-3 pr-10 py-2 text-left cursor-default focus:outline-none focus:ring-1 focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm">
|
||||
<span
|
||||
className="block truncate">{input.value ? actionTypeOptions.find(c => c.value === input.value)!.label : "Choose a type"}</span>
|
||||
className="block truncate">{input.value ? ActionTypeOptions.find(c => c.value === input.value)!.label : "Choose a type"}</span>
|
||||
<span
|
||||
className="absolute inset-y-0 right-0 flex items-center pr-2 pointer-events-none">
|
||||
<SelectorIcon className="h-5 w-5 text-gray-400" aria-hidden="true"/>
|
||||
|
@ -579,7 +568,7 @@ function ListItem({action, clients, filterID, idx}: ListItemProps) {
|
|||
static
|
||||
className="absolute z-10 mt-1 w-full bg-white shadow-lg max-h-60 rounded-md py-1 text-base ring-1 ring-black ring-opacity-5 overflow-auto focus:outline-none sm:text-sm"
|
||||
>
|
||||
{actionTypeOptions.map((opt) => (
|
||||
{ActionTypeOptions.map((opt) => (
|
||||
<Listbox.Option
|
||||
key={opt.value}
|
||||
className={({active}) =>
|
||||
|
|
97
web/src/components/inputs/SelectField.tsx
Normal file
97
web/src/components/inputs/SelectField.tsx
Normal file
|
@ -0,0 +1,97 @@
|
|||
import {Field} from "react-final-form";
|
||||
import {Listbox, Transition} from "@headlessui/react";
|
||||
import {CheckIcon, SelectorIcon} from "@heroicons/react/solid";
|
||||
import React, {Fragment} from "react";
|
||||
import {classNames} from "../../styles/utils";
|
||||
|
||||
|
||||
interface SelectOption {
|
||||
label: string;
|
||||
value: string;
|
||||
}
|
||||
|
||||
interface props {
|
||||
name: string;
|
||||
label: string;
|
||||
optionDefaultText: string;
|
||||
options: SelectOption[];
|
||||
}
|
||||
|
||||
function SelectField({name, label, optionDefaultText, options}: props) {
|
||||
return (
|
||||
<div className="col-span-6 sm:col-span-6">
|
||||
<Field
|
||||
name={name}
|
||||
type="select"
|
||||
render={({input}) => (
|
||||
<Listbox value={input.value} onChange={input.onChange}>
|
||||
{({open}) => (
|
||||
<div
|
||||
className="space-y-1 px-4 sm:space-y-0 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-6 sm:py-5">
|
||||
<Listbox.Label
|
||||
className="block text-sm font-medium text-gray-900 sm:mt-px sm:pt-2">{label}</Listbox.Label>
|
||||
<div className="mt-2 relative">
|
||||
<Listbox.Button
|
||||
className="bg-white relative w-full border border-gray-300 rounded-md shadow-sm pl-3 pr-10 py-2 text-left cursor-default focus:outline-none focus:ring-1 focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm">
|
||||
<span
|
||||
className="block truncate">{input.value ? options.find(c => c.value === input.value)!.label : optionDefaultText}</span>
|
||||
<span
|
||||
className="absolute inset-y-0 right-0 flex items-center pr-2 pointer-events-none">
|
||||
<SelectorIcon className="h-5 w-5 text-gray-400" aria-hidden="true"/>
|
||||
</span>
|
||||
</Listbox.Button>
|
||||
|
||||
<Transition
|
||||
show={open}
|
||||
as={Fragment}
|
||||
leave="transition ease-in duration-100"
|
||||
leaveFrom="opacity-100"
|
||||
leaveTo="opacity-0"
|
||||
>
|
||||
<Listbox.Options
|
||||
static
|
||||
className="absolute z-10 mt-1 w-full bg-white shadow-lg max-h-60 rounded-md py-1 text-base ring-1 ring-black ring-opacity-5 overflow-auto focus:outline-none sm:text-sm"
|
||||
>
|
||||
{options.map((opt) => (
|
||||
<Listbox.Option
|
||||
key={opt.value}
|
||||
className={({active}) =>
|
||||
classNames(
|
||||
active ? 'text-white bg-indigo-600' : 'text-gray-900',
|
||||
'cursor-default select-none relative py-2 pl-3 pr-9'
|
||||
)
|
||||
}
|
||||
value={opt.value}
|
||||
>
|
||||
{({selected, active}) => (
|
||||
<>
|
||||
<span className={classNames(selected ? 'font-semibold' : 'font-normal', 'block truncate')}>
|
||||
{opt.label}
|
||||
</span>
|
||||
|
||||
{selected ? (
|
||||
<span
|
||||
className={classNames(
|
||||
active ? 'text-white' : 'text-indigo-600',
|
||||
'absolute inset-y-0 right-0 flex items-center pr-4'
|
||||
)}
|
||||
>
|
||||
<CheckIcon className="h-5 w-5" aria-hidden="true"/>
|
||||
</span>
|
||||
) : null}
|
||||
</>
|
||||
)}
|
||||
</Listbox.Option>
|
||||
))}
|
||||
</Listbox.Options>
|
||||
</Transition>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</Listbox>
|
||||
)}/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default SelectField;
|
|
@ -5,3 +5,4 @@ export { default as TextAreaWide } from "./TextAreaWide";
|
|||
export { default as MultiSelectField } from "./MultiSelectField";
|
||||
export { default as RadioFieldset } from "./RadioFieldset";
|
||||
export { default as SwitchGroup } from "./SwitchGroup";
|
||||
export { default as SelectField } from "./SelectField";
|
||||
|
|
|
@ -69,10 +69,30 @@ export interface radioFieldsetOption {
|
|||
}
|
||||
|
||||
export const DownloadClientTypeOptions: radioFieldsetOption[] = [
|
||||
{
|
||||
label: "qBittorrent",
|
||||
description: "Add torrents directly to qBittorrent",
|
||||
value: DOWNLOAD_CLIENT_TYPES.qBittorrent
|
||||
},
|
||||
{label: "Deluge", description: "Add torrents directly to Deluge", value: DOWNLOAD_CLIENT_TYPES.Deluge},
|
||||
{label: "qBittorrent", description: "Add torrents directly to qBittorrent", value: DOWNLOAD_CLIENT_TYPES.qBittorrent},
|
||||
{label: "Deluge", description: "Add torrents directly to Deluge", value: DOWNLOAD_CLIENT_TYPES.DelugeV1},
|
||||
{label: "Deluge 2", description: "Add torrents directly to Deluge 2", value: DOWNLOAD_CLIENT_TYPES.DelugeV2},
|
||||
];
|
||||
export const DownloadClientTypeNameMap = {
|
||||
"DELUGE_V1": "Deluge v1",
|
||||
"DELUGE_V2": "Deluge v2",
|
||||
"QBITTORRENT": "qBittorrent"
|
||||
};
|
||||
|
||||
export const ActionTypeOptions: radioFieldsetOption[] = [
|
||||
{label: "Test", description: "A simple action to test a filter.", value: "TEST"},
|
||||
{label: "Watch dir", description: "Add filtered torrents to a watch directory", value: "WATCH_FOLDER"},
|
||||
{label: "Exec", description: "Run a custom command after a filter match", value: "EXEC"},
|
||||
{label: "qBittorrent", description: "Add torrents directly to qBittorrent", value: "QBITTORRENT"},
|
||||
{label: "Deluge", description: "Add torrents directly to Deluge", value: "DELUGE_V1"},
|
||||
{label: "Deluge v2", description: "Add torrents directly to Deluge 2", value: "DELUGE_V2"},
|
||||
];
|
||||
|
||||
export const ActionTypeNameMap = {
|
||||
"TEST": "Test",
|
||||
"WATCH_FOLDER": "Watch folder",
|
||||
"EXEC": "Exec",
|
||||
"DELUGE_V1": "Deluge v1",
|
||||
"DELUGE_V2": "Deluge v2",
|
||||
"QBITTORRENT": "qBittorrent"
|
||||
};
|
||||
|
|
|
@ -64,26 +64,18 @@ export interface Filter {
|
|||
indexers: Indexer[];
|
||||
}
|
||||
|
||||
export interface Tracker {
|
||||
id: number;
|
||||
name: string;
|
||||
type: string;
|
||||
enabled: boolean;
|
||||
}
|
||||
|
||||
export type ActionType = 'TEST' | 'EXEC' | 'WATCH_FOLDER' | 'QBITTORRENT' | 'DELUGE';
|
||||
export const ACTIONTYPES: ActionType[] = ['TEST', 'EXEC' , 'WATCH_FOLDER' , 'QBITTORRENT' , 'DELUGE'];
|
||||
export type ActionType = 'TEST' | 'EXEC' | 'WATCH_FOLDER' | 'QBITTORRENT' | 'DELUGE_V1' | 'DELUGE_V2';
|
||||
export const ACTIONTYPES: ActionType[] = ['TEST', 'EXEC' , 'WATCH_FOLDER' , 'QBITTORRENT' , 'DELUGE_V1', 'DELUGE_V2'];
|
||||
|
||||
|
||||
export type DownloadClientType = 'QBITTORRENT' | 'DELUGE';
|
||||
export type DownloadClientType = 'QBITTORRENT' | 'DELUGE_V1' | 'DELUGE_V2';
|
||||
|
||||
// export const DOWNLOAD_CLIENT_TYPES: DownloadClientType[] = ['QBITTORRENT' , 'DELUGE'];
|
||||
export enum DOWNLOAD_CLIENT_TYPES {
|
||||
qBittorrent = 'QBITTORRENT',
|
||||
Deluge = 'DELUGE'
|
||||
DelugeV1 = 'DELUGE_V1',
|
||||
DelugeV2 = 'DELUGE_V2'
|
||||
}
|
||||
|
||||
|
||||
export interface DownloadClient {
|
||||
id: number;
|
||||
name: string;
|
||||
|
|
|
@ -9,20 +9,7 @@ import {classNames} from "../../styles/utils";
|
|||
import {Field, Form} from "react-final-form";
|
||||
import DEBUG from "../../components/debug";
|
||||
import APIClient from "../../api/APIClient";
|
||||
|
||||
interface radioFieldsetOption {
|
||||
label: string;
|
||||
description: string;
|
||||
value: string;
|
||||
}
|
||||
|
||||
const actionTypeOptions: radioFieldsetOption[] = [
|
||||
{label: "Test", description: "A simple action to test a filter.", value: "TEST"},
|
||||
{label: "Watch dir", description: "Add filtered torrents to a watch directory", value: "WATCH_FOLDER"},
|
||||
{label: "Exec", description: "Run a custom command after a filter match", value: "EXEC"},
|
||||
{label: "qBittorrent", description: "Add torrents directly to qBittorrent", value: "QBITTORRENT"},
|
||||
{label: "Deluge", description: "Add torrents directly to Deluge", value: "DELUGE"},
|
||||
];
|
||||
import {ActionTypeOptions} from "../../domain/constants";
|
||||
|
||||
interface props {
|
||||
filter: Filter;
|
||||
|
@ -393,7 +380,8 @@ function FilterActionAddForm({filter, isOpen, toggle, clients}: props) {
|
|||
</div>
|
||||
</div>
|
||||
)
|
||||
case "DELUGE":
|
||||
case "DELUGE_V1":
|
||||
case "DELUGE_V2":
|
||||
return (
|
||||
<div>
|
||||
{/*TODO choose client*/}
|
||||
|
@ -729,14 +717,14 @@ function FilterActionAddForm({filter, isOpen, toggle, clients}: props) {
|
|||
<RadioGroup value={values.type} onChange={input.onChange}>
|
||||
<RadioGroup.Label className="sr-only">Privacy setting</RadioGroup.Label>
|
||||
<div className="bg-white rounded-md -space-y-px">
|
||||
{actionTypeOptions.map((setting, settingIdx) => (
|
||||
{ActionTypeOptions.map((setting, settingIdx) => (
|
||||
<RadioGroup.Option
|
||||
key={setting.value}
|
||||
value={setting.value}
|
||||
className={({checked}) =>
|
||||
classNames(
|
||||
settingIdx === 0 ? 'rounded-tl-md rounded-tr-md' : '',
|
||||
settingIdx === actionTypeOptions.length - 1 ? 'rounded-bl-md rounded-br-md' : '',
|
||||
settingIdx === ActionTypeOptions.length - 1 ? 'rounded-bl-md rounded-br-md' : '',
|
||||
checked ? 'bg-indigo-50 border-indigo-200 z-10' : 'border-gray-200',
|
||||
'relative border p-4 flex cursor-pointer focus:outline-none'
|
||||
)
|
||||
|
|
|
@ -9,20 +9,7 @@ import {classNames} from "../../styles/utils";
|
|||
import {Field, Form} from "react-final-form";
|
||||
import DEBUG from "../../components/debug";
|
||||
import APIClient from "../../api/APIClient";
|
||||
|
||||
interface radioFieldsetOption {
|
||||
label: string;
|
||||
description: string;
|
||||
value: string;
|
||||
}
|
||||
|
||||
const actionTypeOptions: radioFieldsetOption[] = [
|
||||
{label: "Test", description: "A simple action to test a filter.", value: "TEST"},
|
||||
{label: "Watch dir", description: "Add filtered torrents to a watch directory", value: "WATCH_FOLDER"},
|
||||
{label: "Exec", description: "Run a custom command after a filter match", value: "EXEC"},
|
||||
{label: "qBittorrent", description: "Add torrents directly to qBittorrent", value: "QBITTORRENT"},
|
||||
{label: "Deluge", description: "Add torrents directly to Deluge", value: "DELUGE"},
|
||||
];
|
||||
import {ActionTypeOptions} from "../../domain/constants";
|
||||
|
||||
interface props {
|
||||
filter: Filter;
|
||||
|
@ -35,7 +22,7 @@ interface props {
|
|||
function FilterActionUpdateForm({filter, isOpen, toggle, clients, action}: props) {
|
||||
const mutation = useMutation((action: Action) => APIClient.actions.update(action), {
|
||||
onSuccess: () => {
|
||||
console.log("add action");
|
||||
// console.log("add action");
|
||||
queryClient.invalidateQueries(['filter', filter.id]);
|
||||
sleep(1500)
|
||||
|
||||
|
@ -44,7 +31,7 @@ function FilterActionUpdateForm({filter, isOpen, toggle, clients, action}: props
|
|||
})
|
||||
|
||||
useEffect(() => {
|
||||
console.log("render add action form", clients)
|
||||
// console.log("render add action form", clients)
|
||||
}, [clients]);
|
||||
|
||||
const onSubmit = (data: any) => {
|
||||
|
@ -399,7 +386,8 @@ function FilterActionUpdateForm({filter, isOpen, toggle, clients, action}: props
|
|||
</div>
|
||||
</div>
|
||||
)
|
||||
case "DELUGE":
|
||||
case "DELUGE_V1":
|
||||
case "DELUGE_V2":
|
||||
return (
|
||||
<div>
|
||||
{/*TODO choose client*/}
|
||||
|
@ -734,14 +722,14 @@ function FilterActionUpdateForm({filter, isOpen, toggle, clients, action}: props
|
|||
<RadioGroup value={values.type} onChange={input.onChange}>
|
||||
<RadioGroup.Label className="sr-only">Privacy setting</RadioGroup.Label>
|
||||
<div className="bg-white rounded-md -space-y-px">
|
||||
{actionTypeOptions.map((setting, settingIdx) => (
|
||||
{ActionTypeOptions.map((setting, settingIdx) => (
|
||||
<RadioGroup.Option
|
||||
key={setting.value}
|
||||
value={setting.value}
|
||||
className={({checked}) =>
|
||||
classNames(
|
||||
settingIdx === 0 ? 'rounded-tl-md rounded-tr-md' : '',
|
||||
settingIdx === actionTypeOptions.length - 1 ? 'rounded-bl-md rounded-br-md' : '',
|
||||
settingIdx === ActionTypeOptions.length - 1 ? 'rounded-bl-md rounded-br-md' : '',
|
||||
checked ? 'bg-indigo-50 border-indigo-200 z-10' : 'border-gray-200',
|
||||
'relative border p-4 flex cursor-pointer focus:outline-none'
|
||||
)
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import {Fragment, useState} from "react";
|
||||
import React, {Fragment, useState} from "react";
|
||||
import {useMutation} from "react-query";
|
||||
import {DOWNLOAD_CLIENT_TYPES, DownloadClient} from "../../domain/interfaces";
|
||||
import {Dialog, RadioGroup, Transition} from "@headlessui/react";
|
||||
|
@ -10,21 +10,7 @@ import {SwitchGroup} from "../../components/inputs";
|
|||
import {queryClient} from "../../App";
|
||||
import APIClient from "../../api/APIClient";
|
||||
import {sleep} from "../../utils/utils";
|
||||
|
||||
interface radioFieldsetOption {
|
||||
label: string;
|
||||
description: string;
|
||||
value: string;
|
||||
}
|
||||
|
||||
const downloadClientTypeOptions: radioFieldsetOption[] = [
|
||||
{
|
||||
label: "qBittorrent",
|
||||
description: "Add torrents directly to qBittorrent",
|
||||
value: DOWNLOAD_CLIENT_TYPES.qBittorrent
|
||||
},
|
||||
{label: "Deluge", description: "Add torrents directly to Deluge", value: DOWNLOAD_CLIENT_TYPES.Deluge},
|
||||
];
|
||||
import {DownloadClientTypeOptions} from "../../domain/constants";
|
||||
|
||||
function DownloadClientAddForm({isOpen, toggle}: any) {
|
||||
const [isTesting, setIsTesting] = useState(false)
|
||||
|
@ -181,18 +167,18 @@ function DownloadClientAddForm({isOpen, toggle}: any) {
|
|||
<RadioGroup value={values.type}
|
||||
onChange={input.onChange}>
|
||||
<RadioGroup.Label
|
||||
className="sr-only">Privacy
|
||||
setting</RadioGroup.Label>
|
||||
className="sr-only">Client
|
||||
type</RadioGroup.Label>
|
||||
<div
|
||||
className="bg-white rounded-md -space-y-px">
|
||||
{downloadClientTypeOptions.map((setting, settingIdx) => (
|
||||
{DownloadClientTypeOptions.map((setting, settingIdx) => (
|
||||
<RadioGroup.Option
|
||||
key={setting.value}
|
||||
value={setting.value}
|
||||
className={({checked}) =>
|
||||
classNames(
|
||||
settingIdx === 0 ? 'rounded-tl-md rounded-tr-md' : '',
|
||||
settingIdx === downloadClientTypeOptions.length - 1 ? 'rounded-bl-md rounded-br-md' : '',
|
||||
settingIdx === DownloadClientTypeOptions.length - 1 ? 'rounded-bl-md rounded-br-md' : '',
|
||||
checked ? 'bg-indigo-50 border-indigo-200 z-10' : 'border-gray-200',
|
||||
'relative border p-4 flex cursor-pointer focus:outline-none'
|
||||
)
|
||||
|
@ -245,7 +231,6 @@ function DownloadClientAddForm({isOpen, toggle}: any) {
|
|||
</fieldset>
|
||||
|
||||
<div>
|
||||
|
||||
<div
|
||||
className="space-y-1 px-4 sm:space-y-0 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-6 sm:py-5">
|
||||
<div>
|
||||
|
|
|
@ -7,6 +7,7 @@ import {classNames} from "../../styles/utils";
|
|||
import { DownloadClientAddForm, DownloadClientUpdateForm } from "../../forms";
|
||||
import EmptySimple from "../../components/empty/EmptySimple";
|
||||
import APIClient from "../../api/APIClient";
|
||||
import {DownloadClientTypeNameMap} from "../../domain/constants";
|
||||
|
||||
interface DownloadLClientSettingsListItemProps {
|
||||
client: DownloadClient;
|
||||
|
@ -48,7 +49,7 @@ function DownloadClientSettingsListItem({ client, idx }: DownloadLClientSettings
|
|||
</Switch>
|
||||
</td>
|
||||
<td className="px-6 py-4 whitespace-nowrap text-sm font-medium text-gray-900">{client.name}</td>
|
||||
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500">{client.type}</td>
|
||||
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500">{DownloadClientTypeNameMap[client.type]}</td>
|
||||
<td className="px-6 py-4 whitespace-nowrap text-right text-sm font-medium">
|
||||
<span className="text-indigo-600 hover:text-indigo-900 cursor-pointer" onClick={toggleUpdateClient}>
|
||||
Edit
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue