From 0c4aaa29b00e539df1aaee28901a534d91301ce6 Mon Sep 17 00:00:00 2001 From: Ludvig Lundgren Date: Fri, 20 Aug 2021 22:08:32 +0200 Subject: [PATCH] Feature: Deluge download client (#12) * chore: add go-libdeluge package * feat: implement deluge v1 and v2 clients * feat(web): handle add and update deluge clients * chore: temp remove releaseinfo parser --- go.mod | 1 + go.sum | 4 + internal/action/deluge.go | 195 ++++++++++++++++++ internal/action/service.go | 11 +- internal/announce/parse.go | 14 +- internal/domain/action.go | 3 +- internal/domain/client.go | 3 +- internal/download_client/service.go | 104 ++++++++-- pkg/releaseinfo/parser.go | 9 + pkg/releaseinfo/parser_test.go | 22 ++ web/src/components/FilterActionList.tsx | 23 +-- web/src/components/inputs/SelectField.tsx | 97 +++++++++ web/src/components/inputs/index.ts | 1 + web/src/domain/constants.ts | 32 ++- web/src/domain/interfaces.ts | 18 +- web/src/forms/filters/FilterActionAddForm.tsx | 22 +- .../forms/filters/FilterActionUpdateForm.tsx | 26 +-- .../forms/settings/DownloadClientAddForm.tsx | 27 +-- web/src/screens/settings/DownloadClient.tsx | 3 +- 19 files changed, 493 insertions(+), 122 deletions(-) create mode 100644 internal/action/deluge.go create mode 100644 web/src/components/inputs/SelectField.tsx diff --git a/go.mod b/go.mod index 25f6679..b49c9f1 100644 --- a/go.mod +++ b/go.mod @@ -5,6 +5,7 @@ go 1.16 require ( github.com/anacrolix/torrent v1.29.1 github.com/fluffle/goirc v1.0.3 + github.com/gdm85/go-libdeluge v0.5.4 github.com/go-chi/chi v1.5.4 github.com/gorilla/sessions v1.2.1 github.com/lib/pq v1.10.2 diff --git a/go.sum b/go.sum index 9e95917..176f1aa 100644 --- a/go.sum +++ b/go.sum @@ -222,6 +222,10 @@ github.com/frankban/quicktest v1.11.3/go.mod h1:wRf/ReqHper53s+kmmSZizM8NamnL3IM github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4= github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= +github.com/gdm85/go-libdeluge v0.5.4 h1:Y2vV6wGwvR5skrFrlntTYAXxaWRzg2HDqrcWyVzuUgo= +github.com/gdm85/go-libdeluge v0.5.4/go.mod h1:Fxm576GtD2fTcSUCSPqJINBZRkY8WrtGf9JfYVRtmD0= +github.com/gdm85/go-rencode v0.1.6 h1:JbSv//2Og8aeSMUBMDTRNA6JW55iZbLMJU8bp9GqULY= +github.com/gdm85/go-rencode v0.1.6/go.mod h1:0dr3BuaKzeseY1of6o1KRTGB/Oo7eio+YEyz8KDp5+s= github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= github.com/gliderlabs/ssh v0.1.1/go.mod h1:U7qILu1NlMHj9FlMhZLlkCdDnU1DBEAqr0aevW3Awn0= github.com/glycerine/go-unsnap-stream v0.0.0-20180323001048-9f0cb55181dd/go.mod h1:/20jfyN9Y5QPEAprSgKAUr+glWDY39ZiUEAYOEv5dsE= diff --git a/internal/action/deluge.go b/internal/action/deluge.go new file mode 100644 index 0000000..60796e8 --- /dev/null +++ b/internal/action/deluge.go @@ -0,0 +1,195 @@ +package action + +import ( + "encoding/base64" + "errors" + "io/ioutil" + "time" + + "github.com/autobrr/autobrr/internal/domain" + + delugeClient "github.com/gdm85/go-libdeluge" + "github.com/rs/zerolog/log" +) + +func (s *service) deluge(action domain.Action, torrentFile string) error { + log.Trace().Msgf("action DELUGE: %v", torrentFile) + + var err error + + // get client for action + client, err := s.clientSvc.FindByID(action.ClientID) + if err != nil { + log.Error().Err(err).Msgf("error finding client: %v", action.ClientID) + return err + } + + if client == nil { + return errors.New("no client found") + } + + settings := delugeClient.Settings{ + Hostname: client.Host, + Port: uint(client.Port), + Login: client.Username, + Password: client.Password, + DebugServerResponses: true, + ReadWriteTimeout: time.Second * 20, + } + + switch client.Type { + case "DELUGE_V1": + err = delugeV1(settings, action, torrentFile) + + case "DELUGE_V2": + err = delugeV2(settings, action, torrentFile) + } + + return err +} + +func delugeV1(settings delugeClient.Settings, action domain.Action, torrentFile string) error { + + deluge := delugeClient.NewV1(settings) + + // perform connection to Deluge server + err := deluge.Connect() + if err != nil { + log.Error().Err(err).Msgf("error logging into client: %v", settings.Hostname) + return err + } + + defer deluge.Close() + + t, err := ioutil.ReadFile(torrentFile) + if err != nil { + log.Error().Err(err).Msgf("could not read torrent file: %v", torrentFile) + return err + } + + // encode file to base64 before sending to deluge + encodedFile := base64.StdEncoding.EncodeToString(t) + if encodedFile == "" { + log.Error().Err(err).Msgf("could not encode torrent file: %v", torrentFile) + return err + } + + // set options + options := delugeClient.Options{} + + if action.Paused { + options.AddPaused = &action.Paused + } + if action.SavePath != "" { + options.DownloadLocation = &action.SavePath + } + if action.LimitDownloadSpeed > 0 { + maxDL := int(action.LimitDownloadSpeed) + options.MaxDownloadSpeed = &maxDL + } + if action.LimitUploadSpeed > 0 { + maxUL := int(action.LimitUploadSpeed) + options.MaxUploadSpeed = &maxUL + } + + torrentHash, err := deluge.AddTorrentFile(torrentFile, encodedFile, &options) + if err != nil { + log.Error().Err(err).Msgf("could not add torrent to client: %v", torrentFile) + return err + } + + if action.Label != "" { + + p, err := deluge.LabelPlugin() + if err != nil { + log.Error().Err(err).Msgf("could not load label plugin: %v", torrentFile) + return err + } + + if p != nil { + // TODO first check if label exists, if not, add it, otherwise set + err = p.SetTorrentLabel(torrentHash, action.Label) + if err != nil { + log.Error().Err(err).Msgf("could not set label: %v", torrentFile) + return err + } + } + } + + log.Trace().Msgf("deluge: torrent successfully added! hash: %v", torrentHash) + + return nil +} + +func delugeV2(settings delugeClient.Settings, action domain.Action, torrentFile string) error { + + deluge := delugeClient.NewV2(settings) + + // perform connection to Deluge server + err := deluge.Connect() + if err != nil { + log.Error().Err(err).Msgf("error logging into client: %v", settings.Hostname) + return err + } + + defer deluge.Close() + + t, err := ioutil.ReadFile(torrentFile) + if err != nil { + log.Error().Err(err).Msgf("could not read torrent file: %v", torrentFile) + return err + } + + // encode file to base64 before sending to deluge + encodedFile := base64.StdEncoding.EncodeToString(t) + if encodedFile == "" { + log.Error().Err(err).Msgf("could not encode torrent file: %v", torrentFile) + return err + } + + // set options + options := delugeClient.Options{} + + if action.Paused { + options.AddPaused = &action.Paused + } + if action.SavePath != "" { + options.DownloadLocation = &action.SavePath + } + if action.LimitDownloadSpeed > 0 { + maxDL := int(action.LimitDownloadSpeed) + options.MaxDownloadSpeed = &maxDL + } + if action.LimitUploadSpeed > 0 { + maxUL := int(action.LimitUploadSpeed) + options.MaxUploadSpeed = &maxUL + } + + torrentHash, err := deluge.AddTorrentFile(torrentFile, encodedFile, &options) + if err != nil { + log.Error().Err(err).Msgf("could not add torrent to client: %v", torrentFile) + return err + } + + if action.Label != "" { + + p, err := deluge.LabelPlugin() + if err != nil { + log.Error().Err(err).Msgf("could not load label plugin: %v", torrentFile) + return err + } + + if p != nil { + // TODO first check if label exists, if not, add it, otherwise set + err = p.SetTorrentLabel(torrentHash, action.Label) + if err != nil { + log.Error().Err(err).Msgf("could not set label: %v", torrentFile) + return err + } + } + } + + log.Trace().Msgf("deluge: torrent successfully added! hash: %v", torrentHash) + + return nil +} diff --git a/internal/action/service.go b/internal/action/service.go index d254a58..207f878 100644 --- a/internal/action/service.go +++ b/internal/action/service.go @@ -57,10 +57,17 @@ func (s *service) RunActions(torrentFile string, hash string, filter domain.Filt case domain.ActionTypeExec: go s.execCmd(announce, action, torrentFile) - // deluge + case domain.ActionTypeDelugeV1, domain.ActionTypeDelugeV2: + go func() { + err := s.deluge(action, torrentFile) + if err != nil { + log.Error().Err(err).Msg("error sending torrent to client") + } + }() + // pvr *arr default: - log.Debug().Msgf("unsupported action: %v type: %v", action.Name, action.Type) + log.Warn().Msgf("unsupported action: %v type: %v", action.Name, action.Type) } } diff --git a/internal/announce/parse.go b/internal/announce/parse.go index fa2e91f..50dbcef 100644 --- a/internal/announce/parse.go +++ b/internal/announce/parse.go @@ -11,8 +11,6 @@ import ( "text/template" "github.com/autobrr/autobrr/internal/domain" - "github.com/autobrr/autobrr/pkg/releaseinfo" - "github.com/pkg/errors" "github.com/rs/zerolog/log" ) @@ -262,12 +260,12 @@ func (s *service) extractReleaseInfo(varMap map[string]string, releaseName strin canonReleaseName := cleanReleaseName(releaseName) log.Trace().Msgf("canonicalize release name: %v", canonReleaseName) - release, err := releaseinfo.Parse(releaseName) - if err != nil { - return err - } - - log.Trace().Msgf("release: %+v", release) + //release, err := releaseinfo.Parse(releaseName) + //if err != nil { + // return err + //} + // + //log.Trace().Msgf("release: %+v", release) // https://github.com/autodl-community/autodl-irssi/pull/194/files // year diff --git a/internal/domain/action.go b/internal/domain/action.go index 4085248..dafe2cb 100644 --- a/internal/domain/action.go +++ b/internal/domain/action.go @@ -34,6 +34,7 @@ const ( ActionTypeTest ActionType = "TEST" ActionTypeExec ActionType = "EXEC" ActionTypeQbittorrent ActionType = "QBITTORRENT" - ActionTypeDeluge ActionType = "DELUGE" + ActionTypeDelugeV1 ActionType = "DELUGE_V1" + ActionTypeDelugeV2 ActionType = "DELUGE_V2" ActionTypeWatchFolder ActionType = "WATCH_FOLDER" ) diff --git a/internal/domain/client.go b/internal/domain/client.go index ccf0c03..8d717c2 100644 --- a/internal/domain/client.go +++ b/internal/domain/client.go @@ -24,5 +24,6 @@ type DownloadClientType string const ( DownloadClientTypeQbittorrent DownloadClientType = "QBITTORRENT" - DownloadClientTypeDeluge DownloadClientType = "DELUGE" + DownloadClientTypeDelugeV1 DownloadClientType = "DELUGE_V1" + DownloadClientTypeDelugeV2 DownloadClientType = "DELUGE_V2" ) diff --git a/internal/download_client/service.go b/internal/download_client/service.go index 7b6b4c7..5af27fd 100644 --- a/internal/download_client/service.go +++ b/internal/download_client/service.go @@ -1,10 +1,13 @@ package download_client import ( - "github.com/rs/zerolog/log" + "errors" + "time" "github.com/autobrr/autobrr/internal/domain" "github.com/autobrr/autobrr/pkg/qbittorrent" + delugeClient "github.com/gdm85/go-libdeluge" + "github.com/rs/zerolog/log" ) type Service interface { @@ -43,6 +46,13 @@ func (s *service) FindByID(id int32) (*domain.DownloadClient, error) { func (s *service) Store(client domain.DownloadClient) (*domain.DownloadClient, error) { // validate data + if client.Host == "" { + return nil, errors.New("validation error: no host") + } else if client.Port == 0 { + return nil, errors.New("validation error: no port") + } else if client.Type == "" { + return nil, errors.New("validation error: no type") + } // store c, err := s.repo.Store(client) @@ -64,6 +74,15 @@ func (s *service) Delete(clientID int) error { } func (s *service) Test(client domain.DownloadClient) error { + // basic validation of client + if client.Host == "" { + return errors.New("validation error: no host") + } else if client.Port == 0 { + return errors.New("validation error: no port") + } else if client.Type == "" { + return errors.New("validation error: no type") + } + // test err := s.testConnection(client) if err != nil { @@ -74,22 +93,75 @@ func (s *service) Test(client domain.DownloadClient) error { } func (s *service) testConnection(client domain.DownloadClient) error { - if client.Type == "QBITTORRENT" { - qbtSettings := qbittorrent.Settings{ - Hostname: client.Host, - Port: uint(client.Port), - Username: client.Username, - Password: client.Password, - SSL: client.SSL, - } - - qbt := qbittorrent.NewClient(qbtSettings) - err := qbt.Login() - if err != nil { - log.Error().Err(err).Msgf("error logging into client: %v", client.Host) - return err - } + switch client.Type { + case domain.DownloadClientTypeQbittorrent: + return s.testQbittorrentConnection(client) + case domain.DownloadClientTypeDelugeV1, domain.DownloadClientTypeDelugeV2: + return s.testDelugeConnection(client) } return nil } + +func (s *service) testQbittorrentConnection(client domain.DownloadClient) error { + qbtSettings := qbittorrent.Settings{ + Hostname: client.Host, + Port: uint(client.Port), + Username: client.Username, + Password: client.Password, + SSL: client.SSL, + } + + qbt := qbittorrent.NewClient(qbtSettings) + err := qbt.Login() + if err != nil { + log.Error().Err(err).Msgf("error logging into client: %v", client.Host) + return err + } + + return nil +} + +func (s *service) testDelugeConnection(client domain.DownloadClient) error { + var deluge delugeClient.DelugeClient + + settings := delugeClient.Settings{ + Hostname: client.Host, + Port: uint(client.Port), + Login: client.Username, + Password: client.Password, + DebugServerResponses: true, + ReadWriteTimeout: time.Second * 10, + } + + switch client.Type { + case "DELUGE_V1": + deluge = delugeClient.NewV1(settings) + + case "DELUGE_V2": + deluge = delugeClient.NewV2(settings) + + default: + deluge = delugeClient.NewV2(settings) + } + + // perform connection to Deluge server + err := deluge.Connect() + if err != nil { + log.Error().Err(err).Msgf("error logging into client: %v", client.Host) + return err + } + + defer deluge.Close() + + // print daemon version + ver, err := deluge.DaemonVersion() + if err != nil { + log.Error().Err(err).Msgf("could not get daemon version: %v", client.Host) + return err + } + + log.Debug().Msgf("daemon version: %v", ver) + + return nil +} diff --git a/pkg/releaseinfo/parser.go b/pkg/releaseinfo/parser.go index dba1725..44985c7 100644 --- a/pkg/releaseinfo/parser.go +++ b/pkg/releaseinfo/parser.go @@ -82,6 +82,15 @@ func Parse(filename string) (*ReleaseInfo, error) { setField(tor, pattern.name, matches[matchIdx][1], matches[matchIdx][2]) } + if startIndex > endIndex { + // FIXME temp solution to not panic if the are the reverse + tmpStart := startIndex + tmpEnd := endIndex + + startIndex = tmpEnd + endIndex = tmpStart + } + // Start process for title //fmt.Println(" title: ") raw := strings.Split(filename[startIndex:endIndex], "(")[0] diff --git a/pkg/releaseinfo/parser_test.go b/pkg/releaseinfo/parser_test.go index 6457550..9f9bcb1 100644 --- a/pkg/releaseinfo/parser_test.go +++ b/pkg/releaseinfo/parser_test.go @@ -191,6 +191,7 @@ var tvTests = []string{ "Power Book III: Raising Kanan S01E02 2160p WEB-DL DD+ 5.1 H265-GGWP", "Thea Walking Dead: Origins S01E01 1080p WEB-DL DD+ 2.0 H.264-GOSSIP", "Mean Mums S01 1080p AMZN WEB-DL DD+ 2.0 H.264-FLUX", + "[BBT-RMX] Servant x Service", } func TestParse_TV(t *testing.T) { @@ -250,6 +251,27 @@ func TestParse_TV(t *testing.T) { }, wantErr: false, }, + { + filename: "[BBT-RMX] Servant x Service", + want: &ReleaseInfo{ + Title: "", + }, + wantErr: false, + }, + { + filename: "[Dekinai] Dungeon Ni Deai O Motomeru No Wa Machigatte Iru Darouka ~Familia Myth~ (2015) [BD 1080p x264 10bit - FLAC 2 0]", + want: &ReleaseInfo{ + Title: "", + }, + wantErr: false, + }, + { + filename: "[SubsPlease] Higurashi no Naku Koro ni Sotsu - 09 (1080p) [C00D6C68]", + want: &ReleaseInfo{ + Title: "", + }, + wantErr: false, + }, } for _, tt := range tests { t.Run(tt.filename, func(t *testing.T) { diff --git a/web/src/components/FilterActionList.tsx b/web/src/components/FilterActionList.tsx index 1aff9e4..bbae54b 100644 --- a/web/src/components/FilterActionList.tsx +++ b/web/src/components/FilterActionList.tsx @@ -10,19 +10,7 @@ import {TextField} from "./inputs"; import DEBUG from "./debug"; import APIClient from "../api/APIClient"; import {queryClient} from "../App"; - -interface radioFieldsetOption { - label: string; - value: string; -} - -const actionTypeOptions: radioFieldsetOption[] = [ - {label: "Test", value: "TEST"}, - {label: "Watch dir", value: "WATCH_FOLDER"}, - {label: "Exec", value: "EXEC"}, - {label: "qBittorrent", value: "QBITTORRENT"}, - {label: "Deluge", value: "DELUGE"}, -]; +import {ActionTypeNameMap, ActionTypeOptions, DownloadClientTypeNameMap} from "../domain/constants"; interface FilterListProps { actions: Action[]; @@ -262,7 +250,8 @@ function ListItem({action, clients, filterID, idx}: ListItemProps) { ) - case "DELUGE": + case "DELUGE_V1": + case "DELUGE_V2": return (
@@ -426,7 +415,7 @@ function ListItem({action, clients, filterID, idx}: ListItemProps) {
- {action.type} + {ActionTypeNameMap[action.type]}
@@ -561,7 +550,7 @@ function ListItem({action, clients, filterID, idx}: ListItemProps) { {input.value ? actionTypeOptions.find(c => c.value === input.value)!.label : "Choose a type"} + className="block truncate">{input.value ? ActionTypeOptions.find(c => c.value === input.value)!.label : "Choose a type"}