Files
polaris/pkg/go-qbittorrent/qbt/api.go
2025-05-07 18:16:10 +08:00

1163 lines
33 KiB
Go

package qbt
import (
"bytes"
"encoding/json"
"errors"
"io"
"log"
"mime/multipart"
"net/http"
"net/http/cookiejar"
"os"
"path"
"net/url"
"strconv"
"strings"
wrapper "github.com/pkg/errors"
"golang.org/x/net/publicsuffix"
)
// ErrBadPriority means the priority is not allowd by qbittorrent
var ErrBadPriority = errors.New("priority not available")
// ErrBadResponse means that qbittorrent sent back an unexpected response
var ErrBadResponse = errors.New("received bad response")
// delimit puts list into a combined (single element) map with all items connected separated by the delimiter
// this is how the WEBUI API recognizes multiple items
func delimit(items []string, delimiter string) (delimited string) {
for i, v := range items {
if i > 0 {
delimited += delimiter + v
} else {
delimited = v
}
}
return delimited
}
// Client creates a connection to qbittorrent and performs requests
type Client struct {
http *http.Client
URL string
Authenticated bool
Jar http.CookieJar
}
// NewClient creates a new client connection to qbittorrent
func NewClient(url string) *Client {
client := &Client{}
// ensure url ends with "/"
if url[len(url)-1:] != "/" {
url += "/"
}
client.URL = url
// create cookie jar
client.Jar, _ = cookiejar.New(&cookiejar.Options{PublicSuffixList: publicsuffix.List})
client.http = &http.Client{
Jar: client.Jar,
}
return client
}
// get will perform a GET request with no parameters
func (client *Client) get(endpoint string, opts map[string]string) (*http.Response, error) {
req, err := http.NewRequest("GET", client.URL+endpoint, nil)
if err != nil {
return nil, wrapper.Wrap(err, "failed to build request")
}
//add user-agent header to allow qbittorrent to identify us
req.Header.Set("User-Agent", "go-qbittorrent v0.1")
//add optional parameters that the user wants
if opts != nil {
query := req.URL.Query()
for k, v := range opts {
query.Add(k, v)
}
req.URL.RawQuery = query.Encode()
}
resp, err := client.http.Do(req)
if err != nil {
return nil, wrapper.Wrap(err, "failed to perform request")
}
return resp, nil
}
func (cleint *Client) postJson(endpoint string, body any) (*http.Response, error) {
var buff bytes.Buffer
buff.WriteString("json=")
d, err := json.Marshal(body)
if err!= nil {
return nil, err
}
buff.Write(d)
log.Println(buff.String())
req, err := http.NewRequest("POST", cleint.URL+endpoint, &buff)
if err!= nil {
return nil, err
}
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
req.Header.Set("User-Agent", "go-qbittorrent v0.1")
resp, err := cleint.http.Do(req)
if err!= nil {
return nil, err
}
return resp, nil
}
// post will perform a POST request with no content-type specified
func (client *Client) post(endpoint string, opts map[string]string) (*http.Response, error) {
// add optional parameters that the user wants
form := url.Values{}
for k, v := range opts {
form.Add(k, v)
}
req, err := http.NewRequest("POST", client.URL+endpoint, strings.NewReader(form.Encode()))
if err != nil {
return nil, wrapper.Wrap(err, "failed to build request")
}
// add the content-type so qbittorrent knows what to expect
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
// add user-agent header to allow qbittorrent to identify us
req.Header.Set("User-Agent", "go-qbittorrent v0.1")
resp, err := client.http.Do(req)
if err != nil {
return nil, wrapper.Wrap(err, "failed to perform request")
}
return resp, nil
}
// postMultipart will perform a multiple part POST request
func (client *Client) postMultipart(endpoint string, buffer bytes.Buffer, contentType string) (resp *http.Response, err error) {
req, err := http.NewRequest("POST", client.URL+endpoint, &buffer)
if err != nil {
return nil, wrapper.Wrap(err, "error creating request")
}
// add the content-type so qbittorrent knows what to expect
req.Header.Set("Content-Type", contentType)
// add user-agent header to allow qbittorrent to identify us
req.Header.Set("User-Agent", "go-qbittorrent v0.2")
resp, err = client.http.Do(req)
if err != nil {
return nil, wrapper.Wrap(err, "failed to perform request")
}
return resp, nil
}
// writeOptions will write a map to the buffer through multipart.NewWriter
func writeOptions(writer *multipart.Writer, opts map[string]string) (err error) {
for key, val := range opts {
if err := writer.WriteField(key, val); err != nil {
return err
}
}
return nil
}
// postMultipartData will perform a multiple part POST request without a file
func (client *Client) postMultipartData(endpoint string, opts map[string]string) (*http.Response, error) {
var buffer bytes.Buffer
writer := multipart.NewWriter(&buffer)
// write the options to the buffer
// will contain the link string
if err := writeOptions(writer, opts); err != nil {
return nil, wrapper.Wrap(err, "failed to write options")
}
// close the writer before doing request to get closing line on multipart request
if err := writer.Close(); err != nil {
return nil, wrapper.Wrap(err, "failed to close writer")
}
resp, err := client.postMultipart(endpoint, buffer, writer.FormDataContentType())
if err != nil {
return nil, err
}
return resp, nil
}
// postMultipartFile will perform a multiple part POST request with a file
func (client *Client) postMultipartFile(endpoint string, fileName string, opts map[string]string) (*http.Response, error) {
var buffer bytes.Buffer
writer := multipart.NewWriter(&buffer)
// open the file for reading
file, err := os.Open(fileName)
if err != nil {
return nil, wrapper.Wrap(err, "error opening file")
}
// defer the closing of the file until the end of function
// so we can still copy its contents
defer file.Close()
// write the options to the buffer
writeOptions(writer, opts)
// create form for writing the file to and give it the filename
formWriter, err := writer.CreateFormFile("torrents", path.Base(fileName))
if err != nil {
return nil, wrapper.Wrap(err, "error adding file")
}
// copy the file contents into the form
if _, err = io.Copy(formWriter, file); err != nil {
return nil, wrapper.Wrap(err, "error copying file")
}
// close the writer before doing request to get closing line on multipart request
if err := writer.Close(); err != nil {
return nil, wrapper.Wrap(err, "failed to close writer")
}
resp, err := client.postMultipart(endpoint, buffer, writer.FormDataContentType())
if err != nil {
return nil, err
}
return resp, nil
}
// Application endpoints
// Login logs you in to the qbittorrent client
// returns the current authentication status
func (client *Client) Login(opts LoginOptions) (err error) {
params := map[string]string{}
if opts.Username != "" {
params["username"] = opts.Username
}
if opts.Password != "" {
params["password"] = opts.Password
}
resp, err := client.post("api/v2/auth/login", params)
if err != nil {
return err
} else if resp.StatusCode == 403 {
return wrapper.Errorf("User's IP is banned for too many failed login attempts")
}
// add the cookie to cookie jar to authenticate later requests
if cookies := resp.Cookies(); len(cookies) > 0 {
cookieURL, _ := url.Parse("http://localhost:8080")
client.Jar.SetCookies(cookieURL, cookies)
// create a new client with the cookie jar and replace the old one
// so that all our later requests are authenticated
client.http = &http.Client{
Jar: client.Jar,
}
} else {
return wrapper.Errorf("Could not get cookie")
}
// change authentication status so we know were authenticated in later requests
client.Authenticated = true
return nil
}
// Logout logs you out of the qbittorrent client
// returns the current authentication status
func (client *Client) Logout() (err error) {
resp, err := client.post("api/v2/auth/logout", nil)
if err != nil {
return err
}
// change authentication status so we know were not authenticated in later requests
client.Authenticated = (*resp).StatusCode == 200
if (*resp).StatusCode != 200 {
return wrapper.Errorf("An unknown error occurred causing a status code of: %d", (*resp).StatusCode)
}
return
}
// ApplicationVersion of the qbittorrent client
func (client *Client) ApplicationVersion() (version string, err error) {
resp, err := client.post("api/v2/app/version", nil)
if err != nil {
return version, err
}
buf := new(strings.Builder)
io.Copy(buf, resp.Body)
version = buf.String()
return
}
// WebAPIVersion of the qbittorrent client
func (client *Client) WebAPIVersion() (version string, err error) {
resp, err := client.post("api/v2/app/webapiVersion", nil)
if err != nil {
return version, err
}
buf := new(strings.Builder)
io.Copy(buf, resp.Body)
version = buf.String()
return
}
// BuildInfo of the qbittorrent client
func (client *Client) BuildInfo() (buildInfo BuildInfo, err error) {
resp, err := client.get("api/v2/app/buildInfo", nil)
if err != nil {
return buildInfo, err
}
json.NewDecoder(resp.Body).Decode(&buildInfo)
return buildInfo, err
}
// Preferences of the qbittorrent client
func (client *Client) Preferences() (prefs Preferences, err error) {
resp, err := client.get("api/v2/app/preferences", nil)
if err != nil {
return prefs, err
}
json.NewDecoder(resp.Body).Decode(&prefs)
return prefs, err
}
// SetPreferences of the qbittorrent client
func (client *Client) SetPreferences(m map[string]any) (prefsSet bool, err error) {
resp, err := client.postJson("api/v2/app/setPreferences", m)
return (resp.Status == "200 OK"), err
}
// DefaultSavePath of the qbittorrent client
func (client *Client) DefaultSavePath() (path string, err error) {
resp, err := client.get("api/v2/app/defaultSavePath", nil)
if err != nil {
return path, err
}
buf := new(strings.Builder)
io.Copy(buf, resp.Body)
path = buf.String()
return
}
// Shutdown shuts down the qbittorrent client
func (client *Client) Shutdown() (shuttingDown bool, err error) {
resp, err := client.get("api/v2/app/shutdown", nil)
// return true if successful
return (resp.Status == "200 OK"), err
}
// Log Endpoints
// Logs of the qbittorrent client
func (client *Client) Logs(filters map[string]string) (logs []Log, err error) {
resp, err := client.get("api/v2/log/main", filters)
if err != nil {
return logs, err
}
json.NewDecoder(resp.Body).Decode(&logs)
return logs, err
}
// PeerLogs of the qbittorrent client
func (client *Client) PeerLogs(filters map[string]string) (logs []PeerLog, err error) {
resp, err := client.get("api/v2/log/peers", filters)
if err != nil {
return logs, err
}
json.NewDecoder(resp.Body).Decode(&logs)
return logs, err
}
// TODO: Sync Endpoints
// TODO: Transfer Endpoints
// Info returns info you usually see in qBt status bar.
func (client *Client) Info(opts InfoOptions) (info Info, err error) {
resp, err := client.get("api/v2/transfer/info", nil)
if err != nil {
return info, err
}
json.NewDecoder(resp.Body).Decode(&info)
return info, err
}
// AltSpeedLimitsEnabled returns info you usually see in qBt status bar.
func (client *Client) AltSpeedLimitsEnabled() (mode bool, err error) {
resp, err := client.get("api/v2/transfer/speedLimitsMode", nil)
if err != nil {
return mode, err
}
var decoded int
json.NewDecoder(resp.Body).Decode(&decoded)
mode = decoded == 1
return mode, err
}
// ToggleAltSpeedLimits returns info you usually see in qBt status bar.
func (client *Client) ToggleAltSpeedLimits() (toggled bool, err error) {
resp, err := client.get("api/v2/transfer/toggleSpeedLimitsMode", nil)
if err != nil {
return toggled, err
}
return (resp.Status == "200 OK"), err
}
// DlLimit returns info you usually see in qBt status bar.
func (client *Client) DlLimit() (dlLimit int, err error) {
resp, err := client.get("api/v2/transfer/downloadLimit", nil)
if err != nil {
return dlLimit, err
}
json.NewDecoder(resp.Body).Decode(&dlLimit)
return dlLimit, err
}
// SetDlLimit returns info you usually see in qBt status bar.
func (client *Client) SetDlLimit(limit int) (set bool, err error) {
params := map[string]string{"limit": strconv.Itoa(limit)}
resp, err := client.get("api/v2/transfer/setDownloadLimit", params)
if err != nil {
return set, err
}
return (resp.Status == "200 OK"), err
}
// UlLimit returns info you usually see in qBt status bar.
func (client *Client) UlLimit() (ulLimit int, err error) {
resp, err := client.get("api/v2/transfer/uploadLimit", nil)
if err != nil {
return ulLimit, err
}
json.NewDecoder(resp.Body).Decode(&ulLimit)
return ulLimit, err
}
// SetUlLimit returns info you usually see in qBt status bar.
func (client *Client) SetUlLimit(limit int) (set bool, err error) {
params := map[string]string{"limit": strconv.Itoa(limit)}
resp, err := client.get("api/v2/transfer/setUploadLimit", params)
if err != nil {
return set, err
}
return (resp.Status == "200 OK"), err
}
// Torrents returns a list of all torrents in qbittorrent matching your filter
func (client *Client) Torrents(opts TorrentsOptions) (torrentList []TorrentInfo, err error) {
params := map[string]string{}
if opts.Filter != nil {
params["filter"] = *opts.Filter
}
if opts.Category != nil {
params["category"] = *opts.Category
}
if opts.Sort != nil {
params["sort"] = *opts.Sort
}
if opts.Reverse != nil {
params["reverse"] = strconv.FormatBool(*opts.Reverse)
}
if opts.Offset != nil {
params["offset"] = strconv.Itoa(*opts.Offset)
}
if opts.Limit != nil {
params["limit"] = strconv.Itoa(*opts.Limit)
}
if opts.Hashes != nil {
params["hashes"] = delimit(opts.Hashes, "%0A")
}
resp, err := client.get("api/v2/torrents/info", params)
if err != nil {
return torrentList, err
}
json.NewDecoder(resp.Body).Decode(&torrentList)
return torrentList, nil
}
// Torrent returns a specific torrent matching the hash
func (client *Client) Torrent(hash string) (torrent Torrent, err error) {
var opts = map[string]string{"hash": strings.ToLower(hash)}
resp, err := client.get("api/v2/torrents/properties", opts)
if err != nil {
return torrent, err
}
json.NewDecoder(resp.Body).Decode(&torrent)
return torrent, nil
}
// TorrentTrackers returns all trackers for a specific torrent matching the hash
func (client *Client) TorrentTrackers(hash string) (trackers []Tracker, err error) {
var opts = map[string]string{"hash": strings.ToLower(hash)}
resp, err := client.get("api/v2/torrents/trackers", opts)
if err != nil {
return trackers, err
}
json.NewDecoder(resp.Body).Decode(&trackers)
return trackers, nil
}
// TorrentWebSeeds returns seeders for a specific torrent matching the hash
func (client *Client) TorrentWebSeeds(hash string) (webSeeds []WebSeed, err error) {
var opts = map[string]string{"hash": strings.ToLower(hash)}
resp, err := client.get("api/v2/torrents/webseeds", opts)
if err != nil {
return webSeeds, err
}
json.NewDecoder(resp.Body).Decode(&webSeeds)
return webSeeds, nil
}
// TorrentFiles from given hash
func (client *Client) TorrentFiles(hash string) (files []TorrentFile, err error) {
var opts = map[string]string{"hash": strings.ToLower(hash)}
resp, err := client.get("api/v2/torrents/files", opts)
if err != nil {
return files, err
}
json.NewDecoder(resp.Body).Decode(&files)
return files, nil
}
// TorrentPieceStates for all pieces of torrent
func (client *Client) TorrentPieceStates(hash string) (states []int, err error) {
var opts = map[string]string{"hash": strings.ToLower(hash)}
resp, err := client.get("api/v2/torrents/pieceStates", opts)
if err != nil {
return states, err
}
json.NewDecoder(resp.Body).Decode(&states)
return states, nil
}
// TorrentPieceHashes for all pieces of torrent
func (client *Client) TorrentPieceHashes(hash string) (hashes []string, err error) {
var opts = map[string]string{"hash": strings.ToLower(hash)}
resp, err := client.get("api/v2/torrents/pieceHashes", opts)
if err != nil {
return hashes, err
}
json.NewDecoder(resp.Body).Decode(&hashes)
return hashes, nil
}
// Pause torrents
func (client *Client) Pause(hashes []string) error {
opts := map[string]string{"hashes": delimit(hashes, "|")}
_, err := client.get("api/v2/torrents/pause", opts)
if err != nil {
return err
}
return nil
}
// Resume torrents
func (client *Client) Resume(hashes []string) (bool, error) {
opts := map[string]string{"hashes": delimit(hashes, "|")}
resp, err := client.get("api/v2/torrents/resume", opts)
if err != nil {
return false, err
}
return resp.StatusCode == 200, nil
}
// Delete torrents and optionally delete their files
func (client *Client) Delete(hashes []string, deleteFiles bool) (bool, error) {
opts := map[string]string{"hashes": delimit(hashes, "|")}
opts["deleteFiles"] = strconv.FormatBool(deleteFiles)
resp, err := client.post("api/v2/torrents/delete", opts)
if err != nil {
return false, err
}
return resp.StatusCode == 200, nil
}
// Recheck torrents
func (client *Client) Recheck(hashes []string) (bool, error) {
opts := map[string]string{"hashes": delimit(hashes, "|")}
resp, err := client.post("api/v2/torrents/recheck", opts)
if err != nil {
return false, err
}
return resp.StatusCode == 200, nil
}
// Reannounce torrents
func (client *Client) Reannounce(hashes []string) (bool, error) {
opts := map[string]string{"hashes": delimit(hashes, "|")}
resp, err := client.get("api/v2/torrents/reannounce", opts)
if err != nil {
return false, err
}
return resp.StatusCode == 200, nil
}
// DownloadFromLink starts downloading a torrent from a link
func (client *Client) DownloadLinks(links []string, opts DownloadOptions) error {
params := map[string]string{}
if len(links) == 0 {
return wrapper.Errorf("At least one url must be present")
} else {
delimitedURLs := delimit(links, "%0A")
// TODO: Why is encoding causing problems now?
// encodedURLS := url.QueryEscape(delimitedURLs)
params["urls"] = delimitedURLs
}
if opts.Savepath != nil {
params["savepath"] = *opts.Savepath
}
if opts.Cookie != nil {
params["cookie"] = *opts.Cookie
}
if opts.Category != nil {
params["category"] = *opts.Category
}
if opts.SkipHashChecking != nil {
params["skip_checking"] = strconv.FormatBool(*opts.SkipHashChecking)
}
if opts.Paused != nil {
params["paused"] = strconv.FormatBool(*opts.Paused)
}
if opts.RootFolder != nil {
params["root_folder"] = strconv.FormatBool(*opts.RootFolder)
}
if opts.Rename != nil {
params["rename"] = *opts.Rename
}
if opts.UploadSpeedLimit != nil {
params["upLimit"] = strconv.Itoa(*opts.UploadSpeedLimit)
}
if opts.DownloadSpeedLimit != nil {
params["dlLimit"] = strconv.Itoa(*opts.DownloadSpeedLimit)
}
if opts.SequentialDownload != nil {
params["sequentialDownload"] = strconv.FormatBool(*opts.SequentialDownload)
}
if opts.FirstLastPiecePriority != nil {
params["firstLastPiecePrio"] = strconv.FormatBool(*opts.FirstLastPiecePriority)
}
resp, err := client.postMultipartData("api/v2/torrents/add", params)
if err != nil {
return err
} else if resp.StatusCode == 415 {
return wrapper.Errorf("Torrent file is not valid")
}
return nil
}
// DownloadFromFile starts downloading a torrent from a file
func (client *Client) DownloadFromFile(torrents string, opts DownloadOptions) error {
params := map[string]string{}
if torrents == "" {
return wrapper.Errorf("At least one file must be present")
}
if opts.Savepath != nil {
params["savepath"] = *opts.Savepath
}
if opts.Cookie != nil {
params["cookie"] = *opts.Cookie
}
if opts.Category != nil {
params["category"] = *opts.Category
}
if opts.SkipHashChecking != nil {
params["skip_checking"] = strconv.FormatBool(*opts.SkipHashChecking)
}
if opts.Paused != nil {
params["paused"] = strconv.FormatBool(*opts.Paused)
}
if opts.RootFolder != nil {
params["root_folder"] = strconv.FormatBool(*opts.RootFolder)
}
if opts.Rename != nil {
params["rename"] = *opts.Rename
}
if opts.UploadSpeedLimit != nil {
params["upLimit"] = strconv.Itoa(*opts.UploadSpeedLimit)
}
if opts.DownloadSpeedLimit != nil {
params["dlLimit"] = strconv.Itoa(*opts.DownloadSpeedLimit)
}
if opts.AutomaticTorrentManagement != nil {
params["autoTMM"] = strconv.FormatBool(*opts.AutomaticTorrentManagement)
}
if opts.SequentialDownload != nil {
params["sequentialDownload"] = strconv.FormatBool(*opts.SequentialDownload)
}
if opts.FirstLastPiecePriority != nil {
params["firstLastPiecePrio"] = strconv.FormatBool(*opts.FirstLastPiecePriority)
}
resp, err := client.postMultipartFile("api/v2/torrents/add", torrents, params)
if err != nil {
return err
} else if resp.StatusCode == 415 {
return wrapper.Errorf("Torrent file is not valid")
}
return nil
}
// AddTrackers to a torrent
func (client *Client) AddTrackers(hash string, trackers []string) error {
params := make(map[string]string)
params["hash"] = strings.ToLower(hash)
delimitedTrackers := delimit(trackers, "%0A")
encodedTrackers := url.QueryEscape(delimitedTrackers)
params["urls"] = encodedTrackers
resp, err := client.post("api/v2/torrents/addTrackers", params)
if err != nil {
return err
} else if resp != nil && (*resp).StatusCode == 404 {
return wrapper.Errorf("Torrent hash not found")
}
return nil
}
// EditTracker on a torrent
func (client *Client) EditTracker(hash string, origURL string, newURL string) error {
params := map[string]string{
"hash": hash,
"origUrl": origURL,
"newUrl": newURL,
}
resp, err := client.get("api/v2/torrents/editTracker", params)
if err != nil {
return err
}
switch sc := (*resp).StatusCode; sc {
case 400:
return wrapper.Errorf("newUrl is not a valid url")
case 404:
return wrapper.Errorf("Torrent hash was not found")
case 409:
return wrapper.Errorf("newUrl already exists for this torrent or origUrl was not found")
default:
return nil
}
}
// RemoveTrackers from a torrent
func (client *Client) RemoveTrackers(hash string, trackers []string) error {
params := map[string]string{
"hash": hash,
"urls": delimit(trackers, "|"),
}
resp, err := client.get("api/v2/torrents/removeTrackers", params)
if err != nil {
return err
}
switch sc := (*resp).StatusCode; sc {
case 200:
return nil
case 404:
return wrapper.Errorf("Torrent hash was not found")
case 409:
return wrapper.Errorf("All URLs were not found")
default:
return wrapper.Errorf("An unknown error occurred causing a status code of: %v", sc)
}
}
// IncreasePriority of torrents
func (client *Client) IncreasePriority(hashes []string) error {
opts := map[string]string{"hashes": delimit(hashes, "|")}
resp, err := client.post("api/v2/torrents/increasePrio", opts)
if err != nil {
return err
}
switch sc := (*resp).StatusCode; sc {
case 200:
return nil
case 409:
return wrapper.Errorf("Torrent queueing is not enabled")
default:
return wrapper.Errorf("An unknown error occurred causing a status code of: %v", sc)
}
}
// DecreasePriority of torrents
func (client *Client) DecreasePriority(hashes []string) error {
opts := map[string]string{"hashes": delimit(hashes, "|")}
resp, err := client.post("api/v2/torrents/decreasePrio", opts)
if err != nil {
return err
}
switch sc := (*resp).StatusCode; sc {
case 200:
return nil
case 409:
return wrapper.Errorf("Torrent queueing is not enabled")
default:
return wrapper.Errorf("An unknown error occurred causing a status code of: %v", sc)
}
}
// MaxPriority maximizes the priority of torrents
func (client *Client) MaxPriority(hashes []string) error {
opts := map[string]string{"hashes": delimit(hashes, "|")}
resp, err := client.post("api/v2/torrents/topPrio", opts)
if err != nil {
return err
}
switch sc := (*resp).StatusCode; sc {
case 200:
return nil
case 409:
return wrapper.Errorf("Torrent queueing is not enabled")
default:
return wrapper.Errorf("An unknown error occurred causing a status code of: %v", sc)
}
}
// MinPriority maximizes the priority of torrents
func (client *Client) MinPriority(hashes []string) error {
opts := map[string]string{"hashes": delimit(hashes, "|")}
resp, err := client.post("api/v2/torrents/bottomPrio", opts)
if err != nil {
return err
}
switch sc := (*resp).StatusCode; sc {
case 200:
return nil
case 409:
return wrapper.Errorf("Torrent queueing is not enabled")
default:
return wrapper.Errorf("An unknown error occurred causing a status code of: %v", sc)
}
}
// FilePriority for a torrent
func (client *Client) FilePriority(hash string, ids []int, priority int) error {
formattedIds := []string{}
for _, id := range ids {
formattedIds = append(formattedIds, strconv.Itoa(id))
}
opts := map[string]string{
"hash": hash,
"id": delimit(formattedIds, "|"),
"priority": strconv.Itoa(priority),
}
resp, err := client.post("api/v2/torrents/filePrio", opts)
if err != nil {
return err
}
switch sc := (*resp).StatusCode; sc {
case 200:
return nil
case 400:
return wrapper.Errorf("Priority is invalid or at least one id is not an integer")
case 409:
return wrapper.Errorf("Torrent metadata hasn't downloaded yet or at least one file id was not found")
default:
return wrapper.Errorf("An unknown error occurred causing a status code of: %v", sc)
}
}
// GetTorrentDownloadLimit for a list of torrents
func (client *Client) GetTorrentDownloadLimit(hashes []string) (limits map[string]int, err error) {
opts := map[string]string{"hashes": delimit(hashes, "|")}
resp, err := client.post("api/v2/torrents/downloadLimit", opts)
if err != nil {
return limits, err
}
json.NewDecoder(resp.Body).Decode(&limits)
return limits, nil
}
// SetTorrentDownloadLimit for a list of torrents
func (client *Client) SetTorrentDownloadLimit(hashes []string, limit int) (bool, error) {
opts := map[string]string{
"hashes": delimit(hashes, "|"),
"limit": strconv.Itoa(limit),
}
resp, err := client.post("api/v2/torrents/setDownloadLimit", opts)
if err != nil {
return false, err
}
return resp.StatusCode == 200, nil
}
// SetTorrentShareLimit for a list of torrents
func (client *Client) SetTorrentShareLimit(hashes []string, ratioLimit int, seedingTimeLimit int) (bool, error) {
opts := map[string]string{
"hashes": delimit(hashes, "|"),
"ratioLimit": strconv.Itoa(ratioLimit),
"seedingTimeLimit": strconv.Itoa(seedingTimeLimit),
}
resp, err := client.post("api/v2/torrents/setShareLimits", opts)
if err != nil {
return false, err
}
return resp.StatusCode == 200, nil
}
// GetTorrentUploadLimit for a list of torrents
func (client *Client) GetTorrentUploadLimit(hashes []string) (limits map[string]int, err error) {
opts := map[string]string{"hashes": delimit(hashes, "|")}
resp, err := client.post("api/v2/torrents/uploadLimit", opts)
if err != nil {
return limits, err
}
json.NewDecoder(resp.Body).Decode(&limits)
return limits, nil
}
// SetTorrentUploadLimit for a list of torrents
func (client *Client) SetTorrentUploadLimit(hashes []string, limit int) (bool, error) {
opts := map[string]string{
"hashes": delimit(hashes, "|"),
"limit": strconv.Itoa(limit),
}
resp, err := client.post("api/v2/torrents/setUploadLimit", opts)
if err != nil {
return false, err
}
return resp.StatusCode == 200, nil
}
// SetTorrentLocation for a list of torrents
func (client *Client) SetTorrentLocation(hashes []string, location string) (bool, error) {
opts := map[string]string{
"hashes": delimit(hashes, "|"),
"location": location,
}
resp, err := client.post("api/v2/torrents/setLocation", opts)
if err != nil {
return false, err
}
return resp.StatusCode == 200, nil //TODO: look into other statuses
}
// SetTorrentName for a torrent
func (client *Client) SetTorrentName(hash string, name string) (bool, error) {
opts := map[string]string{
"hash": hash,
"name": name,
}
resp, err := client.post("api/v2/torrents/rename", opts)
if err != nil {
return false, err
}
return resp.StatusCode == 200, nil //TODO: look into other statuses
}
// SetTorrentCategory for a list of torrents
func (client *Client) SetTorrentCategory(hashes []string, category string) (bool, error) {
opts := map[string]string{
"hashes": delimit(hashes, "|"),
"category": category,
}
resp, err := client.post("api/v2/torrents/setCategory", opts)
if err != nil {
return false, err
}
return resp.StatusCode == 200, nil //TODO: look into other statuses
}
// GetCategories used by client
func (client *Client) GetCategories() (categories Categories, err error) {
resp, err := client.get("api/v2/torrents/categories", nil)
if err != nil {
return categories, err
}
json.NewDecoder(resp.Body).Decode(&categories)
return categories, nil
}
// CreateCategory for use by client
func (client *Client) CreateCategory(category string, savePath string) (bool, error) {
opts := map[string]string{
"category": category,
"savePath": savePath,
}
resp, err := client.post("api/v2/torrents/createCategory", opts)
if err != nil {
return false, err
}
return resp.StatusCode == 200, nil //TODO: look into other statuses
}
// UpdateCategory used by client
func (client *Client) UpdateCategory(category string, savePath string) (bool, error) {
opts := map[string]string{
"category": category,
"savePath": savePath,
}
resp, err := client.post("api/v2/torrents/editCategory", opts)
if err != nil {
return false, err
}
return resp.StatusCode == 200, nil //TODO: look into other statuses
}
// DeleteCategories used by client
func (client *Client) DeleteCategories(categories []string) (bool, error) {
opts := map[string]string{"categories": delimit(categories, "\n")}
resp, err := client.post("api/v2/torrents/removeCategories", opts)
if err != nil {
return false, err
}
return resp.StatusCode == 200, nil //TODO: look into other statuses
}
// AddTorrentTags to a list of torrents
func (client *Client) AddTorrentTags(hashes []string, tags []string) (bool, error) {
opts := map[string]string{
"hashes": delimit(hashes, "|"),
"tags": delimit(tags, ","),
}
resp, err := client.post("api/v2/torrents/addTags", opts)
if err != nil {
return false, err
}
return resp.StatusCode == 200, nil //TODO: look into other statuses
}
// RemoveTorrentTags from a list of torrents (empty list removes all tags)
func (client *Client) RemoveTorrentTags(hashes []string, tags []string) (bool, error) {
opts := map[string]string{
"hashes": delimit(hashes, "|"),
"tags": delimit(tags, ","),
}
resp, err := client.post("api/v2/torrents/removeTags", opts)
if err != nil {
return false, err
}
return resp.StatusCode == 200, nil //TODO: look into other statuses
}
// GetTorrentTags from a list of torrents (empty list removes all tags)
func (client *Client) GetTorrentTags() (tags []string, err error) {
resp, err := client.get("api/v2/torrents/tags", nil)
if err != nil {
return nil, err
}
json.NewDecoder(resp.Body).Decode(&tags)
return tags, nil
}
// CreateTags for use by client
func (client *Client) CreateTags(tags []string) (bool, error) {
opts := map[string]string{"tags": delimit(tags, ",")}
resp, err := client.post("api/v2/torrents/createTags", opts)
if err != nil {
return false, err
}
return resp.StatusCode == 200, nil //TODO: look into other statuses
}
// DeleteTags used by client
func (client *Client) DeleteTags(tags []string) (bool, error) {
opts := map[string]string{"tags": delimit(tags, ",")}
resp, err := client.post("api/v2/torrents/deleteTags", opts)
if err != nil {
return false, err
}
return resp.StatusCode == 200, nil //TODO: look into other statuses
}
// SetAutoManagement for a list of torrents
func (client *Client) SetAutoManagement(hashes []string, enable bool) (bool, error) {
opts := map[string]string{
"hashes": delimit(hashes, "|"),
"enable": strconv.FormatBool(enable),
}
resp, err := client.post("api/v2/torrents/setAutoManagement", opts)
if err != nil {
return false, err
}
return resp.StatusCode == 200, nil //TODO: look into other statuses
}
// ToggleSequentialDownload for a list of torrents
func (client *Client) ToggleSequentialDownload(hashes []string) (bool, error) {
opts := map[string]string{"hashes": delimit(hashes, "|")}
resp, err := client.get("api/v2/torrents/toggleSequentialDownload", opts)
if err != nil {
return false, err
}
return resp.StatusCode == 200, nil //TODO: look into other statuses
}
// ToggleFirstLastPiecePriority for a list of torrents
func (client *Client) ToggleFirstLastPiecePriority(hashes []string) (bool, error) {
opts := map[string]string{"hashes": delimit(hashes, "|")}
resp, err := client.get("api/v2/torrents/toggleFirstLastPiecePrio", opts)
if err != nil {
return false, err
}
return resp.StatusCode == 200, nil //TODO: look into other statuses
}
// SetForceStart for a list of torrents
func (client *Client) SetForceStart(hashes []string, value bool) (bool, error) {
opts := map[string]string{
"hashes": delimit(hashes, "|"),
"value": strconv.FormatBool(value),
}
resp, err := client.post("api/v2/torrents/setForceStart", opts)
if err != nil {
return false, err
}
return resp.StatusCode == 200, nil //TODO: look into other statuses
}
// SetSuperSeeding for a list of torrents
func (client *Client) SetSuperSeeding(hashes []string, value bool) (bool, error) {
opts := map[string]string{
"hashes": delimit(hashes, "|"),
"value": strconv.FormatBool(value),
}
resp, err := client.post("api/v2/torrents/setSuperSeeding", opts)
if err != nil {
return false, err
}
return resp.StatusCode == 200, nil //TODO: look into other statuses
}