├── .gitignore ├── tools └── tools.go ├── main.go ├── .idea ├── libraries │ └── GOPATH__go_qbittorrent_.xml └── workspace.xml ├── qbt ├── models.go └── api.go ├── README.rst └── docs.txt /.gitignore: -------------------------------------------------------------------------------- 1 | ./main.go 2 | ./.idea/* 3 | -------------------------------------------------------------------------------- /tools/tools.go: -------------------------------------------------------------------------------- 1 | package tools 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | "io/ioutil" 7 | "net/http" 8 | "net/http/httputil" 9 | ) 10 | 11 | //PrintResponse prints the body of a response 12 | func PrintResponse(body io.ReadCloser) { 13 | r := make([]byte, 256) 14 | r, _ = ioutil.ReadAll(body) 15 | fmt.Println("response: " + string(r)) 16 | } 17 | 18 | //PrintRequest prints a request 19 | func PrintRequest(req *http.Request) error { 20 | r, err := httputil.DumpRequest(req, true) 21 | if err != nil { 22 | return err 23 | } 24 | fmt.Println("request: " + string(r)) 25 | return nil 26 | } 27 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "go-qbittorrent/qbt" 6 | "go-qbittorrent/tools" 7 | ) 8 | 9 | func main() { 10 | // connect to qbittorrent client 11 | qb := qbt.NewClient("http://localhost:8080/") 12 | 13 | // login to the client 14 | _, err := qb.Login("username", "password") 15 | if err != nil { 16 | fmt.Println(err) 17 | } 18 | 19 | // were not using any filters so the options map is empty 20 | options := map[string]string{} 21 | // set the path to the file 22 | file := "/Users/me/Downloads/Source.Code.2011.1080p.BluRay.H264.AAC-RARBG-[rarbg.to].torrent" 23 | // download the torrent using the file 24 | // the wrapper will handle opening and closing the file for you 25 | resp, err := qb.DownloadFromFile(file, options) 26 | if err != nil { 27 | tools.PrintResponse(resp.Body) 28 | fmt.Println(err) 29 | } else { 30 | fmt.Println("downloaded successful") 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /qbt/models.go: -------------------------------------------------------------------------------- 1 | package qbt 2 | 3 | //BasicTorrent holds a basic torrent object from qbittorrent 4 | type BasicTorrent struct { 5 | AddedOn int `json:"added_on"` 6 | Category string `json:"category"` 7 | CompletionOn int64 `json:"completion_on"` 8 | Dlspeed int `json:"dlspeed"` 9 | Eta int `json:"eta"` 10 | ForceStart bool `json:"force_start"` 11 | Hash string `json:"hash"` 12 | Name string `json:"name"` 13 | NumComplete int `json:"num_complete"` 14 | NumIncomplete int `json:"num_incomplete"` 15 | NumLeechs int `json:"num_leechs"` 16 | NumSeeds int `json:"num_seeds"` 17 | Priority int `json:"priority"` 18 | Progress int `json:"progress"` 19 | Ratio int `json:"ratio"` 20 | SavePath string `json:"save_path"` 21 | SeqDl bool `json:"seq_dl"` 22 | Size int `json:"size"` 23 | State string `json:"state"` 24 | SuperSeeding bool `json:"super_seeding"` 25 | Upspeed int `json:"upspeed"` 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 | TotalDownloaded int `json:"total_downloaded"` 56 | TotalDownloadedSession int `json:"total_downloaded_session"` 57 | TotalSize int `json:"total_size"` 58 | TotalUploaded int `json:"total_uploaded"` 59 | TotalUploadedSession 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 | //Tracker holds a tracker object from qbittorrent 67 | type Tracker struct { 68 | Msg string `json:"msg"` 69 | NumPeers int `json:"num_peers"` 70 | Status string `json:"status"` 71 | URL string `json:"url"` 72 | } 73 | 74 | //WebSeed holds a webseed object from qbittorrent 75 | type WebSeed struct { 76 | URL string `json:"url"` 77 | } 78 | 79 | //TorrentFile holds a torrent file object from qbittorrent 80 | type TorrentFile struct { 81 | IsSeed bool `json:"is_seed"` 82 | Name string `json:"name"` 83 | Priority int `json:"priority"` 84 | Progress int `json:"progress"` 85 | Size int `json:"size"` 86 | } 87 | 88 | //Sync holds the sync response struct which contains 89 | //the server state and a map of infohashes to Torrents 90 | type Sync struct { 91 | Categories []string `json:"categories"` 92 | FullUpdate bool `json:"full_update"` 93 | Rid int `json:"rid"` 94 | ServerState struct { 95 | ConnectionStatus string `json:"connection_status"` 96 | DhtNodes int `json:"dht_nodes"` 97 | DlInfoData int `json:"dl_info_data"` 98 | DlInfoSpeed int `json:"dl_info_speed"` 99 | DlRateLimit int `json:"dl_rate_limit"` 100 | Queueing bool `json:"queueing"` 101 | RefreshInterval int `json:"refresh_interval"` 102 | UpInfoData int `json:"up_info_data"` 103 | UpInfoSpeed int `json:"up_info_speed"` 104 | UpRateLimit int `json:"up_rate_limit"` 105 | UseAltSpeedLimits bool `json:"use_alt_speed_limits"` 106 | } `json:"server_state"` 107 | Torrents map[string]Torrent `json:"torrents"` 108 | } 109 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | ================== 2 | go-qbittorrent 3 | ================== 4 | 5 | Golang wrapper for qBittorrent Web API (for versions above v3.1.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 (v3.3.1 when writing). 12 | 13 | It'll be best if you upgrade your client to a 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 := map[string]string{ 59 | "filter": "downloading", 60 | "category": "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 := map[string]string{ 67 | "filter": paused, 68 | "sort": 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 := map[string]string{} 82 | magnetLink = "magnet:?xt=urn:btih:e334ab9ddd91c10938a7....." 83 | qb.DownloadFromLink(magnetLink, options) 84 | 85 | // Will return response object with `200:OK` status code 86 | // regardless of sucess of failure. 87 | 88 | - Download multipe torrents by looping over links:: 89 | .. code-block:: go 90 | 91 | options := map[string]string{} 92 | links := [...]string{link1, link2, link3} 93 | for l := range links{ 94 | qb.DownloadFromLink(l, options) 95 | } 96 | 97 | - Downloading torrents by file:: 98 | .. code-block:: go 99 | 100 | options := map[string]string{} 101 | file = "path/to/file.torrent" 102 | qb.DownloadFromFile(file, options) 103 | 104 | - Downloading multiple torrents by using files:: 105 | .. code-block:: go 106 | 107 | options := map[string]string{} 108 | file = [...]string{path/to/file1, path/to/file2, path/to/file3} 109 | qb.DownloadFromFile(file, options) 110 | 111 | - Specifing save path for downloads:: 112 | .. code-block:: go 113 | 114 | savePath = "/home/user/Downloads/special-dir/" 115 | options := map[string]string{ 116 | "savepath": savePath 117 | } 118 | file = "path/to/file.torrent" 119 | qb.DownloadFromFile(file, options) 120 | 121 | // same for links. 122 | savePath = "/home/user/Downloads/special-dir/" 123 | options := map[string]string{ 124 | "savepath": savePath 125 | } 126 | magnetLink = "magnet:?xt=urn:btih:e334ab9ddd91c10938a7....." 127 | qb.DownloadFromLink(magnetLink, options) 128 | 129 | - Applying labels to downloads:: 130 | .. code-block:: go 131 | 132 | label = "secret-files ;)" 133 | options := map[string]string{ 134 | "label": label 135 | } 136 | file = "path/to/file.torrent" 137 | qb.DownloadFromFile(file, options) 138 | 139 | // same for links. 140 | category = "anime" 141 | options := map[string]string{ 142 | "label": label 143 | } 144 | magnetLink = "magnet:?xt=urn:btih:e334ab9ddd91c10938a7....." 145 | qb.DownloadFromLink(magnetLink, options) 146 | 147 | Pause / Resume torrents 148 | ----------------------- 149 | 150 | - Pausing/ Resuming all torrents:: 151 | .. code-block:: go 152 | 153 | qb.PauseAll() 154 | qb.ResumeAll() 155 | 156 | - Pausing/ Resuming a specific torrent:: 157 | .. code-block:: go 158 | 159 | infoHash = "e334ab9ddd....infohash....5d7fff526cb4" 160 | qb.Pause(infoHash) 161 | qb.Resume(infoHash) 162 | 163 | - Pausing/ Resuming multiple torrents:: 164 | .. code-block:: go 165 | 166 | infoHashes = [...]string{ 167 | "e334ab9ddd9......infohash......fff526cb4", 168 | "c9dc36f46d9......infohash......90ebebc46", 169 | "4c859243615......infohash......8b1f20108", 170 | } 171 | 172 | qb.PauseMultiple(infoHashes) 173 | qb.ResumeMultiple(infoHashes) 174 | 175 | 176 | Full API method documentation 177 | ============================= 178 | 179 | All API methods of qBittorrent are mentioned in docs.txt 180 | 181 | Authors 182 | ======= 183 | 184 | Maintainer 185 | ---------- 186 | 187 | - `Jared Mosley (jaredlmosley) `__ 188 | 189 | Contributors 190 | ------------ 191 | 192 | - Your name here :) 193 | 194 | TODO 195 | ==== 196 | 197 | - Write tests 198 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /qbt/api.go: -------------------------------------------------------------------------------- 1 | package qbt 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "errors" 7 | "io" 8 | "mime/multipart" 9 | "net/http" 10 | "net/http/cookiejar" 11 | "os" 12 | "path" 13 | 14 | "net/url" 15 | "strconv" 16 | "strings" 17 | 18 | wrapper "github.com/pkg/errors" 19 | 20 | "golang.org/x/net/publicsuffix" 21 | ) 22 | 23 | //ErrBadPriority means the priority is not allowd by qbittorrent 24 | var ErrBadPriority = errors.New("priority not available") 25 | 26 | //ErrBadResponse means that qbittorrent sent back an unexpected response 27 | var ErrBadResponse = errors.New("received bad response") 28 | 29 | //Client creates a connection to qbittorrent and performs requests 30 | type Client struct { 31 | http *http.Client 32 | URL string 33 | Authenticated bool 34 | Jar http.CookieJar 35 | } 36 | 37 | //NewClient creates a new client connection to qbittorrent 38 | func NewClient(url string) *Client { 39 | client := &Client{} 40 | 41 | // ensure url ends with "/" 42 | if url[len(url)-1:] != "/" { 43 | url += "/" 44 | } 45 | 46 | client.URL = url 47 | 48 | // create cookie jar 49 | client.Jar, _ = cookiejar.New(&cookiejar.Options{PublicSuffixList: publicsuffix.List}) 50 | client.http = &http.Client{ 51 | Jar: client.Jar, 52 | } 53 | return client 54 | } 55 | 56 | //get will perform a GET request with no parameters 57 | func (client *Client) get(endpoint string, opts map[string]string) (*http.Response, error) { 58 | req, err := http.NewRequest("GET", client.URL+endpoint, nil) 59 | if err != nil { 60 | return nil, wrapper.Wrap(err, "failed to build request") 61 | } 62 | 63 | // add user-agent header to allow qbittorrent to identify us 64 | req.Header.Set("User-Agent", "go-qbittorrent v0.1") 65 | 66 | // add optional parameters that the user wants 67 | if opts != nil { 68 | query := req.URL.Query() 69 | for k, v := range opts { 70 | query.Add(k, v) 71 | } 72 | req.URL.RawQuery = query.Encode() 73 | } 74 | 75 | resp, err := client.http.Do(req) 76 | if err != nil { 77 | return nil, wrapper.Wrap(err, "failed to perform request") 78 | } 79 | 80 | return resp, nil 81 | } 82 | 83 | //post will perform a POST request with no content-type specified 84 | func (client *Client) post(endpoint string, opts map[string]string) (*http.Response, error) { 85 | req, err := http.NewRequest("POST", client.URL+endpoint, nil) 86 | if err != nil { 87 | return nil, wrapper.Wrap(err, "failed to build request") 88 | } 89 | 90 | // add the content-type so qbittorrent knows what to expect 91 | req.Header.Set("Content-Type", "application/x-www-form-urlencoded") 92 | // add user-agent header to allow qbittorrent to identify us 93 | req.Header.Set("User-Agent", "go-qbittorrent v0.1") 94 | 95 | // add optional parameters that the user wants 96 | if opts != nil { 97 | form := url.Values{} 98 | for k, v := range opts { 99 | form.Add(k, v) 100 | } 101 | req.PostForm = form 102 | } 103 | 104 | resp, err := client.http.Do(req) 105 | if err != nil { 106 | return nil, wrapper.Wrap(err, "failed to perform request") 107 | } 108 | 109 | return resp, nil 110 | 111 | } 112 | 113 | //postMultipart will perform a multiple part POST request 114 | func (client *Client) postMultipart(endpoint string, buffer bytes.Buffer, contentType string) (*http.Response, error) { 115 | req, err := http.NewRequest("POST", client.URL+endpoint, &buffer) 116 | if err != nil { 117 | return nil, wrapper.Wrap(err, "error creating request") 118 | } 119 | 120 | // add the content-type so qbittorrent knows what to expect 121 | req.Header.Set("Content-Type", contentType) 122 | // add user-agent header to allow qbittorrent to identify us 123 | req.Header.Set("User-Agent", "go-qbittorrent v0.1") 124 | 125 | resp, err := client.http.Do(req) 126 | if err != nil { 127 | return nil, wrapper.Wrap(err, "failed to perform request") 128 | } 129 | 130 | return resp, nil 131 | } 132 | 133 | //writeOptions will write a map to the buffer through multipart.NewWriter 134 | func writeOptions(writer *multipart.Writer, opts map[string]string) { 135 | for key, val := range opts { 136 | writer.WriteField(key, val) 137 | } 138 | } 139 | 140 | //postMultipartData will perform a multiple part POST request without a file 141 | func (client *Client) postMultipartData(endpoint string, opts map[string]string) (*http.Response, error) { 142 | var buffer bytes.Buffer 143 | writer := multipart.NewWriter(&buffer) 144 | 145 | // write the options to the buffer 146 | // will contain the link string 147 | writeOptions(writer, opts) 148 | 149 | // close the writer before doing request to get closing line on multipart request 150 | if err := writer.Close(); err != nil { 151 | return nil, wrapper.Wrap(err, "failed to close writer") 152 | } 153 | 154 | resp, err := client.postMultipart(endpoint, buffer, writer.FormDataContentType()) 155 | if err != nil { 156 | return nil, err 157 | } 158 | 159 | return resp, nil 160 | } 161 | 162 | //postMultipartFile will perform a multiple part POST request with a file 163 | func (client *Client) postMultipartFile(endpoint string, fileName string, opts map[string]string) (*http.Response, error) { 164 | var buffer bytes.Buffer 165 | writer := multipart.NewWriter(&buffer) 166 | 167 | // open the file for reading 168 | file, err := os.Open(fileName) 169 | if err != nil { 170 | return nil, wrapper.Wrap(err, "error opening file") 171 | } 172 | // defer the closing of the file until the end of function 173 | // so we can still copy its contents 174 | defer file.Close() 175 | 176 | // create form for writing the file to and give it the filename 177 | formWriter, err := writer.CreateFormFile("torrents", path.Base(fileName)) 178 | if err != nil { 179 | return nil, wrapper.Wrap(err, "error adding file") 180 | } 181 | 182 | // write the options to the buffer 183 | writeOptions(writer, opts) 184 | 185 | // copy the file contents into the form 186 | if _, err = io.Copy(formWriter, file); err != nil { 187 | return nil, wrapper.Wrap(err, "error copying file") 188 | } 189 | 190 | // close the writer before doing request to get closing line on multipart request 191 | if err := writer.Close(); err != nil { 192 | return nil, wrapper.Wrap(err, "failed to close writer") 193 | } 194 | 195 | resp, err := client.postMultipart(endpoint, buffer, writer.FormDataContentType()) 196 | if err != nil { 197 | return nil, err 198 | } 199 | 200 | return resp, nil 201 | } 202 | 203 | //Login logs you in to the qbittorrent client 204 | //returns the current authentication status 205 | func (client *Client) Login(username string, password string) (loggedIn bool, err error) { 206 | credentials := make(map[string]string) 207 | credentials["username"] = username 208 | credentials["password"] = password 209 | 210 | resp, err := client.post("login", credentials) 211 | if err != nil { 212 | return false, err 213 | } else if resp.Status != "200 OK" { // check for correct status code 214 | return false, wrapper.Wrap(ErrBadResponse, "couldnt log in") 215 | } 216 | 217 | // change authentication status so we know were authenticated in later requests 218 | client.Authenticated = true 219 | 220 | // add the cookie to cookie jar to authenticate later requests 221 | if cookies := resp.Cookies(); len(cookies) > 0 { 222 | cookieURL, _ := url.Parse("http://localhost:8080") 223 | client.Jar.SetCookies(cookieURL, cookies) 224 | } 225 | 226 | // create a new client with the cookie jar and replace the old one 227 | // so that all our later requests are authenticated 228 | client.http = &http.Client{ 229 | Jar: client.Jar, 230 | } 231 | 232 | return client.Authenticated, nil 233 | } 234 | 235 | //Logout logs you out of the qbittorrent client 236 | //returns the current authentication status 237 | func (client *Client) Logout() (loggedOut bool, err error) { 238 | resp, err := client.get("logout", nil) 239 | if err != nil { 240 | return false, err 241 | } 242 | 243 | // check for correct status code 244 | if resp.Status != "200 OK" { 245 | return false, wrapper.Wrap(ErrBadResponse, "couldnt log in") 246 | } 247 | 248 | // change authentication status so we know were not authenticated in later requests 249 | client.Authenticated = false 250 | 251 | return client.Authenticated, nil 252 | } 253 | 254 | //Shutdown shuts down the qbittorrent client 255 | func (client *Client) Shutdown() (shuttingDown bool, err error) { 256 | resp, err := client.get("command/shutdown", nil) 257 | 258 | // return true if successful 259 | return (resp.Status == "200 OK"), err 260 | } 261 | 262 | //Torrents returns a list of all torrents in qbittorrent matching your filter 263 | func (client *Client) Torrents(filters map[string]string) (torrentList []BasicTorrent, err error) { 264 | var t []BasicTorrent 265 | resp, err := client.get("query/torrents", filters) 266 | if err != nil { 267 | return t, err 268 | } 269 | json.NewDecoder(resp.Body).Decode(&t) 270 | return t, nil 271 | } 272 | 273 | //Torrent returns a specific torrent matching the infoHash 274 | func (client *Client) Torrent(infoHash string) (Torrent, error) { 275 | var t Torrent 276 | resp, err := client.get("query/propertiesGeneral/"+strings.ToLower(infoHash), nil) 277 | if err != nil { 278 | return t, err 279 | } 280 | json.NewDecoder(resp.Body).Decode(&t) 281 | return t, nil 282 | } 283 | 284 | //TorrentTrackers returns all trackers for a specific torrent matching the infoHash 285 | func (client *Client) TorrentTrackers(infoHash string) ([]Tracker, error) { 286 | var t []Tracker 287 | resp, err := client.get("query/propertiesTrackers/"+strings.ToLower(infoHash), nil) 288 | if err != nil { 289 | return t, err 290 | } 291 | json.NewDecoder(resp.Body).Decode(&t) 292 | return t, nil 293 | } 294 | 295 | //TorrentWebSeeds returns seeders for a specific torrent matching the infoHash 296 | func (client *Client) TorrentWebSeeds(infoHash string) ([]WebSeed, error) { 297 | var w []WebSeed 298 | resp, err := client.get("query/propertiesWebSeeds/"+strings.ToLower(infoHash), nil) 299 | if err != nil { 300 | return w, err 301 | } 302 | json.NewDecoder(resp.Body).Decode(&w) 303 | return w, nil 304 | } 305 | 306 | //TorrentFiles gets the files of a specifc torrent matching the infoHash 307 | func (client *Client) TorrentFiles(infoHash string) ([]TorrentFile, error) { 308 | var t []TorrentFile 309 | resp, err := client.get("query/propertiesFiles"+strings.ToLower(infoHash), nil) 310 | if err != nil { 311 | return t, err 312 | } 313 | json.NewDecoder(resp.Body).Decode(&t) 314 | return t, nil 315 | } 316 | 317 | //Sync returns the server state and list of torrents in one struct 318 | func (client *Client) Sync(rid string) (Sync, error) { 319 | var s Sync 320 | 321 | params := make(map[string]string) 322 | params["rid"] = rid 323 | 324 | resp, err := client.get("sync/maindata", params) 325 | if err != nil { 326 | return s, err 327 | } 328 | json.NewDecoder(resp.Body).Decode(&s) 329 | return s, nil 330 | } 331 | 332 | //DownloadFromLink starts downloading a torrent from a link 333 | func (client *Client) DownloadFromLink(link string, options map[string]string) (*http.Response, error) { 334 | options["urls"] = link 335 | return client.postMultipartData("command/download", options) 336 | } 337 | 338 | //DownloadFromFile starts downloading a torrent from a file 339 | func (client *Client) DownloadFromFile(file string, options map[string]string) (*http.Response, error) { 340 | return client.postMultipartFile("command/upload", file, options) 341 | } 342 | 343 | //AddTrackers adds trackers to a specific torrent matching infoHash 344 | func (client *Client) AddTrackers(infoHash string, trackers string) (*http.Response, error) { 345 | params := make(map[string]string) 346 | params["hash"] = strings.ToLower(infoHash) 347 | params["urls"] = trackers 348 | 349 | return client.post("command/addTrackers", params) 350 | } 351 | 352 | //processInfoHashList puts list into a combined (single element) map with all hashes connected with '|' 353 | //this is how the WEBUI API recognizes multiple hashes 354 | func (Client) processInfoHashList(infoHashList []string) (hashMap map[string]string) { 355 | params := map[string]string{} 356 | infoHash := "" 357 | for i, v := range infoHashList { 358 | if i > 0 { 359 | infoHash += "|" + v 360 | } else { 361 | infoHash = v 362 | } 363 | } 364 | params["hashes"] = infoHash 365 | return params 366 | } 367 | 368 | //Pause pauses a specific torrent matching infoHash 369 | func (client *Client) Pause(infoHash string) (*http.Response, error) { 370 | params := make(map[string]string) 371 | params["hash"] = strings.ToLower(infoHash) 372 | 373 | return client.post("command/pause", params) 374 | } 375 | 376 | //PauseAll pauses all torrents 377 | func (client *Client) PauseAll() (*http.Response, error) { 378 | return client.get("command/pauseAll", nil) 379 | } 380 | 381 | //PauseMultiple pauses a list of torrents matching the infoHashes 382 | func (client *Client) PauseMultiple(infoHashList []string) (*http.Response, error) { 383 | params := client.processInfoHashList(infoHashList) 384 | return client.post("command/pauseAll", params) 385 | } 386 | 387 | //SetLabel sets the labels for a list of torrents matching infoHashes 388 | func (client *Client) SetLabel(infoHashList []string, label string) (*http.Response, error) { 389 | params := client.processInfoHashList(infoHashList) 390 | params["label"] = label 391 | 392 | return client.post("command/setLabel", params) 393 | } 394 | 395 | //SetCategory sets the category for a list of torrents matching infoHashes 396 | func (client *Client) SetCategory(infoHashList []string, category string) (*http.Response, error) { 397 | params := client.processInfoHashList(infoHashList) 398 | params["category"] = category 399 | 400 | return client.post("command/setLabel", params) 401 | } 402 | 403 | //Resume resumes a specific torrent matching infoHash 404 | func (client *Client) Resume(infoHash string) (*http.Response, error) { 405 | params := make(map[string]string) 406 | params["hash"] = strings.ToLower(infoHash) 407 | return client.post("command/resume", params) 408 | } 409 | 410 | //ResumeAll resumes all torrents matching infoHashes 411 | func (client *Client) ResumeAll(infoHashList []string) (*http.Response, error) { 412 | return client.get("command/resumeAll", nil) 413 | } 414 | 415 | //ResumeMultiple resumes a list of torrents matching infoHashes 416 | func (client *Client) ResumeMultiple(infoHashList []string) (*http.Response, error) { 417 | params := client.processInfoHashList(infoHashList) 418 | return client.post("command/resumeAll", params) 419 | } 420 | 421 | //DeleteTemp deletes the temporary files for a list of torrents matching infoHashes 422 | func (client *Client) DeleteTemp(infoHashList []string) (*http.Response, error) { 423 | params := client.processInfoHashList(infoHashList) 424 | return client.post("command/delete", params) 425 | } 426 | 427 | //DeletePermanently deletes all files for a list of torrents matching infoHashes 428 | func (client *Client) DeletePermanently(infoHashList []string) (*http.Response, error) { 429 | params := client.processInfoHashList(infoHashList) 430 | return client.post("command/deletePerm", params) 431 | } 432 | 433 | //Recheck rechecks a list of torrents matching infoHashes 434 | func (client *Client) Recheck(infoHashList []string) (*http.Response, error) { 435 | params := client.processInfoHashList(infoHashList) 436 | return client.post("command/recheck", params) 437 | } 438 | 439 | //IncreasePriority increases the priority of a list of torrents matching infoHashes 440 | func (client *Client) IncreasePriority(infoHashList []string) (*http.Response, error) { 441 | params := client.processInfoHashList(infoHashList) 442 | return client.post("command/increasePrio", params) 443 | } 444 | 445 | //DecreasePriority decreases the priority of a list of torrents matching infoHashes 446 | func (client *Client) DecreasePriority(infoHashList []string) (*http.Response, error) { 447 | params := client.processInfoHashList(infoHashList) 448 | return client.post("command/decreasePrio", params) 449 | } 450 | 451 | //SetMaxPriority sets the max priority for a list of torrents matching infoHashes 452 | func (client *Client) SetMaxPriority(infoHashList []string) (*http.Response, error) { 453 | params := client.processInfoHashList(infoHashList) 454 | return client.post("command/topPrio", params) 455 | } 456 | 457 | //SetMinPriority sets the min priority for a list of torrents matching infoHashes 458 | func (client *Client) SetMinPriority(infoHashList []string) (*http.Response, error) { 459 | params := client.processInfoHashList(infoHashList) 460 | return client.post("command/bottomPrio", params) 461 | } 462 | 463 | //SetFilePriority sets the priority for a specific torrent filematching infoHash 464 | func (client *Client) SetFilePriority(infoHash string, fileID string, priority string) (*http.Response, error) { 465 | // disallow certain priorities that are not allowed by the WEBUI API 466 | priorities := [...]string{"0", "1", "2", "7"} 467 | for _, v := range priorities { 468 | if v == priority { 469 | return nil, ErrBadPriority 470 | } 471 | } 472 | 473 | params := make(map[string]string) 474 | params["hash"] = infoHash 475 | params["id"] = fileID 476 | params["priority"] = priority 477 | 478 | return client.post("command/setFilePriority", params) 479 | } 480 | 481 | //GetGlobalDownloadLimit gets the global download limit of your qbittorrent client 482 | func (client *Client) GetGlobalDownloadLimit() (limit int, err error) { 483 | var l int 484 | resp, err := client.get("command/getGlobalDlLimit", nil) 485 | if err != nil { 486 | return l, err 487 | } 488 | json.NewDecoder(resp.Body).Decode(&l) 489 | return l, nil 490 | } 491 | 492 | //SetGlobalDownloadLimit sets the global download limit of your qbittorrent client 493 | func (client *Client) SetGlobalDownloadLimit(limit string) (*http.Response, error) { 494 | params := make(map[string]string) 495 | params["limit"] = limit 496 | return client.post("command/setGlobalDlLimit", params) 497 | } 498 | 499 | //GetGlobalUploadLimit gets the global upload limit of your qbittorrent client 500 | func (client *Client) GetGlobalUploadLimit() (limit int, err error) { 501 | var l int 502 | resp, err := client.get("command/getGlobalUpLimit", nil) 503 | if err != nil { 504 | return l, err 505 | } 506 | json.NewDecoder(resp.Body).Decode(&l) 507 | return l, nil 508 | } 509 | 510 | //SetGlobalUploadLimit sets the global upload limit of your qbittorrent client 511 | func (client *Client) SetGlobalUploadLimit(limit string) (*http.Response, error) { 512 | params := make(map[string]string) 513 | params["limit"] = limit 514 | return client.post("command/setGlobalUpLimit", params) 515 | } 516 | 517 | //GetTorrentDownloadLimit gets the download limit for a list of torrents 518 | func (client *Client) GetTorrentDownloadLimit(infoHashList []string) (limits map[string]string, err error) { 519 | var l map[string]string 520 | params := client.processInfoHashList(infoHashList) 521 | resp, err := client.post("command/getTorrentsDlLimit", params) 522 | if err != nil { 523 | return l, err 524 | } 525 | json.NewDecoder(resp.Body).Decode(&l) 526 | return l, nil 527 | } 528 | 529 | //SetTorrentDownloadLimit sets the download limit for a list of torrents 530 | func (client *Client) SetTorrentDownloadLimit(infoHashList []string, limit string) (*http.Response, error) { 531 | params := client.processInfoHashList(infoHashList) 532 | params["limit"] = limit 533 | return client.post("command/setTorrentsDlLimit", params) 534 | } 535 | 536 | //GetTorrentUploadLimit gets the upload limit for a list of torrents 537 | func (client *Client) GetTorrentUploadLimit(infoHashList []string) (limits map[string]string, err error) { 538 | var l map[string]string 539 | params := client.processInfoHashList(infoHashList) 540 | resp, err := client.post("command/getTorrentsUpLimit", params) 541 | if err != nil { 542 | return l, err 543 | } 544 | json.NewDecoder(resp.Body).Decode(&l) 545 | return l, nil 546 | } 547 | 548 | //SetTorrentUploadLimit sets the upload limit of a list of torrents 549 | func (client *Client) SetTorrentUploadLimit(infoHashList []string, limit string) (*http.Response, error) { 550 | params := client.processInfoHashList(infoHashList) 551 | params["limit"] = limit 552 | return client.post("command/setTorrentsUpLimit", params) 553 | } 554 | 555 | //SetPreferences sets the preferences of your qbittorrent client 556 | func (client *Client) SetPreferences(params map[string]string) (*http.Response, error) { 557 | return client.post("command/setPreferences", params) 558 | } 559 | 560 | //GetAlternativeSpeedStatus gets the alternative speed status of your qbittorrent client 561 | func (client *Client) GetAlternativeSpeedStatus() (status bool, err error) { 562 | var s bool 563 | resp, err := client.get("command/alternativeSpeedLimitsEnabled", nil) 564 | if err != nil { 565 | return s, err 566 | } 567 | json.NewDecoder(resp.Body).Decode(&s) 568 | return s, nil 569 | } 570 | 571 | //ToggleAlternativeSpeed toggles the alternative speed of your qbittorrent client 572 | func (client *Client) ToggleAlternativeSpeed() (*http.Response, error) { 573 | return client.get("command/toggleAlternativeSpeedLimits", nil) 574 | } 575 | 576 | //ToggleSequentialDownload toggles the download sequence of a list of torrents 577 | func (client *Client) ToggleSequentialDownload(infoHashList []string) (*http.Response, error) { 578 | params := client.processInfoHashList(infoHashList) 579 | return client.post("command/toggleSequentialDownload", params) 580 | } 581 | 582 | //ToggleFirstLastPiecePriority toggles first last piece priority of a list of torrents 583 | func (client *Client) ToggleFirstLastPiecePriority(infoHashList []string) (*http.Response, error) { 584 | params := client.processInfoHashList(infoHashList) 585 | return client.post("command/toggleFirstLastPiecePrio", params) 586 | } 587 | 588 | //ForceStart force starts a list of torrents 589 | func (client *Client) ForceStart(infoHashList []string, value bool) (*http.Response, error) { 590 | params := client.processInfoHashList(infoHashList) 591 | params["value"] = strconv.FormatBool(value) 592 | return client.post("command/setForceStart", params) 593 | } 594 | -------------------------------------------------------------------------------- /.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 | --------------------------------------------------------------------------------