├── xmlrpc
├── client_test.go
├── client.go
└── marshaller.go
├── rtorrent
├── testdata
│ └── Fedora-i3-Live-x86_64-35.torrent
├── rtorrent_test.go
└── rtorrent.go
├── README.md
├── test.sh
├── go.mod
├── .gitignore
├── .circleci
└── config.yml
├── LICENSE
├── go.sum
└── main.go
/xmlrpc/client_test.go:
--------------------------------------------------------------------------------
1 | package xmlrpc
2 |
3 | // TODO: add tests
4 |
--------------------------------------------------------------------------------
/rtorrent/testdata/Fedora-i3-Live-x86_64-35.torrent:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mrobinsn/go-rtorrent/HEAD/rtorrent/testdata/Fedora-i3-Live-x86_64-35.torrent
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # DEPRECATED
2 |
3 | This repository is deprecated and is no longer maintained.
4 |
5 | Consider using https://github.com/autobrr/go-rtorrent instead which is an active fork.
6 |
--------------------------------------------------------------------------------
/test.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | docker rm -f rutorrent
4 | rm -rf tmp
5 | mkdir tmp
6 | docker run -d --name=rutorrent -p 8080:8080 -p 8000:8000 crazymax/rtorrent-rutorrent:latest
7 | sleep 60
8 | go test -v -race ./...
9 |
10 |
--------------------------------------------------------------------------------
/go.mod:
--------------------------------------------------------------------------------
1 | module github.com/mrobinsn/go-rtorrent
2 |
3 | go 1.15
4 |
5 | require (
6 | github.com/cpuguy83/go-md2man/v2 v2.0.1 // indirect
7 | github.com/pkg/errors v0.9.1
8 | github.com/stretchr/testify v1.3.0
9 | github.com/urfave/cli v1.22.5
10 | )
11 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Compiled Object files, Static and Dynamic libs (Shared Objects)
2 | *.o
3 | *.a
4 | *.so
5 |
6 | # Folders
7 | _obj
8 | _test
9 |
10 | # Architecture specific extensions/prefixes
11 | *.[568vq]
12 | [568vq].out
13 |
14 | *.cgo1.go
15 | *.cgo2.c
16 | _cgo_defun.c
17 | _cgo_gotypes.go
18 | _cgo_export.*
19 |
20 | _testmain.go
21 |
22 | *.exe
23 | *.test
24 | *.prof
25 |
26 | go-rtorrent
27 | tmp
28 |
--------------------------------------------------------------------------------
/.circleci/config.yml:
--------------------------------------------------------------------------------
1 | version: 2.1
2 | jobs:
3 | build:
4 | docker:
5 | - image: cimg/go:1.17
6 | - image: crazymax/rtorrent-rutorrent:latest
7 | steps:
8 | - checkout
9 | - run:
10 | name: Install Goveralls
11 | command: go install github.com/mattn/goveralls@latest
12 | - run:
13 | name: Wait for rutorrent
14 | command: dockerize -wait tcp://localhost:8080 -timeout 1m && dockerize -wait tcp://localhost:8000 -timeout 1m
15 | - run:
16 | name: Go vet
17 | command: go vet ./...
18 | - run:
19 | name: Run tests
20 | command: goveralls -race -v -show -service=circle-ci -repotoken=$COVERALLS_TOKEN
21 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2015 Michael Robinson
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
23 |
--------------------------------------------------------------------------------
/xmlrpc/client.go:
--------------------------------------------------------------------------------
1 | package xmlrpc
2 |
3 | import (
4 | "bytes"
5 | "crypto/tls"
6 | "net/http"
7 |
8 | "github.com/pkg/errors"
9 | )
10 |
11 | // Client implements a basic XMLRPC client
12 | type Client struct {
13 | addr string
14 | httpClient *http.Client
15 | }
16 |
17 | // NewClient returns a new instance of Client
18 | // Pass in a true value for `insecure` to turn off certificate verification
19 | func NewClient(addr string, insecure bool) *Client {
20 | transport := &http.Transport{}
21 | if insecure {
22 | transport = &http.Transport{
23 | TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
24 | }
25 | }
26 |
27 | httpClient := &http.Client{Transport: transport}
28 |
29 | return &Client{
30 | addr: addr,
31 | httpClient: httpClient,
32 | }
33 | }
34 |
35 | // NewClientWithHTTPClient returns a new instance of Client.
36 | // This allows you to use a custom http.Client setup for your needs.
37 | func NewClientWithHTTPClient(addr string, client *http.Client) *Client {
38 | return &Client{
39 | addr: addr,
40 | httpClient: client,
41 | }
42 | }
43 |
44 | // Call calls the method with "name" with the given args
45 | // Returns the result, and an error for communication errors
46 | func (c *Client) Call(name string, args ...interface{}) (interface{}, error) {
47 | req := bytes.NewBuffer(nil)
48 | if err := Marshal(req, name, args...); err != nil {
49 | return nil, errors.Wrap(err, "failed to marshal request")
50 | }
51 | resp, err := c.httpClient.Post(c.addr, "text/xml", req)
52 | if err != nil {
53 | return nil, errors.Wrap(err, "POST failed")
54 | }
55 | defer resp.Body.Close()
56 |
57 | _, val, fault, err := Unmarshal(resp.Body)
58 | if fault != nil {
59 | err = errors.Errorf("Error: %v: %v", err, fault)
60 | }
61 | return val, err
62 | }
63 |
--------------------------------------------------------------------------------
/go.sum:
--------------------------------------------------------------------------------
1 | github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
2 | github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
3 | github.com/cpuguy83/go-md2man/v2 v2.0.1 h1:r/myEWzV9lfsM1tFLgDyu0atFtJ1fXn261LKYj/3DxU=
4 | github.com/cpuguy83/go-md2man/v2 v2.0.1/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
5 | github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8=
6 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
7 | github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
8 | github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
9 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
10 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
11 | github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
12 | github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
13 | github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
14 | github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
15 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
16 | github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q=
17 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
18 | github.com/urfave/cli v1.22.5 h1:lNq9sAHXK2qfdI8W+GRItjCEkI+2oR4d+MEHy1CKXoU=
19 | github.com/urfave/cli v1.22.5/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0=
20 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
21 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
22 |
--------------------------------------------------------------------------------
/main.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "fmt"
5 | "os"
6 |
7 | "github.com/mrobinsn/go-rtorrent/rtorrent"
8 | "github.com/pkg/errors"
9 | "github.com/urfave/cli"
10 | )
11 |
12 | var (
13 | name = "rTorrent XMLRPC CLI"
14 | version = "1.0.0"
15 | app = initApp()
16 | conn *rtorrent.RTorrent
17 |
18 | endpoint string
19 | view string
20 | hash string
21 | disableCertCheck bool
22 | )
23 |
24 | func initApp() *cli.App {
25 | nApp := cli.NewApp()
26 |
27 | nApp.Name = name
28 | nApp.Version = version
29 | nApp.Authors = []cli.Author{
30 | {Name: "Michael Robinson", Email: "m@michaelrobinson.io"},
31 | }
32 |
33 | // Global flags
34 | nApp.Flags = []cli.Flag{
35 | cli.StringFlag{
36 | Name: "endpoint",
37 | Usage: "rTorrent endpoint",
38 | Value: "http://myrtorrent/RPC2",
39 | Destination: &endpoint,
40 | },
41 | cli.BoolFlag{
42 | Name: "disable-cert-check",
43 | Usage: "disable certificate checking on this endpoint, useful for testing",
44 | Destination: &disableCertCheck,
45 | },
46 | }
47 |
48 | nApp.Before = setupConnection
49 |
50 | nApp.Commands = []cli.Command{{
51 | Name: "get-ip",
52 | Usage: "retrieves the IP for this rTorrent instance",
53 | Action: getIP,
54 | }, {
55 | Name: "get-name",
56 | Usage: "retrieves the name for this rTorrent instance",
57 | Action: getName,
58 | }, {
59 | Name: "get-totals",
60 | Usage: "retrieves the up/down totals for this rTorrent instance",
61 | Action: getTotals,
62 | }, {
63 | Name: "get-torrents",
64 | Usage: "retrieves the torrents from this rTorrent instance",
65 | Action: getTorrents,
66 | Flags: []cli.Flag{
67 | cli.StringFlag{
68 | Name: "view",
69 | Usage: "view to use, known values: main, started, stopped, hashing, seeding",
70 | Value: string(rtorrent.ViewMain),
71 | Destination: &view,
72 | },
73 | },
74 | }, {
75 | Name: "get-files",
76 | Usage: "retrieves the files for a specific torrent",
77 | Action: getFiles,
78 | Flags: []cli.Flag{
79 | cli.StringFlag{
80 | Name: "hash",
81 | Usage: "hash of the torrent",
82 | Value: "unknown",
83 | Destination: &hash,
84 | },
85 | },
86 | },
87 | }
88 |
89 | return nApp
90 | }
91 |
92 | func main() {
93 | if err := app.Run(os.Args); err != nil {
94 | fmt.Println(err)
95 | }
96 | }
97 |
98 | func setupConnection(c *cli.Context) error {
99 | if endpoint == "" {
100 | return errors.New("endpoint must be specified")
101 | }
102 | conn = rtorrent.New(endpoint, disableCertCheck)
103 | return nil
104 | }
105 |
106 | func getIP(c *cli.Context) error {
107 | ip, err := conn.IP()
108 | if err != nil {
109 | return errors.Wrap(err, "failed to get rTorrent IP")
110 | }
111 | fmt.Println(ip)
112 | return nil
113 | }
114 |
115 | func getName(c *cli.Context) error {
116 | name, err := conn.Name()
117 | if err != nil {
118 | return errors.Wrap(err, "failed to get rTorrent name")
119 | }
120 | fmt.Println(name)
121 | return nil
122 | }
123 |
124 | func getTotals(c *cli.Context) error {
125 | // Get Down Total
126 | downTotal, err := conn.DownTotal()
127 | if err != nil {
128 | return errors.Wrap(err, "failed to get rTorrent down total")
129 | }
130 | fmt.Printf("%d\n", downTotal)
131 |
132 | // Get Up Total
133 | upTotal, err := conn.UpTotal()
134 | if err != nil {
135 | return errors.Wrap(err, "failed to get rTorrent up total")
136 | }
137 | fmt.Printf("%d\n", upTotal)
138 | return nil
139 | }
140 |
141 | func getTorrents(c *cli.Context) error {
142 | torrents, err := conn.GetTorrents(rtorrent.View(view))
143 | if err != nil {
144 | return errors.Wrap(err, "failed to get torrents")
145 | }
146 | for _, torrent := range torrents {
147 | fmt.Println(torrent.Pretty())
148 | }
149 | return nil
150 | }
151 |
152 | func getFiles(c *cli.Context) error {
153 | files, err := conn.GetFiles(rtorrent.Torrent{Hash: hash})
154 | if err != nil {
155 | return errors.Wrap(err, "failed to get files")
156 | }
157 | for _, file := range files {
158 | fmt.Println(file.Pretty())
159 | }
160 | return nil
161 | }
162 |
--------------------------------------------------------------------------------
/xmlrpc/marshaller.go:
--------------------------------------------------------------------------------
1 | package xmlrpc
2 |
3 | import (
4 | "bytes"
5 | "encoding/base64"
6 | "encoding/xml"
7 | "errors"
8 | "fmt"
9 | "io"
10 | "reflect"
11 | "strconv"
12 | "strings"
13 | "time"
14 | )
15 |
16 | /// ISO8601 is not very much restrictive, so many combinations exist
17 | const (
18 | // FullXMLRpcTime is the format of a full XML-RPC time
19 | FullXMLRpcTime = "2006-01-02T15:04:05-07:00"
20 | // LocalXMLRpcTime is the XML-RPC time without timezone
21 | LocalXMLRpcTime = "2006-01-02T15:04:05"
22 | // DenseXMLRpcTime is a dense-formatted local time
23 | DenseXMLRpcTime = "20060102T15:04:05"
24 | // DummyXMLRpcTime is seen in the wild
25 | DummyXMLRpcTime = "20060102T15:04:05-0700"
26 | )
27 |
28 | // ErrUnsupported is the error of "Unsupported type"
29 | var ErrUnsupported = errors.New("Unsupported type")
30 |
31 | // Fault is the struct for the fault response
32 | type Fault struct {
33 | Code int
34 | Message string
35 | }
36 |
37 | func (f Fault) String() string {
38 | return fmt.Sprintf("%d: %s", f.Code, f.Message)
39 | }
40 | func (f Fault) Error() string {
41 | return f.String()
42 | }
43 |
44 | // WriteXML writes the XML representation of the fault into the Writer
45 | func (f Fault) WriteXML(w io.Writer) (int, error) {
46 | return fmt.Fprintf(w, `
47 | faultCode%d
48 | faultString%s
49 | `, f.Code, xmlEscape(f.Message))
50 | }
51 |
52 | var xmlSpecial = map[byte]string{
53 | '<': "<",
54 | '>': ">",
55 | '"': """,
56 | '\'': "'",
57 | '&': "&",
58 | }
59 |
60 | func xmlEscape(s string) string {
61 | var b bytes.Buffer
62 | for i := 0; i < len(s); i++ {
63 | c := s[i]
64 | if s, ok := xmlSpecial[c]; ok {
65 | b.WriteString(s)
66 | } else {
67 | b.WriteByte(c)
68 | }
69 | }
70 | return b.String()
71 | }
72 |
73 | type valueNode struct {
74 | Type string `xml:",attr"`
75 | Body string `xml:",chardata"`
76 | }
77 |
78 | type state struct {
79 | p *xml.Decoder
80 | level int
81 | remainder *interface{}
82 | last *xml.Token
83 | }
84 |
85 | func newParser(p *xml.Decoder) *state {
86 | return &state{p, 0, nil, nil}
87 | }
88 |
89 | const (
90 | tokStart = iota
91 | tokText
92 | tokStop
93 | )
94 |
95 | var (
96 | errNotStartElement = errors.New("not start element")
97 | errNameMismatch = errors.New("not the required token")
98 | errNotEndElement = errors.New("not end element")
99 | )
100 |
101 | func (st *state) parseValue() (nv interface{}, e error) {
102 | var se xml.StartElement
103 | if se, e = st.getStart(""); e != nil {
104 | if ErrEq(e, errNotStartElement) {
105 | e = nil
106 | }
107 | return
108 | }
109 |
110 | var vn valueNode
111 | switch se.Name.Local {
112 | case "value":
113 | if nv, e = st.parseValue(); e == nil {
114 | e = st.checkLast("value")
115 | }
116 | return
117 | case "boolean", "string", "int", "i1", "i2", "i4", "i8", "double", "dateTime.iso8601", "base64": //simple
118 | st.last = nil
119 | if e = st.p.DecodeElement(&vn, &se); e != nil {
120 | return
121 | }
122 |
123 | switch se.Name.Local {
124 | case "boolean":
125 | nv, e = strconv.ParseBool(vn.Body)
126 | case "string":
127 | nv = vn.Body
128 | case "int", "i1", "i2", "i4":
129 | var i64 int64
130 | i64, e = strconv.ParseInt(vn.Body, 10, 32)
131 | nv = int(i64)
132 | case "i8":
133 | var i64 int64
134 | i64, e = strconv.ParseInt(vn.Body, 10, 64)
135 | nv = int(i64)
136 | case "double":
137 | nv, e = strconv.ParseFloat(vn.Body, 64)
138 | case "dateTime.iso8601":
139 | for _, format := range []string{FullXMLRpcTime, LocalXMLRpcTime, DenseXMLRpcTime, DummyXMLRpcTime} {
140 | nv, e = time.Parse(format, vn.Body)
141 | if e == nil {
142 | break
143 | }
144 | }
145 | case "base64":
146 | nv, e = base64.StdEncoding.DecodeString(vn.Body)
147 | }
148 | return
149 |
150 | case "struct":
151 | var name string
152 | values := make(map[string]interface{}, 4)
153 | nv = values
154 | for {
155 | if se, e = st.getStart("member"); e != nil {
156 | if ErrEq(e, errNotStartElement) {
157 | e = st.checkLast("struct")
158 | break
159 | }
160 | return
161 | }
162 | if name, e = st.getText("name"); e != nil {
163 | return
164 | }
165 | if se, e = st.getStart("value"); e != nil {
166 | return
167 | }
168 | if values[name], e = st.parseValue(); e != nil {
169 | return
170 | }
171 | if e = st.checkLast("value"); e != nil {
172 | return
173 | }
174 | if e = st.checkLast("member"); e != nil {
175 | return
176 | }
177 | }
178 | return
179 |
180 | case "array":
181 | values := make([]interface{}, 0, 4)
182 | var val interface{}
183 | if _, e = st.getStart("data"); e != nil {
184 | return
185 | }
186 | for {
187 | if se, e = st.getStart("value"); e != nil {
188 | if ErrEq(e, errNotStartElement) {
189 | e = nil //st.checkLast("data")
190 | break
191 | }
192 | return
193 | }
194 | if val, e = st.parseValue(); e != nil {
195 | return
196 | }
197 | values = append(values, val)
198 | if e = st.checkLast("value"); e != nil {
199 | return
200 | }
201 | }
202 | if e = st.checkLast("data"); e == nil {
203 | e = st.checkLast("array")
204 | }
205 | nv = values
206 | return
207 | default:
208 | e = fmt.Errorf("cannot parse unknown tag %s", se)
209 | }
210 | return
211 | }
212 |
213 | func (st *state) token(typ int, name string) (t xml.Token, body string, e error) {
214 | // var ok bool
215 | if st.last != nil {
216 | t = *st.last
217 | st.last = nil
218 | }
219 | Reading:
220 | for {
221 | if t != nil {
222 | switch t.(type) {
223 | case xml.StartElement:
224 | se := t.(xml.StartElement)
225 | if se.Name.Local != "" {
226 | break Reading
227 | }
228 | case xml.EndElement:
229 | ee := t.(xml.EndElement)
230 | if ee.Name.Local != "" {
231 | break Reading
232 | }
233 | default:
234 | }
235 | }
236 | if t, e = st.p.Token(); e != nil {
237 | return
238 | }
239 | if t == nil {
240 | e = errors.New("nil token")
241 | return
242 | }
243 | }
244 | switch typ {
245 | case tokStart, tokText:
246 | se, ok := t.(xml.StartElement)
247 | if !ok {
248 | st.last = &t
249 | e = Errorf2(errNotStartElement, "required startelement(%s), found %s %T", name, t, t)
250 | return
251 | }
252 | switch typ {
253 | case tokStart:
254 | if name != "" && se.Name.Local != name {
255 | e = Errorf2(errNameMismatch, "required <%s>, found <%s>", name, se.Name.Local)
256 | return
257 | }
258 | default:
259 | var vn valueNode
260 | if e = st.p.DecodeElement(&vn, &se); e != nil {
261 | return
262 | }
263 | body = vn.Body
264 | }
265 | default:
266 | ee, ok := t.(xml.EndElement)
267 | if !ok {
268 | st.last = &t
269 | e = Errorf2(errNotEndElement, "required endelement(%s), found %s %T", name, t, t)
270 | return
271 | }
272 | if name != "" && ee.Name.Local != name {
273 | e = Errorf2(errNameMismatch, "required %s>, found %s>", name, ee.Name.Local)
274 | return
275 | }
276 | }
277 | return
278 | }
279 |
280 | func (st *state) getStart(name string) (se xml.StartElement, e error) {
281 | var t xml.Token
282 | t, _, e = st.token(tokStart, name)
283 | se, _ = t.(xml.StartElement)
284 | if e != nil {
285 | return
286 | }
287 | se = t.(xml.StartElement)
288 | return
289 | }
290 |
291 | func (st *state) getText(name string) (text string, e error) {
292 | _, text, e = st.token(tokText, name)
293 | return
294 | }
295 |
296 | func (st *state) checkLast(name string) (e error) {
297 | _, _, e = st.token(tokStop, name)
298 | return
299 | }
300 |
301 | func toXML(v interface{}, typ bool) (s string) {
302 | r := reflect.ValueOf(v)
303 | t := r.Type()
304 | k := t.Kind()
305 |
306 | if b, ok := v.([]byte); ok {
307 | return "" + base64.StdEncoding.EncodeToString(b) + ""
308 | }
309 |
310 | switch k {
311 | case reflect.Invalid:
312 | panic("Unsupported type")
313 | case reflect.Bool:
314 | return fmt.Sprintf("%v", v)
315 | case reflect.Int,
316 | reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64,
317 | reflect.Uint,
318 | reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
319 | if typ {
320 | return fmt.Sprintf("%v", v)
321 | }
322 | return fmt.Sprintf("%v", v)
323 | case reflect.Uintptr:
324 | panic("Unsupported type")
325 | case reflect.Float32, reflect.Float64:
326 | if typ {
327 | return fmt.Sprintf("%v", v)
328 | }
329 | return fmt.Sprintf("%v", v)
330 | case reflect.Complex64, reflect.Complex128:
331 | panic("Unsupported type")
332 | case reflect.Array, reflect.Slice:
333 | s = ""
334 | for n := 0; n < r.Len(); n++ {
335 | s += ""
336 | s += toXML(r.Index(n).Interface(), typ)
337 | s += ""
338 | }
339 | s += ""
340 | return s
341 | case reflect.Chan:
342 | panic("Unsupported type")
343 | case reflect.Func:
344 | panic("Unsupported type")
345 | case reflect.Interface:
346 | return toXML(r.Elem(), typ)
347 | case reflect.Map:
348 | s = ""
349 | for _, key := range r.MapKeys() {
350 | s += ""
351 | s += "" + xmlEscape(key.Interface().(string)) + ""
352 | s += "" + toXML(r.MapIndex(key).Interface(), typ) + ""
353 | s += ""
354 | }
355 | return s + ""
356 | case reflect.Ptr:
357 | panic("Unsupported type")
358 | case reflect.String:
359 | if typ {
360 | return fmt.Sprintf("%v", xmlEscape(v.(string)))
361 | }
362 | return xmlEscape(v.(string))
363 | case reflect.Struct:
364 | s = ""
365 | for n := 0; n < r.NumField(); n++ {
366 | s += ""
367 | s += "" + t.Field(n).Name + ""
368 | s += "" + toXML(r.FieldByIndex([]int{n}).Interface(), true) + ""
369 | s += ""
370 | }
371 | return s + ""
372 | case reflect.UnsafePointer:
373 | return toXML(r.Elem(), typ)
374 | }
375 | return
376 | }
377 |
378 | // WriteXML writes v, typed if typ is true, into w Writer
379 | func WriteXML(w io.Writer, v interface{}, typ bool) (err error) {
380 | var (
381 | r reflect.Value
382 | ok bool
383 | )
384 | // go back from reflect.Value, if needed.
385 | if r, ok = v.(reflect.Value); !ok {
386 | r = reflect.ValueOf(v)
387 | } else {
388 | v = r.Interface()
389 | }
390 | if fp, ok := getFault(v); ok {
391 | _, err = fp.WriteXML(w)
392 | return
393 | }
394 | if b, ok := v.([]byte); ok {
395 | length := base64.StdEncoding.EncodedLen(len(b))
396 | dst := make([]byte, length)
397 | base64.StdEncoding.Encode(dst, b)
398 | _, err = taggedWrite(w, []byte("base64"), dst)
399 | return
400 | }
401 | if tim, ok := v.(time.Time); ok {
402 | _, err = taggedWriteString(w, "dateTime.iso8601", tim.Format(FullXMLRpcTime))
403 | return
404 | }
405 | t := r.Type()
406 | k := t.Kind()
407 |
408 | switch k {
409 | case reflect.Invalid, reflect.Uintptr, reflect.Complex64, reflect.Complex128, reflect.Chan, reflect.Func:
410 | return Errorf2(ErrUnsupported, "v=%#v t=%v k=%s", v, t, k)
411 | case reflect.Bool:
412 | _, err = fmt.Fprintf(w, "%v", v)
413 | return err
414 | case reflect.Int,
415 | reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64,
416 | reflect.Uint,
417 | reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
418 | if typ {
419 | _, err = fmt.Fprintf(w, "%v", v)
420 | return err
421 | }
422 | _, err = fmt.Fprintf(w, "%v", v)
423 | return err
424 | case reflect.Float32, reflect.Float64:
425 | if typ {
426 | _, err = fmt.Fprintf(w, "%v", v)
427 | return err
428 | }
429 | _, err = fmt.Fprintf(w, "%v", v)
430 | return err
431 | case reflect.Array, reflect.Slice:
432 | if _, err = io.WriteString(w, "\n"); err != nil {
433 | return
434 | }
435 | n := r.Len()
436 | for i := 0; i < n; i++ {
437 | if _, err = io.WriteString(w, " "); err != nil {
438 | return
439 | }
440 | if err = WriteXML(w, r.Index(i).Interface(), typ); err != nil {
441 | return
442 | }
443 | if _, err = io.WriteString(w, "\n"); err != nil {
444 | return
445 | }
446 | }
447 | if _, err = io.WriteString(w, "\n"); err != nil {
448 | return
449 | }
450 | case reflect.Interface:
451 | return WriteXML(w, r.Elem(), typ)
452 | case reflect.Map:
453 | if _, err = io.WriteString(w, "\n"); err != nil {
454 | return
455 | }
456 | for _, key := range r.MapKeys() {
457 | if _, err = io.WriteString(w, " "); err != nil {
458 | return
459 | }
460 | if _, err = io.WriteString(w, xmlEscape(key.Interface().(string))); err != nil {
461 | return
462 | }
463 | if _, err = io.WriteString(w, ""); err != nil {
464 | return
465 | }
466 | if err = WriteXML(w, r.MapIndex(key).Interface(), typ); err != nil {
467 | return
468 | }
469 | if _, err = io.WriteString(w, "\n"); err != nil {
470 | return
471 | }
472 | }
473 | _, err = io.WriteString(w, "")
474 | return
475 | case reflect.Ptr:
476 | return WriteXML(w, reflect.Indirect(r), typ)
477 | case reflect.String:
478 | if typ {
479 | _, err = fmt.Fprintf(w, "%v", xmlEscape(v.(string)))
480 | return
481 | }
482 | _, err = io.WriteString(w, xmlEscape(v.(string)))
483 | return
484 | case reflect.Struct:
485 | if _, err = io.WriteString(w, ""); err != nil {
486 | return
487 | }
488 | n := r.NumField()
489 | for i := 0; i < n; i++ {
490 | c := t.Field(i).Name[:1]
491 | if strings.ToLower(c) == c { //have to skip unexported fields
492 | continue
493 | }
494 | if _, err = io.WriteString(w, ""); err != nil {
495 | return
496 | }
497 | if _, err = io.WriteString(w, xmlEscape(getStructFieldName(t.Field(i)))); err != nil {
498 | return
499 | }
500 | if _, err = io.WriteString(w, ""); err != nil {
501 | return
502 | }
503 | if err = WriteXML(w, r.Field(i).Interface(), true); err != nil {
504 | return
505 | }
506 | if _, err = io.WriteString(w, ""); err != nil {
507 | return err
508 | }
509 | }
510 | _, err = io.WriteString(w, "")
511 | return
512 | case reflect.UnsafePointer:
513 | return WriteXML(w, r.Elem(), typ)
514 | }
515 | return
516 | }
517 |
518 | func taggedWrite(w io.Writer, tag, inner []byte) (n int, err error) {
519 | var j int
520 | for _, elt := range [][]byte{[]byte("<"), tag, []byte(">"), inner,
521 | []byte(""), tag, []byte(">")} {
522 | j, err = w.Write(elt)
523 | n += j
524 | if err != nil {
525 | return
526 | }
527 | }
528 | return
529 | }
530 | func taggedWriteString(w io.Writer, tag, inner string) (n int, err error) {
531 | if n, err = io.WriteString(w, "<"+tag+">"); err != nil {
532 | return
533 | }
534 | var j int
535 | j, err = io.WriteString(w, inner)
536 | n += j
537 | if err != nil {
538 | return
539 | }
540 | j, err = io.WriteString(w, ""+tag+">")
541 | n += j
542 | return
543 | }
544 |
545 | func getStructFieldName(sf reflect.StructField) string {
546 | if sf.Tag.Get("xml") == "" {
547 | return sf.Name
548 | }
549 | return sf.Tag.Get("xml")
550 | }
551 |
552 | // Marshal marshals the named thing (methodResponse if name == "", otherwise a methodCall)
553 | // into the w Writer
554 | func Marshal(w io.Writer, name string, args ...interface{}) (err error) {
555 | if name == "" {
556 | if _, err = io.WriteString(w, ""); err != nil {
557 | return
558 | }
559 | if len(args) > 0 {
560 | fp, ok := getFault(args[0])
561 | if ok {
562 | _, err = fp.WriteXML(w)
563 | if err == nil {
564 | _, err = io.WriteString(w, "\n")
565 | }
566 | return
567 | }
568 | }
569 | } else {
570 | if _, err = io.WriteString(w, ""); err != nil {
571 | return
572 | }
573 | if _, err = io.WriteString(w, xmlEscape(name)); err != nil {
574 | return
575 | }
576 | if _, err = io.WriteString(w, "\n"); err != nil {
577 | return
578 | }
579 | }
580 | if _, err = io.WriteString(w, "\n"); err != nil {
581 | return
582 | }
583 | for _, arg := range args {
584 | if _, err = io.WriteString(w, " "); err != nil {
585 | return
586 | }
587 | if err = WriteXML(w, arg, true); err != nil {
588 | return
589 | }
590 | if _, err = io.WriteString(w, "\n"); err != nil {
591 | return
592 | }
593 | }
594 | if name == "" {
595 | _, err = io.WriteString(w, "")
596 | } else {
597 | _, err = io.WriteString(w, "")
598 | }
599 | return err
600 | }
601 |
602 | func getFault(v interface{}) (*Fault, bool) {
603 | if f, ok := v.(Fault); ok {
604 | return &f, true
605 | }
606 | if f, ok := v.(*Fault); ok {
607 | if f != nil {
608 | return f, true
609 | }
610 | } else {
611 | if e, ok := v.(error); ok {
612 | return &Fault{Code: -1, Message: e.Error()}, true
613 | }
614 | }
615 | return nil, false
616 | }
617 |
618 | // Unmarshal unmarshals the thing (methodResponse, methodCall or fault),
619 | // returns the name of the method call in the first return argument;
620 | // the params of the call or the response
621 | // or the Fault if this is a Fault
622 | func Unmarshal(r io.Reader) (name string, params []interface{}, fault *Fault, e error) {
623 | p := xml.NewDecoder(r)
624 | st := newParser(p)
625 | typ := "methodResponse"
626 | if _, e = st.getStart(typ); ErrEq(e, errNameMismatch) { // methodResponse or methodCall
627 | typ = "methodCall"
628 | if name, e = st.getText("methodName"); e != nil {
629 | return
630 | }
631 | }
632 | var se xml.StartElement
633 | if se, e = st.getStart("params"); e != nil {
634 | if ErrEq(e, errNameMismatch) && se.Name.Local == "fault" {
635 | var v interface{}
636 | if v, e = st.parseValue(); e != nil {
637 | return
638 | }
639 | fmap, ok := v.(map[string]interface{})
640 | if !ok {
641 | e = fmt.Errorf("fault not fault: %+v", v)
642 | return
643 | }
644 | fault = &Fault{Code: -1, Message: ""}
645 | code, ok := fmap["faultCode"]
646 | if !ok {
647 | e = fmt.Errorf("no faultCode in fault: %v", fmap)
648 | return
649 | }
650 | fcode, ok := code.(int)
651 | if !ok {
652 | e = fmt.Errorf("faultCode not int? %v", code)
653 | return
654 | }
655 | fault.Code = int(fcode)
656 | msg, ok := fmap["faultString"]
657 | if !ok {
658 | e = fmt.Errorf("no faultString in fault: %v", fmap)
659 | return
660 | }
661 | if fault.Message, ok = msg.(string); !ok {
662 | e = fmt.Errorf("faultString not strin? %v", msg)
663 | return
664 | }
665 | e = st.checkLast("fault")
666 | }
667 | return
668 | }
669 | params = make([]interface{}, 0, 8)
670 | var v interface{}
671 | for {
672 | if _, e = st.getStart("param"); e != nil {
673 | if ErrEq(e, errNotStartElement) {
674 | e = nil
675 | break
676 | }
677 | return
678 | }
679 | if v, e = st.parseValue(); e != nil {
680 | break
681 | }
682 | params = append(params, v)
683 | if e = st.checkLast("param"); e != nil {
684 | return
685 | }
686 | }
687 | if e = st.checkLast("params"); e == nil {
688 | e = st.checkLast(typ)
689 | }
690 | return
691 | }
692 |
693 | type errorStruct struct {
694 | main error
695 | message string
696 | }
697 |
698 | func (es errorStruct) Error() string {
699 | return es.main.Error() + " [" + es.message + "]"
700 | }
701 |
702 | // Errorf2 returns an error embedding the main error with the formatted message
703 | func Errorf2(err error, format string, a ...interface{}) error {
704 | return &errorStruct{main: err, message: fmt.Sprintf(format, a...)}
705 | }
706 |
707 | // ErrEq checks equality of the errorStructs (equality of the embedded main errors
708 | func ErrEq(a, b error) bool {
709 | var maina, mainb error = a, b
710 | if esa, ok := a.(errorStruct); ok {
711 | maina = esa.main
712 | } else if esa, ok := a.(*errorStruct); ok {
713 | maina = esa.main
714 | }
715 | if esb, ok := b.(errorStruct); ok {
716 | mainb = esb.main
717 | } else if esb, ok := b.(*errorStruct); ok {
718 | mainb = esb.main
719 | }
720 | return maina == mainb
721 | }
722 |
--------------------------------------------------------------------------------
/rtorrent/rtorrent_test.go:
--------------------------------------------------------------------------------
1 | package rtorrent
2 |
3 | import (
4 | "io/ioutil"
5 | "testing"
6 | "time"
7 |
8 | "github.com/pkg/errors"
9 | "github.com/stretchr/testify/require"
10 | )
11 |
12 | func TestRTorrent(t *testing.T) {
13 | /*
14 | These tests rely on a local instance of rtorrent to be running in a clean state.
15 | Use the included `test.sh` script to run these tests.
16 | */
17 | client := New("http://localhost:8000", false)
18 | maxRetries := 60
19 |
20 | t.Run("get ip", func(t *testing.T) {
21 | _, err := client.IP()
22 | require.NoError(t, err)
23 | // Don't assert anything about the response, differs based upon the environment
24 | })
25 |
26 | t.Run("get name", func(t *testing.T) {
27 | name, err := client.Name()
28 | require.NoError(t, err)
29 | require.NotEmpty(t, name)
30 | })
31 |
32 | t.Run("down total", func(t *testing.T) {
33 | total, err := client.DownTotal()
34 | require.NoError(t, err)
35 | require.Zero(t, total, "expected no data to be transferred yet")
36 | })
37 |
38 | t.Run("up total", func(t *testing.T) {
39 | total, err := client.UpTotal()
40 | require.NoError(t, err)
41 | require.Zero(t, total, "expected no data to be transferred yet")
42 | })
43 |
44 | t.Run("down rate", func(t *testing.T) {
45 | rate, err := client.DownRate()
46 | require.NoError(t, err)
47 | require.Zero(t, rate, "expected no download yet")
48 | })
49 |
50 | t.Run("up rate", func(t *testing.T) {
51 | rate, err := client.UpRate()
52 | require.NoError(t, err)
53 | require.Zero(t, rate, "expected no upload yet")
54 | })
55 |
56 | t.Run("get no torrents", func(t *testing.T) {
57 | torrents, err := client.GetTorrents(ViewMain)
58 | require.NoError(t, err)
59 | require.Empty(t, torrents, "expected no torrents to be added yet")
60 | })
61 |
62 | t.Run("add", func(t *testing.T) {
63 | t.Run("by url", func(t *testing.T) {
64 | err := client.Add("https://torrent.fedoraproject.org/torrents/Fedora-i3-Live-x86_64-35.torrent")
65 | require.NoError(t, err)
66 |
67 | t.Run("get torrent", func(t *testing.T) {
68 | // It will take some time to appear, so retry a few times
69 | var torrents []Torrent
70 | var err error
71 | retries := maxRetries
72 | for i := 0; i <= retries; i++ {
73 | <-time.After(time.Second)
74 | torrents, err = client.GetTorrents(ViewMain)
75 | require.NoError(t, err)
76 | if len(torrents) > 0 {
77 | break
78 | }
79 | if i == retries {
80 | require.NoError(t, errors.Errorf("torrent did not show up in time"))
81 | }
82 | }
83 | require.NotEmpty(t, torrents)
84 | require.Len(t, torrents, 1)
85 | require.Equal(t, "299939CFF841ED7FFCA2B3C2A35711C12589632B", torrents[0].Hash)
86 | require.Equal(t, "Fedora-i3-Live-x86_64-35", torrents[0].Name)
87 | require.Equal(t, "", torrents[0].Label)
88 | require.Equal(t, 1437206706, torrents[0].Size)
89 | require.Equal(t, "/downloads/temp/Fedora-i3-Live-x86_64-35", torrents[0].Path)
90 | require.False(t, torrents[0].Completed)
91 |
92 | t.Run("get files", func(t *testing.T) {
93 | files, err := client.GetFiles(torrents[0])
94 | require.NoError(t, err)
95 | require.NotEmpty(t, files)
96 | require.Len(t, files, 2)
97 | for _, f := range files {
98 | require.NotEmpty(t, f.Path)
99 | require.NotZero(t, f.Size)
100 | }
101 | })
102 |
103 | t.Run("single get", func(t *testing.T) {
104 | torrent, err := client.GetTorrent(torrents[0].Hash)
105 | require.NoError(t, err)
106 | require.NotEmpty(t, torrent.Hash)
107 | require.NotEmpty(t, torrent.Name)
108 | require.NotEmpty(t, torrent.Path)
109 | require.NotEmpty(t, torrent.Size)
110 | })
111 |
112 | t.Run("change label", func(t *testing.T) {
113 | err := client.SetLabel(torrents[0], "TestLabel")
114 | require.NoError(t, err)
115 |
116 | // It will take some time to change, so try a few times
117 | retries := maxRetries
118 | for i := 0; i <= retries; i++ {
119 | <-time.After(time.Second)
120 | torrents, err = client.GetTorrents(ViewMain)
121 | require.NoError(t, err)
122 | require.Len(t, torrents, 1)
123 | if torrents[0].Label != "" {
124 | break
125 | }
126 | if i == retries {
127 | require.NoError(t, errors.Errorf("torrent label did not change in time"))
128 | }
129 | }
130 | require.Equal(t, "TestLabel", torrents[0].Label)
131 | })
132 |
133 | t.Run("get status", func(t *testing.T) {
134 | var status Status
135 | var err error
136 | // It may take some time for the download to start
137 | retries := maxRetries
138 | for i := 0; i <= retries; i++ {
139 | <-time.After(time.Second)
140 | status, err = client.GetStatus(torrents[0])
141 | require.NoError(t, err)
142 | t.Logf("Status = %+v", status)
143 | if status.CompletedBytes > 0 {
144 | break
145 | }
146 | if i == retries {
147 | require.NoError(t, errors.Errorf("torrent did not start in time"))
148 | }
149 | }
150 |
151 | require.False(t, status.Completed)
152 | require.NotZero(t, status.CompletedBytes)
153 | require.NotZero(t, status.DownRate)
154 | require.NotZero(t, status.Size)
155 | // require.NotZero(t, status.UpRate)
156 | //require.NotZero(t, status.Ratio)
157 | })
158 |
159 | t.Run("delete torrent", func(t *testing.T) {
160 | err := client.Delete(torrents[0])
161 | require.NoError(t, err)
162 |
163 | torrents, err := client.GetTorrents(ViewMain)
164 | require.NoError(t, err)
165 | require.Empty(t, torrents)
166 |
167 | t.Run("get torrent", func(t *testing.T) {
168 | // It will take some time to disappear, so retry a few times
169 | var torrents []Torrent
170 | var err error
171 | retries := maxRetries
172 | for i := 0; i <= retries; i++ {
173 | <-time.After(time.Second)
174 | torrents, err = client.GetTorrents(ViewMain)
175 | require.NoError(t, err)
176 | if len(torrents) == 0 {
177 | break
178 | }
179 | if i == retries {
180 | require.NoError(t, errors.Errorf("torrent did not delete in time"))
181 | }
182 | }
183 | require.Empty(t, torrents)
184 | })
185 | })
186 |
187 | })
188 | })
189 |
190 | t.Run("by url (stopped)", func(t *testing.T) {
191 | label := DLabel.SetValue("test-label")
192 | err := client.AddStopped("https://torrent.fedoraproject.org/torrents/Fedora-i3-Live-x86_64-35.torrent", label)
193 | require.NoError(t, err)
194 |
195 | t.Run("get torrent", func(t *testing.T) {
196 | // It will take some time to appear, so retry a few times
197 | var torrents []Torrent
198 | var err error
199 | retries := maxRetries
200 | for i := 0; i <= retries; i++ {
201 | <-time.After(time.Second)
202 | torrents, err = client.GetTorrents(ViewStopped)
203 | require.NoError(t, err)
204 | if len(torrents) > 0 {
205 | break
206 | }
207 | if i == retries {
208 | require.NoError(t, errors.Errorf("torrent did not show up in time"))
209 | }
210 | }
211 | require.NotEmpty(t, torrents)
212 | require.Len(t, torrents, 1)
213 | require.Equal(t, "299939CFF841ED7FFCA2B3C2A35711C12589632B", torrents[0].Hash)
214 | require.Equal(t, "Fedora-i3-Live-x86_64-35", torrents[0].Name)
215 | require.Equal(t, label.Value, torrents[0].Label)
216 | require.Equal(t, 1437206706, torrents[0].Size)
217 | require.Equal(t, "/downloads/temp/Fedora-i3-Live-x86_64-35", torrents[0].Path)
218 | require.False(t, torrents[0].Completed)
219 |
220 | t.Run("get status", func(t *testing.T) {
221 | <-time.After(time.Second)
222 | status, err := client.GetStatus(torrents[0])
223 | require.NoError(t, err)
224 | t.Logf("Status = %+v", status)
225 |
226 | require.False(t, status.Completed)
227 | require.Zero(t, status.CompletedBytes)
228 | require.Zero(t, status.DownRate)
229 | require.NotZero(t, status.Size)
230 | })
231 |
232 | t.Run("start torrent", func(t *testing.T) {
233 | err = client.StartTorrent(torrents[0])
234 | require.NoError(t, err)
235 |
236 | t.Run("check if started", func(t *testing.T) {
237 | var isOpen, isActive bool
238 | var state, retries int = 0, maxRetries
239 | for i := 0; i <= retries; i++ {
240 | <-time.After(time.Second)
241 |
242 | isOpen, err = client.IsOpen(torrents[0])
243 | require.NoError(t, err)
244 |
245 | isActive, err = client.IsActive(torrents[0])
246 | require.NoError(t, err)
247 |
248 | state, err = client.State(torrents[0])
249 | require.NoError(t, err)
250 |
251 | if isOpen && isActive && state == 1 {
252 | break
253 | }
254 |
255 | if i == retries {
256 | require.NoError(t, errors.Errorf("torrent did not start in time"))
257 | }
258 | }
259 |
260 | require.True(t, isOpen)
261 | require.True(t, isActive)
262 | require.Equal(t, 1, state)
263 | })
264 |
265 | // wait some seconds to properly start to download bytes so
266 | // to allow testing for up/down total post activity
267 | <-time.After(time.Second * 10)
268 |
269 | t.Run("pause torrent", func(t *testing.T) {
270 | err := client.PauseTorrent(torrents[0])
271 | require.NoError(t, err)
272 |
273 | var isOpen, isActive bool
274 | var state, retries int = 0, maxRetries
275 | for i := 0; i <= retries; i++ {
276 | <-time.After(time.Second)
277 | isOpen, err = client.IsOpen(torrents[0])
278 | require.NoError(t, err)
279 |
280 | isActive, err = client.IsActive(torrents[0])
281 | require.NoError(t, err)
282 |
283 | state, err = client.State(torrents[0])
284 | require.NoError(t, err)
285 |
286 | if isOpen && !isActive && state == 1 {
287 | break
288 | }
289 |
290 | if i == retries {
291 | require.NoError(t, errors.Errorf("torrent did not pause in time"))
292 | }
293 | }
294 | require.True(t, isOpen)
295 | require.False(t, isActive)
296 | require.Equal(t, 1, state)
297 | })
298 |
299 | t.Run("resume torrent", func(t *testing.T) {
300 | err := client.ResumeTorrent(torrents[0])
301 | require.NoError(t, err)
302 | var isOpen, isActive bool
303 | var state, retries int = 0, maxRetries
304 | for i := 0; i <= retries; i++ {
305 | <-time.After(time.Second)
306 | isOpen, err = client.IsOpen(torrents[0])
307 | require.NoError(t, err)
308 |
309 | isActive, err = client.IsActive(torrents[0])
310 | require.NoError(t, err)
311 |
312 | state, err = client.State(torrents[0])
313 | require.NoError(t, err)
314 |
315 | if isOpen && isActive && state == 1 {
316 | break
317 | }
318 |
319 | if i == retries {
320 | require.NoError(t, errors.Errorf("torrent did not resume in time"))
321 | }
322 | }
323 | require.True(t, isOpen)
324 | require.True(t, isActive)
325 | require.Equal(t, 1, state)
326 | })
327 |
328 | })
329 |
330 | t.Run("stop torrent", func(t *testing.T) {
331 | err = client.StopTorrent(torrents[0])
332 | require.NoError(t, err)
333 |
334 | t.Run("check if stopped", func(t *testing.T) {
335 | var isOpen, isActive bool
336 | var state, retries int = 0, maxRetries
337 | for i := 0; i <= retries; i++ {
338 | <-time.After(time.Second)
339 |
340 | isOpen, err = client.IsOpen(torrents[0])
341 | require.NoError(t, err)
342 |
343 | isActive, err = client.IsActive(torrents[0])
344 | require.NoError(t, err)
345 |
346 | state, err = client.State(torrents[0])
347 | require.NoError(t, err)
348 |
349 | if isOpen && !isActive && state == 0 {
350 | break
351 | }
352 |
353 | if i == retries {
354 | require.NoError(t, errors.Errorf("torrent did not stop in time"))
355 | }
356 | }
357 | require.True(t, isOpen)
358 | require.False(t, isActive)
359 | require.Equal(t, 0, state)
360 | })
361 | })
362 |
363 | t.Run("close torrent", func(t *testing.T) {
364 | err = client.CloseTorrent(torrents[0])
365 | require.NoError(t, err)
366 |
367 | t.Run("check if closed", func(t *testing.T) {
368 | var isOpen, isActive bool
369 | var state, retries int = 0, maxRetries
370 | for i := 0; i <= retries; i++ {
371 | <-time.After(time.Second)
372 |
373 | isOpen, err = client.IsOpen(torrents[0])
374 | require.NoError(t, err)
375 |
376 | isActive, err = client.IsActive(torrents[0])
377 | require.NoError(t, err)
378 |
379 | state, err = client.State(torrents[0])
380 | require.NoError(t, err)
381 |
382 | if !isOpen && !isActive && state == 0 {
383 | break
384 | }
385 |
386 | if i == retries {
387 | require.NoError(t, errors.Errorf("torrent did not close in time"))
388 | }
389 | }
390 | require.False(t, isOpen)
391 | require.False(t, isActive)
392 | require.Equal(t, 0, state)
393 | })
394 | })
395 |
396 | t.Run("open torrent", func(t *testing.T) {
397 | err = client.OpenTorrent(torrents[0])
398 | require.NoError(t, err)
399 |
400 | t.Run("check if open", func(t *testing.T) {
401 | var isOpen bool
402 | var retries int = maxRetries
403 | for i := 0; i <= retries; i++ {
404 | <-time.After(time.Second)
405 |
406 | isOpen, err = client.IsOpen(torrents[0])
407 | require.NoError(t, err)
408 |
409 | if isOpen {
410 | break
411 | }
412 |
413 | if i == retries {
414 | require.NoError(t, errors.Errorf("torrent did not open in time"))
415 | }
416 | }
417 | require.True(t, isOpen)
418 | })
419 | })
420 |
421 | t.Run("re-close torrent", func(t *testing.T) {
422 | err = client.CloseTorrent(torrents[0])
423 | require.NoError(t, err)
424 |
425 | t.Run("check if closed", func(t *testing.T) {
426 | var isOpen, isActive bool
427 | var state, retries int = 0, maxRetries
428 | for i := 0; i <= retries; i++ {
429 | <-time.After(time.Second)
430 |
431 | isOpen, err = client.IsOpen(torrents[0])
432 | require.NoError(t, err)
433 |
434 | isActive, err = client.IsActive(torrents[0])
435 | require.NoError(t, err)
436 |
437 | state, err = client.State(torrents[0])
438 | require.NoError(t, err)
439 |
440 | if !isOpen && !isActive && state == 0 {
441 | break
442 | }
443 |
444 | if i == retries {
445 | require.NoError(t, errors.Errorf("torrent did not close in time"))
446 | }
447 | }
448 | require.False(t, isOpen)
449 | require.False(t, isActive)
450 | require.Equal(t, 0, state)
451 | })
452 | })
453 |
454 | t.Run("delete torrent", func(t *testing.T) {
455 | err := client.Delete(torrents[0])
456 | require.NoError(t, err)
457 |
458 | torrents, err := client.GetTorrents(ViewMain)
459 | require.NoError(t, err)
460 | require.Empty(t, torrents)
461 |
462 | t.Run("get torrent", func(t *testing.T) {
463 | // It will take some time to disappear, so retry a few times
464 | var torrents []Torrent
465 | var err error
466 | retries := maxRetries
467 | for i := 0; i <= retries; i++ {
468 | <-time.After(time.Second)
469 | torrents, err = client.GetTorrents(ViewMain)
470 | require.NoError(t, err)
471 | if len(torrents) == 0 {
472 | break
473 | }
474 | if i == retries {
475 | require.NoError(t, errors.Errorf("torrent did not delete in time"))
476 | }
477 | }
478 | require.Empty(t, torrents)
479 | })
480 | })
481 | })
482 | })
483 |
484 | t.Run("with data", func(t *testing.T) {
485 | b, err := ioutil.ReadFile("testdata/Fedora-i3-Live-x86_64-35.torrent")
486 | require.NoError(t, err)
487 | require.NotEmpty(t, b)
488 |
489 | err = client.AddTorrent(b)
490 | require.NoError(t, err)
491 |
492 | t.Run("get torrent", func(t *testing.T) {
493 | // It will take some time to appear, so retry a few times
494 | var torrents []Torrent
495 | var err error
496 | retries := maxRetries
497 | for i := 0; i <= retries; i++ {
498 | <-time.After(time.Second)
499 | torrents, err = client.GetTorrents(ViewMain)
500 | require.NoError(t, err)
501 | if len(torrents) > 0 {
502 | break
503 | }
504 | if i == retries {
505 | require.NoError(t, errors.Errorf("torrent did not show up in time"))
506 | }
507 | }
508 | require.NotEmpty(t, torrents)
509 | require.Len(t, torrents, 1)
510 | require.Equal(t, "299939CFF841ED7FFCA2B3C2A35711C12589632B", torrents[0].Hash)
511 | require.Equal(t, "Fedora-i3-Live-x86_64-35", torrents[0].Name)
512 | require.Equal(t, "", torrents[0].Label)
513 | require.Equal(t, 1437206706, torrents[0].Size)
514 | require.Equal(t, "/downloads/temp/Fedora-i3-Live-x86_64-35", torrents[0].Path)
515 | require.False(t, torrents[0].Completed)
516 |
517 | t.Run("get files", func(t *testing.T) {
518 | files, err := client.GetFiles(torrents[0])
519 | require.NoError(t, err)
520 | require.NotEmpty(t, files)
521 | require.Len(t, files, 2)
522 | for _, f := range files {
523 | require.NotEmpty(t, f.Path)
524 | require.NotZero(t, f.Size)
525 | }
526 | })
527 |
528 | t.Run("delete torrent", func(t *testing.T) {
529 | err := client.Delete(torrents[0])
530 | require.NoError(t, err)
531 |
532 | torrents, err := client.GetTorrents(ViewMain)
533 | require.NoError(t, err)
534 | require.Empty(t, torrents)
535 |
536 | t.Run("get torrent", func(t *testing.T) {
537 | // It will take some time to disappear, so retry a few times
538 | var torrents []Torrent
539 | var err error
540 | retries := maxRetries
541 | for i := 0; i <= retries; i++ {
542 | <-time.After(time.Second)
543 | torrents, err = client.GetTorrents(ViewMain)
544 | require.NoError(t, err)
545 | if len(torrents) == 0 {
546 | break
547 | }
548 | if i == retries {
549 | require.NoError(t, errors.Errorf("torrent did not delete in time"))
550 | }
551 | }
552 | require.Empty(t, torrents)
553 | })
554 | })
555 | })
556 | })
557 |
558 | t.Run("with data (stopped)", func(t *testing.T) {
559 | b, err := ioutil.ReadFile("testdata/Fedora-i3-Live-x86_64-35.torrent")
560 | require.NoError(t, err)
561 | require.NotEmpty(t, b)
562 |
563 | label := DLabel.SetValue("test-label")
564 | err = client.AddTorrentStopped(b, label)
565 | require.NoError(t, err)
566 |
567 | t.Run("get torrent", func(t *testing.T) {
568 | // It will take some time to appear, so retry a few times
569 | <-time.After(time.Second)
570 | torrents, err := client.GetTorrents(ViewMain)
571 | require.NoError(t, err)
572 |
573 | require.NotEmpty(t, torrents)
574 | require.Len(t, torrents, 1)
575 | require.Equal(t, "299939CFF841ED7FFCA2B3C2A35711C12589632B", torrents[0].Hash)
576 | require.Equal(t, "Fedora-i3-Live-x86_64-35", torrents[0].Name)
577 | require.Equal(t, label.Value, torrents[0].Label)
578 | require.Equal(t, 1437206706, torrents[0].Size)
579 |
580 | t.Run("delete torrent", func(t *testing.T) {
581 | err := client.Delete(torrents[0])
582 | require.NoError(t, err)
583 |
584 | torrents, err := client.GetTorrents(ViewMain)
585 | require.NoError(t, err)
586 | require.Empty(t, torrents)
587 |
588 | t.Run("get torrent", func(t *testing.T) {
589 | // It will take some time to disappear, so retry a few times
590 | var torrents []Torrent
591 | var err error
592 | retries := maxRetries
593 | for i := 0; i <= retries; i++ {
594 | <-time.After(time.Second)
595 | torrents, err = client.GetTorrents(ViewMain)
596 | require.NoError(t, err)
597 | if len(torrents) == 0 {
598 | break
599 | }
600 | if i == retries {
601 | require.NoError(t, errors.Errorf("torrent did not delete in time"))
602 | }
603 | }
604 | require.Empty(t, torrents)
605 | })
606 | })
607 | })
608 | })
609 | })
610 |
611 | t.Run("down total post activity", func(t *testing.T) {
612 | total, err := client.DownTotal()
613 | require.NoError(t, err)
614 | require.NotZero(t, total, "expected data to be transferred")
615 | })
616 |
617 | t.Run("up total post activity", func(t *testing.T) {
618 | total, err := client.UpTotal()
619 | require.NoError(t, err)
620 | require.NotZero(t, total, "expected data to be transferred")
621 | })
622 |
623 | }
624 |
--------------------------------------------------------------------------------
/rtorrent/rtorrent.go:
--------------------------------------------------------------------------------
1 | package rtorrent
2 |
3 | import (
4 | "fmt"
5 | "net/http"
6 | "time"
7 |
8 | "github.com/mrobinsn/go-rtorrent/xmlrpc"
9 | "github.com/pkg/errors"
10 | )
11 |
12 | // RTorrent is used to communicate with a remote rTorrent instance
13 | type RTorrent struct {
14 | addr string
15 | xmlrpcClient *xmlrpc.Client
16 | }
17 |
18 | // FieldValue contains the Field and Value of an attribute on a rTorrent
19 | type FieldValue struct {
20 | Field Field
21 | Value string
22 | }
23 |
24 | // Torrent represents a torrent in rTorrent
25 | type Torrent struct {
26 | Hash string
27 | Name string
28 | Path string
29 | Size int
30 | Label string
31 | Completed bool
32 | Ratio float64
33 | Created time.Time
34 | Started time.Time
35 | Finished time.Time
36 | }
37 |
38 | // Status represents the status of a torrent
39 | type Status struct {
40 | Completed bool
41 | CompletedBytes int
42 | DownRate int
43 | UpRate int
44 | Ratio float64
45 | Size int
46 | }
47 |
48 | // File represents a file in rTorrent
49 | type File struct {
50 | Path string
51 | Size int
52 | }
53 |
54 | // Field represents a attribute on a RTorrent entity that can be queried or set
55 | type Field string
56 |
57 | // View represents a "view" within RTorrent
58 | type View string
59 |
60 | const (
61 | // ViewMain represents the "main" view, containing all torrents
62 | ViewMain View = "main"
63 | // ViewStarted represents the "started" view, containing only torrents that have been started
64 | ViewStarted View = "started"
65 | // ViewStopped represents the "stopped" view, containing only torrents that have been stopped
66 | ViewStopped View = "stopped"
67 | // ViewHashing represents the "hashing" view, containing only torrents that are currently hashing
68 | ViewHashing View = "hashing"
69 | // ViewSeeding represents the "seeding" view, containing only torrents that are currently seeding
70 | ViewSeeding View = "seeding"
71 |
72 | // DName represents the name of a "Downloading Items"
73 | DName Field = "d.name"
74 | // DLabel represents the label of a "Downloading Item"
75 | DLabel Field = "d.custom1"
76 | // DSizeInBytes represents the size in bytes of a "Downloading Item"
77 | DSizeInBytes Field = "d.size_bytes"
78 | // DHash represents the hash of a "Downloading Item"
79 | DHash Field = "d.hash"
80 | // DBasePath represents the base path of a "Downloading Item"
81 | DBasePath Field = "d.base_path"
82 | // DDirectory represents the directory of a "Downloading Item"
83 | DDirectory Field = "d.directory"
84 | // DIsActive represents whether a "Downloading Item" is active or not
85 | DIsActive Field = "d.is_active"
86 | // DRatio represents the ratio of a "Downloading Item"
87 | DRatio Field = "d.ratio"
88 | // DComplete represents whether the "Downloading Item" is complete or not
89 | DComplete Field = "d.complete"
90 | // DCompletedBytes represents the total of completed bytes of the "Downloading Item"
91 | DCompletedBytes Field = "d.completed_bytes"
92 | // DDownRate represents the download rate of the "Downloading Item"
93 | DDownRate Field = "d.down.rate"
94 | // DUpRate represents the upload rate of the "Downloading Item"
95 | DUpRate Field = "d.up.rate"
96 | // DCreationTime represents the date the torrent was created
97 | DCreationTime Field = "d.creation_date"
98 | // DFinishedTime represents the date the torrent finished downloading
99 | DFinishedTime Field = "d.timestamp.finished"
100 | // DStartedTime represents the date the torrent started downloading
101 | DStartedTime Field = "d.timestamp.started"
102 |
103 | // FPath represents the path of a "File Item"
104 | FPath Field = "f.path"
105 | // FSizeInBytes represents the size in bytes of a "File Item"
106 | FSizeInBytes Field = "f.size_bytes"
107 | )
108 |
109 | // Query converts the field to a string which allows it to be queried
110 | // Example:
111 | // DName.Query() // returns "d.name="
112 | func (f Field) Query() string {
113 | return fmt.Sprintf("%s=", f)
114 | }
115 |
116 | // SetValue returns a FieldValue struct which can be used to set the field on a particular item in rTorrent to the specified value
117 | func (f Field) SetValue(value string) *FieldValue {
118 | return &FieldValue{f, value}
119 | }
120 |
121 | // Cmd returns the representation of the field which allows it to be used a command with RTorrent
122 | func (f Field) Cmd() string {
123 | return string(f)
124 | }
125 |
126 | func (f *FieldValue) String() string {
127 | return fmt.Sprintf("%s.set=\"%s\"", f.Field, f.Value)
128 | }
129 |
130 | // Pretty returns a formatted string representing this Torrent
131 | func (t *Torrent) Pretty() string {
132 | return fmt.Sprintf("Torrent:\n\tHash: %v\n\tName: %v\n\tPath: %v\n\tLabel: %v\n\tSize: %v bytes\n\tCompleted: %v\n\tRatio: %v\n", t.Hash, t.Name, t.Path, t.Label, t.Size, t.Completed, t.Ratio)
133 | }
134 |
135 | // Pretty returns a formatted string representing this File
136 | func (f *File) Pretty() string {
137 | return fmt.Sprintf("File:\n\tPath: %v\n\tSize: %v bytes\n", f.Path, f.Size)
138 | }
139 |
140 | // New returns a new instance of `RTorrent`
141 | // Pass in a true value for `insecure` to turn off certificate verification
142 | func New(addr string, insecure bool) *RTorrent {
143 | return &RTorrent{
144 | addr: addr,
145 | xmlrpcClient: xmlrpc.NewClient(addr, insecure),
146 | }
147 | }
148 |
149 | // WithHTTPClient allows you to a provide a custom http.Client.
150 | func (r *RTorrent) WithHTTPClient(client *http.Client) *RTorrent {
151 | r.xmlrpcClient = xmlrpc.NewClientWithHTTPClient(r.addr, client)
152 | return r
153 | }
154 |
155 | // AddStopped adds a new torrent by URL in a stopped state
156 | //
157 | // extraArgs can be any valid rTorrent rpc command. For instance:
158 | //
159 | // Adds the Torrent by URL (stopped) and sets the label on the torrent
160 | // AddStopped("some-url", &FieldValue{"d.custom1", "my-label"})
161 | // Or:
162 | // AddStopped("some-url", DLabel.SetValue("my-label"))
163 | //
164 | // Adds the Torrent by URL (stopped) and sets the label and base path
165 | // AddStopped("some-url", &FieldValue{"d.custom1", "my-label"}, &FiedValue{"d.base_path", "/some/valid/path"})
166 | // Or:
167 | // AddStopped("some-url", DLabel.SetValue("my-label"), DBasePath.SetValue("/some/valid/path"))
168 | func (r *RTorrent) AddStopped(url string, extraArgs ...*FieldValue) error {
169 | return r.add("load.normal", []byte(url), extraArgs...)
170 | }
171 |
172 | // Add adds a new torrent by URL and starts the torrent
173 | //
174 | // extraArgs can be any valid rTorrent rpc command. For instance:
175 | //
176 | // Adds the Torrent by URL and sets the label on the torrent
177 | // Add("some-url", "d.custom1.set=\"my-label\"")
178 | // Or:
179 | // Add("some-url", DLabel.SetValue("my-label"))
180 | //
181 | // Adds the Torrent by URL and sets the label as well as base path
182 | // Add("some-url", "d.custom1.set=\"my-label\"", "d.base_path=\"/some/valid/path\"")
183 | // Or:
184 | // Add("some-url", DLabel.SetValue("my-label"), DBasePath.SetValue("/some/valid/path"))
185 | func (r *RTorrent) Add(url string, extraArgs ...*FieldValue) error {
186 | return r.add("load.start", []byte(url), extraArgs...)
187 | }
188 |
189 | // AddTorrentStopped adds a new torrent by the torrent files data but does not start the torrent
190 | //
191 | // extraArgs can be any valid rTorrent rpc command. For instance:
192 | //
193 | // Adds the Torrent file (stopped) and sets the label on the torrent
194 | // AddTorrentStopped(fileData, "d.custom1.set=\"my-label\"")
195 | // Or:
196 | // AddTorrentStopped(fileData, DLabel.SetValue("my-label"))
197 | //
198 | // Adds the Torrent file and (stopped) sets the label and base path
199 | // AddTorrentStopped(fileData, "d.custom1.set=\"my-label\"", "d.base_path=\"/some/valid/path\"")
200 | // Or:
201 | // AddTorrentStopped(fileData, DLabel.SetValue("my-label"), DBasePath.SetValue("/some/valid/path"))
202 | func (r *RTorrent) AddTorrentStopped(data []byte, extraArgs ...*FieldValue) error {
203 | return r.add("load.raw", data, extraArgs...)
204 | }
205 |
206 | // AddTorrent adds a new torrent by the torrent files data and starts the torrent
207 | //
208 | // extraArgs can be any valid rTorrent rpc command. For instance:
209 | //
210 | // Adds the Torrent file and sets the label on the torrent
211 | // Add(fileData, "d.custom1.set=\"my-label\"")
212 | // Or:
213 | // AddTorrent(fileData, DLabel.SetValue("my-label"))
214 | //
215 | // Adds the Torrent file and sets the label and base path
216 | // Add(fileData, "d.custom1.set=\"my-label\"", "d.base_path=\"/some/valid/path\"")
217 | // Or:
218 | // AddTorrent(fileData, DLabel.SetValue("my-label"), DBasePath.SetValue("/some/valid/path"))
219 | func (r *RTorrent) AddTorrent(data []byte, extraArgs ...*FieldValue) error {
220 | return r.add("load.raw_start", data, extraArgs...)
221 | }
222 |
223 | func (r *RTorrent) add(cmd string, data []byte, extraArgs ...*FieldValue) error {
224 | args := []interface{}{data}
225 | for _, v := range extraArgs {
226 | args = append(args, v.String())
227 | }
228 |
229 | _, err := r.xmlrpcClient.Call(cmd, "", args)
230 | if err != nil {
231 | return errors.Wrap(err, fmt.Sprintf("%s XMLRPC call failed", cmd))
232 | }
233 | return nil
234 | }
235 |
236 | // IP returns the IP reported by this RTorrent instance
237 | func (r *RTorrent) IP() (string, error) {
238 | result, err := r.xmlrpcClient.Call("network.bind_address")
239 | if err != nil {
240 | return "", errors.Wrap(err, "network.bind_address XMLRPC call failed")
241 | }
242 | if ips, ok := result.([]interface{}); ok {
243 | result = ips[0]
244 | }
245 | if ip, ok := result.(string); ok {
246 | return ip, nil
247 | }
248 | return "", errors.Errorf("result isn't string: %v", result)
249 | }
250 |
251 | // Name returns the name reported by this RTorrent instance
252 | func (r *RTorrent) Name() (string, error) {
253 | result, err := r.xmlrpcClient.Call("system.hostname")
254 | if err != nil {
255 | return "", errors.Wrap(err, "system.hostname XMLRPC call failed")
256 | }
257 | if names, ok := result.([]interface{}); ok {
258 | result = names[0]
259 | }
260 | if name, ok := result.(string); ok {
261 | return name, nil
262 | }
263 | return "", errors.Errorf("result isn't string: %v", result)
264 | }
265 |
266 | // DownTotal returns the total downloaded metric reported by this RTorrent instance (bytes)
267 | func (r *RTorrent) DownTotal() (int, error) {
268 | result, err := r.xmlrpcClient.Call("throttle.global_down.total")
269 | if err != nil {
270 | return 0, errors.Wrap(err, "throttle.global_down.total XMLRPC call failed")
271 | }
272 | if totals, ok := result.([]interface{}); ok {
273 | result = totals[0]
274 | }
275 | if total, ok := result.(int); ok {
276 | return total, nil
277 | }
278 | return 0, errors.Errorf("result isn't int: %v", result)
279 | }
280 |
281 | // DownRate returns the current download rate reported by this RTorrent instance (bytes/s)
282 | func (r *RTorrent) DownRate() (int, error) {
283 | result, err := r.xmlrpcClient.Call("throttle.global_down.rate")
284 | if err != nil {
285 | return 0, errors.Wrap(err, "throttle.global_down.rate XMLRPC call failed")
286 | }
287 | if totals, ok := result.([]interface{}); ok {
288 | result = totals[0]
289 | }
290 | if total, ok := result.(int); ok {
291 | return total, nil
292 | }
293 | return 0, errors.Errorf("result isn't int: %v", result)
294 | }
295 |
296 | // UpTotal returns the total uploaded metric reported by this RTorrent instance (bytes)
297 | func (r *RTorrent) UpTotal() (int, error) {
298 | result, err := r.xmlrpcClient.Call("throttle.global_up.total")
299 | if err != nil {
300 | return 0, errors.Wrap(err, "throttle.global_up.total XMLRPC call failed")
301 | }
302 | if totals, ok := result.([]interface{}); ok {
303 | result = totals[0]
304 | }
305 | if total, ok := result.(int); ok {
306 | return total, nil
307 | }
308 | return 0, errors.Errorf("result isn't int: %v", result)
309 | }
310 |
311 | // UpRate returns the current upload rate reported by this RTorrent instance (bytes/s)
312 | func (r *RTorrent) UpRate() (int, error) {
313 | result, err := r.xmlrpcClient.Call("throttle.global_up.rate")
314 | if err != nil {
315 | return 0, errors.Wrap(err, "throttle.global_up.rate XMLRPC call failed")
316 | }
317 | if totals, ok := result.([]interface{}); ok {
318 | result = totals[0]
319 | }
320 | if total, ok := result.(int); ok {
321 | return total, nil
322 | }
323 | return 0, errors.Errorf("result isn't int: %v", result)
324 | }
325 |
326 | // GetTorrents returns all of the torrents reported by this RTorrent instance
327 | func (r *RTorrent) GetTorrents(view View) ([]Torrent, error) {
328 | args := []interface{}{"", string(view), DName.Query(), DSizeInBytes.Query(), DHash.Query(), DLabel.Query(), DDirectory.Query(), DIsActive.Query(), DComplete.Query(), DRatio.Query(), DCreationTime.Query(), DFinishedTime.Query(), DStartedTime.Query()}
329 | results, err := r.xmlrpcClient.Call("d.multicall2", args...)
330 | var torrents []Torrent
331 | if err != nil {
332 | return torrents, errors.Wrap(err, "d.multicall2 XMLRPC call failed")
333 | }
334 | for _, outerResult := range results.([]interface{}) {
335 | for _, innerResult := range outerResult.([]interface{}) {
336 | torrentData := innerResult.([]interface{})
337 | torrents = append(torrents, Torrent{
338 | Hash: torrentData[2].(string),
339 | Name: torrentData[0].(string),
340 | Path: torrentData[4].(string),
341 | Size: torrentData[1].(int),
342 | Label: torrentData[3].(string),
343 | Completed: torrentData[6].(int) > 0,
344 | Ratio: float64(torrentData[7].(int)) / float64(1000),
345 | Created: time.Unix(int64(torrentData[8].(int)), 0),
346 | Finished: time.Unix(int64(torrentData[9].(int)), 0),
347 | Started: time.Unix(int64(torrentData[10].(int)), 0),
348 | })
349 | }
350 | }
351 | return torrents, nil
352 | }
353 |
354 | // GetTorrent returns the torrent identified by the given hash
355 | func (r *RTorrent) GetTorrent(hash string) (Torrent, error) {
356 | var t Torrent
357 | t.Hash = hash
358 | // Name
359 | results, err := r.xmlrpcClient.Call("d.name", t.Hash)
360 | if err != nil {
361 | return t, errors.Wrap(err, "d.name XMLRPC call failed")
362 | }
363 | t.Name = results.([]interface{})[0].(string)
364 | // Size
365 | results, err = r.xmlrpcClient.Call("d.size_bytes", t.Hash)
366 | if err != nil {
367 | return t, errors.Wrap(err, "d.size_bytes XMLRPC call failed")
368 | }
369 | t.Size = results.([]interface{})[0].(int)
370 | // Label
371 | results, err = r.xmlrpcClient.Call("d.custom1", t.Hash)
372 | if err != nil {
373 | return t, errors.Wrap(err, "d.custom1 XMLRPC call failed")
374 | }
375 | t.Label = results.([]interface{})[0].(string)
376 | // Path
377 | results, err = r.xmlrpcClient.Call("d.directory", t.Hash)
378 | if err != nil {
379 | return t, errors.Wrap(err, "d.directory XMLRPC call failed")
380 | }
381 | t.Path = results.([]interface{})[0].(string)
382 | // Completed
383 | results, err = r.xmlrpcClient.Call("d.complete", t.Hash)
384 | if err != nil {
385 | return t, errors.Wrap(err, "d.complete XMLRPC call failed")
386 | }
387 | t.Completed = results.([]interface{})[0].(int) > 0
388 | // Ratio
389 | results, err = r.xmlrpcClient.Call("d.ratio", t.Hash)
390 | if err != nil {
391 | return t, errors.Wrap(err, "d.ratio XMLRPC call failed")
392 | }
393 | t.Ratio = float64(results.([]interface{})[0].(int)) / float64(1000)
394 | // Created
395 | results, err = r.xmlrpcClient.Call(string(DCreationTime), t.Hash)
396 | if err != nil {
397 | return t, errors.Wrap(err, fmt.Sprintf("%s XMLRPC call failed", string(DCreationTime)))
398 | }
399 | t.Created = time.Unix(int64(results.([]interface{})[0].(int)), 0)
400 | // Finished
401 | results, err = r.xmlrpcClient.Call(string(DFinishedTime), t.Hash)
402 | if err != nil {
403 | return t, errors.Wrap(err, fmt.Sprintf("%s XMLRPC call failed", string(DFinishedTime)))
404 | }
405 | t.Finished = time.Unix(int64(results.([]interface{})[0].(int)), 0)
406 | // Started
407 | results, err = r.xmlrpcClient.Call(string(DStartedTime), t.Hash)
408 | if err != nil {
409 | return t, errors.Wrap(err, fmt.Sprintf("%s XMLRPC call failed", string(DStartedTime)))
410 | }
411 | t.Created = time.Unix(int64(results.([]interface{})[0].(int)), 0)
412 |
413 | return t, nil
414 | }
415 |
416 | // Delete removes the torrent
417 | func (r *RTorrent) Delete(t Torrent) error {
418 | _, err := r.xmlrpcClient.Call("d.erase", t.Hash)
419 | if err != nil {
420 | return errors.Wrap(err, "d.erase XMLRPC call failed")
421 | }
422 | return nil
423 | }
424 |
425 | // GetFiles returns all of the files for a given `Torrent`
426 | func (r *RTorrent) GetFiles(t Torrent) ([]File, error) {
427 | args := []interface{}{t.Hash, 0, FPath.Query(), FSizeInBytes.Query()}
428 | results, err := r.xmlrpcClient.Call("f.multicall", args...)
429 | var files []File
430 | if err != nil {
431 | return files, errors.Wrap(err, "f.multicall XMLRPC call failed")
432 | }
433 | for _, outerResult := range results.([]interface{}) {
434 | for _, innerResult := range outerResult.([]interface{}) {
435 | fileData := innerResult.([]interface{})
436 | files = append(files, File{
437 | Path: fileData[0].(string),
438 | Size: fileData[1].(int),
439 | })
440 | }
441 | }
442 | return files, nil
443 | }
444 |
445 | // SetLabel sets the label on the given Torrent
446 | func (r *RTorrent) SetLabel(t Torrent, newLabel string) error {
447 | t.Label = newLabel
448 | args := []interface{}{t.Hash, newLabel}
449 | if _, err := r.xmlrpcClient.Call("d.custom1.set", args...); err != nil {
450 | return errors.Wrap(err, "d.custom1.set XMLRPC call failed")
451 | }
452 | return nil
453 | }
454 |
455 | // GetStatus returns the Status for a given Torrent
456 | func (r *RTorrent) GetStatus(t Torrent) (Status, error) {
457 | var s Status
458 | // Completed
459 | results, err := r.xmlrpcClient.Call("d.complete", t.Hash)
460 | if err != nil {
461 | return s, errors.Wrap(err, "d.complete XMLRPC call failed")
462 | }
463 | s.Completed = results.([]interface{})[0].(int) > 0
464 | // CompletedBytes
465 | results, err = r.xmlrpcClient.Call("d.completed_bytes", t.Hash)
466 | if err != nil {
467 | return s, errors.Wrap(err, "d.completed_bytes XMLRPC call failed")
468 | }
469 | s.CompletedBytes = results.([]interface{})[0].(int)
470 | // DownRate
471 | results, err = r.xmlrpcClient.Call("d.down.rate", t.Hash)
472 | if err != nil {
473 | return s, errors.Wrap(err, "d.down.rate XMLRPC call failed")
474 | }
475 | s.DownRate = results.([]interface{})[0].(int)
476 | // UpRate
477 | results, err = r.xmlrpcClient.Call("d.up.rate", t.Hash)
478 | if err != nil {
479 | return s, errors.Wrap(err, "d.up.rate XMLRPC call failed")
480 | }
481 | s.UpRate = results.([]interface{})[0].(int)
482 | // Ratio
483 | results, err = r.xmlrpcClient.Call("d.ratio", t.Hash)
484 | if err != nil {
485 | return s, errors.Wrap(err, "d.ratio XMLRPC call failed")
486 | }
487 | s.Ratio = float64(results.([]interface{})[0].(int)) / float64(1000)
488 | // Size
489 | results, err = r.xmlrpcClient.Call("d.size_bytes", t.Hash)
490 | if err != nil {
491 | return s, errors.Wrap(err, "d.size_bytes XMLRPC call failed")
492 | }
493 | s.Size = results.([]interface{})[0].(int)
494 | return s, nil
495 | }
496 |
497 | // StartTorrent starts the torrent
498 | func (r *RTorrent) StartTorrent(t Torrent) error {
499 | _, err := r.xmlrpcClient.Call("d.start", t.Hash)
500 | if err != nil {
501 | return errors.Wrap(err, "d.start XMLRPC call failed")
502 | }
503 | return nil
504 | }
505 |
506 | // StopTorrent stops the torrent
507 | func (r *RTorrent) StopTorrent(t Torrent) error {
508 | _, err := r.xmlrpcClient.Call("d.stop", t.Hash)
509 | if err != nil {
510 | return errors.Wrap(err, "d.stop XMLRPC call failed")
511 | }
512 | return nil
513 | }
514 |
515 | // CloseTorrent closes the torrent
516 | func (r *RTorrent) CloseTorrent(t Torrent) error {
517 | _, err := r.xmlrpcClient.Call("d.close", t.Hash)
518 | if err != nil {
519 | return errors.Wrap(err, "d.close XMLRPC call failed")
520 | }
521 | return nil
522 | }
523 |
524 | // OpenTorrent opens the torrent
525 | func (r *RTorrent) OpenTorrent(t Torrent) error {
526 | _, err := r.xmlrpcClient.Call("d.open", t.Hash)
527 | if err != nil {
528 | return errors.Wrap(err, "d.open XMLRPC call failed")
529 | }
530 | return nil
531 | }
532 |
533 | // PauseTorrent pauses the torrent
534 | func (r *RTorrent) PauseTorrent(t Torrent) error {
535 | _, err := r.xmlrpcClient.Call("d.pause", t.Hash)
536 | if err != nil {
537 | return errors.Wrap(err, "d.pause XMLRPC call failed")
538 | }
539 | return nil
540 | }
541 |
542 | // ResumeTorrent resumes the torrent
543 | func (r *RTorrent) ResumeTorrent(t Torrent) error {
544 | _, err := r.xmlrpcClient.Call("d.resume", t.Hash)
545 | if err != nil {
546 | return errors.Wrap(err, "d.resume XMLRPC call failed")
547 | }
548 | return nil
549 | }
550 |
551 | // IsActive checks if the torrent is active
552 | func (r *RTorrent) IsActive(t Torrent) (bool, error) {
553 | results, err := r.xmlrpcClient.Call("d.is_active", t.Hash)
554 | if err != nil {
555 | return false, errors.Wrap(err, "d.is_active XMLRPC call failed")
556 | }
557 | // active = 1; inactive = 0
558 | return results.([]interface{})[0].(int) == 1, nil
559 | }
560 |
561 | // IsOpen checks if the torrent is open
562 | func (r *RTorrent) IsOpen(t Torrent) (bool, error) {
563 | results, err := r.xmlrpcClient.Call("d.is_open", t.Hash)
564 | if err != nil {
565 | return false, errors.Wrap(err, "d.is_open XMLRPC call failed")
566 | }
567 | // open = 1; closed = 0
568 | return results.([]interface{})[0].(int) == 1, nil
569 | }
570 |
571 | // State returns the state that the torrent is into
572 | // It returns: 0 for stopped, 1 for started/paused
573 | func (r *RTorrent) State(t Torrent) (int, error) {
574 | results, err := r.xmlrpcClient.Call("d.state", t.Hash)
575 | if err != nil {
576 | return 0, errors.Wrap(err, "d.state XMLRPC call failed")
577 | }
578 | return results.([]interface{})[0].(int), nil
579 | }
580 |
--------------------------------------------------------------------------------