mirror of
https://github.com/idanoo/autobrr
synced 2025-07-23 08:49:13 +00:00
Feature: Download client rules (#18)
* feat(web): add and update download client rules * feat: add and update download client rules * feat: add active downloads check * chore: update pkg * feat: deluge max active downloads * feat: use basic rules for deluge * feat: add as paused * refactor: download file if needed * feat: better errors qbit
This commit is contained in:
parent
09eb0b1716
commit
c02f16b64d
25 changed files with 628 additions and 228 deletions
2
go.mod
2
go.mod
|
@ -4,7 +4,7 @@ go 1.16
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/anacrolix/torrent v1.29.1
|
github.com/anacrolix/torrent v1.29.1
|
||||||
github.com/gdm85/go-libdeluge v0.5.4
|
github.com/gdm85/go-libdeluge v0.5.5
|
||||||
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/gorilla/websocket v1.4.2
|
github.com/gorilla/websocket v1.4.2
|
||||||
|
|
4
go.sum
4
go.sum
|
@ -222,8 +222,12 @@ github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWo
|
||||||
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 h1:Y2vV6wGwvR5skrFrlntTYAXxaWRzg2HDqrcWyVzuUgo=
|
||||||
github.com/gdm85/go-libdeluge v0.5.4/go.mod h1:Fxm576GtD2fTcSUCSPqJINBZRkY8WrtGf9JfYVRtmD0=
|
github.com/gdm85/go-libdeluge v0.5.4/go.mod h1:Fxm576GtD2fTcSUCSPqJINBZRkY8WrtGf9JfYVRtmD0=
|
||||||
|
github.com/gdm85/go-libdeluge v0.5.5 h1:vQ2wuphJVU8UaVS46gFH6VgpRZjyg9qSsXhxSkWoGcQ=
|
||||||
|
github.com/gdm85/go-libdeluge v0.5.5/go.mod h1:RYJ0orON2gNxGfh3VOpLDv7HFz3a0/14Rh1fCYNowT0=
|
||||||
github.com/gdm85/go-rencode v0.1.6 h1:JbSv//2Og8aeSMUBMDTRNA6JW55iZbLMJU8bp9GqULY=
|
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/gdm85/go-rencode v0.1.6/go.mod h1:0dr3BuaKzeseY1of6o1KRTGB/Oo7eio+YEyz8KDp5+s=
|
||||||
|
github.com/gdm85/go-rencode v0.1.8 h1:7+qxwoQWU1b1nMGcESOyoUR5dzPtRA6yLQpKn7uXmnI=
|
||||||
|
github.com/gdm85/go-rencode v0.1.8/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/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
|
github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
|
||||||
github.com/gin-gonic/gin v1.5.0/go.mod h1:Nd6IXA8m5kNZdNEHMBd93KT+mdY3+bewLgRvmCsR2Do=
|
github.com/gin-gonic/gin v1.5.0/go.mod h1:Nd6IXA8m5kNZdNEHMBd93KT+mdY3+bewLgRvmCsR2Do=
|
||||||
|
|
|
@ -13,14 +13,14 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
func (s *service) deluge(action domain.Action, torrentFile string) error {
|
func (s *service) deluge(action domain.Action, torrentFile string) error {
|
||||||
log.Trace().Msgf("action DELUGE: %v", torrentFile)
|
log.Debug().Msgf("action Deluge: %v", action.Name)
|
||||||
|
|
||||||
var err error
|
var err error
|
||||||
|
|
||||||
// get client for action
|
// get client for action
|
||||||
client, err := s.clientSvc.FindByID(action.ClientID)
|
client, err := s.clientSvc.FindByID(action.ClientID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error().Err(err).Msgf("error finding client: %v", action.ClientID)
|
log.Error().Stack().Err(err).Msgf("error finding client: %v", action.ClientID)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -39,23 +39,109 @@ func (s *service) deluge(action domain.Action, torrentFile string) error {
|
||||||
|
|
||||||
switch client.Type {
|
switch client.Type {
|
||||||
case "DELUGE_V1":
|
case "DELUGE_V1":
|
||||||
err = delugeV1(settings, action, torrentFile)
|
if err = delugeV1(client, settings, action, torrentFile); err != nil {
|
||||||
|
return err
|
||||||
case "DELUGE_V2":
|
|
||||||
err = delugeV2(settings, action, torrentFile)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
case "DELUGE_V2":
|
||||||
|
if err = delugeV2(client, settings, action, torrentFile); err != nil {
|
||||||
return err
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func delugeV1(settings delugeClient.Settings, action domain.Action, torrentFile string) error {
|
func (s *service) delugeCheckRulesCanDownload(action domain.Action) (bool, error) {
|
||||||
|
log.Trace().Msgf("action Deluge: %v check rules", action.Name)
|
||||||
|
|
||||||
|
// get client for action
|
||||||
|
client, err := s.clientSvc.FindByID(action.ClientID)
|
||||||
|
if err != nil {
|
||||||
|
log.Error().Stack().Err(err).Msgf("error finding client: %v ID %v", action.Name, action.ClientID)
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if client == nil {
|
||||||
|
return false, 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,
|
||||||
|
}
|
||||||
|
var deluge delugeClient.DelugeClient
|
||||||
|
|
||||||
|
switch client.Type {
|
||||||
|
case "DELUGE_V1":
|
||||||
|
deluge = delugeClient.NewV1(settings)
|
||||||
|
|
||||||
|
case "DELUGE_V2":
|
||||||
|
deluge = delugeClient.NewV2(settings)
|
||||||
|
}
|
||||||
|
|
||||||
|
// perform connection to Deluge server
|
||||||
|
err = deluge.Connect()
|
||||||
|
if err != nil {
|
||||||
|
log.Error().Stack().Err(err).Msgf("error logging into client: %v %v", client.Name, client.Host)
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
|
||||||
|
defer deluge.Close()
|
||||||
|
|
||||||
|
// check for active downloads and other rules
|
||||||
|
if client.Settings.Rules.Enabled && !action.IgnoreRules {
|
||||||
|
activeDownloads, err := deluge.TorrentsStatus(delugeClient.StateDownloading, nil)
|
||||||
|
if err != nil {
|
||||||
|
log.Error().Stack().Err(err).Msg("Deluge - could not fetch downloading torrents")
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// make sure it's not set to 0 by default
|
||||||
|
if client.Settings.Rules.MaxActiveDownloads > 0 {
|
||||||
|
|
||||||
|
// if max active downloads reached, check speed and if lower than threshold add anyways
|
||||||
|
if len(activeDownloads) >= client.Settings.Rules.MaxActiveDownloads {
|
||||||
|
log.Debug().Msg("max active downloads reached, skipping")
|
||||||
|
return false, nil
|
||||||
|
|
||||||
|
// // TODO handle ignore slow torrents
|
||||||
|
//if client.Settings.Rules.IgnoreSlowTorrents {
|
||||||
|
//
|
||||||
|
// // get session state
|
||||||
|
// // gives type conversion errors
|
||||||
|
// state, err := deluge.GetSessionStatus()
|
||||||
|
// if err != nil {
|
||||||
|
// log.Error().Err(err).Msg("could not get session state")
|
||||||
|
// return err
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// if int64(state.DownloadRate)*1024 >= client.Settings.Rules.DownloadSpeedThreshold {
|
||||||
|
// log.Trace().Msg("max active downloads reached, skip adding")
|
||||||
|
// return nil
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// log.Trace().Msg("active downloads are slower than set limit, lets add it")
|
||||||
|
//}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func delugeV1(client *domain.DownloadClient, settings delugeClient.Settings, action domain.Action, torrentFile string) error {
|
||||||
|
|
||||||
deluge := delugeClient.NewV1(settings)
|
deluge := delugeClient.NewV1(settings)
|
||||||
|
|
||||||
// perform connection to Deluge server
|
// perform connection to Deluge server
|
||||||
err := deluge.Connect()
|
err := deluge.Connect()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error().Err(err).Msgf("error logging into client: %v", settings.Hostname)
|
log.Error().Stack().Err(err).Msgf("error logging into client: %v %v", client.Name, client.Host)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -63,14 +149,14 @@ func delugeV1(settings delugeClient.Settings, action domain.Action, torrentFile
|
||||||
|
|
||||||
t, err := ioutil.ReadFile(torrentFile)
|
t, err := ioutil.ReadFile(torrentFile)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error().Err(err).Msgf("could not read torrent file: %v", torrentFile)
|
log.Error().Stack().Err(err).Msgf("could not read torrent file: %v", torrentFile)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// encode file to base64 before sending to deluge
|
// encode file to base64 before sending to deluge
|
||||||
encodedFile := base64.StdEncoding.EncodeToString(t)
|
encodedFile := base64.StdEncoding.EncodeToString(t)
|
||||||
if encodedFile == "" {
|
if encodedFile == "" {
|
||||||
log.Error().Err(err).Msgf("could not encode torrent file: %v", torrentFile)
|
log.Error().Stack().Err(err).Msgf("could not encode torrent file: %v", torrentFile)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -92,17 +178,18 @@ func delugeV1(settings delugeClient.Settings, action domain.Action, torrentFile
|
||||||
options.MaxUploadSpeed = &maxUL
|
options.MaxUploadSpeed = &maxUL
|
||||||
}
|
}
|
||||||
|
|
||||||
|
log.Trace().Msgf("action Deluge options: %+v", options)
|
||||||
|
|
||||||
torrentHash, err := deluge.AddTorrentFile(torrentFile, encodedFile, &options)
|
torrentHash, err := deluge.AddTorrentFile(torrentFile, encodedFile, &options)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error().Err(err).Msgf("could not add torrent to client: %v", torrentFile)
|
log.Error().Stack().Err(err).Msgf("could not add torrent %v to client: %v", torrentFile, client.Name)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
if action.Label != "" {
|
if action.Label != "" {
|
||||||
|
|
||||||
p, err := deluge.LabelPlugin()
|
p, err := deluge.LabelPlugin()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error().Err(err).Msgf("could not load label plugin: %v", torrentFile)
|
log.Error().Stack().Err(err).Msgf("could not load label plugin: %v", client.Name)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -110,25 +197,25 @@ func delugeV1(settings delugeClient.Settings, action domain.Action, torrentFile
|
||||||
// TODO first check if label exists, if not, add it, otherwise set
|
// TODO first check if label exists, if not, add it, otherwise set
|
||||||
err = p.SetTorrentLabel(torrentHash, action.Label)
|
err = p.SetTorrentLabel(torrentHash, action.Label)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error().Err(err).Msgf("could not set label: %v", torrentFile)
|
log.Error().Stack().Err(err).Msgf("could not set label: %v on client: %v", action.Label, client.Name)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Trace().Msgf("deluge: torrent successfully added! hash: %v", torrentHash)
|
log.Info().Msgf("torrent with hash %v successfully added to client: '%v'", torrentHash, client.Name)
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func delugeV2(settings delugeClient.Settings, action domain.Action, torrentFile string) error {
|
func delugeV2(client *domain.DownloadClient, settings delugeClient.Settings, action domain.Action, torrentFile string) error {
|
||||||
|
|
||||||
deluge := delugeClient.NewV2(settings)
|
deluge := delugeClient.NewV2(settings)
|
||||||
|
|
||||||
// perform connection to Deluge server
|
// perform connection to Deluge server
|
||||||
err := deluge.Connect()
|
err := deluge.Connect()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error().Err(err).Msgf("error logging into client: %v", settings.Hostname)
|
log.Error().Stack().Err(err).Msgf("error logging into client: %v %v", client.Name, client.Host)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -136,14 +223,14 @@ func delugeV2(settings delugeClient.Settings, action domain.Action, torrentFile
|
||||||
|
|
||||||
t, err := ioutil.ReadFile(torrentFile)
|
t, err := ioutil.ReadFile(torrentFile)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error().Err(err).Msgf("could not read torrent file: %v", torrentFile)
|
log.Error().Stack().Err(err).Msgf("could not read torrent file: %v", torrentFile)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// encode file to base64 before sending to deluge
|
// encode file to base64 before sending to deluge
|
||||||
encodedFile := base64.StdEncoding.EncodeToString(t)
|
encodedFile := base64.StdEncoding.EncodeToString(t)
|
||||||
if encodedFile == "" {
|
if encodedFile == "" {
|
||||||
log.Error().Err(err).Msgf("could not encode torrent file: %v", torrentFile)
|
log.Error().Stack().Err(err).Msgf("could not encode torrent file: %v", torrentFile)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -165,17 +252,18 @@ func delugeV2(settings delugeClient.Settings, action domain.Action, torrentFile
|
||||||
options.MaxUploadSpeed = &maxUL
|
options.MaxUploadSpeed = &maxUL
|
||||||
}
|
}
|
||||||
|
|
||||||
|
log.Trace().Msgf("action Deluge options: %+v", options)
|
||||||
|
|
||||||
torrentHash, err := deluge.AddTorrentFile(torrentFile, encodedFile, &options)
|
torrentHash, err := deluge.AddTorrentFile(torrentFile, encodedFile, &options)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error().Err(err).Msgf("could not add torrent to client: %v", torrentFile)
|
log.Error().Stack().Err(err).Msgf("could not add torrent %v to client: %v", torrentFile, client.Name)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
if action.Label != "" {
|
if action.Label != "" {
|
||||||
|
|
||||||
p, err := deluge.LabelPlugin()
|
p, err := deluge.LabelPlugin()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error().Err(err).Msgf("could not load label plugin: %v", torrentFile)
|
log.Error().Stack().Err(err).Msgf("could not load label plugin: %v", client.Name)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -183,13 +271,13 @@ func delugeV2(settings delugeClient.Settings, action domain.Action, torrentFile
|
||||||
// TODO first check if label exists, if not, add it, otherwise set
|
// TODO first check if label exists, if not, add it, otherwise set
|
||||||
err = p.SetTorrentLabel(torrentHash, action.Label)
|
err = p.SetTorrentLabel(torrentHash, action.Label)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error().Err(err).Msgf("could not set label: %v", torrentFile)
|
log.Error().Stack().Err(err).Msgf("could not set label: %v on client: %v", action.Label, client.Name)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Trace().Msgf("deluge: torrent successfully added! hash: %v", torrentHash)
|
log.Info().Msgf("torrent with hash %v successfully added to client: '%v'", torrentHash, client.Name)
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
|
@ -11,12 +11,12 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
func (s *service) execCmd(announce domain.Announce, action domain.Action, torrentFile string) {
|
func (s *service) execCmd(announce domain.Announce, action domain.Action, torrentFile string) {
|
||||||
log.Trace().Msgf("action EXEC: release: %v", announce.TorrentName)
|
log.Debug().Msgf("action exec: %v release: %v", action.Name, announce.TorrentName)
|
||||||
|
|
||||||
// check if program exists
|
// check if program exists
|
||||||
cmd, err := exec.LookPath(action.ExecCmd)
|
cmd, err := exec.LookPath(action.ExecCmd)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error().Err(err).Msgf("exec failed, could not find program: %v", action.ExecCmd)
|
log.Error().Stack().Err(err).Msgf("exec failed, could not find program: %v", action.ExecCmd)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -30,7 +30,7 @@ func (s *service) execCmd(announce domain.Announce, action domain.Action, torren
|
||||||
// parse and replace values in argument string before continuing
|
// parse and replace values in argument string before continuing
|
||||||
parsedArgs, err := m.Parse(action.ExecArgs)
|
parsedArgs, err := m.Parse(action.ExecArgs)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error().Err(err).Msgf("exec failed, could not parse arguments: %v", action.ExecCmd)
|
log.Error().Stack().Err(err).Msgf("exec failed, could not parse arguments: %v", action.ExecCmd)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -46,7 +46,7 @@ func (s *service) execCmd(announce domain.Announce, action domain.Action, torren
|
||||||
output, err := command.CombinedOutput()
|
output, err := command.CombinedOutput()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
// everything other than exit 0 is considered an error
|
// everything other than exit 0 is considered an error
|
||||||
log.Error().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, torrentFile)
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Trace().Msgf("executed command: '%v'", string(output))
|
log.Trace().Msgf("executed command: '%v'", string(output))
|
||||||
|
|
|
@ -9,13 +9,16 @@ import (
|
||||||
"github.com/rs/zerolog/log"
|
"github.com/rs/zerolog/log"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const REANNOUNCE_MAX_ATTEMPTS = 30
|
||||||
|
const REANNOUNCE_INTERVAL = 7000
|
||||||
|
|
||||||
func (s *service) qbittorrent(action domain.Action, hash string, torrentFile string) error {
|
func (s *service) qbittorrent(action domain.Action, hash string, torrentFile string) error {
|
||||||
log.Trace().Msgf("action QBITTORRENT: %v", torrentFile)
|
log.Debug().Msgf("action qBittorrent: %v", action.Name)
|
||||||
|
|
||||||
// get client for action
|
// get client for action
|
||||||
client, err := s.clientSvc.FindByID(action.ClientID)
|
client, err := s.clientSvc.FindByID(action.ClientID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error().Err(err).Msgf("error finding client: %v", action.ClientID)
|
log.Error().Stack().Err(err).Msgf("error finding client: %v ID %v", action.Name, action.ClientID)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -35,12 +38,10 @@ func (s *service) qbittorrent(action domain.Action, hash string, torrentFile str
|
||||||
// save cookies?
|
// save cookies?
|
||||||
err = qbt.Login()
|
err = qbt.Login()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error().Err(err).Msgf("error logging into client: %v", action.ClientID)
|
log.Error().Stack().Err(err).Msgf("error logging into client: %v %v", client.Name, client.Host)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO check for active downloads and other rules
|
|
||||||
|
|
||||||
options := map[string]string{}
|
options := map[string]string{}
|
||||||
|
|
||||||
if action.Paused {
|
if action.Paused {
|
||||||
|
@ -63,67 +64,144 @@ func (s *service) qbittorrent(action domain.Action, hash string, torrentFile str
|
||||||
options["dlLimit"] = strconv.FormatInt(action.LimitDownloadSpeed, 10)
|
options["dlLimit"] = strconv.FormatInt(action.LimitDownloadSpeed, 10)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
log.Trace().Msgf("action qBittorrent options: %+v", options)
|
||||||
|
|
||||||
err = qbt.AddTorrentFromFile(torrentFile, options)
|
err = qbt.AddTorrentFromFile(torrentFile, options)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error().Err(err).Msgf("error sending to client: %v", action.ClientID)
|
log.Error().Stack().Err(err).Msgf("could not add torrent %v to client: %v", torrentFile, client.Name)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
if !action.Paused && hash != "" {
|
if !action.Paused && hash != "" {
|
||||||
err = checkTrackerStatus(*qbt, hash)
|
err = checkTrackerStatus(*qbt, hash)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error().Err(err).Msgf("could not get tracker status for torrent: %v", hash)
|
log.Error().Stack().Err(err).Msgf("could not get tracker status for torrent: %v", hash)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Debug().Msgf("torrent %v successfully added to: %v", hash, client.Name)
|
log.Info().Msgf("torrent with hash %v successfully added to client: '%v'", hash, client.Name)
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *service) qbittorrentCheckRulesCanDownload(action domain.Action) (bool, error) {
|
||||||
|
log.Trace().Msgf("action qBittorrent: %v check rules", action.Name)
|
||||||
|
|
||||||
|
// get client for action
|
||||||
|
client, err := s.clientSvc.FindByID(action.ClientID)
|
||||||
|
if err != nil {
|
||||||
|
log.Error().Stack().Err(err).Msgf("error finding client: %v", action.ClientID)
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if client == nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
|
||||||
|
qbtSettings := qbittorrent.Settings{
|
||||||
|
Hostname: client.Host,
|
||||||
|
Port: uint(client.Port),
|
||||||
|
Username: client.Username,
|
||||||
|
Password: client.Password,
|
||||||
|
SSL: client.SSL,
|
||||||
|
}
|
||||||
|
|
||||||
|
qbt := qbittorrent.NewClient(qbtSettings)
|
||||||
|
// save cookies?
|
||||||
|
err = qbt.Login()
|
||||||
|
if err != nil {
|
||||||
|
log.Error().Stack().Err(err).Msgf("error logging into client: %v", client.Host)
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// check for active downloads and other rules
|
||||||
|
if client.Settings.Rules.Enabled && !action.IgnoreRules {
|
||||||
|
activeDownloads, err := qbt.GetTorrentsFilter(qbittorrent.TorrentFilterDownloading)
|
||||||
|
if err != nil {
|
||||||
|
log.Error().Stack().Err(err).Msg("could not fetch downloading torrents")
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// make sure it's not set to 0 by default
|
||||||
|
if client.Settings.Rules.MaxActiveDownloads > 0 {
|
||||||
|
|
||||||
|
// if max active downloads reached, check speed and if lower than threshold add anyways
|
||||||
|
if len(activeDownloads) >= client.Settings.Rules.MaxActiveDownloads {
|
||||||
|
if client.Settings.Rules.IgnoreSlowTorrents {
|
||||||
|
// check speeds of downloads
|
||||||
|
info, err := qbt.GetTransferInfo()
|
||||||
|
if err != nil {
|
||||||
|
log.Error().Err(err).Msg("could not get transfer info")
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// if current transfer speed is more than threshold return out and skip
|
||||||
|
// DlInfoSpeed is in bytes so lets convert to KB to match DownloadSpeedThreshold
|
||||||
|
if info.DlInfoSpeed/1024 >= client.Settings.Rules.DownloadSpeedThreshold {
|
||||||
|
log.Debug().Msg("max active downloads reached, skipping")
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Debug().Msg("active downloads are slower than set limit, lets add it")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
|
||||||
func checkTrackerStatus(qb qbittorrent.Client, hash string) error {
|
func checkTrackerStatus(qb qbittorrent.Client, hash string) error {
|
||||||
announceOK := false
|
announceOK := false
|
||||||
attempts := 0
|
attempts := 0
|
||||||
|
|
||||||
for attempts < REANNOUNCE_MAX_ATTEMPTS {
|
|
||||||
log.Debug().Msgf("RE-ANNOUNCE %v attempt: %v", hash, attempts)
|
|
||||||
|
|
||||||
// initial sleep to give tracker a head start
|
// initial sleep to give tracker a head start
|
||||||
time.Sleep(REANNOUNCE_INTERVAL * time.Millisecond)
|
time.Sleep(2 * time.Second)
|
||||||
|
|
||||||
|
for attempts < REANNOUNCE_MAX_ATTEMPTS {
|
||||||
|
log.Debug().Msgf("qBittorrent - run re-announce %v attempt: %v", hash, attempts)
|
||||||
|
|
||||||
trackers, err := qb.GetTorrentTrackers(hash)
|
trackers, err := qb.GetTorrentTrackers(hash)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error().Err(err).Msgf("could not get trackers of torrent: %v", hash)
|
log.Error().Err(err).Msgf("qBittorrent - could not get trackers for torrent: %v", hash)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// check if status not working or something else
|
// check if status not working or something else
|
||||||
_, working := findTrackerStatus(trackers, qbittorrent.TrackerStatusOK)
|
working := findTrackerStatus(trackers, qbittorrent.TrackerStatusOK)
|
||||||
|
|
||||||
if !working {
|
if !working {
|
||||||
|
log.Trace().Msgf("qBittorrent - not working yet, lets re-announce %v attempt: %v", hash, attempts)
|
||||||
err = qb.ReAnnounceTorrents([]string{hash})
|
err = qb.ReAnnounceTorrents([]string{hash})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error().Err(err).Msgf("could not get re-announce torrent: %v", hash)
|
log.Error().Err(err).Msgf("qBittorrent - could not get re-announce torrent: %v", hash)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
attempts++
|
attempts++
|
||||||
|
|
||||||
|
// add delay for next run
|
||||||
|
time.Sleep(REANNOUNCE_INTERVAL * time.Millisecond)
|
||||||
|
|
||||||
continue
|
continue
|
||||||
} else {
|
} else {
|
||||||
log.Debug().Msgf("RE-ANNOUNCE %v OK", hash)
|
log.Debug().Msgf("qBittorrent - re-announce for %v OK", hash)
|
||||||
|
|
||||||
announceOK = true
|
announceOK = true
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// add extra delay before delete
|
||||||
|
time.Sleep(30 * time.Second)
|
||||||
|
|
||||||
if !announceOK {
|
if !announceOK {
|
||||||
log.Debug().Msgf("RE-ANNOUNCE %v took too long, deleting torrent", hash)
|
log.Debug().Msgf("qBittorrent - re-announce for %v took too long, deleting torrent", hash)
|
||||||
|
|
||||||
err := qb.DeleteTorrents([]string{hash}, false)
|
err := qb.DeleteTorrents([]string{hash}, false)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error().Err(err).Msgf("could not delete torrent: %v", hash)
|
log.Error().Stack().Err(err).Msgf("qBittorrent - could not delete torrent: %v", hash)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -138,11 +216,14 @@ func checkTrackerStatus(qb qbittorrent.Client, hash string) error {
|
||||||
// 2 Tracker has been contacted and is working
|
// 2 Tracker has been contacted and is working
|
||||||
// 3 Tracker is updating
|
// 3 Tracker is updating
|
||||||
// 4 Tracker has been contacted, but it is not working (or doesn't send proper replies)
|
// 4 Tracker has been contacted, but it is not working (or doesn't send proper replies)
|
||||||
func findTrackerStatus(slice []qbittorrent.TorrentTracker, status qbittorrent.TrackerStatus) (int, bool) {
|
func findTrackerStatus(slice []qbittorrent.TorrentTracker, status qbittorrent.TrackerStatus) bool {
|
||||||
for i, item := range slice {
|
for _, item := range slice {
|
||||||
if item.Status == status {
|
// if updating skip and give some more time
|
||||||
return i, true
|
if item.Status == qbittorrent.TrackerStatusUpdating {
|
||||||
|
return false
|
||||||
|
} else if item.Status == status {
|
||||||
|
return true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return -1, false
|
return false
|
||||||
}
|
}
|
||||||
|
|
201
internal/action/run.go
Normal file
201
internal/action/run.go
Normal file
|
@ -0,0 +1,201 @@
|
||||||
|
package action
|
||||||
|
|
||||||
|
import (
|
||||||
|
"io"
|
||||||
|
"os"
|
||||||
|
"path"
|
||||||
|
|
||||||
|
"github.com/anacrolix/torrent/metainfo"
|
||||||
|
"github.com/rs/zerolog/log"
|
||||||
|
|
||||||
|
"github.com/autobrr/autobrr/internal/client"
|
||||||
|
"github.com/autobrr/autobrr/internal/domain"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (s *service) RunActions(actions []domain.Action, announce domain.Announce) error {
|
||||||
|
|
||||||
|
var err error
|
||||||
|
var tmpFile string
|
||||||
|
var hash string
|
||||||
|
|
||||||
|
for _, action := range actions {
|
||||||
|
if !action.Enabled {
|
||||||
|
// only run active actions
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Debug().Msgf("process action: %v", action.Name)
|
||||||
|
|
||||||
|
switch action.Type {
|
||||||
|
case domain.ActionTypeTest:
|
||||||
|
s.test(action.Name)
|
||||||
|
|
||||||
|
case domain.ActionTypeExec:
|
||||||
|
if tmpFile == "" {
|
||||||
|
tmpFile, hash, err = downloadFile(announce.TorrentUrl)
|
||||||
|
if err != nil {
|
||||||
|
log.Error().Stack().Err(err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
go func(announce domain.Announce, action domain.Action, tmpFile string) {
|
||||||
|
s.execCmd(announce, action, tmpFile)
|
||||||
|
}(announce, action, tmpFile)
|
||||||
|
|
||||||
|
case domain.ActionTypeWatchFolder:
|
||||||
|
if tmpFile == "" {
|
||||||
|
tmpFile, hash, err = downloadFile(announce.TorrentUrl)
|
||||||
|
if err != nil {
|
||||||
|
log.Error().Stack().Err(err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
s.watchFolder(action.WatchFolder, tmpFile)
|
||||||
|
|
||||||
|
case domain.ActionTypeDelugeV1, domain.ActionTypeDelugeV2:
|
||||||
|
canDownload, err := s.delugeCheckRulesCanDownload(action)
|
||||||
|
if err != nil {
|
||||||
|
log.Error().Stack().Err(err).Msgf("error checking client rules: %v", action.Name)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if canDownload {
|
||||||
|
if tmpFile == "" {
|
||||||
|
tmpFile, hash, err = downloadFile(announce.TorrentUrl)
|
||||||
|
if err != nil {
|
||||||
|
log.Error().Stack().Err(err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
go func(action domain.Action, tmpFile string) {
|
||||||
|
err = s.deluge(action, tmpFile)
|
||||||
|
if err != nil {
|
||||||
|
log.Error().Stack().Err(err).Msg("error sending torrent to Deluge")
|
||||||
|
}
|
||||||
|
}(action, tmpFile)
|
||||||
|
}
|
||||||
|
|
||||||
|
case domain.ActionTypeQbittorrent:
|
||||||
|
canDownload, err := s.qbittorrentCheckRulesCanDownload(action)
|
||||||
|
if err != nil {
|
||||||
|
log.Error().Stack().Err(err).Msgf("error checking client rules: %v", action.Name)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if canDownload {
|
||||||
|
if tmpFile == "" {
|
||||||
|
tmpFile, hash, err = downloadFile(announce.TorrentUrl)
|
||||||
|
if err != nil {
|
||||||
|
log.Error().Stack().Err(err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
go func(action domain.Action, hash string, tmpFile string) {
|
||||||
|
err = s.qbittorrent(action, hash, tmpFile)
|
||||||
|
if err != nil {
|
||||||
|
log.Error().Stack().Err(err).Msg("error sending torrent to qBittorrent")
|
||||||
|
}
|
||||||
|
}(action, hash, tmpFile)
|
||||||
|
}
|
||||||
|
|
||||||
|
case domain.ActionTypeRadarr:
|
||||||
|
go func(announce domain.Announce, action domain.Action) {
|
||||||
|
err = s.radarr(announce, action)
|
||||||
|
if err != nil {
|
||||||
|
log.Error().Stack().Err(err).Msg("error sending torrent to radarr")
|
||||||
|
//continue
|
||||||
|
}
|
||||||
|
}(announce, action)
|
||||||
|
|
||||||
|
case domain.ActionTypeSonarr:
|
||||||
|
go func(announce domain.Announce, action domain.Action) {
|
||||||
|
err = s.sonarr(announce, action)
|
||||||
|
if err != nil {
|
||||||
|
log.Error().Stack().Err(err).Msg("error sending torrent to sonarr")
|
||||||
|
//continue
|
||||||
|
}
|
||||||
|
}(announce, action)
|
||||||
|
|
||||||
|
case domain.ActionTypeLidarr:
|
||||||
|
go func(announce domain.Announce, action domain.Action) {
|
||||||
|
err = s.lidarr(announce, action)
|
||||||
|
if err != nil {
|
||||||
|
log.Error().Stack().Err(err).Msg("error sending torrent to lidarr")
|
||||||
|
//continue
|
||||||
|
}
|
||||||
|
}(announce, action)
|
||||||
|
|
||||||
|
default:
|
||||||
|
log.Warn().Msgf("unsupported action: %v type: %v", action.Name, action.Type)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// safe to delete tmp file
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// downloadFile returns tmpFile, hash, error
|
||||||
|
func downloadFile(url string) (string, string, error) {
|
||||||
|
// create http client
|
||||||
|
c := client.NewHttpClient()
|
||||||
|
|
||||||
|
// download torrent file
|
||||||
|
// TODO check extra headers, cookie
|
||||||
|
res, err := c.DownloadFile(url, nil)
|
||||||
|
if err != nil {
|
||||||
|
log.Error().Stack().Err(err).Msgf("could not download file: %v", url)
|
||||||
|
return "", "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
// match more filters like torrent size
|
||||||
|
|
||||||
|
// Get meta info from file to find out the hash for later use
|
||||||
|
meta, err := metainfo.LoadFromFile(res.FileName)
|
||||||
|
//meta, err := metainfo.Load(res.Body)
|
||||||
|
if err != nil {
|
||||||
|
log.Error().Stack().Err(err).Msgf("metainfo could not open file: %v", res.FileName)
|
||||||
|
return "", "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
// torrent info hash used for re-announce
|
||||||
|
hash := meta.HashInfoBytes().String()
|
||||||
|
|
||||||
|
return res.FileName, hash, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
|
|
||||||
|
// Open original file
|
||||||
|
original, err := os.Open(torrentFile)
|
||||||
|
if err != nil {
|
||||||
|
log.Error().Stack().Err(err).Msgf("could not open temp file '%v'", torrentFile)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer original.Close()
|
||||||
|
|
||||||
|
_, tmpFileName := path.Split(torrentFile)
|
||||||
|
fullFileName := path.Join(dir, tmpFileName)
|
||||||
|
|
||||||
|
// Create new file
|
||||||
|
newFile, err := os.Create(fullFileName)
|
||||||
|
if err != nil {
|
||||||
|
log.Error().Stack().Err(err).Msgf("could not create new temp file '%v'", fullFileName)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer newFile.Close()
|
||||||
|
|
||||||
|
// Copy file
|
||||||
|
_, err = io.Copy(newFile, original)
|
||||||
|
if err != nil {
|
||||||
|
log.Error().Stack().Err(err).Msgf("could not copy file %v to watch folder", fullFileName)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Info().Msgf("saved file to watch folder: %v", fullFileName)
|
||||||
|
}
|
|
@ -1,24 +1,17 @@
|
||||||
package action
|
package action
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"io"
|
|
||||||
"os"
|
|
||||||
"path"
|
|
||||||
|
|
||||||
"github.com/autobrr/autobrr/internal/domain"
|
"github.com/autobrr/autobrr/internal/domain"
|
||||||
"github.com/autobrr/autobrr/internal/download_client"
|
"github.com/autobrr/autobrr/internal/download_client"
|
||||||
"github.com/rs/zerolog/log"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
const REANNOUNCE_MAX_ATTEMPTS = 30
|
|
||||||
const REANNOUNCE_INTERVAL = 7000
|
|
||||||
|
|
||||||
type Service interface {
|
type Service interface {
|
||||||
RunActions(torrentFile string, hash string, filter domain.Filter, announce domain.Announce) error
|
|
||||||
Store(action domain.Action) (*domain.Action, error)
|
Store(action domain.Action) (*domain.Action, error)
|
||||||
Fetch() ([]domain.Action, error)
|
Fetch() ([]domain.Action, error)
|
||||||
Delete(actionID int) error
|
Delete(actionID int) error
|
||||||
ToggleEnabled(actionID int) error
|
ToggleEnabled(actionID int) error
|
||||||
|
|
||||||
|
RunActions(actions []domain.Action, announce domain.Announce) error
|
||||||
}
|
}
|
||||||
|
|
||||||
type service struct {
|
type service struct {
|
||||||
|
@ -30,72 +23,6 @@ func NewService(repo domain.ActionRepo, clientSvc download_client.Service) Servi
|
||||||
return &service{repo: repo, clientSvc: clientSvc}
|
return &service{repo: repo, clientSvc: clientSvc}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *service) RunActions(torrentFile string, hash string, filter domain.Filter, announce domain.Announce) error {
|
|
||||||
for _, action := range filter.Actions {
|
|
||||||
if !action.Enabled {
|
|
||||||
// only run active actions
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
log.Trace().Msgf("process action: %v", action.Name)
|
|
||||||
|
|
||||||
switch action.Type {
|
|
||||||
case domain.ActionTypeTest:
|
|
||||||
go s.test(torrentFile)
|
|
||||||
|
|
||||||
case domain.ActionTypeWatchFolder:
|
|
||||||
go s.watchFolder(action.WatchFolder, torrentFile)
|
|
||||||
|
|
||||||
case domain.ActionTypeQbittorrent:
|
|
||||||
go func() {
|
|
||||||
err := s.qbittorrent(action, hash, torrentFile)
|
|
||||||
if err != nil {
|
|
||||||
log.Error().Err(err).Msg("error sending torrent to client")
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
case domain.ActionTypeExec:
|
|
||||||
go s.execCmd(announce, action, torrentFile)
|
|
||||||
|
|
||||||
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")
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
case domain.ActionTypeRadarr:
|
|
||||||
go func() {
|
|
||||||
err := s.radarr(announce, action)
|
|
||||||
if err != nil {
|
|
||||||
log.Error().Err(err).Msg("error sending torrent to radarr")
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
case domain.ActionTypeSonarr:
|
|
||||||
go func() {
|
|
||||||
err := s.sonarr(announce, action)
|
|
||||||
if err != nil {
|
|
||||||
log.Error().Err(err).Msg("error sending torrent to sonarr")
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
case domain.ActionTypeLidarr:
|
|
||||||
go func() {
|
|
||||||
err := s.lidarr(announce, action)
|
|
||||||
if err != nil {
|
|
||||||
log.Error().Err(err).Msg("error sending torrent to lidarr")
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
default:
|
|
||||||
log.Warn().Msgf("unsupported action: %v type: %v", action.Name, action.Type)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *service) Store(action domain.Action) (*domain.Action, error) {
|
func (s *service) Store(action domain.Action) (*domain.Action, error) {
|
||||||
// validate data
|
// validate data
|
||||||
|
|
||||||
|
@ -131,36 +58,3 @@ func (s *service) ToggleEnabled(actionID int) error {
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *service) test(torrentFile string) {
|
|
||||||
log.Info().Msgf("action TEST: %v", torrentFile)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *service) watchFolder(dir string, torrentFile string) {
|
|
||||||
log.Trace().Msgf("action WATCH_FOLDER: %v file: %v", dir, torrentFile)
|
|
||||||
|
|
||||||
// Open original file
|
|
||||||
original, err := os.Open(torrentFile)
|
|
||||||
if err != nil {
|
|
||||||
log.Fatal().Err(err)
|
|
||||||
}
|
|
||||||
defer original.Close()
|
|
||||||
|
|
||||||
_, tmpFileName := path.Split(torrentFile)
|
|
||||||
fullFileName := path.Join(dir, tmpFileName)
|
|
||||||
|
|
||||||
// Create new file
|
|
||||||
newFile, err := os.Create(fullFileName)
|
|
||||||
if err != nil {
|
|
||||||
log.Fatal().Err(err)
|
|
||||||
}
|
|
||||||
defer newFile.Close()
|
|
||||||
|
|
||||||
// Copy file
|
|
||||||
_, err = io.Copy(newFile, original)
|
|
||||||
if err != nil {
|
|
||||||
log.Fatal().Err(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
log.Info().Msgf("saved file to watch folder: %v", fullFileName)
|
|
||||||
}
|
|
||||||
|
|
|
@ -75,7 +75,7 @@ func (s *service) Parse(announceID string, msg string) error {
|
||||||
|
|
||||||
log.Trace().Msgf("announce: %+v", announce)
|
log.Trace().Msgf("announce: %+v", announce)
|
||||||
|
|
||||||
log.Info().Msgf("Matched %v (%v) for %v", announce.TorrentName, announce.Filter.Name, announce.Site)
|
log.Info().Msgf("Matched '%v' (%v) for %v", announce.TorrentName, announce.Filter.Name, announce.Site)
|
||||||
|
|
||||||
// match release
|
// match release
|
||||||
|
|
||||||
|
|
|
@ -3,6 +3,7 @@ package client
|
||||||
import (
|
import (
|
||||||
"crypto/md5"
|
"crypto/md5"
|
||||||
"encoding/hex"
|
"encoding/hex"
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
@ -40,8 +41,6 @@ func (c *HttpClient) DownloadFile(url string, opts map[string]string) (*Download
|
||||||
hashString := hex.EncodeToString(hash[:])
|
hashString := hex.EncodeToString(hash[:])
|
||||||
tmpFileName := fmt.Sprintf("/tmp/%v", hashString)
|
tmpFileName := fmt.Sprintf("/tmp/%v", hashString)
|
||||||
|
|
||||||
log.Debug().Msgf("tmpFileName: %v", tmpFileName)
|
|
||||||
|
|
||||||
// Create the file
|
// Create the file
|
||||||
out, err := os.Create(tmpFileName)
|
out, err := os.Create(tmpFileName)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -61,8 +60,6 @@ func (c *HttpClient) DownloadFile(url string, opts map[string]string) (*Download
|
||||||
|
|
||||||
// retry logic
|
// retry logic
|
||||||
|
|
||||||
log.Trace().Msgf("downloaded file response: %v - status: %v", resp.Status, resp.StatusCode)
|
|
||||||
|
|
||||||
if resp.StatusCode != 200 {
|
if resp.StatusCode != 200 {
|
||||||
log.Error().Stack().Err(err).Msgf("error downloading file: %v - bad status: %d", tmpFileName, resp.StatusCode)
|
log.Error().Stack().Err(err).Msgf("error downloading file: %v - bad status: %d", tmpFileName, resp.StatusCode)
|
||||||
return nil, err
|
return nil, err
|
||||||
|
@ -82,7 +79,12 @@ func (c *HttpClient) DownloadFile(url string, opts map[string]string) (*Download
|
||||||
FileName: tmpFileName,
|
FileName: tmpFileName,
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Trace().Msgf("successfully downloaded file: %v", tmpFileName)
|
if res.FileName == "" || res.Body == nil {
|
||||||
|
log.Error().Stack().Err(err).Msgf("tmp file error - empty body: %v", url)
|
||||||
|
return nil, errors.New("error downloading file, no tmp file")
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Debug().Msgf("successfully downloaded file: %v", tmpFileName)
|
||||||
|
|
||||||
return &res, nil
|
return &res, nil
|
||||||
}
|
}
|
||||||
|
|
|
@ -91,6 +91,7 @@ func (r *DownloadClientRepo) Store(client domain.DownloadClient) (*domain.Downlo
|
||||||
settings := domain.DownloadClientSettings{
|
settings := domain.DownloadClientSettings{
|
||||||
APIKey: client.Settings.APIKey,
|
APIKey: client.Settings.APIKey,
|
||||||
Basic: client.Settings.Basic,
|
Basic: client.Settings.Basic,
|
||||||
|
Rules: client.Settings.Rules,
|
||||||
}
|
}
|
||||||
|
|
||||||
settingsJson, err := json.Marshal(&settings)
|
settingsJson, err := json.Marshal(&settings)
|
||||||
|
@ -162,6 +163,8 @@ func (r *DownloadClientRepo) Store(client domain.DownloadClient) (*domain.Downlo
|
||||||
|
|
||||||
resId, _ := res.LastInsertId()
|
resId, _ := res.LastInsertId()
|
||||||
client.ID = int(resId)
|
client.ID = int(resId)
|
||||||
|
|
||||||
|
log.Trace().Msgf("download_client: store new record %d", client.ID)
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Info().Msgf("store download client: %v", client.Name)
|
log.Info().Msgf("store download client: %v", client.Name)
|
||||||
|
|
|
@ -24,6 +24,14 @@ type DownloadClient struct {
|
||||||
type DownloadClientSettings struct {
|
type DownloadClientSettings struct {
|
||||||
APIKey string `json:"apikey,omitempty"`
|
APIKey string `json:"apikey,omitempty"`
|
||||||
Basic BasicAuth `json:"basic,omitempty"`
|
Basic BasicAuth `json:"basic,omitempty"`
|
||||||
|
Rules DownloadClientRules `json:"rules,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type DownloadClientRules struct {
|
||||||
|
Enabled bool `json:"enabled"`
|
||||||
|
MaxActiveDownloads int `json:"max_active_downloads"`
|
||||||
|
IgnoreSlowTorrents bool `json:"ignore_slow_torrents"`
|
||||||
|
DownloadSpeedThreshold int64 `json:"download_speed_threshold"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type BasicAuth struct {
|
type BasicAuth struct {
|
||||||
|
|
|
@ -46,8 +46,6 @@ func (s *service) Store(client domain.DownloadClient) (*domain.DownloadClient, e
|
||||||
// validate data
|
// validate data
|
||||||
if client.Host == "" {
|
if client.Host == "" {
|
||||||
return nil, errors.New("validation error: no 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 == "" {
|
} else if client.Type == "" {
|
||||||
return nil, errors.New("validation error: no type")
|
return nil, errors.New("validation error: no type")
|
||||||
}
|
}
|
||||||
|
@ -75,8 +73,6 @@ func (s *service) Test(client domain.DownloadClient) error {
|
||||||
// basic validation of client
|
// basic validation of client
|
||||||
if client.Host == "" {
|
if client.Host == "" {
|
||||||
return errors.New("validation error: no host")
|
return errors.New("validation error: no host")
|
||||||
} else if client.Port == 0 {
|
|
||||||
return errors.New("validation error: no port")
|
|
||||||
} else if client.Type == "" {
|
} else if client.Type == "" {
|
||||||
return errors.New("validation error: no type")
|
return errors.New("validation error: no type")
|
||||||
}
|
}
|
||||||
|
|
|
@ -98,8 +98,8 @@ func (s *service) FindByIndexerIdentifier(announce domain.Announce) (*domain.Fil
|
||||||
// if match, return the filter
|
// if match, return the filter
|
||||||
matchedFilter := s.checkFilter(filter, announce)
|
matchedFilter := s.checkFilter(filter, announce)
|
||||||
if matchedFilter {
|
if matchedFilter {
|
||||||
log.Trace().Msgf("found filter: %+v", &filter)
|
log.Trace().Msgf("found matching filter: %+v", &filter)
|
||||||
log.Debug().Msgf("found filter: %+v", &filter.Name)
|
log.Debug().Msgf("found matching filter: %v", &filter.Name)
|
||||||
|
|
||||||
// find actions and attach
|
// find actions and attach
|
||||||
actions, err := s.actionRepo.FindByFilterID(filter.ID)
|
actions, err := s.actionRepo.FindByFilterID(filter.ID)
|
||||||
|
|
|
@ -1,14 +1,11 @@
|
||||||
package release
|
package release
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
"github.com/anacrolix/torrent/metainfo"
|
|
||||||
"github.com/rs/zerolog/log"
|
"github.com/rs/zerolog/log"
|
||||||
|
|
||||||
"github.com/autobrr/autobrr/internal/action"
|
"github.com/autobrr/autobrr/internal/action"
|
||||||
"github.com/autobrr/autobrr/internal/client"
|
|
||||||
"github.com/autobrr/autobrr/internal/domain"
|
"github.com/autobrr/autobrr/internal/domain"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -31,54 +28,14 @@ func (s *service) Process(announce domain.Announce) error {
|
||||||
return fmt.Errorf("no actions for filter: %v", announce.Filter.Name)
|
return fmt.Errorf("no actions for filter: %v", announce.Filter.Name)
|
||||||
}
|
}
|
||||||
|
|
||||||
// check can download
|
|
||||||
// smart episode?
|
// smart episode?
|
||||||
// check against rules like active downloading torrents
|
|
||||||
|
|
||||||
// create http client
|
// run actions (watchFolder, test, exec, qBittorrent, Deluge etc.)
|
||||||
c := client.NewHttpClient()
|
err := s.actionSvc.RunActions(announce.Filter.Actions, announce)
|
||||||
|
|
||||||
// download torrent file
|
|
||||||
// TODO check extra headers, cookie
|
|
||||||
res, err := c.DownloadFile(announce.TorrentUrl, nil)
|
|
||||||
if err != nil {
|
|
||||||
log.Error().Stack().Err(err).Msgf("could not download file: %v", announce.TorrentName)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
if res.FileName == "" {
|
|
||||||
return errors.New("error downloading file, no tmp file")
|
|
||||||
}
|
|
||||||
|
|
||||||
if res.Body == nil {
|
|
||||||
log.Error().Stack().Err(err).Msgf("tmp file error - empty body: %v", announce.TorrentName)
|
|
||||||
return errors.New("empty body")
|
|
||||||
}
|
|
||||||
|
|
||||||
//log.Debug().Msgf("downloaded torrent file: %v", res.FileName)
|
|
||||||
|
|
||||||
// onTorrentDownloaded
|
|
||||||
|
|
||||||
// match more filters like torrent size
|
|
||||||
|
|
||||||
// Get meta info from file to find out the hash for later use
|
|
||||||
meta, err := metainfo.LoadFromFile(res.FileName)
|
|
||||||
if err != nil {
|
|
||||||
log.Error().Err(err).Msgf("metainfo could not open file: %v", res.FileName)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// torrent info hash used for re-announce
|
|
||||||
hash := meta.HashInfoBytes().String()
|
|
||||||
|
|
||||||
// take action (watchFolder, test, runProgram, qBittorrent, Deluge etc)
|
|
||||||
err = s.actionSvc.RunActions(res.FileName, hash, *announce.Filter, announce)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error().Stack().Err(err).Msgf("error running actions for filter: %v", announce.Filter.Name)
|
log.Error().Stack().Err(err).Msgf("error running actions for filter: %v", announce.Filter.Name)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// safe to delete tmp file
|
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
|
@ -13,7 +13,6 @@ import (
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/rs/zerolog/log"
|
"github.com/rs/zerolog/log"
|
||||||
|
|
||||||
"golang.org/x/net/publicsuffix"
|
"golang.org/x/net/publicsuffix"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
@ -177,3 +177,42 @@ const (
|
||||||
// 4 Tracker has been contacted, but it is not working (or doesn't send proper replies)
|
// 4 Tracker has been contacted, but it is not working (or doesn't send proper replies)
|
||||||
TrackerStatusNotWorking TrackerStatus = 4
|
TrackerStatusNotWorking TrackerStatus = 4
|
||||||
)
|
)
|
||||||
|
|
||||||
|
type ConnectionStatus string
|
||||||
|
|
||||||
|
const (
|
||||||
|
ConnectionStatusConnected = "connected"
|
||||||
|
ConnectionStatusFirewalled = "firewalled"
|
||||||
|
ConnectionStatusDisconnected = "disconnected"
|
||||||
|
)
|
||||||
|
|
||||||
|
// TransferInfo
|
||||||
|
//
|
||||||
|
// https://github.com/qbittorrent/qBittorrent/wiki/WebUI-API-(qBittorrent-4.1)#get-global-transfer-info
|
||||||
|
//
|
||||||
|
// dl_info_speed integer Global download rate (bytes/s)
|
||||||
|
//
|
||||||
|
// dl_info_data integer Data downloaded this session (bytes)
|
||||||
|
//
|
||||||
|
// up_info_speed integer Global upload rate (bytes/s)
|
||||||
|
//
|
||||||
|
// up_info_data integer Data uploaded this session (bytes)
|
||||||
|
//
|
||||||
|
// dl_rate_limit integer Download rate limit (bytes/s)
|
||||||
|
//
|
||||||
|
// up_rate_limit integer Upload rate limit (bytes/s)
|
||||||
|
//
|
||||||
|
// dht_nodes integer DHT nodes connected to
|
||||||
|
//
|
||||||
|
// connection_status string Connection status. See possible values here below
|
||||||
|
//
|
||||||
|
type TransferInfo struct {
|
||||||
|
ConnectionStatus ConnectionStatus `json:"connection_status"`
|
||||||
|
DHTNodes int64 `json:"dht_nodes"`
|
||||||
|
DlInfoData int64 `json:"dl_info_data"`
|
||||||
|
DlInfoSpeed int64 `json:"dl_info_speed"`
|
||||||
|
DlRateLimit int64 `json:"dl_rate_limit"`
|
||||||
|
UpInfoData int64 `json:"up_info_data"`
|
||||||
|
UpInfoSpeed int64 `json:"up_info_speed"`
|
||||||
|
UpRateLimit int64 `json:"up_rate_limit"`
|
||||||
|
}
|
||||||
|
|
|
@ -27,8 +27,8 @@ func (c *Client) Login() error {
|
||||||
return err
|
return err
|
||||||
|
|
||||||
} else if resp.StatusCode != http.StatusOK { // check for correct status code
|
} else if resp.StatusCode != http.StatusOK { // check for correct status code
|
||||||
log.Error().Err(err).Msg("login bad status error")
|
log.Error().Err(err).Msgf("login bad status %v error", resp.StatusCode)
|
||||||
return err
|
return errors.New("qbittorrent login bad status")
|
||||||
}
|
}
|
||||||
|
|
||||||
defer resp.Body.Close()
|
defer resp.Body.Close()
|
||||||
|
@ -220,3 +220,29 @@ func (c *Client) ReAnnounceTorrents(hashes []string) error {
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (c *Client) GetTransferInfo() (*TransferInfo, error) {
|
||||||
|
var info TransferInfo
|
||||||
|
|
||||||
|
resp, err := c.get("transfer/info", nil)
|
||||||
|
if err != nil {
|
||||||
|
log.Error().Err(err).Msg("get torrents error")
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
body, readErr := ioutil.ReadAll(resp.Body)
|
||||||
|
if readErr != nil {
|
||||||
|
log.Error().Err(err).Msg("get torrents read error")
|
||||||
|
return nil, readErr
|
||||||
|
}
|
||||||
|
|
||||||
|
err = json.Unmarshal(body, &info)
|
||||||
|
if err != nil {
|
||||||
|
log.Error().Err(err).Msg("get torrents unmarshal error")
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &info, nil
|
||||||
|
}
|
||||||
|
|
|
@ -10,7 +10,7 @@ import {
|
||||||
import { useToggle } from "../hooks/hooks";
|
import { useToggle } from "../hooks/hooks";
|
||||||
import { useMutation } from "react-query";
|
import { useMutation } from "react-query";
|
||||||
import { Field, Form } from "react-final-form";
|
import { Field, Form } from "react-final-form";
|
||||||
import { TextField } from "./inputs";
|
import { SwitchGroup, TextField } from "./inputs";
|
||||||
import { NumberField, SelectField } from "./inputs/compact";
|
import { NumberField, SelectField } from "./inputs/compact";
|
||||||
import DEBUG from "./debug";
|
import DEBUG from "./debug";
|
||||||
import APIClient from "../api/APIClient";
|
import APIClient from "../api/APIClient";
|
||||||
|
@ -281,6 +281,12 @@ function ListItem({ action, clients, filterID, idx }: ListItemProps) {
|
||||||
label="Limit upload speed (KB/s)"
|
label="Limit upload speed (KB/s)"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div className="mt-6 grid grid-cols-12 gap-6">
|
||||||
|
<div className="col-span-6">
|
||||||
|
<SwitchGroup name="paused" label="Add paused" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
case "DELUGE_V1":
|
case "DELUGE_V1":
|
||||||
|
@ -313,6 +319,12 @@ function ListItem({ action, clients, filterID, idx }: ListItemProps) {
|
||||||
label="Limit upload speed (KB/s)"
|
label="Limit upload speed (KB/s)"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div className="mt-6 grid grid-cols-12 gap-6">
|
||||||
|
<div className="col-span-6">
|
||||||
|
<SwitchGroup name="paused" label="Add paused" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
case "RADARR":
|
case "RADARR":
|
||||||
|
|
|
@ -6,6 +6,7 @@ import { classNames } from "../../../styles/utils";
|
||||||
interface Props {
|
interface Props {
|
||||||
name: string;
|
name: string;
|
||||||
label?: string;
|
label?: string;
|
||||||
|
help?: string;
|
||||||
placeholder?: string;
|
placeholder?: string;
|
||||||
defaultValue?: number;
|
defaultValue?: number;
|
||||||
className?: string;
|
className?: string;
|
||||||
|
@ -17,6 +18,7 @@ const NumberFieldWide: React.FC<Props> = ({
|
||||||
name,
|
name,
|
||||||
label,
|
label,
|
||||||
placeholder,
|
placeholder,
|
||||||
|
help,
|
||||||
defaultValue,
|
defaultValue,
|
||||||
required,
|
required,
|
||||||
hidden,
|
hidden,
|
||||||
|
@ -51,6 +53,9 @@ const NumberFieldWide: React.FC<Props> = ({
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
{help && (
|
||||||
|
<p className="mt-2 text-sm text-gray-500" id={`${name}-description`}>{help}</p>
|
||||||
|
)}
|
||||||
<Error name={name} classNames="block text-red-500 mt-2" />
|
<Error name={name} classNames="block text-red-500 mt-2" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -10,7 +10,7 @@ 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";
|
import { ActionTypeOptions } from "../../domain/constants";
|
||||||
import { TextFieldWide } from "../../components/inputs";
|
import { SwitchGroup, TextFieldWide } from "../../components/inputs";
|
||||||
import { AlertWarning } from "../../components/alerts";
|
import { AlertWarning } from "../../components/alerts";
|
||||||
import {
|
import {
|
||||||
NumberFieldWide,
|
NumberFieldWide,
|
||||||
|
@ -206,6 +206,10 @@ function FilterActionAddForm({ filter, isOpen, toggle, clients }: props) {
|
||||||
/>
|
/>
|
||||||
<TextFieldWide name="save_path" label="Save path" />
|
<TextFieldWide name="save_path" label="Save path" />
|
||||||
|
|
||||||
|
<div className="py-6 px-6 space-y-6 sm:py-0 sm:space-y-0 sm:divide-y sm:divide-gray-200">
|
||||||
|
<SwitchGroup name="paused" label="Add paused" />
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className="divide-y divide-gray-200 pt-8 space-y-6 sm:pt-10 sm:space-y-5">
|
<div className="divide-y divide-gray-200 pt-8 space-y-6 sm:pt-10 sm:space-y-5">
|
||||||
<div className="px-4">
|
<div className="px-4">
|
||||||
<h3 className="text-lg leading-6 font-medium text-gray-900">
|
<h3 className="text-lg leading-6 font-medium text-gray-900">
|
||||||
|
@ -240,6 +244,10 @@ function FilterActionAddForm({ filter, isOpen, toggle, clients }: props) {
|
||||||
<TextFieldWide name="label" label="Label" />
|
<TextFieldWide name="label" label="Label" />
|
||||||
<TextFieldWide name="save_path" label="Save path" />
|
<TextFieldWide name="save_path" label="Save path" />
|
||||||
|
|
||||||
|
<div className="py-6 px-6 space-y-6 sm:py-0 sm:space-y-0 sm:divide-y sm:divide-gray-200">
|
||||||
|
<SwitchGroup name="paused" label="Add paused" />
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className="divide-y divide-gray-200 pt-8 space-y-6 sm:pt-10 sm:space-y-5">
|
<div className="divide-y divide-gray-200 pt-8 space-y-6 sm:pt-10 sm:space-y-5">
|
||||||
<div className="px-4">
|
<div className="px-4">
|
||||||
<h3 className="text-lg leading-6 font-medium text-gray-900">
|
<h3 className="text-lg leading-6 font-medium text-gray-900">
|
||||||
|
|
|
@ -10,7 +10,7 @@ import DEBUG from "../../components/debug";
|
||||||
import APIClient from "../../api/APIClient";
|
import APIClient from "../../api/APIClient";
|
||||||
import { ActionTypeOptions } from "../../domain/constants";
|
import { ActionTypeOptions } from "../../domain/constants";
|
||||||
import { AlertWarning } from "../../components/alerts";
|
import { AlertWarning } from "../../components/alerts";
|
||||||
import { TextFieldWide } from "../../components/inputs";
|
import { SwitchGroup, TextFieldWide } from "../../components/inputs";
|
||||||
import {
|
import {
|
||||||
NumberFieldWide,
|
NumberFieldWide,
|
||||||
RadioFieldsetWide,
|
RadioFieldsetWide,
|
||||||
|
@ -133,6 +133,9 @@ function FilterActionUpdateForm({
|
||||||
label="Limit upload speed"
|
label="Limit upload speed"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
<div className="col-span-6">
|
||||||
|
<SwitchGroup name="paused" label="Add paused" />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
case "DELUGE_V1":
|
case "DELUGE_V1":
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import React, { Fragment, useState } from "react";
|
import { Fragment, useState } from "react";
|
||||||
import { useMutation } from "react-query";
|
import { useMutation } from "react-query";
|
||||||
import {
|
import {
|
||||||
DOWNLOAD_CLIENT_TYPES,
|
DOWNLOAD_CLIENT_TYPES,
|
||||||
|
@ -15,7 +15,7 @@ import APIClient from "../../../api/APIClient";
|
||||||
import { sleep } from "../../../utils/utils";
|
import { sleep } from "../../../utils/utils";
|
||||||
import { DownloadClientTypeOptions } from "../../../domain/constants";
|
import { DownloadClientTypeOptions } from "../../../domain/constants";
|
||||||
import { RadioFieldsetWide } from "../../../components/inputs/wide";
|
import { RadioFieldsetWide } from "../../../components/inputs/wide";
|
||||||
import { componentMap } from "./shared";
|
import { componentMap, rulesComponentMap } from "./shared";
|
||||||
|
|
||||||
import { toast } from 'react-hot-toast'
|
import { toast } from 'react-hot-toast'
|
||||||
import Toast from '../../../components/notifications/Toast';
|
import Toast from '../../../components/notifications/Toast';
|
||||||
|
@ -165,6 +165,8 @@ function DownloadClientAddForm({ isOpen, toggle }: any) {
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{rulesComponentMap[values.type]}
|
||||||
|
|
||||||
<div className="flex-shrink-0 px-4 border-t border-gray-200 py-5 sm:px-6">
|
<div className="flex-shrink-0 px-4 border-t border-gray-200 py-5 sm:px-6">
|
||||||
<div className="space-x-3 flex justify-end">
|
<div className="space-x-3 flex justify-end">
|
||||||
<button
|
<button
|
||||||
|
|
|
@ -12,7 +12,7 @@ import { SwitchGroup, TextFieldWide } from "../../../components/inputs";
|
||||||
import { DownloadClientTypeOptions } from "../../../domain/constants";
|
import { DownloadClientTypeOptions } from "../../../domain/constants";
|
||||||
import APIClient from "../../../api/APIClient";
|
import APIClient from "../../../api/APIClient";
|
||||||
import { sleep } from "../../../utils/utils";
|
import { sleep } from "../../../utils/utils";
|
||||||
import { componentMap } from "./shared";
|
import { componentMap, rulesComponentMap } from "./shared";
|
||||||
import { RadioFieldsetWide } from "../../../components/inputs/wide";
|
import { RadioFieldsetWide } from "../../../components/inputs/wide";
|
||||||
import { DeleteModal } from "../../../components/modals";
|
import { DeleteModal } from "../../../components/modals";
|
||||||
|
|
||||||
|
@ -190,6 +190,8 @@ function DownloadClientUpdateForm({ client, isOpen, toggle }: any) {
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{rulesComponentMap[values.type]}
|
||||||
|
|
||||||
<div className="flex-shrink-0 px-4 border-t border-gray-200 py-5 sm:px-6">
|
<div className="flex-shrink-0 px-4 border-t border-gray-200 py-5 sm:px-6">
|
||||||
<div className="space-x-3 flex justify-between">
|
<div className="space-x-3 flex justify-between">
|
||||||
<button
|
<button
|
||||||
|
|
|
@ -2,6 +2,7 @@ import { Fragment } from "react";
|
||||||
import { SwitchGroup, TextFieldWide } from "../../../components/inputs";
|
import { SwitchGroup, TextFieldWide } from "../../../components/inputs";
|
||||||
import { NumberFieldWide, PasswordFieldWide } from "../../../components/inputs/wide";
|
import { NumberFieldWide, PasswordFieldWide } from "../../../components/inputs/wide";
|
||||||
import { useField } from "react-final-form";
|
import { useField } from "react-final-form";
|
||||||
|
import { Dialog } from "@headlessui/react";
|
||||||
|
|
||||||
function FormFieldsDefault() {
|
function FormFieldsDefault() {
|
||||||
return (
|
return (
|
||||||
|
@ -50,3 +51,72 @@ export const componentMap: any = {
|
||||||
SONARR: <FormFieldsArr />,
|
SONARR: <FormFieldsArr />,
|
||||||
LIDARR: <FormFieldsArr />,
|
LIDARR: <FormFieldsArr />,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
function FormFieldsRulesBasic() {
|
||||||
|
const { input: enabled } = useField("settings.rules.enabled");
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="border-t border-gray-200 py-5">
|
||||||
|
|
||||||
|
<div className="px-6 space-y-1">
|
||||||
|
<Dialog.Title className="text-lg font-medium text-gray-900">Rules</Dialog.Title>
|
||||||
|
<p className="text-sm text-gray-500">
|
||||||
|
Manage max downloads.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="py-6 px-6 space-y-6 sm:py-0 sm:space-y-0 sm:divide-y sm:divide-gray-200">
|
||||||
|
<SwitchGroup name="settings.rules.enabled" label="Enabled" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{enabled.value === true && (
|
||||||
|
<Fragment>
|
||||||
|
<NumberFieldWide name="settings.rules.max_active_downloads" label="Max active downloads" />
|
||||||
|
</Fragment>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function FormFieldsRules() {
|
||||||
|
const { input } = useField("settings.rules.ignore_slow_torrents");
|
||||||
|
const { input: enabled } = useField("settings.rules.enabled");
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="border-t border-gray-200 py-5">
|
||||||
|
|
||||||
|
<div className="px-6 space-y-1">
|
||||||
|
<Dialog.Title className="text-lg font-medium text-gray-900">Rules</Dialog.Title>
|
||||||
|
<p className="text-sm text-gray-500">
|
||||||
|
Manage max downloads etc.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="py-6 px-6 space-y-6 sm:py-0 sm:space-y-0 sm:divide-y sm:divide-gray-200">
|
||||||
|
<SwitchGroup name="settings.rules.enabled" label="Enabled" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{enabled.value === true && (
|
||||||
|
<Fragment>
|
||||||
|
<NumberFieldWide name="settings.rules.max_active_downloads" label="Max active downloads" />
|
||||||
|
<div className="py-6 px-6 space-y-6 sm:py-0 sm:space-y-0 sm:divide-y sm:divide-gray-200">
|
||||||
|
<SwitchGroup name="settings.rules.ignore_slow_torrents" label="Ignore slow torrents" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{input.value === true && (
|
||||||
|
<Fragment>
|
||||||
|
<NumberFieldWide name="settings.rules.download_speed_threshold" label="Download speed threshold" placeholder="in KB/s" help="If download speed is below this when max active downloads is hit, download anyways. KB/s" />
|
||||||
|
</Fragment>
|
||||||
|
)}
|
||||||
|
</Fragment>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export const rulesComponentMap: any = {
|
||||||
|
DELUGE_V1: <FormFieldsRulesBasic />,
|
||||||
|
DELUGE_V2: <FormFieldsRulesBasic />,
|
||||||
|
QBITTORRENT: <FormFieldsRules />,
|
||||||
|
};
|
|
@ -42,7 +42,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 font-medium text-gray-900">{client.host}</td>
|
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500">{client.host}</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-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}>
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue