From d5d1cecc1b83f1bd812056ff43a0828c3e47f4c3 Mon Sep 17 00:00:00 2001
From: ze0s <43699394+zze0s@users.noreply.github.com>
Date: Tue, 31 Oct 2023 00:38:23 +0100
Subject: [PATCH] feat(clients): Transmission support seedtime and ratiolimit
(#1211)
* feat(clients): Transmission seedtime ratiolimit
* feat(clients): update client pkg
* feat(clients): update client pkg test
* feat(actions): update transmission
---
go.mod | 2 +-
go.sum | 9 +--
internal/action/transmission.go | 89 ++++++++++++++++++++++----
internal/download_client/connection.go | 22 +++++--
pkg/transmission/transmission.go | 58 +++++++++++++++++
web/src/screens/filters/Action.tsx | 32 +++++++++
6 files changed, 189 insertions(+), 23 deletions(-)
create mode 100644 pkg/transmission/transmission.go
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) => {
/>
+
+
+
+