transmission/web/javascript/torrent.js

615 lines
15 KiB
JavaScript

/**
* Copyright © Mnemosyne LLC
*
* This file is licensed under the GPLv2.
* http://www.gnu.org/licenses/old-licenses/gpl-2.0.html
*/
function Torrent(data) {
this.initialize(data);
}
/***
****
**** Constants
****
***/
// Torrent.fields.status
Torrent._StatusStopped = 0;
Torrent._StatusCheckWait = 1;
Torrent._StatusCheck = 2;
Torrent._StatusDownloadWait = 3;
Torrent._StatusDownload = 4;
Torrent._StatusSeedWait = 5;
Torrent._StatusSeed = 6;
// Torrent.fields.seedRatioMode
Torrent._RatioUseGlobal = 0;
Torrent._RatioUseLocal = 1;
Torrent._RatioUnlimited = 2;
// Torrent.fields.error
Torrent._ErrNone = 0;
Torrent._ErrTrackerWarning = 1;
Torrent._ErrTrackerError = 2;
Torrent._ErrLocalError = 3;
// TrackerStats' announceState
Torrent._TrackerInactive = 0;
Torrent._TrackerWaiting = 1;
Torrent._TrackerQueued = 2;
Torrent._TrackerActive = 3;
Torrent.Fields = {};
// commonly used fields which only need to be loaded once,
// either on startup or when a magnet finishes downloading its metadata
// finishes downloading its metadata
Torrent.Fields.Metadata = ['addedDate', 'name', 'totalSize'];
// commonly used fields which need to be periodically refreshed
Torrent.Fields.Stats = [
'error',
'errorString',
'eta',
'isFinished',
'isStalled',
'leftUntilDone',
'metadataPercentComplete',
'peersConnected',
'peersGettingFromUs',
'peersSendingToUs',
'percentDone',
'queuePosition',
'rateDownload',
'rateUpload',
'recheckProgress',
'seedRatioMode',
'seedRatioLimit',
'sizeWhenDone',
'status',
'trackers',
'downloadDir',
'uploadedEver',
'uploadRatio',
'webseedsSendingToUs',
];
// fields used by the inspector which only need to be loaded once
Torrent.Fields.InfoExtra = [
'comment',
'creator',
'dateCreated',
'files',
'hashString',
'isPrivate',
'pieceCount',
'pieceSize',
];
// fields used in the inspector which need to be periodically refreshed
Torrent.Fields.StatsExtra = [
'activityDate',
'corruptEver',
'desiredAvailable',
'downloadedEver',
'fileStats',
'haveUnchecked',
'haveValid',
'peers',
'startDate',
'trackerStats',
];
/***
****
**** Methods
****
***/
Torrent.prototype = {
initialize: function (data) {
this.fields = {};
this.fieldObservers = {};
this.refresh(data);
},
notifyOnFieldChange: function (field, callback) {
this.fieldObservers[field] = this.fieldObservers[field] || [];
this.fieldObservers[field].push(callback);
},
setField: function (o, name, value) {
let i, observer;
if (o[name] === value) {
return false;
}
if (o == this.fields && this.fieldObservers[name] && this.fieldObservers[name].length) {
for (i = 0; (observer = this.fieldObservers[name][i]); ++i) {
observer.call(this, value, o[name], name);
}
}
o[name] = value;
return true;
},
// fields.files is an array of unions of RPC's "files" and "fileStats" objects.
updateFiles: function (files) {
let changed = false;
const myfiles = this.fields.files || [];
const keys = ['length', 'name', 'bytesCompleted', 'wanted', 'priority'];
let i, f, j, key, myfile;
for (i = 0; (f = files[i]); ++i) {
myfile = myfiles[i] || {};
for (j = 0; (key = keys[j]); ++j) {
if (key in f) {
changed |= this.setField(myfile, key, f[key]);
}
}
myfiles[i] = myfile;
}
this.fields.files = myfiles;
return changed;
},
collateTrackers: function (trackers) {
return trackers.map((t) => t.announce.toLowerCase()).join('\t');
},
refreshFields: function (data) {
let key;
let changed = false;
for (key in data) {
switch (key) {
case 'files':
case 'fileStats': // merge files and fileStats together
changed |= this.updateFiles(data[key]);
break;
case 'trackerStats': // 'trackerStats' is a superset of 'trackers'...
changed |= this.setField(this.fields, 'trackers', data[key]);
break;
case 'trackers': // ...so only save 'trackers' if we don't have it already
if (!(key in this.fields)) {
changed |= this.setField(this.fields, key, data[key]);
}
break;
default:
changed |= this.setField(this.fields, key, data[key]);
}
}
return changed;
},
refresh: function (data) {
if (this.refreshFields(data)) {
$(this).trigger('dataChanged', this);
}
},
/****
*****
****/
// simple accessors
getComment: function () {
return this.fields.comment;
},
getCreator: function () {
return this.fields.creator;
},
getDateAdded: function () {
return this.fields.addedDate;
},
getDateCreated: function () {
return this.fields.dateCreated;
},
getDesiredAvailable: function () {
return this.fields.desiredAvailable;
},
getDownloadDir: function () {
return this.fields.downloadDir;
},
getDownloadSpeed: function () {
return this.fields.rateDownload;
},
getDownloadedEver: function () {
return this.fields.downloadedEver;
},
getError: function () {
return this.fields.error;
},
getErrorString: function () {
return this.fields.errorString;
},
getETA: function () {
return this.fields.eta;
},
getFailedEver: function (i) {
return this.fields.corruptEver;
},
getFile: function (i) {
return this.fields.files[i];
},
getFileCount: function () {
return this.fields.files ? this.fields.files.length : 0;
},
getHashString: function () {
return this.fields.hashString;
},
getHave: function () {
return this.getHaveValid() + this.getHaveUnchecked();
},
getHaveUnchecked: function () {
return this.fields.haveUnchecked;
},
getHaveValid: function () {
return this.fields.haveValid;
},
getId: function () {
return this.fields.id;
},
getLastActivity: function () {
return this.fields.activityDate;
},
getLeftUntilDone: function () {
return this.fields.leftUntilDone;
},
getMetadataPercentComplete: function () {
return this.fields.metadataPercentComplete;
},
getName: function () {
return this.fields.name || 'Unknown';
},
getPeers: function () {
return this.fields.peers;
},
getPeersConnected: function () {
return this.fields.peersConnected;
},
getPeersGettingFromUs: function () {
return this.fields.peersGettingFromUs;
},
getPeersSendingToUs: function () {
return this.fields.peersSendingToUs;
},
getPieceCount: function () {
return this.fields.pieceCount;
},
getPieceSize: function () {
return this.fields.pieceSize;
},
getPrivateFlag: function () {
return this.fields.isPrivate;
},
getQueuePosition: function () {
return this.fields.queuePosition;
},
getRecheckProgress: function () {
return this.fields.recheckProgress;
},
getSeedRatioLimit: function () {
return this.fields.seedRatioLimit;
},
getSeedRatioMode: function () {
return this.fields.seedRatioMode;
},
getSizeWhenDone: function () {
return this.fields.sizeWhenDone;
},
getStartDate: function () {
return this.fields.startDate;
},
getStatus: function () {
return this.fields.status;
},
getTotalSize: function () {
return this.fields.totalSize;
},
getTrackers: function () {
return this.fields.trackers;
},
getUploadSpeed: function () {
return this.fields.rateUpload;
},
getUploadRatio: function () {
return this.fields.uploadRatio;
},
getUploadedEver: function () {
return this.fields.uploadedEver;
},
getWebseedsSendingToUs: function () {
return this.fields.webseedsSendingToUs;
},
isFinished: function () {
return this.fields.isFinished;
},
// derived accessors
hasExtraInfo: function () {
return 'hashString' in this.fields;
},
isSeeding: function () {
return this.getStatus() === Torrent._StatusSeed;
},
isStopped: function () {
return this.getStatus() === Torrent._StatusStopped;
},
isChecking: function () {
return this.getStatus() === Torrent._StatusCheck;
},
isDownloading: function () {
return this.getStatus() === Torrent._StatusDownload;
},
isQueued: function () {
return (
this.getStatus() === Torrent._StatusDownloadWait ||
this.getStatus() === Torrent._StatusSeedWait
);
},
isDone: function () {
return this.getLeftUntilDone() < 1;
},
needsMetaData: function () {
return this.getMetadataPercentComplete() < 1;
},
getActivity: function () {
return this.getDownloadSpeed() + this.getUploadSpeed();
},
getPercentDoneStr: function () {
return Transmission.fmt.percentString(100 * this.getPercentDone());
},
getPercentDone: function () {
return this.fields.percentDone;
},
getStateString: function () {
switch (this.getStatus()) {
case Torrent._StatusStopped:
return this.isFinished() ? 'Seeding complete' : 'Paused';
case Torrent._StatusCheckWait:
return 'Queued for verification';
case Torrent._StatusCheck:
return 'Verifying local data';
case Torrent._StatusDownloadWait:
return 'Queued for download';
case Torrent._StatusDownload:
return 'Downloading';
case Torrent._StatusSeedWait:
return 'Queued for seeding';
case Torrent._StatusSeed:
return 'Seeding';
case null:
case undefined:
return 'Unknown';
default:
return 'Error';
}
},
seedRatioLimit: function (controller) {
switch (this.getSeedRatioMode()) {
case Torrent._RatioUseGlobal:
return controller.seedRatioLimit();
case Torrent._RatioUseLocal:
return this.getSeedRatioLimit();
default:
return -1;
}
},
getErrorMessage: function () {
const str = this.getErrorString();
switch (this.getError()) {
case Torrent._ErrTrackerWarning:
return 'Tracker returned a warning: ' + str;
case Torrent._ErrTrackerError:
return 'Tracker returned an error: ' + str;
case Torrent._ErrLocalError:
return 'Error: ' + str;
default:
return null;
}
},
getCollatedName: function () {
const f = this.fields;
if (!f.collatedName && f.name) {
f.collatedName = f.name.toLowerCase();
}
return f.collatedName || '';
},
getCollatedTrackers: function () {
const f = this.fields;
if (!f.collatedTrackers && f.trackers) {
f.collatedTrackers = this.collateTrackers(f.trackers);
}
return f.collatedTrackers || '';
},
/****
*****
****/
testState: function (state) {
const s = this.getStatus();
switch (state) {
case Prefs._FilterActive:
return (
this.getPeersGettingFromUs() > 0 ||
this.getPeersSendingToUs() > 0 ||
this.getWebseedsSendingToUs() > 0 ||
this.isChecking()
);
case Prefs._FilterSeeding:
return s === Torrent._StatusSeed || s === Torrent._StatusSeedWait;
case Prefs._FilterDownloading:
return s === Torrent._StatusDownload || s === Torrent._StatusDownloadWait;
case Prefs._FilterPaused:
return this.isStopped();
case Prefs._FilterFinished:
return this.isFinished();
default:
return true;
}
},
/**
* @param filter one of Prefs._Filter*
* @param search substring to look for, or null
* @return true if it passes the test, false if it fails
*/
test: function (state, search, tracker) {
// flter by state...
let pass = this.testState(state);
// maybe filter by text...
if (pass && search && search.length) {
pass = this.getCollatedName().indexOf(search.toLowerCase()) !== -1;
}
// maybe filter by tracker...
if (pass && tracker && tracker.length) {
pass = this.getCollatedTrackers().indexOf(tracker) !== -1;
}
return pass;
},
};
/***
****
**** SORTING
****
***/
Torrent.compareById = function (ta, tb) {
return ta.getId() - tb.getId();
};
Torrent.compareByName = function (ta, tb) {
return ta.getCollatedName().localeCompare(tb.getCollatedName()) || Torrent.compareById(ta, tb);
};
Torrent.compareByQueue = function (ta, tb) {
return ta.getQueuePosition() - tb.getQueuePosition();
};
Torrent.compareByAge = function (ta, tb) {
const a = ta.getDateAdded();
const b = tb.getDateAdded();
return b - a || Torrent.compareByQueue(ta, tb);
};
Torrent.compareByState = function (ta, tb) {
const a = ta.getStatus();
const b = tb.getStatus();
return b - a || Torrent.compareByQueue(ta, tb);
};
Torrent.compareByActivity = function (ta, tb) {
const a = ta.getActivity();
const b = tb.getActivity();
return b - a || Torrent.compareByState(ta, tb);
};
Torrent.compareByRatio = function (ta, tb) {
const a = ta.getUploadRatio();
const b = tb.getUploadRatio();
if (a < b) {
return 1;
}
if (a > b) {
return -1;
}
return Torrent.compareByState(ta, tb);
};
Torrent.compareByProgress = function (ta, tb) {
const a = ta.getPercentDone();
const b = tb.getPercentDone();
return a - b || Torrent.compareByRatio(ta, tb);
};
Torrent.compareBySize = function (ta, tb) {
const a = ta.getTotalSize();
const b = tb.getTotalSize();
return a - b || Torrent.compareByName(ta, tb);
};
Torrent.compareTorrents = function (a, b, sortMethod, sortDirection) {
let i;
switch (sortMethod) {
case Prefs._SortByActivity:
i = Torrent.compareByActivity(a, b);
break;
case Prefs._SortByAge:
i = Torrent.compareByAge(a, b);
break;
case Prefs._SortByQueue:
i = Torrent.compareByQueue(a, b);
break;
case Prefs._SortByProgress:
i = Torrent.compareByProgress(a, b);
break;
case Prefs._SortBySize:
i = Torrent.compareBySize(a, b);
break;
case Prefs._SortByState:
i = Torrent.compareByState(a, b);
break;
case Prefs._SortByRatio:
i = Torrent.compareByRatio(a, b);
break;
default:
i = Torrent.compareByName(a, b);
break;
}
if (sortDirection === Prefs._SortDescending) {
i = -i;
}
return i;
};
/**
* @param torrents an array of Torrent objects
* @param sortMethod one of Prefs._SortBy*
* @param sortDirection Prefs._SortAscending or Prefs._SortDescending
*/
Torrent.sortTorrents = function (torrents, sortMethod, sortDirection) {
switch (sortMethod) {
case Prefs._SortByActivity:
torrents.sort(this.compareByActivity);
break;
case Prefs._SortByAge:
torrents.sort(this.compareByAge);
break;
case Prefs._SortByQueue:
torrents.sort(this.compareByQueue);
break;
case Prefs._SortByProgress:
torrents.sort(this.compareByProgress);
break;
case Prefs._SortBySize:
torrents.sort(this.compareBySize);
break;
case Prefs._SortByState:
torrents.sort(this.compareByState);
break;
case Prefs._SortByRatio:
torrents.sort(this.compareByRatio);
break;
default:
torrents.sort(this.compareByName);
break;
}
if (sortDirection === Prefs._SortDescending) {
torrents.reverse();
}
return torrents;
};