diff --git a/go.mod b/go.mod index 5e02682..442888e 100644 --- a/go.mod +++ b/go.mod @@ -23,7 +23,7 @@ require ( github.com/gorilla/sessions v1.2.1 github.com/gosimple/slug v1.13.1 github.com/hashicorp/go-version v1.6.0 - github.com/hekmon/transmissionrpc/v2 v2.0.1 + github.com/hekmon/transmissionrpc/v3 v3.0.0 github.com/lib/pq v1.10.9 github.com/mattn/go-shellwords v1.0.12 github.com/mmcdole/gofeed v1.2.1 diff --git a/go.sum b/go.sum index ea3a9bf..49db94c 100644 --- a/go.sum +++ b/go.sum @@ -147,8 +147,6 @@ github.com/ergochat/irc-go v0.4.0 h1:0YibCKfAAtwxQdNjLQd9xpIEPisLcJ45f8FNsMHAuZc github.com/ergochat/irc-go v0.4.0/go.mod h1:2vi7KNpIPWnReB5hmLpl92eMywQvuIeIIGdt/FQCph0= github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= -github.com/fsnotify/fsnotify v1.6.0 h1:n+5WquG0fcWoWp6xPWfHdbskMCQaFnG6PfBrh1Ky4HY= -github.com/fsnotify/fsnotify v1.6.0/go.mod h1:sl3t1tCWJFWoRz9R8WJCbQihKKwmorjAbSClcnxKAGw= github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA= github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM= github.com/gdm85/go-rencode v0.1.8 h1:7+qxwoQWU1b1nMGcESOyoUR5dzPtRA6yLQpKn7uXmnI= @@ -261,8 +259,8 @@ github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= github.com/hekmon/cunits/v2 v2.1.0 h1:k6wIjc4PlacNOHwKEMBgWV2/c8jyD4eRMs5mR1BBhI0= github.com/hekmon/cunits/v2 v2.1.0/go.mod h1:9r1TycXYXaTmEWlAIfFV8JT+Xo59U96yUJAYHxzii2M= -github.com/hekmon/transmissionrpc/v2 v2.0.1 h1:WkILCEdbNy3n/N/w7mi449waMPdH2AA1THyw7TfnN/w= -github.com/hekmon/transmissionrpc/v2 v2.0.1/go.mod h1:+s96Pkg7dIP3h2PT3fzhXPvNb3OdLryh5J8PIvQg3aA= +github.com/hekmon/transmissionrpc/v3 v3.0.0 h1:0Fb11qE0IBh4V4GlOwHNYpqpjcYDp5GouolwrpmcUDQ= +github.com/hekmon/transmissionrpc/v3 v3.0.0/go.mod h1:38SlNhFzinVUuY87wGj3acOmRxeYZAZfrj6Re7UgCDg= github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= github.com/huandu/xstrings v1.0.0/go.mod h1:4qWG/gcEcfX4z/mBDHJ++3ReCw9ibxbsNJbcucJdbSo= github.com/huandu/xstrings v1.2.0/go.mod h1:DvyZB1rfVYsBIigL8HwpZgxHwXozlTgGqn63UyNX5k4= @@ -331,8 +329,6 @@ github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lN github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= -github.com/moistari/rls v0.5.9 h1:peRGW+1/HJDUZ76s0v2ukcBLCBUs4/Qf3TKOzRjOOco= -github.com/moistari/rls v0.5.9/go.mod h1:/3P63JjNkaf1MNBoS2tSXqGeqee6l4je+Krakp4ob7c= github.com/moistari/rls v0.5.10 h1:e2XDxF0/+Riz9TEiTSewuZ7nuNZKOpbY1V2KSJWA2e4= github.com/moistari/rls v0.5.10/go.mod h1:/3P63JjNkaf1MNBoS2tSXqGeqee6l4je+Krakp4ob7c= github.com/mschoch/smat v0.0.0-20160514031455-90eadee771ae/go.mod h1:qAyveg+e4CE+eKJXWVjKXM4ck2QobLqTDytGJbLLhJg= @@ -614,7 +610,6 @@ golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= diff --git a/internal/action/transmission.go b/internal/action/transmission.go index a0839ba..1e5540d 100644 --- a/internal/action/transmission.go +++ b/internal/action/transmission.go @@ -6,13 +6,15 @@ package action import ( "context" "fmt" + "net/url" "strings" "time" "github.com/autobrr/autobrr/internal/domain" "github.com/autobrr/autobrr/pkg/errors" + "github.com/autobrr/autobrr/pkg/transmission" - "github.com/hekmon/transmissionrpc/v2" + "github.com/hekmon/transmissionrpc/v3" ) const ( @@ -21,6 +23,7 @@ const ( ) var ErrReannounceTookTooLong = errors.New("ErrReannounceTookTooLong") +var TrTrue = true func (s *service) transmission(ctx context.Context, action *domain.Action, release domain.Release) ([]string, error) { s.log.Debug().Msgf("action Transmission: %s", action.Name) @@ -38,10 +41,21 @@ func (s *service) transmission(ctx context.Context, action *domain.Action, relea return nil, errors.New("could not find client by id: %d", action.ClientID) } - tbt, err := transmissionrpc.New(client.Host, client.Username, client.Password, &transmissionrpc.AdvancedConfig{ - HTTPS: client.TLS, - Port: uint16(client.Port), - UserAgent: "autobrr", + scheme := "http" + if client.TLS { + scheme = "https" + } + + u, err := url.Parse(fmt.Sprintf("%s://%s:%d/transmission/rpc", scheme, client.Host, client.Port)) + if err != nil { + return nil, err + } + + tbt, err := transmission.New(u, &transmission.Config{ + UserAgent: "autobrr", + Username: client.Username, + Password: client.Password, + TLSSkipVerify: client.TLSSkipVerify, }) if err != nil { return nil, errors.Wrap(err, "error logging into client: %s", client.Host) @@ -74,10 +88,36 @@ func (s *service) transmission(ctx context.Context, action *domain.Action, relea return nil, errors.Wrap(err, "could not add torrent from magnet %s to client: %s", release.MagnetURI, client.Host) } - if action.Label != "" { + if action.Label != "" || action.LimitUploadSpeed > 0 || action.LimitDownloadSpeed > 0 || action.LimitRatio > 0 || action.LimitSeedTime > 0 { p := transmissionrpc.TorrentSetPayload{ - IDs: []int64{*torrent.ID}, - Labels: []string{action.Label}, + IDs: []int64{*torrent.ID}, + } + + if action.Label != "" { + p.Labels = []string{action.Label} + } + + if action.LimitUploadSpeed > 0 { + p.UploadLimit = &action.LimitUploadSpeed + p.UploadLimited = &TrTrue + } + if action.LimitDownloadSpeed > 0 { + p.DownloadLimit = &action.LimitDownloadSpeed + p.DownloadLimited = &TrTrue + } + if action.LimitRatio > 0 { + p.SeedRatioLimit = &action.LimitRatio + ratioMode := transmissionrpc.SeedRatioModeCustom + p.SeedRatioMode = &ratioMode + } + if action.LimitSeedTime > 0 { + t := time.Duration(action.LimitSeedTime) * time.Minute + //p.SeedIdleLimit = &action.LimitSeedTime + p.SeedIdleLimit = &t + + // seed idle mode 1 + seedIdleMode := int64(1) + p.SeedIdleMode = &seedIdleMode } if err := tbt.TorrentSet(ctx, p); err != nil { @@ -112,12 +152,39 @@ func (s *service) transmission(ctx context.Context, action *domain.Action, relea return nil, errors.Wrap(err, "could not add torrent %s to client: %s", release.TorrentTmpFile, client.Host) } - if action.Label != "" { + if action.Label != "" || action.LimitUploadSpeed > 0 || action.LimitDownloadSpeed > 0 || action.LimitRatio > 0 || action.LimitSeedTime > 0 { p := transmissionrpc.TorrentSetPayload{ - IDs: []int64{*torrent.ID}, - Labels: []string{action.Label}, + IDs: []int64{*torrent.ID}, } + if action.Label != "" { + p.Labels = []string{action.Label} + } + + if action.LimitUploadSpeed > 0 { + p.UploadLimit = &action.LimitUploadSpeed + p.UploadLimited = &TrTrue + } + if action.LimitDownloadSpeed > 0 { + p.DownloadLimit = &action.LimitDownloadSpeed + p.DownloadLimited = &TrTrue + } + if action.LimitRatio > 0 { + p.SeedRatioLimit = &action.LimitRatio + ratioMode := transmissionrpc.SeedRatioModeCustom + p.SeedRatioMode = &ratioMode + } + if action.LimitSeedTime > 0 { + t := time.Duration(action.LimitSeedTime) * time.Minute + p.SeedIdleLimit = &t + + // seed idle mode 1 + seedIdleMode := int64(1) + p.SeedIdleMode = &seedIdleMode + } + + s.log.Trace().Msgf("transmission torrent set payload: %+v for torrent hash %s client: %s", p, *torrent.HashString, client.Name) + if err := tbt.TorrentSet(ctx, p); err != nil { return nil, errors.Wrap(err, "could not set label for hash %s to client: %s", *torrent.HashString, client.Host) } diff --git a/internal/download_client/connection.go b/internal/download_client/connection.go index 57576fe..0f9c5c3 100644 --- a/internal/download_client/connection.go +++ b/internal/download_client/connection.go @@ -5,6 +5,8 @@ package download_client import ( "context" + "fmt" + "net/url" "time" "github.com/autobrr/autobrr/internal/domain" @@ -15,13 +17,13 @@ import ( "github.com/autobrr/autobrr/pkg/readarr" "github.com/autobrr/autobrr/pkg/sabnzbd" "github.com/autobrr/autobrr/pkg/sonarr" + "github.com/autobrr/autobrr/pkg/transmission" "github.com/autobrr/autobrr/pkg/whisparr" "github.com/autobrr/go-deluge" "github.com/autobrr/go-qbittorrent" "github.com/autobrr/go-rtorrent" "github.com/dcarbone/zadapters/zstdlog" - "github.com/hekmon/transmissionrpc/v2" "github.com/rs/zerolog" ) @@ -175,9 +177,21 @@ func (s *service) testRTorrentConnection(ctx context.Context, client domain.Down } func (s *service) testTransmissionConnection(ctx context.Context, client domain.DownloadClient) error { - tbt, err := transmissionrpc.New(client.Host, client.Username, client.Password, &transmissionrpc.AdvancedConfig{ - HTTPS: client.TLS, - Port: uint16(client.Port), + scheme := "http" + if client.TLS { + scheme = "https" + } + + u, err := url.Parse(fmt.Sprintf("%s://%s:%d/transmission/rpc", scheme, client.Host, client.Port)) + if err != nil { + return err + } + + tbt, err := transmission.New(u, &transmission.Config{ + UserAgent: "autobrr", + Username: client.Username, + Password: client.Password, + TLSSkipVerify: client.TLSSkipVerify, }) if err != nil { return errors.Wrap(err, "error logging into client: %v", client.Host) diff --git a/pkg/transmission/transmission.go b/pkg/transmission/transmission.go new file mode 100644 index 0000000..4c644af --- /dev/null +++ b/pkg/transmission/transmission.go @@ -0,0 +1,58 @@ +package transmission + +import ( + "crypto/tls" + "net/http" + "net/url" + "time" + + "github.com/hekmon/transmissionrpc/v3" +) + +type Config struct { + UserAgent string + CustomClient *http.Client + Username string + Password string + TLSSkipVerify bool + Timeout int +} + +func New(endpoint *url.URL, cfg *Config) (*transmissionrpc.Client, error) { + ct := &customTransport{ + Username: cfg.Username, + Password: cfg.Password, + TLSSkipVerify: cfg.TLSSkipVerify, + } + + extra := &transmissionrpc.Config{ + CustomClient: &http.Client{ + Transport: ct, + Timeout: time.Second * 60, + }, + UserAgent: cfg.UserAgent, + } + + return transmissionrpc.New(endpoint, extra) +} + +type customTransport struct { + Username string + Password string + TLSSkipVerify bool +} + +func (t *customTransport) RoundTrip(req *http.Request) (*http.Response, error) { + dt := http.DefaultTransport.(*http.Transport).Clone() + if t.TLSSkipVerify { + dt.TLSClientConfig = &tls.Config{InsecureSkipVerify: true} + } + + r := req.Clone(req.Context()) + + if t.Username != "" && t.Password != "" { + r.SetBasicAuth(t.Username, t.Password) + } + + return dt.RoundTrip(r) +} diff --git a/web/src/screens/filters/Action.tsx b/web/src/screens/filters/Action.tsx index 32b91b2..6ce41a6 100644 --- a/web/src/screens/filters/Action.tsx +++ b/web/src/screens/filters/Action.tsx @@ -493,6 +493,38 @@ const TypeForm = ({ action, idx, clients }: TypeFormProps) => { /> + +
+
+ + +
+ +
+ + +
+
+
+