2 * Copyright © Jordan Lee, Dave Perrett, Malcolm Jarvis and Bruno Bierbaumer
4 * This file is licensed under the GPLv2.
5 * http://www.gnu.org/licenses/old-licenses/gpl-2.0.html
8 function Inspector(controller) {
16 needsExtraInfo = function (torrents) {
19 for (i = 0; tor = torrents[i]; i++)
20 if (!tor.hasExtraInfo())
26 refreshTorrents = function () {
28 ids = $.map(data.torrents.slice(0), function (t) {return t.getId();});
30 if (ids && ids.length)
32 fields = ['id'].concat(Torrent.Fields.StatsExtra);
34 if (needsExtraInfo(data.torrents))
35 $.merge(fields, Torrent.Fields.InfoExtra);
37 data.controller.updateTorrents(ids, fields);
41 onTabClicked = function (ev) {
42 var tab = ev.currentTarget;
47 // select this tab and deselect the others
48 $(tab).addClass('selected').siblings().removeClass('selected');
50 // show this tab and hide the others
51 $('#' + tab.id.replace('tab','page')).show().siblings('.inspector-page').hide();
56 updateInspector = function () {
57 var e = data.elements,
58 torrents = data.torrents,
61 // update the name, which is shown on all the pages
62 if (!torrents || !torrents.length)
63 name = 'No Selection';
64 else if(torrents.length === 1)
65 name = torrents[0].getName();
67 name = '' + torrents.length+' Transfers Selected';
68 setInnerHTML(e.name_lb, name || na);
70 // update the visible page
71 if ($(e.info_page).is(':visible'))
73 else if ($(e.peers_page).is(':visible'))
75 else if ($(e.trackers_page).is(':visible'))
77 else if ($(e.files_page).is(':visible'))
82 ***** GENERAL INFO PAGE
85 accumulateString = function (oldVal, newVal) {
86 if (!oldVal || !oldVal.length)
88 if (oldVal === newVal)
93 updateInfoPage = function () {
94 var torrents = data.torrents,
96 fmt = Transmission.fmt,
100 isMixed, allPaused, allFinished,
102 baseline, it, s, i, t,
114 creator, mixed_creator,
123 if(torrents.length <1)
130 baseline = torrents[0].getStatus();
131 for(i=0; t=torrents[i]; ++i) {
136 allPaused = allFinished = false;
142 else if( allFinished )
147 str = torrents[0].getStateString();
149 setInnerHTML(e.state_lb, str);
156 if(torrents.length < 1)
159 baseline = torrents[0].getStatus();
160 for(i=0; t=torrents[i]; ++i) {
161 if(!t.needsMetaData()) {
162 haveUnverified += t.getHaveUnchecked();
163 v = t.getHaveValid();
166 verifiedPieces += v / t.getPieceSize();
167 sizeWhenDone += t.getSizeWhenDone();
168 leftUntilDone += t.getLeftUntilDone();
169 available += (t.getHave()) + t.getDesiredAvailable();
173 d = 100.0 * ( sizeWhenDone ? ( sizeWhenDone - leftUntilDone ) / sizeWhenDone : 1 );
174 str = fmt.percentString( d );
176 if( !haveUnverified && !leftUntilDone )
177 str = fmt.size(haveVerified) + ' (100%)';
178 else if( !haveUnverified )
179 str = fmt.size(haveVerified) + ' of ' + fmt.size(sizeWhenDone) + ' (' + str +'%)';
181 str = fmt.size(haveVerified) + ' of ' + fmt.size(sizeWhenDone) + ' (' + str +'%), ' + fmt.size(haveUnverified) + ' Unverified';
183 setInnerHTML(e.have_lb, str);
189 if(torrents.length < 1)
191 else if( sizeWhenDone == 0 )
194 str = '' + fmt.percentString( ( 100.0 * available ) / sizeWhenDone ) + '%';
195 setInnerHTML(e.availability_lb, str);
201 if(torrents.length < 1)
205 for(i=0; t=torrents[i]; ++i) {
206 d = t.getDownloadedEver();
207 f = t.getFailedEver();
210 str = fmt.size(d) + ' (' + fmt.size(f) + ' corrupt)';
214 setInnerHTML(e.downloaded_lb, str);
220 if(torrents.length < 1)
224 for(i=0; t=torrents[i]; ++i) {
225 d = t.getDownloadedEver();
226 u = t.getUploadedEver();
228 str = fmt.size(u) + ' (Ratio: ' + fmt.ratioString( Math.ratio(u,d))+')';
230 setInnerHTML(e.uploaded_lb, str);
236 if(torrents.length < 1)
240 baseline = torrents[0].getStartDate();
241 for(i=0; t=torrents[i]; ++i) {
242 if(baseline != t.getStartDate())
248 str = stateString; // paused || finished
252 str = fmt.timeInterval(now/1000 - baseline);
254 setInnerHTML(e.running_time_lb, str);
261 if(torrents.length < 1)
264 baseline = torrents[0].getETA();
265 for(i=0; t=torrents[i]; ++i) {
266 if(baseline != t.getETA()) {
276 str = fmt.timeInterval(baseline);
278 setInnerHTML(e.remaining_time_lb, str);
285 if(torrents.length < 1)
288 baseline = torrents[0].getLastActivity();
289 for(i=0; t=torrents[i]; ++i) {
290 d = t.getLastActivity();
294 d = now/1000 - latest; // seconds since last activity
300 str = fmt.timeInterval(d) + ' ago';
302 setInnerHTML(e.last_activity_lb, str);
308 if(torrents.length < 1)
311 str = torrents[0].getErrorString();
312 for(i=0; t=torrents[i]; ++i) {
313 if(str != t.getErrorString()) {
319 setInnerHTML(e.error_lb, str || none);
325 if(torrents.length < 1)
330 pieceSize = torrents[0].getPieceSize();
331 for(i=0; t=torrents[i]; ++i) {
332 pieces += t.getPieceCount();
333 size += t.getTotalSize();
334 if(pieceSize != t.getPieceSize())
339 else if(pieceSize > 0)
340 str = fmt.size(size) + ' (' + pieces.toStringWithCommas() + ' pieces @ ' + fmt.mem(pieceSize) + ')';
342 str = fmt.size(size) + ' (' + pieces.toStringWithCommas() + ' pieces)';
344 setInnerHTML(e.size_lb, str);
350 if(torrents.length < 1)
353 str = torrents[0].getHashString();
354 for(i=0; t=torrents[i]; ++i) {
355 if(str != t.getHashString()) {
361 setInnerHTML(e.hash_lb, str);
367 if(torrents.length < 1)
370 baseline = torrents[0].getPrivateFlag();
371 str = baseline ? 'Private to this tracker -- DHT and PEX disabled' : 'Public torrent';
372 for(i=0; t=torrents[i]; ++i) {
373 if(baseline != t.getPrivateFlag()) {
379 setInnerHTML(e.privacy_lb, str);
385 if(torrents.length < 1)
388 str = torrents[0].getComment();
389 for(i=0; t=torrents[i]; ++i) {
390 if(str != t.getComment()) {
398 setInnerHTML(e.comment_lb, str.replace(/(https?|ftp):\/\/([\w\-]+(\.[\w\-]+)*(\.[a-z]{2,4})?)(\d{1,5})?(\/([^<>\s]*))?/g, '<a target="_blank" href="$&">$&</a>'));
404 if(torrents.length < 1)
407 mixed_creator = false;
409 creator = torrents[0].getCreator();
410 date = torrents[0].getDateCreated();
411 for(i=0; t=torrents[i]; ++i) {
412 if(creator != t.getCreator())
413 mixed_creator = true;
414 if(date != t.getDateCreated())
417 if(mixed_creator && mixed_date)
419 else if(mixed_date && creator.length)
420 str = 'Created by ' + creator;
421 else if(mixed_creator && date)
422 str = 'Created on ' + (new Date(date*1000)).toDateString();
424 str = 'Created by ' + creator + ' on ' + (new Date(date*1000)).toDateString();
426 setInnerHTML(e.origin_lb, str);
432 if(torrents.length < 1)
435 str = torrents[0].getDownloadDir();
436 for(i=0; t=torrents[i]; ++i) {
437 if(str != t.getDownloadDir()) {
443 setInnerHTML(e.foldername_lb, str);
450 changeFileCommand = function(rows, command) {
451 var torrentId = data.file_torrent.getId();
452 var rowIndices = $.map(rows.slice(0),function (row) {return row.getIndex();});
453 data.controller.changeFileCommand(torrentId, rowIndices, command);
456 selectAllFiles = function() {
457 changeFileCommand([], 'files-wanted');
460 deselectAllFiles = function() {
461 changeFileCommand([], 'files-unwanted');
464 onFileWantedToggled = function(ev, row, want) {
465 changeFileCommand([row], want?'files-wanted':'files-unwanted');
468 onFilePriorityToggled = function(ev, row, priority) {
471 case -1: command = 'priority-low'; break;
472 case 1: command = 'priority-high'; break;
473 default: command = 'priority-normal'; break;
475 changeFileCommand([row], command);
478 clearFileList = function() {
479 $(data.elements.file_list).empty();
480 delete data.file_torrent;
481 delete data.file_rows;
484 updateFilesPage = function() {
485 var i, n, sel, row, tor, fragment,
486 file_list = data.elements.file_list,
487 torrents = data.torrents;
489 if (torrents.length !== 1) {
494 // build the file list
498 data.file_torrent = tor;
499 n = tor.getFileCount();
501 fragment = document.createDocumentFragment();
503 for (i=0; i<n; ++i) {
504 row = data.file_rows[i] = new FileRow(tor, i);
505 fragment.appendChild(row.getElement());
506 $(row).bind('wantedToggled',onFileWantedToggled);
507 $(row).bind('priorityToggled',onFilePriorityToggled);
510 file_list.appendChild(fragment);
517 updatePeersPage = function() {
518 var i, k, tor, peers, peer, parity,
520 fmt = Transmission.fmt,
521 peers_list = data.elements.peers_list,
522 torrents = data.torrents;
524 for (k=0; tor=torrents[k]; ++k)
526 peers = tor.getPeers();
527 html.push('<div class="inspector_group">');
528 if (torrents.length > 1) {
529 html.push('<div class="inspector_torrent_label">', tor.getName(), '</div>');
531 if (!peers || !peers.length) {
532 html.push('<br></div>'); // firefox won't paint the top border if the div is empty
535 html.push('<table class="peer_list">',
536 '<tr class="inspector_peer_entry even">',
537 '<th class="encryptedCol"></th>',
538 '<th class="upCol">Up</th>',
539 '<th class="downCol">Down</th>',
540 '<th class="percentCol">%</th>',
541 '<th class="statusCol">Status</th>',
542 '<th class="addressCol">Address</th>',
543 '<th class="clientCol">Client</th>',
545 for (i=0; peer=peers[i]; ++i) {
546 parity = (i%2) ? 'odd' : 'even';
547 html.push('<tr class="inspector_peer_entry ', parity, '">',
548 '<td>', (peer.isEncrypted ? '<div class="encrypted-peer-cell" title="Encrypted Connection">'
549 : '<div class="unencrypted-peer-cell">'), '</div>', '</td>',
550 '<td>', (peer.rateToPeer ? fmt.speedBps(peer.rateToPeer) : ''), '</td>',
551 '<td>', (peer.rateToClient ? fmt.speedBps(peer.rateToClient) : ''), '</td>',
552 '<td class="percentCol">', Math.floor(peer.progress*100), '%', '</td>',
553 '<td>', fmt.peerStatus(peer.flagStr), '</td>',
554 '<td>', peer.address, '</td>',
555 '<td class="clientCol">', peer.clientName, '</td>',
558 html.push('</table></div>');
561 setInnerHTML(peers_list, html.join(''));
568 getAnnounceState = function(tracker) {
569 var timeUntilAnnounce, s = '';
570 switch (tracker.announceState) {
571 case Torrent._TrackerActive:
572 s = 'Announce in progress';
574 case Torrent._TrackerWaiting:
575 timeUntilAnnounce = tracker.nextAnnounceTime - ((new Date()).getTime() / 1000);
576 if (timeUntilAnnounce < 0) {
577 timeUntilAnnounce = 0;
579 s = 'Next announce in ' + Transmission.fmt.timeInterval(timeUntilAnnounce);
581 case Torrent._TrackerQueued:
582 s = 'Announce is queued';
584 case Torrent._TrackerInactive:
585 s = tracker.isBackup ?
586 'Tracker will be used as a backup' :
587 'Announce not scheduled';
590 s = 'unknown announce state: ' + tracker.announceState;
595 lastAnnounceStatus = function(tracker) {
597 var lastAnnounceLabel = 'Last Announce',
598 lastAnnounce = [ 'N/A' ],
601 if (tracker.hasAnnounced) {
602 lastAnnounceTime = Transmission.fmt.timestamp(tracker.lastAnnounceTime);
603 if (tracker.lastAnnounceSucceeded) {
604 lastAnnounce = [ lastAnnounceTime, ' (got ', Transmission.fmt.plural(tracker.lastAnnouncePeerCount, 'peer'), ')' ];
606 lastAnnounceLabel = 'Announce error';
607 lastAnnounce = [ (tracker.lastAnnounceResult ? (tracker.lastAnnounceResult + ' - ') : ''), lastAnnounceTime ];
610 return { 'label':lastAnnounceLabel, 'value':lastAnnounce.join('') };
613 lastScrapeStatus = function(tracker) {
615 var lastScrapeLabel = 'Last Scrape',
619 if (tracker.hasScraped) {
620 lastScrapeTime = Transmission.fmt.timestamp(tracker.lastScrapeTime);
621 if (tracker.lastScrapeSucceeded) {
622 lastScrape = lastScrapeTime;
624 lastScrapeLabel = 'Scrape error';
625 lastScrape = (tracker.lastScrapeResult ? tracker.lastScrapeResult + ' - ' : '') + lastScrapeTime;
628 return {'label':lastScrapeLabel, 'value':lastScrape};
631 updateTrackersPage = function() {
632 var i, j, tier, tracker, trackers, tor,
633 html, parity, lastAnnounceStatusHash,
634 announceState, lastScrapeStatusHash,
636 trackers_list = data.elements.trackers_list,
637 torrents = data.torrents;
639 // By building up the HTML as as string, then have the browser
640 // turn this into a DOM tree, this is a fast operation.
642 for (i=0; tor=torrents[i]; ++i)
644 html.push ('<div class="inspector_group">');
646 if (torrents.length > 1)
647 html.push('<div class="inspector_torrent_label">', tor.getName(), '</div>');
650 trackers = tor.getTrackers();
651 for (j=0; tracker=trackers[j]; ++j)
653 if (tier != tracker.tier)
655 if (tier !== -1) // close previous tier
656 html.push('</ul></div>');
660 html.push('<div class="inspector_group_label">',
661 'Tier ', tier, '</div>',
662 '<ul class="tier_list">');
665 // Display construction
666 lastAnnounceStatusHash = lastAnnounceStatus(tracker);
667 announceState = getAnnounceState(tracker);
668 lastScrapeStatusHash = lastScrapeStatus(tracker);
669 parity = (j%2) ? 'odd' : 'even';
670 html.push('<li class="inspector_tracker_entry ', parity, '"><div class="tracker_host" title="', tracker.announce, '">',
671 tracker.host, '</div>',
672 '<div class="tracker_activity">',
673 '<div>', lastAnnounceStatusHash['label'], ': ', lastAnnounceStatusHash['value'], '</div>',
674 '<div>', announceState, '</div>',
675 '<div>', lastScrapeStatusHash['label'], ': ', lastScrapeStatusHash['value'], '</div>',
676 '</div><table class="tracker_stats">',
677 '<tr><th>Seeders:</th><td>', (tracker.seederCount > -1 ? tracker.seederCount : na), '</td></tr>',
678 '<tr><th>Leechers:</th><td>', (tracker.leecherCount > -1 ? tracker.leecherCount : na), '</td></tr>',
679 '<tr><th>Downloads:</th><td>', (tracker.downloadCount > -1 ? tracker.downloadCount : na), '</td></tr>',
682 if (tier !== -1) // close last tier
683 html.push('</ul></div>');
685 html.push('</div>'); // inspector_group
688 setInnerHTML(trackers_list, html.join(''));
691 initialize = function (controller) {
693 var ti = '#torrent_inspector_';
695 data.controller = controller;
697 $('.inspector-tab').click(onTabClicked);
699 data.elements.info_page = $('#inspector-page-info')[0];
700 data.elements.files_page = $('#inspector-page-files')[0];
701 data.elements.peers_page = $('#inspector-page-peers')[0];
702 data.elements.trackers_page = $('#inspector-page-trackers')[0];
704 data.elements.file_list = $('#inspector_file_list')[0];
705 data.elements.peers_list = $('#inspector_peers_list')[0];
706 data.elements.trackers_list = $('#inspector_trackers_list')[0];
708 data.elements.have_lb = $('#inspector-info-have')[0];
709 data.elements.availability_lb = $('#inspector-info-availability')[0];
710 data.elements.downloaded_lb = $('#inspector-info-downloaded')[0];
711 data.elements.uploaded_lb = $('#inspector-info-uploaded')[0];
712 data.elements.state_lb = $('#inspector-info-state')[0];
713 data.elements.running_time_lb = $('#inspector-info-running-time')[0];
714 data.elements.remaining_time_lb = $('#inspector-info-remaining-time')[0];
715 data.elements.last_activity_lb = $('#inspector-info-last-activity')[0];
716 data.elements.error_lb = $('#inspector-info-error')[0];
717 data.elements.size_lb = $('#inspector-info-size')[0];
718 data.elements.foldername_lb = $('#inspector-info-location')[0];
719 data.elements.hash_lb = $('#inspector-info-hash')[0];
720 data.elements.privacy_lb = $('#inspector-info-privacy')[0];
721 data.elements.origin_lb = $('#inspector-info-origin')[0];
722 data.elements.comment_lb = $('#inspector-info-comment')[0];
723 data.elements.name_lb = $('#torrent_inspector_name')[0];
725 // file page's buttons
726 $('#select-all-files').click(selectAllFiles);
727 $('#deselect-all-files').click(deselectAllFiles);
729 // force initial 'N/A' updates on all the pages
733 updateTrackersPage();
738 ***** PUBLIC FUNCTIONS
741 this.setTorrents = function (torrents) {
744 // update the inspector when a selected torrent's data changes.
745 $(d.torrents).unbind('dataChanged.inspector');
746 $(torrents).bind('dataChanged.inspector', $.proxy(updateInspector,this));
747 d.torrents = torrents;
749 // periodically ask for updates to the inspector's torrents
750 clearInterval(d.refreshInterval);
751 d.refreshInterval = setInterval($.proxy(refreshTorrents,this), 2000);
754 // refresh the inspector's UI
758 initialize (controller);