├── .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 | 12 | 13 | 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 | 70 | 71 | 90 | 91 | 92 | 93 | 94 | true 95 | DEFINITION_ORDER 96 | 97 | 98 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 127 | 128 | 131 | 132 | 133 | 134 | 137 | 138 | 141 | 142 | 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 | 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 | 250 | 251 | 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 | --------------------------------------------------------------------------------