|
|
@@ -0,0 +1,211 @@
|
|
|
+package qbitt
|
|
|
+
|
|
|
+import (
|
|
|
+ "encoding/json"
|
|
|
+ "errors"
|
|
|
+ "fmt"
|
|
|
+ "io"
|
|
|
+ "log"
|
|
|
+ "net/http"
|
|
|
+ "net/http/cookiejar"
|
|
|
+ "net/url"
|
|
|
+ "strconv"
|
|
|
+ "strings"
|
|
|
+)
|
|
|
+
|
|
|
+/*
|
|
|
+Need to make a request to the qbittorrent API
|
|
|
+ POST /api/v2/app/setPreferences
|
|
|
+with structure:
|
|
|
+
|
|
|
+ {
|
|
|
+ "listen_port": 58925
|
|
|
+ }
|
|
|
+
|
|
|
+*/
|
|
|
+
|
|
|
+const (
|
|
|
+ LOGIN_PATH = "/api/v2/auth/login"
|
|
|
+ LOGOUT_PATH = "/api/v2/auth/logout"
|
|
|
+ PREFERENCES_PATH = "/api/v2/app/setPreferences"
|
|
|
+ CONFIG_PATH = "/api/v2/app/preferences"
|
|
|
+)
|
|
|
+
|
|
|
+type ListenPortRequest struct {
|
|
|
+ ListenPort int `json:"listen_port"`
|
|
|
+}
|
|
|
+
|
|
|
+type Configuration struct {
|
|
|
+ ListenPort int `json:"listen_port"`
|
|
|
+ RandomPort bool `json:"random_port"`
|
|
|
+ Upnp bool `json:"upnp"`
|
|
|
+ Pex bool `json:"pex"`
|
|
|
+ Dht bool `json:"dht"`
|
|
|
+}
|
|
|
+
|
|
|
+func makeListenPortRequest(port int) (string, error) {
|
|
|
+ b, err := json.Marshal(ListenPortRequest{ListenPort: port})
|
|
|
+ if err != nil {
|
|
|
+ return "", err
|
|
|
+ }
|
|
|
+ return string(b), nil
|
|
|
+
|
|
|
+}
|
|
|
+
|
|
|
+type server struct {
|
|
|
+ proto string // http or https
|
|
|
+ host string // just the domain name, like 'qbit.local.lan' or whatever
|
|
|
+ port int // port that the web ui listens on
|
|
|
+}
|
|
|
+
|
|
|
+func (s server) format() string {
|
|
|
+ return fmt.Sprintf("%s://%s:%v", s.proto, s.host, s.port)
|
|
|
+}
|
|
|
+
|
|
|
+func (s server) formatWith(path string) string {
|
|
|
+ return fmt.Sprintf("%s%s", s.format(), path)
|
|
|
+}
|
|
|
+
|
|
|
+type QbittorrentClient struct {
|
|
|
+ server server
|
|
|
+ client *http.Client // plumbing
|
|
|
+}
|
|
|
+
|
|
|
+// takes string s and returns the host and port
|
|
|
+func splitServerPort(s string) (string, int) {
|
|
|
+ splitPort := strings.Split(s, ":")
|
|
|
+ if len(splitPort) < 1 {
|
|
|
+ // add log here
|
|
|
+ log.Fatal("unhandled for now")
|
|
|
+ }
|
|
|
+ host := splitPort[0]
|
|
|
+ port, err := strconv.Atoi(splitPort[1])
|
|
|
+ if err != nil {
|
|
|
+ log.Fatal("couldnt parse port. unhandled for now.")
|
|
|
+ }
|
|
|
+ return host, port
|
|
|
+
|
|
|
+}
|
|
|
+
|
|
|
+// strips everything but the port number and hostname from a string
|
|
|
+func parseServer(s string) (server, error) {
|
|
|
+ var proto, host string
|
|
|
+ var port int
|
|
|
+ if strings.Contains(s, "https") {
|
|
|
+ proto = "https"
|
|
|
+ } else {
|
|
|
+ proto = "http"
|
|
|
+ }
|
|
|
+ protoStripped := strings.ReplaceAll(
|
|
|
+ strings.ReplaceAll(
|
|
|
+ strings.ReplaceAll(
|
|
|
+ s, "https", "",
|
|
|
+ ), "http", "",
|
|
|
+ ), "://", "",
|
|
|
+ )
|
|
|
+ splitHost := strings.Split(protoStripped, "/")
|
|
|
+ host, port = splitServerPort(splitHost[0])
|
|
|
+ return server{proto: proto, host: host, port: port}, nil
|
|
|
+
|
|
|
+}
|
|
|
+
|
|
|
+/*
|
|
|
+ create a new qbit client
|
|
|
+ :param username: username for your qbittorrent Web UI
|
|
|
+ :param password: password for your qbittorrent Web UI
|
|
|
+ :param host: the host/url of the server. should be in format: <protocol>://<host>:<port>
|
|
|
+*/
|
|
|
+func NewQbittorrentClient(username, password, host string) (QbittorrentClient, error) {
|
|
|
+ ckJar, err := cookiejar.New(nil)
|
|
|
+ client := http.Client{Jar: ckJar}
|
|
|
+ srv, err := parseServer(host)
|
|
|
+ if err != nil {
|
|
|
+ return QbittorrentClient{}, err
|
|
|
+ }
|
|
|
+ formattedUrl := srv.formatWith(LOGIN_PATH)
|
|
|
+ data := url.Values{}
|
|
|
+ data.Set("username", username)
|
|
|
+ data.Set("password", password)
|
|
|
+ body := strings.NewReader(data.Encode())
|
|
|
+ req, err := http.NewRequest(http.MethodPost, formattedUrl, body)
|
|
|
+ if err != nil {
|
|
|
+ return QbittorrentClient{}, err
|
|
|
+ }
|
|
|
+ req.Header.Add("Referer", srv.format())
|
|
|
+ req.Header.Add("Origin", srv.format())
|
|
|
+ req.Header.Add("Host", srv.host)
|
|
|
+ // req.Header.Add("Content-Type", "text/plain")
|
|
|
+ req.Header.Add("Content-Type", "application/x-www-form-urlencoded")
|
|
|
+ resp, err := client.Do(req)
|
|
|
+ if err != nil {
|
|
|
+ return QbittorrentClient{}, err
|
|
|
+ }
|
|
|
+ if resp.StatusCode != 200 {
|
|
|
+ return QbittorrentClient{}, errors.New(fmt.Sprintf("Authentication failed.\n Host: %s\n Status Code: %s\n", srv.format(), resp.Status))
|
|
|
+ }
|
|
|
+
|
|
|
+ client.Jar.SetCookies(resp.Request.URL, resp.Cookies())
|
|
|
+ return QbittorrentClient{server: srv, client: &client}, nil
|
|
|
+
|
|
|
+}
|
|
|
+
|
|
|
+// updates the listening port for incoming bittorrent requests to 'port' on your server
|
|
|
+// :param port: the port to update for incoming connections
|
|
|
+func (q QbittorrentClient) UpdateIncomingPort(port int) error {
|
|
|
+ body, err := makeListenPortRequest(port)
|
|
|
+ if err != nil {
|
|
|
+ return err
|
|
|
+ }
|
|
|
+ data := url.Values{}
|
|
|
+ data.Set("json", body)
|
|
|
+ req, err := http.NewRequest(http.MethodPost, q.server.formatWith(PREFERENCES_PATH), strings.NewReader(data.Encode()))
|
|
|
+ req.Header.Add("Origin", q.server.format())
|
|
|
+ req.Header.Add("Referer", q.server.format())
|
|
|
+ req.Header.Add("Content-Type", "application/x-www-form-urlencoded")
|
|
|
+ resp, err := q.client.Do(req)
|
|
|
+ if err != nil {
|
|
|
+ return err
|
|
|
+ }
|
|
|
+ if resp.StatusCode != 200 {
|
|
|
+ return errors.New(fmt.Sprintf("Update request failed.\n Host: %s\n Status Code: %s\n", q.server.format(), resp.Status))
|
|
|
+ }
|
|
|
+ return nil
|
|
|
+}
|
|
|
+
|
|
|
+// gets the running configuration
|
|
|
+func (q QbittorrentClient) GetRunningConfiguration() (Configuration, error) {
|
|
|
+ var cfg Configuration
|
|
|
+ req, err := http.NewRequest(http.MethodGet, q.server.formatWith(CONFIG_PATH), nil)
|
|
|
+ if err != nil {
|
|
|
+ return cfg, err
|
|
|
+ }
|
|
|
+ req.Header.Add("Origin", q.server.format())
|
|
|
+ req.Header.Add("Referer", q.server.format())
|
|
|
+ resp, err := q.client.Do(req)
|
|
|
+ if err != nil {
|
|
|
+ return cfg, err
|
|
|
+ }
|
|
|
+ defer resp.Body.Close()
|
|
|
+ b, err := io.ReadAll(resp.Body)
|
|
|
+ if err != nil {
|
|
|
+ return cfg, err
|
|
|
+ }
|
|
|
+ err = json.Unmarshal(b, &cfg)
|
|
|
+ if err != nil {
|
|
|
+ return cfg, err
|
|
|
+ }
|
|
|
+ return cfg, nil
|
|
|
+}
|
|
|
+
|
|
|
+// verify that the listening port was modified
|
|
|
+func (q QbittorrentClient) VerifyPortChange(port int) (bool, error) {
|
|
|
+ cfg, err := q.GetRunningConfiguration()
|
|
|
+ if err != nil {
|
|
|
+ return false, err
|
|
|
+ }
|
|
|
+ if cfg.ListenPort != port {
|
|
|
+ return false, errors.New(fmt.Sprintf("Listening port on server: '%v' did not match what was supplied: '%v'\n", cfg.ListenPort, port))
|
|
|
+ }
|
|
|
+ return true, nil
|
|
|
+
|
|
|
+}
|