feat(downloadclients): qBit rules add speed threshold condition (#652)

* fix: qbit add rules min check

* feat(downloadclients): add check condition

* feat(downloadclient): return on rejection
This commit is contained in:
ze0s 2023-01-17 23:34:03 +01:00 committed by GitHub
parent 4ae2773dc9
commit a6c1944df8
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
6 changed files with 194 additions and 34 deletions

View file

@ -104,6 +104,8 @@ func (s *service) prepareQbitOptions(action *domain.Action) (map[string]string,
func (s *service) qbittorrentCheckRulesCanDownload(ctx context.Context, action *domain.Action, client *domain.DownloadClient, qbt *qbittorrent.Client) ([]string, error) { func (s *service) qbittorrentCheckRulesCanDownload(ctx context.Context, action *domain.Action, client *domain.DownloadClient, qbt *qbittorrent.Client) ([]string, error) {
s.log.Trace().Msgf("action qBittorrent: %v check rules", action.Name) s.log.Trace().Msgf("action qBittorrent: %v check rules", action.Name)
checked := false
// check for active downloads and other rules // check for active downloads and other rules
if client.Settings.Rules.Enabled && !action.IgnoreRules { if client.Settings.Rules.Enabled && !action.IgnoreRules {
activeDownloads, err := qbt.GetTorrentsActiveDownloadsCtx(ctx) activeDownloads, err := qbt.GetTorrentsActiveDownloadsCtx(ctx)
@ -117,33 +119,16 @@ func (s *service) qbittorrentCheckRulesCanDownload(ctx context.Context, action *
// if max active downloads reached, check speed and if lower than threshold add anyway // if max active downloads reached, check speed and if lower than threshold add anyway
if len(activeDownloads) >= client.Settings.Rules.MaxActiveDownloads { if len(activeDownloads) >= client.Settings.Rules.MaxActiveDownloads {
if client.Settings.Rules.IgnoreSlowTorrents { if client.Settings.Rules.IgnoreSlowTorrents {
// check speeds of downloads if client.Settings.Rules.IgnoreSlowTorrentsCondition == domain.IgnoreSlowTorrentsModeMaxReached {
info, err := qbt.GetTransferInfoCtx(ctx) rejections, err := s.qbittorrentCheckIgnoreSlow(ctx, client, qbt)
if err != nil { if err != nil {
return nil, errors.Wrap(err, "could not get transfer info") return rejections, err
}
// if current transfer speed is more than threshold return out and skip
// DlInfoSpeed is in bytes so lets convert to KB to match DownloadSpeedThreshold
if info.DlInfoSpeed/1024 >= client.Settings.Rules.DownloadSpeedThreshold {
rejection := fmt.Sprintf("max active downloads reached and total download speed above threshold: %d, skipping", client.Settings.Rules.DownloadSpeedThreshold)
s.log.Debug().Msg(rejection)
return []string{rejection}, nil
}
// if current transfer speed is more than threshold return out and skip
// UpInfoSpeed is in bytes so lets convert to KB to match UploadSpeedThreshold
if info.UpInfoSpeed/1024 >= client.Settings.Rules.UploadSpeedThreshold {
rejection := fmt.Sprintf("max active downloads reached and total upload speed above threshold: %d, skipping", client.Settings.Rules.UploadSpeedThreshold)
s.log.Debug().Msg(rejection)
return []string{rejection}, nil
} }
s.log.Debug().Msg("active downloads are slower than set limit, lets add it") s.log.Debug().Msg("active downloads are slower than set limit, lets add it")
checked = true
}
} else { } else {
rejection := "max active downloads reached, skipping" rejection := "max active downloads reached, skipping"
@ -153,7 +138,56 @@ func (s *service) qbittorrentCheckRulesCanDownload(ctx context.Context, action *
} }
} }
} }
if !checked && client.Settings.Rules.IgnoreSlowTorrentsCondition == domain.IgnoreSlowTorrentsModeAlways {
rejections, err := s.qbittorrentCheckIgnoreSlow(ctx, client, qbt)
if err != nil {
return rejections, err
}
if len(rejections) > 0 {
return rejections, nil
}
}
} }
return nil, nil return nil, nil
} }
func (s *service) qbittorrentCheckIgnoreSlow(ctx context.Context, client *domain.DownloadClient, qbt *qbittorrent.Client) ([]string, error) {
// get transfer info
info, err := qbt.GetTransferInfoCtx(ctx)
if err != nil {
return nil, errors.Wrap(err, "could not get transfer info")
}
s.log.Debug().Msgf("checking client ignore slow torrent rules: %+v", info)
if client.Settings.Rules.DownloadSpeedThreshold > 0 {
// if current transfer speed is more than threshold return out and skip
// DlInfoSpeed is in bytes so lets convert to KB to match DownloadSpeedThreshold
if info.DlInfoSpeed/1024 >= client.Settings.Rules.DownloadSpeedThreshold {
rejection := fmt.Sprintf("max active downloads reached and total download speed (%d) above threshold: (%d), skipping", info.DlInfoSpeed/1024, client.Settings.Rules.DownloadSpeedThreshold)
s.log.Debug().Msg(rejection)
return []string{rejection}, nil
}
}
if client.Settings.Rules.UploadSpeedThreshold > 0 {
// if current transfer speed is more than threshold return out and skip
// UpInfoSpeed is in bytes so lets convert to KB to match UploadSpeedThreshold
if info.UpInfoSpeed/1024 >= client.Settings.Rules.UploadSpeedThreshold {
rejection := fmt.Sprintf("max active downloads reached and total upload speed (%d) above threshold: (%d), skipping", info.UpInfoSpeed/1024, client.Settings.Rules.UploadSpeedThreshold)
s.log.Debug().Msg(rejection)
return []string{rejection}, nil
}
}
s.log.Debug().Msg("active downloads are slower than set limit, lets add it")
return nil, nil
}

View file

@ -47,6 +47,7 @@ type DownloadClientRules struct {
Enabled bool `json:"enabled"` Enabled bool `json:"enabled"`
MaxActiveDownloads int `json:"max_active_downloads"` MaxActiveDownloads int `json:"max_active_downloads"`
IgnoreSlowTorrents bool `json:"ignore_slow_torrents"` IgnoreSlowTorrents bool `json:"ignore_slow_torrents"`
IgnoreSlowTorrentsCondition IgnoreSlowTorrentsCondition `json:"ignore_slow_torrents_condition,omitempty"`
DownloadSpeedThreshold int64 `json:"download_speed_threshold"` DownloadSpeedThreshold int64 `json:"download_speed_threshold"`
UploadSpeedThreshold int64 `json:"upload_speed_threshold"` UploadSpeedThreshold int64 `json:"upload_speed_threshold"`
} }
@ -57,6 +58,13 @@ type BasicAuth struct {
Password string `json:"password,omitempty"` Password string `json:"password,omitempty"`
} }
type IgnoreSlowTorrentsCondition string
const (
IgnoreSlowTorrentsModeAlways IgnoreSlowTorrentsCondition = "ALWAYS"
IgnoreSlowTorrentsModeMaxReached IgnoreSlowTorrentsCondition = "MAX_DOWNLOADS_REACHED"
)
type DownloadClientType string type DownloadClientType string
const ( const (

View file

@ -5,6 +5,8 @@ import { useToggle } from "../../hooks/hooks";
import { EyeIcon, EyeSlashIcon } from "@heroicons/react/24/solid"; import { EyeIcon, EyeSlashIcon } from "@heroicons/react/24/solid";
import { Switch } from "@headlessui/react"; import { Switch } from "@headlessui/react";
import { ErrorField } from "./common"; import { ErrorField } from "./common";
import Select, { components, ControlProps, InputProps, MenuProps, OptionProps } from "react-select";
import { SelectFieldProps } from "./select";
interface TextFieldWideProps { interface TextFieldWideProps {
name: string; name: string;
@ -303,3 +305,100 @@ export const SwitchGroupWideRed = ({
</ul> </ul>
); );
const Input = (props: InputProps) => {
return (
<components.Input
{...props}
inputClassName="outline-none border-none shadow-none focus:ring-transparent"
className="text-gray-400 dark:text-gray-100"
children={props.children}
/>
);
};
const Control = (props: ControlProps) => {
return (
<components.Control
{...props}
className="p-1 block w-full dark:bg-gray-800 border border-gray-300 dark:border-gray-700 rounded-md shadow-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500 dark:text-gray-100 sm:text-sm"
children={props.children}
/>
);
};
const Menu = (props: MenuProps) => {
return (
<components.Menu
{...props}
className="dark:bg-gray-800 border border-gray-300 dark:border-gray-700 dark:text-gray-400 rounded-md shadow-sm"
children={props.children}
/>
);
};
const Option = (props: OptionProps) => {
return (
<components.Option
{...props}
className="dark:text-gray-400 dark:bg-gray-800 dark:hover:bg-gray-900 dark:focus:bg-gray-900"
children={props.children}
/>
);
};
export const SelectFieldWide = ({
name,
label,
optionDefaultText,
options
}: SelectFieldProps) => (
<div className="flex items-center justify-between space-y-1 px-4 py-4 sm:space-y-0 sm:grid sm:grid-cols-3 sm:gap-4">
<div>
<label
htmlFor={name}
className="block text-sm font-medium text-gray-900 dark:text-white"
>
{label}
</label>
</div>
<div className="sm:col-span-2">
<Field name={name} type="select">
{({
field,
form: { setFieldValue }
}: FieldProps) => (
<Select
{...field}
id={name}
isClearable={true}
isSearchable={true}
components={{
Input,
Control,
Menu,
Option
}}
placeholder={optionDefaultText}
styles={{
singleValue: (base) => ({
...base,
color: "unset"
})
}}
theme={(theme) => ({
...theme,
spacing: {
...theme.spacing,
controlHeight: 30,
baseUnit: 2
}
})}
value={field?.value && field.value.value}
onChange={(option) => setFieldValue(field.name, option?.value ?? "")}
options={options}
/>
)}
</Field>
</div>
</div>
);

View file

@ -234,12 +234,12 @@ export function DownloadClientSelect({
); );
} }
interface SelectFieldOption { export interface SelectFieldOption {
label: string; label: string;
value: string; value: string;
} }
interface SelectFieldProps { export interface SelectFieldProps {
name: string; name: string;
label: string; label: string;
optionDefaultText: string; optionDefaultText: string;

View file

@ -403,6 +403,18 @@ export const downloadsPerUnitOptions: OptionBasic[] = [
} }
]; ];
export const DownloadRuleConditionOptions: OptionBasic[] = [
{
label: "Always",
value: "ALWAYS"
},
{
label: "Max downloads reached",
value: "MAX_DOWNLOADS_REACHED"
}
];
export interface SelectOption { export interface SelectOption {
label: string; label: string;
description: string; description: string;

View file

@ -7,7 +7,7 @@ import { Form, Formik, useFormikContext } from "formik";
import DEBUG from "../../components/debug"; import DEBUG from "../../components/debug";
import { queryClient } from "../../App"; import { queryClient } from "../../App";
import { APIClient } from "../../api/APIClient"; import { APIClient } from "../../api/APIClient";
import { DownloadClientTypeOptions } from "../../domain/constants"; import { DownloadClientTypeOptions, DownloadRuleConditionOptions } from "../../domain/constants";
import { toast } from "react-hot-toast"; import { toast } from "react-hot-toast";
import Toast from "../../components/notifications/Toast"; import Toast from "../../components/notifications/Toast";
@ -21,6 +21,7 @@ import {
TextFieldWide TextFieldWide
} from "../../components/inputs"; } from "../../components/inputs";
import DownloadClient from "../../screens/settings/DownloadClient"; import DownloadClient from "../../screens/settings/DownloadClient";
import { SelectFieldWide } from "../../components/inputs/input_wide";
interface InitialValuesSettings { interface InitialValuesSettings {
basic?: { basic?: {
@ -270,6 +271,12 @@ function FormFieldsRules() {
{settings.rules?.ignore_slow_torrents === true && ( {settings.rules?.ignore_slow_torrents === true && (
<> <>
<SelectFieldWide
name="settings.rules.ignore_slow_torrents_condition"
label="Ignore condition"
optionDefaultText="Select ignore condition"
options={DownloadRuleConditionOptions}
/>
<NumberFieldWide <NumberFieldWide
name="settings.rules.download_speed_threshold" name="settings.rules.download_speed_threshold"
label="Download speed threshold" label="Download speed threshold"