Skip to content

Commit

Permalink
feat(store): initial debridlink integration
Browse files Browse the repository at this point in the history
  • Loading branch information
MunifTanjim committed Nov 3, 2024
1 parent 1e0d576 commit 07fe97d
Show file tree
Hide file tree
Showing 9 changed files with 700 additions and 1 deletion.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ Companion for Stremio.
### Store Integration

- [AllDebrid](https://alldebrid.com)
- [Debrid-Link](https://debrid-link.com)
- [Premiumize](https://www.premiumize.me)
- [RealDebrid](https://real-debrid.com) _(Planned)_

Expand Down
4 changes: 4 additions & 0 deletions internal/endpoint/store.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import (
"github.com/MunifTanjim/stremthru/internal/context"
"github.com/MunifTanjim/stremthru/store"
"github.com/MunifTanjim/stremthru/store/alldebrid"
"github.com/MunifTanjim/stremthru/store/debridlink"
"github.com/MunifTanjim/stremthru/store/premiumize"
"github.com/golang-jwt/jwt/v5"
)
Expand Down Expand Up @@ -48,6 +49,7 @@ func getStoreAuthToken(r *http.Request) string {
}

var adStore = alldebrid.NewStore()
var dlStore = debridlink.NewStoreClient()
var pmStore = premiumize.NewStoreClient(&premiumize.StoreClientConfig{})

func getStore(r *http.Request) (store.Store, error) {
Expand All @@ -60,6 +62,8 @@ func getStore(r *http.Request) (store.Store, error) {
switch name {
case store.StoreNameAlldebrid:
return adStore, nil
case store.StoreNameDebridLink:
return dlStore, nil
case store.StoreNamePremiumize:
return pmStore, nil
default:
Expand Down
41 changes: 41 additions & 0 deletions store/debridlink/account.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
package debridlink

type GetAccountInfoDataSettings struct {
Https bool `json:"https"`
ThemeDark bool `json:"themeDark"`
HideOldLinks bool `json:"hideOldLinks"`
Cdn string `json:"cdn"`
TwofaType string `json:"twofaType"`
EmailsNews bool `json:"emailsNews"`
EmailsAccount bool `json:"emailsAccount"`
EmailsSupport bool `json:"emailsSupport"`
}

type GetAccountInfoData struct {
Email string `json:"email"`
EmailVerified bool `json:"emailVerified"`
AccountType int `json:"accountType"`
PremiumLeft int `json:"premiumLeft"`
Pts int `json:"pts"`
Trafficshare int `json:"trafficshare"`
VouchersUrl string `json:"vouchersUrl"`
EditPasswordUrl string `json:"editPasswordUrl"`
EditEmailUrl string `json:"editEmailUrl"`
ViewSessidUrl string `json:"viewSessidUrl"`
UpgradeAccountUrl string `json:"upgradeAccountUrl"`
RegisterDate string `json:"registerDate"`
ServerDetected bool `json:"serverDetected"`
Settings GetAccountInfoDataSettings `json:"settings"`
AvatarUrl string `json:"avatarUrl"`
Username string `json:"username"`
}

type GetAccountInfoParams struct {
Ctx
}

func (c APIClient) GetAccountInfo(params *GetAccountInfoParams) (APIResponse[GetAccountInfoData], error) {
response := &Response[GetAccountInfoData]{}
res, err := c.Request("GET", "/v2/account/infos", params, response)
return newAPIResponse(res, response.Value), err
}
109 changes: 109 additions & 0 deletions store/debridlink/client.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
package debridlink

import (
"context"
"io"
"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
HTTPClient *http.Client
apiKey string
agent string
}

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

if conf.BaseURL == "" {
conf.BaseURL = "https://debrid-link.com/api"
}

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 RequestContext interface {
GetContext() context.Context
PrepareBody(method string, query *url.Values) (body io.Reader, contentType string, err error)
}

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.Set("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.StoreNameAlldebrid)
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
}
83 changes: 83 additions & 0 deletions store/debridlink/error.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
package debridlink

import (
"net/http"

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

type ErrorCode = core.ErrorCode

const (
ErrorCodeBadToken ErrorCode = "badToken"
ErrorCodeBadSign ErrorCode = "badSign"
ErrorCodeHidedToken ErrorCode = "hidedToken"
ErrorCodeServerError ErrorCode = "server_error"
ErrorCodeAccessDenied ErrorCode = "access_denied"
ErrorCodeAuthorizationPending ErrorCode = "authorization_pending"
ErrorCodeUnsupportedGrantType ErrorCode = "unsupported_grant_type"
ErrorCodeUnsupportedResponseType ErrorCode = "unsupported_response_type"
ErrorCodeInvalidRequest ErrorCode = "invalid_request"
ErrorCodeInvalidScope ErrorCode = "invalid_scope"
ErrorCodeExpiredToken ErrorCode = "expired_token"
ErrorCodeUnauthorizedClient ErrorCode = "unauthorized_client"
ErrorCodeInvalidClient ErrorCode = "invalid_client"
ErrorCodeUnknowR ErrorCode = "unknowR"
ErrorCodeInternalError ErrorCode = "internalError"
ErrorCodeBadArguments ErrorCode = "badArguments"
ErrorCodeBadId ErrorCode = "badId"
ErrorCodeFloodDetected ErrorCode = "floodDetected"
ErrorCodeServerNotAllowed ErrorCode = "serverNotAllowed"
ErrorCodeFreeServerOverload ErrorCode = "freeServerOverload"
ErrorCodeMaxAttempts ErrorCode = "maxAttempts"
ErrorCodeCaptchaRequired ErrorCode = "captchaRequired"
ErrorCodeAccountLocked ErrorCode = "accountLocked"
ErrorCodeNotDebrid ErrorCode = "notDebrid"
ErrorCodeHostNotValid ErrorCode = "hostNotValid"
ErrorCodeFileNotFound ErrorCode = "fileNotFound"
ErrorCodeFileNotAvailable ErrorCode = "fileNotAvailable"
ErrorCodeBadFileUrl ErrorCode = "badFileUrl"
ErrorCodeBadFilePassword ErrorCode = "badFilePassword"
ErrorCodeNotFreeHost ErrorCode = "notFreeHost"
ErrorCodeMaintenanceHost ErrorCode = "maintenanceHost"
ErrorCodeNoServerHost ErrorCode = "noServerHost"
ErrorCodeMaxLink ErrorCode = "maxLink"
ErrorCodeMaxLinkHost ErrorCode = "maxLinkHost"
ErrorCodeMaxData ErrorCode = "maxData"
ErrorCodeMaxDataHost ErrorCode = "maxDataHost"
ErrorCodeDisabledServerHost ErrorCode = "disabledServerHost"
ErrorCodeNotAddTorrent ErrorCode = "notAddTorrent"
ErrorCodeTorrentTooBig ErrorCode = "torrentTooBig"
ErrorCodeMaxTorrent ErrorCode = "maxTorrent"
)

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

if rerr, ok := cause.(*ResponseError); ok {
if rerr.ErrDesc != "" {
err.Msg = rerr.ErrDesc
} else {
err.Msg = "Debrid-Link Error Code: " + string(rerr.Err)
}
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
}
99 changes: 99 additions & 0 deletions store/debridlink/response.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
package debridlink

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

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

type ResponseError struct {
Err ErrorCode `json:"error,omitempty"`
ErrId string `json:"error_id,omitempty"`
ErrDesc string `json:"error_description,omitempty"`
}

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

type ResponsePagination struct {
Page int `json:"page"`
Pages int `json:"pages"`
Next int `json:"next"`
Previous int `json:"previous"`
}

type Response[T interface{}] struct {
*ResponseError
Success bool `json:"success"`
Value T `json:"value,omitempty"`
}

type PaginatedResponse[T interface{}] struct {
Response[[]T]
Pagination ResponsePagination `json:"pagination"`
}

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 r.ResponseError
}

type APIResponse[T interface{}] struct {
Header http.Header
StatusCode int
Data T
}

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

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 07fe97d

Please sign in to comment.