├── .circleci └── config.yml ├── .gitignore ├── LICENSE.txt ├── README.md ├── qBT ├── main.go └── structs.go ├── reflection.service ├── reflection ├── cache.go ├── main.go ├── main_test.go ├── metainfo.go ├── testdata │ ├── expected_response.json │ ├── sync_1.json │ ├── sync_2.json │ ├── sync_3.json │ ├── sync_4.json │ ├── sync_5.json │ ├── sync_initial.json │ ├── test_data_fetcher.sh │ ├── torrent_1_files.json │ ├── torrent_1_peers.json │ ├── torrent_1_piecestates.json │ ├── torrent_1_properties.json │ ├── torrent_1_trackers.json │ ├── torrent_2_files.json │ ├── torrent_2_list.json │ ├── torrent_2_peers.json │ ├── torrent_2_piecestates.json │ ├── torrent_2_properties.json │ ├── torrent_2_trackers.json │ ├── torrent_3_files.json │ ├── torrent_3_list.json │ ├── torrent_3_peers.json │ ├── torrent_3_piecestates.json │ ├── torrent_3_properties.json │ ├── torrent_3_trackers.json │ └── torrent_list.json └── utils.go └── transmission ├── structs.go └── templates.go /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | version: 2 # use CircleCI 2.0 2 | jobs: # basic units of work in a run 3 | build: # runs not using Workflows must have a `build` job as entry point 4 | docker: # run the steps with Docker 5 | # CircleCI Go images available at: https://hub.docker.com/r/circleci/golang/ 6 | - image: circleci/golang:1.12 7 | 8 | working_directory: /go/src/github.com/h31/Reflection 9 | 10 | parallelism: 2 11 | 12 | environment: # environment variables for the build itself 13 | TEST_RESULTS: /tmp/test-results # path to where test results will be saved 14 | 15 | steps: # steps that comprise the `build` job 16 | - checkout # check out source code to working directory 17 | - run: mkdir -p $TEST_RESULTS # create the test results directory 18 | 19 | - restore_cache: # restores saved cache if no changes are detected since last run 20 | keys: 21 | - go-mod-v4-{{ checksum "go.sum" }} 22 | 23 | - run: go get -v -t github.com/h31/Reflection/reflection 24 | 25 | - run: 26 | name: Run unit tests 27 | 28 | # store the results of our tests in the $TEST_RESULTS directory 29 | command: | 30 | gotestsum --junitfile ${TEST_RESULTS}/gotestsum-report.xml -- github.com/h31/Reflection/reflection 31 | 32 | - save_cache: 33 | key: go-mod-v4-{{ checksum "go.sum" }} 34 | paths: 35 | - "/go/pkg/mod" 36 | 37 | - store_artifacts: # upload test summary for display in Artifacts 38 | path: /tmp/test-results 39 | destination: raw-test-output 40 | 41 | - store_test_results: # upload test results for display in Test Summary 42 | path: /tmp/test-results 43 | workflows: 44 | version: 2 45 | build-workflow: 46 | jobs: 47 | - build -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ### Go template 2 | # Compiled Object files, Static and Dynamic libs (Shared Objects) 3 | *.o 4 | *.a 5 | *.so 6 | 7 | # Folders 8 | _obj 9 | _test 10 | 11 | # Architecture specific extensions/prefixes 12 | *.[568vq] 13 | [568vq].out 14 | 15 | *.cgo1.go 16 | *.cgo2.c 17 | _cgo_defun.c 18 | _cgo_gotypes.go 19 | _cgo_export.* 20 | 21 | _testmain.go 22 | 23 | *.exe 24 | *.test 25 | *.prof 26 | ### JetBrains template 27 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio 28 | 29 | *.iml 30 | 31 | ## Directory-based project format: 32 | .idea/ 33 | # if you remove the above rule, at least ignore the following: 34 | 35 | # User-specific stuff: 36 | # .idea/workspace.xml 37 | # .idea/tasks.xml 38 | # .idea/dictionaries 39 | 40 | # Sensitive or high-churn files: 41 | # .idea/dataSources.ids 42 | # .idea/dataSources.xml 43 | # .idea/sqlDataSources.xml 44 | # .idea/dynamic.xml 45 | # .idea/uiDesigner.xml 46 | 47 | # Gradle: 48 | # .idea/gradle.xml 49 | # .idea/libraries 50 | 51 | # Mongo Explorer plugin: 52 | # .idea/mongoSettings.xml 53 | 54 | ## File-based project format: 55 | *.ipr 56 | *.iws 57 | 58 | ## Plugin-specific files: 59 | 60 | # IntelliJ 61 | /out/ 62 | 63 | # mpeltonen/sbt-idea plugin 64 | .idea_modules/ 65 | 66 | # JIRA plugin 67 | atlassian-ide-plugin.xml 68 | 69 | # Crashlytics plugin (for Android Studio and IntelliJ) 70 | com_crashlytics_export_strings.xml 71 | crashlytics.properties 72 | crashlytics-build.properties 73 | 74 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Artyom Aleksyuk 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Reflection 2 | 3 | Reflection allows to use Transmission remote controls with qBittorrent. 4 | It acts as a bridge between Transmission RPC and qBittorrent WebUI API. 5 | 6 | Currently Reflection is able to: 7 | * Show torrent list 8 | * Show torrent details (files, trackers, statistics) 9 | * Start, stop, delete torrents 10 | * Add new torrents from a file, a magnet link of an HTTP/HTTPS link 11 | * Choose which files should be downloaded 12 | * Change destination directory 13 | * Show actual free space 14 | * Show peer table 15 | 16 | ## Installation 17 | ### Linux 18 | ``` 19 | # replace amd64 with your architecture: amd64, i386, armv7, arm64 20 | curl -L https://github.com/h31/Reflection/releases/download/v1.0-rc1/reflection_linux_amd64.gz | gunzip | sudo dd of=/usr/local/bin/reflection 21 | sudo chmod +x /usr/local/bin/reflection 22 | curl -L https://raw.githubusercontent.com/h31/Reflection/v1.0-rc1/reflection.service | sudo dd of=/etc/systemd/system/reflection.service 23 | sudo systemctl start reflection.service 24 | sudo systemctl enable reflection.service 25 | ``` 26 | ### MacOS 27 | Untested 28 | ``` 29 | curl -L https://github.com/h31/Reflection/releases/download/v1.0-rc1/reflection_macos_amd64.gz | gunzip | sudo dd of=/usr/local/bin/reflection 30 | sudo chmod +x /usr/local/bin/reflection 31 | ``` 32 | ### Windows 33 | [64 bit .zip](https://github.com/h31/Reflection/releases/download/v1.0-rc1/reflection_windows_64.zip) 34 | 35 | [32 bit .zip](https://github.com/h31/Reflection/releases/download/v1.0-rc1/reflection_windows_32.zip) 36 | 37 | ## Compatibility 38 | * Reflection emulates the latest version of Transmission (2.94) 39 | * Requires at least qBittorrent 4.1.0. 40 | * Tested against Transmission Remote GUI, built-in Transmission Web UI, Torrnado client for Android, Transmission-Qt and Transmission Remote by Yury Polek. Please fill an issue if you experience an incompatibility with any client. 41 | 42 | What features are not supported yet: 43 | * Setting torrent properties (download/upload speed, etc) 44 | * Showing and changing torrent client settings 45 | 46 | ## qBittorrent and Transmission-specific options 47 | 48 | Please note that both qBittorrent and Transmission have some unique features. 49 | For example, some torrent properties such as a private flag are not exposed by qBittorrent. 50 | In case Transmission clients request such information, Reflection responds with predefined template data. Template values are stored in [transmission/templates.go](https://github.com/h31/Reflection/blob/master/transmission/templates.go). 51 | 52 | To enable some qBittorrent-specific options, use a "Specify torrent location" command in your Transmission GUI 53 | and append a special flag to the path. You can also specify such paths when adding a new torrent. 54 | For example, if download directory is `/home/user/`, those paths can be used: 55 | * `/home/user/+s` to enable sequential download 56 | * `/home/user/+f` to download first and last pieces first 57 | * `/home/user/+h` to skip hash checking when adding torrent. 58 | 59 | It is possible to combine several commands, i.e. `/home/user/+sf`. Use `-` sign instead of `+` to disable an option. 60 | If your want to disable command processing and treat a path just as a path, end it with `/`, i.e. `/home/user/my+path+s/`. 61 | 62 | Reflection applies some optimizations to make things smoother: 63 | * Both qBittorent and Transmission have an ability to transmit not the whole torrents list but just the recently changed information. 64 | They do it in a slightly different way, but Reflection tries to compensate differences. 65 | Use `-sync=false` command line flag to disable this optimization and make Reflection request the whole torrents list each time. 66 | * Due to the way the qBittorrent's API was designed, some requests can be quite slow. 67 | Reflection caches those requests, so things work noticeably faster. Most of the time you won't notice an existence of the cache. 68 | By default, the cache timeout is set to 15 seconds. Use `-cache-timeout seconds` to tune the timeout. If set to 0, the cache is disabled. 69 | 70 | ## Usage: 71 | 72 | ```bash 73 | mkdir Reflection 74 | cd Reflection/ 75 | export GOPATH=$(pwd) 76 | go get github.com/h31/Reflection/reflection 77 | ./bin/reflection 78 | ``` 79 | 80 | Use a `--help` flag to show settings. Default qBittorrent address is `http://localhost:8080/`. 81 | -------------------------------------------------------------------------------- /qBT/main.go: -------------------------------------------------------------------------------- 1 | package qBT 2 | 3 | import ( 4 | "encoding/json" 5 | "github.com/iancoleman/orderedmap" 6 | log "github.com/sirupsen/logrus" 7 | "io" 8 | "io/ioutil" 9 | "net/http" 10 | "net/url" 11 | "sort" 12 | "strconv" 13 | "strings" 14 | "sync" 15 | "time" 16 | ) 17 | 18 | func check(e error) { 19 | if e != nil { 20 | panic(e) 21 | } 22 | } 23 | 24 | func checkAndLog(e error, payload []byte) { 25 | if e != nil { 26 | tmpfile, _ := ioutil.TempFile("", "reflection") 27 | tmpfile.Write(payload) 28 | log.WithField("filename", tmpfile.Name()).Error("Saved payload in file") 29 | tmpfile.Close() 30 | 31 | panic(e) 32 | } 33 | } 34 | 35 | type Auth struct { 36 | LoggedIn bool 37 | Cookie http.Cookie 38 | } 39 | 40 | type Hash string 41 | 42 | type ID int 43 | 44 | const ( 45 | INVALID_ID ID = -1 46 | ) 47 | 48 | var RECENTLY_ACTIVE_TIMEOUT = 60 * time.Second 49 | 50 | type Connection struct { 51 | addr *url.URL 52 | client *http.Client 53 | auth Auth 54 | TorrentsList TorrentsList 55 | } 56 | 57 | type TorrentsList struct { 58 | useSync bool 59 | items map[Hash]*TorrentInfo 60 | activity map[Hash]*time.Time 61 | deleted map[ID]*time.Time 62 | rid int 63 | hashIds map[ID]Hash 64 | lastIndex ID 65 | mutex sync.RWMutex 66 | } 67 | 68 | type TorrentInfoList []*TorrentInfo 69 | 70 | func (torrents TorrentInfoList) Len() int { 71 | return len(torrents) 72 | } 73 | func (torrents TorrentInfoList) Swap(i, j int) { 74 | torrents[i], torrents[j] = torrents[j], torrents[i] 75 | } 76 | 77 | func (torrents TorrentInfoList) Less(i, j int) bool { 78 | return torrents[i].Id < torrents[j].Id 79 | } 80 | 81 | func (torrents TorrentInfoList) ConcatenateHashes() string { 82 | hashesStrings := make([]string, len(torrents)) 83 | for i, torrent := range torrents { 84 | hashesStrings[i] = string(torrent.Hash) 85 | } 86 | return strings.Join(hashesStrings, "|") 87 | } 88 | 89 | func (torrents TorrentInfoList) Hashes() []Hash { 90 | hashesStrings := make([]Hash, len(torrents)) 91 | for i, torrent := range torrents { 92 | hashesStrings[i] = torrent.Hash 93 | } 94 | return hashesStrings 95 | } 96 | 97 | func (list *TorrentsList) AllItems() map[Hash]*TorrentInfo { 98 | return list.items 99 | } 100 | 101 | func (list *TorrentsList) Slice() TorrentInfoList { 102 | list.mutex.RLock() 103 | defer list.mutex.RUnlock() 104 | 105 | result := make(TorrentInfoList, 0, len(list.items)) 106 | for _, item := range list.items { 107 | result = append(result, item) 108 | } 109 | sort.Sort(result) 110 | return result 111 | } 112 | 113 | func (list *TorrentsList) GetActive() (resp TorrentInfoList) { 114 | list.mutex.RLock() 115 | defer list.mutex.RUnlock() 116 | 117 | for _, item := range list.items { 118 | activity := list.activity[item.Hash] 119 | if activity != nil && time.Since(*activity) < RECENTLY_ACTIVE_TIMEOUT { 120 | resp = append(resp, item) 121 | } 122 | } 123 | sort.Sort(resp) 124 | return 125 | } 126 | 127 | func (list *TorrentsList) GetRemoved() (removed []ID) { 128 | list.mutex.RLock() 129 | defer list.mutex.RUnlock() 130 | 131 | removed = make([]ID, 0, len(list.deleted)) 132 | for id, additionTime := range list.deleted { 133 | if additionTime != nil && time.Since(*additionTime) < RECENTLY_ACTIVE_TIMEOUT { 134 | removed = append(removed, id) 135 | } 136 | } 137 | list.maintainListOfDeleted(nil) 138 | return 139 | } 140 | 141 | func (list *TorrentsList) ByID(id ID) *TorrentInfo { 142 | list.mutex.RLock() 143 | defer list.mutex.RUnlock() 144 | if hash, ok := list.hashIds[id]; ok { 145 | return list.items[hash] 146 | } else { 147 | return nil 148 | } 149 | } 150 | 151 | func (list *TorrentsList) ByHash(hash Hash) *TorrentInfo { 152 | list.mutex.RLock() 153 | defer list.mutex.RUnlock() 154 | if item, ok := list.items[hash]; ok { 155 | return item 156 | } else { 157 | return nil 158 | } 159 | } 160 | 161 | func (list *TorrentsList) ItemsNum() int { 162 | list.mutex.RLock() 163 | defer list.mutex.RUnlock() 164 | return len(list.hashIds) 165 | } 166 | 167 | func (q *Connection) Init(baseUrl string, client *http.Client, useSync bool) { 168 | q.TorrentsList.items = make(map[Hash]*TorrentInfo, 0) 169 | q.TorrentsList.activity = make(map[Hash]*time.Time) 170 | q.TorrentsList.deleted = make(map[ID]*time.Time) 171 | q.TorrentsList.hashIds = make(map[ID]Hash) 172 | q.TorrentsList.useSync = useSync 173 | q.TorrentsList.rid = 0 174 | q.auth.LoggedIn = false 175 | 176 | apiAddr, _ := url.Parse("api/v2/") 177 | parsedBaseAddr, _ := url.Parse(baseUrl) 178 | q.addr = parsedBaseAddr.ResolveReference(apiAddr) 179 | q.client = client 180 | } 181 | 182 | func (q *Connection) IsLoggedIn() bool { 183 | return q.auth.LoggedIn 184 | } 185 | 186 | func (q *Connection) MakeRequestURLWithParam(path string, params map[string]string) string { 187 | if strings.HasPrefix(path, "/") { 188 | panic("Invalid API path: " + path) 189 | } 190 | parsedPath, err := url.Parse(path) 191 | check(err) 192 | u := q.addr.ResolveReference(parsedPath) 193 | if len(params) > 0 { 194 | query := u.Query() 195 | for key, value := range params { 196 | query.Set(key, value) 197 | } 198 | u.RawQuery = query.Encode() 199 | } 200 | 201 | return u.String() 202 | } 203 | 204 | func (q *Connection) MakeRequestURL(path string) string { 205 | return q.MakeRequestURLWithParam(path, map[string]string{}) 206 | } 207 | 208 | func (q *Connection) UpdateTorrentListDirectly() TorrentInfoList { 209 | torrents := make(TorrentInfoList, 0) 210 | 211 | params := map[string]string{} 212 | url := q.MakeRequestURLWithParam("torrents/info", params) 213 | torrentsJSON := q.DoGET(url) 214 | 215 | err := json.Unmarshal(torrentsJSON, &torrents) 216 | checkAndLog(err, torrentsJSON) 217 | 218 | q.TorrentsList.items = make(map[Hash]*TorrentInfo) 219 | for _, torrent := range torrents { 220 | q.TorrentsList.items[torrent.Hash] = torrent 221 | torrent.Id = INVALID_ID 222 | } 223 | return torrents 224 | } 225 | 226 | func (q *Connection) UpdateCachedTorrentsList() (added, deleted TorrentInfoList) { 227 | torrentsList := &q.TorrentsList 228 | url := q.MakeRequestURLWithParam("sync/maindata", map[string]string{"rid": strconv.Itoa(torrentsList.rid)}) 229 | mainData := q.DoGET(url) 230 | 231 | mainDataCache := MainData{} 232 | 233 | err := json.Unmarshal(mainData, &mainDataCache) 234 | checkAndLog(err, mainData) 235 | 236 | torrentsList.rid = mainDataCache.Rid 237 | now := time.Now() 238 | for _, deletedHash := range mainDataCache.Torrents_removed { 239 | deleted = append(deleted, torrentsList.items[deletedHash]) 240 | delete(torrentsList.items, deletedHash) 241 | } 242 | 243 | if mainDataCache.Torrents != nil { 244 | orderedTorrentsMap := orderedmap.New() 245 | 246 | err = json.Unmarshal(*mainDataCache.Torrents, &orderedTorrentsMap) 247 | checkAndLog(err, *mainDataCache.Torrents) 248 | 249 | nativeTorrentsMap := make(map[Hash]*json.RawMessage) 250 | 251 | err = json.Unmarshal(*mainDataCache.Torrents, &nativeTorrentsMap) 252 | checkAndLog(err, *mainDataCache.Torrents) 253 | 254 | for _, hashString := range orderedTorrentsMap.Keys() { 255 | hash := Hash(hashString) 256 | torrent, exists := torrentsList.items[hash] 257 | if !exists { 258 | torrent = &TorrentInfo{Id: INVALID_ID} 259 | torrentsList.items[hash] = torrent 260 | added = append(added, torrent) 261 | } 262 | err := json.Unmarshal(*nativeTorrentsMap[hash], torrent) 263 | checkAndLog(err, mainData) 264 | torrent.Hash = hash 265 | torrentsList.activity[hash] = &now 266 | } 267 | } 268 | 269 | return 270 | } 271 | 272 | func (q *Connection) UpdateTorrentsList() { 273 | q.TorrentsList.mutex.Lock() 274 | defer q.TorrentsList.mutex.Unlock() 275 | 276 | if q.TorrentsList.useSync { 277 | added, deleted := q.UpdateCachedTorrentsList() 278 | q.TorrentsList.DeleteIDsSync(deleted) 279 | q.TorrentsList.UpdateIDs(added) 280 | } else { 281 | added := q.UpdateTorrentListDirectly() 282 | q.TorrentsList.DeleteIDsFullRescan() 283 | q.TorrentsList.UpdateIDs(added) 284 | } 285 | } 286 | 287 | func (q *Connection) AddNewCategory(category string) { 288 | url := q.MakeRequestURLWithParam("torrents/createCategory", map[string]string{"category": category}) 289 | q.DoGET(url) 290 | } 291 | 292 | func (q *Connection) GetPropsGeneral(hash Hash) (propGeneral PropertiesGeneral) { 293 | propGeneralURL := q.MakeRequestURLWithParam("torrents/properties", map[string]string{"hash": string(hash)}) 294 | propGeneralRaw := q.DoGET(propGeneralURL) 295 | 296 | err := json.Unmarshal(propGeneralRaw, &propGeneral) 297 | checkAndLog(err, propGeneralRaw) 298 | return 299 | } 300 | 301 | func (q *Connection) GetPropsTrackers(hash Hash) (trackers []PropertiesTrackers) { 302 | trackersURL := q.MakeRequestURLWithParam("torrents/trackers", map[string]string{"hash": string(hash)}) 303 | trackersRaw := q.DoGET(trackersURL) 304 | 305 | err := json.Unmarshal(trackersRaw, &trackers) 306 | 307 | checkAndLog(err, trackersRaw) 308 | return 309 | } 310 | 311 | func (q *Connection) GetPiecesStates(hash Hash) (pieces []byte) { 312 | piecesURL := q.MakeRequestURLWithParam("torrents/pieceStates", map[string]string{"hash": string(hash)}) 313 | piecesRaw := q.DoGET(piecesURL) 314 | 315 | err := json.Unmarshal(piecesRaw, &pieces) 316 | 317 | checkAndLog(err, piecesRaw) 318 | return 319 | } 320 | 321 | func (q *Connection) GetPreferences() (pref Preferences) { 322 | prefURL := q.MakeRequestURL("app/preferences") 323 | prefRaw := q.DoGET(prefURL) 324 | 325 | err := json.Unmarshal(prefRaw, &pref) 326 | checkAndLog(err, prefRaw) 327 | return 328 | } 329 | 330 | func (q *Connection) GetTransferInfo() (info TransferInfo) { 331 | infoURL := q.MakeRequestURL("transfer/info") 332 | infoRaw := q.DoGET(infoURL) 333 | 334 | err := json.Unmarshal(infoRaw, &info) 335 | checkAndLog(err, infoRaw) 336 | return 337 | } 338 | 339 | func (q *Connection) GetVersion() string { 340 | versionURL := q.MakeRequestURL("app/version") 341 | return string(q.DoGET(versionURL)) 342 | } 343 | 344 | func (q *Connection) GetPropsFiles(hash Hash) (files []PropertiesFiles) { 345 | filesURL := q.MakeRequestURLWithParam("torrents/files", map[string]string{"hash": string(hash)}) 346 | filesRaw := q.DoGET(filesURL) 347 | 348 | err := json.Unmarshal(filesRaw, &files) 349 | checkAndLog(err, filesRaw) 350 | return 351 | } 352 | 353 | func (q *Connection) DoGET(url string) []byte { 354 | req, err := http.NewRequest("GET", url, nil) 355 | check(err) 356 | req.AddCookie(&q.auth.Cookie) 357 | 358 | resp, err := q.client.Do(req) 359 | check(err) 360 | defer resp.Body.Close() 361 | data, err := ioutil.ReadAll(resp.Body) 362 | return data 363 | } 364 | 365 | func (q *Connection) DoPOST(url string, contentType string, body io.Reader) []byte { 366 | req, err := http.NewRequest("POST", url, body) 367 | check(err) 368 | req.Header.Set("Content-Type", contentType) 369 | req.AddCookie(&q.auth.Cookie) 370 | 371 | resp, err := q.client.Do(req) 372 | check(err) 373 | defer resp.Body.Close() 374 | data, err := ioutil.ReadAll(resp.Body) 375 | return data 376 | } 377 | 378 | func (q *Connection) PostForm(url string, data url.Values) []byte { 379 | return q.DoPOST(url, "application/x-www-form-urlencoded", strings.NewReader(data.Encode())) 380 | } 381 | 382 | func (q *Connection) Login(username, password string) bool { 383 | resp, err := http.PostForm(q.MakeRequestURL("auth/login"), 384 | url.Values{"username": {username}, "password": {password}}) 385 | check(err) 386 | for _, value := range resp.Cookies() { 387 | if value != nil { 388 | cookie := *value 389 | if cookie.Name == "SID" { 390 | q.auth.LoggedIn = true 391 | q.auth.Cookie = cookie 392 | break 393 | } 394 | } 395 | } 396 | return q.auth.LoggedIn 397 | } 398 | 399 | func (q *Connection) PostWithHashes(path string, torrents TorrentInfoList) { 400 | hashes := torrents.ConcatenateHashes() 401 | q.PostForm(q.MakeRequestURL(path), url.Values{"hashes": {hashes}}) 402 | } 403 | 404 | func (q *Connection) SetToggleFlag(path string, hash Hash, newState bool) { 405 | item := q.TorrentsList.ByHash(hash) 406 | if item.Seq_dl != newState { 407 | q.PostForm(q.MakeRequestURL(path), 408 | url.Values{"hashes": {string(hash)}}) 409 | } 410 | return 411 | } 412 | 413 | func (q *Connection) SetSequentialDownload(hash Hash, newState bool) { 414 | q.SetToggleFlag("torrents/toggleSequentialDownload", hash, newState) 415 | } 416 | 417 | func (q *Connection) SetFirstLastPieceFirst(hash Hash, newState bool) { 418 | q.SetToggleFlag("torrents/toggleFirstLastPiecePrio", hash, newState) 419 | } 420 | 421 | func (list *TorrentsList) DeleteIDsSync(deleted TorrentInfoList) { 422 | for _, torrent := range deleted { 423 | if _, exists := list.hashIds[torrent.Id]; exists { 424 | log.WithField("hash", torrent.Hash).WithField("id", torrent.Id).Info("Hash was removed from the torrent list") 425 | delete(list.hashIds, torrent.Id) 426 | list.maintainListOfDeleted(torrent) 427 | } 428 | } 429 | } 430 | 431 | func (list *TorrentsList) DeleteIDsFullRescan() { 432 | for id, hash := range list.hashIds { 433 | if torrent, exists := list.items[hash]; exists { 434 | torrent.Id = id 435 | } else { 436 | log.WithField("hash", hash).WithField("id", id).Info("Hash disappeared from the torrent list") 437 | delete(list.hashIds, id) 438 | list.maintainListOfDeleted(torrent) 439 | } 440 | } 441 | } 442 | 443 | func (list *TorrentsList) maintainListOfDeleted(deletedTorrent *TorrentInfo) { 444 | for id, additionTime := range list.deleted { 445 | if additionTime != nil && time.Since(*additionTime) > RECENTLY_ACTIVE_TIMEOUT { 446 | delete(list.deleted, id) 447 | } 448 | } 449 | 450 | if deletedTorrent != nil { 451 | now := time.Now() 452 | list.deleted[deletedTorrent.Id] = &now 453 | } 454 | } 455 | 456 | func (list *TorrentsList) UpdateIDs(added TorrentInfoList) { 457 | for _, torrent := range added { 458 | if torrent.Id == INVALID_ID { 459 | list.hashIds[list.lastIndex] = torrent.Hash 460 | torrent.Id = list.lastIndex 461 | log.WithField("hash", torrent.Hash).WithField("id", list.lastIndex).Info("Torrent got assigned ID") 462 | list.lastIndex++ 463 | } 464 | } 465 | } 466 | -------------------------------------------------------------------------------- /qBT/structs.go: -------------------------------------------------------------------------------- 1 | package qBT 2 | 3 | import ( 4 | "encoding/json" 5 | ) 6 | 7 | // https://github.com/qbittorrent/qBittorrent/wiki/WebUI-API-Documentation 8 | 9 | type JsonMap map[string]interface{} 10 | 11 | type TorrentInfo struct { 12 | Id ID // Transmission's ID 13 | Hash Hash // Torrent hash 14 | Name string // Torrent name 15 | Size int64 // Total size (bytes) of files selected for download 16 | Total_size int64 // Torrent total size (bytes) 17 | Progress float64 // Torrent progress (percentage/100) 18 | Dlspeed int // Torrent download speed (bytes/s) 19 | Upspeed int // Torrent upload speed (bytes/s) 20 | Priority int // Torrent priority. Returns -1 if queuing is disabled or torrent is in seed mode 21 | Num_seeds int // Number of seeds connected to 22 | Num_complete int // Number of seeds in the swarm 23 | Num_leechs int // Number of leechers connected to 24 | Num_incomplete int // Number of leechers in the swarm 25 | Ratio float64 // Torrent share ratio. Max ratio value: 9999. 26 | Eta int64 // Torrent ETA (seconds) 27 | State string // Torrent state. See table here below for the possible values 28 | Seq_dl bool // True if sequential download is enabled 29 | F_l_piece_prio bool // True if first last piece are prioritized 30 | Label string // Label of the torrent 31 | Super_seeding bool // True if super seeding is enabled 32 | Force_start bool // True if force start is enabled for this torrent 33 | Save_path string // Torrent save path 34 | Added_on int64 35 | Completion_on int64 // Torrent completion time 36 | } 37 | 38 | type PeerInfo struct { 39 | Up_speed int 40 | Uploaded int64 41 | Dl_speed int 42 | Port int 43 | Downloaded int64 44 | Client string 45 | Country string 46 | Flags string 47 | IP string 48 | Progress float64 // Torrent progress (percentage/100) 49 | } 50 | 51 | type PropertiesGeneral struct { 52 | Save_path string // Torrent save path 53 | Creation_date int64 // Torrent creation date (Unix timestamp) 54 | Piece_size int // Torrent piece size (bytes) 55 | Comment string // Torrent comment 56 | Total_wasted int64 // Total data wasted for torrent (bytes) 57 | Total_uploaded int64 // Total data uploaded for torrent (bytes) 58 | Total_uploaded_session int64 // Total data uploaded this session (bytes) 59 | Total_downloaded int64 // Total data uploaded for torrent (bytes) 60 | Total_downloaded_session int64 // Total data downloaded this session (bytes) 61 | Up_limit int // Torrent upload limit (bytes/s) 62 | Dl_limit int // Torrent download limit (bytes/s) 63 | Time_elapsed int // Torrent elapsed time (seconds) 64 | Seeding_time int // Torrent elapsed time while complete (seconds) 65 | Nb_connections int // Torrent connection count 66 | Nb_connections_limit int // Torrent connection count limit 67 | Share_ratio float64 // Torrent share ratio 68 | Addition_date int64 // When this torrent was added (unix timestamp) 69 | Completion_date int64 // Torrent completion date (unix timestamp) 70 | Created_by string // Torrent creator 71 | Dl_speed_avg int // Torrent average download speed (bytes/second) 72 | Dl_speed int // Torrent download speed (bytes/second) 73 | Eta int64 // Torrent ETA (seconds) 74 | Last_seen int64 // Last seen complete date (unix timestamp) 75 | Peers int // Number of peers connected to 76 | Peers_total int // Number of peers in the swarm 77 | Pieces_have int // Number of pieces owned 78 | Pieces_num int // Number of pieces of the torrent 79 | Reannounce int64 // Number of seconds until the next announce 80 | Seeds int // Number of seeds connected to 81 | Seeds_total int // Number of seeds in the swarm 82 | Total_size int64 // Torrent total size (bytes) 83 | Up_speed_avg int // Torrent average upload speed (bytes/second) 84 | Up_speed int // Torrent upload speed (bytes/second) 85 | } 86 | 87 | type PropertiesTrackers struct { 88 | Url string // Tracker url 89 | Status int // Tracker status. See the table below for possible values 90 | Num_peers int // Number of peers for current torrent reported by the tracker 91 | Msg string // Tracker message (there is no way of knowing what this message is - it's up to tracker admins) 92 | } 93 | 94 | type PropertiesFiles struct { 95 | Name string // File name (including relative path) 96 | Size int64 // File size (bytes) 97 | Progress float64 // File progress (percentage/100) 98 | Priority int // File priority. See possible values here below 99 | Is_seed bool // True if file is seeding/complete 100 | } 101 | 102 | type TransferInfo struct { 103 | Dl_info_speed int // Global download rate (bytes/s) 104 | Dl_info_data int64 // Data downloaded this session (bytes) 105 | Up_info_speed int // Global upload rate (bytes/s) 106 | Up_info_data int64 // Data uploaded this session (bytes) 107 | Dl_rate_limit int // Download rate limit (bytes/s) 108 | Up_rate_limit int // Upload rate limit (bytes/s) 109 | Dht_nodes int // DHT nodes connected to 110 | Connection_status string // Connection status. See possible values here below 111 | } 112 | 113 | type MainData struct { 114 | Rid int 115 | Full_update bool 116 | Torrents *json.RawMessage 117 | Torrents_removed []Hash 118 | Categories *json.RawMessage 119 | Categories_removed *json.RawMessage 120 | Queueing bool 121 | Server_state *TransferInfo 122 | } 123 | 124 | type Preferences struct { 125 | Locale string // Currently selected language (e.g. en_GB for english) 126 | Save_path string // Default save path for torrents, separated by slashes 127 | Temp_path_enabled bool // True if folder for incomplete torrents is enabled 128 | Temp_path string // Path for incomplete torrents, separated by slashes 129 | Scan_dirs interface{} // List of watch folders to add torrent automatically; slashes are used as path separators; list entries are separated by commas 130 | Download_in_scan_dirs []bool // True if torrents should be downloaded to watch folder; list entries are separated by commas 131 | Export_dir_enabled bool // True if .torrent file should be copied to export directory upon adding 132 | Export_dir string // Path to directory to copy .torrent files ifexport_dir_enabled is enabled; path is separated by slashes 133 | Mail_notification_enabled bool // True if e-mail notification should be enabled 134 | Mail_notification_email string // e-mail to send notifications to 135 | Mail_notification_smtp string // smtp server for e-mail notifications 136 | Mail_notification_ssl_enabled bool // True if smtp server requires SSL connection 137 | Mail_notification_auth_enabled bool // True if smtp server requires authentication 138 | Mail_notification_username string // Username for smtp authentication 139 | Mail_notification_password string // Password for smtp authentication 140 | Autorun_enabled bool // True if external program should be run after torrent has finished downloading 141 | Autorun_program string // Program path/name/arguments to run ifautorun_enabled is enabled; path is separated by slashes; you can use %f and%n arguments, which will be expanded by qBittorent as path_to_torrent_file and torrent_name (from the GUI; not the .torrent file name) respectively 142 | Preallocate_all bool // True if file preallocation should take place, otherwise sparse files are used 143 | Queueing_enabled bool // True if torrent queuing is enabled 144 | Max_active_downloads int // Maximum number of active simultaneous downloads 145 | Max_active_torrents int // Maximum number of active simultaneous downloads and uploads 146 | Max_active_uploads int // Maximum number of active simultaneous uploads 147 | Dont_count_slow_torrents bool // If true torrents w/o any activity (stalled ones) will not be counted towards max_active_*limits; see dont_count_slow_torrents for more information 148 | Max_ratio_enabled bool // True if share ratio limit is enabled 149 | Max_ratio float64 // Get the global share ratio limit 150 | Max_ratio_act int // Action performed when a torrent reaches the maximum share ratio. See list of possible values here below. 151 | Incomplete_files_ext bool // If true .!qB extension will be appended to incomplete files 152 | Listen_port int // Port for incoming connections 153 | Upnp bool // True if UPnP/NAT-PMP is enabled 154 | Random_port bool // True if the port is randomly selected 155 | Dl_limit int // Global download speed limit in B/s; -1means no limit is applied 156 | Up_limit int // Global upload speed limit in B/s; -1means no limit is applied 157 | Max_connec int // Maximum global number of simultaneous connections 158 | Max_connec_per_torrent int // Maximum number of simultaneous connections per torrent 159 | Max_uploads int // Maximum number of upload slots 160 | Max_uploads_per_torrent int // Maximum number of upload slots per torrent 161 | Enable_utp bool // True if uTP protocol should be enabled; this option is only available in qBittorent built against libtorrent version 0.16.X and higher 162 | Limit_utp_rate bool // True if [du]l_limit should be applied to uTP connections; this option is only available in qBittorent built against libtorrent version 0.16.X and higher 163 | Limit_tcp_overhead bool // True if [du]l_limit should be applied to estimated TCP overhead (service data: e.g. packet headers) 164 | Alt_dl_limit int // Alternative global download speed limit in KiB/s 165 | Alt_up_limit int // Alternative global upload speed limit in KiB/s 166 | Scheduler_enabled bool // True if alternative limits should be applied according to schedule 167 | Schedule_from_hour int // Scheduler starting hour 168 | Schedule_from_min int // Scheduler starting minute 169 | Schedule_to_hour int // Scheduler ending hour 170 | Schedule_to_min int // Scheduler ending minute 171 | Scheduler_days int // Scheduler days. See possible values here below 172 | Dht bool // True if DHT is enabled 173 | DhtSameAsBT bool // True if DHT port should match TCP port 174 | Dht_port int // DHT port if dhtSameAsBT is false 175 | Pex bool // True if PeX is enabled 176 | Lsd bool // True if LSD is eanbled 177 | Encryption int // See list of possible values here below 178 | Anonymous_mode bool // If true anonymous mode will be enabled; read more here; this option is only available in qBittorent built against libtorrent version 0.16.X and higher 179 | Proxy_type int // See list of possible values here below 180 | Proxy_ip string // Proxy Address address or domain name 181 | Proxy_port int // Proxy port 182 | Proxy_peer_connections bool // True if peer and web seed connections should be proxified; this option will have any effect only in qBittorent built against libtorrent version 0.16.X and higher 183 | Force_proxy bool // True if the connections not supported by the proxy are disabled 184 | Proxy_auth_enabled bool // True proxy requires authentication; doesn't apply to SOCKS4 proxies 185 | Proxy_username string // Username for proxy authentication 186 | Proxy_password string // Password for proxy authentication 187 | Ip_filter_enabled bool // True if external Address filter should be enabled 188 | Ip_filter_path string // Path to Address filter file (.dat, .p2p, .p2b files are supported); path is separated by slashes 189 | Ip_filter_trackers bool // True if Address filters are applied to trackers 190 | Web_ui_port int // WebUI port 191 | Web_ui_upnp bool // True if UPnP is used for the WebUI port 192 | Web_ui_username string // WebUI username 193 | Web_ui_password string // MD5 hash of WebUI password; hash is generated from the following string:username:Web UI Access:plain_text_web_ui_password 194 | Bypass_local_auth bool // True if auithetication challenge for loopback address (127.0.0.1) should be disabled 195 | Use_https bool // True if WebUI HTTPS access is eanbled 196 | Ssl_key string // SSL keyfile contents (this is a not a path) 197 | Ssl_cert string // SSL certificate contents (this is a not a path) 198 | Dyndns_enabled bool // True if server DNS should be updated dynamically 199 | Dyndns_service int // See list of possible values here below 200 | Dyndns_username string // Username for DDNS service 201 | Dyndns_password string // Password for DDNS service 202 | Dyndns_domain string // Your DDNS domain name 203 | } 204 | -------------------------------------------------------------------------------- /reflection.service: -------------------------------------------------------------------------------- 1 | [Service] 2 | User=root 3 | ExecStart=/usr/local/bin/reflection 4 | 5 | [Install] 6 | WantedBy=multi-user.target 7 | -------------------------------------------------------------------------------- /reflection/cache.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/h31/Reflection/qBT" 5 | log "github.com/sirupsen/logrus" 6 | "sync" 7 | "time" 8 | ) 9 | 10 | type Cache struct { 11 | Timeout time.Duration 12 | Values map[qBT.Hash]JsonMap 13 | FilledAt map[qBT.Hash]time.Time 14 | lock sync.Mutex 15 | } 16 | 17 | func (c *Cache) isStillValid(hash qBT.Hash, timeout time.Duration) (bool, JsonMap) { 18 | value, hasValue := c.Values[hash] 19 | filledAt, hasFillTime := c.FilledAt[hash] 20 | if hasValue && hasFillTime && time.Since(filledAt) < timeout { 21 | return true, value 22 | } else { 23 | return false, nil 24 | } 25 | } 26 | 27 | func (c *Cache) fill(hash qBT.Hash, data JsonMap) { 28 | if c.Values == nil { 29 | c.Values = make(map[qBT.Hash]JsonMap) 30 | } 31 | c.Values[hash] = data 32 | 33 | if c.FilledAt == nil { 34 | c.FilledAt = make(map[qBT.Hash]time.Time) 35 | } 36 | c.FilledAt[hash] = time.Now() 37 | } 38 | 39 | func (c *Cache) GetOrFill(hash qBT.Hash, dest JsonMap, cacheAllowed bool, fillFunc func(dest JsonMap)) { 40 | c.lock.Lock() 41 | defer c.lock.Unlock() 42 | 43 | if isCached, values := c.isStillValid(hash, c.Timeout); cacheAllowed && isCached { 44 | log.WithField("hash", hash).Debug("Got info from cache") 45 | dest.addAll(values) 46 | } else { 47 | log.WithField("hash", hash).Debug("Executing callback to fill the cache") 48 | newValues := make(JsonMap) 49 | fillFunc(newValues) 50 | dest.addAll(newValues) 51 | c.fill(hash, newValues) 52 | } 53 | } 54 | 55 | func (m JsonMap) addAll(source JsonMap) { 56 | for key, value := range source { 57 | m[key] = value 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /reflection/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bytes" 5 | "crypto/sha1" 6 | "encoding/base64" 7 | "encoding/json" 8 | "errors" 9 | "flag" 10 | "fmt" 11 | "github.com/Workiva/go-datastructures/bitarray" 12 | "github.com/h31/Reflection/qBT" 13 | "github.com/h31/Reflection/transmission" 14 | "github.com/ricochet2200/go-disk-usage/du" 15 | log "github.com/sirupsen/logrus" 16 | "io/ioutil" 17 | "math" 18 | "mime/multipart" 19 | "net/http" 20 | "net/url" 21 | "regexp" 22 | "strconv" 23 | "strings" 24 | "time" 25 | "unicode" 26 | ) 27 | 28 | var ( 29 | verbose = flag.Bool("verbose", false, "Enable verbose output") 30 | debug = flag.Bool("debug", false, "Enable debug output") 31 | apiAddr = flag.String("api-addr", "http://localhost:8080/", "qBittorrent API address") 32 | port = flag.Uint("port", 9091, "Transmission RPC port") 33 | cacheTimeout = flag.Uint("cache-timeout", 15, "Cache timeout (in seconds)") 34 | disableKeepAlive = flag.Bool("disable-keep-alive", false, "Disable HTTP Keep-Alive in requests (may be necessary for older qBittorrent versions)") 35 | useSync = flag.Bool("sync", true, "Use Sync endpoint (recommended)") 36 | ) 37 | 38 | func init() { 39 | flag.BoolVar(verbose, "v", false, "") 40 | flag.BoolVar(debug, "d", false, "") 41 | flag.StringVar(apiAddr, "r", "http://localhost:8080/", "") 42 | flag.UintVar(port, "p", 9091, "") 43 | flag.Parse() 44 | } 45 | 46 | var deprecatedFields = map[string]struct{}{ 47 | "announceUrl": {}, 48 | "announceResponse": {}, 49 | "seeders": {}, 50 | "leechers": {}, 51 | "downloadLimitMode": {}, 52 | "uploadLimitMode": {}, 53 | "nextAnnounceTime": {}, 54 | } 55 | 56 | type argumentValue int 57 | 58 | const ( 59 | ARGUMENT_NOT_SET argumentValue = iota 60 | ARGUMENT_TRUE argumentValue = iota 61 | ARGUMENT_FALSE argumentValue = iota 62 | ) 63 | 64 | type additionalArguments struct { 65 | sequentialDownload argumentValue 66 | firstLastPiecesFirst argumentValue 67 | skipChecking argumentValue 68 | } 69 | 70 | var qBTConn qBT.Connection 71 | 72 | func IsFieldDeprecated(field string) bool { 73 | _, ok := deprecatedFields[field] 74 | return ok 75 | } 76 | 77 | //func parseIDsArgument(args *json.RawMessage) []*qBT.TorrentInfo { 78 | // allIds := parseIDsField(args) 79 | // filtered := make([]qBT.ID, 0) 80 | // for _, id := range allIds { 81 | // if qBTConn.TorrentsList.ByID(id) != nil { 82 | // filtered = append(filtered, id) 83 | // } 84 | // } 85 | // return filtered 86 | //} 87 | 88 | func parseIDsField(args *json.RawMessage) qBT.TorrentInfoList { 89 | qBTConn.UpdateTorrentsList() 90 | 91 | if args == nil { 92 | log.Debug("No IDs provided") 93 | return qBTConn.TorrentsList.Slice() 94 | } 95 | 96 | var ids interface{} 97 | err := json.Unmarshal(*args, &ids) 98 | Check(err) 99 | 100 | switch ids := ids.(type) { 101 | case float64: 102 | log.Debug("Query a single ID") 103 | return []*qBT.TorrentInfo{qBTConn.TorrentsList.ByID(qBT.ID(ids))} 104 | case []interface{}: 105 | log.Debug("Query an ID list of length ", len(ids)) 106 | result := make([]*qBT.TorrentInfo, len(ids)) 107 | for i, value := range ids { 108 | switch id := value.(type) { 109 | case float64: 110 | result[i] = qBTConn.TorrentsList.ByID(qBT.ID(id)) 111 | case string: 112 | hash := qBT.Hash(id) 113 | result[i] = qBTConn.TorrentsList.ByHash(hash) 114 | } 115 | if result[i] == nil { 116 | panic("hash not found") 117 | } 118 | } 119 | return result 120 | case string: 121 | if ids != "recently-active" { 122 | panic("Unsupported ID type: " + ids) 123 | } 124 | log.Debug("Query recently-active") 125 | if *useSync { 126 | return qBTConn.TorrentsList.GetActive() 127 | } else { 128 | return qBTConn.TorrentsList.Slice() 129 | } 130 | default: 131 | log.Panicf("Unknown ID type: %s", ids) 132 | panic("Unknown ID type") 133 | } 134 | } 135 | 136 | func parseActionArgument(args json.RawMessage) qBT.TorrentInfoList { 137 | var req struct { 138 | Ids json.RawMessage 139 | } 140 | err := json.Unmarshal(args, &req) 141 | Check(err) 142 | 143 | return parseIDsField(&req.Ids) 144 | } 145 | 146 | func MapTorrentList(dst JsonMap, src *qBT.TorrentInfo) { 147 | for key, value := range transmission.TorrentGetBase { 148 | dst[key] = value 149 | } 150 | dst["hashString"] = src.Hash 151 | convertedName := EscapeString(src.Name) 152 | dst["name"] = convertedName 153 | dst["addedDate"] = src.Added_on 154 | dst["startDate"] = src.Added_on // TODO 155 | dst["doneDate"] = src.Completion_on 156 | dst["sizeWhenDone"] = src.Size 157 | dst["totalSize"] = src.Total_size 158 | dst["downloadDir"] = EscapeString(src.Save_path) 159 | dst["rateDownload"] = src.Dlspeed 160 | dst["rateUpload"] = src.Upspeed 161 | dst["uploadRatio"] = src.Ratio 162 | if src.Eta >= 0 { 163 | dst["eta"] = src.Eta 164 | } else { 165 | dst["eta"] = -1 166 | } 167 | dst["status"] = qBTStateToTransmissionStatus(src.State) 168 | if dst["status"] == TR_STATUS_CHECK { 169 | dst["recheckProgress"] = src.Progress 170 | } else { 171 | dst["recheckProgress"] = 0 172 | } 173 | dst["error"] = qBTStateToTransmissionError(src.State) 174 | dst["isStalled"] = qBTStateToTransmissionStalled(src.State) 175 | dst["percentDone"] = src.Progress 176 | dst["peersGettingFromUs"] = src.Num_leechs 177 | dst["peersSendingToUs"] = src.Num_seeds 178 | dst["leftUntilDone"] = float64(src.Size) * (1 - src.Progress) 179 | dst["desiredAvailable"] = float64(src.Size) * (1 - src.Progress) // TODO 180 | dst["haveUnchecked"] = 0 // TODO 181 | if src.State == "metaDL" { 182 | dst["metadataPercentComplete"] = 0 183 | } else { 184 | dst["metadataPercentComplete"] = 1 185 | } 186 | } 187 | 188 | const TR_STAT_OK = 0 189 | const TR_STATUS_LOCAL_ERROR = 3 190 | 191 | func qBTStateToTransmissionError(state string) int { 192 | if state == "error" || state == "missingFiles" { 193 | return TR_STATUS_LOCAL_ERROR // TR_STAT_LOCAL_ERROR 194 | } else { 195 | return TR_STAT_OK // TR_STAT_OK 196 | } 197 | } 198 | 199 | func qBTStateToTransmissionStalled(state string) bool { 200 | switch state { 201 | case "stalledDL", "stalledUP": 202 | return true 203 | default: 204 | return false 205 | } 206 | } 207 | 208 | const TR_STATUS_STOPPED = 0 209 | const TR_STATUS_CHECK = 2 210 | const TR_STATUS_DOWNLOAD_WAIT = 3 211 | const TR_STATUS_DOWNLOAD = 4 212 | const TR_STATUS_SEED_WAIT = 5 213 | const TR_STATUS_SEED = 6 214 | 215 | func qBTStateToTransmissionStatus(state string) int { 216 | switch state { 217 | case "pausedUP", "pausedDL": 218 | return TR_STATUS_STOPPED // TR_STATUS_STOPPED 219 | case "checkingUP", "checkingDL": 220 | return TR_STATUS_CHECK // TR_STATUS_CHECK 221 | case "queuedDL": 222 | return TR_STATUS_DOWNLOAD_WAIT // TR_STATUS_DOWNLOAD_WAIT 223 | case "downloading", "stalledDL", "forceDL": 224 | return TR_STATUS_DOWNLOAD // TR_STATUS_DOWNLOAD 225 | case "queuedUP": 226 | return TR_STATUS_SEED_WAIT // TR_STATUS_SEED_WAIT 227 | case "uploading", "stalledUP", "forcedUP": 228 | return TR_STATUS_SEED // TR_STATUS_SEED 229 | case "error", "missingFiles": 230 | return TR_STATUS_STOPPED // TR_STATUS_STOPPED 231 | default: 232 | return TR_STATUS_STOPPED // TR_STATUS_STOPPED 233 | } 234 | } 235 | 236 | func MapPieceStates(dst JsonMap, pieces []byte) { 237 | bits := bitarray.NewSparseBitArray() 238 | 239 | for i, value := range pieces { 240 | if value == 2 { 241 | bits.SetBit(uint64(i)) 242 | } 243 | } 244 | 245 | serialized, _ := bitarray.Marshal(bits) 246 | 247 | dst["pieces"] = base64.StdEncoding.EncodeToString(serialized) 248 | } 249 | 250 | func MakePiecesBitArray(total, have int) string { 251 | if (total < 0) || (have < 0) { 252 | return "" // Empty array 253 | } 254 | arrLen := uint(math.Ceil(float64(total) / 8)) 255 | arr := make([]byte, arrLen) 256 | 257 | fullBytes := uint(math.Floor(float64(have) / 8)) 258 | for i := uint(0); i < fullBytes; i++ { 259 | arr[i] = math.MaxUint8 260 | } 261 | for i := uint(0); i < (uint(have) - fullBytes*8); i++ { 262 | arr[fullBytes] |= 128 >> i 263 | } 264 | 265 | return base64.StdEncoding.EncodeToString(arr) 266 | } 267 | 268 | func isMn(r rune) bool { 269 | return unicode.Is(unicode.Mn, r) // Mn: nonspacing marks 270 | } 271 | 272 | type escapedString string 273 | 274 | func (s escapedString) MarshalJSON() ([]byte, error) { 275 | return []byte(strconv.QuoteToASCII(string(s))), nil 276 | } 277 | 278 | func EscapeString(in string) escapedString { 279 | return escapedString(in) 280 | } 281 | 282 | func boolToYesNo(value bool) string { 283 | if value { 284 | return "yes" 285 | } else { 286 | return "no" 287 | } 288 | } 289 | 290 | func addPropertiesToCommentField(dst JsonMap, torrentItem *qBT.TorrentInfo, propGeneral qBT.PropertiesGeneral) { 291 | dst["comment"] = fmt.Sprintf("%s\n"+ 292 | " ----- \n"+ 293 | "Sequential download: %s\n"+ 294 | "First and last pieces first: %s", propGeneral.Comment, 295 | boolToYesNo(torrentItem.Seq_dl), boolToYesNo(torrentItem.F_l_piece_prio)) 296 | } 297 | 298 | func MapPropsGeneral(dst JsonMap, propGeneral qBT.PropertiesGeneral) { 299 | dst["pieceSize"] = propGeneral.Piece_size 300 | dst["pieceCount"] = propGeneral.Pieces_num 301 | dst["addedDate"] = propGeneral.Addition_date 302 | dst["startDate"] = propGeneral.Addition_date // TODO 303 | dst["dateCreated"] = propGeneral.Creation_date 304 | dst["creator"] = propGeneral.Created_by 305 | dst["doneDate"] = propGeneral.Completion_date 306 | dst["totalSize"] = propGeneral.Total_size 307 | dst["haveValid"] = propGeneral.Piece_size * propGeneral.Pieces_have 308 | dst["downloadedEver"] = propGeneral.Total_downloaded 309 | dst["uploadedEver"] = propGeneral.Total_uploaded 310 | dst["peersConnected"] = propGeneral.Peers 311 | dst["peersFrom"] = struct { 312 | fromCache int 313 | fromDht int 314 | fromIncoming int 315 | fromLdp int 316 | fromLtep int 317 | fromPex int 318 | fromTracker int 319 | }{ 320 | fromTracker: propGeneral.Peers, 321 | } 322 | dst["corruptEver"] = propGeneral.Total_wasted 323 | 324 | if propGeneral.Up_limit >= 0 { 325 | dst["uploadLimited"] = true 326 | dst["uploadLimit"] = propGeneral.Up_limit 327 | } else { 328 | dst["uploadLimited"] = false 329 | dst["uploadLimit"] = 0 330 | } 331 | 332 | if propGeneral.Dl_limit >= 0 { 333 | dst["downloadLimited"] = true 334 | dst["downloadLimit"] = propGeneral.Dl_limit // TODO: Kb/s? 335 | } else { 336 | dst["downloadLimited"] = false 337 | dst["downloadLimit"] = 0 338 | } 339 | 340 | dst["maxConnectedPeers"] = propGeneral.Nb_connections_limit 341 | dst["peer-limit"] = propGeneral.Nb_connections_limit // TODO: What's it? 342 | } 343 | 344 | func MapPropsPeers(dst JsonMap, hash qBT.Hash) { 345 | url := qBTConn.MakeRequestURLWithParam("sync/torrentPeers", map[string]string{"hash": string(hash), "rid": "0"}) 346 | torrents := qBTConn.DoGET(url) 347 | 348 | log.Debug(string(torrents)) 349 | var resp struct { 350 | //Peers map[string]qBT.PeerInfo 351 | Peers map[string]qBT.PeerInfo 352 | } 353 | err := json.Unmarshal(torrents, &resp) 354 | Check(err) 355 | //var trPeers []transmission.PeerInfo 356 | trPeers := make([]transmission.PeerInfo, 0) 357 | 358 | for _, peer := range resp.Peers { 359 | clientName := EscapeString(peer.Client) 360 | country := EscapeString(peer.Country) 361 | trPeers = append(trPeers, transmission.PeerInfo{ 362 | RateToPeer: peer.Up_speed, 363 | RateToClient: peer.Dl_speed, 364 | ClientName: clientName, 365 | FlagStr: peer.Flags, 366 | Country: country, 367 | Address: peer.IP, 368 | Progress: peer.Progress, 369 | Port: peer.Port, 370 | }) 371 | } 372 | 373 | dst["peers"] = trPeers 374 | } 375 | 376 | func MapPropsTrackers(dst JsonMap, trackers []qBT.PropertiesTrackers) { 377 | trackersList := make([]JsonMap, len(trackers)) 378 | 379 | for i, value := range trackers { 380 | id := i 381 | trackersList[i] = make(JsonMap) 382 | trackersList[i]["announce"] = value.Url 383 | trackersList[i]["id"] = id 384 | trackersList[i]["scrape"] = value.Url 385 | trackersList[i]["tier"] = 0 // TODO 386 | } 387 | 388 | dst["trackers"] = trackersList 389 | } 390 | 391 | func MapPropsTrackerStats(dst JsonMap, trackers []qBT.PropertiesTrackers, torrentInfo *qBT.TorrentInfo) { 392 | trackerStats := make([]JsonMap, len(trackers)) 393 | 394 | for i, value := range trackers { 395 | id := i 396 | 397 | trackerStats[i] = make(JsonMap) 398 | for key, value := range transmission.TrackerStatsTemplate { 399 | trackerStats[i][key] = value 400 | } 401 | trackerStats[i]["announce"] = value.Url 402 | trackerStats[i]["host"] = value.Url 403 | trackerStats[i]["leecherCount"] = torrentInfo.Num_incomplete 404 | trackerStats[i]["seederCount"] = torrentInfo.Num_complete 405 | trackerStats[i]["downloadCount"] = torrentInfo.Num_complete // TODO: Find a more accurate source 406 | trackerStats[i]["lastAnnouncePeerCount"] = torrentInfo.Num_complete + torrentInfo.Num_incomplete // TODO: Is it correct? 407 | trackerStats[i]["lastAnnounceResult"] = decodeTrackerStatus(value.Status) 408 | trackerStats[i]["lastAnnounceSucceeded"] = value.Status == 2 409 | trackerStats[i]["hasAnnounced"] = value.Status == 2 410 | trackerStats[i]["id"] = id 411 | trackerStats[i]["scrape"] = "" 412 | trackerStats[i]["tier"] = 0 // TODO 413 | } 414 | 415 | dst["trackerStats"] = trackerStats 416 | } 417 | 418 | func decodeTrackerStatus(status int) string { 419 | switch status { 420 | case 0: 421 | return "Tracker is disabled" 422 | case 1: 423 | return "Tracker has not been contacted yet" 424 | case 2: 425 | return "Tracker has been contacted and is working" 426 | case 3: 427 | return "Tracker is updating" 428 | case 4: 429 | return "Tracker has been contacted, but it is not working (or doesn't send proper replies)" 430 | default: 431 | return "Unknown status" 432 | } 433 | } 434 | 435 | func MapPropsFiles(dst JsonMap, filesInfo []qBT.PropertiesFiles) { 436 | fileNum := len(filesInfo) 437 | files := make([]JsonMap, fileNum) 438 | fileStats := make([]JsonMap, fileNum) 439 | priorities := make([]int, fileNum) 440 | wanted := make([]int, fileNum) 441 | for i, value := range filesInfo { 442 | files[i] = make(JsonMap) 443 | fileStats[i] = make(JsonMap) 444 | 445 | files[i]["bytesCompleted"] = float64(value.Size) * value.Progress 446 | files[i]["length"] = value.Size 447 | convertedName := EscapeString(value.Name) 448 | files[i]["name"] = convertedName 449 | 450 | fileStats[i]["bytesCompleted"] = float64(value.Size) * value.Progress 451 | if value.Priority == 0 { 452 | fileStats[i]["wanted"] = false 453 | wanted[i] = 0 454 | } else { 455 | fileStats[i]["wanted"] = true 456 | wanted[i] = 1 457 | } 458 | fileStats[i]["priority"] = 0 // TODO 459 | priorities[i] = 0 // TODO 460 | } 461 | 462 | dst["files"] = files 463 | dst["fileStats"] = fileStats 464 | dst["priorities"] = priorities 465 | dst["wanted"] = wanted 466 | } 467 | 468 | var propsCache = Cache{Timeout: time.Duration(*cacheTimeout) * time.Second} 469 | var trackersCache = Cache{Timeout: time.Duration(*cacheTimeout) * time.Second} 470 | 471 | func TorrentGet(args json.RawMessage) (JsonMap, string) { 472 | var req transmission.GetRequest 473 | err := json.Unmarshal(args, &req) 474 | Check(err) 475 | 476 | torrents := parseIDsField(req.Ids) 477 | severalIDsRequired := len(torrents) > 1 478 | fields := req.Fields 479 | filesNeeded := false 480 | trackersNeeded := false 481 | trackerStatsNeeded := false 482 | peersNeeded := false 483 | propsGeneralNeeded := false 484 | piecesNeeded := false 485 | for _, field := range fields { 486 | additionalRequestsNeeded := true 487 | switch field { 488 | case "files", "fileStats", "priorities", "wanted": 489 | filesNeeded = true 490 | case "trackers": 491 | trackersNeeded = true 492 | case "trackerStats": 493 | trackerStatsNeeded = true 494 | case "peers": 495 | peersNeeded = true 496 | case "pieceSize", "pieceCount", 497 | "comment", "dateCreated", "creator", 498 | "haveValid", "downloadedEver", 499 | "uploadedEver", "peersConnected", "peersFrom", 500 | "corruptEver", "uploadLimited", "uploadLimit", "downloadLimited", 501 | "downloadLimit", "maxConnectedPeers", "peer-limit": 502 | propsGeneralNeeded = true 503 | case "pieces": 504 | piecesNeeded = true 505 | default: 506 | additionalRequestsNeeded = false 507 | } 508 | if additionalRequestsNeeded && severalIDsRequired { 509 | log.Info("Field which caused a full torrent scan (slow op!): " + field) 510 | } 511 | } 512 | 513 | resultList := make([]JsonMap, len(torrents)) 514 | for i, torrentItem := range torrents { 515 | translated := make(JsonMap) 516 | 517 | hash := torrentItem.Hash 518 | id := torrentItem.Id 519 | 520 | MapTorrentList(translated, torrentItem) // TODO: Make it conditional too 521 | 522 | if propsGeneralNeeded { 523 | log.WithField("id", id).WithField("hash", hash).Debug("Props required") 524 | propsCache.GetOrFill(hash, translated, severalIDsRequired, func(dest JsonMap) { 525 | propGeneral := qBTConn.GetPropsGeneral(hash) 526 | MapPropsGeneral(dest, propGeneral) 527 | addPropertiesToCommentField(dest, torrentItem, propGeneral) 528 | }) 529 | } 530 | if trackersNeeded || trackerStatsNeeded { 531 | log.WithField("id", id).WithField("hash", hash).Debug("Trackers required") 532 | trackersCache.GetOrFill(hash, translated, severalIDsRequired, func(dest JsonMap) { 533 | trackers := qBTConn.GetPropsTrackers(hash) 534 | MapPropsTrackers(dest, trackers) 535 | MapPropsTrackerStats(dest, trackers, torrentItem) 536 | }) 537 | } 538 | if piecesNeeded { 539 | log.WithField("id", id).WithField("hash", hash).Debug("Pieces required") 540 | pieces := qBTConn.GetPiecesStates(hash) 541 | MapPieceStates(translated, pieces) 542 | } 543 | if filesNeeded { 544 | log.WithField("id", id).WithField("hash", hash).Debug("Files required") 545 | files := qBTConn.GetPropsFiles(hash) 546 | MapPropsFiles(translated, files) 547 | } 548 | if peersNeeded { 549 | log.WithField("id", id).WithField("hash", hash).Debug("Peers required") 550 | MapPropsPeers(translated, hash) 551 | } 552 | 553 | translated["id"] = id 554 | translated["queuePosition"] = i + 1 555 | // TODO: Check it once 556 | for _, field := range fields { 557 | if _, ok := translated[field]; !ok { 558 | if !IsFieldDeprecated(field) { 559 | log.Error("Unsupported field: ", field) 560 | panic("Unsupported field: " + field) 561 | } 562 | } 563 | } 564 | for translatedField := range translated { 565 | if !Any(fields, translatedField) { 566 | // Remove unneeded fields 567 | delete(translated, translatedField) 568 | } 569 | } 570 | resultList[i] = translated 571 | } 572 | response := JsonMap{"torrents": resultList} 573 | addRemovedList(req.Ids, response) 574 | return response, "success" 575 | } 576 | 577 | func addRemovedList(idsField *json.RawMessage, resp JsonMap) { 578 | if idsField == nil { 579 | return 580 | } 581 | var ids interface{} 582 | err := json.Unmarshal(*idsField, &ids) 583 | Check(err) 584 | 585 | switch ids.(type) { 586 | case string: 587 | resp["removed"] = qBTConn.TorrentsList.GetRemoved() 588 | } 589 | } 590 | 591 | func qBTEncryptionToTR(enc int) (res string) { 592 | switch enc { 593 | case 0: 594 | return "preferred" 595 | case 1: 596 | return "required" 597 | default: 598 | return "tolerated" 599 | } 600 | } 601 | 602 | func SessionGet() (JsonMap, string) { 603 | session := make(JsonMap) 604 | for key, value := range transmission.SessionGetBase { 605 | session[key] = value 606 | } 607 | 608 | prefs := qBTConn.GetPreferences() 609 | session["download-dir"] = prefs.Save_path 610 | session["speed-limit-down"] = prefs.Dl_limit / 1024 611 | session["speed-limit-up"] = prefs.Up_limit / 1024 612 | if prefs.Dl_limit == -1 { 613 | session["speed-limit-down-enabled"] = false 614 | } else { 615 | session["speed-limit-down-enabled"] = true 616 | } 617 | 618 | if prefs.Up_limit == -1 { 619 | session["speed-limit-up-enabled"] = false 620 | } else { 621 | session["speed-limit-up-enabled"] = true 622 | } 623 | 624 | session["peer-limit-global"] = prefs.Max_connec 625 | session["peer-limit-per-torrent"] = prefs.Max_connec_per_torrent 626 | session["peer-port"] = prefs.Listen_port 627 | session["seedRatioLimit"] = prefs.Max_ratio 628 | session["seedRatioLimited"] = prefs.Max_ratio_enabled 629 | session["peer-port-random-on-start"] = prefs.Random_port 630 | session["port-forwarding-enabled"] = prefs.Upnp 631 | session["utp-enabled"] = prefs.Enable_utp 632 | session["dht-enabled"] = prefs.Dht 633 | session["incomplete-dir"] = prefs.Temp_path 634 | session["incomplete-dir-enabled"] = prefs.Temp_path_enabled 635 | session["lpd-enabled"] = prefs.Lsd 636 | session["pex-enabled"] = prefs.Pex 637 | session["encryption"] = qBTEncryptionToTR(prefs.Encryption) 638 | session["download-queue-size"] = prefs.Max_active_downloads 639 | session["seed-queue-size"] = prefs.Max_active_uploads 640 | session["download-queue-enabled"] = prefs.Queueing_enabled 641 | session["seed-queue-enabled"] = prefs.Queueing_enabled 642 | session["download-dir"] = prefs.Save_path 643 | 644 | version := qBTConn.GetVersion() 645 | session["version"] = "2.94 (really qBT " + string(version) + ")" 646 | return session, "success" 647 | } 648 | 649 | func FreeSpace(args json.RawMessage) (JsonMap, string) { 650 | req := struct { 651 | Path string 652 | }{} 653 | err := json.Unmarshal(args, &req) 654 | Check(err) 655 | 656 | diskUsage := du.NewDiskUsage(req.Path) 657 | freeSpace := diskUsage.Available() 658 | 659 | log.WithField("path", req.Path).WithField("free space", freeSpace).Debug("Free space") 660 | 661 | return JsonMap{ 662 | "path": req.Path, 663 | "size-bytes": freeSpace, 664 | }, "success" 665 | } 666 | 667 | func SessionStats() (JsonMap, string) { 668 | session := make(JsonMap) 669 | for key, value := range transmission.SessionStatsTemplate { 670 | session[key] = value 671 | } 672 | 673 | torrentList := qBTConn.TorrentsList.AllItems() 674 | 675 | paused := 0 676 | active := 0 677 | all := len(torrentList) 678 | timeElapsed := 0 679 | 680 | for _, torrent := range torrentList { 681 | if qBTStateToTransmissionStatus(torrent.State) == TR_STATUS_STOPPED { 682 | paused++ 683 | } else { 684 | active++ 685 | } 686 | } 687 | 688 | info := qBTConn.GetTransferInfo() 689 | session["activeTorrentCount"] = active 690 | session["pausedTorrentCount"] = paused 691 | session["torrentCount"] = all 692 | session["downloadSpeed"] = info.Dl_info_speed 693 | session["uploadSpeed"] = info.Up_info_speed 694 | session["current-stats"].(map[string]int64)["downloadedBytes"] = info.Dl_info_data 695 | session["current-stats"].(map[string]int64)["uploadedBytes"] = info.Up_info_data 696 | session["current-stats"].(map[string]int64)["secondsActive"] = int64(timeElapsed) 697 | session["cumulative-stats"] = session["current-stats"] 698 | return session, "success" 699 | } 700 | 701 | func TorrentPause(args json.RawMessage) (JsonMap, string) { 702 | torrents := parseActionArgument(args) 703 | log.WithField("hashes", torrents.Hashes()).Debug("Stopping torrents") 704 | qBTConn.PostWithHashes("torrents/pause", torrents) 705 | return JsonMap{}, "success" 706 | } 707 | 708 | func TorrentResume(args json.RawMessage) (JsonMap, string) { 709 | torrents := parseActionArgument(args) 710 | log.WithField("hashes", torrents.Hashes()).Debug("Starting torrents") 711 | 712 | qBTConn.PostWithHashes("torrents/resume", torrents) 713 | return JsonMap{}, "success" 714 | } 715 | 716 | func TorrentRecheck(args json.RawMessage) (JsonMap, string) { 717 | torrents := parseActionArgument(args) 718 | log.WithField("hashes", torrents.Hashes()).Debug("Verifying torrents") 719 | 720 | qBTConn.PostWithHashes("torrents/recheck", torrents) 721 | return JsonMap{}, "success" 722 | } 723 | 724 | func TorrentDelete(args json.RawMessage) (JsonMap, string) { 725 | var req struct { 726 | Ids json.RawMessage 727 | DeleteLocalData interface{} `json:"delete-local-data"` 728 | } 729 | err := json.Unmarshal(args, &req) 730 | Check(err) 731 | 732 | torrents := parseIDsField(&req.Ids) 733 | log.WithField("hashes", torrents.Hashes()).Warn("Going to remove torrents") 734 | 735 | joinedHashes := torrents.ConcatenateHashes() 736 | 737 | deleteFiles := parseDeleteFilesField(req.DeleteLocalData) 738 | 739 | params := map[string]string{"hashes": joinedHashes} 740 | 741 | if deleteFiles { 742 | log.Info("Going to remove torrents with files: ", joinedHashes) 743 | params["deleteFiles"] = "true" 744 | } else { 745 | log.Info("Going to remove torrents: ", joinedHashes) 746 | params["deleteFiles"] = "false" 747 | } 748 | url := qBTConn.MakeRequestURLWithParam("torrents/delete", params) 749 | qBTConn.DoGET(url) 750 | 751 | return JsonMap{}, "success" 752 | } 753 | 754 | func parseDeleteFilesField(deleteLocalData interface{}) bool { 755 | switch val := deleteLocalData.(type) { 756 | case bool: 757 | return val 758 | case float64: 759 | return val != 0 760 | default: 761 | return false // Default value 762 | } 763 | } 764 | 765 | func PutMIMEField(mime *multipart.Writer, fieldName string, value string) { 766 | urlsWriter, err := mime.CreateFormField(fieldName) 767 | Check(err) 768 | _, err = urlsWriter.Write([]byte(value)) 769 | Check(err) 770 | } 771 | 772 | func AdditionalArgumentToString(value argumentValue) string { 773 | switch value { 774 | case ARGUMENT_NOT_SET: 775 | return "" // TODO 776 | case ARGUMENT_TRUE: 777 | return "true" 778 | case ARGUMENT_FALSE: 779 | return "false" 780 | default: 781 | return "" 782 | } 783 | } 784 | 785 | func UploadTorrent(metainfo *[]byte, urls *string, req *transmission.TorrentAddRequest, paused bool) { 786 | var buffer bytes.Buffer 787 | mime := multipart.NewWriter(&buffer) 788 | 789 | if metainfo != nil { 790 | mimeWriter, err := mime.CreateFormFile("torrents", "example.torrent") 791 | Check(err) 792 | mimeWriter.Write(*metainfo) 793 | } 794 | 795 | if urls != nil { 796 | PutMIMEField(mime, "urls", *urls) 797 | } 798 | 799 | if req.Download_dir != nil { 800 | extraArgs, strippedLocation, err := parseAdditionalLocationArguments(*req.Download_dir) 801 | Check(err) 802 | log.Debug("Stripped location is ", strippedLocation) 803 | 804 | if extraArgs.sequentialDownload != ARGUMENT_NOT_SET { 805 | log.Debug("Sequential download: ", AdditionalArgumentToString(extraArgs.sequentialDownload)) 806 | PutMIMEField(mime, "sequentialDownload", 807 | AdditionalArgumentToString(extraArgs.sequentialDownload)) 808 | } 809 | 810 | if extraArgs.firstLastPiecesFirst != ARGUMENT_NOT_SET { 811 | log.Debug("FirstLastPiecePrio: ", AdditionalArgumentToString(extraArgs.firstLastPiecesFirst)) 812 | PutMIMEField(mime, "firstLastPiecePrio", 813 | AdditionalArgumentToString(extraArgs.firstLastPiecesFirst)) 814 | } 815 | 816 | if extraArgs.skipChecking != ARGUMENT_NOT_SET { 817 | log.Debug("Skip checking: ", AdditionalArgumentToString(extraArgs.skipChecking)) 818 | PutMIMEField(mime, "skip_checking", 819 | AdditionalArgumentToString(extraArgs.skipChecking)) 820 | } 821 | 822 | PutMIMEField(mime, "savepath", strippedLocation) 823 | } 824 | 825 | pausedWriter, err := mime.CreateFormField("paused") 826 | Check(err) 827 | if paused { 828 | pausedWriter.Write([]byte("true")) 829 | } else { 830 | pausedWriter.Write([]byte("false")) 831 | } 832 | mime.CreateFormField("cookie") 833 | mime.CreateFormField("label") 834 | 835 | mime.Close() 836 | 837 | qBTConn.DoPOST(qBTConn.MakeRequestURL("torrents/add"), mime.FormDataContentType(), &buffer) 838 | log.Debug("Torrent uploaded") 839 | } 840 | 841 | func ParseMagnetLink(link string) (newHash qBT.Hash, newName string) { 842 | path := strings.TrimPrefix(link, "magnet:?") 843 | params, err := url.ParseQuery(path) 844 | Check(err) 845 | log.WithFields(log.Fields{ 846 | "params": params, 847 | }).Debug("Params decoded") 848 | trimmed := strings.TrimPrefix(params["xt"][0], "urn:btih:") 849 | newHash = qBT.Hash(strings.ToLower(trimmed)) 850 | name, nameProvided := params["dn"] 851 | if nameProvided { 852 | newName = name[0] 853 | } else { 854 | newName = "Torrent name" 855 | } 856 | return 857 | } 858 | 859 | func ParseMetainfo(metainfo []byte) (newHash qBT.Hash, newName string) { 860 | var parsedMetaInfo MetaInfo 861 | parsedMetaInfo.ReadTorrentMetaInfoFile(bytes.NewBuffer(metainfo)) 862 | 863 | log.WithFields(log.Fields{ 864 | "len": len(metainfo), 865 | "sha1": fmt.Sprintf("%x\n", sha1.Sum(metainfo)), 866 | }).Debug("Decoded metainfo") 867 | 868 | newHash = qBT.Hash(fmt.Sprintf("%x", parsedMetaInfo.InfoHash)) 869 | newName = parsedMetaInfo.Info.Name 870 | return 871 | } 872 | 873 | func TorrentAdd(args json.RawMessage) (JsonMap, string) { 874 | var req transmission.TorrentAddRequest 875 | err := json.Unmarshal(args, &req) 876 | Check(err) 877 | 878 | qBTConn.UpdateTorrentsList() 879 | 880 | var newHash qBT.Hash 881 | var newName string 882 | 883 | paused := false 884 | if req.Paused != nil { 885 | if value, ok := (*req.Paused).(float64); ok { 886 | // Workaround: Transmission Remote GUI uses a number instead of a boolean 887 | log.Debug("Apply Transmission Remote GUI workaround") 888 | paused = value != 0 889 | } 890 | if value, ok := (*req.Paused).(bool); ok { 891 | paused = value 892 | } 893 | } 894 | 895 | if req.Metainfo != nil { 896 | log.Debug("Upload torrent from metainfo") 897 | metainfo, err := base64.StdEncoding.DecodeString(*req.Metainfo) 898 | Check(err) 899 | newHash, newName = ParseMetainfo(metainfo) 900 | UploadTorrent(&metainfo, nil, &req, paused) 901 | } else if req.Filename != nil { 902 | path := *req.Filename 903 | if strings.HasPrefix(path, "magnet:?") { 904 | newHash, newName = ParseMagnetLink(path) 905 | 906 | UploadTorrent(nil, &path, &req, paused) 907 | } else if strings.HasPrefix(path, "http") { 908 | metainfo := DoGetWithCookies(path, req.Cookies) 909 | 910 | newHash, newName = ParseMetainfo(metainfo) 911 | UploadTorrent(&metainfo, nil, &req, paused) 912 | } 913 | } 914 | 915 | log.WithFields(log.Fields{ 916 | "hash": newHash, 917 | "name": newName, 918 | }).Debug("Attempting to add torrent") 919 | 920 | if torrent := qBTConn.TorrentsList.ByHash(newHash); torrent != nil { 921 | return JsonMap{ 922 | "torrent-duplicate": JsonMap{ 923 | "id": torrent.Id, 924 | "name": newName, 925 | "hashString": newHash, 926 | }, 927 | }, "success" 928 | } 929 | 930 | var torrent *qBT.TorrentInfo 931 | for retries := 0; retries < 100; retries++ { 932 | time.Sleep(50 * time.Millisecond) 933 | qBTConn.UpdateTorrentsList() 934 | torrent = qBTConn.TorrentsList.ByHash(newHash) 935 | if torrent != nil { 936 | log.Debug("Found ID ", torrent.Id) 937 | break 938 | } 939 | 940 | log.Debug("Nothing was found, waiting...") 941 | } 942 | 943 | if torrent == nil { 944 | return JsonMap{}, "Torrent-add timeout" 945 | } 946 | 947 | log.WithFields(log.Fields{ 948 | "hash": newHash, 949 | "id": torrent.Id, 950 | "name": newName, 951 | }).Debug("New torrent") 952 | 953 | return JsonMap{ 954 | "torrent-added": JsonMap{ 955 | "id": torrent.Id, 956 | "name": newName, 957 | "hashString": newHash, 958 | }, 959 | }, "success" 960 | } 961 | 962 | func TorrentSet(args json.RawMessage) (JsonMap, string) { 963 | var req struct { 964 | Ids *json.RawMessage 965 | Files_wanted *[]int `json:"files-wanted"` 966 | Files_unwanted *[]int `json:"files-unwanted"` 967 | } 968 | err := json.Unmarshal(args, &req) 969 | Check(err) 970 | 971 | if req.Files_wanted != nil || req.Files_unwanted != nil { 972 | torrents := parseIDsField(req.Ids) 973 | if len(torrents) != 1 { 974 | log.Error("Unsupported torrent-set request") 975 | return JsonMap{}, "Unsupported torrent-set request" 976 | } 977 | torrent := torrents[0] 978 | 979 | newFilesPriorities := make(map[int]int) 980 | if req.Files_wanted != nil { 981 | wanted := *req.Files_wanted 982 | for _, fileId := range wanted { 983 | newFilesPriorities[fileId] = 1 // Normal priority 984 | } 985 | } 986 | if req.Files_unwanted != nil { 987 | unwanted := *req.Files_unwanted 988 | for _, fileId := range unwanted { 989 | newFilesPriorities[fileId] = 0 // Do not download 990 | } 991 | } 992 | log.WithFields(log.Fields{ 993 | "priorities": newFilesPriorities, 994 | }).Debug("New files priorities") 995 | 996 | for fileId, priority := range newFilesPriorities { 997 | params := url.Values{ 998 | "hash": {string(torrent.Hash)}, 999 | "id": {strconv.Itoa(fileId)}, 1000 | "priority": {strconv.Itoa(priority)}, 1001 | } 1002 | qBTConn.PostForm(qBTConn.MakeRequestURL("torrents/filePrio"), params) 1003 | } 1004 | } 1005 | 1006 | return JsonMap{}, "success" // TODO 1007 | } 1008 | 1009 | var additionalArgumentsRegexp = regexp.MustCompile("([+\\-])([sfh]+)$") 1010 | 1011 | func parseAdditionalLocationArguments(originalLocation string) (args additionalArguments, strippedLocation string, err error) { 1012 | strippedLocation = additionalArgumentsRegexp.ReplaceAllLiteralString(originalLocation, "") 1013 | submatches := additionalArgumentsRegexp.FindStringSubmatch(originalLocation) 1014 | if len(submatches) == 0 { 1015 | return 1016 | } 1017 | for _, c := range submatches[2] { 1018 | flagValue := ARGUMENT_NOT_SET 1019 | switch submatches[1] { 1020 | case "+": 1021 | flagValue = ARGUMENT_TRUE 1022 | case "-": 1023 | flagValue = ARGUMENT_FALSE 1024 | default: 1025 | err = errors.New("Unknown value: " + submatches[1]) 1026 | return 1027 | } 1028 | switch c { 1029 | case 's': 1030 | args.sequentialDownload = flagValue 1031 | case 'f': 1032 | args.firstLastPiecesFirst = flagValue 1033 | case 'h': 1034 | args.skipChecking = flagValue 1035 | default: 1036 | err = errors.New("Unknown value: " + submatches[1]) 1037 | return 1038 | } 1039 | } 1040 | return 1041 | } 1042 | 1043 | func TorrentSetLocation(args json.RawMessage) (JsonMap, string) { 1044 | var req struct { 1045 | Ids *json.RawMessage 1046 | Location *string `json:"location"` 1047 | Move interface{} `json:"move"` 1048 | } 1049 | err := json.Unmarshal(args, &req) 1050 | Check(err) 1051 | 1052 | log.Debug("New location: ", *req.Location) 1053 | if req.Location == nil { 1054 | return JsonMap{}, "Absent location field" 1055 | } 1056 | 1057 | torrents := parseIDsField(req.Ids) 1058 | 1059 | /*var move bool // TODO: Move to a function 1060 | switch val := req.Move.(type) { 1061 | case bool: 1062 | move = val 1063 | case float64: 1064 | move = (val != 0) 1065 | }*/ 1066 | 1067 | extraArgs, strippedLocation, err := parseAdditionalLocationArguments(*req.Location) 1068 | Check(err) 1069 | 1070 | if extraArgs.firstLastPiecesFirst != ARGUMENT_NOT_SET { 1071 | for _, torrent := range torrents { 1072 | qBTConn.SetFirstLastPieceFirst(torrent.Hash, extraArgs.firstLastPiecesFirst == ARGUMENT_TRUE) 1073 | } 1074 | } 1075 | 1076 | if extraArgs.sequentialDownload != ARGUMENT_NOT_SET { 1077 | for _, torrent := range torrents { 1078 | qBTConn.SetSequentialDownload(torrent.Hash, extraArgs.sequentialDownload == ARGUMENT_TRUE) 1079 | } 1080 | } 1081 | 1082 | params := url.Values{ 1083 | "hashes": {torrents.ConcatenateHashes()}, 1084 | "location": {strippedLocation}, 1085 | } 1086 | qBTConn.PostForm(qBTConn.MakeRequestURL("torrents/setLocation"), params) 1087 | 1088 | return JsonMap{}, "success" 1089 | } 1090 | 1091 | func handler(w http.ResponseWriter, r *http.Request) { 1092 | var req transmission.RPCRequest 1093 | reqBody, err := ioutil.ReadAll(r.Body) 1094 | log.Debug("Got request ", string(reqBody)) 1095 | err = json.Unmarshal(reqBody, &req) 1096 | Check(err) 1097 | 1098 | if !qBTConn.IsLoggedIn() { 1099 | var authOK = false 1100 | username, password, present := r.BasicAuth() 1101 | if present { 1102 | authOK = qBTConn.Login(username, password) 1103 | } else { 1104 | authOK = qBTConn.Login("", "") 1105 | } 1106 | if !authOK { 1107 | w.WriteHeader(http.StatusUnauthorized) 1108 | return 1109 | } 1110 | } 1111 | 1112 | var resp JsonMap 1113 | var result string 1114 | switch req.Method { 1115 | case "session-get": 1116 | resp, result = SessionGet() 1117 | case "free-space": 1118 | resp, result = FreeSpace(req.Arguments) 1119 | case "torrent-get": 1120 | resp, result = TorrentGet(req.Arguments) 1121 | case "session-stats": 1122 | resp, result = SessionStats() 1123 | case "torrent-stop": 1124 | resp, result = TorrentPause(req.Arguments) 1125 | case "torrent-start": 1126 | resp, result = TorrentResume(req.Arguments) 1127 | case "torrent-start-now": 1128 | resp, result = TorrentResume(req.Arguments) 1129 | case "torrent-verify": 1130 | resp, result = TorrentRecheck(req.Arguments) 1131 | case "torrent-remove": 1132 | resp, result = TorrentDelete(req.Arguments) 1133 | case "torrent-add": 1134 | resp, result = TorrentAdd(req.Arguments) 1135 | case "torrent-set": 1136 | resp, result = TorrentSet(req.Arguments) 1137 | case "torrent-set-location": 1138 | resp, result = TorrentSetLocation(req.Arguments) 1139 | default: 1140 | log.Error("Unknown method: ", req.Method) 1141 | } 1142 | response := JsonMap{ 1143 | "result": result, 1144 | "arguments": resp, 1145 | } 1146 | if req.Tag != nil { 1147 | response["tag"] = req.Tag 1148 | } 1149 | respBody, err := json.Marshal(response) 1150 | Check(err) 1151 | log.Debug("respBody: ", string(respBody)) 1152 | w.Header().Set("Content-Length", strconv.Itoa(len(respBody))) 1153 | w.Header().Set("Content-Type", "application/json; charset=utf-8") 1154 | _, err = w.Write(respBody) // TODO: Check whether it's necessary to evaluate written bytes number 1155 | Check(err) 1156 | } 1157 | 1158 | func main() { 1159 | switch { 1160 | case *debug: 1161 | log.SetLevel(log.DebugLevel) 1162 | case *verbose: 1163 | log.SetLevel(log.InfoLevel) 1164 | default: 1165 | log.SetLevel(log.WarnLevel) 1166 | } 1167 | var cl *http.Client 1168 | if *disableKeepAlive { 1169 | log.Info("Disabled HTTP keep-alive") 1170 | tr := &http.Transport{ 1171 | DisableKeepAlives: true, 1172 | } 1173 | cl = &http.Client{Transport: tr} 1174 | } else { 1175 | cl = &http.Client{} 1176 | } 1177 | qBTConn.Init(*apiAddr, cl, *useSync) 1178 | 1179 | http.HandleFunc("/transmission/rpc", handler) 1180 | http.HandleFunc("/rpc", handler) 1181 | http.Handle("/", http.FileServer(http.Dir("web/"))) 1182 | err := http.ListenAndServe(fmt.Sprintf(":%d", *port), nil) 1183 | Check(err) 1184 | } 1185 | -------------------------------------------------------------------------------- /reflection/main_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/h31/Reflection/qBT" 5 | "github.com/hekmon/transmissionrpc" 6 | log "github.com/sirupsen/logrus" 7 | "gopkg.in/h2non/gock.v1" 8 | "net" 9 | "net/http" 10 | "net/http/httptest" 11 | "testing" 12 | "time" 13 | ) 14 | 15 | var currentLogLevel = log.DebugLevel 16 | 17 | func TestAdditionalLocationArguments(t *testing.T) { 18 | tables := []struct { 19 | input string 20 | args additionalArguments 21 | strippedLocation string 22 | err error 23 | }{ 24 | {"/home/user", additionalArguments{}, "/home/user", nil}, 25 | {"/home/user/", additionalArguments{}, "/home/user/", nil}, 26 | {"/home/user/+s", additionalArguments{sequentialDownload: ARGUMENT_TRUE}, "/home/user/", nil}, 27 | {"/home/user+s", additionalArguments{sequentialDownload: ARGUMENT_TRUE}, "/home/user", nil}, 28 | {"/home/user+data", additionalArguments{}, "/home/user+data", nil}, 29 | {"/home/user+data+s", additionalArguments{sequentialDownload: ARGUMENT_TRUE}, "/home/user+data", nil}, 30 | {"/home/user+s/", additionalArguments{}, "/home/user+s/", nil}, 31 | {"/home/user/+f", additionalArguments{firstLastPiecesFirst: ARGUMENT_TRUE}, "/home/user/", nil}, 32 | {"/home/user/+sf", additionalArguments{sequentialDownload: ARGUMENT_TRUE, firstLastPiecesFirst: ARGUMENT_TRUE}, 33 | "/home/user/", nil}, 34 | {"/home/user/+h", additionalArguments{skipChecking: ARGUMENT_TRUE}, "/home/user/", nil}, 35 | {"/home/user/-s", additionalArguments{sequentialDownload: ARGUMENT_FALSE}, "/home/user/", nil}, 36 | {"/home/user/-sh", additionalArguments{sequentialDownload: ARGUMENT_FALSE, skipChecking: ARGUMENT_FALSE}, "/home/user/", nil}, 37 | {"C:\\Users\\+s\\", additionalArguments{}, "C:\\Users\\+s\\", nil}, 38 | {"C:\\Users\\+s", additionalArguments{sequentialDownload: ARGUMENT_TRUE}, "C:\\Users\\", nil}, 39 | } 40 | 41 | for _, table := range tables { 42 | args, location, err := parseAdditionalLocationArguments(table.input) 43 | if args != table.args || location != table.strippedLocation || err != table.err { 44 | t.Errorf("Input %s, expected (%+v, %s, %v), got: (%+v, %s, %v)", table.input, 45 | table.args, table.strippedLocation, table.err, 46 | args, location, err) 47 | } 48 | } 49 | } 50 | 51 | func TestTorrentListing(t *testing.T) { 52 | const apiAddr = "http://localhost:8080" 53 | log.SetLevel(currentLogLevel) 54 | 55 | defer gock.Off() 56 | 57 | gock.Observe(gock.DumpRequest) 58 | 59 | gock.New(apiAddr). 60 | Get("/api/v2/torrents/info"). 61 | Times(2). 62 | Reply(200). 63 | File("testdata/torrent_list.json") 64 | 65 | gock.New(apiAddr). 66 | Post("/api/v2/auth/login"). 67 | Reply(200). 68 | SetHeader("Set-Cookie", "SID=1") 69 | 70 | // 1 71 | setUpMocks(apiAddr, "cf7da7ab4d4e6125567bd979994f13bb1f23dddd", "1") 72 | 73 | // 2 74 | setUpMocks(apiAddr, "842783e3005495d5d1637f5364b59343c7844707", "2") 75 | 76 | client := &http.Client{Transport: &http.Transport{}} 77 | gock.InterceptClient(client) 78 | 79 | qBTConn.Init(apiAddr, client, false) 80 | server := httptest.NewServer(http.HandlerFunc(handler)) 81 | defer server.Close() 82 | defer server.CloseClientConnections() 83 | serverAddr := server.Listener.Addr().(*net.TCPAddr) 84 | 85 | transmissionbt, err := transmissionrpc.New(serverAddr.IP.String(), "", "", 86 | &transmissionrpc.AdvancedConfig{Port: uint16(serverAddr.Port)}) 87 | Check(err) 88 | torrents, err := transmissionbt.TorrentGetAll() 89 | Check(err) 90 | if len(torrents) != 2 { 91 | t.Error("Number of torrents is not equal to 2") 92 | } 93 | if *torrents[0].Name != "ubuntu-18.04.2-desktop-amd64.iso" { 94 | t.Error("Unexpected torrent 0") 95 | } 96 | if *torrents[1].Name != "ubuntu-18.04.2-live-server-amd64.iso" { 97 | t.Error("Unexpected torrent 1") 98 | } 99 | 100 | id := *torrents[1].ID 101 | 102 | singleTorrent, err := transmissionbt.TorrentGetAllFor([]int64{id}) 103 | Check(err) 104 | if *singleTorrent[0].Name != "ubuntu-18.04.2-live-server-amd64.iso" { 105 | t.Error("Unexpected torrent") 106 | } 107 | } 108 | 109 | func TestTorrentListingRepeated(t *testing.T) { 110 | prevLogLevel := currentLogLevel 111 | currentLogLevel = log.ErrorLevel 112 | for i := 0; i < 50; i++ { 113 | TestTorrentListing(t) 114 | } 115 | currentLogLevel = prevLogLevel 116 | } 117 | 118 | func TestSyncing(t *testing.T) { 119 | const apiAddr = "http://localhost:8080" 120 | log.SetLevel(currentLogLevel) 121 | 122 | defer gock.Off() 123 | 124 | gock.Observe(gock.DumpRequest) 125 | 126 | gock.New(apiAddr). 127 | Post("/api/v2/auth/login"). 128 | Reply(200). 129 | SetHeader("Set-Cookie", "SID=1") 130 | 131 | setUpSyncEndpoint(apiAddr) 132 | 133 | // 1 134 | setUpMocks(apiAddr, "cf7da7ab4d4e6125567bd979994f13bb1f23dddd", "1") 135 | 136 | // 2 137 | setUpMocks(apiAddr, "842783e3005495d5d1637f5364b59343c7844707", "2") 138 | 139 | // 3 140 | setUpMocks(apiAddr, "7a1448be6d15bcde08ee9915350d0725775b73a3", "3") 141 | 142 | client := &http.Client{Transport: &http.Transport{}} 143 | gock.InterceptClient(client) 144 | 145 | qBTConn.Init(apiAddr, client, true) 146 | server := httptest.NewServer(http.HandlerFunc(handler)) 147 | defer server.Close() 148 | defer server.CloseClientConnections() 149 | serverAddr := server.Listener.Addr().(*net.TCPAddr) 150 | 151 | transmissionbt, err := transmissionrpc.New(serverAddr.IP.String(), "", "", 152 | &transmissionrpc.AdvancedConfig{Port: uint16(serverAddr.Port)}) 153 | Check(err) 154 | torrents, err := transmissionbt.TorrentGetAll() 155 | Check(err) 156 | if len(torrents) != 2 { 157 | t.Error("Number of torrents is not equal to 2") 158 | } 159 | if *torrents[0].Name != "ubuntu-18.04.2-live-server-amd64.iso" { 160 | t.Error("Unexpected torrent 0") 161 | } 162 | if *torrents[1].Name != "ubuntu-18.04.2-desktop-amd64.iso" { 163 | t.Error("Unexpected torrent 1") 164 | } 165 | 166 | torrents, err = transmissionbt.TorrentGetAll() 167 | Check(err) 168 | if len(torrents) != 2 { 169 | t.Error("Number of torrents is not equal to 2") 170 | } 171 | 172 | torrents, err = transmissionbt.TorrentGetAll() 173 | Check(err) 174 | if len(torrents) != 3 { 175 | t.Error("Number of torrents is not equal to 3") 176 | } 177 | if *torrents[2].Name != "xubuntu-18.04.2-desktop-amd64.iso" { 178 | t.Error("Unexpected torrent name") 179 | } 180 | 181 | torrents, err = transmissionbt.TorrentGetAll() 182 | Check(err) 183 | if len(torrents) != 3 { 184 | t.Error("Number of torrents is not equal to 3") 185 | } 186 | 187 | torrents, err = transmissionbt.TorrentGetAll() 188 | Check(err) 189 | if len(torrents) != 2 { 190 | t.Error("Number of torrents is not equal to 2") 191 | } 192 | 193 | id := *torrents[1].ID 194 | 195 | singleTorrent, err := transmissionbt.TorrentGetAllFor([]int64{id}) 196 | Check(err) 197 | if *singleTorrent[0].Name != "ubuntu-18.04.2-desktop-amd64.iso" { 198 | t.Error("Unexpected torrent") 199 | } 200 | } 201 | 202 | func TestSyncingRecentlyActive(t *testing.T) { 203 | const apiAddr = "http://localhost:8080" 204 | log.SetLevel(currentLogLevel) 205 | 206 | defer gock.Off() 207 | 208 | gock.Observe(gock.DumpRequest) 209 | 210 | gock.New(apiAddr). 211 | Post("/api/v2/auth/login"). 212 | Reply(200). 213 | SetHeader("Set-Cookie", "SID=1") 214 | 215 | setUpSyncEndpoint(apiAddr) 216 | 217 | // 1 218 | setUpMocks(apiAddr, "cf7da7ab4d4e6125567bd979994f13bb1f23dddd", "1") 219 | 220 | // 2 221 | setUpMocks(apiAddr, "842783e3005495d5d1637f5364b59343c7844707", "2") 222 | 223 | // 3 224 | setUpMocks(apiAddr, "7a1448be6d15bcde08ee9915350d0725775b73a3", "3") 225 | 226 | client := &http.Client{Transport: &http.Transport{}} 227 | gock.InterceptClient(client) 228 | 229 | qBTConn.Init(apiAddr, client, true) 230 | server := httptest.NewServer(http.HandlerFunc(handler)) 231 | defer server.Close() 232 | defer server.CloseClientConnections() 233 | serverAddr := server.Listener.Addr().(*net.TCPAddr) 234 | 235 | transmissionbt, err := transmissionrpc.New(serverAddr.IP.String(), "", "", 236 | &transmissionrpc.AdvancedConfig{Port: uint16(serverAddr.Port)}) 237 | Check(err) 238 | 239 | gock.New(apiAddr). 240 | Post("/api/v2/torrents/resume"). 241 | MatchType("url"). 242 | BodyString("^hashes=842783e3005495d5d1637f5364b59343c7844707%7Ccf7da7ab4d4e6125567bd979994f13bb1f23dddd$"). 243 | Times(2). 244 | Reply(200) 245 | gock.New(apiAddr). 246 | Post("/api/v2/torrents/resume"). 247 | MatchType("url"). 248 | BodyString("^hashes=842783e3005495d5d1637f5364b59343c7844707%7Ccf7da7ab4d4e6125567bd979994f13bb1f23dddd%7C7a1448be6d15bcde08ee9915350d0725775b73a3$"). 249 | Times(5). 250 | Reply(200) 251 | gock.New(apiAddr). 252 | Post("/api/v2/torrents/resume"). 253 | MatchType("url"). 254 | BodyString("^hashes=842783e3005495d5d1637f5364b59343c7844707%7Ccf7da7ab4d4e6125567bd979994f13bb1f23dddd$"). 255 | Times(1). 256 | Reply(200) 257 | 258 | for i := 0; i < 5; i++ { 259 | err = transmissionbt.TorrentStartRecentlyActive() 260 | Check(err) 261 | } 262 | } 263 | 264 | func TestSyncingRecentlyActiveLong(t *testing.T) { 265 | const apiAddr = "http://localhost:8080" 266 | log.SetLevel(currentLogLevel) 267 | 268 | defer gock.Off() 269 | 270 | prevTime := qBT.RECENTLY_ACTIVE_TIMEOUT 271 | qBT.RECENTLY_ACTIVE_TIMEOUT = 5 * time.Second 272 | 273 | gock.Observe(gock.DumpRequest) 274 | 275 | gock.New(apiAddr). 276 | Post("/api/v2/auth/login"). 277 | Reply(200). 278 | SetHeader("Set-Cookie", "SID=1") 279 | 280 | setUpSyncEndpoint(apiAddr) 281 | 282 | // 1 283 | setUpMocks(apiAddr, "cf7da7ab4d4e6125567bd979994f13bb1f23dddd", "1") 284 | 285 | // 2 286 | setUpMocks(apiAddr, "842783e3005495d5d1637f5364b59343c7844707", "2") 287 | 288 | // 3 289 | setUpMocks(apiAddr, "7a1448be6d15bcde08ee9915350d0725775b73a3", "3") 290 | 291 | client := &http.Client{Transport: &http.Transport{}} 292 | gock.InterceptClient(client) 293 | 294 | qBTConn.Init(apiAddr, client, true) 295 | server := httptest.NewServer(http.HandlerFunc(handler)) 296 | defer server.Close() 297 | defer server.CloseClientConnections() 298 | serverAddr := server.Listener.Addr().(*net.TCPAddr) 299 | 300 | transmissionbt, err := transmissionrpc.New(serverAddr.IP.String(), "", "", 301 | &transmissionrpc.AdvancedConfig{Port: uint16(serverAddr.Port)}) 302 | Check(err) 303 | 304 | torrents, err := transmissionbt.TorrentGetAll() 305 | Check(err) 306 | if len(torrents) != 2 { 307 | t.Error("Number of torrents is not equal to 2") 308 | } 309 | 310 | time.Sleep(qBT.RECENTLY_ACTIVE_TIMEOUT + 5*time.Second) 311 | 312 | gock.New(apiAddr). 313 | Post("/api/v2/torrents/resume"). 314 | MatchType("url"). 315 | BodyString("^hashes=$"). 316 | Reply(200) 317 | err = transmissionbt.TorrentStartRecentlyActive() 318 | Check(err) 319 | 320 | gock.New(apiAddr). 321 | Post("/api/v2/torrents/resume"). 322 | MatchType("url"). 323 | BodyString("^hashes=7a1448be6d15bcde08ee9915350d0725775b73a3$"). 324 | Reply(200) 325 | err = transmissionbt.TorrentStartRecentlyActive() 326 | Check(err) 327 | 328 | qBT.RECENTLY_ACTIVE_TIMEOUT = prevTime 329 | } 330 | 331 | func TestTorrentAdd(t *testing.T) { 332 | const apiAddr = "http://localhost:8080" 333 | log.SetLevel(currentLogLevel) 334 | 335 | defer gock.Off() 336 | 337 | gock.Observe(gock.DumpRequest) 338 | 339 | gock.New(apiAddr). 340 | Post("/api/v2/auth/login"). 341 | Reply(200). 342 | SetHeader("Set-Cookie", "SID=1") 343 | 344 | setUpSyncEndpoint(apiAddr) 345 | 346 | // 1 347 | setUpMocks(apiAddr, "cf7da7ab4d4e6125567bd979994f13bb1f23dddd", "1") 348 | 349 | // 2 350 | setUpMocks(apiAddr, "842783e3005495d5d1637f5364b59343c7844707", "2") 351 | 352 | // 3 353 | setUpMocks(apiAddr, "7a1448be6d15bcde08ee9915350d0725775b73a3", "3") 354 | 355 | client := &http.Client{Transport: &http.Transport{}} 356 | gock.InterceptClient(client) 357 | 358 | qBTConn.Init(apiAddr, client, true) 359 | server := httptest.NewServer(http.HandlerFunc(handler)) 360 | defer server.Close() 361 | defer server.CloseClientConnections() 362 | serverAddr := server.Listener.Addr().(*net.TCPAddr) 363 | 364 | transmissionbt, err := transmissionrpc.New(serverAddr.IP.String(), "", "", 365 | &transmissionrpc.AdvancedConfig{Port: uint16(serverAddr.Port)}) 366 | Check(err) 367 | 368 | //urlMatcher := func(req *http.Request, ereq *gock.Request) (bool, error) { 369 | // err := req.ParseMultipartForm(1 << 20) // 1 MB 370 | // Check(err) 371 | // 372 | // _, exists := req.MultipartForm.Value["urls"] 373 | // 374 | // if !exists { 375 | // return false, errors.New("No URL value") 376 | // } 377 | // return true, nil 378 | //} 379 | 380 | gock.New(apiAddr). 381 | Post("/api/v2/torrents/add"). 382 | MatchType("form"). 383 | BodyString("Content-Disposition: form-data; name=\"urls\""). 384 | Reply(200) 385 | 386 | magnet := "magnet:?xt=urn:btih:7a1448be6d15bcde08ee9915350d0725775b73a3&dn=xubuntu-18.04.2-desktop-amd64.iso&tr=http%3a%2f%2ftorrent.ubuntu.com%3a6969%2fannounce" 387 | 388 | torrent, err := transmissionbt.TorrentAdd(&transmissionrpc.TorrentAddPayload{Filename: &magnet}) 389 | Check(err) 390 | 391 | if *torrent.Name != "xubuntu-18.04.2-desktop-amd64.iso" { 392 | t.Error("Unexpected torrent") 393 | } 394 | 395 | gock.New(apiAddr). 396 | Post("/api/v2/torrents/add"). 397 | MatchType("form"). 398 | BodyString("Content-Disposition: form-data; name=\"sequentialDownload\""). 399 | Reply(200) 400 | 401 | location := "/home/user/+sf" 402 | 403 | torrent, err = transmissionbt.TorrentAdd(&transmissionrpc.TorrentAddPayload{Filename: &magnet, DownloadDir: &location}) 404 | Check(err) 405 | 406 | if *torrent.Name != "xubuntu-18.04.2-desktop-amd64.iso" { 407 | t.Error("Unexpected torrent") 408 | } 409 | } 410 | 411 | func TestTorrentMove(t *testing.T) { 412 | const apiAddr = "http://localhost:8080" 413 | log.SetLevel(currentLogLevel) 414 | 415 | defer gock.Off() 416 | gock.Flush() 417 | gock.CleanUnmatchedRequest() 418 | 419 | gock.Observe(gock.DumpRequest) 420 | 421 | gock.New(apiAddr). 422 | Post("/api/v2/auth/login"). 423 | Reply(200). 424 | SetHeader("Set-Cookie", "SID=1") 425 | 426 | gock.New(apiAddr). 427 | Get("/api/v2/torrents/info"). 428 | Reply(200). 429 | File("testdata/torrent_list.json") 430 | 431 | client := &http.Client{Transport: &http.Transport{}} 432 | gock.InterceptClient(client) 433 | 434 | qBTConn.Init(apiAddr, client, false) 435 | server := httptest.NewServer(http.HandlerFunc(handler)) 436 | defer server.Close() 437 | defer server.CloseClientConnections() 438 | serverAddr := server.Listener.Addr().(*net.TCPAddr) 439 | 440 | transmissionbt, err := transmissionrpc.New(serverAddr.IP.String(), "", "", 441 | &transmissionrpc.AdvancedConfig{Port: uint16(serverAddr.Port)}) 442 | Check(err) 443 | 444 | gock.New(apiAddr). 445 | Post("/api/v2/torrents/toggleFirstLastPiecePrio"). 446 | MatchType("url"). 447 | BodyString("hashes=842783e3005495d5d1637f5364b59343c7844707"). 448 | Reply(200) 449 | 450 | gock.New(apiAddr). 451 | Post("/api/v2/torrents/toggleSequentialDownload"). 452 | MatchType("url"). 453 | BodyString("hashes=842783e3005495d5d1637f5364b59343c7844707"). 454 | Reply(200) 455 | 456 | gock.New(apiAddr). 457 | Post("/api/v2/torrents/setLocation"). 458 | MatchType("url"). 459 | BodyString("hashes=842783e3005495d5d1637f5364b59343c7844707&location=%2Fnew%2Fdir"). 460 | Reply(200) 461 | 462 | err = transmissionbt.TorrentSetLocationHash("842783e3005495d5d1637f5364b59343c7844707", "/new/dir+sf", true) 463 | Check(err) 464 | 465 | p := gock.Pending() 466 | println(p) 467 | 468 | if gock.IsPending() { 469 | t.Fail() 470 | } 471 | } 472 | 473 | func setUpSyncEndpoint(apiAddr string) { 474 | gock.New(apiAddr). 475 | Get("/api/v2/sync/maindata"). 476 | MatchParam("rid", "0"). 477 | HeaderPresent("Cookie"). 478 | Reply(200). 479 | File("testdata/sync_initial.json") 480 | gock.New(apiAddr). 481 | Get("/api/v2/sync/maindata"). 482 | MatchParam("rid", "1"). 483 | HeaderPresent("Cookie"). 484 | Reply(200). 485 | File("testdata/sync_1.json") 486 | gock.New(apiAddr). 487 | Get("/api/v2/sync/maindata"). 488 | MatchParam("rid", "2"). 489 | HeaderPresent("Cookie"). 490 | Reply(200). 491 | File("testdata/sync_2.json") 492 | gock.New(apiAddr). 493 | Get("/api/v2/sync/maindata"). 494 | MatchParam("rid", "3"). 495 | HeaderPresent("Cookie"). 496 | Reply(200). 497 | File("testdata/sync_3.json") 498 | gock.New(apiAddr). 499 | Get("/api/v2/sync/maindata"). 500 | MatchParam("rid", "4"). 501 | HeaderPresent("Cookie"). 502 | Reply(200). 503 | File("testdata/sync_4.json") 504 | gock.New(apiAddr). 505 | Get("/api/v2/sync/maindata"). 506 | MatchParam("rid", "5"). 507 | HeaderPresent("Cookie"). 508 | Reply(200). 509 | File("testdata/sync_5.json") 510 | } 511 | 512 | func TestSyncingRepeated(t *testing.T) { 513 | prevLogLevel := currentLogLevel 514 | currentLogLevel = log.ErrorLevel 515 | for i := 0; i < 50; i++ { 516 | TestSyncing(t) 517 | } 518 | currentLogLevel = prevLogLevel 519 | } 520 | 521 | func setUpMocks(apiAddr string, hash string, name string) { 522 | gock.New(apiAddr). 523 | Get("/api/v2/torrents/properties"). 524 | MatchParam("hash", hash). 525 | Persist(). 526 | Reply(200). 527 | File("testdata/torrent_" + name + "_properties.json") 528 | gock.New(apiAddr). 529 | Get("/api/v2/torrents/trackers"). 530 | MatchParam("hash", hash). 531 | Persist(). 532 | Reply(200). 533 | File("testdata/torrent_" + name + "_trackers.json") 534 | gock.New(apiAddr). 535 | Get("/api/v2/torrents/pieceStates"). 536 | MatchParam("hash", hash). 537 | Persist(). 538 | Reply(200). 539 | File("testdata/torrent_" + name + "_piecestates.json") 540 | gock.New(apiAddr). 541 | Get("/api/v2/torrents/files"). 542 | MatchParam("hash", hash). 543 | Persist(). 544 | Reply(200). 545 | File("testdata/torrent_" + name + "_files.json") 546 | gock.New(apiAddr). 547 | Get("/api/v2/sync/torrentPeers"). 548 | MatchParam("hash", hash). 549 | MatchParam("rid", "0"). 550 | Persist(). 551 | Reply(200). 552 | File("testdata/torrent_" + name + "_peers.json") 553 | } 554 | -------------------------------------------------------------------------------- /reflection/metainfo.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bytes" 5 | "crypto/sha1" 6 | "fmt" 7 | "github.com/jackpal/bencode-go" 8 | "io" 9 | "time" 10 | ) 11 | 12 | // Structs into which torrent metafile is 13 | // parsed and stored into. 14 | type FileDict struct { 15 | Length int64 "length" 16 | Path []string "path" 17 | Md5sum string "md5sum" 18 | } 19 | 20 | type InfoDict struct { 21 | FileDuration []int64 "file-duration" 22 | FileMedia []int64 "file-media" 23 | // Single file 24 | Name string "name" 25 | Length int64 "length" 26 | Md5sum string "md5sum" 27 | // Multiple files 28 | Files []FileDict "files" 29 | PieceLength int64 "piece length" 30 | Pieces string "pieces" 31 | Private int64 "private" 32 | } 33 | 34 | type MetaInfo struct { 35 | Info InfoDict "info" 36 | InfoHash string "info hash" 37 | Announce string "announce" 38 | AnnounceList [][]string "announce-list" 39 | CreationDate int64 "creation date" 40 | Comment string "comment" 41 | CreatedBy string "created by" 42 | Encoding string "encoding" 43 | } 44 | 45 | // Open .torrent file, un-bencode it and load them into MetaInfo struct. 46 | func (metaInfo *MetaInfo) ReadTorrentMetaInfoFile(src io.Reader) bool { 47 | // Decode bencoded metainfo file. 48 | fileMetaData, er := bencode.Decode(src) 49 | if er != nil { 50 | return false 51 | } 52 | 53 | // fileMetaData is map of maps of... maps. Get top level map. 54 | metaInfoMap, ok := fileMetaData.(map[string]interface{}) 55 | if !ok { 56 | return false 57 | } 58 | 59 | // Enumerate through child maps. 60 | var bytesBuf bytes.Buffer 61 | for mapKey, mapVal := range metaInfoMap { 62 | switch mapKey { 63 | case "info": 64 | if er = bencode.Marshal(&bytesBuf, mapVal); er != nil { 65 | return false 66 | } 67 | 68 | infoHash := sha1.New() 69 | infoHash.Write(bytesBuf.Bytes()) 70 | metaInfo.InfoHash = string(infoHash.Sum(nil)) 71 | 72 | if er = bencode.Unmarshal(&bytesBuf, &metaInfo.Info); er != nil { 73 | return false 74 | } 75 | 76 | case "announce-list": 77 | if er = bencode.Marshal(&bytesBuf, mapVal); er != nil { 78 | return false 79 | } 80 | if er = bencode.Unmarshal(&bytesBuf, &metaInfo.AnnounceList); er != nil { 81 | return false 82 | } 83 | 84 | case "announce": 85 | metaInfo.Announce = mapVal.(string) 86 | 87 | case "creation date": 88 | metaInfo.CreationDate = mapVal.(int64) 89 | 90 | case "comment": 91 | metaInfo.Comment = mapVal.(string) 92 | 93 | case "created by": 94 | metaInfo.CreatedBy = mapVal.(string) 95 | 96 | case "encoding": 97 | metaInfo.Encoding = mapVal.(string) 98 | } 99 | } 100 | 101 | return true 102 | } 103 | 104 | // Print torrent meta info struct data. 105 | func (metaInfo *MetaInfo) DumpTorrentMetaInfo() { 106 | fmt.Println("Announce:", metaInfo.Announce) 107 | fmt.Println("Announce List:") 108 | for _, anncListEntry := range metaInfo.AnnounceList { 109 | for _, elem := range anncListEntry { 110 | fmt.Println(" ", elem) 111 | } 112 | } 113 | strCreationDate := time.Unix(metaInfo.CreationDate, 0) 114 | fmt.Println("Creation Date:", strCreationDate) 115 | fmt.Println("Comment:", metaInfo.Comment) 116 | fmt.Println("Created By:", metaInfo.CreatedBy) 117 | fmt.Println("Encoding:", metaInfo.Encoding) 118 | fmt.Printf("InfoHash: %X\n", metaInfo.InfoHash) 119 | fmt.Println("Info:") 120 | fmt.Println(" Piece Length:", metaInfo.Info.PieceLength) 121 | piecesList := metaInfo.getPiecesList() 122 | fmt.Printf(" Pieces:%X -- %X\n", len(piecesList), len(metaInfo.Info.Pieces)/20) 123 | fmt.Println(" File Duration:", metaInfo.Info.FileDuration) 124 | fmt.Println(" File Media:", metaInfo.Info.FileMedia) 125 | fmt.Println(" Private:", metaInfo.Info.Private) 126 | fmt.Println(" Name:", metaInfo.Info.Name) 127 | fmt.Println(" Length:", metaInfo.Info.Length) 128 | fmt.Println(" Md5sum:", metaInfo.Info.Md5sum) 129 | fmt.Println(" Files:") 130 | for _, fileDict := range metaInfo.Info.Files { 131 | fmt.Println(" Length:", fileDict.Length) 132 | fmt.Println(" Path:", fileDict.Path) 133 | fmt.Println(" Md5sum:", fileDict.Md5sum) 134 | } 135 | } 136 | 137 | // Splits pieces string into an array of 20 byte SHA1 hashes. 138 | func (metaInfo *MetaInfo) getPiecesList() []string { 139 | var piecesList []string 140 | piecesLen := len(metaInfo.Info.Pieces) 141 | for i, j := 0, 0; i < piecesLen; i, j = i+20, j+1 { 142 | piecesList = append(piecesList, metaInfo.Info.Pieces[i:i+19]) 143 | } 144 | return piecesList 145 | } 146 | -------------------------------------------------------------------------------- /reflection/testdata/expected_response.json: -------------------------------------------------------------------------------- 1 | { 2 | "arguments": { 3 | "torrents": [ 4 | { 5 | "activityDate": 1443977197, 6 | "addedDate": 1557767746, 7 | "bandwidthPriority": 0, 8 | "comment": "Ubuntu CD releases.ubuntu.com\n ----- \nSequential download: no\nFirst and last pieces first: no", 9 | "corruptEver": 504870, 10 | "creator": "", 11 | "dateCreated": 1550184717, 12 | "desiredAvailable": 1937899520, 13 | "doneDate": -1, 14 | "downloadDir": "/home/artyom/Downloads/", 15 | "downloadLimit": 0, 16 | "downloadLimited": false, 17 | "downloadedEver": 56137484, 18 | "error": 0, 19 | "errorString": "", 20 | "eta": 8640000, 21 | "etaIdle": 0, 22 | "fileStats": [ 23 | { 24 | "bytesCompleted": 39845888, 25 | "priority": 0, 26 | "wanted": true 27 | } 28 | ], 29 | "files": [ 30 | { 31 | "bytesCompleted": 39845888, 32 | "length": 1996488704, 33 | "name": "ubuntu-18.04.2-desktop-amd64.iso" 34 | } 35 | ], 36 | "hashString": "cf7da7ab4d4e6125567bd979994f13bb1f23dddd", 37 | "haveUnchecked": 0, 38 | "haveValid": 39845888, 39 | "honorsSessionLimits": true, 40 | "id": 1, 41 | "isFinished": false, 42 | "isPrivate": false, 43 | "isStalled": false, 44 | "leftUntilDone": 1937899520, 45 | "magnetLink": "", 46 | "manualAnnounceTime": 0, 47 | "maxConnectedPeers": 100, 48 | "metadataPercentComplete": 0, 49 | "name": "ubuntu-18.04.2-desktop-amd64.iso", 50 | "peer-limit": 100, 51 | "peers": [], 52 | "peersConnected": 0, 53 | "peersFrom": {}, 54 | "peersGettingFromUs": 0, 55 | "peersSendingToUs": 0, 56 | "percentDone": 0.02934611344537815, 57 | "pieceCount": 3808, 58 | "pieceSize": 524288, 59 | "pieces": "UyAAAAAAAAAAf1cBKxOQB8IAAAAAAAAEAAEAACAAEAAAAAAEAAAAAAAQAAAAAAAAAAAAIAAEAEAAAAAAAAAAEAQAAEAACAAAAAAAAAAAACAAAAAAQAAAAAAAAAAAIAAAAAAAAAABAAAAABAAAAAAAAAAAAAAAIAAAABAAAAAAAEAQACIAAAAAAAAAAAAAAAEAAAgAAAAAAAAAAAAACAAAAAAAAAAABAAAABAAAAAAAAAAgAAAAAAAAAAAABABAAAAAAIAAACAAAAAAAAAAAgAAgAAAAAAUAAAAQAAAAAAAAAIAAAAAAAAAAAAIAEAAAAAAAAgAAAAAAAAARAAAAIAAQAAAAAIAAAACAAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAIAAAAAAAAAAwAAAAAAAAAFAAAAAAAAAAcAAAAAAAAADAAAAAAAAAANAAAAAAAAAA4AAAAAAAAAEwAAAAAAAAAVAAAAAAAAABYAAAAAAAAAFwAAAAAAAAAbAAAAAAAAABwAAAAAAAAAHwAAAAAAAAAhAAAAAAAAACIAAAAAAAAAIwAAAAAAAAAmAAAAAAAAACgAAAAAAAAALAAAAAAAAAAtAAAAAAAAADAAAAAAAAAAMQAAAAAAAAAyAAAAAAAAADMAAAAAAAAANAAAAAAAAAA1AAAAAAAAADcAAAAAAAAAOAAAAAAAAAA6AAAAAAAAAA==", 60 | "priorities": [ 61 | 0 62 | ], 63 | "queuePosition": 1, 64 | "rateDownload": 0, 65 | "rateUpload": 0, 66 | "recheckProgress": 0, 67 | "secondsDownloading": 500, 68 | "secondsSeeding": 80000, 69 | "seedIdleLimit": 10, 70 | "seedIdleMode": 0, 71 | "seedRatioLimit": 2, 72 | "seedRatioMode": 0, 73 | "sizeWhenDone": 1996488704, 74 | "startDate": 1557767746, 75 | "status": 0, 76 | "torrentFile": 0, 77 | "totalSize": 1996488704, 78 | "trackerStats": [ 79 | { 80 | "announce": "** [DHT] **", 81 | "announceState": 0, 82 | "downloadCount": 2110, 83 | "hasAnnounced": true, 84 | "hasScraped": false, 85 | "host": "** [DHT] **", 86 | "id": 0, 87 | "isBackup": false, 88 | "lastAnnouncePeerCount": 2147, 89 | "lastAnnounceResult": "Tracker has been contacted and is working", 90 | "lastAnnounceStartTime": 0, 91 | "lastAnnounceSucceeded": true, 92 | "lastAnnounceTime": 0, 93 | "lastAnnounceTimedOut": false, 94 | "lastScrapeResult": "", 95 | "lastScrapeStartTime": 0, 96 | "lastScrapeSucceeded": false, 97 | "lastScrapeTime": 0, 98 | "lastScrapeTimedOut": 0, 99 | "leecherCount": 37, 100 | "nextAnnounceTime": 0, 101 | "nextScrapeTime": 0, 102 | "scrape": "", 103 | "scrapeState": 2, 104 | "seederCount": 2110, 105 | "tier": 0 106 | }, 107 | { 108 | "announce": "** [PeX] **", 109 | "announceState": 0, 110 | "downloadCount": 2110, 111 | "hasAnnounced": true, 112 | "hasScraped": false, 113 | "host": "** [PeX] **", 114 | "id": 1, 115 | "isBackup": false, 116 | "lastAnnouncePeerCount": 2147, 117 | "lastAnnounceResult": "Tracker has been contacted and is working", 118 | "lastAnnounceStartTime": 0, 119 | "lastAnnounceSucceeded": true, 120 | "lastAnnounceTime": 0, 121 | "lastAnnounceTimedOut": false, 122 | "lastScrapeResult": "", 123 | "lastScrapeStartTime": 0, 124 | "lastScrapeSucceeded": false, 125 | "lastScrapeTime": 0, 126 | "lastScrapeTimedOut": 0, 127 | "leecherCount": 37, 128 | "nextAnnounceTime": 0, 129 | "nextScrapeTime": 0, 130 | "scrape": "", 131 | "scrapeState": 2, 132 | "seederCount": 2110, 133 | "tier": 0 134 | }, 135 | { 136 | "announce": "** [LSD] **", 137 | "announceState": 0, 138 | "downloadCount": 2110, 139 | "hasAnnounced": true, 140 | "hasScraped": false, 141 | "host": "** [LSD] **", 142 | "id": 2, 143 | "isBackup": false, 144 | "lastAnnouncePeerCount": 2147, 145 | "lastAnnounceResult": "Tracker has been contacted and is working", 146 | "lastAnnounceStartTime": 0, 147 | "lastAnnounceSucceeded": true, 148 | "lastAnnounceTime": 0, 149 | "lastAnnounceTimedOut": false, 150 | "lastScrapeResult": "", 151 | "lastScrapeStartTime": 0, 152 | "lastScrapeSucceeded": false, 153 | "lastScrapeTime": 0, 154 | "lastScrapeTimedOut": 0, 155 | "leecherCount": 37, 156 | "nextAnnounceTime": 0, 157 | "nextScrapeTime": 0, 158 | "scrape": "", 159 | "scrapeState": 2, 160 | "seederCount": 2110, 161 | "tier": 0 162 | }, 163 | { 164 | "announce": "http://torrent.ubuntu.com:6969/announce", 165 | "announceState": 0, 166 | "downloadCount": 2110, 167 | "hasAnnounced": true, 168 | "hasScraped": false, 169 | "host": "http://torrent.ubuntu.com:6969/announce", 170 | "id": 3, 171 | "isBackup": false, 172 | "lastAnnouncePeerCount": 2147, 173 | "lastAnnounceResult": "Tracker has been contacted and is working", 174 | "lastAnnounceStartTime": 0, 175 | "lastAnnounceSucceeded": true, 176 | "lastAnnounceTime": 0, 177 | "lastAnnounceTimedOut": false, 178 | "lastScrapeResult": "", 179 | "lastScrapeStartTime": 0, 180 | "lastScrapeSucceeded": false, 181 | "lastScrapeTime": 0, 182 | "lastScrapeTimedOut": 0, 183 | "leecherCount": 37, 184 | "nextAnnounceTime": 0, 185 | "nextScrapeTime": 0, 186 | "scrape": "", 187 | "scrapeState": 2, 188 | "seederCount": 2110, 189 | "tier": 0 190 | }, 191 | { 192 | "announce": "http://ipv6.torrent.ubuntu.com:6969/announce", 193 | "announceState": 0, 194 | "downloadCount": 2110, 195 | "hasAnnounced": false, 196 | "hasScraped": false, 197 | "host": "http://ipv6.torrent.ubuntu.com:6969/announce", 198 | "id": 4, 199 | "isBackup": false, 200 | "lastAnnouncePeerCount": 2147, 201 | "lastAnnounceResult": "Tracker has been contacted, but it is not working (or doesn't send proper replies)", 202 | "lastAnnounceStartTime": 0, 203 | "lastAnnounceSucceeded": false, 204 | "lastAnnounceTime": 0, 205 | "lastAnnounceTimedOut": false, 206 | "lastScrapeResult": "", 207 | "lastScrapeStartTime": 0, 208 | "lastScrapeSucceeded": false, 209 | "lastScrapeTime": 0, 210 | "lastScrapeTimedOut": 0, 211 | "leecherCount": 37, 212 | "nextAnnounceTime": 0, 213 | "nextScrapeTime": 0, 214 | "scrape": "", 215 | "scrapeState": 2, 216 | "seederCount": 2110, 217 | "tier": 0 218 | } 219 | ], 220 | "trackers": [ 221 | { 222 | "announce": "** [DHT] **", 223 | "id": 0, 224 | "scrape": "** [DHT] **", 225 | "tier": 0 226 | }, 227 | { 228 | "announce": "** [PeX] **", 229 | "id": 1, 230 | "scrape": "** [PeX] **", 231 | "tier": 0 232 | }, 233 | { 234 | "announce": "** [LSD] **", 235 | "id": 2, 236 | "scrape": "** [LSD] **", 237 | "tier": 0 238 | }, 239 | { 240 | "announce": "http://torrent.ubuntu.com:6969/announce", 241 | "id": 3, 242 | "scrape": "http://torrent.ubuntu.com:6969/announce", 243 | "tier": 0 244 | }, 245 | { 246 | "announce": "http://ipv6.torrent.ubuntu.com:6969/announce", 247 | "id": 4, 248 | "scrape": "http://ipv6.torrent.ubuntu.com:6969/announce", 249 | "tier": 0 250 | } 251 | ], 252 | "uploadLimit": 0, 253 | "uploadLimited": false, 254 | "uploadRatio": 0, 255 | "uploadedEver": 0, 256 | "wanted": [ 257 | 1 258 | ], 259 | "webseeds": [], 260 | "webseedsSendingToUs": 0 261 | }, 262 | { 263 | "activityDate": 1443977197, 264 | "addedDate": 1557772572, 265 | "bandwidthPriority": 0, 266 | "comment": "Ubuntu CD releases.ubuntu.com\n ----- \nSequential download: no\nFirst and last pieces first: no", 267 | "corruptEver": 0, 268 | "creator": "", 269 | "dateCreated": 1550184797, 270 | "desiredAvailable": 874512384, 271 | "doneDate": -1, 272 | "downloadDir": "/home/artyom/Downloads/", 273 | "downloadLimit": 0, 274 | "downloadLimited": false, 275 | "downloadedEver": 0, 276 | "error": 0, 277 | "errorString": "", 278 | "eta": 8640000, 279 | "etaIdle": 0, 280 | "fileStats": [ 281 | { 282 | "bytesCompleted": 0, 283 | "priority": 0, 284 | "wanted": true 285 | } 286 | ], 287 | "files": [ 288 | { 289 | "bytesCompleted": 0, 290 | "length": 874512384, 291 | "name": "ubuntu-18.04.2-live-server-amd64.iso" 292 | } 293 | ], 294 | "hashString": "842783e3005495d5d1637f5364b59343c7844707", 295 | "haveUnchecked": 0, 296 | "haveValid": 0, 297 | "honorsSessionLimits": true, 298 | "id": 2, 299 | "isFinished": false, 300 | "isPrivate": false, 301 | "isStalled": false, 302 | "leftUntilDone": 874512384, 303 | "magnetLink": "", 304 | "manualAnnounceTime": 0, 305 | "maxConnectedPeers": 100, 306 | "metadataPercentComplete": 0, 307 | "name": "ubuntu-18.04.2-live-server-amd64.iso", 308 | "peer-limit": 100, 309 | "peers": [], 310 | "peersConnected": 0, 311 | "peersFrom": {}, 312 | "peersGettingFromUs": 0, 313 | "peersSendingToUs": 0, 314 | "percentDone": 0, 315 | "pieceCount": 1668, 316 | "pieceSize": 524288, 317 | "pieces": "UwAAAAAAAAAAAAAAAAAAAAA=", 318 | "priorities": [ 319 | 0 320 | ], 321 | "queuePosition": 2, 322 | "rateDownload": 0, 323 | "rateUpload": 0, 324 | "recheckProgress": 0, 325 | "secondsDownloading": 500, 326 | "secondsSeeding": 80000, 327 | "seedIdleLimit": 10, 328 | "seedIdleMode": 0, 329 | "seedRatioLimit": 2, 330 | "seedRatioMode": 0, 331 | "sizeWhenDone": 874512384, 332 | "startDate": 1557772572, 333 | "status": 0, 334 | "torrentFile": 0, 335 | "totalSize": 874512384, 336 | "trackerStats": [ 337 | { 338 | "announce": "** [DHT] **", 339 | "announceState": 0, 340 | "downloadCount": 0, 341 | "hasAnnounced": true, 342 | "hasScraped": false, 343 | "host": "** [DHT] **", 344 | "id": 0, 345 | "isBackup": false, 346 | "lastAnnouncePeerCount": 0, 347 | "lastAnnounceResult": "Tracker has been contacted and is working", 348 | "lastAnnounceStartTime": 0, 349 | "lastAnnounceSucceeded": true, 350 | "lastAnnounceTime": 0, 351 | "lastAnnounceTimedOut": false, 352 | "lastScrapeResult": "", 353 | "lastScrapeStartTime": 0, 354 | "lastScrapeSucceeded": false, 355 | "lastScrapeTime": 0, 356 | "lastScrapeTimedOut": 0, 357 | "leecherCount": 0, 358 | "nextAnnounceTime": 0, 359 | "nextScrapeTime": 0, 360 | "scrape": "", 361 | "scrapeState": 2, 362 | "seederCount": 0, 363 | "tier": 0 364 | }, 365 | { 366 | "announce": "** [PeX] **", 367 | "announceState": 0, 368 | "downloadCount": 0, 369 | "hasAnnounced": true, 370 | "hasScraped": false, 371 | "host": "** [PeX] **", 372 | "id": 1, 373 | "isBackup": false, 374 | "lastAnnouncePeerCount": 0, 375 | "lastAnnounceResult": "Tracker has been contacted and is working", 376 | "lastAnnounceStartTime": 0, 377 | "lastAnnounceSucceeded": true, 378 | "lastAnnounceTime": 0, 379 | "lastAnnounceTimedOut": false, 380 | "lastScrapeResult": "", 381 | "lastScrapeStartTime": 0, 382 | "lastScrapeSucceeded": false, 383 | "lastScrapeTime": 0, 384 | "lastScrapeTimedOut": 0, 385 | "leecherCount": 0, 386 | "nextAnnounceTime": 0, 387 | "nextScrapeTime": 0, 388 | "scrape": "", 389 | "scrapeState": 2, 390 | "seederCount": 0, 391 | "tier": 0 392 | }, 393 | { 394 | "announce": "** [LSD] **", 395 | "announceState": 0, 396 | "downloadCount": 0, 397 | "hasAnnounced": true, 398 | "hasScraped": false, 399 | "host": "** [LSD] **", 400 | "id": 2, 401 | "isBackup": false, 402 | "lastAnnouncePeerCount": 0, 403 | "lastAnnounceResult": "Tracker has been contacted and is working", 404 | "lastAnnounceStartTime": 0, 405 | "lastAnnounceSucceeded": true, 406 | "lastAnnounceTime": 0, 407 | "lastAnnounceTimedOut": false, 408 | "lastScrapeResult": "", 409 | "lastScrapeStartTime": 0, 410 | "lastScrapeSucceeded": false, 411 | "lastScrapeTime": 0, 412 | "lastScrapeTimedOut": 0, 413 | "leecherCount": 0, 414 | "nextAnnounceTime": 0, 415 | "nextScrapeTime": 0, 416 | "scrape": "", 417 | "scrapeState": 2, 418 | "seederCount": 0, 419 | "tier": 0 420 | }, 421 | { 422 | "announce": "http://torrent.ubuntu.com:6969/announce", 423 | "announceState": 0, 424 | "downloadCount": 0, 425 | "hasAnnounced": false, 426 | "hasScraped": false, 427 | "host": "http://torrent.ubuntu.com:6969/announce", 428 | "id": 3, 429 | "isBackup": false, 430 | "lastAnnouncePeerCount": 0, 431 | "lastAnnounceResult": "Tracker has not been contacted yet", 432 | "lastAnnounceStartTime": 0, 433 | "lastAnnounceSucceeded": false, 434 | "lastAnnounceTime": 0, 435 | "lastAnnounceTimedOut": false, 436 | "lastScrapeResult": "", 437 | "lastScrapeStartTime": 0, 438 | "lastScrapeSucceeded": false, 439 | "lastScrapeTime": 0, 440 | "lastScrapeTimedOut": 0, 441 | "leecherCount": 0, 442 | "nextAnnounceTime": 0, 443 | "nextScrapeTime": 0, 444 | "scrape": "", 445 | "scrapeState": 2, 446 | "seederCount": 0, 447 | "tier": 0 448 | }, 449 | { 450 | "announce": "http://ipv6.torrent.ubuntu.com:6969/announce", 451 | "announceState": 0, 452 | "downloadCount": 0, 453 | "hasAnnounced": false, 454 | "hasScraped": false, 455 | "host": "http://ipv6.torrent.ubuntu.com:6969/announce", 456 | "id": 4, 457 | "isBackup": false, 458 | "lastAnnouncePeerCount": 0, 459 | "lastAnnounceResult": "Tracker has not been contacted yet", 460 | "lastAnnounceStartTime": 0, 461 | "lastAnnounceSucceeded": false, 462 | "lastAnnounceTime": 0, 463 | "lastAnnounceTimedOut": false, 464 | "lastScrapeResult": "", 465 | "lastScrapeStartTime": 0, 466 | "lastScrapeSucceeded": false, 467 | "lastScrapeTime": 0, 468 | "lastScrapeTimedOut": 0, 469 | "leecherCount": 0, 470 | "nextAnnounceTime": 0, 471 | "nextScrapeTime": 0, 472 | "scrape": "", 473 | "scrapeState": 2, 474 | "seederCount": 0, 475 | "tier": 0 476 | } 477 | ], 478 | "trackers": [ 479 | { 480 | "announce": "** [DHT] **", 481 | "id": 0, 482 | "scrape": "** [DHT] **", 483 | "tier": 0 484 | }, 485 | { 486 | "announce": "** [PeX] **", 487 | "id": 1, 488 | "scrape": "** [PeX] **", 489 | "tier": 0 490 | }, 491 | { 492 | "announce": "** [LSD] **", 493 | "id": 2, 494 | "scrape": "** [LSD] **", 495 | "tier": 0 496 | }, 497 | { 498 | "announce": "http://torrent.ubuntu.com:6969/announce", 499 | "id": 3, 500 | "scrape": "http://torrent.ubuntu.com:6969/announce", 501 | "tier": 0 502 | }, 503 | { 504 | "announce": "http://ipv6.torrent.ubuntu.com:6969/announce", 505 | "id": 4, 506 | "scrape": "http://ipv6.torrent.ubuntu.com:6969/announce", 507 | "tier": 0 508 | } 509 | ], 510 | "uploadLimit": 0, 511 | "uploadLimited": false, 512 | "uploadRatio": 0, 513 | "uploadedEver": 0, 514 | "wanted": [ 515 | 1 516 | ], 517 | "webseeds": [], 518 | "webseedsSendingToUs": 0 519 | } 520 | ] 521 | }, 522 | "result": "success", 523 | "tag": 5976970723859768750 524 | } -------------------------------------------------------------------------------- /reflection/testdata/sync_1.json: -------------------------------------------------------------------------------- 1 | { 2 | "rid": 2, 3 | "server_state": { 4 | "alltime_dl": 12509583145, 5 | "alltime_ul": 2764045485, 6 | "free_space_on_disk": 71111356416 7 | } 8 | } -------------------------------------------------------------------------------- /reflection/testdata/sync_2.json: -------------------------------------------------------------------------------- 1 | { 2 | "rid": 3, 3 | "server_state": { 4 | "alltime_dl": 12509583817, 5 | "alltime_ul": 2764046157 6 | }, 7 | "torrents": { 8 | "7a1448be6d15bcde08ee9915350d0725775b73a3": { 9 | "added_on": 1561400225, 10 | "amount_left": 1474953216, 11 | "auto_tmm": false, 12 | "category": "", 13 | "completed": 0, 14 | "completion_on": 4294967295, 15 | "dl_limit": -1, 16 | "dlspeed": 0, 17 | "downloaded": 0, 18 | "downloaded_session": 0, 19 | "eta": 8640000, 20 | "f_l_piece_prio": false, 21 | "force_start": false, 22 | "last_activity": 0, 23 | "magnet_uri": "magnet:?xt=urn:btih:7a1448be6d15bcde08ee9915350d0725775b73a3&dn=xubuntu-18.04.2-desktop-amd64.iso&tr=http%3a%2f%2ftorrent.ubuntu.com%3a6969%2fannounce", 24 | "max_ratio": -1, 25 | "max_seeding_time": -1, 26 | "name": "xubuntu-18.04.2-desktop-amd64.iso", 27 | "num_complete": 0, 28 | "num_incomplete": 0, 29 | "num_leechs": 0, 30 | "num_seeds": 0, 31 | "priority": 3, 32 | "progress": 0, 33 | "ratio": 0, 34 | "ratio_limit": -2, 35 | "save_path": "/home/artyom/Downloads/", 36 | "seeding_time_limit": -2, 37 | "seen_complete": 4294967295, 38 | "seq_dl": false, 39 | "size": 1474953216, 40 | "state": "pausedDL", 41 | "super_seeding": false, 42 | "tags": "", 43 | "time_active": 0, 44 | "total_size": 1474953216, 45 | "tracker": "", 46 | "up_limit": -1, 47 | "uploaded": 0, 48 | "uploaded_session": 0, 49 | "upspeed": 0 50 | } 51 | } 52 | } -------------------------------------------------------------------------------- /reflection/testdata/sync_3.json: -------------------------------------------------------------------------------- 1 | { 2 | "rid": 4, 3 | "server_state": { 4 | "alltime_dl": 12509584153, 5 | "alltime_ul": 2764046493, 6 | "free_space_on_disk": 71110914048 7 | } 8 | } -------------------------------------------------------------------------------- /reflection/testdata/sync_4.json: -------------------------------------------------------------------------------- 1 | { 2 | "rid": 5, 3 | "server_state": { 4 | "alltime_dl": 12509584433, 5 | "alltime_ul": 2764046829, 6 | "free_space_on_disk": 71110983680 7 | }, 8 | "torrents_removed": [ 9 | "7a1448be6d15bcde08ee9915350d0725775b73a3" 10 | ] 11 | } -------------------------------------------------------------------------------- /reflection/testdata/sync_5.json: -------------------------------------------------------------------------------- 1 | { 2 | "rid": 6 3 | } -------------------------------------------------------------------------------- /reflection/testdata/sync_initial.json: -------------------------------------------------------------------------------- 1 | { 2 | "categories": {}, 3 | "full_update": true, 4 | "rid": 1, 5 | "server_state": { 6 | "alltime_dl": 12509582837, 7 | "alltime_ul": 2764045149, 8 | "average_time_queue": 264, 9 | "connection_status": "firewalled", 10 | "dht_nodes": 398, 11 | "dl_info_data": 59094054, 12 | "dl_info_speed": 0, 13 | "dl_rate_limit": 8192000, 14 | "free_space_on_disk": 71112196096, 15 | "global_ratio": "0,22", 16 | "queued_io_jobs": 0, 17 | "queueing": true, 18 | "read_cache_hits": "0", 19 | "read_cache_overload": "0", 20 | "refresh_interval": 1500, 21 | "total_buffers_size": 0, 22 | "total_peer_connections": 0, 23 | "total_queued_size": 0, 24 | "total_wasted_session": 504870, 25 | "up_info_data": 0, 26 | "up_info_speed": 0, 27 | "up_rate_limit": 0, 28 | "use_alt_speed_limits": false, 29 | "write_cache_overload": "0" 30 | }, 31 | "torrents": { 32 | "842783e3005495d5d1637f5364b59343c7844707": { 33 | "added_on": 1557772572, 34 | "amount_left": 874512384, 35 | "auto_tmm": false, 36 | "category": "", 37 | "completed": 0, 38 | "completion_on": 4294967295, 39 | "dl_limit": -1, 40 | "dlspeed": 0, 41 | "downloaded": 0, 42 | "downloaded_session": 0, 43 | "eta": 8640000, 44 | "f_l_piece_prio": false, 45 | "force_start": false, 46 | "last_activity": 0, 47 | "magnet_uri": "magnet:?xt=urn:btih:842783e3005495d5d1637f5364b59343c7844707&dn=ubuntu-18.04.2-live-server-amd64.iso&tr=http%3a%2f%2ftorrent.ubuntu.com%3a6969%2fannounce&tr=http%3a%2f%2fipv6.torrent.ubuntu.com%3a6969%2fannounce", 48 | "max_ratio": -1, 49 | "max_seeding_time": -1, 50 | "name": "ubuntu-18.04.2-live-server-amd64.iso", 51 | "num_complete": 0, 52 | "num_incomplete": 0, 53 | "num_leechs": 0, 54 | "num_seeds": 0, 55 | "priority": 2, 56 | "progress": 0, 57 | "ratio": 0, 58 | "ratio_limit": -2, 59 | "save_path": "/home/artyom/Downloads/", 60 | "seeding_time_limit": -2, 61 | "seen_complete": 4294967295, 62 | "seq_dl": false, 63 | "size": 874512384, 64 | "state": "pausedDL", 65 | "super_seeding": false, 66 | "tags": "", 67 | "time_active": 0, 68 | "total_size": 874512384, 69 | "tracker": "", 70 | "up_limit": -1, 71 | "uploaded": 0, 72 | "uploaded_session": 0, 73 | "upspeed": 0 74 | }, 75 | "cf7da7ab4d4e6125567bd979994f13bb1f23dddd": { 76 | "added_on": 1557767746, 77 | "amount_left": 1937899520, 78 | "auto_tmm": false, 79 | "category": "", 80 | "completed": 58589184, 81 | "completion_on": 4294967295, 82 | "dl_limit": -1, 83 | "dlspeed": 0, 84 | "downloaded": 56137484, 85 | "downloaded_session": 59094054, 86 | "eta": 8640000, 87 | "f_l_piece_prio": false, 88 | "force_start": false, 89 | "last_activity": 0, 90 | "magnet_uri": "magnet:?xt=urn:btih:cf7da7ab4d4e6125567bd979994f13bb1f23dddd&dn=ubuntu-18.04.2-desktop-amd64.iso&tr=http%3a%2f%2ftorrent.ubuntu.com%3a6969%2fannounce&tr=http%3a%2f%2fipv6.torrent.ubuntu.com%3a6969%2fannounce", 91 | "max_ratio": -1, 92 | "max_seeding_time": -1, 93 | "name": "ubuntu-18.04.2-desktop-amd64.iso", 94 | "num_complete": 2110, 95 | "num_incomplete": 37, 96 | "num_leechs": 0, 97 | "num_seeds": 0, 98 | "priority": 1, 99 | "progress": 0.02934611344537815, 100 | "ratio": 0, 101 | "ratio_limit": -2, 102 | "save_path": "/home/artyom/Downloads/", 103 | "seeding_time_limit": -2, 104 | "seen_complete": 1561057640, 105 | "seq_dl": false, 106 | "size": 1996488704, 107 | "state": "pausedDL", 108 | "super_seeding": false, 109 | "tags": "", 110 | "time_active": 23, 111 | "total_size": 1996488704, 112 | "tracker": "http://torrent.ubuntu.com:6969/announce", 113 | "up_limit": -1, 114 | "uploaded": 0, 115 | "uploaded_session": 0, 116 | "upspeed": 0 117 | } 118 | } 119 | } -------------------------------------------------------------------------------- /reflection/testdata/test_data_fetcher.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | hash=842783e3005495d5d1637f5364b59343c7844707 4 | num=2 5 | 6 | curl -o torrent_${num}_list.json http://localhost:8080/api/v2/torrents/info 7 | curl -o torrent_${num}_properties.json http://localhost:8080/api/v2/torrents/properties?hash=$hash 8 | curl -o torrent_${num}_trackers.json http://localhost:8080/api/v2/torrents/trackers?hash=$hash 9 | curl -o torrent_${num}_piecestates.json http://localhost:8080/api/v2/torrents/pieceStates?hash=$hash 10 | curl -o torrent_${num}_files.json http://localhost:8080/api/v2/torrents/files?hash=$hash 11 | curl -o torrent_${num}_peers.json http://localhost:8080/api/v2/sync/torrentPeers?hash=$hash\&rid=0 12 | 13 | curl -o sync_initial.json http://localhost:8080/api/v2/sync/maindata?rid=0 --cookie cookie.txt --cookie-jar cookie.txt 14 | -------------------------------------------------------------------------------- /reflection/testdata/torrent_1_files.json: -------------------------------------------------------------------------------- 1 | [{"availability":0,"is_seed":false,"name":"ubuntu-18.04.2-desktop-amd64.iso","piece_range":[0,3807],"priority":4,"progress":0.01995798319327731,"size":1996488704}] -------------------------------------------------------------------------------- /reflection/testdata/torrent_1_peers.json: -------------------------------------------------------------------------------- 1 | {"full_update":true,"peers":{},"rid":1,"show_flags":true} -------------------------------------------------------------------------------- /reflection/testdata/torrent_1_piecestates.json: -------------------------------------------------------------------------------- 1 | [2,2,2,2,2,2,2,1,2,2,2,1,2,1,2,1,2,1,1,1,1,1,1,1,2,2,1,2,1,2,1,1,2,2,1,1,2,1,1,1,1,1,1,1,2,1,1,2,2,2,2,1,1,1,0,1,1,2,1,1,1,1,2,2,1,1,1,1,1,1,1,1,0,1,1,1,0,1,1,1,1,0,1,0,1,1,0,0,0,1,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,2,0,0,0,0,0,0,0,0,0,0,0,0,0,2,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,2,0,0,0,0,0,0,0,0,0,0,0,1,0,0,2,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,2,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,2,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,2,0,0,0,0,0,0,0,0,0,0,0,0,2,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,2,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,2,0,0,0,0,0,2,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,2,0,0,0,0,0,0,0,0,0,0,0,0,2,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,2,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,2,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,2,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,2,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,2,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,2,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,2,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,2,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,2,0,0,0,0,0,0,0,0,0,0,0,0,2,0,0,0,2,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,2,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,2,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,2,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,2,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,2,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,2,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,2,0,0,0,2,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,2,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,2,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,2,0,0,0,0,0,0,0,0,0,0,0,0,0,2,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,2,0,0,0,0,0,0,0,0,0,0,0,0,0,2,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,2,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,2,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,2,0,0,2,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,2,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,2,0,0,0,0,0,0,0,0,0,0,0,2,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,2,0,0,0,0,0,0,0,0,0,0,0,0,0,0,2,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,2,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0] -------------------------------------------------------------------------------- /reflection/testdata/torrent_1_properties.json: -------------------------------------------------------------------------------- 1 | {"addition_date":1557767746,"comment":"Ubuntu CD releases.ubuntu.com","completion_date":-1,"created_by":"","creation_date":1550184717,"dl_limit":-1,"dl_speed":0,"dl_speed_avg":2440760,"eta":8640000,"last_seen":1561057640,"nb_connections":0,"nb_connections_limit":100,"peers":0,"peers_total":37,"piece_size":524288,"pieces_have":76,"pieces_num":3808,"reannounce":0,"save_path":"/home/artyom/Downloads/","seeding_time":0,"seeds":0,"seeds_total":2110,"share_ratio":0,"time_elapsed":23,"total_downloaded":56137484,"total_downloaded_session":59094054,"total_size":1996488704,"total_uploaded":0,"total_uploaded_session":0,"total_wasted":504870,"up_limit":-1,"up_speed":0,"up_speed_avg":0} -------------------------------------------------------------------------------- /reflection/testdata/torrent_1_trackers.json: -------------------------------------------------------------------------------- 1 | [{"msg":"","num_downloaded":0,"num_leeches":0,"num_peers":0,"num_seeds":0,"status":2,"tier":"","url":"** [DHT] **"},{"msg":"","num_downloaded":0,"num_leeches":0,"num_peers":0,"num_seeds":0,"status":2,"tier":"","url":"** [PeX] **"},{"msg":"","num_downloaded":0,"num_leeches":0,"num_peers":0,"num_seeds":0,"status":2,"tier":"","url":"** [LSD] **"},{"msg":"","num_downloaded":-1,"num_leeches":37,"num_peers":0,"num_seeds":2110,"status":2,"tier":0,"url":"http://torrent.ubuntu.com:6969/announce"},{"msg":"","num_downloaded":-1,"num_leeches":-1,"num_peers":0,"num_seeds":-1,"status":4,"tier":1,"url":"http://ipv6.torrent.ubuntu.com:6969/announce"}] -------------------------------------------------------------------------------- /reflection/testdata/torrent_2_files.json: -------------------------------------------------------------------------------- 1 | [{"availability":0,"is_seed":false,"name":"ubuntu-18.04.2-live-server-amd64.iso","piece_range":[0,1667],"priority":4,"progress":0,"size":874512384}] -------------------------------------------------------------------------------- /reflection/testdata/torrent_2_list.json: -------------------------------------------------------------------------------- 1 | [{"added_on":1557767746,"amount_left":1937899520,"auto_tmm":false,"category":"","completed":58589184,"completion_on":4294967295,"dl_limit":-1,"dlspeed":0,"downloaded":56137484,"downloaded_session":59094054,"eta":8640000,"f_l_piece_prio":false,"force_start":false,"hash":"cf7da7ab4d4e6125567bd979994f13bb1f23dddd","last_activity":0,"magnet_uri":"magnet:?xt=urn:btih:cf7da7ab4d4e6125567bd979994f13bb1f23dddd&dn=ubuntu-18.04.2-desktop-amd64.iso&tr=http%3a%2f%2ftorrent.ubuntu.com%3a6969%2fannounce&tr=http%3a%2f%2fipv6.torrent.ubuntu.com%3a6969%2fannounce","max_ratio":-1,"max_seeding_time":-1,"name":"ubuntu-18.04.2-desktop-amd64.iso","num_complete":2110,"num_incomplete":37,"num_leechs":0,"num_seeds":0,"priority":1,"progress":0.02934611344537815,"ratio":0,"ratio_limit":-2,"save_path":"/home/artyom/Downloads/","seeding_time_limit":-2,"seen_complete":1561057640,"seq_dl":false,"size":1996488704,"state":"pausedDL","super_seeding":false,"tags":"","time_active":23,"total_size":1996488704,"tracker":"http://torrent.ubuntu.com:6969/announce","up_limit":-1,"uploaded":0,"uploaded_session":0,"upspeed":0},{"added_on":1557772572,"amount_left":874512384,"auto_tmm":false,"category":"","completed":0,"completion_on":4294967295,"dl_limit":-1,"dlspeed":0,"downloaded":0,"downloaded_session":0,"eta":8640000,"f_l_piece_prio":false,"force_start":false,"hash":"842783e3005495d5d1637f5364b59343c7844707","last_activity":0,"magnet_uri":"magnet:?xt=urn:btih:842783e3005495d5d1637f5364b59343c7844707&dn=ubuntu-18.04.2-live-server-amd64.iso&tr=http%3a%2f%2ftorrent.ubuntu.com%3a6969%2fannounce&tr=http%3a%2f%2fipv6.torrent.ubuntu.com%3a6969%2fannounce","max_ratio":-1,"max_seeding_time":-1,"name":"ubuntu-18.04.2-live-server-amd64.iso","num_complete":0,"num_incomplete":0,"num_leechs":0,"num_seeds":0,"priority":2,"progress":0,"ratio":0,"ratio_limit":-2,"save_path":"/home/artyom/Downloads/","seeding_time_limit":-2,"seen_complete":4294967295,"seq_dl":false,"size":874512384,"state":"pausedDL","super_seeding":false,"tags":"","time_active":0,"total_size":874512384,"tracker":"","up_limit":-1,"uploaded":0,"uploaded_session":0,"upspeed":0}] -------------------------------------------------------------------------------- /reflection/testdata/torrent_2_peers.json: -------------------------------------------------------------------------------- 1 | {"full_update":true,"peers":{},"rid":1,"show_flags":true} -------------------------------------------------------------------------------- /reflection/testdata/torrent_2_piecestates.json: -------------------------------------------------------------------------------- 1 | [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0] -------------------------------------------------------------------------------- /reflection/testdata/torrent_2_properties.json: -------------------------------------------------------------------------------- 1 | {"addition_date":1557772572,"comment":"Ubuntu CD releases.ubuntu.com","completion_date":-1,"created_by":"","creation_date":1550184797,"dl_limit":-1,"dl_speed":0,"dl_speed_avg":0,"eta":8640000,"last_seen":-1,"nb_connections":0,"nb_connections_limit":100,"peers":0,"peers_total":0,"piece_size":524288,"pieces_have":0,"pieces_num":1668,"reannounce":0,"save_path":"/home/artyom/Downloads/","seeding_time":0,"seeds":0,"seeds_total":0,"share_ratio":0,"time_elapsed":0,"total_downloaded":0,"total_downloaded_session":0,"total_size":874512384,"total_uploaded":0,"total_uploaded_session":0,"total_wasted":0,"up_limit":-1,"up_speed":0,"up_speed_avg":0} -------------------------------------------------------------------------------- /reflection/testdata/torrent_2_trackers.json: -------------------------------------------------------------------------------- 1 | [{"msg":"","num_downloaded":0,"num_leeches":0,"num_peers":0,"num_seeds":0,"status":2,"tier":"","url":"** [DHT] **"},{"msg":"","num_downloaded":0,"num_leeches":0,"num_peers":0,"num_seeds":0,"status":2,"tier":"","url":"** [PeX] **"},{"msg":"","num_downloaded":0,"num_leeches":0,"num_peers":0,"num_seeds":0,"status":2,"tier":"","url":"** [LSD] **"},{"msg":"","num_downloaded":-1,"num_leeches":-1,"num_peers":0,"num_seeds":-1,"status":1,"tier":0,"url":"http://torrent.ubuntu.com:6969/announce"},{"msg":"","num_downloaded":-1,"num_leeches":-1,"num_peers":0,"num_seeds":-1,"status":1,"tier":1,"url":"http://ipv6.torrent.ubuntu.com:6969/announce"}] -------------------------------------------------------------------------------- /reflection/testdata/torrent_3_files.json: -------------------------------------------------------------------------------- 1 | [{"availability":0,"is_seed":false,"name":"xubuntu-18.04.2-desktop-amd64.iso","piece_range":[0,2813],"priority":1,"progress":0,"size":1474953216}] -------------------------------------------------------------------------------- /reflection/testdata/torrent_3_list.json: -------------------------------------------------------------------------------- 1 | [{"added_on":1557772572,"amount_left":874512384,"auto_tmm":false,"category":"","completed":0,"completion_on":4294967295,"dl_limit":-1,"dlspeed":0,"downloaded":0,"downloaded_session":0,"eta":8640000,"f_l_piece_prio":false,"force_start":false,"hash":"842783e3005495d5d1637f5364b59343c7844707","last_activity":0,"magnet_uri":"magnet:?xt=urn:btih:842783e3005495d5d1637f5364b59343c7844707&dn=ubuntu-18.04.2-live-server-amd64.iso&tr=http%3a%2f%2ftorrent.ubuntu.com%3a6969%2fannounce&tr=http%3a%2f%2fipv6.torrent.ubuntu.com%3a6969%2fannounce","max_ratio":-1,"max_seeding_time":-1,"name":"ubuntu-18.04.2-live-server-amd64.iso","num_complete":0,"num_incomplete":0,"num_leechs":0,"num_seeds":0,"priority":2,"progress":0,"ratio":0,"ratio_limit":-2,"save_path":"/home/artyom/Downloads/","seeding_time_limit":-2,"seen_complete":4294967295,"seq_dl":false,"size":874512384,"state":"pausedDL","super_seeding":false,"tags":"","time_active":0,"total_size":874512384,"tracker":"","up_limit":-1,"uploaded":0,"uploaded_session":0,"upspeed":0},{"added_on":1561403486,"amount_left":1474953216,"auto_tmm":false,"category":"","completed":0,"completion_on":4294967295,"dl_limit":-1,"dlspeed":0,"downloaded":0,"downloaded_session":0,"eta":8640000,"f_l_piece_prio":false,"force_start":false,"hash":"7a1448be6d15bcde08ee9915350d0725775b73a3","last_activity":0,"magnet_uri":"magnet:?xt=urn:btih:7a1448be6d15bcde08ee9915350d0725775b73a3&dn=xubuntu-18.04.2-desktop-amd64.iso&tr=http%3a%2f%2ftorrent.ubuntu.com%3a6969%2fannounce","max_ratio":-1,"max_seeding_time":-1,"name":"xubuntu-18.04.2-desktop-amd64.iso","num_complete":0,"num_incomplete":0,"num_leechs":0,"num_seeds":0,"priority":3,"progress":0,"ratio":0,"ratio_limit":-2,"save_path":"/home/artyom/Downloads/","seeding_time_limit":-2,"seen_complete":4294967295,"seq_dl":false,"size":1474953216,"state":"pausedDL","super_seeding":false,"tags":"","time_active":0,"total_size":1474953216,"tracker":"","up_limit":-1,"uploaded":0,"uploaded_session":0,"upspeed":0},{"added_on":1557767746,"amount_left":1937899520,"auto_tmm":false,"category":"","completed":58589184,"completion_on":4294967295,"dl_limit":-1,"dlspeed":0,"downloaded":56137484,"downloaded_session":0,"eta":8640000,"f_l_piece_prio":false,"force_start":false,"hash":"cf7da7ab4d4e6125567bd979994f13bb1f23dddd","last_activity":0,"magnet_uri":"magnet:?xt=urn:btih:cf7da7ab4d4e6125567bd979994f13bb1f23dddd&dn=ubuntu-18.04.2-desktop-amd64.iso&tr=http%3a%2f%2ftorrent.ubuntu.com%3a6969%2fannounce&tr=http%3a%2f%2fipv6.torrent.ubuntu.com%3a6969%2fannounce","max_ratio":-1,"max_seeding_time":-1,"name":"ubuntu-18.04.2-desktop-amd64.iso","num_complete":2110,"num_incomplete":37,"num_leechs":0,"num_seeds":0,"priority":1,"progress":0.02934611344537815,"ratio":0,"ratio_limit":-2,"save_path":"/home/artyom/Downloads/","seeding_time_limit":-2,"seen_complete":4294967295,"seq_dl":false,"size":1996488704,"state":"pausedDL","super_seeding":false,"tags":"","time_active":23,"total_size":1996488704,"tracker":"","up_limit":-1,"uploaded":0,"uploaded_session":0,"upspeed":0}] -------------------------------------------------------------------------------- /reflection/testdata/torrent_3_peers.json: -------------------------------------------------------------------------------- 1 | {"full_update":true,"peers":{},"rid":1,"show_flags":true} -------------------------------------------------------------------------------- /reflection/testdata/torrent_3_piecestates.json: -------------------------------------------------------------------------------- 1 | [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0] -------------------------------------------------------------------------------- /reflection/testdata/torrent_3_properties.json: -------------------------------------------------------------------------------- 1 | {"addition_date":1561403486,"comment":"Xubuntu CD cdimage.ubuntu.com","completion_date":-1,"created_by":"","creation_date":1550179670,"dl_limit":-1,"dl_speed":0,"dl_speed_avg":0,"eta":8640000,"last_seen":-1,"nb_connections":0,"nb_connections_limit":100,"peers":0,"peers_total":0,"piece_size":524288,"pieces_have":0,"pieces_num":2814,"reannounce":0,"save_path":"/home/artyom/Downloads/","seeding_time":0,"seeds":0,"seeds_total":0,"share_ratio":0,"time_elapsed":0,"total_downloaded":0,"total_downloaded_session":0,"total_size":1474953216,"total_uploaded":0,"total_uploaded_session":0,"total_wasted":0,"up_limit":-1,"up_speed":0,"up_speed_avg":0} -------------------------------------------------------------------------------- /reflection/testdata/torrent_3_trackers.json: -------------------------------------------------------------------------------- 1 | [{"msg":"","num_downloaded":0,"num_leeches":0,"num_peers":0,"num_seeds":0,"status":2,"tier":"","url":"** [DHT] **"},{"msg":"","num_downloaded":0,"num_leeches":0,"num_peers":0,"num_seeds":0,"status":2,"tier":"","url":"** [PeX] **"},{"msg":"","num_downloaded":0,"num_leeches":0,"num_peers":0,"num_seeds":0,"status":2,"tier":"","url":"** [LSD] **"},{"msg":"","num_downloaded":-1,"num_leeches":-1,"num_peers":0,"num_seeds":-1,"status":1,"tier":0,"url":"http://torrent.ubuntu.com:6969/announce"}] -------------------------------------------------------------------------------- /reflection/testdata/torrent_list.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "added_on": 1557767746, 4 | "amount_left": 1937899520, 5 | "auto_tmm": false, 6 | "category": "", 7 | "completed": 58589184, 8 | "completion_on": 4294967295, 9 | "dl_limit": -1, 10 | "dlspeed": 0, 11 | "downloaded": 56137484, 12 | "downloaded_session": 59094054, 13 | "eta": 8640000, 14 | "f_l_piece_prio": false, 15 | "force_start": false, 16 | "hash": "cf7da7ab4d4e6125567bd979994f13bb1f23dddd", 17 | "last_activity": 0, 18 | "magnet_uri": "magnet:?xt=urn:btih:cf7da7ab4d4e6125567bd979994f13bb1f23dddd&dn=ubuntu-18.04.2-desktop-amd64.iso&tr=http%3a%2f%2ftorrent.ubuntu.com%3a6969%2fannounce&tr=http%3a%2f%2fipv6.torrent.ubuntu.com%3a6969%2fannounce", 19 | "max_ratio": -1, 20 | "max_seeding_time": -1, 21 | "name": "ubuntu-18.04.2-desktop-amd64.iso", 22 | "num_complete": 2110, 23 | "num_incomplete": 37, 24 | "num_leechs": 0, 25 | "num_seeds": 0, 26 | "priority": 1, 27 | "progress": 0.02934611344537815, 28 | "ratio": 0, 29 | "ratio_limit": -2, 30 | "save_path": "/home/artyom/Downloads/", 31 | "seeding_time_limit": -2, 32 | "seen_complete": 1561057640, 33 | "seq_dl": false, 34 | "size": 1996488704, 35 | "state": "pausedDL", 36 | "super_seeding": false, 37 | "tags": "", 38 | "time_active": 23, 39 | "total_size": 1996488704, 40 | "tracker": "http://torrent.ubuntu.com:6969/announce", 41 | "up_limit": -1, 42 | "uploaded": 0, 43 | "uploaded_session": 0, 44 | "upspeed": 0 45 | }, 46 | { 47 | "added_on": 1557772572, 48 | "amount_left": 874512384, 49 | "auto_tmm": false, 50 | "category": "", 51 | "completed": 0, 52 | "completion_on": 4294967295, 53 | "dl_limit": -1, 54 | "dlspeed": 0, 55 | "downloaded": 0, 56 | "downloaded_session": 0, 57 | "eta": 8640000, 58 | "f_l_piece_prio": false, 59 | "force_start": false, 60 | "hash": "842783e3005495d5d1637f5364b59343c7844707", 61 | "last_activity": 0, 62 | "magnet_uri": "magnet:?xt=urn:btih:842783e3005495d5d1637f5364b59343c7844707&dn=ubuntu-18.04.2-live-server-amd64.iso&tr=http%3a%2f%2ftorrent.ubuntu.com%3a6969%2fannounce&tr=http%3a%2f%2fipv6.torrent.ubuntu.com%3a6969%2fannounce", 63 | "max_ratio": -1, 64 | "max_seeding_time": -1, 65 | "name": "ubuntu-18.04.2-live-server-amd64.iso", 66 | "num_complete": 0, 67 | "num_incomplete": 0, 68 | "num_leechs": 0, 69 | "num_seeds": 0, 70 | "priority": 2, 71 | "progress": 0, 72 | "ratio": 0, 73 | "ratio_limit": -2, 74 | "save_path": "/home/artyom/Downloads/", 75 | "seeding_time_limit": -2, 76 | "seen_complete": 4294967295, 77 | "seq_dl": false, 78 | "size": 874512384, 79 | "state": "pausedDL", 80 | "super_seeding": false, 81 | "tags": "", 82 | "time_active": 0, 83 | "total_size": 874512384, 84 | "tracker": "", 85 | "up_limit": -1, 86 | "uploaded": 0, 87 | "uploaded_session": 0, 88 | "upspeed": 0 89 | } 90 | ] -------------------------------------------------------------------------------- /reflection/utils.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "io/ioutil" 5 | "net/http" 6 | ) 7 | 8 | type JsonMap map[string]interface{} 9 | 10 | func Check(e error) { 11 | if e != nil { 12 | panic(e) 13 | } 14 | } 15 | 16 | func Any(vs []string, dst string) bool { 17 | for _, v := range vs { 18 | if v == dst { 19 | return true 20 | } 21 | } 22 | return false 23 | } 24 | 25 | func DoGetWithCookies(path string, cookies *string) []byte { 26 | httpReq, err := http.NewRequest("GET", path, nil) 27 | Check(err) 28 | if cookies != nil { 29 | header := http.Header{} 30 | header.Add("Cookie", *cookies) 31 | httpReq.Header = header 32 | } 33 | cl := &http.Client{} 34 | resp, err := cl.Do(httpReq) 35 | Check(err) 36 | defer resp.Body.Close() 37 | data, err := ioutil.ReadAll(resp.Body) 38 | return data 39 | } 40 | -------------------------------------------------------------------------------- /transmission/structs.go: -------------------------------------------------------------------------------- 1 | package transmission 2 | 3 | import ( 4 | "encoding/json" 5 | ) 6 | 7 | type RPCRequest struct { 8 | Method string 9 | Tag *int 10 | Arguments json.RawMessage 11 | } 12 | 13 | type GetRequest struct { 14 | Ids *json.RawMessage 15 | Fields []string 16 | } 17 | 18 | type TorrentAddRequest struct { 19 | Cookies *string // pointer to a string of one or more cookies. 20 | Download_dir *string `json:"download-dir"` // path to download the torrent to 21 | Filename *string // filename or URL of the .torrent file 22 | Metainfo *string // base64-encoded .torrent content 23 | Paused *interface{} // if true, don't start the torrent 24 | Peer_limit *int // maximum number of peers 25 | BandwidthPriority *int // torrent's bandwidth tr_priority_t 26 | Files_wanted *[]JsonMap // indices of file(s) to download 27 | Files_unwanted *[]JsonMap // indices of file(s) to not download 28 | Priority_high *[]JsonMap // indices of high-priority file(s) 29 | Priority_low *[]JsonMap // indices of low-priority file(s) 30 | Priority_normal *[]JsonMap // indices of normal-priority file(s) 31 | } 32 | 33 | type PeerInfo struct { 34 | RateToPeer int `json:"rateToPeer"` 35 | RateToClient int `json:"rateToClient"` 36 | Port int `json:"port"` 37 | ClientName interface{} `json:"clientName"` 38 | FlagStr string `json:"flagStr"` 39 | Country interface{} `json:"country"` 40 | Address string `json:"address"` 41 | Progress float64 `json:"progress"` // Torrent progress (percentage/100) 42 | } 43 | -------------------------------------------------------------------------------- /transmission/templates.go: -------------------------------------------------------------------------------- 1 | package transmission 2 | 3 | type JsonMap map[string]interface{} 4 | 5 | var SessionGetBase = JsonMap{ 6 | "alt-speed-down": 50, 7 | "alt-speed-enabled": false, 8 | "alt-speed-time-begin": 540, 9 | "alt-speed-time-day": 127, 10 | "alt-speed-time-enabled": false, 11 | "alt-speed-time-end": 1020, 12 | "alt-speed-up": 50, 13 | "blocklist-enabled": false, 14 | "blocklist-size": 393006, 15 | "blocklist-url": "http://www.example.com/blocklist", 16 | "cache-size-mb": 4, 17 | "config-dir": "/var/lib/transmission-daemon", 18 | "idle-seeding-limit": 30, 19 | "idle-seeding-limit-enabled": false, 20 | "queue-stalled-enabled": true, 21 | "queue-stalled-minutes": 30, 22 | "rename-partial-files": true, 23 | "rpc-version": 15, 24 | "rpc-version-minimum": 1, 25 | "script-torrent-done-enabled": false, 26 | "script-torrent-done-filename": "", 27 | "start-added-torrents": true, 28 | "trash-original-torrent-files": false, 29 | "units": map[string]interface{}{ 30 | "memory-bytes": 1024, 31 | "memory-units": []string{ 32 | "KiB", 33 | "MiB", 34 | "GiB", 35 | "TiB", 36 | }, 37 | "size-bytes": 1000, 38 | "size-units": []string{ 39 | "kB", 40 | "MB", 41 | "GB", 42 | "TB", 43 | }, 44 | "speed-bytes": 1000, 45 | "speed-units": []string{ 46 | "kB/s", 47 | "MB/s", 48 | "GB/s", 49 | "TB/s", 50 | }, 51 | }, 52 | "version": "2.84 (14307)", 53 | } 54 | 55 | var TorrentGetBase = JsonMap{ 56 | "errorString": "", 57 | "metadataPercentComplete": 1, 58 | "isFinished": false, 59 | "queuePosition": 0, // Looks like not supported by qBittorent 60 | "seedRatioLimit": 2, 61 | "seedRatioMode": 0, // No local limits in qBittorrent 62 | "activityDate": 1443977197, 63 | "secondsDownloading": 500, 64 | "secondsSeeding": 80000, 65 | "isPrivate": false, // Not exposed by qBittorrent 66 | "honorsSessionLimits": true, 67 | "webseedsSendingToUs": 0, 68 | "bandwidthPriority": 0, 69 | "seedIdleLimit": 10, 70 | "seedIdleMode": 0, // TR_IDLELIMIT_GLOBAL 71 | "manualAnnounceTime": 0, 72 | "etaIdle": 0, 73 | "torrentFile": "", 74 | "webseeds": []string{}, 75 | "peers": []string{}, 76 | "magnetLink": "", 77 | } 78 | 79 | var TrackerStatsTemplate = JsonMap{ 80 | "announceState": 0, 81 | "hasScraped": false, 82 | "isBackup": false, 83 | "lastAnnounceStartTime": 0, 84 | "lastAnnounceTime": 0, 85 | "lastAnnounceTimedOut": false, 86 | "lastScrapeResult": "", 87 | "lastScrapeStartTime": 0, 88 | "lastScrapeSucceeded": false, 89 | "lastScrapeTime": 0, 90 | "lastScrapeTimedOut": 0, 91 | "nextAnnounceTime": 0, 92 | "nextScrapeTime": 0, 93 | "scrapeState": 2, 94 | } 95 | 96 | var SessionStatsTemplate = JsonMap{ 97 | "current-stats": map[string]int64{ 98 | "filesAdded": 13, 99 | "sessionCount": 1, 100 | }, 101 | } 102 | --------------------------------------------------------------------------------