1
0
Fork 0
mirror of https://github.com/transmission/transmission synced 2024-12-25 09:13:06 +00:00
transmission/web/javascript/inspector.js
Mike Gelfand ea368388ed Fix JS error on undefined creator during inspector update
Since torrents' info is fetched asynchronously, it may not yet be
available when inspector updates. Account for possibly undefined
creator.

Refine logic used to construct torrent origin phrase to prevent
results such as "Created by  on <date>" or "Created by <creator>
on ".
2015-04-22 20:36:28 +00:00

827 lines
27 KiB
JavaScript

/**
* Copyright © Jordan Lee, 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) {
var i, id, tor;
for (i = 0; tor = torrents[i]; i++)
if (!tor.hasExtraInfo())
return true;
return false;
},
refreshTorrents = function () {
var 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);
}
},
onTabClicked = function (ev) {
var 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 () {
var 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 () {
var torrents = data.torrents,
e = data.elements,
fmt = Transmission.fmt,
none = 'None',
mixed = 'Mixed',
unknown = 'Unknown',
isMixed, allPaused, allFinished,
str,
baseline, it, s, 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, pct,
uri,
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 {
baseline = torrents[0].getLastActivity();
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;
uri = parseUri(str);
if (uri.protocol == 'http' || uri.parseUri == 'https') {
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;
}
var empty_creator = !creator || !creator.length,
empty_date = !date;
if(mixed_creator && mixed_date)
str = mixed;
else if(empty_creator && empty_date)
str = unknown;
else if((mixed_date || empty_date) && !empty_creator)
str = 'Created by ' + creator;
else if((mixed_creator || 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) {
var 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) {
var 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) {
var i, j, n, name, tokens, walk, tree, 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) {
var 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) {
var 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() {
var 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() {
var 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) {
var 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) {
var 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) {
var 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() {
var 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">', 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'], ': ', lastAnnounceStatusHash['value'], '</div>',
'<div>', announceState, '</div>',
'<div>', lastScrapeStatusHash['label'], ': ', 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) {
var ti = '#torrent_inspector_';
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) {
var 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
clearInterval(d.refreshInterval);
d.refreshInterval = setInterval($.proxy(refreshTorrents,this), 2000);
refreshTorrents();
// refresh the inspector's UI
updateInspector();
};
initialize (controller);
};