1/** 2 * Copyright �� Mnemosyne LLC 3 * 4 * This file is licensed under the GPLv2. 5 * http://www.gnu.org/licenses/old-licenses/gpl-2.0.html 6 */ 7 8function Torrent(data) 9{ 10 this.initialize(data); 11} 12 13/*** 14**** 15**** Constants 16**** 17***/ 18 19// Torrent.fields.status 20Torrent._StatusStopped = 0; 21Torrent._StatusCheckWait = 1; 22Torrent._StatusCheck = 2; 23Torrent._StatusDownloadWait = 3; 24Torrent._StatusDownload = 4; 25Torrent._StatusSeedWait = 5; 26Torrent._StatusSeed = 6; 27 28// Torrent.fields.seedRatioMode 29Torrent._RatioUseGlobal = 0; 30Torrent._RatioUseLocal = 1; 31Torrent._RatioUnlimited = 2; 32 33// Torrent.fields.error 34Torrent._ErrNone = 0; 35Torrent._ErrTrackerWarning = 1; 36Torrent._ErrTrackerError = 2; 37Torrent._ErrLocalError = 3; 38 39// TrackerStats' announceState 40Torrent._TrackerInactive = 0; 41Torrent._TrackerWaiting = 1; 42Torrent._TrackerQueued = 2; 43Torrent._TrackerActive = 3; 44 45 46Torrent.Fields = { }; 47 48// commonly used fields which only need to be loaded once, 49// either on startup or when a magnet finishes downloading its metadata 50// finishes downloading its metadata 51Torrent.Fields.Metadata = [ 52 'addedDate', 53 'name', 54 'totalSize' 55]; 56 57// commonly used fields which need to be periodically refreshed 58Torrent.Fields.Stats = [ 59 'error', 60 'errorString', 61 'eta', 62 'isFinished', 63 'isStalled', 64 'leftUntilDone', 65 'metadataPercentComplete', 66 'peersConnected', 67 'peersGettingFromUs', 68 'peersSendingToUs', 69 'percentDone', 70 'queuePosition', 71 'rateDownload', 72 'rateUpload', 73 'recheckProgress', 74 'seedRatioMode', 75 'seedRatioLimit', 76 'sizeWhenDone', 77 'status', 78 'trackers', 79 'downloadDir', 80 'uploadedEver', 81 'uploadRatio', 82 'webseedsSendingToUs' 83]; 84 85// fields used by the inspector which only need to be loaded once 86Torrent.Fields.InfoExtra = [ 87 'comment', 88 'creator', 89 'dateCreated', 90 'files', 91 'hashString', 92 'isPrivate', 93 'pieceCount', 94 'pieceSize' 95]; 96 97// fields used in the inspector which need to be periodically refreshed 98Torrent.Fields.StatsExtra = [ 99 'activityDate', 100 'corruptEver', 101 'desiredAvailable', 102 'downloadedEver', 103 'fileStats', 104 'haveUnchecked', 105 'haveValid', 106 'peers', 107 'startDate', 108 'trackerStats' 109]; 110 111/*** 112**** 113**** Methods 114**** 115***/ 116 117Torrent.prototype = 118{ 119 initialize: function(data) 120 { 121 this.fields = {}; 122 this.fieldObservers = {}; 123 this.refresh (data); 124 }, 125 126 notifyOnFieldChange: function(field, callback) { 127 this.fieldObservers[field] = this.fieldObservers[field] || []; 128 this.fieldObservers[field].push(callback); 129 }, 130 131 setField: function(o, name, value) 132 { 133 var i, observer; 134 135 if (o[name] === value) 136 return false; 137 if (o == this.fields && this.fieldObservers[name] && this.fieldObservers[name].length) { 138 for (i=0; observer=this.fieldObservers[name][i]; ++i) { 139 observer.call(this, value, o[name], name); 140 } 141 } 142 o[name] = value; 143 return true; 144 }, 145 146 // fields.files is an array of unions of RPC's "files" and "fileStats" objects. 147 updateFiles: function(files) 148 { 149 var changed = false, 150 myfiles = this.fields.files || [], 151 keys = [ 'length', 'name', 'bytesCompleted', 'wanted', 'priority' ], 152 i, f, j, key, myfile; 153 154 for (i=0; f=files[i]; ++i) { 155 myfile = myfiles[i] || {}; 156 for (j=0; key=keys[j]; ++j) 157 if(key in f) 158 changed |= this.setField(myfile,key,f[key]); 159 myfiles[i] = myfile; 160 } 161 this.fields.files = myfiles; 162 return changed; 163 }, 164 165 collateTrackers: function(trackers) 166 { 167 var i, t, announces = []; 168 169 for (i=0; t=trackers[i]; ++i) 170 announces.push(t.announce.toLowerCase()); 171 return announces.join('\t'); 172 }, 173 174 refreshFields: function(data) 175 { 176 var key, 177 changed = false; 178 179 for (key in data) { 180 switch (key) { 181 case 'files': 182 case 'fileStats': // merge files and fileStats together 183 changed |= this.updateFiles(data[key]); 184 break; 185 case 'trackerStats': // 'trackerStats' is a superset of 'trackers'... 186 changed |= this.setField(this.fields,'trackers',data[key]); 187 break; 188 case 'trackers': // ...so only save 'trackers' if we don't have it already 189 if (!(key in this.fields)) 190 changed |= this.setField(this.fields,key,data[key]); 191 break; 192 default: 193 changed |= this.setField(this.fields,key,data[key]); 194 } 195 } 196 197 return changed; 198 }, 199 200 refresh: function(data) 201 { 202 if (this.refreshFields(data)) 203 $(this).trigger('dataChanged', this); 204 }, 205 206 /**** 207 ***** 208 ****/ 209 210 // simple accessors 211 getComment: function() { return this.fields.comment; }, 212 getCreator: function() { return this.fields.creator; }, 213 getDateAdded: function() { return this.fields.addedDate; }, 214 getDateCreated: function() { return this.fields.dateCreated; }, 215 getDesiredAvailable: function() { return this.fields.desiredAvailable; }, 216 getDownloadDir: function() { return this.fields.downloadDir; }, 217 getDownloadSpeed: function() { return this.fields.rateDownload; }, 218 getDownloadedEver: function() { return this.fields.downloadedEver; }, 219 getError: function() { return this.fields.error; }, 220 getErrorString: function() { return this.fields.errorString; }, 221 getETA: function() { return this.fields.eta; }, 222 getFailedEver: function(i) { return this.fields.corruptEver; }, 223 getFile: function(i) { return this.fields.files[i]; }, 224 getFileCount: function() { return this.fields.files ? this.fields.files.length : 0; }, 225 getHashString: function() { return this.fields.hashString; }, 226 getHave: function() { return this.getHaveValid() + this.getHaveUnchecked() }, 227 getHaveUnchecked: function() { return this.fields.haveUnchecked; }, 228 getHaveValid: function() { return this.fields.haveValid; }, 229 getId: function() { return this.fields.id; }, 230 getLastActivity: function() { return this.fields.activityDate; }, 231 getLeftUntilDone: function() { return this.fields.leftUntilDone; }, 232 getMetadataPercentComplete: function() { return this.fields.metadataPercentComplete; }, 233 getName: function() { return this.fields.name || 'Unknown'; }, 234 getPeers: function() { return this.fields.peers; }, 235 getPeersConnected: function() { return this.fields.peersConnected; }, 236 getPeersGettingFromUs: function() { return this.fields.peersGettingFromUs; }, 237 getPeersSendingToUs: function() { return this.fields.peersSendingToUs; }, 238 getPieceCount: function() { return this.fields.pieceCount; }, 239 getPieceSize: function() { return this.fields.pieceSize; }, 240 getPrivateFlag: function() { return this.fields.isPrivate; }, 241 getQueuePosition: function() { return this.fields.queuePosition; }, 242 getRecheckProgress: function() { return this.fields.recheckProgress; }, 243 getSeedRatioLimit: function() { return this.fields.seedRatioLimit; }, 244 getSeedRatioMode: function() { return this.fields.seedRatioMode; }, 245 getSizeWhenDone: function() { return this.fields.sizeWhenDone; }, 246 getStartDate: function() { return this.fields.startDate; }, 247 getStatus: function() { return this.fields.status; }, 248 getTotalSize: function() { return this.fields.totalSize; }, 249 getTrackers: function() { return this.fields.trackers; }, 250 getUploadSpeed: function() { return this.fields.rateUpload; }, 251 getUploadRatio: function() { return this.fields.uploadRatio; }, 252 getUploadedEver: function() { return this.fields.uploadedEver; }, 253 getWebseedsSendingToUs: function() { return this.fields.webseedsSendingToUs; }, 254 isFinished: function() { return this.fields.isFinished; }, 255 256 // derived accessors 257 hasExtraInfo: function() { return 'hashString' in this.fields; }, 258 isSeeding: function() { return this.getStatus() === Torrent._StatusSeed; }, 259 isStopped: function() { return this.getStatus() === Torrent._StatusStopped; }, 260 isChecking: function() { return this.getStatus() === Torrent._StatusCheck; }, 261 isDownloading: function() { return this.getStatus() === Torrent._StatusDownload; }, 262 isDone: function() { return this.getLeftUntilDone() < 1; }, 263 needsMetaData: function(){ return this.getMetadataPercentComplete() < 1; }, 264 getActivity: function() { return this.getDownloadSpeed() + this.getUploadSpeed(); }, 265 getPercentDoneStr: function() { return Transmission.fmt.percentString(100*this.getPercentDone()); }, 266 getPercentDone: function() { return this.fields.percentDone; }, 267 getStateString: function() { 268 switch(this.getStatus()) { 269 case Torrent._StatusStopped: return this.isFinished() ? 'Seeding complete' : 'Paused'; 270 case Torrent._StatusCheckWait: return 'Queued for verification'; 271 case Torrent._StatusCheck: return 'Verifying local data'; 272 case Torrent._StatusDownloadWait: return 'Queued for download'; 273 case Torrent._StatusDownload: return 'Downloading'; 274 case Torrent._StatusSeedWait: return 'Queued for seeding'; 275 case Torrent._StatusSeed: return 'Seeding'; 276 case null: 277 case undefined: return 'Unknown'; 278 default: return 'Error'; 279 } 280 }, 281 seedRatioLimit: function(controller){ 282 switch(this.getSeedRatioMode()) { 283 case Torrent._RatioUseGlobal: return controller.seedRatioLimit(); 284 case Torrent._RatioUseLocal: return this.getSeedRatioLimit(); 285 default: return -1; 286 } 287 }, 288 getErrorMessage: function() { 289 var str = this.getErrorString(); 290 switch(this.getError()) { 291 case Torrent._ErrTrackerWarning: 292 return 'Tracker returned a warning: ' + str; 293 case Torrent._ErrTrackerError: 294 return 'Tracker returned an error: ' + str; 295 case Torrent._ErrLocalError: 296 return 'Error: ' + str; 297 default: 298 return null; 299 } 300 }, 301 getCollatedName: function() { 302 var f = this.fields; 303 if (!f.collatedName && f.name) 304 f.collatedName = f.name.toLowerCase(); 305 return f.collatedName || ''; 306 }, 307 getCollatedTrackers: function() { 308 var f = this.fields; 309 if (!f.collatedTrackers && f.trackers) 310 f.collatedTrackers = this.collateTrackers(f.trackers); 311 return f.collatedTrackers || ''; 312 }, 313 314 /**** 315 ***** 316 ****/ 317 318 testState: function(state) 319 { 320 var s = this.getStatus(); 321 322 switch(state) 323 { 324 case Prefs._FilterActive: 325 return this.getPeersGettingFromUs() > 0 326 || this.getPeersSendingToUs() > 0 327 || this.getWebseedsSendingToUs() > 0 328 || this.isChecking(); 329 case Prefs._FilterSeeding: 330 return (s === Torrent._StatusSeed) 331 || (s === Torrent._StatusSeedWait); 332 case Prefs._FilterDownloading: 333 return (s === Torrent._StatusDownload) 334 || (s === Torrent._StatusDownloadWait); 335 case Prefs._FilterPaused: 336 return this.isStopped(); 337 case Prefs._FilterFinished: 338 return this.isFinished(); 339 default: 340 return true; 341 } 342 }, 343 344 /** 345 * @param filter one of Prefs._Filter* 346 * @param search substring to look for, or null 347 * @return true if it passes the test, false if it fails 348 */ 349 test: function(state, search, tracker) 350 { 351 // flter by state... 352 var pass = this.testState(state); 353 354 // maybe filter by text... 355 if (pass && search && search.length) 356 pass = this.getCollatedName().indexOf(search.toLowerCase()) !== -1; 357 358 // maybe filter by tracker... 359 if (pass && tracker && tracker.length) 360 pass = this.getCollatedTrackers().indexOf(tracker) !== -1; 361 362 return pass; 363 } 364}; 365 366 367/*** 368**** 369**** SORTING 370**** 371***/ 372 373Torrent.compareById = function(ta, tb) 374{ 375 return ta.getId() - tb.getId(); 376}; 377Torrent.compareByName = function(ta, tb) 378{ 379 return ta.getCollatedName().localeCompare(tb.getCollatedName()) 380 || Torrent.compareById(ta, tb); 381}; 382Torrent.compareByQueue = function(ta, tb) 383{ 384 return ta.getQueuePosition() - tb.getQueuePosition(); 385}; 386Torrent.compareByAge = function(ta, tb) 387{ 388 var a = ta.getDateAdded(), 389 b = tb.getDateAdded(); 390 391 return (b - a) || Torrent.compareByQueue(ta, tb); 392}; 393Torrent.compareByState = function(ta, tb) 394{ 395 var a = ta.getStatus(), 396 b = tb.getStatus(); 397 398 return (b - a) || Torrent.compareByQueue(ta, tb); 399}; 400Torrent.compareByActivity = function(ta, tb) 401{ 402 var a = ta.getActivity(), 403 b = tb.getActivity(); 404 405 return (b - a) || Torrent.compareByState(ta, tb); 406}; 407Torrent.compareByRatio = function(ta, tb) 408{ 409 var a = ta.getUploadRatio(), 410 b = tb.getUploadRatio(); 411 412 if (a < b) return 1; 413 if (a > b) return -1; 414 return Torrent.compareByState(ta, tb); 415}; 416Torrent.compareByProgress = function(ta, tb) 417{ 418 var a = ta.getPercentDone(), 419 b = tb.getPercentDone(); 420 421 return (a - b) || Torrent.compareByRatio(ta, tb); 422}; 423 424Torrent.compareBySize = function(ta, tb) 425{ 426 var a = ta.getTotalSize(), 427 b = tb.getTotalSize(); 428 429 return (a - b) || Torrent.compareByName(ta, tb); 430} 431 432Torrent.compareTorrents = function(a, b, sortMethod, sortDirection) 433{ 434 var i; 435 436 switch(sortMethod) 437 { 438 case Prefs._SortByActivity: 439 i = Torrent.compareByActivity(a,b); 440 break; 441 case Prefs._SortByAge: 442 i = Torrent.compareByAge(a,b); 443 break; 444 case Prefs._SortByQueue: 445 i = Torrent.compareByQueue(a,b); 446 break; 447 case Prefs._SortByProgress: 448 i = Torrent.compareByProgress(a,b); 449 break; 450 case Prefs._SortBySize: 451 i = Torrent.compareBySize(a,b); 452 break; 453 case Prefs._SortByState: 454 i = Torrent.compareByState(a,b); 455 break; 456 case Prefs._SortByRatio: 457 i = Torrent.compareByRatio(a,b); 458 break; 459 default: 460 i = Torrent.compareByName(a,b); 461 break; 462 } 463 464 if (sortDirection === Prefs._SortDescending) 465 i = -i; 466 467 return i; 468}; 469 470/** 471 * @param torrents an array of Torrent objects 472 * @param sortMethod one of Prefs._SortBy* 473 * @param sortDirection Prefs._SortAscending or Prefs._SortDescending 474 */ 475Torrent.sortTorrents = function(torrents, sortMethod, sortDirection) 476{ 477 switch(sortMethod) 478 { 479 case Prefs._SortByActivity: 480 torrents.sort(this.compareByActivity); 481 break; 482 case Prefs._SortByAge: 483 torrents.sort(this.compareByAge); 484 break; 485 case Prefs._SortByQueue: 486 torrents.sort(this.compareByQueue); 487 break; 488 case Prefs._SortByProgress: 489 torrents.sort(this.compareByProgress); 490 break; 491 case Prefs._SortBySize: 492 torrents.sort(this.compareBySize); 493 break; 494 case Prefs._SortByState: 495 torrents.sort(this.compareByState); 496 break; 497 case Prefs._SortByRatio: 498 torrents.sort(this.compareByRatio); 499 break; 500 default: 501 torrents.sort(this.compareByName); 502 break; 503 } 504 505 if (sortDirection === Prefs._SortDescending) 506 torrents.reverse(); 507 508 return torrents; 509}; 510