transmission/web/javascript/inspector.js

959 lines
26 KiB
JavaScript

/**
* Copyright © Charles Kerr, Dave Perrett, Malcolm Jarvis and Bruno Bierbaumer
*
* This file is licensed under the GPLv2.
* http://www.gnu.org/licenses/old-licenses/gpl-2.0.html
*/
function Inspector(controller) {
var data = {
controller: null,
elements: {},
torrents: [],
},
needsExtraInfo = function (torrents) {
return torrents.some((tor) => !tor.hasExtraInfo());
},
refreshTorrents = function (callback) {
let fields,
ids = $.map(data.torrents.slice(0), function (t) {
return t.getId();
});
if (ids && ids.length) {
fields = ['id'].concat(Torrent.Fields.StatsExtra);
if (needsExtraInfo(data.torrents)) {
$.merge(fields, Torrent.Fields.InfoExtra);
}
data.controller.updateTorrents(ids, fields, callback);
}
},
onTabClicked = function (ev) {
const tab = ev.currentTarget;
if (isMobileDevice) {
ev.stopPropagation();
}
// select this tab and deselect the others
$(tab).addClass('selected').siblings().removeClass('selected');
// show this tab and hide the others
$('#' + tab.id.replace('tab', 'page'))
.show()
.siblings('.inspector-page')
.hide();
updateInspector();
},
updateInspector = function () {
let e = data.elements,
torrents = data.torrents,
name;
// update the name, which is shown on all the pages
if (!torrents || !torrents.length) {
name = 'No Selection';
} else if (torrents.length === 1) {
name = torrents[0].getName();
} else {
name = '' + torrents.length + ' Transfers Selected';
}
setTextContent(e.name_lb, name || na);
// update the visible page
if ($(e.info_page).is(':visible')) {
updateInfoPage();
} else if ($(e.peers_page).is(':visible')) {
updatePeersPage();
} else if ($(e.trackers_page).is(':visible')) {
updateTrackersPage();
} else if ($(e.files_page).is(':visible')) {
updateFilesPage();
}
},
/****
***** GENERAL INFO PAGE
****/
updateInfoPage = function () {
let torrents = data.torrents,
e = data.elements,
fmt = Transmission.fmt,
none = 'None',
mixed = 'Mixed',
unknown = 'Unknown',
isMixed,
allPaused,
allFinished,
str,
baseline,
it,
i,
t,
sizeWhenDone = 0,
leftUntilDone = 0,
available = 0,
haveVerified = 0,
haveUnverified = 0,
verifiedPieces = 0,
stateString,
latest,
pieces,
size,
pieceSize,
creator,
mixed_creator,
date,
mixed_date,
v,
u,
f,
d,
now = Date.now();
//
// state_lb
//
if (torrents.length < 1) {
str = none;
} else {
isMixed = false;
allPaused = true;
allFinished = true;
baseline = torrents[0].getStatus();
for (i = 0; (t = torrents[i]); ++i) {
it = t.getStatus();
if (it != baseline) {
isMixed = true;
}
if (!t.isStopped()) {
allPaused = allFinished = false;
}
if (!t.isFinished()) {
allFinished = false;
}
}
if (isMixed) {
str = mixed;
} else if (allFinished) {
str = 'Finished';
} else if (allPaused) {
str = 'Paused';
} else {
str = torrents[0].getStateString();
}
}
setTextContent(e.state_lb, str);
stateString = str;
//
// have_lb
//
if (torrents.length < 1) {
str = none;
} else {
baseline = torrents[0].getStatus();
for (i = 0; (t = torrents[i]); ++i) {
if (!t.needsMetaData()) {
haveUnverified += t.getHaveUnchecked();
v = t.getHaveValid();
haveVerified += v;
if (t.getPieceSize()) {
verifiedPieces += v / t.getPieceSize();
}
sizeWhenDone += t.getSizeWhenDone();
leftUntilDone += t.getLeftUntilDone();
available += t.getHave() + t.getDesiredAvailable();
}
}
d = 100.0 * (sizeWhenDone ? (sizeWhenDone - leftUntilDone) / sizeWhenDone : 1);
str = fmt.percentString(d);
if (!haveUnverified && !leftUntilDone) {
str = fmt.size(haveVerified) + ' (100%)';
} else if (!haveUnverified) {
str = fmt.size(haveVerified) + ' of ' + fmt.size(sizeWhenDone) + ' (' + str + '%)';
} else {
str =
fmt.size(haveVerified) +
' of ' +
fmt.size(sizeWhenDone) +
' (' +
str +
'%), ' +
fmt.size(haveUnverified) +
' Unverified';
}
}
setTextContent(e.have_lb, str);
//
// availability_lb
//
if (torrents.length < 1) {
str = none;
} else if (sizeWhenDone == 0) {
str = none;
} else {
str = '' + fmt.percentString((100.0 * available) / sizeWhenDone) + '%';
}
setTextContent(e.availability_lb, str);
//
// downloaded_lb
//
if (torrents.length < 1) {
str = none;
} else {
d = f = 0;
for (i = 0; (t = torrents[i]); ++i) {
d += t.getDownloadedEver();
f += t.getFailedEver();
}
if (f) {
str = fmt.size(d) + ' (' + fmt.size(f) + ' corrupt)';
} else {
str = fmt.size(d);
}
}
setTextContent(e.downloaded_lb, str);
//
// uploaded_lb
//
if (torrents.length < 1) {
str = none;
} else {
d = u = 0;
if (torrents.length == 1) {
d = torrents[0].getDownloadedEver();
u = torrents[0].getUploadedEver();
if (d == 0) {
d = torrents[0].getHaveValid();
}
} else {
for (i = 0; (t = torrents[i]); ++i) {
d += t.getDownloadedEver();
u += t.getUploadedEver();
}
}
str = fmt.size(u) + ' (Ratio: ' + fmt.ratioString(Math.ratio(u, d)) + ')';
}
setTextContent(e.uploaded_lb, str);
//
// running time
//
if (torrents.length < 1) {
str = none;
} else {
allPaused = true;
baseline = torrents[0].getStartDate();
for (i = 0; (t = torrents[i]); ++i) {
if (baseline != t.getStartDate()) {
baseline = 0;
}
if (!t.isStopped()) {
allPaused = false;
}
}
if (allPaused) {
str = stateString; // paused || finished}
} else if (!baseline) {
str = mixed;
} else {
str = fmt.timeInterval(now / 1000 - baseline);
}
}
setTextContent(e.running_time_lb, str);
//
// remaining time
//
str = '';
if (torrents.length < 1) {
str = none;
} else {
baseline = torrents[0].getETA();
for (i = 0; (t = torrents[i]); ++i) {
if (baseline != t.getETA()) {
str = mixed;
break;
}
}
}
if (!str.length) {
if (baseline < 0) {
str = unknown;
} else {
str = fmt.timeInterval(baseline);
}
}
setTextContent(e.remaining_time_lb, str);
//
// last activity
//
latest = -1;
if (torrents.length < 1) {
str = none;
} else {
for (i = 0; (t = torrents[i]); ++i) {
d = t.getLastActivity();
if (latest < d) {
latest = d;
}
}
d = now / 1000 - latest; // seconds since last activity
if (d < 0) {
str = none;
} else if (d < 5) {
str = 'Active now';
} else {
str = fmt.timeInterval(d) + ' ago';
}
}
setTextContent(e.last_activity_lb, str);
//
// error
//
if (torrents.length < 1) {
str = none;
} else {
str = torrents[0].getErrorString();
for (i = 0; (t = torrents[i]); ++i) {
if (str != t.getErrorString()) {
str = mixed;
break;
}
}
}
setTextContent(e.error_lb, str || none);
//
// size
//
if (torrents.length < 1) {
{
str = none;
}
} else {
pieces = 0;
size = 0;
pieceSize = torrents[0].getPieceSize();
for (i = 0; (t = torrents[i]); ++i) {
pieces += t.getPieceCount();
size += t.getTotalSize();
if (pieceSize != t.getPieceSize()) {
pieceSize = 0;
}
}
if (!size) {
str = none;
} else if (pieceSize > 0) {
str =
fmt.size(size) +
' (' +
pieces.toStringWithCommas() +
' pieces @ ' +
fmt.mem(pieceSize) +
')';
} else {
str = fmt.size(size) + ' (' + pieces.toStringWithCommas() + ' pieces)';
}
}
setTextContent(e.size_lb, str);
//
// hash
//
if (torrents.length < 1) {
str = none;
} else {
str = torrents[0].getHashString();
for (i = 0; (t = torrents[i]); ++i) {
if (str != t.getHashString()) {
str = mixed;
break;
}
}
}
setTextContent(e.hash_lb, str);
//
// privacy
//
if (torrents.length < 1) {
str = none;
} else {
baseline = torrents[0].getPrivateFlag();
str = baseline ? 'Private to this tracker -- DHT and PEX disabled' : 'Public torrent';
for (i = 0; (t = torrents[i]); ++i) {
if (baseline != t.getPrivateFlag()) {
str = mixed;
break;
}
}
}
setTextContent(e.privacy_lb, str);
//
// comment
//
if (torrents.length < 1) {
str = none;
} else {
str = torrents[0].getComment();
for (i = 0; (t = torrents[i]); ++i) {
if (str != t.getComment()) {
str = mixed;
break;
}
}
}
if (!str) {
str = none;
}
if (str.startsWith('https://') || str.startsWith('http://')) {
str = encodeURI(str);
setInnerHTML(e.comment_lb, '<a href="' + str + '" target="_blank" >' + str + '</a>');
} else {
setTextContent(e.comment_lb, str);
}
//
// origin
//
if (torrents.length < 1) {
str = none;
} else {
mixed_creator = false;
mixed_date = false;
creator = torrents[0].getCreator();
date = torrents[0].getDateCreated();
for (i = 0; (t = torrents[i]); ++i) {
if (creator != t.getCreator()) {
mixed_creator = true;
}
if (date != t.getDateCreated()) {
mixed_date = true;
}
}
const empty_creator = !creator || !creator.length;
const empty_date = !date;
if (mixed_creator || mixed_date) {
str = mixed;
} else if (empty_creator && empty_date) {
str = unknown;
} else if (empty_date && !empty_creator) {
str = 'Created by ' + creator;
} else if (empty_creator && !empty_date) {
str = 'Created on ' + new Date(date * 1000).toDateString();
} else {
str = 'Created by ' + creator + ' on ' + new Date(date * 1000).toDateString();
}
}
setTextContent(e.origin_lb, str);
//
// foldername
//
if (torrents.length < 1) {
str = none;
} else {
str = torrents[0].getDownloadDir();
for (i = 0; (t = torrents[i]); ++i) {
if (str != t.getDownloadDir()) {
str = mixed;
break;
}
}
}
setTextContent(e.foldername_lb, str);
},
/****
***** FILES PAGE
****/
changeFileCommand = function (fileIndices, command) {
const torrentId = data.file_torrent.getId();
data.controller.changeFileCommand(torrentId, fileIndices, command);
},
onFileWantedToggled = function (ev, fileIndices, want) {
changeFileCommand(fileIndices, want ? 'files-wanted' : 'files-unwanted');
},
onFilePriorityToggled = function (ev, fileIndices, priority) {
let command;
switch (priority) {
case -1:
command = 'priority-low';
break;
case 1:
command = 'priority-high';
break;
default:
command = 'priority-normal';
break;
}
changeFileCommand(fileIndices, command);
},
onNameClicked = function (ev, fileRow, fileIndices) {
$(fileRow.getElement()).siblings().slideToggle();
},
clearFileList = function () {
$(data.elements.file_list).empty();
delete data.file_torrent;
delete data.file_torrent_n;
delete data.file_rows;
},
createFileTreeModel = function (tor) {
let i,
j,
n,
name,
tokens,
walk,
token,
sub,
leaves = [],
tree = {
children: {},
file_indices: [],
};
n = tor.getFileCount();
for (i = 0; i < n; ++i) {
name = tor.getFile(i).name;
tokens = name.split('/');
walk = tree;
for (j = 0; j < tokens.length; ++j) {
token = tokens[j];
sub = walk.children[token];
if (!sub) {
walk.children[token] = sub = {
name: token,
parent: walk,
children: {},
file_indices: [],
depth: j,
};
}
walk = sub;
}
walk.file_index = i;
delete walk.children;
leaves.push(walk);
}
for (i = 0; i < leaves.length; ++i) {
walk = leaves[i];
j = walk.file_index;
do {
walk.file_indices.push(j);
walk = walk.parent;
} while (walk);
}
return tree;
},
addNodeToView = function (tor, parent, sub, i) {
let row;
row = new FileRow(tor, sub.depth, sub.name, sub.file_indices, i % 2);
data.file_rows.push(row);
parent.appendChild(row.getElement());
$(row).bind('wantedToggled', onFileWantedToggled);
$(row).bind('priorityToggled', onFilePriorityToggled);
$(row).bind('nameClicked', onNameClicked);
},
addSubtreeToView = function (tor, parent, sub, i) {
let key, div;
div = document.createElement('div');
if (sub.parent) {
addNodeToView(tor, div, sub, i++);
}
if (sub.children) {
for (key in sub.children) {
i = addSubtreeToView(tor, div, sub.children[key]);
}
}
parent.appendChild(div);
return i;
},
updateFilesPage = function () {
let i,
n,
tor,
fragment,
tree,
file_list = data.elements.file_list,
torrents = data.torrents;
// only show one torrent at a time
if (torrents.length !== 1) {
clearFileList();
return;
}
tor = torrents[0];
n = tor ? tor.getFileCount() : 0;
if (tor != data.file_torrent || n != data.file_torrent_n) {
// rebuild the file list...
clearFileList();
data.file_torrent = tor;
data.file_torrent_n = n;
data.file_rows = [];
fragment = document.createDocumentFragment();
tree = createFileTreeModel(tor);
addSubtreeToView(tor, fragment, tree, 0);
file_list.appendChild(fragment);
} else {
// ...refresh the already-existing file list
for (i = 0, n = data.file_rows.length; i < n; ++i) {
data.file_rows[i].refresh();
}
}
},
/****
***** PEERS PAGE
****/
updatePeersPage = function () {
let i,
k,
tor,
peers,
peer,
parity,
html = [],
fmt = Transmission.fmt,
peers_list = data.elements.peers_list,
torrents = data.torrents;
for (k = 0; (tor = torrents[k]); ++k) {
peers = tor.getPeers();
html.push('<div class="inspector_group">');
if (torrents.length > 1) {
html.push('<div class="inspector_torrent_label">', sanitizeText(tor.getName()), '</div>');
}
if (!peers || !peers.length) {
html.push('<br></div>'); // firefox won't paint the top border if the div is empty
continue;
}
html.push(
'<table class="peer_list">',
'<tr class="inspector_peer_entry even">',
'<th class="encryptedCol"></th>',
'<th class="upCol">Up</th>',
'<th class="downCol">Down</th>',
'<th class="percentCol">%</th>',
'<th class="statusCol">Status</th>',
'<th class="addressCol">Address</th>',
'<th class="clientCol">Client</th>',
'</tr>'
);
for (i = 0; (peer = peers[i]); ++i) {
parity = i % 2 ? 'odd' : 'even';
html.push(
'<tr class="inspector_peer_entry ',
parity,
'">',
'<td>',
peer.isEncrypted
? '<div class="encrypted-peer-cell" title="Encrypted Connection">'
: '<div class="unencrypted-peer-cell">',
'</div>',
'</td>',
'<td>',
peer.rateToPeer ? fmt.speedBps(peer.rateToPeer) : '',
'</td>',
'<td>',
peer.rateToClient ? fmt.speedBps(peer.rateToClient) : '',
'</td>',
'<td class="percentCol">',
Math.floor(peer.progress * 100),
'%',
'</td>',
'<td>',
fmt.peerStatus(peer.flagStr),
'</td>',
'<td>',
sanitizeText(peer.address),
'</td>',
'<td class="clientCol">',
sanitizeText(peer.clientName),
'</td>',
'</tr>'
);
}
html.push('</table></div>');
}
setInnerHTML(peers_list, html.join(''));
},
/****
***** TRACKERS PAGE
****/
getAnnounceState = function (tracker) {
let timeUntilAnnounce,
s = '';
switch (tracker.announceState) {
case Torrent._TrackerActive:
s = 'Announce in progress';
break;
case Torrent._TrackerWaiting:
timeUntilAnnounce = tracker.nextAnnounceTime - new Date().getTime() / 1000;
if (timeUntilAnnounce < 0) {
timeUntilAnnounce = 0;
}
s = 'Next announce in ' + Transmission.fmt.timeInterval(timeUntilAnnounce);
break;
case Torrent._TrackerQueued:
s = 'Announce is queued';
break;
case Torrent._TrackerInactive:
s = tracker.isBackup ? 'Tracker will be used as a backup' : 'Announce not scheduled';
break;
default:
s = 'unknown announce state: ' + tracker.announceState;
}
return s;
},
lastAnnounceStatus = function (tracker) {
let lastAnnounceLabel = 'Last Announce',
lastAnnounce = ['N/A'],
lastAnnounceTime;
if (tracker.hasAnnounced) {
lastAnnounceTime = Transmission.fmt.timestamp(tracker.lastAnnounceTime);
if (tracker.lastAnnounceSucceeded) {
lastAnnounce = [
lastAnnounceTime,
' (got ',
Transmission.fmt.countString('peer', 'peers', tracker.lastAnnouncePeerCount),
')',
];
} else {
lastAnnounceLabel = 'Announce error';
lastAnnounce = [
tracker.lastAnnounceResult ? tracker.lastAnnounceResult + ' - ' : '',
lastAnnounceTime,
];
}
}
return {
label: lastAnnounceLabel,
value: lastAnnounce.join(''),
};
},
lastScrapeStatus = function (tracker) {
let lastScrapeLabel = 'Last Scrape',
lastScrape = 'N/A',
lastScrapeTime;
if (tracker.hasScraped) {
lastScrapeTime = Transmission.fmt.timestamp(tracker.lastScrapeTime);
if (tracker.lastScrapeSucceeded) {
lastScrape = lastScrapeTime;
} else {
lastScrapeLabel = 'Scrape error';
lastScrape =
(tracker.lastScrapeResult ? tracker.lastScrapeResult + ' - ' : '') + lastScrapeTime;
}
}
return {
label: lastScrapeLabel,
value: lastScrape,
};
},
updateTrackersPage = function () {
let i,
j,
tier,
tracker,
trackers,
tor,
html,
parity,
lastAnnounceStatusHash,
announceState,
lastScrapeStatusHash,
na = 'N/A',
trackers_list = data.elements.trackers_list,
torrents = data.torrents;
// By building up the HTML as as string, then have the browser
// turn this into a DOM tree, this is a fast operation.
html = [];
for (i = 0; (tor = torrents[i]); ++i) {
html.push('<div class="inspector_group">');
if (torrents.length > 1) {
html.push('<div class="inspector_torrent_label">', sanitizeText(tor.getName()), '</div>');
}
tier = -1;
trackers = tor.getTrackers();
for (j = 0; (tracker = trackers[j]); ++j) {
if (tier != tracker.tier) {
if (tier !== -1) {
// close previous tier
html.push('</ul></div>');
}
tier = tracker.tier;
html.push(
'<div class="inspector_group_label">',
'Tier ',
tier + 1,
'</div>',
'<ul class="tier_list">'
);
}
// Display construction
lastAnnounceStatusHash = lastAnnounceStatus(tracker);
announceState = getAnnounceState(tracker);
lastScrapeStatusHash = lastScrapeStatus(tracker);
parity = j % 2 ? 'odd' : 'even';
html.push(
'<li class="inspector_tracker_entry ',
parity,
'"><div class="tracker_host" title="',
sanitizeText(tracker.announce),
'">',
sanitizeText(tracker.host || tracker.announce),
'</div>',
'<div class="tracker_activity">',
'<div>',
lastAnnounceStatusHash['label'],
': ',
sanitizeText(lastAnnounceStatusHash['value']),
'</div>',
'<div>',
announceState,
'</div>',
'<div>',
lastScrapeStatusHash['label'],
': ',
sanitizeText(lastScrapeStatusHash['value']),
'</div>',
'</div><table class="tracker_stats">',
'<tr><th>Seeders:</th><td>',
tracker.seederCount > -1 ? tracker.seederCount : na,
'</td></tr>',
'<tr><th>Leechers:</th><td>',
tracker.leecherCount > -1 ? tracker.leecherCount : na,
'</td></tr>',
'<tr><th>Downloads:</th><td>',
tracker.downloadCount > -1 ? tracker.downloadCount : na,
'</td></tr>',
'</table></li>'
);
}
if (tier !== -1) {
// close last tier
html.push('</ul></div>');
}
html.push('</div>'); // inspector_group
}
setInnerHTML(trackers_list, html.join(''));
},
initialize = function (controller) {
data.controller = controller;
$('.inspector-tab').click(onTabClicked);
data.elements.info_page = $('#inspector-page-info')[0];
data.elements.files_page = $('#inspector-page-files')[0];
data.elements.peers_page = $('#inspector-page-peers')[0];
data.elements.trackers_page = $('#inspector-page-trackers')[0];
data.elements.file_list = $('#inspector_file_list')[0];
data.elements.peers_list = $('#inspector_peers_list')[0];
data.elements.trackers_list = $('#inspector_trackers_list')[0];
data.elements.have_lb = $('#inspector-info-have')[0];
data.elements.availability_lb = $('#inspector-info-availability')[0];
data.elements.downloaded_lb = $('#inspector-info-downloaded')[0];
data.elements.uploaded_lb = $('#inspector-info-uploaded')[0];
data.elements.state_lb = $('#inspector-info-state')[0];
data.elements.running_time_lb = $('#inspector-info-running-time')[0];
data.elements.remaining_time_lb = $('#inspector-info-remaining-time')[0];
data.elements.last_activity_lb = $('#inspector-info-last-activity')[0];
data.elements.error_lb = $('#inspector-info-error')[0];
data.elements.size_lb = $('#inspector-info-size')[0];
data.elements.foldername_lb = $('#inspector-info-location')[0];
data.elements.hash_lb = $('#inspector-info-hash')[0];
data.elements.privacy_lb = $('#inspector-info-privacy')[0];
data.elements.origin_lb = $('#inspector-info-origin')[0];
data.elements.comment_lb = $('#inspector-info-comment')[0];
data.elements.name_lb = $('#torrent_inspector_name')[0];
// force initial 'N/A' updates on all the pages
updateInspector();
updateInfoPage();
updatePeersPage();
updateTrackersPage();
updateFilesPage();
};
/****
***** PUBLIC FUNCTIONS
****/
this.setTorrents = function (torrents) {
const d = data;
// update the inspector when a selected torrent's data changes.
$(d.torrents).unbind('dataChanged.inspector');
$(torrents).bind('dataChanged.inspector', $.proxy(updateInspector, this));
d.torrents = torrents;
// periodically ask for updates to the inspector's torrents
clearTimeout(d.refreshTimeout);
function callback() {
refreshTorrents(rescheduleTimeout);
}
function rescheduleTimeout() {
d.refreshTimeout = setTimeout(callback, 2000);
}
rescheduleTimeout();
refreshTorrents();
// refresh the inspector's UI
updateInspector();
};
initialize(controller);
}