Feature: Radarr (#13)

* feat(web): add and update radarr

* feat: add radarr download client

* feat: add tests
This commit is contained in:
Ludvig Lundgren 2021-08-21 23:36:06 +02:00 committed by GitHub
parent 0c4aaa29b0
commit 455284a94b
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
35 changed files with 2898 additions and 3348 deletions

65
internal/action/radarr.go Normal file
View file

@ -0,0 +1,65 @@
package action
import (
"time"
"github.com/autobrr/autobrr/internal/domain"
"github.com/autobrr/autobrr/pkg/radarr"
"github.com/rs/zerolog/log"
)
func (s *service) radarr(announce domain.Announce, action domain.Action) error {
log.Trace().Msg("action RADARR")
// TODO validate data
// 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
}
// return early if no client found
if client == nil {
return err
}
// initial config
cfg := radarr.Config{
Hostname: client.Host,
APIKey: client.Settings.APIKey,
}
// only set basic auth if enabled
if client.Settings.Basic.Auth {
cfg.BasicAuth = client.Settings.Basic.Auth
cfg.Username = client.Settings.Basic.Username
cfg.Password = client.Settings.Basic.Password
}
r := radarr.New(cfg)
release := radarr.Release{
Title: announce.TorrentName,
DownloadUrl: announce.TorrentUrl,
Size: 0,
Indexer: announce.Site,
DownloadProtocol: "torrent",
Protocol: "torrent",
PublishDate: time.Now().String(),
}
err = r.Push(release)
if err != nil {
log.Error().Err(err).Msgf("radarr: failed to push release: %v", release)
return err
}
// TODO save pushed release
log.Debug().Msgf("radarr: successfully pushed release: %v, indexer %v to %v", release.Title, release.Indexer, client.Host)
return nil
}

View file

@ -65,7 +65,14 @@ func (s *service) RunActions(torrentFile string, hash string, filter domain.Filt
}
}()
// pvr *arr
case domain.ActionTypeRadarr:
go func() {
err := s.radarr(announce, action)
if err != nil {
log.Error().Err(err).Msg("error sending torrent to radarr")
}
}()
default:
log.Warn().Msgf("unsupported action: %v type: %v", action.Name, action.Type)
}

View file

@ -2,6 +2,7 @@ package database
import (
"database/sql"
"encoding/json"
"github.com/autobrr/autobrr/internal/domain"
@ -18,7 +19,7 @@ func NewDownloadClientRepo(db *sql.DB) domain.DownloadClientRepo {
func (r *DownloadClientRepo) List() ([]domain.DownloadClient, error) {
rows, err := r.db.Query("SELECT id, name, type, enabled, host, port, ssl, username, password FROM client")
rows, err := r.db.Query("SELECT id, name, type, enabled, host, port, ssl, username, password, settings FROM client")
if err != nil {
log.Fatal().Err(err)
}
@ -29,14 +30,21 @@ func (r *DownloadClientRepo) List() ([]domain.DownloadClient, error) {
for rows.Next() {
var f domain.DownloadClient
var settingsJsonStr string
if err := rows.Scan(&f.ID, &f.Name, &f.Type, &f.Enabled, &f.Host, &f.Port, &f.SSL, &f.Username, &f.Password); err != nil {
if err := rows.Scan(&f.ID, &f.Name, &f.Type, &f.Enabled, &f.Host, &f.Port, &f.SSL, &f.Username, &f.Password, &settingsJsonStr); err != nil {
log.Error().Err(err)
}
if err != nil {
return nil, err
}
if settingsJsonStr != "" {
if err := json.Unmarshal([]byte(settingsJsonStr), &f.Settings); err != nil {
return nil, err
}
}
clients = append(clients, f)
}
if err := rows.Err(); err != nil {
@ -49,7 +57,7 @@ func (r *DownloadClientRepo) List() ([]domain.DownloadClient, error) {
func (r *DownloadClientRepo) FindByID(id int32) (*domain.DownloadClient, error) {
query := `
SELECT id, name, type, enabled, host, port, ssl, username, password FROM client WHERE id = ?
SELECT id, name, type, enabled, host, port, ssl, username, password, settings FROM client WHERE id = ?
`
row := r.db.QueryRow(query, id)
@ -58,18 +66,25 @@ func (r *DownloadClientRepo) FindByID(id int32) (*domain.DownloadClient, error)
}
var client domain.DownloadClient
var settingsJsonStr string
if err := row.Scan(&client.ID, &client.Name, &client.Type, &client.Enabled, &client.Host, &client.Port, &client.SSL, &client.Username, &client.Password); err != nil {
if err := row.Scan(&client.ID, &client.Name, &client.Type, &client.Enabled, &client.Host, &client.Port, &client.SSL, &client.Username, &client.Password, &settingsJsonStr); err != nil {
log.Error().Err(err).Msg("could not scan download client to struct")
return nil, err
}
if settingsJsonStr != "" {
if err := json.Unmarshal([]byte(settingsJsonStr), &client.Settings); err != nil {
return nil, err
}
}
return &client, nil
}
func (r *DownloadClientRepo) FindByActionID(actionID int) ([]domain.DownloadClient, error) {
rows, err := r.db.Query("SELECT id, name, type, enabled, host, port, ssl, username, password FROM client, action_client WHERE client.id = action_client.client_id AND action_client.action_id = ?", actionID)
rows, err := r.db.Query("SELECT id, name, type, enabled, host, port, ssl, username, password, settings FROM client, action_client WHERE client.id = action_client.client_id AND action_client.action_id = ?", actionID)
if err != nil {
log.Fatal().Err(err)
}
@ -79,14 +94,21 @@ func (r *DownloadClientRepo) FindByActionID(actionID int) ([]domain.DownloadClie
var clients []domain.DownloadClient
for rows.Next() {
var f domain.DownloadClient
var settingsJsonStr string
if err := rows.Scan(&f.ID, &f.Name, &f.Type, &f.Enabled, &f.Host, &f.Port, &f.SSL, &f.Username, &f.Password); err != nil {
if err := rows.Scan(&f.ID, &f.Name, &f.Type, &f.Enabled, &f.Host, &f.Port, &f.SSL, &f.Username, &f.Password, &settingsJsonStr); err != nil {
log.Error().Err(err)
}
if err != nil {
return nil, err
}
if settingsJsonStr != "" {
if err := json.Unmarshal([]byte(settingsJsonStr), &f.Settings); err != nil {
return nil, err
}
}
clients = append(clients, f)
}
if err := rows.Err(); err != nil {
@ -97,16 +119,27 @@ func (r *DownloadClientRepo) FindByActionID(actionID int) ([]domain.DownloadClie
}
func (r *DownloadClientRepo) Store(client domain.DownloadClient) (*domain.DownloadClient, error) {
var err error
settings := domain.DownloadClientSettings{
APIKey: client.Settings.APIKey,
Basic: client.Settings.Basic,
}
settingsJson, err := json.Marshal(&settings)
if err != nil {
log.Error().Err(err).Msgf("could not marshal download client settings %v", settings)
return nil, err
}
if client.ID != 0 {
log.Info().Msg("UPDATE existing record")
_, err = r.db.Exec(`UPDATE client SET name = ?, type = ?, enabled = ?, host = ?, port = ?, ssl = ?, username = ?, password = ? WHERE id = ?`, client.Name, client.Type, client.Enabled, client.Host, client.Port, client.SSL, client.Username, client.Password, client.ID)
_, err = r.db.Exec(`UPDATE client SET name = ?, type = ?, enabled = ?, host = ?, port = ?, ssl = ?, username = ?, password = ?, settings = json_set(?) WHERE id = ?`, client.Name, client.Type, client.Enabled, client.Host, client.Port, client.SSL, client.Username, client.Password, client.ID, settingsJson)
} else {
var res sql.Result
res, err = r.db.Exec(`INSERT INTO client(name, type, enabled, host, port, ssl, username, password)
VALUES (?, ?, ?, ?, ?, ? , ?, ?) ON CONFLICT DO NOTHING`, client.Name, client.Type, client.Enabled, client.Host, client.Port, client.SSL, client.Username, client.Password)
res, err = r.db.Exec(`INSERT INTO client(name, type, enabled, host, port, ssl, username, password, settings)
VALUES (?, ?, ?, ?, ?, ? , ?, ?, json_set(?)) ON CONFLICT DO NOTHING`, client.Name, client.Type, client.Enabled, client.Host, client.Port, client.SSL, client.Username, client.Password, settingsJson)
if err != nil {
log.Error().Err(err)
return nil, err

View file

@ -111,7 +111,7 @@ CREATE TABLE client
ssl BOOLEAN,
username TEXT,
password TEXT,
settings TEXT
settings JSON
);
CREATE TABLE action

View file

@ -37,4 +37,5 @@ const (
ActionTypeDelugeV1 ActionType = "DELUGE_V1"
ActionTypeDelugeV2 ActionType = "DELUGE_V2"
ActionTypeWatchFolder ActionType = "WATCH_FOLDER"
ActionTypeRadarr ActionType = "RADARR"
)

View file

@ -9,15 +9,27 @@ type DownloadClientRepo interface {
}
type DownloadClient struct {
ID int `json:"id"`
Name string `json:"name"`
Type DownloadClientType `json:"type"`
Enabled bool `json:"enabled"`
Host string `json:"host"`
Port int `json:"port"`
SSL bool `json:"ssl"`
Username string `json:"username"`
Password string `json:"password"`
ID int `json:"id"`
Name string `json:"name"`
Type DownloadClientType `json:"type"`
Enabled bool `json:"enabled"`
Host string `json:"host"`
Port int `json:"port"`
SSL bool `json:"ssl"`
Username string `json:"username"`
Password string `json:"password"`
Settings DownloadClientSettings `json:"settings,omitempty"`
}
type DownloadClientSettings struct {
APIKey string `json:"apikey,omitempty"`
Basic BasicAuth `json:"basic,omitempty"`
}
type BasicAuth struct {
Auth bool `json:"auth,omitempty"`
Username string `json:"username,omitempty"`
Password string `json:"password,omitempty"`
}
type DownloadClientType string
@ -26,4 +38,5 @@ const (
DownloadClientTypeQbittorrent DownloadClientType = "QBITTORRENT"
DownloadClientTypeDelugeV1 DownloadClientType = "DELUGE_V1"
DownloadClientTypeDelugeV2 DownloadClientType = "DELUGE_V2"
DownloadClientTypeRadarr DownloadClientType = "RADARR"
)

View file

@ -0,0 +1,108 @@
package download_client
import (
"time"
"github.com/autobrr/autobrr/internal/domain"
"github.com/autobrr/autobrr/pkg/qbittorrent"
"github.com/autobrr/autobrr/pkg/radarr"
delugeClient "github.com/gdm85/go-libdeluge"
"github.com/rs/zerolog/log"
)
func (s *service) testConnection(client domain.DownloadClient) error {
switch client.Type {
case domain.DownloadClientTypeQbittorrent:
return s.testQbittorrentConnection(client)
case domain.DownloadClientTypeDelugeV1, domain.DownloadClientTypeDelugeV2:
return s.testDelugeConnection(client)
case domain.DownloadClientTypeRadarr:
return s.testRadarrConnection(client)
}
return nil
}
func (s *service) testQbittorrentConnection(client domain.DownloadClient) error {
qbtSettings := qbittorrent.Settings{
Hostname: client.Host,
Port: uint(client.Port),
Username: client.Username,
Password: client.Password,
SSL: client.SSL,
}
qbt := qbittorrent.NewClient(qbtSettings)
err := qbt.Login()
if err != nil {
log.Error().Err(err).Msgf("error logging into client: %v", client.Host)
return err
}
return nil
}
func (s *service) testDelugeConnection(client domain.DownloadClient) error {
var deluge delugeClient.DelugeClient
settings := delugeClient.Settings{
Hostname: client.Host,
Port: uint(client.Port),
Login: client.Username,
Password: client.Password,
DebugServerResponses: true,
ReadWriteTimeout: time.Second * 10,
}
switch client.Type {
case "DELUGE_V1":
deluge = delugeClient.NewV1(settings)
case "DELUGE_V2":
deluge = delugeClient.NewV2(settings)
default:
deluge = delugeClient.NewV2(settings)
}
// perform connection to Deluge server
err := deluge.Connect()
if err != nil {
log.Error().Err(err).Msgf("error logging into client: %v", client.Host)
return err
}
defer deluge.Close()
// print daemon version
ver, err := deluge.DaemonVersion()
if err != nil {
log.Error().Err(err).Msgf("could not get daemon version: %v", client.Host)
return err
}
log.Debug().Msgf("daemon version: %v", ver)
return nil
}
func (s *service) testRadarrConnection(client domain.DownloadClient) error {
r := radarr.New(radarr.Config{
Hostname: client.Host,
APIKey: client.Settings.APIKey,
BasicAuth: client.Settings.Basic.Auth,
Username: client.Settings.Basic.Username,
Password: client.Settings.Basic.Password,
})
_, err := r.Test()
if err != nil {
log.Error().Err(err).Msgf("radarr: connection test failed: %v", client.Host)
return err
}
return nil
}

View file

@ -2,11 +2,9 @@ package download_client
import (
"errors"
"time"
"github.com/autobrr/autobrr/internal/domain"
"github.com/autobrr/autobrr/pkg/qbittorrent"
delugeClient "github.com/gdm85/go-libdeluge"
"github.com/rs/zerolog/log"
)
@ -91,77 +89,3 @@ func (s *service) Test(client domain.DownloadClient) error {
return nil
}
func (s *service) testConnection(client domain.DownloadClient) error {
switch client.Type {
case domain.DownloadClientTypeQbittorrent:
return s.testQbittorrentConnection(client)
case domain.DownloadClientTypeDelugeV1, domain.DownloadClientTypeDelugeV2:
return s.testDelugeConnection(client)
}
return nil
}
func (s *service) testQbittorrentConnection(client domain.DownloadClient) error {
qbtSettings := qbittorrent.Settings{
Hostname: client.Host,
Port: uint(client.Port),
Username: client.Username,
Password: client.Password,
SSL: client.SSL,
}
qbt := qbittorrent.NewClient(qbtSettings)
err := qbt.Login()
if err != nil {
log.Error().Err(err).Msgf("error logging into client: %v", client.Host)
return err
}
return nil
}
func (s *service) testDelugeConnection(client domain.DownloadClient) error {
var deluge delugeClient.DelugeClient
settings := delugeClient.Settings{
Hostname: client.Host,
Port: uint(client.Port),
Login: client.Username,
Password: client.Password,
DebugServerResponses: true,
ReadWriteTimeout: time.Second * 10,
}
switch client.Type {
case "DELUGE_V1":
deluge = delugeClient.NewV1(settings)
case "DELUGE_V2":
deluge = delugeClient.NewV2(settings)
default:
deluge = delugeClient.NewV2(settings)
}
// perform connection to Deluge server
err := deluge.Connect()
if err != nil {
log.Error().Err(err).Msgf("error logging into client: %v", client.Host)
return err
}
defer deluge.Close()
// print daemon version
ver, err := deluge.DaemonVersion()
if err != nil {
log.Error().Err(err).Msgf("could not get daemon version: %v", client.Host)
return err
}
log.Debug().Msgf("daemon version: %v", ver)
return nil
}