From 5a4585167726d49fd6f8ffc6d6c2fd474369c343 Mon Sep 17 00:00:00 2001 From: Ludvig Lundgren Date: Fri, 4 Mar 2022 20:29:53 +0100 Subject: [PATCH] feat(actions): add more macro variables (#157) * feat(actions): add more macro variables * feat: add more macros * feat: add more tests --- internal/action/deluge.go | 72 ++++++++++++++----- internal/action/exec.go | 10 +-- internal/action/macros.go | 41 +++++++++++ internal/action/macros_test.go | 122 +++++++++++++++++++++++++++------ internal/action/qbittorrent.go | 47 ++++++++++--- internal/action/run.go | 28 +++++--- 6 files changed, 252 insertions(+), 68 deletions(-) diff --git a/internal/action/deluge.go b/internal/action/deluge.go index 4358fea..41ee0f0 100644 --- a/internal/action/deluge.go +++ b/internal/action/deluge.go @@ -13,7 +13,7 @@ import ( "github.com/rs/zerolog/log" ) -func (s *service) deluge(action domain.Action, torrentFile string) error { +func (s *service) deluge(action domain.Action, release domain.Release) error { log.Debug().Msgf("action Deluge: %v", action.Name) var err error @@ -40,12 +40,12 @@ func (s *service) deluge(action domain.Action, torrentFile string) error { switch client.Type { case "DELUGE_V1": - if err = delugeV1(client, settings, action, torrentFile); err != nil { + if err = delugeV1(client, settings, action, release); err != nil { return err } case "DELUGE_V2": - if err = delugeV2(client, settings, action, torrentFile); err != nil { + if err = delugeV2(client, settings, action, release); err != nil { return err } } @@ -135,7 +135,7 @@ func (s *service) delugeCheckRulesCanDownload(action domain.Action) (bool, error return true, nil } -func delugeV1(client *domain.DownloadClient, settings delugeClient.Settings, action domain.Action, torrentFile string) error { +func delugeV1(client *domain.DownloadClient, settings delugeClient.Settings, action domain.Action, release domain.Release) error { deluge := delugeClient.NewV1(settings) @@ -148,27 +148,37 @@ func delugeV1(client *domain.DownloadClient, settings delugeClient.Settings, act defer deluge.Close() - t, err := ioutil.ReadFile(torrentFile) + t, err := ioutil.ReadFile(release.TorrentTmpFile) if err != nil { - log.Error().Stack().Err(err).Msgf("could not read torrent file: %v", torrentFile) + log.Error().Stack().Err(err).Msgf("could not read torrent file: %v", release.TorrentTmpFile) return err } // encode file to base64 before sending to deluge encodedFile := base64.StdEncoding.EncodeToString(t) if encodedFile == "" { - log.Error().Stack().Err(err).Msgf("could not encode torrent file: %v", torrentFile) + log.Error().Stack().Err(err).Msgf("could not encode torrent file: %v", release.TorrentTmpFile) return err } // set options options := delugeClient.Options{} + // macros handle args and replace vars + m := NewMacro(release) + if action.Paused { options.AddPaused = &action.Paused } if action.SavePath != "" { - options.DownloadLocation = &action.SavePath + // parse and replace values in argument string before continuing + savePathArgs, err := m.Parse(action.SavePath) + if err != nil { + log.Error().Stack().Err(err).Msgf("could not parse macro: %v", action.SavePath) + return err + } + + options.DownloadLocation = &savePathArgs } if action.LimitDownloadSpeed > 0 { maxDL := int(action.LimitDownloadSpeed) @@ -181,9 +191,9 @@ func delugeV1(client *domain.DownloadClient, settings delugeClient.Settings, act log.Trace().Msgf("action Deluge options: %+v", options) - torrentHash, err := deluge.AddTorrentFile(torrentFile, encodedFile, &options) + torrentHash, err := deluge.AddTorrentFile(release.TorrentTmpFile, encodedFile, &options) if err != nil { - log.Error().Stack().Err(err).Msgf("could not add torrent %v to client: %v", torrentFile, client.Name) + log.Error().Stack().Err(err).Msgf("could not add torrent %v to client: %v", release.TorrentTmpFile, client.Name) return err } @@ -194,9 +204,16 @@ func delugeV1(client *domain.DownloadClient, settings delugeClient.Settings, act return err } + // parse and replace values in argument string before continuing + labelArgs, err := m.Parse(action.Label) + if err != nil { + log.Error().Stack().Err(err).Msgf("could not parse macro: %v", action.Label) + return err + } + if p != nil { // TODO first check if label exists, if not, add it, otherwise set - err = p.SetTorrentLabel(torrentHash, action.Label) + err = p.SetTorrentLabel(torrentHash, labelArgs) if err != nil { log.Error().Stack().Err(err).Msgf("could not set label: %v on client: %v", action.Label, client.Name) return err @@ -209,7 +226,7 @@ func delugeV1(client *domain.DownloadClient, settings delugeClient.Settings, act return nil } -func delugeV2(client *domain.DownloadClient, settings delugeClient.Settings, action domain.Action, torrentFile string) error { +func delugeV2(client *domain.DownloadClient, settings delugeClient.Settings, action domain.Action, release domain.Release) error { deluge := delugeClient.NewV2(settings) @@ -222,27 +239,37 @@ func delugeV2(client *domain.DownloadClient, settings delugeClient.Settings, act defer deluge.Close() - t, err := ioutil.ReadFile(torrentFile) + t, err := ioutil.ReadFile(release.TorrentTmpFile) if err != nil { - log.Error().Stack().Err(err).Msgf("could not read torrent file: %v", torrentFile) + log.Error().Stack().Err(err).Msgf("could not read torrent file: %v", release.TorrentTmpFile) return err } // encode file to base64 before sending to deluge encodedFile := base64.StdEncoding.EncodeToString(t) if encodedFile == "" { - log.Error().Stack().Err(err).Msgf("could not encode torrent file: %v", torrentFile) + log.Error().Stack().Err(err).Msgf("could not encode torrent file: %v", release.TorrentTmpFile) return err } // set options options := delugeClient.Options{} + // macros handle args and replace vars + m := NewMacro(release) + if action.Paused { options.AddPaused = &action.Paused } if action.SavePath != "" { - options.DownloadLocation = &action.SavePath + // parse and replace values in argument string before continuing + savePathArgs, err := m.Parse(action.SavePath) + if err != nil { + log.Error().Stack().Err(err).Msgf("could not parse macro: %v", action.SavePath) + return err + } + + options.DownloadLocation = &savePathArgs } if action.LimitDownloadSpeed > 0 { maxDL := int(action.LimitDownloadSpeed) @@ -255,9 +282,9 @@ func delugeV2(client *domain.DownloadClient, settings delugeClient.Settings, act log.Trace().Msgf("action Deluge options: %+v", options) - torrentHash, err := deluge.AddTorrentFile(torrentFile, encodedFile, &options) + torrentHash, err := deluge.AddTorrentFile(release.TorrentTmpFile, encodedFile, &options) if err != nil { - log.Error().Stack().Err(err).Msgf("could not add torrent %v to client: %v", torrentFile, client.Name) + log.Error().Stack().Err(err).Msgf("could not add torrent %v to client: %v", release.TorrentTmpFile, client.Name) return err } @@ -268,9 +295,16 @@ func delugeV2(client *domain.DownloadClient, settings delugeClient.Settings, act return err } + // parse and replace values in argument string before continuing + labelArgs, err := m.Parse(action.Label) + if err != nil { + log.Error().Stack().Err(err).Msgf("could not parse macro: %v", action.Label) + return err + } + if p != nil { // TODO first check if label exists, if not, add it, otherwise set - err = p.SetTorrentLabel(torrentHash, action.Label) + err = p.SetTorrentLabel(torrentHash, labelArgs) if err != nil { log.Error().Stack().Err(err).Msgf("could not set label: %v on client: %v", action.Label, client.Name) return err diff --git a/internal/action/exec.go b/internal/action/exec.go index 74f9c3f..fd44e8e 100644 --- a/internal/action/exec.go +++ b/internal/action/exec.go @@ -10,7 +10,7 @@ import ( "github.com/rs/zerolog/log" ) -func (s *service) execCmd(release domain.Release, action domain.Action, torrentFile string) { +func (s *service) execCmd(release domain.Release, action domain.Action) { log.Debug().Msgf("action exec: %v release: %v", action.Name, release.TorrentName) // check if program exists @@ -21,11 +21,7 @@ func (s *service) execCmd(release domain.Release, action domain.Action, torrentF } // handle args and replace vars - m := Macro{ - TorrentName: release.TorrentName, - TorrentPathName: torrentFile, - TorrentUrl: release.TorrentURL, - } + m := NewMacro(release) // parse and replace values in argument string before continuing parsedArgs, err := m.Parse(action.ExecArgs) @@ -46,7 +42,7 @@ func (s *service) execCmd(release domain.Release, action domain.Action, torrentF output, err := command.CombinedOutput() if err != nil { // everything other than exit 0 is considered an error - log.Error().Stack().Err(err).Msgf("command: %v args: %v failed, torrent: %v", cmd, parsedArgs, torrentFile) + log.Error().Stack().Err(err).Msgf("command: %v args: %v failed, torrent: %v", cmd, parsedArgs, release.TorrentTmpFile) } log.Trace().Msgf("executed command: '%v'", string(output)) diff --git a/internal/action/macros.go b/internal/action/macros.go index 62a46a8..bf00e18 100644 --- a/internal/action/macros.go +++ b/internal/action/macros.go @@ -3,12 +3,53 @@ package action import ( "bytes" "text/template" + "time" + + "github.com/autobrr/autobrr/internal/domain" ) type Macro struct { TorrentName string TorrentPathName string + TorrentHash string TorrentUrl string + Indexer string + Resolution string + Source string + HDR string + Season int + Episode int + Year int + Month int + Day int + Hour int + Minute int + Second int +} + +func NewMacro(release domain.Release) Macro { + currentTime := time.Now() + + ma := Macro{ + TorrentName: release.TorrentName, + TorrentUrl: release.TorrentURL, + TorrentPathName: release.TorrentTmpFile, + TorrentHash: release.TorrentHash, + Indexer: release.Indexer, + Resolution: release.Resolution, + Source: release.Source, + HDR: release.HDR, + Season: release.Season, + Episode: release.Episode, + Year: currentTime.Year(), + Month: int(currentTime.Month()), + Day: currentTime.Day(), + Hour: currentTime.Hour(), + Minute: currentTime.Minute(), + Second: currentTime.Second(), + } + + return ma } // Parse takes a string and replaces valid vars diff --git a/internal/action/macros_test.go b/internal/action/macros_test.go index b9b4050..ebece92 100644 --- a/internal/action/macros_test.go +++ b/internal/action/macros_test.go @@ -1,14 +1,21 @@ package action import ( + "fmt" + "github.com/autobrr/autobrr/internal/domain" + "github.com/stretchr/testify/assert" "testing" + "time" ) func TestMacros_Parse(t *testing.T) { + currentTime := time.Now() + type fields struct { TorrentName string TorrentPathName string TorrentUrl string + Indexer string } type args struct { text string @@ -16,43 +23,60 @@ func TestMacros_Parse(t *testing.T) { tests := []struct { name string fields fields + release domain.Release args args want string wantErr bool }{ { - name: "test_ok", - fields: fields{TorrentPathName: "/tmp/a-temporary-file.torrent"}, + name: "test_ok", + release: domain.Release{ + TorrentName: "This movie 2021", + TorrentTmpFile: "/tmp/a-temporary-file.torrent", + Indexer: "mock1", + }, args: args{text: "Print mee {{.TorrentPathName}}"}, want: "Print mee /tmp/a-temporary-file.torrent", wantErr: false, }, { - name: "test_bad", - fields: fields{TorrentPathName: "/tmp/a-temporary-file.torrent"}, + name: "test_bad", + release: domain.Release{ + TorrentName: "This movie 2021", + TorrentTmpFile: "/tmp/a-temporary-file.torrent", + Indexer: "mock1", + }, args: args{text: "Print mee {{TorrentPathName}}"}, want: "", wantErr: true, }, { - name: "test_program_arg", - fields: fields{TorrentPathName: "/tmp/a-temporary-file.torrent"}, + name: "test_program_arg", + release: domain.Release{ + TorrentName: "This movie 2021", + TorrentTmpFile: "/tmp/a-temporary-file.torrent", + Indexer: "mock1", + }, args: args{text: "add {{.TorrentPathName}} --category test"}, want: "add /tmp/a-temporary-file.torrent --category test", wantErr: false, }, { - name: "test_program_arg_bad", - fields: fields{TorrentPathName: "/tmp/a-temporary-file.torrent"}, + name: "test_program_arg_bad", + release: domain.Release{ + TorrentTmpFile: "/tmp/a-temporary-file.torrent", + Indexer: "mock1", + }, args: args{text: "add {{.TorrenttPathName}} --category test"}, want: "", wantErr: true, }, { name: "test_program_arg", - fields: fields{ - TorrentName: "This movie 2021", - TorrentPathName: "/tmp/a-temporary-file.torrent", + release: domain.Release{ + TorrentName: "This movie 2021", + TorrentTmpFile: "/tmp/a-temporary-file.torrent", + Indexer: "mock1", }, args: args{text: "add {{.TorrentPathName}} --category test --other {{.TorrentName}}"}, want: "add /tmp/a-temporary-file.torrent --category test --other This movie 2021", @@ -60,31 +84,87 @@ func TestMacros_Parse(t *testing.T) { }, { name: "test_args_long", - fields: fields{ + release: domain.Release{ TorrentName: "This movie 2021", - TorrentUrl: "https://some.site/download/fakeid", + TorrentURL: "https://some.site/download/fakeid", + Indexer: "mock1", }, args: args{text: "{{.TorrentName}} {{.TorrentUrl}} SOME_LONG_TOKEN"}, want: "This movie 2021 https://some.site/download/fakeid SOME_LONG_TOKEN", wantErr: false, }, + { + name: "test_args_long_1", + release: domain.Release{ + TorrentName: "This movie 2021", + TorrentURL: "https://some.site/download/fakeid", + Indexer: "mock1", + }, + args: args{text: "{{.Indexer}} {{.TorrentName}} {{.TorrentUrl}} SOME_LONG_TOKEN"}, + want: "mock1 This movie 2021 https://some.site/download/fakeid SOME_LONG_TOKEN", + wantErr: false, + }, + { + name: "test_args_category", + release: domain.Release{ + TorrentName: "This movie 2021", + TorrentURL: "https://some.site/download/fakeid", + Indexer: "mock1", + }, + args: args{text: "{{.Indexer}}-race"}, + want: "mock1-race", + wantErr: false, + }, + { + name: "test_args_category_year", + release: domain.Release{ + TorrentName: "This movie 2021", + TorrentURL: "https://some.site/download/fakeid", + Indexer: "mock1", + }, + args: args{text: "{{.Indexer}}-{{.Year}}-race"}, + want: fmt.Sprintf("mock1-%v-race", currentTime.Year()), + wantErr: false, + }, + { + name: "test_args_category_year", + release: domain.Release{ + TorrentName: "This movie 2021", + TorrentURL: "https://some.site/download/fakeid", + Indexer: "mock1", + Resolution: "2160p", + HDR: "DV", + }, + args: args{text: "movies-{{.Resolution}}{{ if .HDR }}-{{.HDR}}{{ end }}"}, + want: "movies-2160p-DV", + wantErr: false, + }, + { + name: "test_args_category_and_if", + release: domain.Release{ + TorrentName: "This movie 2021", + TorrentURL: "https://some.site/download/fakeid", + Indexer: "mock1", + Resolution: "2160p", + HDR: "HDR", + }, + args: args{text: "movies-{{.Resolution}}{{ if .HDR }}-{{.HDR}}{{ end }}"}, + want: "movies-2160p-HDR", + wantErr: false, + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - m := Macro{ - TorrentPathName: tt.fields.TorrentPathName, - TorrentUrl: tt.fields.TorrentUrl, - TorrentName: tt.fields.TorrentName, - } + m := NewMacro(tt.release) got, err := m.Parse(tt.args.text) + assert.Equal(t, currentTime.Year(), m.Year) + if (err != nil) != tt.wantErr { t.Errorf("Parse() error = %v, wantErr %v", err, tt.wantErr) return } - if got != tt.want { - t.Errorf("Parse() got = %v, want %v", got, tt.want) - } + assert.Equal(t, tt.want, got) }) } } diff --git a/internal/action/qbittorrent.go b/internal/action/qbittorrent.go index 0be9b02..b3d5e91 100644 --- a/internal/action/qbittorrent.go +++ b/internal/action/qbittorrent.go @@ -5,31 +5,56 @@ import ( "strconv" "time" + "github.com/rs/zerolog/log" + "github.com/autobrr/autobrr/internal/domain" "github.com/autobrr/autobrr/pkg/qbittorrent" - "github.com/rs/zerolog/log" ) const ReannounceMaxAttempts = 50 const ReannounceInterval = 7000 -func (s *service) qbittorrent(qbt *qbittorrent.Client, action domain.Action, torrentFile string, hash string) error { +func (s *service) qbittorrent(qbt *qbittorrent.Client, action domain.Action, release domain.Release) error { log.Debug().Msgf("action qBittorrent: %v", action.Name) + // macros handle args and replace vars + m := NewMacro(release) + options := map[string]string{} if action.Paused { options["paused"] = "true" } if action.SavePath != "" { - options["savepath"] = action.SavePath + // parse and replace values in argument string before continuing + actionArgs, err := m.Parse(action.SavePath) + if err != nil { + log.Error().Stack().Err(err).Msgf("could not parse macro: %v", action.SavePath) + return err + } + + options["savepath"] = actionArgs options["autoTMM"] = "false" } if action.Category != "" { - options["category"] = action.Category + // parse and replace values in argument string before continuing + categoryArgs, err := m.Parse(action.Category) + if err != nil { + log.Error().Stack().Err(err).Msgf("could not parse macro: %v", action.Category) + return err + } + + options["category"] = categoryArgs } if action.Tags != "" { - options["tags"] = action.Tags + // parse and replace values in argument string before continuing + tagsArgs, err := m.Parse(action.Tags) + if err != nil { + log.Error().Stack().Err(err).Msgf("could not parse macro: %v", action.Tags) + return err + } + + options["tags"] = tagsArgs } if action.LimitUploadSpeed > 0 { options["upLimit"] = strconv.FormatInt(action.LimitUploadSpeed, 10) @@ -40,21 +65,21 @@ func (s *service) qbittorrent(qbt *qbittorrent.Client, action domain.Action, tor log.Trace().Msgf("action qBittorrent options: %+v", options) - err := qbt.AddTorrentFromFile(torrentFile, options) + err := qbt.AddTorrentFromFile(release.TorrentTmpFile, options) if err != nil { - log.Error().Stack().Err(err).Msgf("could not add torrent %v to client: %v", torrentFile, qbt.Name) + log.Error().Stack().Err(err).Msgf("could not add torrent %v to client: %v", release.TorrentTmpFile, qbt.Name) return err } - if !action.Paused && hash != "" { - err = checkTrackerStatus(qbt, hash) + if !action.Paused && release.TorrentHash != "" { + err = checkTrackerStatus(qbt, release.TorrentHash) if err != nil { - log.Error().Stack().Err(err).Msgf("could not reannounce torrent: %v", hash) + log.Error().Stack().Err(err).Msgf("could not reannounce torrent: %v", release.TorrentHash) return err } } - log.Info().Msgf("torrent with hash %v successfully added to client: '%v'", hash, qbt.Name) + log.Info().Msgf("torrent with hash %v successfully added to client: '%v'", release.TorrentHash, qbt.Name) return nil } diff --git a/internal/action/run.go b/internal/action/run.go index 8d9295f..0f2f5fe 100644 --- a/internal/action/run.go +++ b/internal/action/run.go @@ -61,7 +61,7 @@ func (s *service) runAction(action domain.Action, release domain.Release) error } } - s.execCmd(release, action, release.TorrentTmpFile) + s.execCmd(release, action) case domain.ActionTypeWatchFolder: if release.TorrentTmpFile == "" { @@ -71,7 +71,7 @@ func (s *service) runAction(action domain.Action, release domain.Release) error } } - s.watchFolder(action.WatchFolder, release.TorrentTmpFile) + s.watchFolder(action, release) case domain.ActionTypeDelugeV1, domain.ActionTypeDelugeV2: canDownload, err := s.delugeCheckRulesCanDownload(action) @@ -91,7 +91,7 @@ func (s *service) runAction(action domain.Action, release domain.Release) error } } - err = s.deluge(action, release.TorrentTmpFile) + err = s.deluge(action, release) if err != nil { log.Error().Stack().Err(err).Msg("error sending torrent to Deluge") return err @@ -115,7 +115,7 @@ func (s *service) runAction(action domain.Action, release domain.Release) error } } - err = s.qbittorrent(client, action, release.TorrentTmpFile, release.TorrentHash) + err = s.qbittorrent(client, action, release) if err != nil { log.Error().Stack().Err(err).Msg("error sending torrent to qBittorrent") return err @@ -215,19 +215,27 @@ func (s *service) test(name string) { log.Info().Msgf("action TEST: %v", name) } -func (s *service) watchFolder(dir string, torrentFile string) { - log.Trace().Msgf("action WATCH_FOLDER: %v file: %v", dir, torrentFile) +func (s *service) watchFolder(action domain.Action, release domain.Release) { + m := NewMacro(release) + + // parse and replace values in argument string before continuing + watchFolderArgs, err := m.Parse(action.WatchFolder) + if err != nil { + log.Error().Stack().Err(err).Msgf("could not parse macro: %v", action.WatchFolder) + } + + log.Trace().Msgf("action WATCH_FOLDER: %v file: %v", watchFolderArgs, release.TorrentTmpFile) // Open original file - original, err := os.Open(torrentFile) + original, err := os.Open(release.TorrentTmpFile) if err != nil { - log.Error().Stack().Err(err).Msgf("could not open temp file '%v'", torrentFile) + log.Error().Stack().Err(err).Msgf("could not open temp file '%v'", release.TorrentTmpFile) return } defer original.Close() - _, tmpFileName := path.Split(torrentFile) - fullFileName := path.Join(dir, tmpFileName+".torrent") + _, tmpFileName := path.Split(release.TorrentTmpFile) + fullFileName := path.Join(watchFolderArgs, tmpFileName+".torrent") // Create new file newFile, err := os.Create(fullFileName)