feat(download-client): add support for Porla (#553)

* Add support for the 'Test' button to work

* Make Porla show up in filter actions select

* Add an empty Porla action

* Make Porla action find download client

* Make implementation actually add torrent to Porla

* Fix qBittorrent import

* Finish up Porla action

* Check length on commitish before slicing

* Move Porla to the other DL clients

* Add Porla to type name map

* Move Porla to beneath the other download clients
This commit is contained in:
Viktor Elofsson 2023-01-29 18:17:01 +01:00 committed by GitHub
parent b95c1e6913
commit 870e109f6c
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
12 changed files with 271 additions and 2 deletions

79
internal/action/porla.go Normal file
View file

@ -0,0 +1,79 @@
package action
import (
"bufio"
"context"
"encoding/base64"
"io/ioutil"
"os"
"github.com/autobrr/autobrr/internal/domain"
"github.com/autobrr/autobrr/pkg/errors"
"github.com/autobrr/autobrr/pkg/porla"
"github.com/dcarbone/zadapters/zstdlog"
"github.com/rs/zerolog"
)
func (s *service) porla(action domain.Action, release domain.Release) ([]string, error) {
s.log.Debug().Msgf("action Porla: %v", action.Name)
client, err := s.clientSvc.FindByID(context.TODO(), action.ClientID)
if err != nil {
return nil, errors.Wrap(err, "error finding client: %v", action.ClientID)
}
if client == nil {
return nil, errors.New("could not find client by id: %v", action.ClientID)
}
porlaSettings := porla.Settings{
Hostname: client.Host,
AuthToken: client.Settings.APIKey,
}
porlaSettings.Log = zstdlog.NewStdLoggerWithLevel(s.log.With().Str("type", "Porla").Str("client", client.Name).Logger(), zerolog.TraceLevel)
prl := porla.NewClient(porlaSettings)
if release.TorrentTmpFile == "" {
if err := release.DownloadTorrentFile(); err != nil {
return nil, errors.Wrap(err, "error downloading torrent file for release: %v", release.TorrentName)
}
}
file, err := os.Open(release.TorrentTmpFile)
if err != nil {
return nil, errors.Wrap(err, "error opening file %v", release.TorrentTmpFile)
}
defer file.Close()
reader := bufio.NewReader(file)
content, err := ioutil.ReadAll(reader)
if err != nil {
return nil, errors.Wrap(err, "failed to read file: %v", release.TorrentTmpFile)
}
opts := &porla.TorrentsAddReq{
DownloadLimit: -1,
SavePath: action.SavePath,
Ti: base64.StdEncoding.EncodeToString(content),
UploadLimit: -1,
}
if action.LimitDownloadSpeed > 0 {
opts.DownloadLimit = action.LimitDownloadSpeed * 1000
}
if action.LimitUploadSpeed > 0 {
opts.UploadLimit = action.LimitUploadSpeed * 1000
}
if err = prl.TorrentsAdd(opts); err != nil {
return nil, errors.Wrap(err, "could not add torrent %v to client: %v", release.TorrentTmpFile, client.Name)
}
s.log.Info().Msgf("torrent with hash %v successfully added to client: '%v'", release.TorrentHash, client.Name)
return nil, nil
}

View file

@ -60,6 +60,9 @@ func (s *service) RunAction(ctx context.Context, action *domain.Action, release
case domain.ActionTypeTransmission: case domain.ActionTypeTransmission:
rejections, err = s.transmission(ctx, action, release) rejections, err = s.transmission(ctx, action, release)
case domain.ActionTypePorla:
rejections, err = s.porla(*action, release)
case domain.ActionTypeRadarr: case domain.ActionTypeRadarr:
rejections, err = s.radarr(ctx, action, release) rejections, err = s.radarr(ctx, action, release)

View file

@ -2,6 +2,7 @@ package domain
import ( import (
"context" "context"
"github.com/autobrr/autobrr/pkg/errors" "github.com/autobrr/autobrr/pkg/errors"
) )
@ -80,6 +81,7 @@ const (
ActionTypeDelugeV2 ActionType = "DELUGE_V2" ActionTypeDelugeV2 ActionType = "DELUGE_V2"
ActionTypeRTorrent ActionType = "RTORRENT" ActionTypeRTorrent ActionType = "RTORRENT"
ActionTypeTransmission ActionType = "TRANSMISSION" ActionTypeTransmission ActionType = "TRANSMISSION"
ActionTypePorla ActionType = "PORLA"
ActionTypeWatchFolder ActionType = "WATCH_FOLDER" ActionTypeWatchFolder ActionType = "WATCH_FOLDER"
ActionTypeWebhook ActionType = "WEBHOOK" ActionTypeWebhook ActionType = "WEBHOOK"
ActionTypeRadarr ActionType = "RADARR" ActionTypeRadarr ActionType = "RADARR"

View file

@ -73,6 +73,7 @@ const (
DownloadClientTypeDelugeV2 DownloadClientType = "DELUGE_V2" DownloadClientTypeDelugeV2 DownloadClientType = "DELUGE_V2"
DownloadClientTypeRTorrent DownloadClientType = "RTORRENT" DownloadClientTypeRTorrent DownloadClientType = "RTORRENT"
DownloadClientTypeTransmission DownloadClientType = "TRANSMISSION" DownloadClientTypeTransmission DownloadClientType = "TRANSMISSION"
DownloadClientTypePorla DownloadClientType = "PORLA"
DownloadClientTypeRadarr DownloadClientType = "RADARR" DownloadClientTypeRadarr DownloadClientType = "RADARR"
DownloadClientTypeSonarr DownloadClientType = "SONARR" DownloadClientTypeSonarr DownloadClientType = "SONARR"
DownloadClientTypeLidarr DownloadClientType = "LIDARR" DownloadClientTypeLidarr DownloadClientType = "LIDARR"

View file

@ -7,6 +7,7 @@ import (
"github.com/autobrr/autobrr/internal/domain" "github.com/autobrr/autobrr/internal/domain"
"github.com/autobrr/autobrr/pkg/errors" "github.com/autobrr/autobrr/pkg/errors"
"github.com/autobrr/autobrr/pkg/lidarr" "github.com/autobrr/autobrr/pkg/lidarr"
"github.com/autobrr/autobrr/pkg/porla"
"github.com/autobrr/autobrr/pkg/radarr" "github.com/autobrr/autobrr/pkg/radarr"
"github.com/autobrr/autobrr/pkg/readarr" "github.com/autobrr/autobrr/pkg/readarr"
"github.com/autobrr/autobrr/pkg/sonarr" "github.com/autobrr/autobrr/pkg/sonarr"
@ -32,6 +33,9 @@ func (s *service) testConnection(ctx context.Context, client domain.DownloadClie
case domain.DownloadClientTypeTransmission: case domain.DownloadClientTypeTransmission:
return s.testTransmissionConnection(ctx, client) return s.testTransmissionConnection(ctx, client)
case domain.DownloadClientTypePorla:
return s.testPorlaConnection(client)
case domain.DownloadClientTypeRadarr: case domain.DownloadClientTypeRadarr:
return s.testRadarrConnection(ctx, client) return s.testRadarrConnection(ctx, client)
@ -258,3 +262,26 @@ func (s *service) testReadarrConnection(ctx context.Context, client domain.Downl
return nil return nil
} }
func (s *service) testPorlaConnection(client domain.DownloadClient) error {
p := porla.NewClient(porla.Settings{
Hostname: client.Host,
AuthToken: client.Settings.APIKey,
})
version, err := p.Version()
if err != nil {
return errors.Wrap(err, "porla: failed to get version: %v", client.Host)
}
commitish := version.Commitish
if len(commitish) > 8 {
commitish = commitish[:8]
}
s.log.Debug().Msgf("test client connection for porla: found version %s (commit %s)", version.Version, commitish)
return nil
}

30
pkg/porla/client.go Normal file
View file

@ -0,0 +1,30 @@
package porla
import (
"log"
"github.com/autobrr/autobrr/pkg/jsonrpc"
)
type Client struct {
Name string
Hostname string
rpcClient jsonrpc.Client
}
type Settings struct {
Hostname string
AuthToken string
Log *log.Logger
}
func NewClient(settings Settings) *Client {
c := &Client{
rpcClient: jsonrpc.NewClientWithOpts(settings.Hostname+"/api/v1/jsonrpc", &jsonrpc.ClientOpts{
Headers: map[string]string{
"Authorization": "Bearer " + settings.AuthToken,
},
}),
}
return c
}

20
pkg/porla/domain.go Normal file
View file

@ -0,0 +1,20 @@
package porla
type SysVersions struct {
Porla SysVersionsPorla `json:"porla"`
}
type SysVersionsPorla struct {
Commitish string `json:"commitish"`
Version string `json:"version"`
}
type TorrentsAddReq struct {
DownloadLimit int64 `json:"download_limit,omitempty"`
SavePath string `json:"save_path,omitempty"`
Ti string `json:"ti"`
UploadLimit int64 `json:"upload_limit,omitempty"`
}
type TorrentsAddRes struct {
}

43
pkg/porla/methods.go Normal file
View file

@ -0,0 +1,43 @@
package porla
func (c *Client) Version() (*SysVersionsPorla, error) {
response, err := c.rpcClient.Call("sys.versions")
if err != nil {
return nil, err
}
if response.Error != nil {
return nil, response.Error
}
var versions *SysVersions
err = response.GetObject(&versions)
if err != nil {
return nil, err
}
return &versions.Porla, nil
}
func (c *Client) TorrentsAdd(req *TorrentsAddReq) error {
response, err := c.rpcClient.Call("torrents.add", req)
if err != nil {
return err
}
if response.Error != nil {
return response.Error
}
var res *TorrentsAddRes
err = response.GetObject(&res)
if err != nil {
return err
}
return nil
}

View file

@ -241,6 +241,11 @@ export const DownloadClientTypeOptions: RadioFieldsetOption[] = [
description: "Add torrents directly to Transmission", description: "Add torrents directly to Transmission",
value: "TRANSMISSION" value: "TRANSMISSION"
}, },
{
label: "Porla",
description: "Add torrents directly to Porla",
value: "PORLA"
},
{ {
label: "Radarr", label: "Radarr",
description: "Send to Radarr and let it decide", description: "Send to Radarr and let it decide",
@ -274,6 +279,7 @@ export const DownloadClientTypeNameMap: Record<DownloadClientType | string, stri
"QBITTORRENT": "qBittorrent", "QBITTORRENT": "qBittorrent",
"RTORRENT": "rTorrent", "RTORRENT": "rTorrent",
"TRANSMISSION": "Transmission", "TRANSMISSION": "Transmission",
"PORLA": "Porla",
"RADARR": "Radarr", "RADARR": "Radarr",
"SONARR": "Sonarr", "SONARR": "Sonarr",
"LIDARR": "Lidarr", "LIDARR": "Lidarr",
@ -291,6 +297,7 @@ export const ActionTypeOptions: RadioFieldsetOption[] = [
{ label: "Deluge v2", description: "Add torrents directly to Deluge 2", value: "DELUGE_V2" }, { label: "Deluge v2", description: "Add torrents directly to Deluge 2", value: "DELUGE_V2" },
{ label: "rTorrent", description: "Add torrents directly to rTorrent", value: "RTORRENT" }, { label: "rTorrent", description: "Add torrents directly to rTorrent", value: "RTORRENT" },
{ label: "Transmission", description: "Add torrents directly to Transmission", value: "TRANSMISSION" }, { label: "Transmission", description: "Add torrents directly to Transmission", value: "TRANSMISSION" },
{ label: "Porla", description: "Add torrents directly to Porla", value: "PORLA" },
{ label: "Radarr", description: "Send to Radarr and let it decide", value: "RADARR" }, { label: "Radarr", description: "Send to Radarr and let it decide", value: "RADARR" },
{ label: "Sonarr", description: "Send to Sonarr and let it decide", value: "SONARR" }, { label: "Sonarr", description: "Send to Sonarr and let it decide", value: "SONARR" },
{ label: "Lidarr", description: "Send to Lidarr and let it decide", value: "LIDARR" }, { label: "Lidarr", description: "Send to Lidarr and let it decide", value: "LIDARR" },
@ -308,6 +315,7 @@ export const ActionTypeNameMap = {
"QBITTORRENT": "qBittorrent", "QBITTORRENT": "qBittorrent",
"RTORRENT": "rTorrent", "RTORRENT": "rTorrent",
"TRANSMISSION": "Transmission", "TRANSMISSION": "Transmission",
"PORLA": "Porla",
"RADARR": "Radarr", "RADARR": "Radarr",
"SONARR": "Sonarr", "SONARR": "Sonarr",
"LIDARR": "Lidarr", "LIDARR": "Lidarr",

View file

@ -157,6 +157,24 @@ function FormFieldsQbit() {
); );
} }
function FormFieldsPorla() {
const {
values: {}
} = useFormikContext<InitialValues>();
return (
<div className="flex flex-col space-y-4 px-1 py-6 sm:py-0 sm:space-y-0">
<TextFieldWide
name="host"
label="Host"
help="Eg. http(s)://client.domain.ltd, http(s)://domain.ltd/porla, http://domain.ltd:port"
/>
<PasswordFieldWide name="settings.apikey" label="Auth token" />
</div>
)
}
function FormFieldsRTorrent() { function FormFieldsRTorrent() {
return ( return (
<div className="flex flex-col space-y-4 px-1 py-6 sm:py-0 sm:space-y-0"> <div className="flex flex-col space-y-4 px-1 py-6 sm:py-0 sm:space-y-0">
@ -209,6 +227,7 @@ export const componentMap: componentMapType = {
QBITTORRENT: <FormFieldsQbit/>, QBITTORRENT: <FormFieldsQbit/>,
RTORRENT: <FormFieldsRTorrent />, RTORRENT: <FormFieldsRTorrent />,
TRANSMISSION: <FormFieldsTransmission/>, TRANSMISSION: <FormFieldsTransmission/>,
PORLA: <FormFieldsPorla />,
RADARR: <FormFieldsArr/>, RADARR: <FormFieldsArr/>,
SONARR: <FormFieldsArr/>, SONARR: <FormFieldsArr/>,
LIDARR: <FormFieldsArr/>, LIDARR: <FormFieldsArr/>,

View file

@ -411,6 +411,42 @@ const TypeForm = ({ action, idx, clients }: TypeFormProps) => {
/> />
</div> </div>
); );
case "PORLA":
return (
<div className="w-full">
<div className="mt-6 grid grid-cols-12 gap-6">
<DownloadClientSelect
name={`actions.${idx}.client_id`}
action={action}
clients={clients}
/>
<div className="col-span-6 sm:col-span-6">
<TextField
name={`actions.${idx}.save_path`}
label="Save path"
columns={6}
placeholder="eg. /full/path/to/torrent/data"
/>
</div>
</div>
<CollapsableSection title="Rules" subtitle="client options">
<div className="col-span-12">
<div className="mt-6 grid grid-cols-12 gap-6">
<NumberField
name={`actions.${idx}.limit_download_speed`}
label="Limit download speed (KiB/s)"
/>
<NumberField
name={`actions.${idx}.limit_upload_speed`}
label="Limit upload speed (KiB/s)"
/>
</div>
</div>
</CollapsableSection>
</div>
);
default: default:
return null; return null;

View file

@ -4,6 +4,7 @@ type DownloadClientType =
"DELUGE_V2" | "DELUGE_V2" |
"RTORRENT" | "RTORRENT" |
"TRANSMISSION" | "TRANSMISSION" |
"PORLA" |
"RADARR" | "RADARR" |
"SONARR" | "SONARR" |
"LIDARR" | "LIDARR" |