/** * 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, '' + str + ''); } 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('
'); if (torrents.length > 1) { html.push('
', sanitizeText(tor.getName()), '
'); } if (!peers || !peers.length) { html.push('
'); // firefox won't paint the top border if the div is empty continue; } html.push( '', '', '', '', '', '', '', '', '', '' ); for (i = 0; (peer = peers[i]); ++i) { parity = i % 2 ? 'odd' : 'even'; html.push( '', '', '', '', '', '', '', '', '' ); } html.push('
UpDown%StatusAddressClient
', peer.isEncrypted ? '
' : '
', '
', '
', peer.rateToPeer ? fmt.speedBps(peer.rateToPeer) : '', '', peer.rateToClient ? fmt.speedBps(peer.rateToClient) : '', '', Math.floor(peer.progress * 100), '%', '', fmt.peerStatus(peer.flagStr), '', sanitizeText(peer.address), '', sanitizeText(peer.clientName), '
'); } 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('
'); if (torrents.length > 1) { html.push('
', sanitizeText(tor.getName()), '
'); } tier = -1; trackers = tor.getTrackers(); for (j = 0; (tracker = trackers[j]); ++j) { if (tier != tracker.tier) { if (tier !== -1) { // close previous tier html.push('
'); } tier = tracker.tier; html.push( '
', 'Tier ', tier + 1, '
', ''); } html.push(''); // 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); }