Skip to content

Commit

Permalink
feat(store): initial torbox integration
Browse files Browse the repository at this point in the history
  • Loading branch information
MunifTanjim committed Nov 4, 2024
1 parent 2aa59ff commit 23d5cfd
Show file tree
Hide file tree
Showing 9 changed files with 839 additions and 1 deletion.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ Companion for Stremio.
- [Debrid-Link](https://debrid-link.com)
- [Premiumize](https://www.premiumize.me)
- [RealDebrid](https://real-debrid.com) _(Planned)_
- [TorBox](https://torbox.app)

## Configuration

Expand Down
4 changes: 4 additions & 0 deletions internal/endpoint/store.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import (
"github.com/MunifTanjim/stremthru/store/alldebrid"
"github.com/MunifTanjim/stremthru/store/debridlink"
"github.com/MunifTanjim/stremthru/store/premiumize"
"github.com/MunifTanjim/stremthru/store/torbox"
"github.com/golang-jwt/jwt/v5"
)

Expand Down Expand Up @@ -51,6 +52,7 @@ func getStoreAuthToken(r *http.Request) string {
var adStore = alldebrid.NewStore()
var dlStore = debridlink.NewStoreClient()
var pmStore = premiumize.NewStoreClient(&premiumize.StoreClientConfig{})
var tbStore = torbox.NewStoreClient()

func getStore(r *http.Request) (store.Store, error) {
name, err := getStoreName(r)
Expand All @@ -66,6 +68,8 @@ func getStore(r *http.Request) (store.Store, error) {
return dlStore, nil
case store.StoreNamePremiumize:
return pmStore, nil
case store.StoreNameTorBox:
return tbStore, nil
default:
return nil, nil
}
Expand Down
3 changes: 2 additions & 1 deletion store/store.go
Original file line number Diff line number Diff line change
Expand Up @@ -69,10 +69,11 @@ const (
StoreNameAlldebrid StoreName = "alldebrid"
StoreNameDebridLink StoreName = "debridlink"
StoreNamePremiumize StoreName = "premiumize"
StoreNameTorBox StoreName = "torbox"
)

func (sn StoreName) Validate() (StoreName, *core.StoreError) {
if sn == StoreNameAlldebrid || sn == StoreNameDebridLink || sn == StoreNamePremiumize {
if sn == StoreNameAlldebrid || sn == StoreNameDebridLink || sn == StoreNamePremiumize || sn == StoreNameTorBox {
return sn, nil
}
return sn, ErrorInvalidStoreName(string(sn))
Expand Down
102 changes: 102 additions & 0 deletions store/torbox/client.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
package torbox

import (
"net/http"
"net/url"

"github.com/MunifTanjim/stremthru/core"
"github.com/MunifTanjim/stremthru/store"
)

var DefaultHTTPTransport = core.DefaultHTTPTransport
var DefaultHTTPClient = core.DefaultHTTPClient

type APIClientConfig struct {
BaseURL string
APIKey string
HTTPClient *http.Client
agent string
}

type APIClient struct {
BaseURL *url.URL // default: "https://api.torbox.app"
HTTPClient *http.Client
apiKey string
agent string
}

func NewAPIClient(conf *APIClientConfig) *APIClient {
if conf.agent == "" {
conf.agent = "stremthru"
}

if conf.BaseURL == "" {
conf.BaseURL = "https://api.torbox.app"
}

if conf.HTTPClient == nil {
conf.HTTPClient = DefaultHTTPClient
}

c := &APIClient{}

baseUrl, err := url.Parse(conf.BaseURL)
if err != nil {
panic(err)
}

c.BaseURL = baseUrl
c.HTTPClient = conf.HTTPClient
c.apiKey = conf.APIKey
c.agent = conf.agent

return c
}

type Ctx = store.Ctx

func (c APIClient) newRequest(method, path string, params store.RequestContext) (req *http.Request, err error) {
if params == nil {
params = &Ctx{}
}

url := c.BaseURL.JoinPath(path)

query := url.Query()

body, contentType, err := params.PrepareBody(method, &query)
if err != nil {
return nil, err
}

url.RawQuery = query.Encode()

req, err = http.NewRequestWithContext(params.GetContext(), method, url.String(), body)
if err != nil {
return nil, err
}

req.Header.Add("Authorization", "Bearer "+params.GetAPIKey(c.apiKey))
req.Header.Add("User-Agent", c.agent)
if len(contentType) > 0 {
req.Header.Add("Content-Type", contentType)
}

return req, nil
}

func (c APIClient) Request(method, path string, params store.RequestContext, v ResponseEnvelop) (*http.Response, error) {
req, err := c.newRequest(method, path, params)
if err != nil {
error := core.NewStoreError("failed to create request")
error.StoreName = string(store.StoreNameTorBox)
error.Cause = err
return nil, error
}
res, err := c.HTTPClient.Do(req)
err = processResponseBody(res, err, v)
if err != nil {
return res, UpstreamErrorFromRequest(err, req, res)
}
return res, nil
}
34 changes: 34 additions & 0 deletions store/torbox/error.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
package torbox

import (
"net/http"

"github.com/MunifTanjim/stremthru/core"
"github.com/MunifTanjim/stremthru/store"
)

func UpstreamErrorWithCause(cause error) *core.UpstreamError {
err := core.NewUpstreamError("")
err.StoreName = string(store.StoreNameTorBox)

if rerr, ok := cause.(*ResponseError); ok {
err.Msg = rerr.Detail
err.UpstreamCause = rerr
} else {
err.Cause = cause
}

return err
}

func UpstreamErrorFromRequest(cause error, req *http.Request, res *http.Response) error {
err := UpstreamErrorWithCause(cause)
err.InjectReq(req)
if res != nil {
err.StatusCode = res.StatusCode
}
if err.StatusCode <= http.StatusBadRequest {
err.StatusCode = http.StatusBadRequest
}
return err
}
134 changes: 134 additions & 0 deletions store/torbox/response.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
package torbox

import (
"encoding/json"
"errors"
"io"
"net/http"

"github.com/MunifTanjim/stremthru/core"
)

type ResponseStatus string

const (
ResponseStatusSuccess ResponseStatus = "success"
ResponseStatusError ResponseStatus = "error"
)

type ErrorCode string

const (
ErrorCodeDatabaseError ErrorCode = "DATABASE_ERROR"
ErrorCodeUnknownError ErrorCode = "UNKNOWN_ERROR"
ErrorCodeNoAuth ErrorCode = "NO_AUTH"
ErrorCodeBadToken ErrorCode = "BAD_TOKEN"
ErrorCodeAuthError ErrorCode = "AUTH_ERROR"
ErrorCodeInvalidOption ErrorCode = "INVALID_OPTION"
ErrorCodeRedirectError ErrorCode = "REDIRECT_ERROR"
ErrorCodeOAuthVerificationError ErrorCode = "OAUTH_VERIFICATION_ERROR"
ErrorCodeEndpointNotFound ErrorCode = "ENDPOINT_NOT_FOUND"
ErrorCodeItemNotFound ErrorCode = "ITEM_NOT_FOUND"
ErrorCodePlanRestrictedFeature ErrorCode = "PLAN_RESTRICTED_FEATURE"
ErrorCodeDuplicateItem ErrorCode = "DUPLICATE_ITEM"
ErrorCodeBozoRssFeed ErrorCode = "BOZO_RSS_FEED"
ErrorCodeSellixError ErrorCode = "SELLIX_ERROR"
ErrorCodeTooMuchData ErrorCode = "TOO_MUCH_DATA"
ErrorCodeDownloadTooLarge ErrorCode = "DOWNLOAD_TOO_LARGE"
ErrorCodeMissingRequiredOption ErrorCode = "MISSING_REQUIRED_OPTION"
ErrorCodeTooManyOptions ErrorCode = "TOO_MANY_OPTIONS"
ErrorCodeBozoTorrent ErrorCode = "BOZO_TORRENT"
ErrorCodeNoServersAvailableError ErrorCode = "NO_SERVERS_AVAILABLE_ERROR"
ErrorCodeMonthlyLimit ErrorCode = "MONTHLY_LIMIT"
ErrorCodeCooldownLimit ErrorCode = "COOLDOWN_LIMIT"
ErrorCodeActiveLimit ErrorCode = "ACTIVE_LIMIT"
ErrorCodeDownloadServerError ErrorCode = "DOWNLOAD_SERVER_ERROR"
ErrorCodeBozoNzb ErrorCode = "BOZO_NZB"
ErrorCodeSearchError ErrorCode = "SEARCH_ERROR"
ErrorCodeInvalidDevice ErrorCode = "INVALID_DEVICE"
ErrorCodeDiffIssue ErrorCode = "DIFF_ISSUE"
ErrorCodeLinkOffline ErrorCode = "LINK_OFFLINE"
ErrorCodeVendorDisabled ErrorCode = "VENDOR_DISABLED"
)

type ResponseError struct {
Detail string `json:"detail"`
Err ErrorCode `json:"error"`
}

func (e *ResponseError) Error() string {
ret, _ := json.Marshal(e)
return string(ret)
}

type Response[T any] struct {
Success bool `json:"success"`
Data T `json:"data,omitempty"`
Detail string `json:"detail"`
Error ErrorCode `json:"error,omitempty"`
}

type ResponseEnvelop interface {
IsSuccess() bool
GetError() *ResponseError
}

func (r Response[any]) IsSuccess() bool {
return r.Success
}

func (r Response[any]) GetError() *ResponseError {
if r.IsSuccess() {
return nil
}
return &ResponseError{
Err: r.Error,
Detail: r.Detail,
}
}

type APIResponse[T any] struct {
Header http.Header
StatusCode int
Data T
Detail string
}

func newAPIResponse[T any](res *http.Response, data T, detail string) APIResponse[T] {
return APIResponse[T]{
Header: res.Header,
StatusCode: res.StatusCode,
Data: data,
Detail: detail,
}
}

func extractResponseError(statusCode int, body []byte, v ResponseEnvelop) error {
if !v.IsSuccess() {
return v.GetError()
}
if statusCode >= http.StatusBadRequest {
return errors.New(string(body))
}
return nil
}

func processResponseBody(res *http.Response, err error, v ResponseEnvelop) error {
if err != nil {
return err
}

body, err := io.ReadAll(res.Body)
defer res.Body.Close()

if err != nil {
return err
}

err = core.UnmarshalJSON(res.StatusCode, body, v)
if err != nil {
return err
}

return extractResponseError(res.StatusCode, body, v)
}
Loading

0 comments on commit 23d5cfd

Please sign in to comment.