From 9508cbb46c07435cf07859d5880f558d421082ea Mon Sep 17 00:00:00 2001 From: ze0s <43699394+zze0s@users.noreply.github.com> Date: Tue, 2 Aug 2022 18:06:45 +0200 Subject: [PATCH] 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 --- internal/action/qbittorrent.go | 40 ++++-- internal/database/action.go | 20 ++- internal/database/postgres.go | 2 + internal/database/postgres_migrate.go | 9 ++ internal/database/sqlite.go | 2 + internal/database/sqlite_migrate.go | 9 ++ internal/domain/action.go | 68 +++++----- internal/filter/service.go | 4 +- pkg/qbittorrent/domain.go | 69 ++++++++++ pkg/qbittorrent/domain_test.go | 177 ++++++++++++++++++++++++++ web/src/domain/constants.ts | 12 ++ web/src/screens/filters/details.tsx | 28 +++- web/src/types/Filter.d.ts | 4 + 13 files changed, 394 insertions(+), 50 deletions(-) create mode 100644 pkg/qbittorrent/domain_test.go diff --git a/internal/action/qbittorrent.go b/internal/action/qbittorrent.go index 60787e4..fa431f7 100644 --- a/internal/action/qbittorrent.go +++ b/internal/action/qbittorrent.go @@ -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) { diff --git a/internal/database/action.go b/internal/database/action.go index 54d3f9b..b1821a9 100644 --- a/internal/database/action.go +++ b/internal/database/action.go @@ -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, diff --git a/internal/database/postgres.go b/internal/database/postgres.go index 442aa58..837f59c 100644 --- a/internal/database/postgres.go +++ b/internal/database/postgres.go @@ -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() } diff --git a/internal/database/postgres_migrate.go b/internal/database/postgres_migrate.go index cf686bd..1244ec5 100644 --- a/internal/database/postgres_migrate.go +++ b/internal/database/postgres_migrate.go @@ -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; + `, } diff --git a/internal/database/sqlite.go b/internal/database/sqlite.go index d8dc72b..80f29bb 100644 --- a/internal/database/sqlite.go +++ b/internal/database/sqlite.go @@ -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() } diff --git a/internal/database/sqlite_migrate.go b/internal/database/sqlite_migrate.go index 782b935..c4444bc 100644 --- a/internal/database/sqlite_migrate.go +++ b/internal/database/sqlite_migrate.go @@ -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; + `, } diff --git a/internal/domain/action.go b/internal/domain/action.go index 3aaa84e..c4d7491 100644 --- a/internal/domain/action.go +++ b/internal/domain/action.go @@ -13,35 +13,37 @@ type ActionRepo interface { } type Action struct { - ID int `json:"id"` - Name string `json:"name"` - Type ActionType `json:"type"` - Enabled bool `json:"enabled"` - ExecCmd string `json:"exec_cmd,omitempty"` - ExecArgs string `json:"exec_args,omitempty"` - WatchFolder string `json:"watch_folder,omitempty"` - Category string `json:"category,omitempty"` - Tags string `json:"tags,omitempty"` - Label string `json:"label,omitempty"` - SavePath string `json:"save_path,omitempty"` - Paused bool `json:"paused,omitempty"` - IgnoreRules bool `json:"ignore_rules,omitempty"` - LimitUploadSpeed int64 `json:"limit_upload_speed,omitempty"` - LimitDownloadSpeed int64 `json:"limit_download_speed,omitempty"` - LimitRatio float64 `json:"limit_ratio,omitempty"` - LimitSeedTime int64 `json:"limit_seed_time,omitempty"` - ReAnnounceSkip bool `json:"reannounce_skip,omitempty"` - ReAnnounceDelete bool `json:"reannounce_delete,omitempty"` - ReAnnounceInterval int64 `json:"reannounce_interval,omitempty"` - ReAnnounceMaxAttempts int64 `json:"reannounce_max_attempts,omitempty"` - WebhookHost string `json:"webhook_host,omitempty"` - WebhookType string `json:"webhook_type,omitempty"` - WebhookMethod string `json:"webhook_method,omitempty"` - WebhookData string `json:"webhook_data,omitempty"` - WebhookHeaders []string `json:"webhook_headers,omitempty"` - FilterID int `json:"filter_id,omitempty"` - ClientID int32 `json:"client_id,omitempty"` - Client DownloadClient `json:"client,omitempty"` + ID int `json:"id"` + Name string `json:"name"` + Type ActionType `json:"type"` + Enabled bool `json:"enabled"` + ExecCmd string `json:"exec_cmd,omitempty"` + ExecArgs string `json:"exec_args,omitempty"` + WatchFolder string `json:"watch_folder,omitempty"` + Category string `json:"category,omitempty"` + Tags string `json:"tags,omitempty"` + Label string `json:"label,omitempty"` + 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"` + LimitSeedTime int64 `json:"limit_seed_time,omitempty"` + ReAnnounceSkip bool `json:"reannounce_skip,omitempty"` + ReAnnounceDelete bool `json:"reannounce_delete,omitempty"` + ReAnnounceInterval int64 `json:"reannounce_interval,omitempty"` + ReAnnounceMaxAttempts int64 `json:"reannounce_max_attempts,omitempty"` + WebhookHost string `json:"webhook_host,omitempty"` + WebhookType string `json:"webhook_type,omitempty"` + WebhookMethod string `json:"webhook_method,omitempty"` + WebhookData string `json:"webhook_data,omitempty"` + WebhookHeaders []string `json:"webhook_headers,omitempty"` + FilterID int `json:"filter_id,omitempty"` + ClientID int32 `json:"client_id,omitempty"` + Client DownloadClient `json:"client,omitempty"` } type ActionType string @@ -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" +) diff --git a/internal/filter/service.go b/internal/filter/service.go index 79e8a2a..3a260bd 100644 --- a/internal/filter/service.go +++ b/internal/filter/service.go @@ -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 diff --git a/pkg/qbittorrent/domain.go b/pkg/qbittorrent/domain.go index 6008410..8e94937 100644 --- a/pkg/qbittorrent/domain.go +++ b/pkg/qbittorrent/domain.go @@ -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 +} diff --git a/pkg/qbittorrent/domain_test.go b/pkg/qbittorrent/domain_test.go new file mode 100644 index 0000000..e6f42fd --- /dev/null +++ b/pkg/qbittorrent/domain_test.go @@ -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) + }) + } +} diff --git a/web/src/domain/constants.ts b/web/src/domain/constants.ts index cef59cc..2137ce1 100644 --- a/web/src/domain/constants.ts +++ b/web/src/domain/constants.ts @@ -244,6 +244,12 @@ export const ActionTypeNameMap = { "WHISPARR": "Whisparr" }; +export const ActionContentLayoutOptions: SelectGenericOption[] = [ + { 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 { + label: string; + description: string; + value: T; +} + export const EventOptions: SelectOption[] = [ { label: "Push Rejected", diff --git a/web/src/screens/filters/details.tsx b/web/src/screens/filters/details.tsx index 7284d03..bb003eb 100644 --- a/web/src/screens/filters/details.tsx +++ b/web/src/screens/filters/details.tsx @@ -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, @@ -658,7 +661,7 @@ export function FilterActions({ filter, values }: FilterActionsProps) {

Actions

- Add to download clients or run custom commands. + Add to download clients or run custom commands.

@@ -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" onClick={() => push(newAction)} > - Add new + Add new
@@ -790,11 +793,11 @@ const TypeForm = ({ action, idx, clients }: TypeFormProps) => {
@@ -822,6 +825,21 @@ const TypeForm = ({ action, idx, clients }: TypeFormProps) => { description="Download if max active reached" /> +
+