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 ( import (
"context" "context"
"strconv"
"strings" "strings"
"time" "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) { func (s *service) prepareQbitOptions(action domain.Action, m domain.Macro) (map[string]string, error) {
options := map[string]string{} opts := &qbittorrent.TorrentAddOptions{}
if action.Paused { 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 != "" { if action.SavePath != "" {
// parse and replace values in argument string before continuing // 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) return nil, errors.Wrap(err, "could not parse savepath macro: %v", action.SavePath)
} }
options["savepath"] = actionArgs opts.SavePath = &actionArgs
options["autoTMM"] = "false" opts.AutoTMM = BoolPointer(false)
} }
if action.Category != "" { if action.Category != "" {
// parse and replace values in argument string before continuing // 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) return nil, errors.Wrap(err, "could not parse category macro: %v", action.Category)
} }
options["category"] = categoryArgs opts.Category = &categoryArgs
} }
if action.Tags != "" { if action.Tags != "" {
// parse and replace values in argument string before continuing // 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) return nil, errors.Wrap(err, "could not parse tags macro: %v", action.Tags)
} }
options["tags"] = tagsArgs opts.Tags = &tagsArgs
} }
if action.LimitUploadSpeed > 0 { if action.LimitUploadSpeed > 0 {
options["upLimit"] = strconv.FormatInt(action.LimitUploadSpeed*1000, 10) opts.LimitUploadSpeed = &action.LimitUploadSpeed
} }
if action.LimitDownloadSpeed > 0 { if action.LimitDownloadSpeed > 0 {
options["dlLimit"] = strconv.FormatInt(action.LimitDownloadSpeed*1000, 10) opts.LimitDownloadSpeed = &action.LimitDownloadSpeed
} }
if action.LimitRatio > 0 { if action.LimitRatio > 0 {
options["ratioLimit"] = strconv.FormatFloat(action.LimitRatio, 'r', 2, 64) opts.LimitRatio = &action.LimitRatio
} }
if action.LimitSeedTime > 0 { 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) { 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", "save_path",
"paused", "paused",
"ignore_rules", "ignore_rules",
"skip_hash_check",
"content_layout",
"limit_download_speed", "limit_download_speed",
"limit_upload_speed", "limit_upload_speed",
"limit_ratio", "limit_ratio",
@ -103,7 +105,7 @@ func (r *ActionRepo) findByFilterID(ctx context.Context, tx *Tx, filterID int) (
for rows.Next() { for rows.Next() {
var a domain.Action 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 limitUl, limitDl, limitSeedTime sql.NullInt64
var limitRatio sql.NullFloat64 var limitRatio sql.NullFloat64
@ -111,7 +113,7 @@ func (r *ActionRepo) findByFilterID(ctx context.Context, tx *Tx, filterID int) (
// filterID // filterID
var paused, ignoreRules sql.NullBool 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") 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.SavePath = savePath.String
a.Paused = paused.Bool a.Paused = paused.Bool
a.IgnoreRules = ignoreRules.Bool a.IgnoreRules = ignoreRules.Bool
a.ContentLayout = domain.ActionContentLayout(contentLayout.String)
a.LimitDownloadSpeed = limitDl.Int64 a.LimitDownloadSpeed = limitDl.Int64
a.LimitUploadSpeed = limitUl.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) tags := toNullString(action.Tags)
label := toNullString(action.Label) label := toNullString(action.Label)
savePath := toNullString(action.SavePath) savePath := toNullString(action.SavePath)
contentLayout := toNullString(string(action.ContentLayout))
webhookHost := toNullString(action.WebhookHost) webhookHost := toNullString(action.WebhookHost)
webhookData := toNullString(action.WebhookData) webhookData := toNullString(action.WebhookData)
webhookType := toNullString(action.WebhookType) webhookType := toNullString(action.WebhookType)
@ -351,6 +355,8 @@ func (r *ActionRepo) Store(ctx context.Context, action domain.Action) (*domain.A
"save_path", "save_path",
"paused", "paused",
"ignore_rules", "ignore_rules",
"skip_hash_check",
"content_layout",
"limit_upload_speed", "limit_upload_speed",
"limit_download_speed", "limit_download_speed",
"limit_ratio", "limit_ratio",
@ -379,6 +385,8 @@ func (r *ActionRepo) Store(ctx context.Context, action domain.Action) (*domain.A
savePath, savePath,
action.Paused, action.Paused,
action.IgnoreRules, action.IgnoreRules,
action.SkipHashCheck,
contentLayout,
limitUL, limitUL,
limitDL, limitDL,
limitRatio, limitRatio,
@ -418,6 +426,7 @@ func (r *ActionRepo) Update(ctx context.Context, action domain.Action) (*domain.
tags := toNullString(action.Tags) tags := toNullString(action.Tags)
label := toNullString(action.Label) label := toNullString(action.Label)
savePath := toNullString(action.SavePath) savePath := toNullString(action.SavePath)
contentLayout := toNullString(string(action.ContentLayout))
webhookHost := toNullString(action.WebhookHost) webhookHost := toNullString(action.WebhookHost)
webhookType := toNullString(action.WebhookType) webhookType := toNullString(action.WebhookType)
webhookMethod := toNullString(action.WebhookMethod) webhookMethod := toNullString(action.WebhookMethod)
@ -447,6 +456,8 @@ func (r *ActionRepo) Update(ctx context.Context, action domain.Action) (*domain.
Set("save_path", savePath). Set("save_path", savePath).
Set("paused", action.Paused). Set("paused", action.Paused).
Set("ignore_rules", action.IgnoreRules). Set("ignore_rules", action.IgnoreRules).
Set("skip_hash_check", action.SkipHashCheck).
Set("content_layout", contentLayout).
Set("limit_upload_speed", limitUL). Set("limit_upload_speed", limitUL).
Set("limit_download_speed", limitDL). Set("limit_download_speed", limitDL).
Set("limit_ratio", limitRatio). Set("limit_ratio", limitRatio).
@ -507,6 +518,7 @@ func (r *ActionRepo) StoreFilterActions(ctx context.Context, actions []*domain.A
tags := toNullString(action.Tags) tags := toNullString(action.Tags)
label := toNullString(action.Label) label := toNullString(action.Label)
savePath := toNullString(action.SavePath) savePath := toNullString(action.SavePath)
contentLayout := toNullString(string(action.ContentLayout))
webhookHost := toNullString(action.WebhookHost) webhookHost := toNullString(action.WebhookHost)
webhookType := toNullString(action.WebhookType) webhookType := toNullString(action.WebhookType)
webhookMethod := toNullString(action.WebhookMethod) webhookMethod := toNullString(action.WebhookMethod)
@ -533,6 +545,8 @@ func (r *ActionRepo) StoreFilterActions(ctx context.Context, actions []*domain.A
"save_path", "save_path",
"paused", "paused",
"ignore_rules", "ignore_rules",
"skip_hash_check",
"content_layout",
"limit_upload_speed", "limit_upload_speed",
"limit_download_speed", "limit_download_speed",
"limit_ratio", "limit_ratio",
@ -561,6 +575,8 @@ func (r *ActionRepo) StoreFilterActions(ctx context.Context, actions []*domain.A
savePath, savePath,
action.Paused, action.Paused,
action.IgnoreRules, action.IgnoreRules,
action.SkipHashCheck,
contentLayout,
limitUL, limitUL,
limitDL, limitDL,
limitRatio, limitRatio,

View file

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

View file

@ -158,6 +158,8 @@ CREATE TABLE action
save_path TEXT, save_path TEXT,
paused BOOLEAN, paused BOOLEAN,
ignore_rules BOOLEAN, ignore_rules BOOLEAN,
skip_hash_check BOOLEAN DEFAULT false,
content_layout TEXT,
limit_upload_speed INT, limit_upload_speed INT,
limit_download_speed INT, limit_download_speed INT,
limit_ratio REAL, limit_ratio REAL,
@ -527,4 +529,11 @@ CREATE INDEX indexer_identifier_index
ALTER TABLE filter ALTER TABLE filter
ADD COLUMN external_webhook_expect_status INTEGER; 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") return errors.Wrap(err, "failed to bump schema version")
} }
db.log.Info().Msgf("Database schema upgraded to version: %v", len(sqliteMigrations))
return tx.Commit() return tx.Commit()
} }

View file

@ -158,6 +158,8 @@ CREATE TABLE action
save_path TEXT, save_path TEXT,
paused BOOLEAN, paused BOOLEAN,
ignore_rules BOOLEAN, ignore_rules BOOLEAN,
skip_hash_check BOOLEAN DEFAULT false,
content_layout TEXT,
limit_upload_speed INT, limit_upload_speed INT,
limit_download_speed INT, limit_download_speed INT,
limit_ratio REAL, limit_ratio REAL,
@ -847,4 +849,11 @@ CREATE INDEX indexer_identifier_index
ALTER TABLE filter ALTER TABLE filter
ADD COLUMN external_webhook_expect_status INTEGER; 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

@ -13,35 +13,37 @@ type ActionRepo interface {
} }
type Action struct { type Action struct {
ID int `json:"id"` ID int `json:"id"`
Name string `json:"name"` Name string `json:"name"`
Type ActionType `json:"type"` Type ActionType `json:"type"`
Enabled bool `json:"enabled"` Enabled bool `json:"enabled"`
ExecCmd string `json:"exec_cmd,omitempty"` ExecCmd string `json:"exec_cmd,omitempty"`
ExecArgs string `json:"exec_args,omitempty"` ExecArgs string `json:"exec_args,omitempty"`
WatchFolder string `json:"watch_folder,omitempty"` WatchFolder string `json:"watch_folder,omitempty"`
Category string `json:"category,omitempty"` Category string `json:"category,omitempty"`
Tags string `json:"tags,omitempty"` Tags string `json:"tags,omitempty"`
Label string `json:"label,omitempty"` Label string `json:"label,omitempty"`
SavePath string `json:"save_path,omitempty"` SavePath string `json:"save_path,omitempty"`
Paused bool `json:"paused,omitempty"` Paused bool `json:"paused,omitempty"`
IgnoreRules bool `json:"ignore_rules,omitempty"` IgnoreRules bool `json:"ignore_rules,omitempty"`
LimitUploadSpeed int64 `json:"limit_upload_speed,omitempty"` SkipHashCheck bool `json:"skip_hash_check,omitempty"`
LimitDownloadSpeed int64 `json:"limit_download_speed,omitempty"` ContentLayout ActionContentLayout `json:"content_layout,omitempty"`
LimitRatio float64 `json:"limit_ratio,omitempty"` LimitUploadSpeed int64 `json:"limit_upload_speed,omitempty"`
LimitSeedTime int64 `json:"limit_seed_time,omitempty"` LimitDownloadSpeed int64 `json:"limit_download_speed,omitempty"`
ReAnnounceSkip bool `json:"reannounce_skip,omitempty"` LimitRatio float64 `json:"limit_ratio,omitempty"`
ReAnnounceDelete bool `json:"reannounce_delete,omitempty"` LimitSeedTime int64 `json:"limit_seed_time,omitempty"`
ReAnnounceInterval int64 `json:"reannounce_interval,omitempty"` ReAnnounceSkip bool `json:"reannounce_skip,omitempty"`
ReAnnounceMaxAttempts int64 `json:"reannounce_max_attempts,omitempty"` ReAnnounceDelete bool `json:"reannounce_delete,omitempty"`
WebhookHost string `json:"webhook_host,omitempty"` ReAnnounceInterval int64 `json:"reannounce_interval,omitempty"`
WebhookType string `json:"webhook_type,omitempty"` ReAnnounceMaxAttempts int64 `json:"reannounce_max_attempts,omitempty"`
WebhookMethod string `json:"webhook_method,omitempty"` WebhookHost string `json:"webhook_host,omitempty"`
WebhookData string `json:"webhook_data,omitempty"` WebhookType string `json:"webhook_type,omitempty"`
WebhookHeaders []string `json:"webhook_headers,omitempty"` WebhookMethod string `json:"webhook_method,omitempty"`
FilterID int `json:"filter_id,omitempty"` WebhookData string `json:"webhook_data,omitempty"`
ClientID int32 `json:"client_id,omitempty"` WebhookHeaders []string `json:"webhook_headers,omitempty"`
Client DownloadClient `json:"client,omitempty"` FilterID int `json:"filter_id,omitempty"`
ClientID int32 `json:"client_id,omitempty"`
Client DownloadClient `json:"client,omitempty"`
} }
type ActionType string type ActionType string
@ -60,3 +62,11 @@ const (
ActionTypeLidarr ActionType = "LIDARR" ActionTypeLidarr ActionType = "LIDARR"
ActionTypeWhisparr ActionType = "WHISPARR" 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 // find actions and attach
actions, err := s.actionRepo.FindByFilterID(ctx, filter.ID) actions, err := s.actionRepo.FindByFilterID(ctx, filter.ID)
if err != nil { 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 filter.Actions = actions
// find indexers and attach // find indexers and attach
indexers, err := s.indexerSvc.FindByFilterID(ctx, filter.ID) indexers, err := s.indexerSvc.FindByFilterID(ctx, filter.ID)
if err != nil { 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 return nil, err
} }
filter.Indexers = indexers filter.Indexers = indexers

View file

@ -1,5 +1,9 @@
package qbittorrent package qbittorrent
import (
"strconv"
)
type Torrent struct { type Torrent struct {
AddedOn int `json:"added_on"` AddedOn int `json:"added_on"`
AmountLeft int `json:"amount_left"` AmountLeft int `json:"amount_left"`
@ -216,3 +220,68 @@ type TransferInfo struct {
UpInfoSpeed int64 `json:"up_info_speed"` UpInfoSpeed int64 `json:"up_info_speed"`
UpRateLimit int64 `json:"up_rate_limit"` 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" "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 { export interface OptionBasic {
label: string; label: string;
value: string; value: string;
@ -308,6 +314,12 @@ export interface SelectOption {
value: NotificationEvent; value: NotificationEvent;
} }
export interface SelectGenericOption<T> {
label: string;
description: string;
value: T;
}
export const EventOptions: SelectOption[] = [ export const EventOptions: SelectOption[] = [
{ {
label: "Push Rejected", label: "Push Rejected",

View file

@ -27,7 +27,8 @@ import {
RELEASE_TYPE_MUSIC_OPTIONS, RELEASE_TYPE_MUSIC_OPTIONS,
OTHER_OPTIONS, OTHER_OPTIONS,
ORIGIN_OPTIONS, ORIGIN_OPTIONS,
downloadsPerUnitOptions downloadsPerUnitOptions,
ActionContentLayoutOptions
} from "../../domain/constants"; } from "../../domain/constants";
import { queryClient } from "../../App"; import { queryClient } from "../../App";
import { APIClient } from "../../api/APIClient"; import { APIClient } from "../../api/APIClient";
@ -632,6 +633,8 @@ export function FilterActions({ filter, values }: FilterActionsProps) {
save_path: "", save_path: "",
paused: false, paused: false,
ignore_rules: false, ignore_rules: false,
skip_hash_check: false,
content_layout: "",
limit_upload_speed: 0, limit_upload_speed: 0,
limit_download_speed: 0, limit_download_speed: 0,
limit_ratio: 0, limit_ratio: 0,
@ -658,7 +661,7 @@ export function FilterActions({ filter, values }: FilterActionsProps) {
<div className="ml-4 mt-4"> <div className="ml-4 mt-4">
<h3 className="text-lg leading-6 font-medium text-gray-900 dark:text-gray-200">Actions</h3> <h3 className="text-lg leading-6 font-medium text-gray-900 dark:text-gray-200">Actions</h3>
<p className="mt-1 text-sm text-gray-500 dark:text-gray-400"> <p className="mt-1 text-sm text-gray-500 dark:text-gray-400">
Add to download clients or run custom commands. Add to download clients or run custom commands.
</p> </p>
</div> </div>
<div className="ml-4 mt-4 flex-shrink-0"> <div className="ml-4 mt-4 flex-shrink-0">
@ -667,7 +670,7 @@ export function FilterActions({ filter, values }: FilterActionsProps) {
className="relative inline-flex items-center px-4 py-2 border border-transparent shadow-sm text-sm font-medium rounded-md text-white bg-indigo-600 dark:bg-blue-600 hover:bg-indigo-700 dark:hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 dark:focus:ring-blue-500" className="relative inline-flex items-center px-4 py-2 border border-transparent shadow-sm text-sm font-medium rounded-md text-white bg-indigo-600 dark:bg-blue-600 hover:bg-indigo-700 dark:hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 dark:focus:ring-blue-500"
onClick={() => push(newAction)} onClick={() => push(newAction)}
> >
Add new Add new
</button> </button>
</div> </div>
</div> </div>
@ -790,11 +793,11 @@ const TypeForm = ({ action, idx, clients }: TypeFormProps) => {
<div className="mt-6 grid grid-cols-12 gap-6"> <div className="mt-6 grid grid-cols-12 gap-6">
<NumberField <NumberField
name={`actions.${idx}.limit_download_speed`} name={`actions.${idx}.limit_download_speed`}
label="Limit download speed (KB/s)" label="Limit download speed (KiB/s)"
/> />
<NumberField <NumberField
name={`actions.${idx}.limit_upload_speed`} name={`actions.${idx}.limit_upload_speed`}
label="Limit upload speed (KB/s)" label="Limit upload speed (KiB/s)"
/> />
</div> </div>
@ -822,6 +825,21 @@ const TypeForm = ({ action, idx, clients }: TypeFormProps) => {
description="Download if max active reached" description="Download if max active reached"
/> />
</div> </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>
<CollapsableSection title="Advanced" subtitle="Advanced options"> <CollapsableSection title="Advanced" subtitle="Advanced options">

View file

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