-
-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(store): initial torbox integration
- Loading branch information
1 parent
2aa59ff
commit 23d5cfd
Showing
9 changed files
with
839 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | ||
} |
Oops, something went wrong.