mirror of
https://github.com/idanoo/autobrr
synced 2025-07-23 08:49:13 +00:00
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:
parent
4ae2773dc9
commit
a6c1944df8
6 changed files with 194 additions and 34 deletions
|
@ -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) {
|
||||
s.log.Trace().Msgf("action qBittorrent: %v check rules", action.Name)
|
||||
|
||||
checked := false
|
||||
|
||||
// check for active downloads and other rules
|
||||
if client.Settings.Rules.Enabled && !action.IgnoreRules {
|
||||
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 len(activeDownloads) >= client.Settings.Rules.MaxActiveDownloads {
|
||||
if client.Settings.Rules.IgnoreSlowTorrents {
|
||||
// check speeds of downloads
|
||||
info, err := qbt.GetTransferInfoCtx(ctx)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "could not get transfer info")
|
||||
if client.Settings.Rules.IgnoreSlowTorrentsCondition == domain.IgnoreSlowTorrentsModeMaxReached {
|
||||
rejections, err := s.qbittorrentCheckIgnoreSlow(ctx, client, qbt)
|
||||
if err != nil {
|
||||
return rejections, err
|
||||
}
|
||||
|
||||
s.log.Debug().Msg("active downloads are slower than set limit, lets add it")
|
||||
|
||||
checked = true
|
||||
}
|
||||
|
||||
// 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")
|
||||
} else {
|
||||
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
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
|
|
@ -44,11 +44,12 @@ type DownloadClientSettings struct {
|
|||
}
|
||||
|
||||
type DownloadClientRules struct {
|
||||
Enabled bool `json:"enabled"`
|
||||
MaxActiveDownloads int `json:"max_active_downloads"`
|
||||
IgnoreSlowTorrents bool `json:"ignore_slow_torrents"`
|
||||
DownloadSpeedThreshold int64 `json:"download_speed_threshold"`
|
||||
UploadSpeedThreshold int64 `json:"upload_speed_threshold"`
|
||||
Enabled bool `json:"enabled"`
|
||||
MaxActiveDownloads int `json:"max_active_downloads"`
|
||||
IgnoreSlowTorrents bool `json:"ignore_slow_torrents"`
|
||||
IgnoreSlowTorrentsCondition IgnoreSlowTorrentsCondition `json:"ignore_slow_torrents_condition,omitempty"`
|
||||
DownloadSpeedThreshold int64 `json:"download_speed_threshold"`
|
||||
UploadSpeedThreshold int64 `json:"upload_speed_threshold"`
|
||||
}
|
||||
|
||||
type BasicAuth struct {
|
||||
|
@ -57,6 +58,13 @@ type BasicAuth struct {
|
|||
Password string `json:"password,omitempty"`
|
||||
}
|
||||
|
||||
type IgnoreSlowTorrentsCondition string
|
||||
|
||||
const (
|
||||
IgnoreSlowTorrentsModeAlways IgnoreSlowTorrentsCondition = "ALWAYS"
|
||||
IgnoreSlowTorrentsModeMaxReached IgnoreSlowTorrentsCondition = "MAX_DOWNLOADS_REACHED"
|
||||
)
|
||||
|
||||
type DownloadClientType string
|
||||
|
||||
const (
|
||||
|
|
|
@ -5,6 +5,8 @@ import { useToggle } from "../../hooks/hooks";
|
|||
import { EyeIcon, EyeSlashIcon } from "@heroicons/react/24/solid";
|
||||
import { Switch } from "@headlessui/react";
|
||||
import { ErrorField } from "./common";
|
||||
import Select, { components, ControlProps, InputProps, MenuProps, OptionProps } from "react-select";
|
||||
import { SelectFieldProps } from "./select";
|
||||
|
||||
interface TextFieldWideProps {
|
||||
name: string;
|
||||
|
@ -303,3 +305,100 @@ export const SwitchGroupWideRed = ({
|
|||
</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>
|
||||
);
|
||||
|
|
|
@ -234,12 +234,12 @@ export function DownloadClientSelect({
|
|||
);
|
||||
}
|
||||
|
||||
interface SelectFieldOption {
|
||||
export interface SelectFieldOption {
|
||||
label: string;
|
||||
value: string;
|
||||
}
|
||||
|
||||
interface SelectFieldProps {
|
||||
export interface SelectFieldProps {
|
||||
name: string;
|
||||
label: string;
|
||||
optionDefaultText: string;
|
||||
|
|
|
@ -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 {
|
||||
label: string;
|
||||
description: string;
|
||||
|
|
|
@ -7,7 +7,7 @@ import { Form, Formik, useFormikContext } from "formik";
|
|||
import DEBUG from "../../components/debug";
|
||||
import { queryClient } from "../../App";
|
||||
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 "../../components/notifications/Toast";
|
||||
|
@ -21,6 +21,7 @@ import {
|
|||
TextFieldWide
|
||||
} from "../../components/inputs";
|
||||
import DownloadClient from "../../screens/settings/DownloadClient";
|
||||
import { SelectFieldWide } from "../../components/inputs/input_wide";
|
||||
|
||||
interface InitialValuesSettings {
|
||||
basic?: {
|
||||
|
@ -270,6 +271,12 @@ function FormFieldsRules() {
|
|||
|
||||
{settings.rules?.ignore_slow_torrents === true && (
|
||||
<>
|
||||
<SelectFieldWide
|
||||
name="settings.rules.ignore_slow_torrents_condition"
|
||||
label="Ignore condition"
|
||||
optionDefaultText="Select ignore condition"
|
||||
options={DownloadRuleConditionOptions}
|
||||
/>
|
||||
<NumberFieldWide
|
||||
name="settings.rules.download_speed_threshold"
|
||||
label="Download speed threshold"
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue