diff --git a/internal/action/porla.go b/internal/action/porla.go new file mode 100644 index 0000000..f20c69e --- /dev/null +++ b/internal/action/porla.go @@ -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 +} diff --git a/internal/action/run.go b/internal/action/run.go index be7ce66..97d23b1 100644 --- a/internal/action/run.go +++ b/internal/action/run.go @@ -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) diff --git a/internal/domain/action.go b/internal/domain/action.go index dde97ba..8c656ad 100644 --- a/internal/domain/action.go +++ b/internal/domain/action.go @@ -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" diff --git a/internal/domain/client.go b/internal/domain/client.go index cf8b973..a82e5fe 100644 --- a/internal/domain/client.go +++ b/internal/domain/client.go @@ -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" diff --git a/internal/download_client/connection.go b/internal/download_client/connection.go index d9b9928..d99e5bd 100644 --- a/internal/download_client/connection.go +++ b/internal/download_client/connection.go @@ -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 +} diff --git a/pkg/porla/client.go b/pkg/porla/client.go new file mode 100644 index 0000000..384a813 --- /dev/null +++ b/pkg/porla/client.go @@ -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 +} diff --git a/pkg/porla/domain.go b/pkg/porla/domain.go new file mode 100644 index 0000000..db1a461 --- /dev/null +++ b/pkg/porla/domain.go @@ -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 { +} diff --git a/pkg/porla/methods.go b/pkg/porla/methods.go new file mode 100644 index 0000000..584e6a1 --- /dev/null +++ b/pkg/porla/methods.go @@ -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 +} diff --git a/web/src/domain/constants.ts b/web/src/domain/constants.ts index a529755..e52fb4f 100644 --- a/web/src/domain/constants.ts +++ b/web/src/domain/constants.ts @@ -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[] = [ diff --git a/web/src/forms/settings/DownloadClientForms.tsx b/web/src/forms/settings/DownloadClientForms.tsx index a99227d..f3a090a 100644 --- a/web/src/forms/settings/DownloadClientForms.tsx +++ b/web/src/forms/settings/DownloadClientForms.tsx @@ -157,6 +157,24 @@ function FormFieldsQbit() { ); } +function FormFieldsPorla() { + const { + values: {} + } = useFormikContext(); + + return ( +
+ + + +
+ ) +} + function FormFieldsRTorrent() { return (
@@ -209,11 +227,12 @@ export const componentMap: componentMapType = { QBITTORRENT: , RTORRENT: , TRANSMISSION: , + PORLA: , RADARR: , SONARR: , LIDARR: , WHISPARR: , - READARR: + READARR: }; function FormFieldsRulesBasic() { diff --git a/web/src/screens/filters/action.tsx b/web/src/screens/filters/action.tsx index f173ca0..52705c5 100644 --- a/web/src/screens/filters/action.tsx +++ b/web/src/screens/filters/action.tsx @@ -411,6 +411,42 @@ const TypeForm = ({ action, idx, clients }: TypeFormProps) => { />
); + case "PORLA": + return ( +
+
+ + +
+ +
+
+ + +
+
+ + +
+
+
+
+ ); default: return null; diff --git a/web/src/types/Download.d.ts b/web/src/types/Download.d.ts index f74d58a..d5c5f35 100644 --- a/web/src/types/Download.d.ts +++ b/web/src/types/Download.d.ts @@ -4,6 +4,7 @@ type DownloadClientType = "DELUGE_V2" | "RTORRENT" | "TRANSMISSION" | + "PORLA" | "RADARR" | "SONARR" | "LIDARR" |