mirror of
https://github.com/idanoo/autobrr
synced 2025-07-23 08:49:13 +00:00
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
This commit is contained in:
parent
eb5b040eeb
commit
0c4aaa29b0
19 changed files with 493 additions and 122 deletions
1
go.mod
1
go.mod
|
@ -5,6 +5,7 @@ go 1.16
|
||||||
require (
|
require (
|
||||||
github.com/anacrolix/torrent v1.29.1
|
github.com/anacrolix/torrent v1.29.1
|
||||||
github.com/fluffle/goirc v1.0.3
|
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/go-chi/chi v1.5.4
|
||||||
github.com/gorilla/sessions v1.2.1
|
github.com/gorilla/sessions v1.2.1
|
||||||
github.com/lib/pq v1.10.2
|
github.com/lib/pq v1.10.2
|
||||||
|
|
4
go.sum
4
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.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 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4=
|
||||||
github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
|
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/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
|
||||||
github.com/gliderlabs/ssh v0.1.1/go.mod h1:U7qILu1NlMHj9FlMhZLlkCdDnU1DBEAqr0aevW3Awn0=
|
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=
|
github.com/glycerine/go-unsnap-stream v0.0.0-20180323001048-9f0cb55181dd/go.mod h1:/20jfyN9Y5QPEAprSgKAUr+glWDY39ZiUEAYOEv5dsE=
|
||||||
|
|
195
internal/action/deluge.go
Normal file
195
internal/action/deluge.go
Normal file
|
@ -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
|
||||||
|
}
|
|
@ -57,10 +57,17 @@ func (s *service) RunActions(torrentFile string, hash string, filter domain.Filt
|
||||||
case domain.ActionTypeExec:
|
case domain.ActionTypeExec:
|
||||||
go s.execCmd(announce, action, torrentFile)
|
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
|
// pvr *arr
|
||||||
default:
|
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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -11,8 +11,6 @@ import (
|
||||||
"text/template"
|
"text/template"
|
||||||
|
|
||||||
"github.com/autobrr/autobrr/internal/domain"
|
"github.com/autobrr/autobrr/internal/domain"
|
||||||
"github.com/autobrr/autobrr/pkg/releaseinfo"
|
|
||||||
|
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
"github.com/rs/zerolog/log"
|
"github.com/rs/zerolog/log"
|
||||||
)
|
)
|
||||||
|
@ -262,12 +260,12 @@ func (s *service) extractReleaseInfo(varMap map[string]string, releaseName strin
|
||||||
canonReleaseName := cleanReleaseName(releaseName)
|
canonReleaseName := cleanReleaseName(releaseName)
|
||||||
log.Trace().Msgf("canonicalize release name: %v", canonReleaseName)
|
log.Trace().Msgf("canonicalize release name: %v", canonReleaseName)
|
||||||
|
|
||||||
release, err := releaseinfo.Parse(releaseName)
|
//release, err := releaseinfo.Parse(releaseName)
|
||||||
if err != nil {
|
//if err != nil {
|
||||||
return err
|
// return err
|
||||||
}
|
//}
|
||||||
|
//
|
||||||
log.Trace().Msgf("release: %+v", release)
|
//log.Trace().Msgf("release: %+v", release)
|
||||||
|
|
||||||
// https://github.com/autodl-community/autodl-irssi/pull/194/files
|
// https://github.com/autodl-community/autodl-irssi/pull/194/files
|
||||||
// year
|
// year
|
||||||
|
|
|
@ -34,6 +34,7 @@ const (
|
||||||
ActionTypeTest ActionType = "TEST"
|
ActionTypeTest ActionType = "TEST"
|
||||||
ActionTypeExec ActionType = "EXEC"
|
ActionTypeExec ActionType = "EXEC"
|
||||||
ActionTypeQbittorrent ActionType = "QBITTORRENT"
|
ActionTypeQbittorrent ActionType = "QBITTORRENT"
|
||||||
ActionTypeDeluge ActionType = "DELUGE"
|
ActionTypeDelugeV1 ActionType = "DELUGE_V1"
|
||||||
|
ActionTypeDelugeV2 ActionType = "DELUGE_V2"
|
||||||
ActionTypeWatchFolder ActionType = "WATCH_FOLDER"
|
ActionTypeWatchFolder ActionType = "WATCH_FOLDER"
|
||||||
)
|
)
|
||||||
|
|
|
@ -24,5 +24,6 @@ type DownloadClientType string
|
||||||
|
|
||||||
const (
|
const (
|
||||||
DownloadClientTypeQbittorrent DownloadClientType = "QBITTORRENT"
|
DownloadClientTypeQbittorrent DownloadClientType = "QBITTORRENT"
|
||||||
DownloadClientTypeDeluge DownloadClientType = "DELUGE"
|
DownloadClientTypeDelugeV1 DownloadClientType = "DELUGE_V1"
|
||||||
|
DownloadClientTypeDelugeV2 DownloadClientType = "DELUGE_V2"
|
||||||
)
|
)
|
||||||
|
|
|
@ -1,10 +1,13 @@
|
||||||
package download_client
|
package download_client
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/rs/zerolog/log"
|
"errors"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/autobrr/autobrr/internal/domain"
|
"github.com/autobrr/autobrr/internal/domain"
|
||||||
"github.com/autobrr/autobrr/pkg/qbittorrent"
|
"github.com/autobrr/autobrr/pkg/qbittorrent"
|
||||||
|
delugeClient "github.com/gdm85/go-libdeluge"
|
||||||
|
"github.com/rs/zerolog/log"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Service interface {
|
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) {
|
func (s *service) Store(client domain.DownloadClient) (*domain.DownloadClient, error) {
|
||||||
// validate data
|
// 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
|
// store
|
||||||
c, err := s.repo.Store(client)
|
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 {
|
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
|
// test
|
||||||
err := s.testConnection(client)
|
err := s.testConnection(client)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -74,7 +93,17 @@ func (s *service) Test(client domain.DownloadClient) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *service) testConnection(client domain.DownloadClient) error {
|
func (s *service) testConnection(client domain.DownloadClient) error {
|
||||||
if client.Type == "QBITTORRENT" {
|
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{
|
qbtSettings := qbittorrent.Settings{
|
||||||
Hostname: client.Host,
|
Hostname: client.Host,
|
||||||
Port: uint(client.Port),
|
Port: uint(client.Port),
|
||||||
|
@ -89,7 +118,50 @@ func (s *service) testConnection(client domain.DownloadClient) error {
|
||||||
log.Error().Err(err).Msgf("error logging into client: %v", client.Host)
|
log.Error().Err(err).Msgf("error logging into client: %v", client.Host)
|
||||||
return err
|
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
|
return nil
|
||||||
}
|
}
|
||||||
|
|
|
@ -82,6 +82,15 @@ func Parse(filename string) (*ReleaseInfo, error) {
|
||||||
setField(tor, pattern.name, matches[matchIdx][1], matches[matchIdx][2])
|
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
|
// Start process for title
|
||||||
//fmt.Println(" title: <internal>")
|
//fmt.Println(" title: <internal>")
|
||||||
raw := strings.Split(filename[startIndex:endIndex], "(")[0]
|
raw := strings.Split(filename[startIndex:endIndex], "(")[0]
|
||||||
|
|
|
@ -191,6 +191,7 @@ var tvTests = []string{
|
||||||
"Power Book III: Raising Kanan S01E02 2160p WEB-DL DD+ 5.1 H265-GGWP",
|
"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",
|
"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",
|
"Mean Mums S01 1080p AMZN WEB-DL DD+ 2.0 H.264-FLUX",
|
||||||
|
"[BBT-RMX] Servant x Service",
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestParse_TV(t *testing.T) {
|
func TestParse_TV(t *testing.T) {
|
||||||
|
@ -250,6 +251,27 @@ func TestParse_TV(t *testing.T) {
|
||||||
},
|
},
|
||||||
wantErr: false,
|
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 {
|
for _, tt := range tests {
|
||||||
t.Run(tt.filename, func(t *testing.T) {
|
t.Run(tt.filename, func(t *testing.T) {
|
||||||
|
|
|
@ -10,19 +10,7 @@ import {TextField} from "./inputs";
|
||||||
import DEBUG from "./debug";
|
import DEBUG from "./debug";
|
||||||
import APIClient from "../api/APIClient";
|
import APIClient from "../api/APIClient";
|
||||||
import {queryClient} from "../App";
|
import {queryClient} from "../App";
|
||||||
|
import {ActionTypeNameMap, ActionTypeOptions, DownloadClientTypeNameMap} from "../domain/constants";
|
||||||
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"},
|
|
||||||
];
|
|
||||||
|
|
||||||
interface FilterListProps {
|
interface FilterListProps {
|
||||||
actions: Action[];
|
actions: Action[];
|
||||||
|
@ -262,7 +250,8 @@ function ListItem({action, clients, filterID, idx}: ListItemProps) {
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
case "DELUGE":
|
case "DELUGE_V1":
|
||||||
|
case "DELUGE_V2":
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<div className="mt-6 grid grid-cols-12 gap-6">
|
<div className="mt-6 grid grid-cols-12 gap-6">
|
||||||
|
@ -426,7 +415,7 @@ function ListItem({action, clients, filterID, idx}: ListItemProps) {
|
||||||
</div>
|
</div>
|
||||||
<div className="mt-4 flex-shrink-0 sm:mt-0 sm:ml-5">
|
<div className="mt-4 flex-shrink-0 sm:mt-0 sm:ml-5">
|
||||||
<div className="flex overflow-hidden -space-x-1">
|
<div className="flex overflow-hidden -space-x-1">
|
||||||
<span className="text-sm font-normal text-gray-500">{action.type}</span>
|
<span className="text-sm font-normal text-gray-500">{ActionTypeNameMap[action.type]}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -561,7 +550,7 @@ function ListItem({action, clients, filterID, idx}: ListItemProps) {
|
||||||
<Listbox.Button
|
<Listbox.Button
|
||||||
className="bg-white relative w-full border border-gray-300 rounded-md shadow-sm pl-3 pr-10 py-2 text-left cursor-default focus:outline-none focus:ring-1 focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm">
|
className="bg-white relative w-full border border-gray-300 rounded-md shadow-sm pl-3 pr-10 py-2 text-left cursor-default focus:outline-none focus:ring-1 focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm">
|
||||||
<span
|
<span
|
||||||
className="block truncate">{input.value ? actionTypeOptions.find(c => c.value === input.value)!.label : "Choose a type"}</span>
|
className="block truncate">{input.value ? ActionTypeOptions.find(c => c.value === input.value)!.label : "Choose a type"}</span>
|
||||||
<span
|
<span
|
||||||
className="absolute inset-y-0 right-0 flex items-center pr-2 pointer-events-none">
|
className="absolute inset-y-0 right-0 flex items-center pr-2 pointer-events-none">
|
||||||
<SelectorIcon className="h-5 w-5 text-gray-400" aria-hidden="true"/>
|
<SelectorIcon className="h-5 w-5 text-gray-400" aria-hidden="true"/>
|
||||||
|
@ -579,7 +568,7 @@ function ListItem({action, clients, filterID, idx}: ListItemProps) {
|
||||||
static
|
static
|
||||||
className="absolute z-10 mt-1 w-full bg-white shadow-lg max-h-60 rounded-md py-1 text-base ring-1 ring-black ring-opacity-5 overflow-auto focus:outline-none sm:text-sm"
|
className="absolute z-10 mt-1 w-full bg-white shadow-lg max-h-60 rounded-md py-1 text-base ring-1 ring-black ring-opacity-5 overflow-auto focus:outline-none sm:text-sm"
|
||||||
>
|
>
|
||||||
{actionTypeOptions.map((opt) => (
|
{ActionTypeOptions.map((opt) => (
|
||||||
<Listbox.Option
|
<Listbox.Option
|
||||||
key={opt.value}
|
key={opt.value}
|
||||||
className={({active}) =>
|
className={({active}) =>
|
||||||
|
|
97
web/src/components/inputs/SelectField.tsx
Normal file
97
web/src/components/inputs/SelectField.tsx
Normal file
|
@ -0,0 +1,97 @@
|
||||||
|
import {Field} from "react-final-form";
|
||||||
|
import {Listbox, Transition} from "@headlessui/react";
|
||||||
|
import {CheckIcon, SelectorIcon} from "@heroicons/react/solid";
|
||||||
|
import React, {Fragment} from "react";
|
||||||
|
import {classNames} from "../../styles/utils";
|
||||||
|
|
||||||
|
|
||||||
|
interface SelectOption {
|
||||||
|
label: string;
|
||||||
|
value: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface props {
|
||||||
|
name: string;
|
||||||
|
label: string;
|
||||||
|
optionDefaultText: string;
|
||||||
|
options: SelectOption[];
|
||||||
|
}
|
||||||
|
|
||||||
|
function SelectField({name, label, optionDefaultText, options}: props) {
|
||||||
|
return (
|
||||||
|
<div className="col-span-6 sm:col-span-6">
|
||||||
|
<Field
|
||||||
|
name={name}
|
||||||
|
type="select"
|
||||||
|
render={({input}) => (
|
||||||
|
<Listbox value={input.value} onChange={input.onChange}>
|
||||||
|
{({open}) => (
|
||||||
|
<div
|
||||||
|
className="space-y-1 px-4 sm:space-y-0 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-6 sm:py-5">
|
||||||
|
<Listbox.Label
|
||||||
|
className="block text-sm font-medium text-gray-900 sm:mt-px sm:pt-2">{label}</Listbox.Label>
|
||||||
|
<div className="mt-2 relative">
|
||||||
|
<Listbox.Button
|
||||||
|
className="bg-white relative w-full border border-gray-300 rounded-md shadow-sm pl-3 pr-10 py-2 text-left cursor-default focus:outline-none focus:ring-1 focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm">
|
||||||
|
<span
|
||||||
|
className="block truncate">{input.value ? options.find(c => c.value === input.value)!.label : optionDefaultText}</span>
|
||||||
|
<span
|
||||||
|
className="absolute inset-y-0 right-0 flex items-center pr-2 pointer-events-none">
|
||||||
|
<SelectorIcon className="h-5 w-5 text-gray-400" aria-hidden="true"/>
|
||||||
|
</span>
|
||||||
|
</Listbox.Button>
|
||||||
|
|
||||||
|
<Transition
|
||||||
|
show={open}
|
||||||
|
as={Fragment}
|
||||||
|
leave="transition ease-in duration-100"
|
||||||
|
leaveFrom="opacity-100"
|
||||||
|
leaveTo="opacity-0"
|
||||||
|
>
|
||||||
|
<Listbox.Options
|
||||||
|
static
|
||||||
|
className="absolute z-10 mt-1 w-full bg-white shadow-lg max-h-60 rounded-md py-1 text-base ring-1 ring-black ring-opacity-5 overflow-auto focus:outline-none sm:text-sm"
|
||||||
|
>
|
||||||
|
{options.map((opt) => (
|
||||||
|
<Listbox.Option
|
||||||
|
key={opt.value}
|
||||||
|
className={({active}) =>
|
||||||
|
classNames(
|
||||||
|
active ? 'text-white bg-indigo-600' : 'text-gray-900',
|
||||||
|
'cursor-default select-none relative py-2 pl-3 pr-9'
|
||||||
|
)
|
||||||
|
}
|
||||||
|
value={opt.value}
|
||||||
|
>
|
||||||
|
{({selected, active}) => (
|
||||||
|
<>
|
||||||
|
<span className={classNames(selected ? 'font-semibold' : 'font-normal', 'block truncate')}>
|
||||||
|
{opt.label}
|
||||||
|
</span>
|
||||||
|
|
||||||
|
{selected ? (
|
||||||
|
<span
|
||||||
|
className={classNames(
|
||||||
|
active ? 'text-white' : 'text-indigo-600',
|
||||||
|
'absolute inset-y-0 right-0 flex items-center pr-4'
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<CheckIcon className="h-5 w-5" aria-hidden="true"/>
|
||||||
|
</span>
|
||||||
|
) : null}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Listbox.Option>
|
||||||
|
))}
|
||||||
|
</Listbox.Options>
|
||||||
|
</Transition>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</Listbox>
|
||||||
|
)}/>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default SelectField;
|
|
@ -5,3 +5,4 @@ export { default as TextAreaWide } from "./TextAreaWide";
|
||||||
export { default as MultiSelectField } from "./MultiSelectField";
|
export { default as MultiSelectField } from "./MultiSelectField";
|
||||||
export { default as RadioFieldset } from "./RadioFieldset";
|
export { default as RadioFieldset } from "./RadioFieldset";
|
||||||
export { default as SwitchGroup } from "./SwitchGroup";
|
export { default as SwitchGroup } from "./SwitchGroup";
|
||||||
|
export { default as SelectField } from "./SelectField";
|
||||||
|
|
|
@ -69,10 +69,30 @@ export interface radioFieldsetOption {
|
||||||
}
|
}
|
||||||
|
|
||||||
export const DownloadClientTypeOptions: radioFieldsetOption[] = [
|
export const DownloadClientTypeOptions: radioFieldsetOption[] = [
|
||||||
{
|
{label: "qBittorrent", description: "Add torrents directly to qBittorrent", value: DOWNLOAD_CLIENT_TYPES.qBittorrent},
|
||||||
label: "qBittorrent",
|
{label: "Deluge", description: "Add torrents directly to Deluge", value: DOWNLOAD_CLIENT_TYPES.DelugeV1},
|
||||||
description: "Add torrents directly to qBittorrent",
|
{label: "Deluge 2", description: "Add torrents directly to Deluge 2", value: DOWNLOAD_CLIENT_TYPES.DelugeV2},
|
||||||
value: DOWNLOAD_CLIENT_TYPES.qBittorrent
|
|
||||||
},
|
|
||||||
{label: "Deluge", description: "Add torrents directly to Deluge", value: DOWNLOAD_CLIENT_TYPES.Deluge},
|
|
||||||
];
|
];
|
||||||
|
export const DownloadClientTypeNameMap = {
|
||||||
|
"DELUGE_V1": "Deluge v1",
|
||||||
|
"DELUGE_V2": "Deluge v2",
|
||||||
|
"QBITTORRENT": "qBittorrent"
|
||||||
|
};
|
||||||
|
|
||||||
|
export const ActionTypeOptions: radioFieldsetOption[] = [
|
||||||
|
{label: "Test", description: "A simple action to test a filter.", value: "TEST"},
|
||||||
|
{label: "Watch dir", description: "Add filtered torrents to a watch directory", value: "WATCH_FOLDER"},
|
||||||
|
{label: "Exec", description: "Run a custom command after a filter match", value: "EXEC"},
|
||||||
|
{label: "qBittorrent", description: "Add torrents directly to qBittorrent", value: "QBITTORRENT"},
|
||||||
|
{label: "Deluge", description: "Add torrents directly to Deluge", value: "DELUGE_V1"},
|
||||||
|
{label: "Deluge v2", description: "Add torrents directly to Deluge 2", value: "DELUGE_V2"},
|
||||||
|
];
|
||||||
|
|
||||||
|
export const ActionTypeNameMap = {
|
||||||
|
"TEST": "Test",
|
||||||
|
"WATCH_FOLDER": "Watch folder",
|
||||||
|
"EXEC": "Exec",
|
||||||
|
"DELUGE_V1": "Deluge v1",
|
||||||
|
"DELUGE_V2": "Deluge v2",
|
||||||
|
"QBITTORRENT": "qBittorrent"
|
||||||
|
};
|
||||||
|
|
|
@ -64,26 +64,18 @@ export interface Filter {
|
||||||
indexers: Indexer[];
|
indexers: Indexer[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Tracker {
|
export type ActionType = 'TEST' | 'EXEC' | 'WATCH_FOLDER' | 'QBITTORRENT' | 'DELUGE_V1' | 'DELUGE_V2';
|
||||||
id: number;
|
export const ACTIONTYPES: ActionType[] = ['TEST', 'EXEC' , 'WATCH_FOLDER' , 'QBITTORRENT' , 'DELUGE_V1', 'DELUGE_V2'];
|
||||||
name: string;
|
|
||||||
type: string;
|
|
||||||
enabled: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
export type ActionType = 'TEST' | 'EXEC' | 'WATCH_FOLDER' | 'QBITTORRENT' | 'DELUGE';
|
|
||||||
export const ACTIONTYPES: ActionType[] = ['TEST', 'EXEC' , 'WATCH_FOLDER' , 'QBITTORRENT' , 'DELUGE'];
|
|
||||||
|
|
||||||
|
|
||||||
export type DownloadClientType = 'QBITTORRENT' | 'DELUGE';
|
export type DownloadClientType = 'QBITTORRENT' | 'DELUGE_V1' | 'DELUGE_V2';
|
||||||
|
|
||||||
// export const DOWNLOAD_CLIENT_TYPES: DownloadClientType[] = ['QBITTORRENT' , 'DELUGE'];
|
|
||||||
export enum DOWNLOAD_CLIENT_TYPES {
|
export enum DOWNLOAD_CLIENT_TYPES {
|
||||||
qBittorrent = 'QBITTORRENT',
|
qBittorrent = 'QBITTORRENT',
|
||||||
Deluge = 'DELUGE'
|
DelugeV1 = 'DELUGE_V1',
|
||||||
|
DelugeV2 = 'DELUGE_V2'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
export interface DownloadClient {
|
export interface DownloadClient {
|
||||||
id: number;
|
id: number;
|
||||||
name: string;
|
name: string;
|
||||||
|
|
|
@ -9,20 +9,7 @@ import {classNames} from "../../styles/utils";
|
||||||
import {Field, Form} from "react-final-form";
|
import {Field, Form} from "react-final-form";
|
||||||
import DEBUG from "../../components/debug";
|
import DEBUG from "../../components/debug";
|
||||||
import APIClient from "../../api/APIClient";
|
import APIClient from "../../api/APIClient";
|
||||||
|
import {ActionTypeOptions} from "../../domain/constants";
|
||||||
interface radioFieldsetOption {
|
|
||||||
label: string;
|
|
||||||
description: string;
|
|
||||||
value: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
const actionTypeOptions: radioFieldsetOption[] = [
|
|
||||||
{label: "Test", description: "A simple action to test a filter.", value: "TEST"},
|
|
||||||
{label: "Watch dir", description: "Add filtered torrents to a watch directory", value: "WATCH_FOLDER"},
|
|
||||||
{label: "Exec", description: "Run a custom command after a filter match", value: "EXEC"},
|
|
||||||
{label: "qBittorrent", description: "Add torrents directly to qBittorrent", value: "QBITTORRENT"},
|
|
||||||
{label: "Deluge", description: "Add torrents directly to Deluge", value: "DELUGE"},
|
|
||||||
];
|
|
||||||
|
|
||||||
interface props {
|
interface props {
|
||||||
filter: Filter;
|
filter: Filter;
|
||||||
|
@ -393,7 +380,8 @@ function FilterActionAddForm({filter, isOpen, toggle, clients}: props) {
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
case "DELUGE":
|
case "DELUGE_V1":
|
||||||
|
case "DELUGE_V2":
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
{/*TODO choose client*/}
|
{/*TODO choose client*/}
|
||||||
|
@ -729,14 +717,14 @@ function FilterActionAddForm({filter, isOpen, toggle, clients}: props) {
|
||||||
<RadioGroup value={values.type} onChange={input.onChange}>
|
<RadioGroup value={values.type} onChange={input.onChange}>
|
||||||
<RadioGroup.Label className="sr-only">Privacy setting</RadioGroup.Label>
|
<RadioGroup.Label className="sr-only">Privacy setting</RadioGroup.Label>
|
||||||
<div className="bg-white rounded-md -space-y-px">
|
<div className="bg-white rounded-md -space-y-px">
|
||||||
{actionTypeOptions.map((setting, settingIdx) => (
|
{ActionTypeOptions.map((setting, settingIdx) => (
|
||||||
<RadioGroup.Option
|
<RadioGroup.Option
|
||||||
key={setting.value}
|
key={setting.value}
|
||||||
value={setting.value}
|
value={setting.value}
|
||||||
className={({checked}) =>
|
className={({checked}) =>
|
||||||
classNames(
|
classNames(
|
||||||
settingIdx === 0 ? 'rounded-tl-md rounded-tr-md' : '',
|
settingIdx === 0 ? 'rounded-tl-md rounded-tr-md' : '',
|
||||||
settingIdx === actionTypeOptions.length - 1 ? 'rounded-bl-md rounded-br-md' : '',
|
settingIdx === ActionTypeOptions.length - 1 ? 'rounded-bl-md rounded-br-md' : '',
|
||||||
checked ? 'bg-indigo-50 border-indigo-200 z-10' : 'border-gray-200',
|
checked ? 'bg-indigo-50 border-indigo-200 z-10' : 'border-gray-200',
|
||||||
'relative border p-4 flex cursor-pointer focus:outline-none'
|
'relative border p-4 flex cursor-pointer focus:outline-none'
|
||||||
)
|
)
|
||||||
|
|
|
@ -9,20 +9,7 @@ import {classNames} from "../../styles/utils";
|
||||||
import {Field, Form} from "react-final-form";
|
import {Field, Form} from "react-final-form";
|
||||||
import DEBUG from "../../components/debug";
|
import DEBUG from "../../components/debug";
|
||||||
import APIClient from "../../api/APIClient";
|
import APIClient from "../../api/APIClient";
|
||||||
|
import {ActionTypeOptions} from "../../domain/constants";
|
||||||
interface radioFieldsetOption {
|
|
||||||
label: string;
|
|
||||||
description: string;
|
|
||||||
value: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
const actionTypeOptions: radioFieldsetOption[] = [
|
|
||||||
{label: "Test", description: "A simple action to test a filter.", value: "TEST"},
|
|
||||||
{label: "Watch dir", description: "Add filtered torrents to a watch directory", value: "WATCH_FOLDER"},
|
|
||||||
{label: "Exec", description: "Run a custom command after a filter match", value: "EXEC"},
|
|
||||||
{label: "qBittorrent", description: "Add torrents directly to qBittorrent", value: "QBITTORRENT"},
|
|
||||||
{label: "Deluge", description: "Add torrents directly to Deluge", value: "DELUGE"},
|
|
||||||
];
|
|
||||||
|
|
||||||
interface props {
|
interface props {
|
||||||
filter: Filter;
|
filter: Filter;
|
||||||
|
@ -35,7 +22,7 @@ interface props {
|
||||||
function FilterActionUpdateForm({filter, isOpen, toggle, clients, action}: props) {
|
function FilterActionUpdateForm({filter, isOpen, toggle, clients, action}: props) {
|
||||||
const mutation = useMutation((action: Action) => APIClient.actions.update(action), {
|
const mutation = useMutation((action: Action) => APIClient.actions.update(action), {
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
console.log("add action");
|
// console.log("add action");
|
||||||
queryClient.invalidateQueries(['filter', filter.id]);
|
queryClient.invalidateQueries(['filter', filter.id]);
|
||||||
sleep(1500)
|
sleep(1500)
|
||||||
|
|
||||||
|
@ -44,7 +31,7 @@ function FilterActionUpdateForm({filter, isOpen, toggle, clients, action}: props
|
||||||
})
|
})
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
console.log("render add action form", clients)
|
// console.log("render add action form", clients)
|
||||||
}, [clients]);
|
}, [clients]);
|
||||||
|
|
||||||
const onSubmit = (data: any) => {
|
const onSubmit = (data: any) => {
|
||||||
|
@ -399,7 +386,8 @@ function FilterActionUpdateForm({filter, isOpen, toggle, clients, action}: props
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
case "DELUGE":
|
case "DELUGE_V1":
|
||||||
|
case "DELUGE_V2":
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
{/*TODO choose client*/}
|
{/*TODO choose client*/}
|
||||||
|
@ -734,14 +722,14 @@ function FilterActionUpdateForm({filter, isOpen, toggle, clients, action}: props
|
||||||
<RadioGroup value={values.type} onChange={input.onChange}>
|
<RadioGroup value={values.type} onChange={input.onChange}>
|
||||||
<RadioGroup.Label className="sr-only">Privacy setting</RadioGroup.Label>
|
<RadioGroup.Label className="sr-only">Privacy setting</RadioGroup.Label>
|
||||||
<div className="bg-white rounded-md -space-y-px">
|
<div className="bg-white rounded-md -space-y-px">
|
||||||
{actionTypeOptions.map((setting, settingIdx) => (
|
{ActionTypeOptions.map((setting, settingIdx) => (
|
||||||
<RadioGroup.Option
|
<RadioGroup.Option
|
||||||
key={setting.value}
|
key={setting.value}
|
||||||
value={setting.value}
|
value={setting.value}
|
||||||
className={({checked}) =>
|
className={({checked}) =>
|
||||||
classNames(
|
classNames(
|
||||||
settingIdx === 0 ? 'rounded-tl-md rounded-tr-md' : '',
|
settingIdx === 0 ? 'rounded-tl-md rounded-tr-md' : '',
|
||||||
settingIdx === actionTypeOptions.length - 1 ? 'rounded-bl-md rounded-br-md' : '',
|
settingIdx === ActionTypeOptions.length - 1 ? 'rounded-bl-md rounded-br-md' : '',
|
||||||
checked ? 'bg-indigo-50 border-indigo-200 z-10' : 'border-gray-200',
|
checked ? 'bg-indigo-50 border-indigo-200 z-10' : 'border-gray-200',
|
||||||
'relative border p-4 flex cursor-pointer focus:outline-none'
|
'relative border p-4 flex cursor-pointer focus:outline-none'
|
||||||
)
|
)
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import {Fragment, useState} from "react";
|
import React, {Fragment, useState} from "react";
|
||||||
import {useMutation} from "react-query";
|
import {useMutation} from "react-query";
|
||||||
import {DOWNLOAD_CLIENT_TYPES, DownloadClient} from "../../domain/interfaces";
|
import {DOWNLOAD_CLIENT_TYPES, DownloadClient} from "../../domain/interfaces";
|
||||||
import {Dialog, RadioGroup, Transition} from "@headlessui/react";
|
import {Dialog, RadioGroup, Transition} from "@headlessui/react";
|
||||||
|
@ -10,21 +10,7 @@ import {SwitchGroup} from "../../components/inputs";
|
||||||
import {queryClient} from "../../App";
|
import {queryClient} from "../../App";
|
||||||
import APIClient from "../../api/APIClient";
|
import APIClient from "../../api/APIClient";
|
||||||
import {sleep} from "../../utils/utils";
|
import {sleep} from "../../utils/utils";
|
||||||
|
import {DownloadClientTypeOptions} from "../../domain/constants";
|
||||||
interface radioFieldsetOption {
|
|
||||||
label: string;
|
|
||||||
description: string;
|
|
||||||
value: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
const downloadClientTypeOptions: radioFieldsetOption[] = [
|
|
||||||
{
|
|
||||||
label: "qBittorrent",
|
|
||||||
description: "Add torrents directly to qBittorrent",
|
|
||||||
value: DOWNLOAD_CLIENT_TYPES.qBittorrent
|
|
||||||
},
|
|
||||||
{label: "Deluge", description: "Add torrents directly to Deluge", value: DOWNLOAD_CLIENT_TYPES.Deluge},
|
|
||||||
];
|
|
||||||
|
|
||||||
function DownloadClientAddForm({isOpen, toggle}: any) {
|
function DownloadClientAddForm({isOpen, toggle}: any) {
|
||||||
const [isTesting, setIsTesting] = useState(false)
|
const [isTesting, setIsTesting] = useState(false)
|
||||||
|
@ -181,18 +167,18 @@ function DownloadClientAddForm({isOpen, toggle}: any) {
|
||||||
<RadioGroup value={values.type}
|
<RadioGroup value={values.type}
|
||||||
onChange={input.onChange}>
|
onChange={input.onChange}>
|
||||||
<RadioGroup.Label
|
<RadioGroup.Label
|
||||||
className="sr-only">Privacy
|
className="sr-only">Client
|
||||||
setting</RadioGroup.Label>
|
type</RadioGroup.Label>
|
||||||
<div
|
<div
|
||||||
className="bg-white rounded-md -space-y-px">
|
className="bg-white rounded-md -space-y-px">
|
||||||
{downloadClientTypeOptions.map((setting, settingIdx) => (
|
{DownloadClientTypeOptions.map((setting, settingIdx) => (
|
||||||
<RadioGroup.Option
|
<RadioGroup.Option
|
||||||
key={setting.value}
|
key={setting.value}
|
||||||
value={setting.value}
|
value={setting.value}
|
||||||
className={({checked}) =>
|
className={({checked}) =>
|
||||||
classNames(
|
classNames(
|
||||||
settingIdx === 0 ? 'rounded-tl-md rounded-tr-md' : '',
|
settingIdx === 0 ? 'rounded-tl-md rounded-tr-md' : '',
|
||||||
settingIdx === downloadClientTypeOptions.length - 1 ? 'rounded-bl-md rounded-br-md' : '',
|
settingIdx === DownloadClientTypeOptions.length - 1 ? 'rounded-bl-md rounded-br-md' : '',
|
||||||
checked ? 'bg-indigo-50 border-indigo-200 z-10' : 'border-gray-200',
|
checked ? 'bg-indigo-50 border-indigo-200 z-10' : 'border-gray-200',
|
||||||
'relative border p-4 flex cursor-pointer focus:outline-none'
|
'relative border p-4 flex cursor-pointer focus:outline-none'
|
||||||
)
|
)
|
||||||
|
@ -245,7 +231,6 @@ function DownloadClientAddForm({isOpen, toggle}: any) {
|
||||||
</fieldset>
|
</fieldset>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
className="space-y-1 px-4 sm:space-y-0 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-6 sm:py-5">
|
className="space-y-1 px-4 sm:space-y-0 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-6 sm:py-5">
|
||||||
<div>
|
<div>
|
||||||
|
|
|
@ -7,6 +7,7 @@ import {classNames} from "../../styles/utils";
|
||||||
import { DownloadClientAddForm, DownloadClientUpdateForm } from "../../forms";
|
import { DownloadClientAddForm, DownloadClientUpdateForm } from "../../forms";
|
||||||
import EmptySimple from "../../components/empty/EmptySimple";
|
import EmptySimple from "../../components/empty/EmptySimple";
|
||||||
import APIClient from "../../api/APIClient";
|
import APIClient from "../../api/APIClient";
|
||||||
|
import {DownloadClientTypeNameMap} from "../../domain/constants";
|
||||||
|
|
||||||
interface DownloadLClientSettingsListItemProps {
|
interface DownloadLClientSettingsListItemProps {
|
||||||
client: DownloadClient;
|
client: DownloadClient;
|
||||||
|
@ -48,7 +49,7 @@ function DownloadClientSettingsListItem({ client, idx }: DownloadLClientSettings
|
||||||
</Switch>
|
</Switch>
|
||||||
</td>
|
</td>
|
||||||
<td className="px-6 py-4 whitespace-nowrap text-sm font-medium text-gray-900">{client.name}</td>
|
<td className="px-6 py-4 whitespace-nowrap text-sm font-medium text-gray-900">{client.name}</td>
|
||||||
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500">{client.type}</td>
|
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500">{DownloadClientTypeNameMap[client.type]}</td>
|
||||||
<td className="px-6 py-4 whitespace-nowrap text-right text-sm font-medium">
|
<td className="px-6 py-4 whitespace-nowrap text-right text-sm font-medium">
|
||||||
<span className="text-indigo-600 hover:text-indigo-900 cursor-pointer" onClick={toggleUpdateClient}>
|
<span className="text-indigo-600 hover:text-indigo-900 cursor-pointer" onClick={toggleUpdateClient}>
|
||||||
Edit
|
Edit
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue