mirror of
https://github.com/idanoo/autobrr
synced 2025-07-23 08:49:13 +00:00
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:
parent
b95c1e6913
commit
870e109f6c
12 changed files with 271 additions and 2 deletions
79
internal/action/porla.go
Normal file
79
internal/action/porla.go
Normal 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
|
||||
}
|
|
@ -60,6 +60,9 @@ func (s *service) RunAction(ctx context.Context, action *domain.Action, release
|
|||
case domain.ActionTypeTransmission:
|
||||
rejections, err = s.transmission(ctx, action, release)
|
||||
|
||||
case domain.ActionTypePorla:
|
||||
rejections, err = s.porla(*action, release)
|
||||
|
||||
case domain.ActionTypeRadarr:
|
||||
rejections, err = s.radarr(ctx, action, release)
|
||||
|
||||
|
|
|
@ -2,6 +2,7 @@ package domain
|
|||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/autobrr/autobrr/pkg/errors"
|
||||
)
|
||||
|
||||
|
@ -80,6 +81,7 @@ const (
|
|||
ActionTypeDelugeV2 ActionType = "DELUGE_V2"
|
||||
ActionTypeRTorrent ActionType = "RTORRENT"
|
||||
ActionTypeTransmission ActionType = "TRANSMISSION"
|
||||
ActionTypePorla ActionType = "PORLA"
|
||||
ActionTypeWatchFolder ActionType = "WATCH_FOLDER"
|
||||
ActionTypeWebhook ActionType = "WEBHOOK"
|
||||
ActionTypeRadarr ActionType = "RADARR"
|
||||
|
|
|
@ -73,6 +73,7 @@ const (
|
|||
DownloadClientTypeDelugeV2 DownloadClientType = "DELUGE_V2"
|
||||
DownloadClientTypeRTorrent DownloadClientType = "RTORRENT"
|
||||
DownloadClientTypeTransmission DownloadClientType = "TRANSMISSION"
|
||||
DownloadClientTypePorla DownloadClientType = "PORLA"
|
||||
DownloadClientTypeRadarr DownloadClientType = "RADARR"
|
||||
DownloadClientTypeSonarr DownloadClientType = "SONARR"
|
||||
DownloadClientTypeLidarr DownloadClientType = "LIDARR"
|
||||
|
|
|
@ -7,6 +7,7 @@ import (
|
|||
"github.com/autobrr/autobrr/internal/domain"
|
||||
"github.com/autobrr/autobrr/pkg/errors"
|
||||
"github.com/autobrr/autobrr/pkg/lidarr"
|
||||
"github.com/autobrr/autobrr/pkg/porla"
|
||||
"github.com/autobrr/autobrr/pkg/radarr"
|
||||
"github.com/autobrr/autobrr/pkg/readarr"
|
||||
"github.com/autobrr/autobrr/pkg/sonarr"
|
||||
|
@ -32,6 +33,9 @@ func (s *service) testConnection(ctx context.Context, client domain.DownloadClie
|
|||
case domain.DownloadClientTypeTransmission:
|
||||
return s.testTransmissionConnection(ctx, client)
|
||||
|
||||
case domain.DownloadClientTypePorla:
|
||||
return s.testPorlaConnection(client)
|
||||
|
||||
case domain.DownloadClientTypeRadarr:
|
||||
return s.testRadarrConnection(ctx, client)
|
||||
|
||||
|
@ -258,3 +262,26 @@ func (s *service) testReadarrConnection(ctx context.Context, client domain.Downl
|
|||
|
||||
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
30
pkg/porla/client.go
Normal 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
20
pkg/porla/domain.go
Normal 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
43
pkg/porla/methods.go
Normal 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
|
||||
}
|
|
@ -241,6 +241,11 @@ export const DownloadClientTypeOptions: RadioFieldsetOption[] = [
|
|||
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",
|
||||
|
@ -274,6 +279,7 @@ export const DownloadClientTypeNameMap: Record<DownloadClientType | string, stri
|
|||
"QBITTORRENT": "qBittorrent",
|
||||
"RTORRENT": "rTorrent",
|
||||
"TRANSMISSION": "Transmission",
|
||||
"PORLA": "Porla",
|
||||
"RADARR": "Radarr",
|
||||
"SONARR": "Sonarr",
|
||||
"LIDARR": "Lidarr",
|
||||
|
@ -291,6 +297,7 @@ export const ActionTypeOptions: RadioFieldsetOption[] = [
|
|||
{ label: "Deluge v2", description: "Add torrents directly to Deluge 2", value: "DELUGE_V2" },
|
||||
{ label: "rTorrent", description: "Add torrents directly to rTorrent", value: "RTORRENT" },
|
||||
{ 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: "Sonarr", description: "Send to Sonarr and let it decide", value: "SONARR" },
|
||||
{ label: "Lidarr", description: "Send to Lidarr and let it decide", value: "LIDARR" },
|
||||
|
@ -308,6 +315,7 @@ export const ActionTypeNameMap = {
|
|||
"QBITTORRENT": "qBittorrent",
|
||||
"RTORRENT": "rTorrent",
|
||||
"TRANSMISSION": "Transmission",
|
||||
"PORLA": "Porla",
|
||||
"RADARR": "Radarr",
|
||||
"SONARR": "Sonarr",
|
||||
"LIDARR": "Lidarr",
|
||||
|
|
|
@ -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() {
|
||||
return (
|
||||
<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/>,
|
||||
RTORRENT: <FormFieldsRTorrent />,
|
||||
TRANSMISSION: <FormFieldsTransmission/>,
|
||||
PORLA: <FormFieldsPorla />,
|
||||
RADARR: <FormFieldsArr/>,
|
||||
SONARR: <FormFieldsArr/>,
|
||||
LIDARR: <FormFieldsArr/>,
|
||||
|
|
|
@ -411,6 +411,42 @@ const TypeForm = ({ action, idx, clients }: TypeFormProps) => {
|
|||
/>
|
||||
</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:
|
||||
return null;
|
||||
|
|
1
web/src/types/Download.d.ts
vendored
1
web/src/types/Download.d.ts
vendored
|
@ -4,6 +4,7 @@ type DownloadClientType =
|
|||
"DELUGE_V2" |
|
||||
"RTORRENT" |
|
||||
"TRANSMISSION" |
|
||||
"PORLA" |
|
||||
"RADARR" |
|
||||
"SONARR" |
|
||||
"LIDARR" |
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue