mirror of
https://github.com/idanoo/autobrr
synced 2025-07-23 16:59:12 +00:00

* (feat): qbittorrent: Add funcs * sync * Delete go.mod * more more more * a cat for your trouble * no pointers for you * maybe they weren't so bad afterall.
489 lines
12 KiB
Go
489 lines
12 KiB
Go
package qbittorrent
|
|
|
|
import (
|
|
"encoding/json"
|
|
"io/ioutil"
|
|
"net/http"
|
|
"net/http/httputil"
|
|
"strconv"
|
|
"strings"
|
|
|
|
"github.com/autobrr/autobrr/pkg/errors"
|
|
)
|
|
|
|
// Login https://github.com/qbittorrent/qBittorrent/wiki/WebUI-API-(qBittorrent-4.1)#authentication
|
|
func (c *Client) Login() error {
|
|
opts := map[string]string{
|
|
"username": c.settings.Username,
|
|
"password": c.settings.Password,
|
|
}
|
|
|
|
resp, err := c.postBasic("auth/login", opts)
|
|
if err != nil {
|
|
return errors.Wrap(err, "login error")
|
|
} else if resp.StatusCode == http.StatusForbidden {
|
|
return errors.New("User's IP is banned for too many failed login attempts")
|
|
|
|
} else if resp.StatusCode != http.StatusOK { // check for correct status code
|
|
return errors.New("qbittorrent login bad status %v", resp.StatusCode)
|
|
}
|
|
|
|
defer resp.Body.Close()
|
|
|
|
bodyBytes, err := ioutil.ReadAll(resp.Body)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
bodyString := string(bodyBytes)
|
|
|
|
// read output
|
|
if bodyString == "Fails." {
|
|
return errors.New("bad credentials")
|
|
}
|
|
|
|
// good response == "Ok."
|
|
|
|
// place cookies in jar for future requests
|
|
if cookies := resp.Cookies(); len(cookies) > 0 {
|
|
c.setCookies(cookies)
|
|
} else {
|
|
return errors.New("bad credentials")
|
|
}
|
|
|
|
c.log.Printf("logged into client: %v", c.Name)
|
|
|
|
return nil
|
|
}
|
|
|
|
func (c *Client) GetTorrents() ([]Torrent, error) {
|
|
|
|
resp, err := c.get("torrents/info", nil)
|
|
if err != nil {
|
|
return nil, errors.Wrap(err, "get torrents error")
|
|
}
|
|
|
|
defer resp.Body.Close()
|
|
|
|
body, readErr := ioutil.ReadAll(resp.Body)
|
|
if readErr != nil {
|
|
return nil, errors.Wrap(readErr, "could not read body")
|
|
}
|
|
|
|
var torrents []Torrent
|
|
err = json.Unmarshal(body, &torrents)
|
|
if err != nil {
|
|
return nil, errors.Wrap(err, "could not unmarshal body")
|
|
}
|
|
|
|
return torrents, nil
|
|
}
|
|
|
|
func (c *Client) GetTorrentsFilter(filter TorrentFilter) ([]Torrent, error) {
|
|
opts := map[string]string{
|
|
"filter": string(filter),
|
|
}
|
|
|
|
resp, err := c.get("torrents/info", opts)
|
|
if err != nil {
|
|
return nil, errors.Wrap(err, "could not get filtered torrents with filter: %v", filter)
|
|
}
|
|
|
|
defer resp.Body.Close()
|
|
|
|
body, readErr := ioutil.ReadAll(resp.Body)
|
|
if readErr != nil {
|
|
return nil, errors.Wrap(readErr, "could not read body")
|
|
}
|
|
|
|
var torrents []Torrent
|
|
err = json.Unmarshal(body, &torrents)
|
|
if err != nil {
|
|
return nil, errors.Wrap(err, "could not unmarshal body")
|
|
}
|
|
|
|
return torrents, nil
|
|
}
|
|
|
|
func (c *Client) GetTorrentsActiveDownloads() ([]Torrent, error) {
|
|
var filter = TorrentFilterDownloading
|
|
|
|
opts := map[string]string{
|
|
"filter": string(filter),
|
|
}
|
|
|
|
resp, err := c.get("torrents/info", opts)
|
|
if err != nil {
|
|
return nil, errors.Wrap(err, "could not get active torrents")
|
|
}
|
|
|
|
defer resp.Body.Close()
|
|
|
|
body, readErr := ioutil.ReadAll(resp.Body)
|
|
if readErr != nil {
|
|
return nil, errors.Wrap(readErr, "could not read body")
|
|
}
|
|
|
|
var torrents []Torrent
|
|
err = json.Unmarshal(body, &torrents)
|
|
if err != nil {
|
|
return nil, errors.Wrap(readErr, "could not unmarshal body")
|
|
}
|
|
|
|
res := make([]Torrent, 0)
|
|
for _, torrent := range torrents {
|
|
// qbit counts paused torrents as downloading as well by default
|
|
// so only add torrents with state downloading, and not pausedDl, stalledDl etc
|
|
if torrent.State == TorrentStateDownloading || torrent.State == TorrentStateStalledDl {
|
|
res = append(res, torrent)
|
|
}
|
|
}
|
|
|
|
return res, nil
|
|
}
|
|
|
|
func (c *Client) GetTorrentsRaw() (string, error) {
|
|
resp, err := c.get("torrents/info", nil)
|
|
if err != nil {
|
|
return "", errors.Wrap(err, "could not get torrents raw")
|
|
}
|
|
|
|
defer resp.Body.Close()
|
|
|
|
data, err := ioutil.ReadAll(resp.Body)
|
|
if err != nil {
|
|
return "", errors.Wrap(err, "could not get read body torrents raw")
|
|
}
|
|
|
|
return string(data), nil
|
|
}
|
|
|
|
func (c *Client) GetTorrentTrackers(hash string) ([]TorrentTracker, error) {
|
|
opts := map[string]string{
|
|
"hash": hash,
|
|
}
|
|
|
|
resp, err := c.get("torrents/trackers", opts)
|
|
if err != nil {
|
|
return nil, errors.Wrap(err, "could not get torrent trackers for hash: %v", hash)
|
|
}
|
|
|
|
defer resp.Body.Close()
|
|
|
|
dump, err := httputil.DumpResponse(resp, true)
|
|
if err != nil {
|
|
//c.log.Printf("get torrent trackers error dump response: %v\n", string(dump))
|
|
return nil, errors.Wrap(err, "could not dump response for hash: %v", hash)
|
|
}
|
|
|
|
c.log.Printf("get torrent trackers response dump: %q", dump)
|
|
|
|
if resp.StatusCode == http.StatusNotFound {
|
|
return nil, nil
|
|
} else if resp.StatusCode == http.StatusForbidden {
|
|
return nil, nil
|
|
}
|
|
|
|
body, readErr := ioutil.ReadAll(resp.Body)
|
|
if readErr != nil {
|
|
return nil, errors.Wrap(err, "could not read body")
|
|
}
|
|
|
|
c.log.Printf("get torrent trackers body: %v\n", string(body))
|
|
|
|
var trackers []TorrentTracker
|
|
err = json.Unmarshal(body, &trackers)
|
|
if err != nil {
|
|
return nil, errors.Wrap(err, "could not unmarshal body")
|
|
}
|
|
|
|
return trackers, nil
|
|
}
|
|
|
|
// AddTorrentFromFile add new torrent from torrent file
|
|
func (c *Client) AddTorrentFromFile(file string, options map[string]string) error {
|
|
|
|
res, err := c.postFile("torrents/add", file, options)
|
|
if err != nil {
|
|
return errors.Wrap(err, "could not add torrent %v", file)
|
|
} else if res.StatusCode != http.StatusOK {
|
|
return errors.Wrap(err, "could not add torrent %v unexpected status: %v", file, res.StatusCode)
|
|
}
|
|
|
|
defer res.Body.Close()
|
|
|
|
return nil
|
|
}
|
|
|
|
func (c *Client) DeleteTorrents(hashes []string, deleteFiles bool) error {
|
|
// Add hashes together with | separator
|
|
hv := strings.Join(hashes, "|")
|
|
|
|
opts := map[string]string{
|
|
"hashes": hv,
|
|
"deleteFiles": strconv.FormatBool(deleteFiles),
|
|
}
|
|
|
|
resp, err := c.get("torrents/delete", opts)
|
|
if err != nil {
|
|
return errors.Wrap(err, "could not delete torrents: %+v", hashes)
|
|
} else if resp.StatusCode != http.StatusOK {
|
|
return errors.Wrap(err, "could not delete torrents %v unexpected status: %v", hashes, resp.StatusCode)
|
|
}
|
|
|
|
defer resp.Body.Close()
|
|
|
|
return nil
|
|
}
|
|
|
|
func (c *Client) ReAnnounceTorrents(hashes []string) error {
|
|
// Add hashes together with | separator
|
|
hv := strings.Join(hashes, "|")
|
|
opts := map[string]string{
|
|
"hashes": hv,
|
|
}
|
|
|
|
resp, err := c.get("torrents/reannounce", opts)
|
|
if err != nil {
|
|
return errors.Wrap(err, "could not re-announce torrents: %v", hashes)
|
|
} else if resp.StatusCode != http.StatusOK {
|
|
return errors.Wrap(err, "could not re-announce torrents: %v unexpected status: %v", hashes, resp.StatusCode)
|
|
}
|
|
|
|
defer resp.Body.Close()
|
|
|
|
return nil
|
|
}
|
|
|
|
func (c *Client) GetTransferInfo() (*TransferInfo, error) {
|
|
resp, err := c.get("transfer/info", nil)
|
|
if err != nil {
|
|
return nil, errors.Wrap(err, "could not get transfer info")
|
|
}
|
|
|
|
defer resp.Body.Close()
|
|
|
|
body, readErr := ioutil.ReadAll(resp.Body)
|
|
if readErr != nil {
|
|
return nil, errors.Wrap(readErr, "could not read body")
|
|
}
|
|
|
|
var info TransferInfo
|
|
err = json.Unmarshal(body, &info)
|
|
if err != nil {
|
|
return nil, errors.Wrap(readErr, "could not unmarshal body")
|
|
}
|
|
|
|
return &info, nil
|
|
}
|
|
|
|
func (c *Client) Resume(hashes []string) error {
|
|
// Add hashes together with | separator
|
|
hv := strings.Join(hashes, "|")
|
|
opts := map[string]string{
|
|
"hashes": hv,
|
|
}
|
|
|
|
resp, err := c.get("torrents/resume", opts)
|
|
if err != nil {
|
|
return errors.Wrap(err, "could not resume torrents: %v", hashes)
|
|
} else if resp.StatusCode != http.StatusOK {
|
|
return errors.Wrap(err, "could not resume torrents: %v unexpected status: %v", hashes, resp.StatusCode)
|
|
}
|
|
|
|
defer resp.Body.Close()
|
|
|
|
return nil
|
|
}
|
|
|
|
func (c *Client) SetForceStart(hashes []string, value bool) error {
|
|
// Add hashes together with | separator
|
|
hv := strings.Join(hashes, "|")
|
|
opts := map[string]string{
|
|
"hashes": hv,
|
|
"value": strconv.FormatBool(value),
|
|
}
|
|
|
|
resp, err := c.get("torrents/setForceStart", opts)
|
|
if err != nil {
|
|
return errors.Wrap(err, "could not setForceStart torrents: %v", hashes)
|
|
} else if resp.StatusCode != http.StatusOK {
|
|
return errors.Wrap(err, "could not setForceStart torrents: %v unexpected status: %v", hashes, resp.StatusCode)
|
|
}
|
|
|
|
defer resp.Body.Close()
|
|
return nil
|
|
}
|
|
|
|
func (c *Client) Recheck(hashes []string) error {
|
|
// Add hashes together with | separator
|
|
hv := strings.Join(hashes, "|")
|
|
opts := map[string]string{
|
|
"hashes": hv,
|
|
}
|
|
|
|
resp, err := c.get("torrents/recheck", opts)
|
|
if err != nil {
|
|
return errors.Wrap(err, "could not recheck torrents: %v", hashes)
|
|
} else if resp.StatusCode != http.StatusOK {
|
|
return errors.Wrap(err, "could not recheck torrents: %v unexpected status: %v", hashes, resp.StatusCode)
|
|
}
|
|
|
|
defer resp.Body.Close()
|
|
|
|
return nil
|
|
}
|
|
|
|
func (c *Client) Pause(hashes []string) error {
|
|
// Add hashes together with | separator
|
|
hv := strings.Join(hashes, "|")
|
|
opts := map[string]string{
|
|
"hashes": hv,
|
|
}
|
|
|
|
resp, err := c.get("torrents/pause", opts)
|
|
if err != nil {
|
|
return errors.Wrap(err, "could not pause torrents: %v", hashes)
|
|
} else if resp.StatusCode != http.StatusOK {
|
|
return errors.Wrap(err, "could not pause torrents: %v unexpected status: %v", hashes, resp.StatusCode)
|
|
}
|
|
|
|
defer resp.Body.Close()
|
|
|
|
return nil
|
|
}
|
|
|
|
func (c *Client) SetAutoManagement(hashes []string, enable bool) error {
|
|
// Add hashes together with | separator
|
|
hv := strings.Join(hashes, "|")
|
|
opts := map[string]string{
|
|
"hashes": hv,
|
|
"enable": strconv.FormatBool(enable),
|
|
}
|
|
|
|
resp, err := c.get("torrents/setAutoManagement", opts)
|
|
if err != nil {
|
|
return errors.Wrap(err, "could not setAutoManagement torrents: %v", hashes)
|
|
} else if resp.StatusCode != http.StatusOK {
|
|
return errors.Wrap(err, "could not setAutoManagement torrents: %v unexpected status: %v", hashes, resp.StatusCode)
|
|
}
|
|
|
|
defer resp.Body.Close()
|
|
return nil
|
|
}
|
|
|
|
func (c *Client) CreateCategory(category string, path string) error {
|
|
opts := map[string]string{
|
|
"category": category,
|
|
"savePath": path,
|
|
}
|
|
|
|
resp, err := c.get("torrents/createCategory", opts)
|
|
if err != nil {
|
|
return errors.Wrap(err, "could not createCategory torrents: %v", category)
|
|
} else if resp.StatusCode != http.StatusOK {
|
|
return errors.Wrap(err, "could not createCategory torrents: %v unexpected status: %v", category, resp.StatusCode)
|
|
}
|
|
|
|
defer resp.Body.Close()
|
|
return nil
|
|
}
|
|
|
|
func (c *Client) EditCategory(category string, path string) error {
|
|
opts := map[string]string{
|
|
"category": category,
|
|
"savePath": path,
|
|
}
|
|
|
|
resp, err := c.get("torrents/editCategory", opts)
|
|
if err != nil {
|
|
return errors.Wrap(err, "could not editCategory torrents: %v", category)
|
|
} else if resp.StatusCode != http.StatusOK {
|
|
return errors.Wrap(err, "could not editCategory torrents: %v unexpected status: %v", category, resp.StatusCode)
|
|
}
|
|
|
|
defer resp.Body.Close()
|
|
return nil
|
|
}
|
|
|
|
func (c *Client) RemoveCategories(categories[] string) error {
|
|
opts := map[string]string{
|
|
"categories": strings.Join(categories, "\n"),
|
|
}
|
|
|
|
resp, err := c.get("torrents/removeCategories", opts)
|
|
if err != nil {
|
|
return errors.Wrap(err, "could not removeCategories torrents: %v", opts["categories"])
|
|
} else if resp.StatusCode != http.StatusOK {
|
|
return errors.Wrap(err, "could not removeCategories torrents: %v unexpected status: %v", opts["categories"], resp.StatusCode)
|
|
}
|
|
|
|
defer resp.Body.Close()
|
|
return nil
|
|
}
|
|
|
|
func (c *Client) SetCategory(hashes []string, category string) error {
|
|
// Add hashes together with | separator
|
|
hv := strings.Join(hashes, "|")
|
|
opts := map[string]string{
|
|
"hashes": hv,
|
|
"category": category,
|
|
}
|
|
|
|
resp, err := c.get("torrents/setCategory", opts)
|
|
if err != nil {
|
|
return errors.Wrap(err, "could not setCategory torrents: %v", hashes)
|
|
} else if resp.StatusCode != http.StatusOK {
|
|
return errors.Wrap(err, "could not setCategory torrents: %v unexpected status: %v", hashes, resp.StatusCode)
|
|
}
|
|
|
|
defer resp.Body.Close()
|
|
return nil
|
|
}
|
|
|
|
func (c *Client) GetFilesInformation(hash string) (*TorrentFiles, error) {
|
|
opts := map[string]string{
|
|
"hash": hash,
|
|
}
|
|
|
|
resp, err := c.get("torrents/files", opts)
|
|
if err != nil {
|
|
return nil, errors.Wrap(err, "could not get files info")
|
|
}
|
|
|
|
defer resp.Body.Close()
|
|
|
|
body, readErr := ioutil.ReadAll(resp.Body)
|
|
if readErr != nil {
|
|
return nil, errors.Wrap(readErr, "could not read body")
|
|
}
|
|
|
|
var info TorrentFiles
|
|
err = json.Unmarshal(body, &info)
|
|
if err != nil {
|
|
return nil, errors.Wrap(readErr, "could not unmarshal body")
|
|
}
|
|
|
|
return &info, nil
|
|
}
|
|
|
|
func (c *Client) GetCategories() (map[string]Category, error) {
|
|
resp, err := c.get("torrents/categories", nil)
|
|
if err != nil {
|
|
return nil, errors.Wrap(err, "could not get files info")
|
|
}
|
|
|
|
defer resp.Body.Close()
|
|
|
|
body, readErr := ioutil.ReadAll(resp.Body)
|
|
if readErr != nil {
|
|
return nil, errors.Wrap(readErr, "could not read body")
|
|
}
|
|
|
|
m := make(map[string]Category)
|
|
err = json.Unmarshal(body, &m)
|
|
if err != nil {
|
|
return nil, errors.Wrap(readErr, "could not unmarshal body")
|
|
}
|
|
|
|
return m, nil
|
|
}
|