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:
|
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)
|
||||||
|
|
||||||
|
|
|
@ -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"
|
||||||
|
|
|
@ -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"
|
||||||
|
|
|
@ -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
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",
|
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,11 +315,12 @@ 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",
|
||||||
"WHISPARR": "Whisparr",
|
"WHISPARR": "Whisparr",
|
||||||
"READARR": "Readarr"
|
"READARR": "Readarr"
|
||||||
};
|
};
|
||||||
|
|
||||||
export const ActionContentLayoutOptions: SelectGenericOption<ActionContentLayout>[] = [
|
export const ActionContentLayoutOptions: SelectGenericOption<ActionContentLayout>[] = [
|
||||||
|
|
|
@ -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,11 +227,12 @@ 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/>,
|
||||||
WHISPARR: <FormFieldsArr/>,
|
WHISPARR: <FormFieldsArr/>,
|
||||||
READARR: <FormFieldsArr/>
|
READARR: <FormFieldsArr/>
|
||||||
};
|
};
|
||||||
|
|
||||||
function FormFieldsRulesBasic() {
|
function FormFieldsRulesBasic() {
|
||||||
|
|
|
@ -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;
|
||||||
|
|
1
web/src/types/Download.d.ts
vendored
1
web/src/types/Download.d.ts
vendored
|
@ -4,6 +4,7 @@ type DownloadClientType =
|
||||||
"DELUGE_V2" |
|
"DELUGE_V2" |
|
||||||
"RTORRENT" |
|
"RTORRENT" |
|
||||||
"TRANSMISSION" |
|
"TRANSMISSION" |
|
||||||
|
"PORLA" |
|
||||||
"RADARR" |
|
"RADARR" |
|
||||||
"SONARR" |
|
"SONARR" |
|
||||||
"LIDARR" |
|
"LIDARR" |
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue