├── .gitignore
├── .idea
├── libraries
│ └── GOPATH__go_qbittorrent_.xml
└── workspace.xml
├── README.rst
├── docs.txt
├── go-qbittorrent.go
├── go.mod
├── go.sum
├── qbt
├── api.go
└── models.go
└── tools
└── tools.go
/.gitignore:
--------------------------------------------------------------------------------
1 | ./main.go
2 | ./.idea/*
3 |
--------------------------------------------------------------------------------
/.idea/libraries/GOPATH__go_qbittorrent_.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
--------------------------------------------------------------------------------
/.idea/workspace.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
58 |
59 |
60 |
61 | (c Client)
62 | params := map[string]string{}
63 | file
64 | printResp
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 |
78 |
79 |
80 |
81 |
82 |
83 |
84 |
85 |
86 |
87 |
88 |
89 |
90 |
91 |
92 |
93 |
94 | true
95 | DEFINITION_ORDER
96 |
97 |
98 |
99 |
100 |
101 |
102 |
103 |
104 |
105 |
106 |
107 |
108 |
109 |
110 |
111 |
112 |
113 |
114 |
115 |
116 |
117 |
118 |
119 |
120 |
121 |
122 |
123 |
124 |
125 |
126 |
127 |
128 |
129 |
130 |
131 |
132 |
133 |
134 |
135 |
136 |
137 |
138 |
139 |
140 |
141 |
142 |
143 |
144 |
145 |
146 |
147 |
148 |
149 |
150 |
151 |
152 |
153 |
154 |
155 |
156 |
157 |
158 |
159 |
160 |
161 |
162 |
163 |
164 |
165 |
166 |
167 |
168 |
169 |
170 |
171 |
172 |
173 |
174 |
175 |
176 |
177 |
178 |
179 |
180 |
181 |
182 |
183 |
184 |
185 |
186 |
187 |
188 |
189 |
190 |
191 |
192 |
193 |
194 |
195 |
196 |
197 | project
198 |
199 |
200 |
201 |
202 |
203 |
204 |
205 |
206 |
207 |
208 |
209 |
210 |
211 |
212 |
213 |
214 |
215 |
216 |
217 |
218 |
219 |
220 |
221 |
222 |
223 |
224 |
225 |
226 |
227 |
228 |
229 |
230 |
231 |
232 |
233 |
234 |
235 |
236 |
237 |
238 |
239 |
240 |
241 |
242 |
243 |
244 |
245 |
246 |
247 |
248 |
249 |
250 |
251 |
252 |
253 |
254 |
255 |
256 |
257 |
258 |
259 |
260 |
261 |
262 |
263 |
264 |
265 |
266 |
267 |
268 |
269 |
270 |
271 |
272 |
273 |
274 |
275 |
276 |
277 |
278 |
279 |
280 |
281 |
282 |
283 |
284 |
285 |
286 |
287 |
288 |
289 |
290 |
291 |
292 |
293 |
294 |
295 |
296 |
297 |
298 |
299 |
300 |
301 |
302 |
303 |
304 |
305 |
306 |
307 |
308 |
309 |
310 |
311 |
312 |
313 |
314 |
315 |
316 |
317 |
318 |
319 |
320 |
321 |
322 |
323 |
324 |
325 |
326 |
327 |
328 |
329 |
330 |
331 |
332 |
333 |
334 |
335 |
336 |
337 |
338 |
339 |
340 |
341 |
342 |
343 |
344 |
345 |
346 |
347 |
348 |
349 |
350 |
351 |
352 |
353 |
354 |
355 |
356 |
357 |
358 |
359 |
360 |
361 |
362 |
363 |
364 |
365 |
366 |
367 |
368 |
369 |
370 |
371 |
372 |
373 |
374 |
375 |
376 |
377 |
378 |
379 |
380 |
381 |
382 |
383 |
384 |
385 |
386 |
387 |
388 |
389 |
390 |
391 |
392 |
393 |
394 |
395 |
396 |
397 |
398 |
399 |
400 |
401 |
402 |
403 |
404 |
405 |
406 |
407 |
408 |
409 |
410 |
411 |
412 |
413 |
414 |
415 |
416 |
417 |
418 |
419 |
420 |
421 |
422 |
423 |
424 |
425 |
426 |
427 |
428 |
429 |
430 |
431 |
432 |
433 |
434 |
435 |
436 |
437 |
--------------------------------------------------------------------------------
/README.rst:
--------------------------------------------------------------------------------
1 | ==================
2 | go-qbittorrent
3 | ==================
4 |
5 | Golang wrapper for qBittorrent Web API (for versions above v5.0.x).
6 |
7 | This wrapper is based on the methods described in `qBittorrent's Official Web API Documentation `__
8 |
9 | This project is based on the Python wrapper by `v1k45 `__
10 |
11 | Some methods are only supported in qBittorent's latest version (v5.0.5 when writing).
12 |
13 | It'll be best if you upgrade your client to the latest version.
14 |
15 | An example can be found in main.go
16 |
17 | Installation
18 | ============
19 |
20 | The best way is to install with go get::
21 |
22 | $ go get github.com/superturkey650/go-qbittorrent/qbt
23 |
24 |
25 | Quick usage guide
26 | =================
27 | .. code-block:: go
28 |
29 | import (
30 | "superturkey650/go-qbittorrent/qbt
31 | )
32 |
33 | qb := qbt.NewClient("http://localhost:8080/")
34 |
35 | qb.Login("admin", "your-secret-password")
36 | // not required when 'Bypass from localhost' setting is active.
37 |
38 | torrents = qb.Torrents()
39 |
40 | for torrent := range torrents{
41 | fmt.Println(torrent.Name)
42 | }
43 |
44 | API methods
45 | ===========
46 |
47 | Getting torrents
48 | ----------------
49 |
50 | - Get all ``active`` torrents::
51 | .. code-block:: go
52 |
53 | qb.Torrents()
54 |
55 | - Filter torrents::
56 | .. code-block:: go
57 |
58 | filters := qbt.TorrentsOptions{
59 | Filter: ptrString("downloading"),
60 | Category: ptrString("my category"),
61 | }
62 | qb.Torrents(filters)
63 | // This will return all torrents which are currently
64 | // downloading and are labeled as `my category`.
65 |
66 | filters := qbt.TorrentsOptions{
67 | Filter: ptrString("paused"),
68 | Sort: ptrString("ratio"),
69 | }
70 | qb.Torrents(filters)
71 | // This will return all paused torrents sorted by their Leech:Seed ratio.
72 |
73 | Refer to qBittorents WEB API documentation for all possible filters.
74 |
75 | Downloading torrents
76 | --------------------
77 |
78 | - Download torrents by link::
79 | .. code-block:: go
80 |
81 | options := qbt.DownloadOptions{}
82 | magnetLinks = []string{"magnet:?xt=urn:btih:e334ab9ddd91c10938a7....."}
83 | qb.DownloadLinks(magnetLinks, options)
84 |
85 | - Downloading torrents by file::
86 | .. code-block:: go
87 |
88 | options := qbt.DownloadOptions{}
89 | files = []string{"path/to/file.torrent"}
90 | qb.DownloadFiles(files, options)
91 |
92 | - Downloading multiple torrents by using files::
93 | .. code-block:: go
94 |
95 | options := qbt.DownloadOptions{}
96 | files = []string{"path/to/file1", "path/to/file2", "path/to/file3"}
97 | qb.DownloadFiles(files, options)
98 |
99 | - Specifing save path for downloads::
100 | .. code-block:: go
101 |
102 | savePath = "/home/user/Downloads/special-dir/"
103 | options := qbt.DownloadOptions{
104 | Savepath: ptrString(savePath),
105 | }
106 | files = []string{"path/to/file.torrent"}
107 | qb.DownloadFiles(files, options)
108 |
109 | // same for links.
110 | savePath = "/home/user/Downloads/special-dir/"
111 | options := qbt.DownloadOptions{
112 | Savepath: ptrString(savePath),
113 | }
114 | magnetLinks = []string{"magnet:?xt=urn:btih:e334ab9ddd91c10938a7....."}
115 | qb.DownloadLinks(magnetLinks, options)
116 |
117 | - Applying labels to downloads::
118 | .. code-block:: go
119 |
120 | label = "secret-files"
121 | options := qbt.DownloadOptions{
122 | Label: ptrString(label),
123 | }
124 | files = []string{"path/to/file.torrent"}
125 | qb.DownloadFiles(files, options)
126 |
127 | // same for links.
128 | category = "anime"
129 | options := qbt.DownloadOptions{
130 | Category: ptrString(category),
131 | }
132 | magnetLinks = []string{"magnet:?xt=urn:btih:e334ab9ddd91c10938a7....."}
133 | qb.DownloadLinks(magnetLinks, options)
134 |
135 | Pause / Resume torrents
136 | -----------------------
137 |
138 | - Pausing/ Resuming multiple torrents::
139 | .. code-block:: go
140 |
141 | infoHashes = [...]string{
142 | "e334ab9ddd9......infohash......fff526cb4",
143 | "c9dc36f46d9......infohash......90ebebc46",
144 | "4c859243615......infohash......8b1f20108",
145 | }
146 |
147 | qb.Pause(infoHashes)
148 | qb.Resume(infoHashes)
149 |
150 |
151 | Maintainer
152 | ----------
153 |
154 | - `Jared Mosley (jaredlmosley) `__
155 |
156 | Contributors
157 | ------------
158 |
159 | - Your name here :)
160 |
161 | TODO
162 | ====
163 |
164 | - Write tests
165 | - Implement RSS Endpoints
166 | - Implement Search Endpoints
--------------------------------------------------------------------------------
/docs.txt:
--------------------------------------------------------------------------------
1 | PACKAGE DOCUMENTATION
2 |
3 | package qbt
4 | import "/Users/me/Repos/go/src/go-qbittorrent/qbt"
5 |
6 |
7 | TYPES
8 |
9 | type BasicTorrent struct {
10 | AddedOn int `json:"added_on"`
11 | Category string `json:"category"`
12 | CompletionOn int64 `json:"completion_on"`
13 | Dlspeed int `json:"dlspeed"`
14 | Eta int `json:"eta"`
15 | ForceStart bool `json:"force_start"`
16 | Hash string `json:"hash"`
17 | Name string `json:"name"`
18 | NumComplete int `json:"num_complete"`
19 | NumIncomplete int `json:"num_incomplete"`
20 | NumLeechs int `json:"num_leechs"`
21 | NumSeeds int `json:"num_seeds"`
22 | Priority int `json:"priority"`
23 | Progress int `json:"progress"`
24 | Ratio int `json:"ratio"`
25 | SavePath string `json:"save_path"`
26 | SeqDl bool `json:"seq_dl"`
27 | Size int `json:"size"`
28 | State string `json:"state"`
29 | SuperSeeding bool `json:"super_seeding"`
30 | Upspeed int `json:"upspeed"`
31 | }
32 | BasicTorrent holds a basic torrent object from qbittorrent
33 |
34 | type Client struct {
35 | URL string
36 | Authenticated bool
37 | Session string //replace with session type
38 | Jar http.CookieJar
39 | // contains filtered or unexported fields
40 | }
41 | Client creates a connection to qbittorrent and performs requests
42 |
43 | func NewClient(url string) *Client
44 | NewClient creates a new client connection to qbittorrent
45 |
46 | func (c *Client) AddTrackers(infoHash string, trackers string) (*http.Response, error)
47 | AddTrackers adds trackers to a specific torrent
48 |
49 | func (c *Client) DecreasePriority(infoHashList []string) (*http.Response, error)
50 | DecreasePriority decreases the priority of a list of torrents
51 |
52 | func (c *Client) DeletePermanently(infoHashList []string) (*http.Response, error)
53 | DeletePermanently deletes all files for a list of torrents
54 |
55 | func (c *Client) DeleteTemp(infoHashList []string) (*http.Response, error)
56 | DeleteTemp deletes the temporary files for a list of torrents
57 |
58 | func (c *Client) DownloadFromFile(file string, options map[string]string) (*http.Response, error)
59 | DownloadFromFile downloads a torrent from a file
60 |
61 | func (c *Client) DownloadFromLink(link string, options map[string]string) (*http.Response, error)
62 | DownloadFromLink starts downloading a torrent from a link
63 |
64 | func (c *Client) ForceStart(infoHashList []string, value bool) (*http.Response, error)
65 | ForceStart force starts a list of torrents
66 |
67 | func (c *Client) GetAlternativeSpeedStatus() (status bool, err error)
68 | GetAlternativeSpeedStatus gets the alternative speed status of your
69 | qbittorrent client
70 |
71 | func (c *Client) GetGlobalDownloadLimit() (limit int, err error)
72 | GetGlobalDownloadLimit gets the global download limit of your
73 | qbittorrent client
74 |
75 | func (c *Client) GetGlobalUploadLimit() (limit int, err error)
76 | GetGlobalUploadLimit gets the global upload limit of your qbittorrent
77 | client
78 |
79 | func (c *Client) GetTorrentDownloadLimit(infoHashList []string) (limits map[string]string, err error)
80 | GetTorrentDownloadLimit gets the download limit for a list of torrents
81 |
82 | func (c *Client) GetTorrentUploadLimit(infoHashList []string) (limits map[string]string, err error)
83 | GetTorrentUploadLimit gets the upload limit for a list of torrents
84 |
85 | func (c *Client) IncreasePriority(infoHashList []string) (*http.Response, error)
86 | IncreasePriority increases the priority of a list of torrents
87 |
88 | func (c *Client) Login(username string, password string) (loggedIn bool, err error)
89 | Login logs you in to the qbittorrent client
90 |
91 | func (c *Client) Logout() (loggedOut bool, err error)
92 | Logout logs you out of the qbittorrent client
93 |
94 | func (c *Client) Pause(infoHash string) (*http.Response, error)
95 | Pause pauses a specific torrent
96 |
97 | func (c *Client) PauseAll() (*http.Response, error)
98 | PauseAll pauses all torrents
99 |
100 | func (c *Client) PauseMultiple(infoHashList []string) (*http.Response, error)
101 | PauseMultiple pauses a list of torrents
102 |
103 | func (c *Client) Recheck(infoHashList []string) (*http.Response, error)
104 | Recheck rechecks a list of torrents
105 |
106 | func (c *Client) Resume(infoHash string) (*http.Response, error)
107 | Resume resumes a specific torrent
108 |
109 | func (c *Client) ResumeAll(infoHashList []string) (*http.Response, error)
110 | ResumeAll resumes all torrents
111 |
112 | func (c *Client) ResumeMultiple(infoHashList []string) (*http.Response, error)
113 | ResumeMultiple resumes a list of torrents
114 |
115 | func (c *Client) SetCategory(infoHashList []string, category string) (*http.Response, error)
116 | SetCategory sets the category for a list of torrents
117 |
118 | func (c *Client) SetFilePriority(infoHash string, fileID string, priority string) (*http.Response, error)
119 | SetFilePriority sets the priority for a specific torrent file
120 |
121 | func (c *Client) SetGlobalDownloadLimit(limit string) (*http.Response, error)
122 | SetGlobalDownloadLimit sets the global download limit of your
123 | qbittorrent client
124 |
125 | func (c *Client) SetGlobalUploadLimit(limit string) (*http.Response, error)
126 | SetGlobalUploadLimit sets the global upload limit of your qbittorrent
127 | client
128 |
129 | func (c *Client) SetLabel(infoHashList []string, label string) (*http.Response, error)
130 | SetLabel sets the labels for a list of torrents
131 |
132 | func (c *Client) SetMaxPriority(infoHashList []string) (*http.Response, error)
133 | SetMaxPriority sets the max priority for a list of torrents
134 |
135 | func (c *Client) SetMinPriority(infoHashList []string) (*http.Response, error)
136 | SetMinPriority sets the min priority for a list of torrents
137 |
138 | func (c *Client) SetPreferences(params map[string]string) (*http.Response, error)
139 | SetPreferences sets the preferences of your qbittorrent client
140 |
141 | func (c *Client) SetTorrentDownloadLimit(infoHashList []string, limit string) (*http.Response, error)
142 | SetTorrentDownloadLimit sets the download limit for a list of torrents
143 |
144 | func (c *Client) SetTorrentUploadLimit(infoHashList []string, limit string) (*http.Response, error)
145 | SetTorrentUploadLimit sets the upload limit of a list of torrents
146 |
147 | func (c *Client) Shutdown() (shuttingDown bool, err error)
148 | Shutdown shuts down the qbittorrent client
149 |
150 | func (c *Client) Sync(rid string) (Sync, error)
151 | Sync syncs main data of qbittorrent
152 |
153 | func (c *Client) ToggleAlternativeSpeed() (*http.Response, error)
154 | ToggleAlternativeSpeed toggles the alternative speed of your qbittorrent
155 | client
156 |
157 | func (c *Client) ToggleFirstLastPiecePriority(infoHashList []string) (*http.Response, error)
158 | ToggleFirstLastPiecePriority toggles first last piece priority of a list
159 | of torrents
160 |
161 | func (c *Client) ToggleSequentialDownload(infoHashList []string) (*http.Response, error)
162 | ToggleSequentialDownload toggles the download sequence of a list of
163 | torrents
164 |
165 | func (c *Client) Torrent(infoHash string) (Torrent, error)
166 | Torrent gets a specific torrent
167 |
168 | func (c *Client) TorrentFiles(infoHash string) ([]TorrentFile, error)
169 | TorrentFiles gets the files of a specifc torrent
170 |
171 | func (c *Client) TorrentTrackers(infoHash string) ([]Tracker, error)
172 | TorrentTrackers gets all trackers for a specific torrent
173 |
174 | func (c *Client) TorrentWebSeeds(infoHash string) ([]WebSeed, error)
175 | TorrentWebSeeds gets seeders for a specific torrent
176 |
177 | func (c *Client) Torrents(filters map[string]string) (torrentList []BasicTorrent, err error)
178 | Torrents gets a list of all torrents in qbittorrent matching your filter
179 |
180 | type Sync struct {
181 | Categories []string `json:"categories"`
182 | FullUpdate bool `json:"full_update"`
183 | Rid int `json:"rid"`
184 | ServerState struct {
185 | ConnectionStatus string `json:"connection_status"`
186 | DhtNodes int `json:"dht_nodes"`
187 | DlInfoData int `json:"dl_info_data"`
188 | DlInfoSpeed int `json:"dl_info_speed"`
189 | DlRateLimit int `json:"dl_rate_limit"`
190 | Queueing bool `json:"queueing"`
191 | RefreshInterval int `json:"refresh_interval"`
192 | UpInfoData int `json:"up_info_data"`
193 | UpInfoSpeed int `json:"up_info_speed"`
194 | UpRateLimit int `json:"up_rate_limit"`
195 | UseAltSpeedLimits bool `json:"use_alt_speed_limits"`
196 | } `json:"server_state"`
197 | Torrents map[string]Torrent `json:"torrents"`
198 | }
199 | Sync holds the sync response struct
200 |
201 | type Torrent struct {
202 | AdditionDate int `json:"addition_date"`
203 | Comment string `json:"comment"`
204 | CompletionDate int `json:"completion_date"`
205 | CreatedBy string `json:"created_by"`
206 | CreationDate int `json:"creation_date"`
207 | DlLimit int `json:"dl_limit"`
208 | DlSpeed int `json:"dl_speed"`
209 | DlSpeedAvg int `json:"dl_speed_avg"`
210 | Eta int `json:"eta"`
211 | LastSeen int `json:"last_seen"`
212 | NbConnections int `json:"nb_connections"`
213 | NbConnectionsLimit int `json:"nb_connections_limit"`
214 | Peers int `json:"peers"`
215 | PeersTotal int `json:"peers_total"`
216 | PieceSize int `json:"piece_size"`
217 | PiecesHave int `json:"pieces_have"`
218 | PiecesNum int `json:"pieces_num"`
219 | Reannounce int `json:"reannounce"`
220 | SavePath string `json:"save_path"`
221 | SeedingTime int `json:"seeding_time"`
222 | Seeds int `json:"seeds"`
223 | SeedsTotal int `json:"seeds_total"`
224 | ShareRatio float64 `json:"share_ratio"`
225 | TimeElapsed int `json:"time_elapsed"`
226 | TotalDownloaded int `json:"total_downloaded"`
227 | TotalDownloadedSession int `json:"total_downloaded_session"`
228 | TotalSize int `json:"total_size"`
229 | TotalUploaded int `json:"total_uploaded"`
230 | TotalUploadedSession int `json:"total_uploaded_session"`
231 | TotalWasted int `json:"total_wasted"`
232 | UpLimit int `json:"up_limit"`
233 | UpSpeed int `json:"up_speed"`
234 | UpSpeedAvg int `json:"up_speed_avg"`
235 | }
236 | Torrent holds a torrent object from qbittorrent
237 |
238 | type TorrentFile struct {
239 | IsSeed bool `json:"is_seed"`
240 | Name string `json:"name"`
241 | Priority int `json:"priority"`
242 | Progress int `json:"progress"`
243 | Size int `json:"size"`
244 | }
245 | TorrentFile holds a torrent file object from qbittorrent
246 |
247 | type Tracker struct {
248 | Msg string `json:"msg"`
249 | NumPeers int `json:"num_peers"`
250 | Status string `json:"status"`
251 | URL string `json:"url"`
252 | }
253 | Tracker holds a tracker object from qbittorrent
254 |
255 | type WebSeed struct {
256 | URL string `json:"url"`
257 | }
258 | WebSeed holds a webseed object from qbittorrent
259 |
260 |
261 |
--------------------------------------------------------------------------------
/go-qbittorrent.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "fmt"
5 |
6 | "github.com/davecgh/go-spew/spew"
7 | "github.com/superturkey650/go-qbittorrent/qbt"
8 | )
9 |
10 | func ptrString(s string) *string {
11 | return &s
12 | }
13 |
14 | func main() {
15 | // set up a test hash to pause and resume
16 | testHashes := []string{"7cdd7029da5aeaf2589375c32bfc0a065a8ae3a7"}
17 |
18 | // connect to qbittorrent client
19 | qb := qbt.NewClient("http://localhost:8080")
20 |
21 | // Application Endpoints
22 |
23 | // login to the client
24 | err := qb.Login("username", "password")
25 | if err != nil {
26 | fmt.Println(err)
27 | }
28 |
29 | appVersion, err := qb.ApplicationVersion()
30 | if err != nil {
31 | fmt.Println("[-] Application Version")
32 | fmt.Println(err)
33 | } else {
34 | fmt.Println("[+] Application Version: ", appVersion)
35 | }
36 |
37 | webAPIVersion, err := qb.WebAPIVersion()
38 | if err != nil {
39 | fmt.Println("[-] Web API Version")
40 | fmt.Println(err)
41 | } else {
42 | fmt.Println("[+] Web API Version: ", webAPIVersion)
43 | }
44 |
45 | buildInfo, err := qb.BuildInfo()
46 | if err != nil {
47 | fmt.Println("[-] Build Info")
48 | fmt.Println(err)
49 | } else {
50 | fmt.Println("[+] Build Info: ", buildInfo)
51 | }
52 |
53 | preferences, err := qb.Preferences()
54 | if err != nil {
55 | fmt.Println("[-] Preferences")
56 | fmt.Println(err)
57 | } else {
58 | fmt.Println("[+] Preferences: ", preferences)
59 | }
60 |
61 | // TODO: Preferences
62 |
63 | defaultSavePath, err := qb.DefaultSavePath()
64 | if err != nil {
65 | fmt.Println("[-] Default Save Path")
66 | fmt.Println(err)
67 | } else {
68 | fmt.Println("[+] Default Save Path: ", defaultSavePath)
69 | }
70 |
71 | // LOG ENDPOINTS
72 |
73 | logs, err := qb.Logs(nil)
74 | if err != nil {
75 | fmt.Println("[-] Logs")
76 | fmt.Println(err)
77 | } else {
78 | fmt.Println("[+] Logs: ", logs[34])
79 | }
80 |
81 | peerLogs, err := qb.PeerLogs(nil)
82 | if err != nil {
83 | fmt.Println("[-] Peer Logs")
84 | fmt.Println(err)
85 | } else {
86 | fmt.Println("[+] Peer Logs: ", peerLogs)
87 | }
88 |
89 | // SYNC ENDPOINTS
90 |
91 | mainData, err := qb.MainData("")
92 | if err != nil {
93 | fmt.Println("[-] Main Data")
94 | fmt.Println(err)
95 | } else {
96 | fmt.Println("[+] Main Data: ", mainData)
97 | }
98 |
99 | torrentPeers, err := qb.TorrentPeers(testHashes[0], "")
100 | if err != nil {
101 | fmt.Println("[-] Torrent Peers")
102 | fmt.Println(err)
103 | } else {
104 | fmt.Println("[+] Torrent Peers: ", torrentPeers)
105 | }
106 |
107 | // TRANSFER ENDPOINTS
108 |
109 | info, err := qb.Info()
110 | if err != nil {
111 | fmt.Println("[-] Info")
112 | fmt.Println(err)
113 | } else {
114 | fmt.Println("[+] Info: ", info)
115 | }
116 |
117 | altSpeedLimitsEnabled, err := qb.AltSpeedLimitsEnabled()
118 | if err != nil {
119 | fmt.Println("[-] Alt Speed Limits Enabled")
120 | fmt.Println(err)
121 | } else {
122 | fmt.Println("[+] Alt Speed Limits Enabled: ", altSpeedLimitsEnabled)
123 | }
124 |
125 | // err = qb.ToggleAltSpeedLimits()
126 | // if err != nil {
127 | // fmt.Println("[-] Toggled Alt Speed Limits")
128 | // fmt.Println(err)
129 | // } else {
130 | // fmt.Println("[+] Toggled Alt Speed Limits")
131 | // }
132 |
133 | dlLimit, err := qb.DlLimit()
134 | if err != nil {
135 | fmt.Println("[-] Download Limit")
136 | fmt.Println(err)
137 | } else {
138 | fmt.Println("[+] Download Limit: ", dlLimit)
139 | }
140 |
141 | err = qb.SetDlLimit(0)
142 | if err != nil {
143 | fmt.Println("[-] Set Download Limit")
144 | fmt.Println(err)
145 | } else {
146 | fmt.Println("[+] Set Download Limit")
147 | }
148 |
149 | ulLimit, err := qb.UlLimit()
150 | if err != nil {
151 | fmt.Println("[-] Upload Limit")
152 | fmt.Println(err)
153 | } else {
154 | fmt.Println("[+] Upload Limit: ", ulLimit)
155 | }
156 |
157 | err = qb.SetUlLimit(0)
158 | if err != nil {
159 | fmt.Println("[-] Set Upload Limit")
160 | fmt.Println(err)
161 | } else {
162 | fmt.Println("[+] Set Upload Limit")
163 | }
164 |
165 | // TODO: Ban Peers
166 |
167 | // Torrent Endpoints
168 |
169 | // ******************
170 | // GET ALL TORRENTS *
171 | // ******************
172 |
173 | //filter := "inactive"
174 | //hash := "d739f78a12b241ba62719b1064701ffbb45498a8"
175 | //torrentsOpts.Filter = &filter
176 | //torrentsOpts.Hashes = []string{hash}
177 | torrentsOpts := qbt.TorrentsOptions{
178 | Sort: ptrString("name"),
179 | }
180 | torrents, err := qb.Torrents(torrentsOpts)
181 | if err != nil {
182 | fmt.Println("[-] Get torrent list")
183 | fmt.Println(err)
184 | } else {
185 | fmt.Println("[+] Get torrent list")
186 | if len(torrents) > 0 {
187 | spew.Dump(torrents[0])
188 | } else {
189 | fmt.Println("No torrents found")
190 | }
191 | }
192 |
193 | torrent, err := qb.Torrent(testHashes[0])
194 | if err != nil {
195 | fmt.Println("[-] Torrent")
196 | fmt.Println(err)
197 | } else {
198 | fmt.Println("[+] Torrent: ", torrent)
199 | }
200 |
201 | torrentTrackers, err := qb.TorrentTrackers(testHashes[0])
202 | if err != nil {
203 | fmt.Println("[-] Torrent Trackers")
204 | fmt.Println(err)
205 | } else {
206 | fmt.Println("[+] Torrent Trackers: ", torrentTrackers)
207 | }
208 |
209 | torrentWebSeeds, err := qb.TorrentWebSeeds(testHashes[0])
210 | if err != nil {
211 | fmt.Println("[-] Torrent Web Seeds")
212 | fmt.Println(err)
213 | } else {
214 | fmt.Println("[+] Torrent Web Seeds: ", torrentWebSeeds)
215 | }
216 |
217 | torrentFiles, err := qb.TorrentFiles(testHashes[0])
218 | if err != nil {
219 | fmt.Println("[-] Torrent Files")
220 | fmt.Println(err)
221 | } else {
222 | fmt.Println("[+] Torrent Files: ", torrentFiles)
223 | }
224 |
225 | torrentPieceStates, err := qb.TorrentPieceStates(testHashes[0])
226 | if err != nil {
227 | fmt.Println("[-] Torrent Piece States")
228 | fmt.Println(err)
229 | } else {
230 | fmt.Println("[+] Torrent Piece States: ", torrentPieceStates)
231 | }
232 |
233 | torrentPieceHashes, err := qb.TorrentPieceHashes(testHashes[0])
234 | if err != nil {
235 | fmt.Println("[-] Torrent Piece Hashes")
236 | fmt.Println(err)
237 | } else {
238 | fmt.Println("[+] Torrent Piece Hashes: ", torrentPieceHashes)
239 | }
240 |
241 | err = qb.Pause(testHashes)
242 | if err != nil {
243 | fmt.Println("[-] Pause torrent")
244 | fmt.Println(err)
245 | } else {
246 | fmt.Println("[+] Pause torrent")
247 | }
248 |
249 | err = qb.Resume(testHashes)
250 | if err != nil {
251 | fmt.Println("[-] Resume torrent")
252 | fmt.Println(err)
253 | } else {
254 | fmt.Println("[+] Resume torrent")
255 | }
256 |
257 | // err = qb.Delete(testHashes, true)
258 | // if err != nil {
259 | // fmt.Println("[-] Delete torrent")
260 | // fmt.Println(err)
261 | // } else {
262 | // fmt.Println("[+] Delete torrent")
263 | // }
264 |
265 | err = qb.Recheck(testHashes)
266 | if err != nil {
267 | fmt.Println("[-] Recheck torrent")
268 | fmt.Println(err)
269 | } else {
270 | fmt.Println("[+] Recheck torrent")
271 | }
272 |
273 | err = qb.Reannounce(testHashes)
274 | if err != nil {
275 | fmt.Println("[-] Reannounce torrent")
276 | fmt.Println(err)
277 | } else {
278 | fmt.Println("[+] Reannounce torrent")
279 | }
280 |
281 | // ********************
282 | // DOWNLOAD A TORRENT *
283 | // ********************
284 |
285 | // were not using any filters so the options map is empty
286 | downloadOpts := qbt.DownloadOptions{}
287 | // set the path to the file
288 | //path := "/Users/me/Downloads/Source.Code.2011.1080p.BluRay.H264.AAC-RARBG-[rarbg.to].torrent"
289 | links := []string{"magnet:?xt=urn:btih:7CDD7029DA5AEAF2589375C32BFC0A065A8AE3A7&dn=Courage%20The%20Cowardly%20Dog%20S01E08%20480p%20x264-mSD&tr=udp%3A%2F%2Ftracker.opentrackr.org%3A1337&tr=udp%3A%2F%2Fopen.stealth.si%3A80%2Fannounce&tr=udp%3A%2F%2Ftracker.torrent.eu.org%3A451%2Fannounce&tr=udp%3A%2F%2Ftracker.bittor.pw%3A1337%2Fannounce&tr=udp%3A%2F%2Fpublic.popcorn-tracker.org%3A6969%2Fannounce&tr=udp%3A%2F%2Ftracker.dler.org%3A6969%2Fannounce&tr=udp%3A%2F%2Fexodus.desync.com%3A6969&tr=udp%3A%2F%2Fopen.demonii.com%3A1337%2Fannounce"}
290 | // download the torrent using the file
291 | // the wrapper will handle opening and closing the file for you
292 | err = qb.DownloadLinks(links, downloadOpts)
293 |
294 | if err != nil {
295 | fmt.Println("[-] Download torrent from link")
296 | fmt.Println(err)
297 | } else {
298 | fmt.Println("[+] Download torrent from link")
299 | }
300 |
301 | // TODO: File-based download
302 |
303 | // TODO: Tracker actions
304 |
305 | err = qb.IncreasePriority(testHashes)
306 | if err != nil {
307 | fmt.Println("[-] Increase Priority")
308 | fmt.Println(err)
309 | } else {
310 | fmt.Println("[+] Increase Priority")
311 | }
312 |
313 | err = qb.DecreasePriority(testHashes)
314 | if err != nil {
315 | fmt.Println("[-] Decrease Priority")
316 | fmt.Println(err)
317 | } else {
318 | fmt.Println("[+] Decrease Priority")
319 | }
320 |
321 | err = qb.MaxPriority(testHashes)
322 | if err != nil {
323 | fmt.Println("[-] Max Priority")
324 | fmt.Println(err)
325 | } else {
326 | fmt.Println("[+] Max Priority")
327 | }
328 |
329 | err = qb.MinPriority(testHashes)
330 | if err != nil {
331 | fmt.Println("[-] Min Priority")
332 | fmt.Println(err)
333 | } else {
334 | fmt.Println("[+] Min Priority")
335 | }
336 |
337 | // TODO: File Priority
338 |
339 | dlLimits, err := qb.GetTorrentDownloadLimit(testHashes)
340 | if err != nil {
341 | fmt.Println("[-] Download Limits")
342 | fmt.Println(err)
343 | } else {
344 | fmt.Println("[+] Download Limits: ", dlLimits)
345 | }
346 |
347 | err = qb.SetTorrentDownloadLimit(testHashes, dlLimits[testHashes[0]])
348 | if err != nil {
349 | fmt.Println("[-] Download Limits")
350 | fmt.Println(err)
351 | } else {
352 | fmt.Println("[+] Download Limits")
353 | }
354 |
355 | err = qb.SetTorrentShareLimit(testHashes, 1, -2, -2)
356 | if err != nil {
357 | fmt.Println("[-] Torrent Share Limits")
358 | fmt.Println(err)
359 | } else {
360 | fmt.Println("[+] Torrent Share Limits")
361 | }
362 |
363 | ulLimits, err := qb.GetTorrentUploadLimit(testHashes)
364 | if err != nil {
365 | fmt.Println("[-] Upload Limits")
366 | fmt.Println(err)
367 | } else {
368 | fmt.Println("[+] Upload Limits: ", ulLimits)
369 | }
370 |
371 | err = qb.SetTorrentUploadLimit(testHashes, ulLimits[testHashes[0]])
372 | if err != nil {
373 | fmt.Println("[-] Upload Limits")
374 | fmt.Println(err)
375 | } else {
376 | fmt.Println("[+] Upload Limits")
377 | }
378 |
379 | err = qb.SetTorrentLocation(testHashes, "/Users/jared/Downloads")
380 | if err != nil {
381 | fmt.Println("[-] Torrent Location")
382 | fmt.Println(err)
383 | } else {
384 | fmt.Println("[+] Torrent Location")
385 | }
386 |
387 | err = qb.SetTorrentName(testHashes[0], "test_name")
388 | if err != nil {
389 | fmt.Println("[-] Torrent Name")
390 | fmt.Println(err)
391 | } else {
392 | fmt.Println("[+] Torrent Name")
393 | }
394 |
395 | err = qb.SetTorrentCategory(testHashes, "")
396 | if err != nil {
397 | fmt.Println("[-] Torrent Name")
398 | fmt.Println(err)
399 | } else {
400 | fmt.Println("[+] Torrent Name")
401 | }
402 |
403 | categories, err := qb.GetCategories()
404 | if err != nil {
405 | fmt.Println("[-] Categories")
406 | fmt.Println(err)
407 | } else {
408 | fmt.Println("[+] Categories: ", categories)
409 | }
410 |
411 | err = qb.CreateCategory("test_category", "/Users/jared/Downloads")
412 | if err != nil {
413 | fmt.Println("[-] Create Category")
414 | fmt.Println(err)
415 | } else {
416 | fmt.Println("[+] Create Category")
417 | }
418 |
419 | err = qb.UpdateCategory("test_category", "/Users/jared/Downloads")
420 | if err != nil {
421 | fmt.Println("[-] Update Category")
422 | fmt.Println(err)
423 | } else {
424 | fmt.Println("[+] Update Category")
425 | }
426 |
427 | categories, err = qb.GetCategories()
428 | if err != nil {
429 | fmt.Println("[-] Categories")
430 | fmt.Println(err)
431 | } else {
432 | fmt.Println("[+] Categories: ", categories)
433 | }
434 |
435 | testCategories := []string{"test_category"}
436 | err = qb.DeleteCategories(testCategories)
437 | if err != nil {
438 | fmt.Println("[-] Delete Category")
439 | fmt.Println(err)
440 | } else {
441 | fmt.Println("[+] Delete Category")
442 | }
443 |
444 | categories, err = qb.GetCategories()
445 | if err != nil {
446 | fmt.Println("[-] Categories")
447 | fmt.Println(err)
448 | } else {
449 | fmt.Println("[+] Categories: ", categories)
450 | }
451 |
452 | // TODO: Tags
453 | }
454 |
--------------------------------------------------------------------------------
/go.mod:
--------------------------------------------------------------------------------
1 | module github.com/superturkey650/go-qbittorrent
2 |
3 | go 1.14
4 |
5 | require (
6 | github.com/davecgh/go-spew v1.1.1
7 | golang.org/x/net v0.38.0
8 | )
9 |
--------------------------------------------------------------------------------
/go.sum:
--------------------------------------------------------------------------------
1 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
2 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
3 | github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
4 | github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
5 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
6 | golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
7 | golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc=
8 | golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
9 | golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
10 | golang.org/x/crypto v0.36.0/go.mod h1:Y4J0ReaxCR1IMaabaSMugxJES1EpwhBHhv2bDHklZvc=
11 | golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
12 | golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
13 | golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
14 | golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
15 | golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
16 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
17 | golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
18 | golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
19 | golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
20 | golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
21 | golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk=
22 | golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
23 | golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
24 | golang.org/x/net v0.38.0 h1:vRMAPTMaeGqVhG5QyLJHqNDwecKTomGeqbnfZyKlBI8=
25 | golang.org/x/net v0.38.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8=
26 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
27 | golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
28 | golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
29 | golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
30 | golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
31 | golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
32 | golang.org/x/sync v0.12.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
33 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
34 | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
35 | golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
36 | golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
37 | golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
38 | golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
39 | golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
40 | golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
41 | golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
42 | golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
43 | golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
44 | golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE=
45 | golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
46 | golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
47 | golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
48 | golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
49 | golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU=
50 | golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk=
51 | golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY=
52 | golang.org/x/term v0.30.0/go.mod h1:NYYFdzHoI5wRh/h5tDMdMqCqPJZEuNqVR5xJLd/n67g=
53 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
54 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
55 | golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
56 | golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
57 | golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
58 | golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
59 | golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
60 | golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
61 | golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4=
62 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
63 | golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
64 | golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
65 | golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
66 | golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58=
67 | golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk=
68 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
69 |
--------------------------------------------------------------------------------
/qbt/api.go:
--------------------------------------------------------------------------------
1 | package qbt
2 |
3 | import (
4 | "bytes"
5 | "context"
6 | "encoding/json"
7 | "fmt"
8 | "io"
9 | "mime/multipart"
10 | "net/http"
11 | "net/http/cookiejar"
12 | "os"
13 | "path"
14 |
15 | "net/url"
16 | "strconv"
17 | "strings"
18 |
19 | "golang.org/x/net/publicsuffix"
20 | )
21 |
22 | const (
23 | apiBase = "api/v2/"
24 | )
25 |
26 | // delimit puts list into a combined (single element) map with all items connected separated by the delimiter
27 | // this is how the WEBUI API recognizes multiple items
28 | func delimit(items []string, delimiter string) (delimited string) {
29 | for i, v := range items {
30 | if i > 0 {
31 | delimited += delimiter + v
32 | } else {
33 | delimited = v
34 | }
35 | }
36 | return delimited
37 | }
38 |
39 | // Client creates a connection to qbittorrent and performs requests
40 | type Client struct {
41 | http *http.Client
42 | URL string
43 | Authenticated bool
44 | Jar http.CookieJar
45 | }
46 |
47 | // NewClient creates a new client connection to qbittorrent
48 | func NewClient(url string) *Client {
49 | c := &Client{}
50 |
51 | // ensure url ends with "/"
52 | if !strings.HasSuffix(url, "/") {
53 | url += "/"
54 | }
55 |
56 | c.URL = url
57 |
58 | // create cookie jar
59 | c.Jar, _ = cookiejar.New(&cookiejar.Options{PublicSuffixList: publicsuffix.List})
60 | c.http = &http.Client{
61 | Jar: c.Jar,
62 | }
63 | return c
64 | }
65 |
66 | // get will perform a GET request with no parameters
67 | func (c *Client) get(endpoint string, opts map[string]string) (*http.Response, error) {
68 | req, err := http.NewRequest("GET", c.URL+endpoint, nil)
69 | if err != nil {
70 | return nil, fmt.Errorf("failed to build request: %w", err)
71 | }
72 |
73 | // add user-agent header to allow qbittorrent to identify us
74 | req.Header.Set("User-Agent", "go-qbittorrent v0.1")
75 |
76 | // add optional parameters that the user wants
77 | if opts != nil {
78 | query := req.URL.Query()
79 | for k, v := range opts {
80 | query.Add(k, v)
81 | }
82 | req.URL.RawQuery = query.Encode()
83 | }
84 |
85 | resp, err := c.http.Do(req)
86 | if err != nil {
87 | return nil, fmt.Errorf("failed to perform request: %w", err)
88 | }
89 |
90 | return resp, nil
91 | }
92 |
93 | // post will perform a POST request with no content-type specified
94 | func (c *Client) post(endpoint string, opts map[string]string) (*http.Response, error) {
95 |
96 | // add optional parameters that the user wants
97 | form := url.Values{}
98 | for k, v := range opts {
99 | form.Set(k, v)
100 | }
101 |
102 | req, err := http.NewRequestWithContext(
103 | context.Background(),
104 | http.MethodPost,
105 | c.URL+endpoint,
106 | strings.NewReader(form.Encode()),
107 | )
108 | if err != nil {
109 | return nil, fmt.Errorf("failed to build request: %w", err)
110 | }
111 |
112 | // add the content-type so qbittorrent knows what to expect
113 | req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
114 | // add user-agent header to allow qbittorrent to identify us
115 | req.Header.Set("User-Agent", "go-qbittorrent v0.2")
116 | // add referer header to allow qbittorrent to identify us
117 | req.Header.Set("Referer", c.URL)
118 |
119 | resp, err := c.http.Do(req)
120 | if err != nil {
121 | return nil, fmt.Errorf("failed to perform request: %w", err)
122 | }
123 |
124 | return resp, nil
125 |
126 | }
127 |
128 | // postMultipart will perform a multiple part POST request
129 | func (c *Client) postMultipart(endpoint string, buffer bytes.Buffer, contentType string) (resp *http.Response, err error) {
130 | req, err := http.NewRequest("POST", c.URL+endpoint, &buffer)
131 | if err != nil {
132 | return nil, fmt.Errorf("error creating request: %w", err)
133 | }
134 |
135 | // add the content-type so qbittorrent knows what to expect
136 | req.Header.Set("Content-Type", contentType)
137 | // add user-agent header to allow qbittorrent to identify us
138 | req.Header.Set("User-Agent", "go-qbittorrent v0.2")
139 |
140 | resp, err = c.http.Do(req)
141 | if err != nil {
142 | return nil, fmt.Errorf("failed to perform request: %w", err)
143 | }
144 |
145 | return resp, nil
146 | }
147 |
148 | // writeOptions will write a map to the buffer through multipart.NewWriter
149 | func writeOptions(writer *multipart.Writer, opts map[string]string) (err error) {
150 | for key, val := range opts {
151 | if err := writer.WriteField(key, val); err != nil {
152 | return err
153 | }
154 | }
155 | return nil
156 | }
157 |
158 | // postMultipartData will perform a multiple part POST request without a file
159 | func (c *Client) postMultipartData(endpoint string, opts map[string]string) (*http.Response, error) {
160 | var buffer bytes.Buffer
161 | writer := multipart.NewWriter(&buffer)
162 |
163 | // write the options to the buffer
164 | // will contain the link string
165 | if err := writeOptions(writer, opts); err != nil {
166 | return nil, fmt.Errorf("failed to write options: %w", err)
167 | }
168 |
169 | // close the writer before doing request to get closing line on multipart request
170 | if err := writer.Close(); err != nil {
171 | return nil, fmt.Errorf("failed to close writer: %w", err)
172 | }
173 |
174 | resp, err := c.postMultipart(endpoint, buffer, writer.FormDataContentType())
175 | if err != nil {
176 | return nil, err
177 | }
178 |
179 | return resp, nil
180 | }
181 |
182 | // postMultipartFile will perform a multiple part POST request with a file
183 | func (c *Client) postMultipartFile(endpoint string, fileName string, opts map[string]string) (*http.Response, error) {
184 | var buffer bytes.Buffer
185 | writer := multipart.NewWriter(&buffer)
186 |
187 | // open the file for reading
188 | file, err := os.Open(fileName)
189 | if err != nil {
190 | return nil, fmt.Errorf("error opening file: %w", err)
191 | }
192 | // defer the closing of the file until the end of function
193 | // so we can still copy its contents
194 | defer file.Close()
195 |
196 | // create form for writing the file to and give it the filename
197 | formWriter, err := writer.CreateFormFile("torrents", path.Base(fileName))
198 | if err != nil {
199 | return nil, fmt.Errorf("error adding file: %w", err)
200 | }
201 |
202 | // write the options to the buffer
203 | writeOptions(writer, opts)
204 |
205 | // copy the file contents into the form
206 | if _, err = io.Copy(formWriter, file); err != nil {
207 | return nil, fmt.Errorf("error copying file: %w", err)
208 | }
209 |
210 | // close the writer before doing request to get closing line on multipart request
211 | if err := writer.Close(); err != nil {
212 | return nil, fmt.Errorf("failed to close writer: %w", err)
213 | }
214 |
215 | resp, err := c.postMultipart(endpoint, buffer, writer.FormDataContentType())
216 | if err != nil {
217 | return nil, err
218 | }
219 |
220 | return resp, nil
221 | }
222 |
223 | // Application endpoints
224 |
225 | // Login authenticates with the qBittorrent client using the provided credentials.
226 | // It returns an error if authentication fails or if the client's IP is banned.
227 | func (c *Client) Login(username string, password string) (err error) {
228 | params := map[string]string{"username": username, "password": password}
229 |
230 | if c.http == nil {
231 | c.http = &http.Client{Jar: c.Jar}
232 | }
233 |
234 | resp, err := c.post(apiBase+"auth/login", params)
235 | if err != nil {
236 | return err
237 | } else if resp.StatusCode == http.StatusForbidden {
238 | return fmt.Errorf("user's IP is banned for too many failed login attempts: %w", err)
239 | }
240 |
241 | // change authentication status so we know were authenticated in later requests
242 | c.Authenticated = true
243 |
244 | return nil
245 | }
246 |
247 | // Logout logs you out of the qbittorrent client
248 | // returns the current authentication status
249 | func (c *Client) Logout() (err error) {
250 | resp, err := c.get(apiBase+"auth/logout", nil)
251 | if err != nil {
252 | return err
253 | }
254 |
255 | // change authentication status so we know were not authenticated in later requests
256 | c.Authenticated = (*resp).StatusCode == 200
257 |
258 | return nil
259 | }
260 |
261 | // ApplicationVersion of the qbittorrent client
262 | func (c *Client) ApplicationVersion() (version string, err error) {
263 | resp, err := c.get(apiBase+"app/version", nil)
264 | if err != nil {
265 | return version, err
266 | }
267 | body, err := io.ReadAll(resp.Body)
268 | if err != nil {
269 | return version, fmt.Errorf("failed to read response body: %w", err)
270 | }
271 | return string(body), err
272 | }
273 |
274 | // WebAPIVersion of the qbittorrent client
275 | func (c *Client) WebAPIVersion() (version string, err error) {
276 | resp, err := c.get(apiBase+"app/webapiVersion", nil)
277 | if err != nil {
278 | return version, err
279 | }
280 | body, err := io.ReadAll(resp.Body)
281 | if err != nil {
282 | return version, fmt.Errorf("failed to read response body: %w", err)
283 | }
284 |
285 | return string(body), err
286 | }
287 |
288 | // BuildInfo of the qbittorrent client
289 | func (c *Client) BuildInfo() (buildInfo BuildInfo, err error) {
290 | resp, err := c.get(apiBase+"app/buildInfo", nil)
291 | if err != nil {
292 | return buildInfo, err
293 | }
294 | if err := json.NewDecoder(resp.Body).Decode(&buildInfo); err != nil {
295 | return buildInfo, err
296 | }
297 | return buildInfo, err
298 | }
299 |
300 | // Preferences of the qbittorrent client
301 | func (c *Client) Preferences() (prefs Preferences, err error) {
302 | resp, err := c.get(apiBase+"app/preferences", nil)
303 | if err != nil {
304 | return prefs, err
305 | }
306 | if err := json.NewDecoder(resp.Body).Decode(&prefs); err != nil {
307 | return prefs, err
308 | }
309 | return prefs, err
310 | }
311 |
312 | // SetPreferences of the qbittorrent client
313 | func (c *Client) SetPreferences() (prefsSet bool, err error) {
314 | resp, err := c.post(apiBase+"app/setPreferences", nil)
315 | return (resp.StatusCode == http.StatusOK), err
316 | }
317 |
318 | // DefaultSavePath of the qbittorrent client
319 | func (c *Client) DefaultSavePath() (path string, err error) {
320 | resp, err := c.get(apiBase+"app/defaultSavePath", nil)
321 | if err != nil {
322 | return path, err
323 | }
324 | body, err := io.ReadAll(resp.Body)
325 | if err != nil {
326 | return path, fmt.Errorf("failed to read response body: %w", err)
327 | }
328 |
329 | return string(body), err
330 | }
331 |
332 | // Shutdown shuts down the qbittorrent client
333 | func (c *Client) Shutdown() error {
334 | _, err := c.post(apiBase+"app/shutdown", nil)
335 | return err
336 | }
337 |
338 | // Log Endpoints
339 |
340 | // Logs of the qbittorrent client
341 | func (c *Client) Logs(filters map[string]string) (logs []Log, err error) {
342 | resp, err := c.get(apiBase+"log/main", filters)
343 | if err != nil {
344 | return logs, err
345 | }
346 | if err := json.NewDecoder(resp.Body).Decode(&logs); err != nil {
347 | return logs, err
348 | }
349 | return logs, err
350 | }
351 |
352 | // PeerLogs of the qbittorrent client
353 | func (c *Client) PeerLogs(filters map[string]string) (logs []PeerLog, err error) {
354 | resp, err := c.get(apiBase+"log/peers", filters)
355 | if err != nil {
356 | return logs, err
357 | }
358 | if err := json.NewDecoder(resp.Body).Decode(&logs); err != nil {
359 | return logs, err
360 | }
361 | return logs, err
362 | }
363 |
364 | // Sync Endpoints
365 |
366 | // MainData returns info you usually see in qBt status bar.
367 | func (c *Client) MainData(rid string) (mainData MainData, err error) {
368 | params := map[string]string{"rid": rid}
369 | resp, err := c.get(apiBase+"sync/maindata", params)
370 | if err != nil {
371 | return mainData, err
372 | }
373 | if err := json.NewDecoder(resp.Body).Decode(&mainData); err != nil {
374 | return mainData, err
375 | }
376 | return mainData, err
377 | }
378 |
379 | // TorrentPeers returns info you usually see in qBt status bar.
380 | func (c *Client) TorrentPeers(hash string, rid string) (torrentPeers TorrentPeers, err error) {
381 | params := map[string]string{"hash": hash, "rid": rid}
382 | resp, err := c.get(apiBase+"sync/torrentPeers", params)
383 | if err != nil {
384 | return torrentPeers, err
385 | }
386 | if err := json.NewDecoder(resp.Body).Decode(&torrentPeers); err != nil {
387 | return torrentPeers, err
388 | } else if resp != nil && (*resp).StatusCode == http.StatusNotFound {
389 | return torrentPeers, fmt.Errorf("torrent hash not found")
390 | }
391 | return torrentPeers, err
392 | }
393 |
394 | // Transfer Endpoints
395 |
396 | // Info returns info you usually see in qBt status bar.
397 | func (c *Client) Info() (info Info, err error) {
398 | resp, err := c.get(apiBase+"transfer/info", nil)
399 | if err != nil {
400 | return info, err
401 | }
402 | if err := json.NewDecoder(resp.Body).Decode(&info); err != nil {
403 | return info, err
404 | }
405 | return info, err
406 | }
407 |
408 | // AltSpeedLimitsEnabled returns the alternative speed limits state
409 | func (c *Client) AltSpeedLimitsEnabled() (mode bool, err error) {
410 | resp, err := c.get(apiBase+"transfer/speedLimitsMode", nil)
411 | if err != nil {
412 | return mode, err
413 | }
414 | var decoded int
415 | if err := json.NewDecoder(resp.Body).Decode(&decoded); err != nil {
416 | return mode, err
417 | }
418 | mode = decoded == 1
419 | return mode, err
420 | }
421 |
422 | // ToggleAltSpeedLimits toggles the alternative speed limits
423 | func (c *Client) ToggleAltSpeedLimits() error {
424 | _, err := c.post(apiBase+"transfer/toggleSpeedLimitsMode", nil)
425 | return err
426 | }
427 |
428 | // DlLimit returns the global download limit
429 | func (c *Client) DlLimit() (dlLimit int, err error) {
430 | resp, err := c.get(apiBase+"transfer/downloadLimit", nil)
431 | if err != nil {
432 | return dlLimit, err
433 | }
434 | if err := json.NewDecoder(resp.Body).Decode(&dlLimit); err != nil {
435 | return dlLimit, err
436 | }
437 | return dlLimit, err
438 | }
439 |
440 | // SetDlLimit sets the global download limit
441 | func (c *Client) SetDlLimit(limit int) error {
442 | params := map[string]string{"limit": strconv.Itoa(limit)}
443 | _, err := c.post(apiBase+"transfer/setDownloadLimit", params)
444 | return err
445 | }
446 |
447 | // UlLimit returns the global upload limit
448 | func (c *Client) UlLimit() (ulLimit int, err error) {
449 | resp, err := c.get(apiBase+"transfer/uploadLimit", nil)
450 | if err != nil {
451 | return ulLimit, err
452 | }
453 | json.NewDecoder(resp.Body).Decode(&ulLimit)
454 | return ulLimit, err
455 | }
456 |
457 | // SetUlLimit sets the global upload limit
458 | func (c *Client) SetUlLimit(limit int) error {
459 | params := map[string]string{"limit": strconv.Itoa(limit)}
460 | _, err := c.post(apiBase+"transfer/setUploadLimit", params)
461 | return err
462 | }
463 |
464 | // BanPeers bans the given peers
465 | func (c *Client) BanPeers(peers []string) (set bool, err error) {
466 | params := map[string]string{"peers": delimit(peers, "%7C")}
467 | resp, err := c.post(apiBase+"transfer/banPeers", params)
468 | if err != nil {
469 | return set, err
470 | }
471 | return (resp.StatusCode == http.StatusOK), err
472 | }
473 |
474 | // Torrents Endpoints
475 |
476 | // Torrents returns a list of all torrents in qbittorrent matching your filter
477 | func (c *Client) Torrents(opts TorrentsOptions) (torrentList []TorrentInfo, err error) {
478 | params := map[string]string{}
479 | if opts.Filter != nil {
480 | params["filter"] = *opts.Filter
481 | }
482 | if opts.Category != nil {
483 | params["category"] = *opts.Category
484 | }
485 | if opts.Sort != nil {
486 | params["sort"] = *opts.Sort
487 | }
488 | if opts.Reverse != nil {
489 | params["reverse"] = strconv.FormatBool(*opts.Reverse)
490 | }
491 | if opts.Offset != nil {
492 | params["offset"] = strconv.Itoa(*opts.Offset)
493 | }
494 | if opts.Limit != nil {
495 | params["limit"] = strconv.Itoa(*opts.Limit)
496 | }
497 | if opts.Hashes != nil {
498 | params["hashes"] = delimit(opts.Hashes, "%7C")
499 | }
500 | resp, err := c.get(apiBase+"torrents/info", params)
501 | if err != nil {
502 | return torrentList, err
503 | }
504 | if err := json.NewDecoder(resp.Body).Decode(&torrentList); err != nil {
505 | return torrentList, err
506 | }
507 | return torrentList, nil
508 | }
509 |
510 | // Torrent returns a specific torrent matching the hash
511 | func (c *Client) Torrent(hash string) (torrent Torrent, err error) {
512 | var opts = map[string]string{"hash": strings.ToLower(hash)}
513 | resp, err := c.get(apiBase+"torrents/properties", opts)
514 | if err != nil {
515 | return torrent, err
516 | }
517 | if err := json.NewDecoder(resp.Body).Decode(&torrent); err != nil {
518 | return torrent, err
519 | }
520 | return torrent, nil
521 | }
522 |
523 | // TorrentTrackers returns all trackers for a specific torrent matching the hash
524 | func (c *Client) TorrentTrackers(hash string) (trackers []Tracker, err error) {
525 | var opts = map[string]string{"hash": strings.ToLower(hash)}
526 | resp, err := c.get(apiBase+"torrents/trackers", opts)
527 | if err != nil {
528 | return trackers, err
529 | }
530 | if err := json.NewDecoder(resp.Body).Decode(&trackers); err != nil {
531 | return trackers, err
532 | }
533 | return trackers, nil
534 | }
535 |
536 | // TorrentWebSeeds returns seeders for a specific torrent matching the hash
537 | func (c *Client) TorrentWebSeeds(hash string) (webSeeds []WebSeed, err error) {
538 | var opts = map[string]string{"hash": strings.ToLower(hash)}
539 | resp, err := c.get(apiBase+"torrents/webseeds", opts)
540 | if err != nil {
541 | return webSeeds, err
542 | }
543 | if err := json.NewDecoder(resp.Body).Decode(&webSeeds); err != nil {
544 | return webSeeds, err
545 | }
546 | return webSeeds, nil
547 | }
548 |
549 | // TorrentFiles from given hash
550 | func (c *Client) TorrentFiles(hash string) (files []TorrentFile, err error) {
551 | var opts = map[string]string{"hash": strings.ToLower(hash)}
552 | resp, err := c.get(apiBase+"torrents/files", opts)
553 | if err != nil {
554 | return files, err
555 | }
556 | if err := json.NewDecoder(resp.Body).Decode(&files); err != nil {
557 | return files, err
558 | }
559 | return files, nil
560 | }
561 |
562 | // TorrentPieceStates for all pieces of torrent
563 | func (c *Client) TorrentPieceStates(hash string) (states []int, err error) {
564 | var opts = map[string]string{"hash": strings.ToLower(hash)}
565 | resp, err := c.get(apiBase+"torrents/pieceStates", opts)
566 | if err != nil {
567 | return states, err
568 | }
569 | if err := json.NewDecoder(resp.Body).Decode(&states); err != nil {
570 | return states, err
571 | }
572 | return states, nil
573 | }
574 |
575 | // TorrentPieceHashes for all pieces of torrent
576 | func (c *Client) TorrentPieceHashes(hash string) (hashes []string, err error) {
577 | var opts = map[string]string{"hash": strings.ToLower(hash)}
578 | resp, err := c.get(apiBase+"torrents/pieceHashes", opts)
579 | if err != nil {
580 | return hashes, err
581 | }
582 | if err := json.NewDecoder(resp.Body).Decode(&hashes); err != nil {
583 | return hashes, err
584 | }
585 | return hashes, nil
586 | }
587 |
588 | // Pause torrents
589 | func (c *Client) Pause(hashes []string) error {
590 | opts := map[string]string{"hashes": delimit(hashes, "|")}
591 | _, err := c.post(apiBase+"torrents/stop", opts)
592 | return err
593 | }
594 |
595 | // Resume torrents
596 | func (c *Client) Resume(hashes []string) error {
597 | opts := map[string]string{"hashes": delimit(hashes, "|")}
598 | _, err := c.post(apiBase+"torrents/start", opts)
599 | return err
600 | }
601 |
602 | // Delete torrents and optionally delete their files
603 | func (c *Client) Delete(hashes []string, deleteFiles bool) error {
604 | opts := map[string]string{
605 | "hashes": delimit(hashes, "|"),
606 | "deleteFiles": strconv.FormatBool(deleteFiles),
607 | }
608 | _, err := c.post(apiBase+"torrents/delete", opts)
609 | return err
610 | }
611 |
612 | // Recheck torrents
613 | func (c *Client) Recheck(hashes []string) error {
614 | opts := map[string]string{"hashes": delimit(hashes, "|")}
615 | _, err := c.post(apiBase+"torrents/recheck", opts)
616 | return err
617 | }
618 |
619 | // Reannounce torrents
620 | func (c *Client) Reannounce(hashes []string) error {
621 | opts := map[string]string{"hashes": delimit(hashes, "|")}
622 | _, err := c.post(apiBase+"torrents/reannounce", opts)
623 | return err
624 | }
625 |
626 | // DownloadFromLink starts downloading a torrent from a link
627 | func (c *Client) DownloadLinks(links []string, opts DownloadOptions) error {
628 | params := map[string]string{}
629 | if len(links) == 0 {
630 | return fmt.Errorf("at least one url must be present")
631 | } else {
632 | delimitedURLs := delimit(links, "%0A")
633 | // TODO: Why is encoding causing problems now?
634 | // encodedURLS := url.QueryEscape(delimitedURLs)
635 | params["urls"] = delimitedURLs
636 | }
637 | if opts.Savepath != nil {
638 | params["savepath"] = *opts.Savepath
639 | }
640 | if opts.Cookie != nil {
641 | params["cookie"] = *opts.Cookie
642 | }
643 | if opts.Category != nil {
644 | params["category"] = *opts.Category
645 | }
646 | if opts.SkipHashChecking != nil {
647 | params["skip_checking"] = strconv.FormatBool(*opts.SkipHashChecking)
648 | }
649 | if opts.Paused != nil {
650 | params["paused"] = strconv.FormatBool(*opts.Paused)
651 | }
652 | if opts.RootFolder != nil {
653 | params["root_folder"] = strconv.FormatBool(*opts.RootFolder)
654 | }
655 | if opts.Rename != nil {
656 | params["rename"] = *opts.Rename
657 | }
658 | if opts.UploadSpeedLimit != nil {
659 | params["upLimit"] = strconv.Itoa(*opts.UploadSpeedLimit)
660 | }
661 | if opts.DownloadSpeedLimit != nil {
662 | params["dlLimit"] = strconv.Itoa(*opts.DownloadSpeedLimit)
663 | }
664 | if opts.SequentialDownload != nil {
665 | params["sequentialDownload"] = strconv.FormatBool(*opts.SequentialDownload)
666 | }
667 | if opts.FirstLastPiecePriority != nil {
668 | params["firstLastPiecePrio"] = strconv.FormatBool(*opts.FirstLastPiecePriority)
669 | }
670 |
671 | resp, err := c.postMultipartData(apiBase+"torrents/add", params)
672 | if err != nil {
673 | return err
674 | } else if resp.StatusCode == 415 {
675 | return fmt.Errorf("torrent file is not valid")
676 | }
677 |
678 | return nil
679 | }
680 |
681 | // DownloadFromFile starts downloading a torrent from a file
682 | func (c *Client) DownloadFiles(torrents string, opts DownloadOptions) error {
683 | params := map[string]string{}
684 | if torrents == "" {
685 | return fmt.Errorf("at least one file must be present")
686 | }
687 | if opts.Savepath != nil {
688 | params["savepath"] = *opts.Savepath
689 | }
690 | if opts.Cookie != nil {
691 | params["cookie"] = *opts.Cookie
692 | }
693 | if opts.Category != nil {
694 | params["category"] = *opts.Category
695 | }
696 | if opts.SkipHashChecking != nil {
697 | params["skip_checking"] = strconv.FormatBool(*opts.SkipHashChecking)
698 | }
699 | if opts.Paused != nil {
700 | params["paused"] = strconv.FormatBool(*opts.Paused)
701 | }
702 | if opts.RootFolder != nil {
703 | params["root_folder"] = strconv.FormatBool(*opts.RootFolder)
704 | }
705 | if opts.Rename != nil {
706 | params["rename"] = *opts.Rename
707 | }
708 | if opts.UploadSpeedLimit != nil {
709 | params["upLimit"] = strconv.Itoa(*opts.UploadSpeedLimit)
710 | }
711 | if opts.DownloadSpeedLimit != nil {
712 | params["dlLimit"] = strconv.Itoa(*opts.DownloadSpeedLimit)
713 | }
714 | if opts.AutomaticTorrentManagement != nil {
715 | params["autoTMM"] = strconv.FormatBool(*opts.AutomaticTorrentManagement)
716 | }
717 | if opts.SequentialDownload != nil {
718 | params["sequentialDownload"] = strconv.FormatBool(*opts.SequentialDownload)
719 | }
720 | if opts.FirstLastPiecePriority != nil {
721 | params["firstLastPiecePrio"] = strconv.FormatBool(*opts.FirstLastPiecePriority)
722 | }
723 | resp, err := c.postMultipartFile(apiBase+"torrents/add", torrents, params)
724 | if err != nil {
725 | return err
726 | } else if resp.StatusCode == 415 {
727 | return fmt.Errorf("torrent file is not valid")
728 | }
729 |
730 | return nil
731 | }
732 |
733 | // AddTrackers to a torrent
734 | func (c *Client) AddTrackers(hash string, trackers []string) error {
735 | params := make(map[string]string)
736 | params["hash"] = strings.ToLower(hash)
737 | delimitedTrackers := delimit(trackers, "%0A")
738 | encodedTrackers := url.QueryEscape(delimitedTrackers)
739 | params["urls"] = encodedTrackers
740 |
741 | resp, err := c.post(apiBase+"torrents/addTrackers", params)
742 | if err != nil {
743 | return err
744 | } else if resp != nil && (*resp).StatusCode == http.StatusNotFound {
745 | return fmt.Errorf("torrent hash not found")
746 | }
747 | return nil
748 | }
749 |
750 | // EditTracker on a torrent
751 | func (c *Client) EditTracker(hash string, origURL string, newURL string) error {
752 | params := map[string]string{
753 | "hash": hash,
754 | "origUrl": origURL,
755 | "newUrl": newURL,
756 | }
757 | resp, err := c.post(apiBase+"torrents/editTracker", params)
758 | if err != nil {
759 | return err
760 | }
761 | switch sc := (*resp).StatusCode; sc {
762 | case http.StatusBadRequest:
763 | return fmt.Errorf("newUrl is not a valid url")
764 | case http.StatusNotFound:
765 | return fmt.Errorf("torrent hash was not found")
766 | case http.StatusConflict:
767 | return fmt.Errorf("newUrl already exists for this torrent or origUrl was not found")
768 | default:
769 | return nil
770 | }
771 | }
772 |
773 | // RemoveTrackers from a torrent
774 | func (c *Client) RemoveTrackers(hash string, trackers []string) error {
775 | params := map[string]string{
776 | "hash": hash,
777 | "urls": delimit(trackers, "|"),
778 | }
779 | resp, err := c.post(apiBase+"torrents/removeTrackers", params)
780 | if err != nil {
781 | return err
782 | }
783 |
784 | switch sc := (*resp).StatusCode; sc {
785 | case http.StatusOK:
786 | return nil
787 | case http.StatusNotFound:
788 | return fmt.Errorf("torrent hash was not found")
789 | case http.StatusConflict:
790 | return fmt.Errorf("all URLs were not found")
791 | default:
792 | return fmt.Errorf("an unknown error occurred causing a status code of: %d", sc)
793 | }
794 | }
795 |
796 | // IncreasePriority of torrents
797 | func (c *Client) IncreasePriority(hashes []string) error {
798 | opts := map[string]string{"hashes": delimit(hashes, "|")}
799 | resp, err := c.post(apiBase+"torrents/increasePrio", opts)
800 | if err != nil {
801 | return err
802 | }
803 |
804 | switch sc := (*resp).StatusCode; sc {
805 | case http.StatusOK:
806 | return nil
807 | case http.StatusConflict:
808 | return fmt.Errorf("torrent queueing is not enabled")
809 | default:
810 | return fmt.Errorf("an unknown error occurred causing a status code of: %d", sc)
811 | }
812 | }
813 |
814 | // DecreasePriority of torrents
815 | func (c *Client) DecreasePriority(hashes []string) error {
816 | opts := map[string]string{"hashes": delimit(hashes, "|")}
817 | resp, err := c.post(apiBase+"torrents/decreasePrio", opts)
818 | if err != nil {
819 | return err
820 | }
821 |
822 | switch sc := (*resp).StatusCode; sc {
823 | case http.StatusOK:
824 | return nil
825 | case http.StatusConflict:
826 | return fmt.Errorf("Torrent queueing is not enabled")
827 | default:
828 | return fmt.Errorf("an unknown error occurred causing a status code of: %d", sc)
829 | }
830 | }
831 |
832 | // MaxPriority maximizes the priority of torrents
833 | func (c *Client) MaxPriority(hashes []string) error {
834 | opts := map[string]string{"hashes": delimit(hashes, "|")}
835 | resp, err := c.post(apiBase+"torrents/topPrio", opts)
836 | if err != nil {
837 | return err
838 | }
839 |
840 | switch sc := (*resp).StatusCode; sc {
841 | case http.StatusOK:
842 | return nil
843 | case http.StatusConflict:
844 | return fmt.Errorf("torrent queueing is not enabled")
845 | default:
846 | return fmt.Errorf("an unknown error occurred causing a status code of: %d", sc)
847 | }
848 | }
849 |
850 | // MinPriority maximizes the priority of torrents
851 | func (c *Client) MinPriority(hashes []string) error {
852 | opts := map[string]string{"hashes": delimit(hashes, "|")}
853 | resp, err := c.post(apiBase+"torrents/bottomPrio", opts)
854 | if err != nil {
855 | return err
856 | }
857 |
858 | switch sc := (*resp).StatusCode; sc {
859 | case http.StatusOK:
860 | return nil
861 | case http.StatusConflict:
862 | return fmt.Errorf("torrent queueing is not enabled")
863 | default:
864 | return fmt.Errorf("an unknown error occurred causing a status code of: %d", sc)
865 | }
866 | }
867 |
868 | // FilePriority for a torrent
869 | func (c *Client) FilePriority(hash string, ids []int, priority int) error {
870 | formattedIds := []string{}
871 | for _, id := range ids {
872 | formattedIds = append(formattedIds, strconv.Itoa(id))
873 | }
874 |
875 | opts := map[string]string{
876 | "hashes": hash,
877 | "id": delimit(formattedIds, "|"),
878 | "priority": strconv.Itoa(priority),
879 | }
880 | resp, err := c.get(apiBase+"torrents/filePrio", opts)
881 | if err != nil {
882 | return err
883 | }
884 |
885 | switch sc := (*resp).StatusCode; sc {
886 | case http.StatusOK:
887 | return nil
888 | case http.StatusBadRequest:
889 | return fmt.Errorf("priority is invalid or at least one id is not an integer")
890 | case http.StatusConflict:
891 | return fmt.Errorf("Torrent metadata hasn't downloaded yet or at least one file id was not found")
892 | default:
893 | return fmt.Errorf("an unknown error occurred causing a status code of: %d", sc)
894 | }
895 | }
896 |
897 | // GetTorrentDownloadLimit for a list of torrents
898 | func (c *Client) GetTorrentDownloadLimit(hashes []string) (limits map[string]int, err error) {
899 | opts := map[string]string{"hashes": delimit(hashes, "|")}
900 | resp, err := c.post(apiBase+"torrents/downloadLimit", opts)
901 | if err != nil {
902 | return limits, err
903 | }
904 | if err := json.NewDecoder(resp.Body).Decode(&limits); err != nil {
905 | return limits, err
906 | }
907 | return limits, nil
908 | }
909 |
910 | // SetTorrentDownloadLimit for a list of torrents
911 | func (c *Client) SetTorrentDownloadLimit(hashes []string, limit int) error {
912 | opts := map[string]string{
913 | "hashes": delimit(hashes, "|"),
914 | "limit": strconv.Itoa(limit),
915 | }
916 | _, err := c.post(apiBase+"torrents/setDownloadLimit", opts)
917 | return err
918 | }
919 |
920 | // SetTorrentShareLimit for a list of torrents
921 | func (c *Client) SetTorrentShareLimit(hashes []string, ratioLimit int, seedingTimeLimit int, inactiveSeedTimeLimit int) error {
922 | opts := map[string]string{
923 | "hashes": delimit(hashes, "|"),
924 | "ratioLimit": strconv.Itoa(ratioLimit),
925 | "seedingTimeLimit": strconv.Itoa(seedingTimeLimit),
926 | "inactiveSeedingTimeLimit": strconv.Itoa(seedingTimeLimit),
927 | }
928 | resp, err := c.post(apiBase+"torrents/setShareLimits", opts)
929 | if err != nil {
930 | return err
931 | }
932 |
933 | switch sc := (*resp).StatusCode; sc {
934 | case http.StatusOK:
935 | return nil
936 | case http.StatusBadRequest:
937 | return fmt.Errorf("a share limit or at least one id is invalid")
938 | default:
939 | return fmt.Errorf("an unknown error occurred causing a status code of: %d", sc)
940 | }
941 | }
942 |
943 | // GetTorrentUploadLimit for a list of torrents
944 | func (c *Client) GetTorrentUploadLimit(hashes []string) (limits map[string]int, err error) {
945 | opts := map[string]string{"hashes": delimit(hashes, "|")}
946 | resp, err := c.post(apiBase+"torrents/uploadLimit", opts)
947 | if err != nil {
948 | return limits, err
949 | }
950 | if err := json.NewDecoder(resp.Body).Decode(&limits); err != nil {
951 | return limits, err
952 | }
953 | return limits, nil
954 | }
955 |
956 | // SetTorrentUploadLimit for a list of torrents
957 | func (c *Client) SetTorrentUploadLimit(hashes []string, limit int) error {
958 | opts := map[string]string{
959 | "hashes": delimit(hashes, "|"),
960 | "limit": strconv.Itoa(limit),
961 | }
962 | _, err := c.post(apiBase+"torrents/setUploadLimit", opts)
963 | return err
964 | }
965 |
966 | // SetTorrentLocation for a list of torrents
967 | func (c *Client) SetTorrentLocation(hashes []string, location string) error {
968 | opts := map[string]string{
969 | "hashes": delimit(hashes, "|"),
970 | "location": location,
971 | }
972 | resp, err := c.post(apiBase+"torrents/setLocation", opts)
973 | if err != nil {
974 | return err
975 | }
976 |
977 | switch sc := (*resp).StatusCode; sc {
978 | case http.StatusOK:
979 | return nil
980 | case http.StatusBadRequest:
981 | return fmt.Errorf("save path is empty")
982 | case http.StatusForbidden:
983 | return fmt.Errorf("user does not have write access to directory")
984 | case http.StatusConflict:
985 | return fmt.Errorf("unable to create save path directory")
986 | default:
987 | return fmt.Errorf("an unknown error occurred causing a status code of: %d", sc)
988 | }
989 | }
990 |
991 | // SetTorrentName for a torrent
992 | func (c *Client) SetTorrentName(hash string, name string) error {
993 | opts := map[string]string{
994 | "hash": hash,
995 | "name": name,
996 | }
997 | resp, err := c.post(apiBase+"torrents/rename", opts)
998 | if err != nil {
999 | return err
1000 | }
1001 |
1002 | switch sc := (*resp).StatusCode; sc {
1003 | case http.StatusOK:
1004 | return nil
1005 | case http.StatusNotFound:
1006 | return fmt.Errorf("torrent hash is invalid")
1007 | case http.StatusConflict:
1008 | return fmt.Errorf("torrent name is empty")
1009 | default:
1010 | return fmt.Errorf("an unknown error occurred causing a status code of: %d", sc)
1011 | }
1012 | }
1013 |
1014 | // SetTorrentCategory for a list of torrents
1015 | func (c *Client) SetTorrentCategory(hashes []string, category string) error {
1016 | opts := map[string]string{
1017 | "hashes": delimit(hashes, "|"),
1018 | "category": category,
1019 | }
1020 | resp, err := c.post(apiBase+"torrents/setCategory", opts)
1021 | if err != nil {
1022 | return err
1023 | }
1024 |
1025 | switch sc := (*resp).StatusCode; sc {
1026 | case http.StatusOK:
1027 | return nil
1028 | case http.StatusConflict:
1029 | return fmt.Errorf("category name does not exist")
1030 | default:
1031 | return fmt.Errorf("an unknown error occurred causing a status code of: %d", sc)
1032 | }
1033 | }
1034 |
1035 | // GetCategories used by client
1036 | func (c *Client) GetCategories() (categories Categories, err error) {
1037 | resp, err := c.get(apiBase+"torrents/categories", nil)
1038 | if err != nil {
1039 | return categories, err
1040 | }
1041 | if err := json.NewDecoder(resp.Body).Decode(&categories); err != nil {
1042 | return categories, err
1043 | }
1044 | return categories, nil
1045 | }
1046 |
1047 | // CreateCategory for use by client
1048 | func (c *Client) CreateCategory(category string, savePath string) error {
1049 | opts := map[string]string{
1050 | "category": category,
1051 | "savePath": savePath,
1052 | }
1053 | resp, err := c.post(apiBase+"torrents/createCategory", opts)
1054 | if err != nil {
1055 | return err
1056 | }
1057 |
1058 | switch sc := (*resp).StatusCode; sc {
1059 | case http.StatusOK:
1060 | return nil
1061 | case http.StatusBadRequest:
1062 | return fmt.Errorf("category name is empty")
1063 | case http.StatusConflict:
1064 | return fmt.Errorf("category name is invalid")
1065 | default:
1066 | return fmt.Errorf("an unknown error occurred causing a status code of: %d", sc)
1067 | }
1068 | }
1069 |
1070 | // UpdateCategory used by client
1071 | func (c *Client) UpdateCategory(category string, savePath string) error {
1072 | opts := map[string]string{
1073 | "category": category,
1074 | "savePath": savePath,
1075 | }
1076 | resp, err := c.post(apiBase+"torrents/editCategory", opts)
1077 | if err != nil {
1078 | return err
1079 | }
1080 |
1081 | switch sc := (*resp).StatusCode; sc {
1082 | case http.StatusOK:
1083 | return nil
1084 | case http.StatusBadRequest:
1085 | return fmt.Errorf("category name is empty")
1086 | case http.StatusConflict:
1087 | return fmt.Errorf("category editing failed")
1088 | default:
1089 | return fmt.Errorf("an unknown error occurred causing a status code of: %d", sc)
1090 | }
1091 | }
1092 |
1093 | // DeleteCategories used by client
1094 | func (c *Client) DeleteCategories(categories []string) error {
1095 | opts := map[string]string{"categories": delimit(categories, "\n")}
1096 | _, err := c.post(apiBase+"torrents/removeCategories", opts)
1097 | return err
1098 | }
1099 |
1100 | // AddTorrentTags to a list of torrents
1101 | func (c *Client) AddTorrentTags(hashes []string, tags []string) (bool, error) {
1102 | opts := map[string]string{
1103 | "hashes": delimit(hashes, "|"),
1104 | "tags": delimit(tags, ","),
1105 | }
1106 | resp, err := c.post(apiBase+"torrents/addTags", opts)
1107 | if err != nil {
1108 | return false, err
1109 | }
1110 |
1111 | return resp.StatusCode == http.StatusOK, nil //TODO: look into other statuses
1112 | }
1113 |
1114 | // RemoveTorrentTags from a list of torrents (empty list removes all tags)
1115 | func (c *Client) RemoveTorrentTags(hashes []string, tags []string) (bool, error) {
1116 | opts := map[string]string{
1117 | "hashes": delimit(hashes, "|"),
1118 | "tags": delimit(tags, ","),
1119 | }
1120 | resp, err := c.post(apiBase+"torrents/removeTags", opts)
1121 | if err != nil {
1122 | return false, err
1123 | }
1124 |
1125 | return resp.StatusCode == http.StatusOK, nil //TODO: look into other statuses
1126 | }
1127 |
1128 | // GetTorrentTags from a list of torrents (empty list removes all tags)
1129 | func (c *Client) GetTorrentTags() (tags []string, err error) {
1130 | resp, err := c.get(apiBase+"torrents/tags", nil)
1131 | if err != nil {
1132 | return nil, err
1133 | }
1134 | if err := json.NewDecoder(resp.Body).Decode(&tags); err != nil {
1135 | return tags, err
1136 | }
1137 | return tags, nil
1138 | }
1139 |
1140 | // CreateTags for use by client
1141 | func (c *Client) CreateTags(tags []string) (bool, error) {
1142 | opts := map[string]string{"tags": delimit(tags, ",")}
1143 | resp, err := c.post(apiBase+"torrents/createTags", opts)
1144 | if err != nil {
1145 | return false, err
1146 | }
1147 |
1148 | return resp.StatusCode == http.StatusOK, nil //TODO: look into other statuses
1149 | }
1150 |
1151 | // DeleteTags used by client
1152 | func (c *Client) DeleteTags(tags []string) (bool, error) {
1153 | opts := map[string]string{"tags": delimit(tags, ",")}
1154 | resp, err := c.post(apiBase+"torrents/deleteTags", opts)
1155 | if err != nil {
1156 | return false, err
1157 | }
1158 |
1159 | return resp.StatusCode == http.StatusOK, nil //TODO: look into other statuses
1160 | }
1161 |
1162 | // SetAutoManagement for a list of torrents
1163 | func (c *Client) SetAutoManagement(hashes []string, enable bool) (bool, error) {
1164 | opts := map[string]string{
1165 | "hashes": delimit(hashes, "|"),
1166 | "enable": strconv.FormatBool(enable),
1167 | }
1168 | resp, err := c.post(apiBase+"torrents/setAutoManagement", opts)
1169 | if err != nil {
1170 | return false, err
1171 | }
1172 | return resp.StatusCode == http.StatusOK, nil //TODO: look into other statuses
1173 | }
1174 |
1175 | // ToggleSequentialDownload for a list of torrents
1176 | func (c *Client) ToggleSequentialDownload(hashes []string) (bool, error) {
1177 | opts := map[string]string{"hashes": delimit(hashes, "|")}
1178 | resp, err := c.get(apiBase+"torrents/toggleSequentialDownload", opts)
1179 | if err != nil {
1180 | return false, err
1181 | }
1182 | return resp.StatusCode == http.StatusOK, nil //TODO: look into other statuses
1183 | }
1184 |
1185 | // ToggleFirstLastPiecePriority for a list of torrents
1186 | func (c *Client) ToggleFirstLastPiecePriority(hashes []string) (bool, error) {
1187 | opts := map[string]string{"hashes": delimit(hashes, "|")}
1188 | resp, err := c.get(apiBase+"torrents/toggleFirstLastPiecePrio", opts)
1189 | if err != nil {
1190 | return false, err
1191 | }
1192 | return resp.StatusCode == http.StatusOK, nil //TODO: look into other statuses
1193 | }
1194 |
1195 | // SetForceStart for a list of torrents
1196 | func (c *Client) SetForceStart(hashes []string, value bool) (bool, error) {
1197 | opts := map[string]string{
1198 | "hashes": delimit(hashes, "|"),
1199 | "value": strconv.FormatBool(value),
1200 | }
1201 | resp, err := c.post(apiBase+"torrents/setForceStart", opts)
1202 | if err != nil {
1203 | return false, err
1204 | }
1205 | return resp.StatusCode == http.StatusOK, nil //TODO: look into other statuses
1206 | }
1207 |
1208 | // SetSuperSeeding for a list of torrents
1209 | func (c *Client) SetSuperSeeding(hashes []string, value bool) (bool, error) {
1210 | opts := map[string]string{
1211 | "hashes": delimit(hashes, "|"),
1212 | "value": strconv.FormatBool(value),
1213 | }
1214 | resp, err := c.post(apiBase+"torrents/setSuperSeeding", opts)
1215 | if err != nil {
1216 | return false, err
1217 | }
1218 | return resp.StatusCode == http.StatusOK, nil //TODO: look into other statuses
1219 | }
1220 |
1221 | // RenameFile for a single torrent
1222 | func (c *Client) RenameFile(hash string, oldPath string, newPath string) (err error) {
1223 | opts := map[string]string{
1224 | "hash": hash,
1225 | "oldPath": oldPath,
1226 | "newPath": newPath,
1227 | }
1228 | resp, err := c.post(apiBase+"torrents/renameFile", opts)
1229 | if err != nil {
1230 | return err
1231 | }
1232 | switch sc := (*resp).StatusCode; sc {
1233 | case http.StatusOK:
1234 | return nil
1235 | case http.StatusBadRequest:
1236 | return fmt.Errorf("missing newPath parameter")
1237 | case http.StatusConflict:
1238 | return fmt.Errorf("invalid newPath or oldPath, or newPath already in use")
1239 | default:
1240 | return fmt.Errorf("an unknown error occurred causing a status code of: %d", sc)
1241 | }
1242 | }
1243 |
1244 | // RenameFolder for a single torrent
1245 | func (c *Client) RenameFolder(hash string, oldPath string, newPath string) (err error) {
1246 | opts := map[string]string{
1247 | "hash": hash,
1248 | "oldPath": oldPath,
1249 | "newPath": newPath,
1250 | }
1251 | resp, err := c.post(apiBase+"torrents/renameFile", opts)
1252 | if err != nil {
1253 | return err
1254 | }
1255 | switch sc := (*resp).StatusCode; sc {
1256 | case http.StatusOK:
1257 | return nil
1258 | case http.StatusBadRequest:
1259 | return fmt.Errorf("missing newPath parameter")
1260 | case http.StatusConflict:
1261 | return fmt.Errorf("invalid newPath or oldPath, or newPath already in use")
1262 | default:
1263 | return fmt.Errorf("an unknown error occurred causing a status code of: %d", sc)
1264 | }
1265 | }
1266 |
--------------------------------------------------------------------------------
/qbt/models.go:
--------------------------------------------------------------------------------
1 | package qbt
2 |
3 | // BasicTorrent holds a basic torrent object from qbittorrent
4 | type BasicTorrent struct {
5 | Category string `json:"category"`
6 | CompletionOn int64 `json:"completion_on"`
7 | Dlspeed int `json:"dlspeed"`
8 | Eta int `json:"eta"`
9 | ForceStart bool `json:"force_start"`
10 | Hash string `json:"hash"`
11 | Name string `json:"name"`
12 | NumComplete int `json:"num_complete"`
13 | NumIncomplete int `json:"num_incomplete"`
14 | NumLeechs int `json:"num_leechs"`
15 | NumSeeds int `json:"num_seeds"`
16 | Priority int `json:"priority"`
17 | Progress int `json:"progress"`
18 | Ratio int `json:"ratio"`
19 | SavePath string `json:"save_path"`
20 | SeqDl bool `json:"seq_dl"`
21 | Size int `json:"size"`
22 | State string `json:"state"`
23 | SuperSeeding bool `json:"super_seeding"`
24 | Upspeed int `json:"upspeed"`
25 | FirstLastPiecePriority bool `json:"f_l_piece_prio"`
26 | }
27 |
28 | // Torrent holds a torrent object from qbittorrent
29 | // with more information than BasicTorrent
30 | type Torrent struct {
31 | AdditionDate int `json:"addition_date"`
32 | Comment string `json:"comment"`
33 | CompletionDate int `json:"completion_date"`
34 | CreatedBy string `json:"created_by"`
35 | CreationDate int `json:"creation_date"`
36 | DlLimit int `json:"dl_limit"`
37 | DlSpeed int `json:"dl_speed"`
38 | DlSpeedAvg int `json:"dl_speed_avg"`
39 | Eta int `json:"eta"`
40 | LastSeen int `json:"last_seen"`
41 | NbConnections int `json:"nb_connections"`
42 | NbConnectionsLimit int `json:"nb_connections_limit"`
43 | Peers int `json:"peers"`
44 | PeersTotal int `json:"peers_total"`
45 | PieceSize int `json:"piece_size"`
46 | PiecesHave int `json:"pieces_have"`
47 | PiecesNum int `json:"pieces_num"`
48 | Reannounce int `json:"reannounce"`
49 | SavePath string `json:"save_path"`
50 | SeedingTime int `json:"seeding_time"`
51 | Seeds int `json:"seeds"`
52 | SeedsTotal int `json:"seeds_total"`
53 | ShareRatio float64 `json:"share_ratio"`
54 | TimeElapsed int `json:"time_elapsed"`
55 | TotalDl int `json:"total_downloaded"`
56 | TotalDlSession int `json:"total_downloaded_session"`
57 | TotalSize int `json:"total_size"`
58 | TotalUl int `json:"total_uploaded"`
59 | TotalUlSession int `json:"total_uploaded_session"`
60 | TotalWasted int `json:"total_wasted"`
61 | UpLimit int `json:"up_limit"`
62 | UpSpeed int `json:"up_speed"`
63 | UpSpeedAvg int `json:"up_speed_avg"`
64 | }
65 |
66 | type TorrentInfo struct {
67 | AddedOn int64 `json:"added_on"`
68 | AmountLeft int64 `json:"amount_left"`
69 | AutoTmm bool `json:"auto_tmm"`
70 | Availability float64 `json:"availability"`
71 | Category string `json:"category"`
72 | Completed int64 `json:"completed"`
73 | CompletionOn int64 `json:"completion_on"`
74 | ContentPath string `json:"content_path"`
75 | DlLimit int64 `json:"dl_limit"`
76 | Dlspeed int64 `json:"dlspeed"`
77 | Downloaded int64 `json:"downloaded"`
78 | DownloadedSession int64 `json:"downloaded_session"`
79 | Eta int64 `json:"eta"`
80 | FLPiecePrio bool `json:"f_l_piece_prio"`
81 | ForceStart bool `json:"force_start"`
82 | Hash string `json:"hash"`
83 | LastActivity int64 `json:"last_activity"`
84 | MagnetURI string `json:"magnet_uri"`
85 | MaxRatio float64 `json:"max_ratio"`
86 | MaxSeedingTime int64 `json:"max_seeding_time"`
87 | Name string `json:"name"`
88 | NumComplete int64 `json:"num_complete"`
89 | NumIncomplete int64 `json:"num_incomplete"`
90 | NumLeechs int64 `json:"num_leechs"`
91 | NumSeeds int64 `json:"num_seeds"`
92 | Priority int64 `json:"priority"`
93 | Progress float64 `json:"progress"`
94 | Ratio float64 `json:"ratio"`
95 | RatioLimit int64 `json:"ratio_limit"`
96 | SavePath string `json:"save_path"`
97 | SeedingTimeLimit int64 `json:"seeding_time_limit"`
98 | SeenComplete int64 `json:"seen_complete"`
99 | SeqDl bool `json:"seq_dl"`
100 | Size int64 `json:"size"`
101 | State string `json:"state"`
102 | SuperSeeding bool `json:"super_seeding"`
103 | Tags string `json:"tags"`
104 | TimeActive int64 `json:"time_active"`
105 | TotalSize int64 `json:"total_size"`
106 | Tracker string `json:"tracker"`
107 | TrackersCount int64 `json:"trackers_count"`
108 | UpLimit int64 `json:"up_limit"`
109 | Uploaded int64 `json:"uploaded"`
110 | UploadedSession int64 `json:"uploaded_session"`
111 | Upspeed int64 `json:"upspeed"`
112 | }
113 |
114 | // Tracker holds a tracker object from qbittorrent
115 | type Tracker struct {
116 | Msg string `json:"msg"`
117 | NumPeers int `json:"num_peers"`
118 | NumSeeds int `json:"num_seeds"`
119 | NumLeeches int `json:"num_leeches"`
120 | NumDownloaded int `json:"num_downloaded"`
121 | Tier int `json:"tier"`
122 | Status int `json:"status"`
123 | URL string `json:"url"`
124 | }
125 |
126 | // WebSeed holds a webseed object from qbittorrent
127 | type WebSeed struct {
128 | URL string `json:"url"`
129 | }
130 |
131 | // TorrentFile holds a torrent file object from qbittorrent
132 | type TorrentFile struct {
133 | IsSeed bool `json:"is_seed"`
134 | Name string `json:"name"`
135 | Availability float32 `json:"availability"`
136 | Priority int `json:"priority"`
137 | Progress float64 `json:"progress"`
138 | Size int `json:"size"`
139 | PieceRange []int `json:"piece_range"`
140 | }
141 |
142 | // serverState holds the server state struct
143 | type serverState struct {
144 | ConnectionStatus string `json:"connection_status"`
145 | DhtNodes int `json:"dht_nodes"`
146 | DlInfoData int `json:"dl_info_data"`
147 | DlInfoSpeed int `json:"dl_info_speed"`
148 | DlRateLimit int `json:"dl_rate_limit"`
149 | Queueing bool `json:"queueing"`
150 | RefreshInterval int `json:"refresh_interval"`
151 | UpInfoData int `json:"up_info_data"`
152 | UpInfoSpeed int `json:"up_info_speed"`
153 | UpRateLimit int `json:"up_rate_limit"`
154 | UseAltSpeedLimits bool `json:"use_alt_speed_limits"`
155 | }
156 |
157 | // Sync holds the sync response struct which contains
158 | // the server state and a map of infohashes to Torrents
159 | type Sync struct {
160 | Categories []string `json:"categories"`
161 | FullUpdate bool `json:"full_update"`
162 | Rid int `json:"rid"`
163 | ServerState serverState `json:"server_state"`
164 | Torrents map[string]Torrent `json:"torrents"`
165 | }
166 |
167 | type BuildInfo struct {
168 | QTVersion string `json:"qt"`
169 | LibtorrentVersion string `json:"libtorrent"`
170 | BoostVersion string `json:"boost"`
171 | OpenSSLVersion string `json:"openssl"`
172 | AppBitness int `json:"bitness"`
173 | }
174 |
175 | type Preferences struct {
176 | Locale string `json:"locale"`
177 | CreateSubfolderEnabled bool `json:"create_subfolder_enabled"`
178 | StartPausedEnabled bool `json:"start_paused_enabled"`
179 | AutoDeleteMode int `json:"auto_delete_mode"`
180 | PreallocateAll bool `json:"preallocate_all"`
181 | IncompleteFilesExt bool `json:"incomplete_files_ext"`
182 | AutoTMMEnabled bool `json:"auto_tmm_enabled"`
183 | TorrentChangedTMMEnabled bool `json:"torrent_changed_tmm_enabled"`
184 | SavePathChangedTMMEnabled bool `json:"save_path_changed_tmm_enabled"`
185 | CategoryChangedTMMEnabled bool `json:"category_changed_tmm_enabled"`
186 | SavePath string `json:"save_path"`
187 | TempPathEnabled bool `json:"temp_path_enabled"`
188 | TempPath string `json:"temp_path"`
189 | ScanDirs map[string]interface{} `json:"scan_dirs"`
190 | ExportDir string `json:"export_dir"`
191 | ExportDirFin string `json:"export_dir_fin"`
192 | MailNotificationEnabled bool `json:"mail_notification_enabled"`
193 | MailNotificationSender string `json:"mail_notification_sender"`
194 | MailNotificationEmail string `json:"mail_notification_email"`
195 | MailNotificationSMPTP string `json:"mail_notification_smtp"`
196 | MailNotificationSSLEnabled bool `json:"mail_notification_ssl_enabled"`
197 | MailNotificationAuthEnabled bool `json:"mail_notification_auth_enabled"`
198 | MailNotificationUsername string `json:"mail_notification_username"`
199 | MailNotificationPassword string `json:"mail_notification_password"`
200 | AutorunEnabled bool `json:"autorun_enabled"`
201 | AutorunProgram string `json:"autorun_program"`
202 | QueueingEnabled bool `json:"queueing_enabled"`
203 | MaxActiveDls int `json:"max_active_downloads"`
204 | MaxActiveTorrents int `json:"max_active_torrents"`
205 | MaxActiveUls int `json:"max_active_uploads"`
206 | DontCountSlowTorrents bool `json:"dont_count_slow_torrents"`
207 | SlowTorrentDlRateThreshold int `json:"slow_torrent_dl_rate_threshold"`
208 | SlowTorrentUlRateThreshold int `json:"slow_torrent_ul_rate_threshold"`
209 | SlowTorrentInactiveTimer int `json:"slow_torrent_inactive_timer"`
210 | MaxRatioEnabled bool `json:"max_ratio_enabled"`
211 | MaxRatio float64 `json:"max_ratio"`
212 | MaxRatioAct float64 `json:"max_ratio_act"`
213 | ListenPort int `json:"listen_port"`
214 | UPNP bool `json:"upnp"`
215 | RandomPort bool `json:"random_port"`
216 | DlLimit int `json:"dl_limit"`
217 | UlLimit int `json:"up_limit"`
218 | MaxConnections int `json:"max_connec"`
219 | MaxConnectionsPerTorrent int `json:"max_connec_per_torrent"`
220 | MaxUls int `json:"max_uploads"`
221 | MaxUlsPerTorrent int `json:"max_uploads_per_torrent"`
222 | UTPEnabled bool `json:"enable_utp"`
223 | LimitUTPRate bool `json:"limit_utp_rate"`
224 | LimitTCPOverhead bool `json:"limit_tcp_overhead"`
225 | LimitLANPeers bool `json:"limit_lan_peers"`
226 | AltDlLimit int `json:"alt_dl_limit"`
227 | AltUlLimit int `json:"alt_up_limit"`
228 | SchedulerEnabled bool `json:"scheduler_enabled"`
229 | ScheduleFromHour int `json:"schedule_from_hour"`
230 | ScheduleFromMin int `json:"schedule_from_min"`
231 | ScheduleToHour int `json:"schedule_to_hour"`
232 | ScheduleToMin int `json:"schedule_to_min"`
233 | SchedulerDays int `json:"scheduler_days"`
234 | DHTEnabled bool `json:"dht"`
235 | DHTSameAsBT bool `json:"dhtSameAsBT"`
236 | DHTPort int `json:"dht_port"`
237 | PexEnabled bool `json:"pex"`
238 | LSDEnabled bool `json:"lsd"`
239 | Encryption int `json:"encryption"`
240 | AnonymousMode bool `json:"anonymous_mode"`
241 | ProxyType string `json:"proxy_type"`
242 | ProxyIP string `json:"proxy_ip"`
243 | ProxyPort int `json:"proxy_port"`
244 | ProxyPeerConnections bool `json:"proxy_peer_connections"`
245 | ForceProxy bool `json:"force_proxy"`
246 | ProxyAuthEnabled bool `json:"proxy_auth_enabled"`
247 | ProxyUsername string `json:"proxy_username"`
248 | ProxyPassword string `json:"proxy_password"`
249 | IPFilterEnabled bool `json:"ip_filter_enabled"`
250 | IPFilterPath string `json:"ip_filter_path"`
251 | IPFilterTrackers bool `json:"ip_filter_trackers"`
252 | WebUIDomainList string `json:"web_ui_domain_list"`
253 | WebUIAddress string `json:"web_ui_address"`
254 | WebUIPort int `json:"web_ui_port"`
255 | WebUIUPNPEnabled bool `json:"web_ui_upnp"`
256 | WebUIUsername string `json:"web_ui_username"`
257 | WebUIPassword string `json:"web_ui_password"`
258 | WebUICSRFProtectionEnabled bool `json:"web_ui_csrf_protection_enabled"`
259 | WebUIClickjackingProtectionEnabled bool `json:"web_ui_clickjacking_protection_enabled"`
260 | BypassLocalAuth bool `json:"bypass_local_auth"`
261 | BypassAuthSubnetWhitelistEnabled bool `json:"bypass_auth_subnet_whitelist_enabled"`
262 | BypassAuthSubnetWhitelist string `json:"bypass_auth_subnet_whitelist"`
263 | AltWebUIEnabled bool `json:"alternative_webui_enabled"`
264 | AltWebUIPath string `json:"alternative_webui_path"`
265 | UseHTTPS bool `json:"use_https"`
266 | SSLKey string `json:"ssl_key"`
267 | SSLCert string `json:"ssl_cert"`
268 | DynDNSEnabled bool `json:"dyndns_enabled"`
269 | DynDNSService int `json:"dyndns_service"`
270 | DynDNSUsername string `json:"dyndns_username"`
271 | DynDNSPassword string `json:"dyndns_password"`
272 | DynDNSDomain string `json:"dyndns_domain"`
273 | RSSRefreshInterval int `json:"rss_refresh_interval"`
274 | RSSMaxArtPerFeed int `json:"rss_max_articles_per_feed"`
275 | RSSProcessingEnabled bool `json:"rss_processing_enabled"`
276 | RSSAutoDlEnabled bool `json:"rss_auto_downloading_enabled"`
277 | }
278 |
279 | // Log
280 | type Log struct {
281 | ID int `json:"id"`
282 | Message string `json:"message"`
283 | Timestamp int `json:"timestamp"`
284 | Type int `json:"type"`
285 | }
286 |
287 | // PeerLog
288 | type PeerLog struct {
289 | ID int `json:"id"`
290 | IP string `json:"ip"`
291 | Blocked bool `json:"blocked"`
292 | Timestamp int `json:"timestamp"`
293 | Reason string `json:"reason"`
294 | }
295 |
296 | // MainData
297 | type MainData struct {
298 | Rid int `json:"rid"`
299 | FullUpdate bool `json:"full_update"`
300 | Torrents map[string]Torrent `json:"torrents"`
301 | TorrentsRemoved []string `json:"torrents_removed"`
302 | Categories []string `json:"categories"`
303 | CategoriesRemoved []string `json:"categories_removed"`
304 | Tags []string `json:"tags"`
305 | TagsRemoved []string `json:"tags_removed"`
306 | ServerState serverState `json:"server_state"`
307 | }
308 |
309 | // Main Data Options
310 | type MainDataOptions struct {
311 | Rid int
312 | }
313 |
314 | // Torrent Peers Options
315 | type TorrentPeersOptions struct {
316 | Hash string
317 | Rid int
318 | }
319 |
320 | // Torrent Peer
321 | type TorrentPeer struct {
322 | Client string `json:"client"`
323 | Connection string `json:"connection"`
324 | Country string `json:"country"`
325 | CountryCode string `json:"country_code"`
326 | DlSpeed int `json:"dl_speed"`
327 | Downloaded int `json:"downloaded"`
328 | Files string `json:"files"`
329 | Flags string `json:"flags"`
330 | FlagsDesc string `json:"flags_desc"`
331 | IP string `json:"ip"`
332 | PeerIDClient string `json:"peer_id_client"`
333 | Port int `json:"port"`
334 | Progress int `json:"progress"`
335 | Relevance int `json:"relevance"`
336 | UpSpeed int `json:"up_speed"`
337 | Uploaded int `json:"uploaded"`
338 | }
339 |
340 | // Torrent Peers
341 | type TorrentPeers struct {
342 | FullUpdate bool `json:"full_update"`
343 | Peers map[string]TorrentPeer `json:"peers"`
344 | Rid int `json:"rid"`
345 | ShowFlags bool `json:"show_flags"`
346 | }
347 |
348 | // Info
349 | type Info struct {
350 | ConnectionStatus string `json:"connection_status"`
351 | DHTNodes int `json:"dht_nodes"`
352 | DlInfoData int `json:"dl_info_data"`
353 | DlInfoSpeed int `json:"dl_info_speed"`
354 | DlRateLimit int `json:"dl_rate_limit"`
355 | UlInfoData int `json:"up_info_data"`
356 | UlInfoSpeed int `json:"up_info_speed"`
357 | UlRateLimit int `json:"up_rate_limit"`
358 | Queueing bool `json:"queueing"`
359 | UseAltSpeedLimits bool `json:"use_alt_speed_limits"`
360 | RefreshInterval int `json:"refresh_interval"`
361 | }
362 |
363 | type TorrentsOptions struct {
364 | Filter *string // all, downloading, completed, paused, active, inactive => optional
365 | Category *string // => optional
366 | Sort *string // => optional
367 | Reverse *bool // => optional
368 | Limit *int // => optional (no negatives)
369 | Offset *int // => optional (negatives allowed)
370 | Hashes []string // separated by | => optional
371 | }
372 |
373 | // Category of torrent
374 | type Category struct {
375 | Name string `json:"name"`
376 | SavePath string `json:"savePath"`
377 | }
378 |
379 | // Categories mapping
380 | type Categories struct {
381 | Category map[string]Category
382 | }
383 |
384 | // DownloadOptions stores optional parameters for downloading torrents
385 | // Uses pointers instead of functional parameters to allow for zero valued options
386 | type DownloadOptions struct {
387 | Savepath *string
388 | Cookie *string
389 | Category *string
390 | SkipHashChecking *bool
391 | Paused *bool
392 | RootFolder *bool
393 | Rename *string
394 | UploadSpeedLimit *int
395 | DownloadSpeedLimit *int
396 | SequentialDownload *bool
397 | AutomaticTorrentManagement *bool
398 | FirstLastPiecePriority *bool
399 | }
400 |
--------------------------------------------------------------------------------
/tools/tools.go:
--------------------------------------------------------------------------------
1 | package tools
2 |
3 | import (
4 | "fmt"
5 | "io"
6 | "net/http"
7 | "net/http/httputil"
8 | )
9 |
10 | // PrintResponse prints the body of a response
11 | func PrintResponse(body io.ReadCloser) {
12 | r, _ := io.ReadAll(body)
13 | fmt.Println("response: " + string(r))
14 | }
15 |
16 | // PrintRequest prints a request
17 | func PrintRequest(req *http.Request) error {
18 | r, err := httputil.DumpRequest(req, true)
19 | if err != nil {
20 | return err
21 | }
22 | fmt.Println("request: " + string(r))
23 | return nil
24 | }
25 |
--------------------------------------------------------------------------------