feat(actions): qbit add options content layout and skip hash check (#393)

* feat(actions): qbit content layout and skip hash check

* feat(actions): qbit options
This commit is contained in:
ze0s 2022-08-02 18:06:45 +02:00 committed by GitHub
parent db9d048f5d
commit 9508cbb46c
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
13 changed files with 394 additions and 50 deletions

View file

@ -2,7 +2,6 @@ package action
import (
"context"
"strconv"
"strings"
"time"
@ -102,10 +101,23 @@ func (s *service) qbittorrent(action domain.Action, release domain.Release) ([]s
func (s *service) prepareQbitOptions(action domain.Action, m domain.Macro) (map[string]string, error) {
options := map[string]string{}
opts := &qbittorrent.TorrentAddOptions{}
if action.Paused {
options["paused"] = "true"
opts.Paused = BoolPointer(true)
}
if action.SkipHashCheck {
opts.SkipHashCheck = BoolPointer(true)
}
if action.ContentLayout != "" {
if action.ContentLayout == domain.ActionContentLayoutSubfolderCreate {
layout := qbittorrent.ContentLayoutSubfolderCreate
opts.ContentLayout = &layout
} else if action.ContentLayout == domain.ActionContentLayoutSubfolderNone {
layout := qbittorrent.ContentLayoutSubfolderNone
opts.ContentLayout = &layout
}
// if ORIGINAL then leave empty
}
if action.SavePath != "" {
// parse and replace values in argument string before continuing
@ -114,8 +126,8 @@ func (s *service) prepareQbitOptions(action domain.Action, m domain.Macro) (map[
return nil, errors.Wrap(err, "could not parse savepath macro: %v", action.SavePath)
}
options["savepath"] = actionArgs
options["autoTMM"] = "false"
opts.SavePath = &actionArgs
opts.AutoTMM = BoolPointer(false)
}
if action.Category != "" {
// parse and replace values in argument string before continuing
@ -124,7 +136,7 @@ func (s *service) prepareQbitOptions(action domain.Action, m domain.Macro) (map[
return nil, errors.Wrap(err, "could not parse category macro: %v", action.Category)
}
options["category"] = categoryArgs
opts.Category = &categoryArgs
}
if action.Tags != "" {
// parse and replace values in argument string before continuing
@ -133,22 +145,26 @@ func (s *service) prepareQbitOptions(action domain.Action, m domain.Macro) (map[
return nil, errors.Wrap(err, "could not parse tags macro: %v", action.Tags)
}
options["tags"] = tagsArgs
opts.Tags = &tagsArgs
}
if action.LimitUploadSpeed > 0 {
options["upLimit"] = strconv.FormatInt(action.LimitUploadSpeed*1000, 10)
opts.LimitUploadSpeed = &action.LimitUploadSpeed
}
if action.LimitDownloadSpeed > 0 {
options["dlLimit"] = strconv.FormatInt(action.LimitDownloadSpeed*1000, 10)
opts.LimitDownloadSpeed = &action.LimitDownloadSpeed
}
if action.LimitRatio > 0 {
options["ratioLimit"] = strconv.FormatFloat(action.LimitRatio, 'r', 2, 64)
opts.LimitRatio = &action.LimitRatio
}
if action.LimitSeedTime > 0 {
options["seedingTimeLimit"] = strconv.FormatInt(action.LimitSeedTime, 10)
opts.LimitSeedTime = &action.LimitSeedTime
}
return options, nil
return opts.Prepare(), nil
}
func BoolPointer(b bool) *bool {
return &b
}
func (s *service) qbittorrentCheckRulesCanDownload(action domain.Action, client *domain.DownloadClient, qbt *qbittorrent.Client) ([]string, error) {

View file

@ -70,6 +70,8 @@ func (r *ActionRepo) findByFilterID(ctx context.Context, tx *Tx, filterID int) (
"save_path",
"paused",
"ignore_rules",
"skip_hash_check",
"content_layout",
"limit_download_speed",
"limit_upload_speed",
"limit_ratio",
@ -103,7 +105,7 @@ func (r *ActionRepo) findByFilterID(ctx context.Context, tx *Tx, filterID int) (
for rows.Next() {
var a domain.Action
var execCmd, execArgs, watchFolder, category, tags, label, savePath, webhookHost, webhookType, webhookMethod, webhookData sql.NullString
var execCmd, execArgs, watchFolder, category, tags, label, savePath, contentLayout, webhookHost, webhookType, webhookMethod, webhookData sql.NullString
var limitUl, limitDl, limitSeedTime sql.NullInt64
var limitRatio sql.NullFloat64
@ -111,7 +113,7 @@ func (r *ActionRepo) findByFilterID(ctx context.Context, tx *Tx, filterID int) (
// filterID
var paused, ignoreRules sql.NullBool
if err := rows.Scan(&a.ID, &a.Name, &a.Type, &a.Enabled, &execCmd, &execArgs, &watchFolder, &category, &tags, &label, &savePath, &paused, &ignoreRules, &limitDl, &limitUl, &limitRatio, &limitSeedTime, &a.ReAnnounceSkip, &a.ReAnnounceDelete, &a.ReAnnounceInterval, &a.ReAnnounceMaxAttempts, &webhookHost, &webhookType, &webhookMethod, &webhookData, &clientID); err != nil {
if err := rows.Scan(&a.ID, &a.Name, &a.Type, &a.Enabled, &execCmd, &execArgs, &watchFolder, &category, &tags, &label, &savePath, &paused, &ignoreRules, &a.SkipHashCheck, &contentLayout, &limitDl, &limitUl, &limitRatio, &limitSeedTime, &a.ReAnnounceSkip, &a.ReAnnounceDelete, &a.ReAnnounceInterval, &a.ReAnnounceMaxAttempts, &webhookHost, &webhookType, &webhookMethod, &webhookData, &clientID); err != nil {
return nil, errors.Wrap(err, "error scanning row")
}
@ -124,6 +126,7 @@ func (r *ActionRepo) findByFilterID(ctx context.Context, tx *Tx, filterID int) (
a.SavePath = savePath.String
a.Paused = paused.Bool
a.IgnoreRules = ignoreRules.Bool
a.ContentLayout = domain.ActionContentLayout(contentLayout.String)
a.LimitDownloadSpeed = limitDl.Int64
a.LimitUploadSpeed = limitUl.Int64
@ -324,6 +327,7 @@ func (r *ActionRepo) Store(ctx context.Context, action domain.Action) (*domain.A
tags := toNullString(action.Tags)
label := toNullString(action.Label)
savePath := toNullString(action.SavePath)
contentLayout := toNullString(string(action.ContentLayout))
webhookHost := toNullString(action.WebhookHost)
webhookData := toNullString(action.WebhookData)
webhookType := toNullString(action.WebhookType)
@ -351,6 +355,8 @@ func (r *ActionRepo) Store(ctx context.Context, action domain.Action) (*domain.A
"save_path",
"paused",
"ignore_rules",
"skip_hash_check",
"content_layout",
"limit_upload_speed",
"limit_download_speed",
"limit_ratio",
@ -379,6 +385,8 @@ func (r *ActionRepo) Store(ctx context.Context, action domain.Action) (*domain.A
savePath,
action.Paused,
action.IgnoreRules,
action.SkipHashCheck,
contentLayout,
limitUL,
limitDL,
limitRatio,
@ -418,6 +426,7 @@ func (r *ActionRepo) Update(ctx context.Context, action domain.Action) (*domain.
tags := toNullString(action.Tags)
label := toNullString(action.Label)
savePath := toNullString(action.SavePath)
contentLayout := toNullString(string(action.ContentLayout))
webhookHost := toNullString(action.WebhookHost)
webhookType := toNullString(action.WebhookType)
webhookMethod := toNullString(action.WebhookMethod)
@ -447,6 +456,8 @@ func (r *ActionRepo) Update(ctx context.Context, action domain.Action) (*domain.
Set("save_path", savePath).
Set("paused", action.Paused).
Set("ignore_rules", action.IgnoreRules).
Set("skip_hash_check", action.SkipHashCheck).
Set("content_layout", contentLayout).
Set("limit_upload_speed", limitUL).
Set("limit_download_speed", limitDL).
Set("limit_ratio", limitRatio).
@ -507,6 +518,7 @@ func (r *ActionRepo) StoreFilterActions(ctx context.Context, actions []*domain.A
tags := toNullString(action.Tags)
label := toNullString(action.Label)
savePath := toNullString(action.SavePath)
contentLayout := toNullString(string(action.ContentLayout))
webhookHost := toNullString(action.WebhookHost)
webhookType := toNullString(action.WebhookType)
webhookMethod := toNullString(action.WebhookMethod)
@ -533,6 +545,8 @@ func (r *ActionRepo) StoreFilterActions(ctx context.Context, actions []*domain.A
"save_path",
"paused",
"ignore_rules",
"skip_hash_check",
"content_layout",
"limit_upload_speed",
"limit_download_speed",
"limit_ratio",
@ -561,6 +575,8 @@ func (r *ActionRepo) StoreFilterActions(ctx context.Context, actions []*domain.A
savePath,
action.Paused,
action.IgnoreRules,
action.SkipHashCheck,
contentLayout,
limitUL,
limitDL,
limitRatio,

View file

@ -78,5 +78,7 @@ func (db *DB) migratePostgres() error {
return errors.Wrap(err, "failed to bump schema version")
}
db.log.Info().Msgf("Database schema upgraded to version: %v", len(postgresMigrations))
return tx.Commit()
}

View file

@ -158,6 +158,8 @@ CREATE TABLE action
save_path TEXT,
paused BOOLEAN,
ignore_rules BOOLEAN,
skip_hash_check BOOLEAN DEFAULT false,
content_layout TEXT,
limit_upload_speed INT,
limit_download_speed INT,
limit_ratio REAL,
@ -527,4 +529,11 @@ CREATE INDEX indexer_identifier_index
ALTER TABLE filter
ADD COLUMN external_webhook_expect_status INTEGER;
`,
`
ALTER TABLE action
ADD COLUMN skip_hash_check BOOLEAN DEFAULT FALSE;
ALTER TABLE action
ADD COLUMN content_layout TEXT;
`,
}

View file

@ -98,6 +98,8 @@ func (db *DB) migrateSQLite() error {
return errors.Wrap(err, "failed to bump schema version")
}
db.log.Info().Msgf("Database schema upgraded to version: %v", len(sqliteMigrations))
return tx.Commit()
}

View file

@ -158,6 +158,8 @@ CREATE TABLE action
save_path TEXT,
paused BOOLEAN,
ignore_rules BOOLEAN,
skip_hash_check BOOLEAN DEFAULT false,
content_layout TEXT,
limit_upload_speed INT,
limit_download_speed INT,
limit_ratio REAL,
@ -847,4 +849,11 @@ CREATE INDEX indexer_identifier_index
ALTER TABLE filter
ADD COLUMN external_webhook_expect_status INTEGER;
`,
`
ALTER TABLE action
ADD COLUMN skip_hash_check BOOLEAN DEFAULT FALSE;
ALTER TABLE action
ADD COLUMN content_layout TEXT;
`,
}

View file

@ -26,6 +26,8 @@ type Action struct {
SavePath string `json:"save_path,omitempty"`
Paused bool `json:"paused,omitempty"`
IgnoreRules bool `json:"ignore_rules,omitempty"`
SkipHashCheck bool `json:"skip_hash_check,omitempty"`
ContentLayout ActionContentLayout `json:"content_layout,omitempty"`
LimitUploadSpeed int64 `json:"limit_upload_speed,omitempty"`
LimitDownloadSpeed int64 `json:"limit_download_speed,omitempty"`
LimitRatio float64 `json:"limit_ratio,omitempty"`
@ -60,3 +62,11 @@ const (
ActionTypeLidarr ActionType = "LIDARR"
ActionTypeWhisparr ActionType = "WHISPARR"
)
type ActionContentLayout string
const (
ActionContentLayoutOriginal ActionContentLayout = "ORIGINAL"
ActionContentLayoutSubfolderNone ActionContentLayout = "SUBFOLDER_NONE"
ActionContentLayoutSubfolderCreate ActionContentLayout = "SUBFOLDER_CREATE"
)

View file

@ -83,14 +83,14 @@ func (s *service) FindByID(ctx context.Context, filterID int) (*domain.Filter, e
// find actions and attach
actions, err := s.actionRepo.FindByFilterID(ctx, filter.ID)
if err != nil {
s.log.Error().Msgf("could not find filter actions: %+v", &filter.ID)
s.log.Error().Msgf("could not find filter actions for filter id: %v", filter.ID)
}
filter.Actions = actions
// find indexers and attach
indexers, err := s.indexerSvc.FindByFilterID(ctx, filter.ID)
if err != nil {
s.log.Error().Err(err).Msgf("could not find indexers for filter: %+v", &filter.Name)
s.log.Error().Err(err).Msgf("could not find indexers for filter: %v", filter.Name)
return nil, err
}
filter.Indexers = indexers

View file

@ -1,5 +1,9 @@
package qbittorrent
import (
"strconv"
)
type Torrent struct {
AddedOn int `json:"added_on"`
AmountLeft int `json:"amount_left"`
@ -216,3 +220,68 @@ type TransferInfo struct {
UpInfoSpeed int64 `json:"up_info_speed"`
UpRateLimit int64 `json:"up_rate_limit"`
}
type ContentLayout string
const (
ContentLayoutOriginal ContentLayout = "ORIGINAL"
ContentLayoutSubfolderNone ContentLayout = "SUBFOLDER_NONE"
ContentLayoutSubfolderCreate ContentLayout = "SUBFOLDER_CREATE"
)
type TorrentAddOptions struct {
Paused *bool
SkipHashCheck *bool
ContentLayout *ContentLayout
SavePath *string
AutoTMM *bool
Category *string
Tags *string
LimitUploadSpeed *int64
LimitDownloadSpeed *int64
LimitRatio *float64
LimitSeedTime *int64
}
func (o *TorrentAddOptions) Prepare() map[string]string {
options := map[string]string{}
if o.Paused != nil {
options["paused"] = "true"
}
if o.SkipHashCheck != nil {
options["skip_checking"] = "true"
}
if o.ContentLayout != nil {
if *o.ContentLayout == ContentLayoutSubfolderCreate {
options["root_folder"] = "true"
} else if *o.ContentLayout == ContentLayoutSubfolderNone {
options["root_folder"] = "false"
}
// if ORIGINAL then leave empty
}
if o.SavePath != nil && *o.SavePath != "" {
options["savepath"] = *o.SavePath
options["autoTMM"] = "false"
}
if o.Category != nil && *o.Category != "" {
options["category"] = *o.Category
}
if o.Tags != nil && *o.Tags != "" {
options["tags"] = *o.Tags
}
if o.LimitUploadSpeed != nil && *o.LimitUploadSpeed > 0 {
options["upLimit"] = strconv.FormatInt(*o.LimitUploadSpeed*1000, 10)
}
if o.LimitDownloadSpeed != nil && *o.LimitDownloadSpeed > 0 {
options["dlLimit"] = strconv.FormatInt(*o.LimitDownloadSpeed*1000, 10)
}
if o.LimitRatio != nil && *o.LimitRatio > 0 {
options["ratioLimit"] = strconv.FormatFloat(*o.LimitRatio, 'f', 2, 64)
}
if o.LimitSeedTime != nil && *o.LimitSeedTime > 0 {
options["seedingTimeLimit"] = strconv.FormatInt(*o.LimitSeedTime, 10)
}
return options
}

View file

@ -0,0 +1,177 @@
package qbittorrent
import (
"testing"
"github.com/stretchr/testify/assert"
)
func PtrBool(b bool) *bool {
return &b
}
func PtrStr(s string) *string {
return &s
}
func PtrInt64(i int64) *int64 {
return &i
}
func PtrFloat64(f float64) *float64 {
return &f
}
func TestTorrentAddOptions_Prepare(t *testing.T) {
layoutNone := ContentLayoutSubfolderNone
layoutCreate := ContentLayoutSubfolderCreate
layoutOriginal := ContentLayoutOriginal
type fields struct {
Paused *bool
SkipHashCheck *bool
ContentLayout *ContentLayout
SavePath *string
AutoTMM *bool
Category *string
Tags *string
LimitUploadSpeed *int64
LimitDownloadSpeed *int64
LimitRatio *float64
LimitSeedTime *int64
}
tests := []struct {
name string
fields fields
want map[string]string
}{
{
name: "test_01",
fields: fields{
Paused: nil,
SkipHashCheck: PtrBool(true),
ContentLayout: nil,
SavePath: PtrStr("/home/test/torrents"),
AutoTMM: nil,
Category: PtrStr("test"),
Tags: PtrStr("limited,slow"),
LimitUploadSpeed: PtrInt64(100000),
LimitDownloadSpeed: PtrInt64(100000),
LimitRatio: PtrFloat64(2.0),
LimitSeedTime: PtrInt64(100),
},
want: map[string]string{
"skip_checking": "true",
"autoTMM": "false",
"ratioLimit": "2.00",
"savepath": "/home/test/torrents",
"seedingTimeLimit": "100",
"category": "test",
"tags": "limited,slow",
"upLimit": "100000000",
"dlLimit": "100000000",
},
},
{
name: "test_02",
fields: fields{
Paused: nil,
SkipHashCheck: PtrBool(true),
ContentLayout: &layoutCreate,
SavePath: PtrStr("/home/test/torrents"),
AutoTMM: nil,
Category: PtrStr("test"),
Tags: PtrStr("limited,slow"),
LimitUploadSpeed: PtrInt64(100000),
LimitDownloadSpeed: PtrInt64(100000),
LimitRatio: PtrFloat64(2.0),
LimitSeedTime: PtrInt64(100),
},
want: map[string]string{
"skip_checking": "true",
"root_folder": "true",
"autoTMM": "false",
"ratioLimit": "2.00",
"savepath": "/home/test/torrents",
"seedingTimeLimit": "100",
"category": "test",
"tags": "limited,slow",
"upLimit": "100000000",
"dlLimit": "100000000",
},
},
{
name: "test_03",
fields: fields{
Paused: nil,
SkipHashCheck: PtrBool(true),
ContentLayout: &layoutNone,
SavePath: PtrStr("/home/test/torrents"),
AutoTMM: nil,
Category: PtrStr("test"),
Tags: PtrStr("limited,slow"),
LimitUploadSpeed: PtrInt64(100000),
LimitDownloadSpeed: PtrInt64(100000),
LimitRatio: PtrFloat64(2.0),
LimitSeedTime: PtrInt64(100),
},
want: map[string]string{
"skip_checking": "true",
"root_folder": "false",
"autoTMM": "false",
"ratioLimit": "2.00",
"savepath": "/home/test/torrents",
"seedingTimeLimit": "100",
"category": "test",
"tags": "limited,slow",
"upLimit": "100000000",
"dlLimit": "100000000",
},
},
{
name: "test_04",
fields: fields{
Paused: nil,
SkipHashCheck: PtrBool(true),
ContentLayout: &layoutOriginal,
SavePath: PtrStr("/home/test/torrents"),
AutoTMM: nil,
Category: PtrStr("test"),
Tags: PtrStr("limited,slow"),
LimitUploadSpeed: PtrInt64(100000),
LimitDownloadSpeed: PtrInt64(100000),
LimitRatio: PtrFloat64(2.0),
LimitSeedTime: PtrInt64(100),
},
want: map[string]string{
"skip_checking": "true",
"autoTMM": "false",
"ratioLimit": "2.00",
"savepath": "/home/test/torrents",
"seedingTimeLimit": "100",
"category": "test",
"tags": "limited,slow",
"upLimit": "100000000",
"dlLimit": "100000000",
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
o := &TorrentAddOptions{
Paused: tt.fields.Paused,
SkipHashCheck: tt.fields.SkipHashCheck,
ContentLayout: tt.fields.ContentLayout,
SavePath: tt.fields.SavePath,
AutoTMM: tt.fields.AutoTMM,
Category: tt.fields.Category,
Tags: tt.fields.Tags,
LimitUploadSpeed: tt.fields.LimitUploadSpeed,
LimitDownloadSpeed: tt.fields.LimitDownloadSpeed,
LimitRatio: tt.fields.LimitRatio,
LimitSeedTime: tt.fields.LimitSeedTime,
}
got := o.Prepare()
assert.Equal(t, tt.want, got)
})
}
}

View file

@ -244,6 +244,12 @@ export const ActionTypeNameMap = {
"WHISPARR": "Whisparr"
};
export const ActionContentLayoutOptions: SelectGenericOption<ActionContentLayout>[] = [
{ label: "Original", description: "Original", value: "ORIGINAL" },
{ label: "Create subfolder", description: "Create subfolder", value: "SUBFOLDER_CREATE" },
{ label: "Don't create subfolder", description: "Don't create subfolder", value: "SUBFOLDER_NONE" },
];
export interface OptionBasic {
label: string;
value: string;
@ -308,6 +314,12 @@ export interface SelectOption {
value: NotificationEvent;
}
export interface SelectGenericOption<T> {
label: string;
description: string;
value: T;
}
export const EventOptions: SelectOption[] = [
{
label: "Push Rejected",

View file

@ -27,7 +27,8 @@ import {
RELEASE_TYPE_MUSIC_OPTIONS,
OTHER_OPTIONS,
ORIGIN_OPTIONS,
downloadsPerUnitOptions
downloadsPerUnitOptions,
ActionContentLayoutOptions
} from "../../domain/constants";
import { queryClient } from "../../App";
import { APIClient } from "../../api/APIClient";
@ -632,6 +633,8 @@ export function FilterActions({ filter, values }: FilterActionsProps) {
save_path: "",
paused: false,
ignore_rules: false,
skip_hash_check: false,
content_layout: "",
limit_upload_speed: 0,
limit_download_speed: 0,
limit_ratio: 0,
@ -790,11 +793,11 @@ const TypeForm = ({ action, idx, clients }: TypeFormProps) => {
<div className="mt-6 grid grid-cols-12 gap-6">
<NumberField
name={`actions.${idx}.limit_download_speed`}
label="Limit download speed (KB/s)"
label="Limit download speed (KiB/s)"
/>
<NumberField
name={`actions.${idx}.limit_upload_speed`}
label="Limit upload speed (KB/s)"
label="Limit upload speed (KiB/s)"
/>
</div>
@ -822,6 +825,21 @@ const TypeForm = ({ action, idx, clients }: TypeFormProps) => {
description="Download if max active reached"
/>
</div>
<div className="col-span-6">
<Select
name={`actions.${idx}.content_layout`}
label="Content Layout"
optionDefaultText="Select content layout"
options={ActionContentLayoutOptions}
/>
<div className="mt-2">
<SwitchGroup
name={`actions.${idx}.skip_hash_check`}
label="Skip hash check"
description="Add torrent and skip hash check"
/>
</div>
</div>
</CollapsableSection>
<CollapsableSection title="Advanced" subtitle="Advanced options">

View file

@ -77,6 +77,8 @@ interface Action {
save_path?: string;
paused?: boolean;
ignore_rules?: boolean;
skip_hash_check: boolean;
content_layout?: ActionContentLayout;
limit_upload_speed?: number;
limit_download_speed?: number;
limit_ratio?: number;
@ -94,4 +96,6 @@ interface Action {
client_id?: number;
}
type ActionContentLayout = "ORIGINAL" | "SUBFOLDER_CREATE" | "SUBFOLDER_NONE";
type ActionType = "TEST" | "EXEC" | "WATCH_FOLDER" | "WEBHOOK" | DownloadClientType;