From f6a32e532658e79cbfc630ffe51c751f0076d3ee Mon Sep 17 00:00:00 2001 From: Munif Tanjim Date: Mon, 4 Nov 2024 09:47:59 +0600 Subject: [PATCH] feat(store): initial torbox integration --- core/util.go | 5 +- internal/endpoint/store.go | 4 + store/store.go | 3 +- store/torbox/client.go | 102 +++++++++++++++ store/torbox/error.go | 34 +++++ store/torbox/response.go | 137 ++++++++++++++++++++ store/torbox/store.go | 254 +++++++++++++++++++++++++++++++++++++ store/torbox/torrent.go | 192 ++++++++++++++++++++++++++++ store/torbox/user.go | 47 +++++++ 9 files changed, 776 insertions(+), 2 deletions(-) create mode 100644 store/torbox/client.go create mode 100644 store/torbox/error.go create mode 100644 store/torbox/response.go create mode 100644 store/torbox/store.go create mode 100644 store/torbox/torrent.go create mode 100644 store/torbox/user.go diff --git a/core/util.go b/core/util.go index 30b5e65..f24fdb0 100644 --- a/core/util.go +++ b/core/util.go @@ -59,6 +59,9 @@ func ParseMagnetLink(value string) (MagnetLink, error) { magnet.Trackers = params["tr"] params.Del("tr") } - magnet.Link = "magnet:?xt=" + xt + "&dn=" + magnet.Name + magnet.Link = "magnet:?xt=" + xt + if magnet.Name != "" { + magnet.Link = magnet.Link + "&dn=" + magnet.Name + } return magnet, nil } diff --git a/internal/endpoint/store.go b/internal/endpoint/store.go index f86f37b..7c0704f 100644 --- a/internal/endpoint/store.go +++ b/internal/endpoint/store.go @@ -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" ) @@ -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) @@ -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 } diff --git a/store/store.go b/store/store.go index 47d3008..ece4f19 100644 --- a/store/store.go +++ b/store/store.go @@ -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)) diff --git a/store/torbox/client.go b/store/torbox/client.go new file mode 100644 index 0000000..bf6d3d7 --- /dev/null +++ b/store/torbox/client.go @@ -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 +} diff --git a/store/torbox/error.go b/store/torbox/error.go new file mode 100644 index 0000000..f35e47b --- /dev/null +++ b/store/torbox/error.go @@ -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 +} diff --git a/store/torbox/response.go b/store/torbox/response.go new file mode 100644 index 0000000..a21ccfa --- /dev/null +++ b/store/torbox/response.go @@ -0,0 +1,137 @@ +package torbox + +import ( + "encoding/json" + "errors" + "io" + "log" + "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() + + log.Println("res body:", string(body)) + + if err != nil { + return err + } + + err = core.UnmarshalJSON(res.StatusCode, body, v) + if err != nil { + return err + } + + return extractResponseError(res.StatusCode, body, v) +} diff --git a/store/torbox/store.go b/store/torbox/store.go new file mode 100644 index 0000000..c9f797e --- /dev/null +++ b/store/torbox/store.go @@ -0,0 +1,254 @@ +package torbox + +import ( + "errors" + "strconv" + "strings" + + "github.com/MunifTanjim/stremthru/core" + "github.com/MunifTanjim/stremthru/store" +) + +type StoreClient struct { + Name store.StoreName + client *APIClient +} + +func NewStoreClient() *StoreClient { + c := &StoreClient{} + c.client = NewAPIClient(&APIClientConfig{}) + c.Name = store.StoreNameTorBox + return c +} + +func (c *StoreClient) GetName() store.StoreName { + return c.Name +} + +func (c *StoreClient) GetUser(params *store.GetUserParams) (*store.User, error) { + res, err := c.client.GetUser(&GetUserParams{ + Ctx: params.Ctx, + Settings: true, + }) + if err != nil { + return nil, err + } + data := &store.User{ + Id: strconv.Itoa(res.Data.Id), + Email: res.Data.Email, + } + if res.Data.Plan == PlanFree { + data.SubscriptionStatus = store.UserSubscriptionStatusTrial + } else { + data.SubscriptionStatus = store.UserSubscriptionStatusPremium + } + return data, nil +} + +func (c *StoreClient) CheckMagnet(params *store.CheckMagnetParams) (*store.CheckMagnetData, error) { + magnetByHash := map[string]core.MagnetLink{} + hashes := []string{} + for _, m := range params.Magnets { + magnet, err := core.ParseMagnetLink(m) + if err != nil { + return nil, err + } + hash := strings.ToLower(magnet.Hash) + magnetByHash[hash] = magnet + hashes = append(hashes, hash) + } + res, err := c.client.CheckTorrentsCached(&CheckTorrentsCachedParams{ + Ctx: params.Ctx, + Hashes: hashes, + ListFiles: true, + }) + if err != nil { + return nil, err + } + tByHash := map[string]CheckTorrentsCachedDataItem{} + for _, t := range res.Data { + tByHash[strings.ToLower(t.Hash)] = t + } + data := &store.CheckMagnetData{} + for _, hash := range hashes { + m := magnetByHash[hash] + item := store.CheckMagnetDataItem{ + Hash: m.Hash, + Magnet: m.Link, + Status: store.MagnetStatusUnknown, + Files: []store.MagnetFile{}, + } + if t, ok := tByHash[hash]; ok { + item.Status = store.MagnetStatusCached + for idx, f := range t.Files { + item.Files = append(item.Files, store.MagnetFile{ + Idx: idx, + Name: f.Name, + Size: f.Size, + }) + } + } + data.Items = append(data.Items, item) + } + return data, nil +} + +type LockedFileLink string + +const lockedFileLinkPrefix = "stremthru://store/torbox/" + +func (l LockedFileLink) encodeData(torrentId int, torrentFileId int) string { + return core.Base64Encode(strconv.Itoa(torrentId) + ":" + strconv.Itoa(torrentFileId)) +} + +func (l LockedFileLink) decodeData(encoded string) (torrentId, torrentFileId int, err error) { + decoded, err := core.Base64Decode(encoded) + if err != nil { + return 0, 0, err + } + tId, tfId, found := strings.Cut(decoded, ":") + if !found { + return 0, 0, errors.New("invalid link") + } + torrentId, err = strconv.Atoi(tId) + if err != nil { + return 0, 0, err + } + torrentFileId, err = strconv.Atoi(tfId) + if err != nil { + return 0, 0, err + } + return torrentId, torrentFileId, nil +} + +func (l LockedFileLink) create(torrentId int, torrentFileId int) string { + return lockedFileLinkPrefix + l.encodeData(torrentId, torrentFileId) +} + +func (l LockedFileLink) parse() (torrentId, torrentFileId int, err error) { + encoded := strings.TrimPrefix(string(l), lockedFileLinkPrefix) + return l.decodeData(encoded) +} + +func (c *StoreClient) AddMagnet(params *store.AddMagnetParams) (*store.AddMagnetData, error) { + magnet, err := core.ParseMagnetLink(params.Magnet) + if err != nil { + return nil, err + } + res, err := c.client.CreateTorrent(&CreateTorrentParams{ + Ctx: params.Ctx, + Magnet: magnet.Link, + AllowZip: false, + }) + if err != nil { + return nil, err + } + data := &store.AddMagnetData{ + Id: strconv.Itoa(res.Data.TorrentId), + Hash: res.Data.Hash, + Magnet: magnet.Link, + Name: res.Data.Name, + Status: store.MagnetStatusQueued, + Files: []store.MagnetFile{}, + } + detail := strings.ToLower(res.Detail) + if strings.Contains(detail, "found cached") || strings.Contains(detail, "cached torrent") { + t, err := c.client.GetTorrent(&GetTorrentParams{ + Ctx: params.Ctx, + Id: res.Data.TorrentId, + BypassCache: true, + }) + if err != nil { + return nil, err + } + for _, f := range t.Data.Files { + file := store.MagnetFile{ + Idx: f.Id, + Link: LockedFileLink("").create(res.Data.TorrentId, f.Id), + Name: f.ShortName, + Path: f.Name, + Size: f.Size, + } + data.Files = append(data.Files, file) + } + } + return data, nil +} + +func (c *StoreClient) GenerateLink(params *store.GenerateLinkParams) (*store.GenerateLinkData, error) { + panic("unimplemented") +} + +func (c *StoreClient) GetMagnet(params *store.GetMagnetParams) (*store.GetMagnetData, error) { + id, err := strconv.Atoi(params.Id) + if err != nil { + return nil, err + } + res, err := c.client.GetTorrent(&GetTorrentParams{ + Ctx: params.Ctx, + Id: id, + BypassCache: true, + }) + data := &store.GetMagnetData{ + Id: params.Id, + Name: res.Data.Name, + Status: store.MagnetStatusUnknown, + Files: []store.MagnetFile{}, + } + if res.Data.DownloadFinished && res.Data.DownloadPresent { + data.Status = store.MagnetStatusDownloaded + } + for _, f := range res.Data.Files { + file := store.MagnetFile{ + Idx: f.Id, + Link: LockedFileLink("").create(res.Data.Id, f.Id), + Name: f.ShortName, + Path: f.Name, + Size: f.Size, + } + data.Files = append(data.Files, file) + } + return data, nil +} + +func (c *StoreClient) ListMagnets(params *store.ListMagnetsParams) (*store.ListMagnetsData, error) { + res, err := c.client.ListTorrents(&ListTorrentsParams{ + Ctx: params.Ctx, + BypassCache: true, + Offset: 0, + Limit: 0, + }) + if err != nil { + return nil, err + } + data := &store.ListMagnetsData{} + for _, t := range res.Data { + item := store.ListMagnetsDataItem{ + Id: strconv.Itoa(t.Id), + Name: t.Name, + Status: store.MagnetStatusUnknown, + } + if t.DownloadFinished && t.DownloadPresent { + item.Status = store.MagnetStatusDownloaded + } + data.Items = append(data.Items, item) + } + return data, nil +} + +func (c *StoreClient) RemoveMagnet(params *store.RemoveMagnetParams) (*store.RemoveMagnetData, error) { + id, err := strconv.Atoi(params.Id) + if err != nil { + return nil, err + } + _, err = c.client.ControlTorrent(&ControlTorrentParams{ + Ctx: params.Ctx, + TorrentId: id, + Operation: ControlTorrentOperationDelete, + }) + if err != nil { + return nil, err + } + data := &store.RemoveMagnetData{Id: params.Id} + return data, nil +} diff --git a/store/torbox/torrent.go b/store/torbox/torrent.go new file mode 100644 index 0000000..6f62f21 --- /dev/null +++ b/store/torbox/torrent.go @@ -0,0 +1,192 @@ +package torbox + +import ( + "net/url" + "strconv" +) + +type CheckTorrentsCachedDataItemFile struct { + Name string `json:"name"` + Size int `json:"size"` +} + +type CheckTorrentsCachedDataItem struct { + Name string `json:"name"` + Size int `json:"size"` + Hash string `json:"hash"` + Files []CheckTorrentsCachedDataItemFile `json:"files"` +} + +type CheckTorrentsCachedData []CheckTorrentsCachedDataItem + +type CheckTorrentsCachedParams struct { + Ctx + Hashes []string + ListFiles bool +} + +func (c APIClient) CheckTorrentsCached(params *CheckTorrentsCachedParams) (APIResponse[CheckTorrentsCachedData], error) { + form := &url.Values{"hash": params.Hashes} + form.Add("format", "list") + form.Add("list_files", strconv.FormatBool(params.ListFiles)) + params.Form = form + response := &Response[CheckTorrentsCachedData]{} + res, err := c.Request("GET", "/v1/api/torrents/checkcached", params, response) + return newAPIResponse(res, response.Data, response.Detail), err +} + +type CreateTorrentData struct { + TorrentId int `json:"torrent_id"` + Name string `json:"name"` + Hash string `json:"hash"` + AuthId string `json:"auth_id"` +} + +type CreateTorrentParamsSeed int + +const ( + CreateTorrentParamsSeedAuto CreateTorrentParamsSeed = 1 + CreateTorrentParamsSeedYes CreateTorrentParamsSeed = 2 + CreateTorrentParamsSeedNo CreateTorrentParamsSeed = 3 +) + +type CreateTorrentParams struct { + Ctx + Magnet string + Seed int + AllowZip bool + Name string +} + +/* +Possible Detail values: + - Found Cached Torrent. Using Cached Torrent. +*/ +func (c APIClient) CreateTorrent(params *CreateTorrentParams) (APIResponse[CreateTorrentData], error) { + form := &url.Values{} + form.Add("magnet", params.Magnet) + if params.Seed == 0 { + params.Seed = int(CreateTorrentParamsSeedAuto) + } + form.Add("seed", strconv.Itoa(int(params.Seed))) + form.Add("allow_zip", strconv.FormatBool(params.AllowZip)) + if params.Name != "" { + form.Add("name", params.Name) + } + params.Form = form + response := &Response[CreateTorrentData]{} + res, err := c.Request("POST", "/v1/api/torrents/createtorrent", params, response) + return newAPIResponse(res, response.Data, response.Detail), err +} + +type TorrentFile struct { + Id int `json:"id"` + MD5 string `json:"md5"` + S3Path string `json:"s3_path"` + Name string `json:"name"` + Size int `json:"size"` + MimeType string `json:"mimetype"` + ShortName string `json:"short_name"` +} + +type TorrentDownloadState string + +const ( + TorrentDownloadStateDownloading TorrentDownloadState = "downloading" +) + +type Torrent struct { + Id int `json:"id"` + Hash string `json:"hash"` + CreatedAt string `json:"created_at"` + UpdatedAt string `json:"updated_at"` + Magnet string `json:"magnet"` + Size int `json:"size"` + Active bool `json:"active"` + AuthId string `json:"auth_id"` + DownloadState TorrentDownloadState `json:"download_state"` + Seeds int `json:"seeds"` + Peers int `json:"peers"` + Ratio int `json:"ratio"` + Progress int `json:"progress"` + DownloadSpeed int `json:"download_speed"` + UploadSpeed int `json:"upload_speed"` + Name string `json:"name"` + ETA int `json:"eta"` + Server int `json:"server"` + TorrentFile bool `json:"torrent_file"` + ExpiresAt string `json:"expires_at"` + DownloadPresent bool `json:"download_present"` + DownloadFinished bool `json:"download_finished"` + Files []TorrentFile `json:"files"` + InactiveCheck int `json:"inactive_check"` + Availability int `json:"availability"` +} + +type ListTorrentsData []Torrent + +type ListTorrentsParams struct { + Ctx + BypassCache bool + Offset int // default: 0 + Limit int // default: 1000 +} + +func (c APIClient) ListTorrents(params *ListTorrentsParams) (APIResponse[ListTorrentsData], error) { + form := &url.Values{} + form.Add("bypass_cache", strconv.FormatBool(params.BypassCache)) + if params.Offset != 0 { + form.Add("offset", strconv.Itoa(params.Offset)) + } + if params.Limit != 0 { + form.Add("limit", strconv.Itoa(params.Limit)) + } + params.Form = form + response := &Response[ListTorrentsData]{} + res, err := c.Request("GET", "/v1/api/torrents/mylist", params, response) + return newAPIResponse(res, response.Data, response.Detail), err +} + +type GetTorrentData Torrent + +const ( + ControlTorrentOperationReannounce ControlTorrentOperation = "reannounce" + ControlTorrentOperationDelete ControlTorrentOperation = "delete" + ControlTorrentOperationResume ControlTorrentOperation = "resume" + ControlTorrentOperationPause ControlTorrentOperation = "pause" +) + +type GetTorrentParams struct { + Ctx + Id int + BypassCache bool +} + +func (c APIClient) GetTorrent(params *GetTorrentParams) (APIResponse[GetTorrentData], error) { + form := &url.Values{} + form.Add("bypass_cache", strconv.FormatBool(params.BypassCache)) + form.Add("id", strconv.Itoa(params.Id)) + params.Form = form + response := &Response[GetTorrentData]{} + res, err := c.Request("GET", "/v1/api/torrents/mylist", params, response) + return newAPIResponse(res, response.Data, response.Detail), err +} + +type ControlTorrentOperation string + +type ControlTorrentParams struct { + Ctx + TorrentId int `json:"torrent_id"` + Operation ControlTorrentOperation `json:"operation"` + All bool `json:"all"` +} + +type ControlTorrentData struct { +} + +func (c APIClient) ControlTorrent(params *ControlTorrentParams) (APIResponse[ControlTorrentData], error) { + params.JSON = params + response := &Response[ControlTorrentData]{} + res, err := c.Request("POST", "/v1/api/torrents/controltorrent", params, response) + return newAPIResponse(res, response.Data, response.Detail), err +} diff --git a/store/torbox/user.go b/store/torbox/user.go new file mode 100644 index 0000000..66b07ee --- /dev/null +++ b/store/torbox/user.go @@ -0,0 +1,47 @@ +package torbox + +import ( + "net/url" + "strconv" +) + +type Plan int + +const ( + PlanFree = iota + PlanEssential + PlanPro + PlanStandard +) + +type GetUserData struct { + Id int `json:"id"` + CreatedAt string `json:"created_at"` + UpdatedAt string `json:"updated_at"` + Email string `json:"email"` + Plan Plan `json:"plan"` + TotalDownloaded int `json:"total_downloaded"` + Customer string `json:"customer"` + Server int `json:"server"` + IsSubscribed bool `json:"is_subscribed"` + PremiumExpiresAt string `json:"premium_expires_at"` + CooldownUntil string `json:"cooldown_until"` + AuthId string `json:"auth_id"` + UserReferral string `json:"user_referral"` + BaseEmail string `json:"base_email"` + Settings *map[string]any `json:"settings,omitempty"` +} + +type GetUserParams struct { + Ctx + Settings bool // Allows you to retrieve user settings. +} + +func (c APIClient) GetUser(params *GetUserParams) (APIResponse[GetUserData], error) { + form := &url.Values{} + form.Add("settings", strconv.FormatBool(params.Settings)) + params.Form = form + response := &Response[GetUserData]{} + res, err := c.Request("GET", "/v1/api/user/me", params, response) + return newAPIResponse(res, response.Data, response.Detail), err +}