Transmission: update from 2.42 to 2.50
[tomato.git] / release / src / router / transmission / web / javascript / inspector.js
blob37768a4185eb28cbb6566ae4a3a857c2af28a33c
1 /**
2  * Copyright © Jordan Lee, Dave Perrett, Malcolm Jarvis and Bruno Bierbaumer
3  *
4  * This file is licensed under the GPLv2.
5  * http://www.gnu.org/licenses/old-licenses/gpl-2.0.html
6  */
8 function Inspector(controller) {
10     var data = {
11         controller: null,
12         elements: { },
13         torrents: [ ]
14     },
16     needsExtraInfo = function (torrents) {
17         var i, id, tor;
19         for (i = 0; tor = torrents[i]; i++)
20             if (!tor.hasExtraInfo())
21                 return true;
23         return false;
24     },
26     refreshTorrents = function () {
27         var fields,
28             ids = $.map(data.torrents.slice(0), function (t) {return t.getId();});
30         if (ids && ids.length)
31         {
32             fields = ['id'].concat(Torrent.Fields.StatsExtra);
34             if (needsExtraInfo(data.torrents))
35                 $.merge(fields, Torrent.Fields.InfoExtra);
37             data.controller.updateTorrents(ids, fields);
38         }
39     },
41     onTabClicked = function (ev) {
42         var tab = ev.currentTarget;
44         if (isMobileDevice)
45             ev.stopPropagation();
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();
53         updateInspector();
54     },
56     updateInspector = function () {
57         var e = data.elements,
58             torrents = data.torrents,
59             name;
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();
66         else
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'))
72             updateInfoPage();
73         else if ($(e.peers_page).is(':visible'))
74             updatePeersPage();
75         else if ($(e.trackers_page).is(':visible'))
76             updateTrackersPage();
77         else if ($(e.files_page).is(':visible'))
78             updateFilesPage();
79     },
81     /****
82     *****  GENERAL INFO PAGE
83     ****/
85     accumulateString = function (oldVal, newVal) {
86         if (!oldVal || !oldVal.length)
87             return newVal;
88         if (oldVal === newVal)
89             return newVal;
90         return 'Mixed';
91     },
93     updateInfoPage = function () {
94         var torrents = data.torrents,
95             e = data.elements,
96             fmt = Transmission.fmt,
97             none = 'None',
98             mixed = 'Mixed',
99             unknown = 'Unknown',
100             isMixed, allPaused, allFinished,
101             str,
102             baseline, it, s, i, t,
103             sizeWhenDone = 0,
104             leftUntilDone = 0,
105             available = 0,
106             haveVerified = 0,
107             haveUnverified = 0,
108             verifiedPieces = 0,
109             stateString,
110             latest,
111             pieces,
112             size,
113             pieceSize,
114             creator, mixed_creator,
115             date, mixed_date,
116             v, u, f, d, pct,
117             now = Date.now();
119         //
120         //  state_lb
121         //
123         if(torrents.length <1)
124             str = none;
125         else {
126             isMixed = false;
127             allPaused = true;
128             allFinished = true;
130             baseline = torrents[0].getStatus();
131             for(i=0; t=torrents[i]; ++i) {
132                 it = t.getStatus();
133                 if(it != baseline)
134                     isMixed = true;
135                 if(!t.isStopped())
136                     allPaused = allFinished = false;
137                 if(!t.isFinished())
138                     allFinished = false;
139             }
140             if( isMixed )
141                 str = mixed;
142             else if( allFinished )
143                 str = 'Finished';
144             else if( allPaused )
145                 str = 'Paused';
146             else
147                 str = torrents[0].getStateString();
148         }
149         setInnerHTML(e.state_lb, str);
150         stateString = str;
152         //
153         //  have_lb
154         //
156         if(torrents.length < 1)
157             str = none;
158         else {
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();
164                     haveVerified += v;
165                     if(t.getPieceSize())
166                         verifiedPieces += v / t.getPieceSize();
167                     sizeWhenDone += t.getSizeWhenDone();
168                     leftUntilDone += t.getLeftUntilDone();
169                     available += (t.getHave()) + t.getDesiredAvailable();
170                 }
171             }
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 +'%)';
180             else
181                 str = fmt.size(haveVerified) + ' of ' + fmt.size(sizeWhenDone) + ' (' + str +'%), ' + fmt.size(haveUnverified) + ' Unverified';
182         }
183         setInnerHTML(e.have_lb, str);
185         //
186         //  availability_lb
187         //
189         if(torrents.length < 1)
190             str = none;
191         else if( sizeWhenDone == 0 )
192             str = none;
193         else
194             str = '' + fmt.percentString( ( 100.0 * available ) / sizeWhenDone ) +  '%';
195         setInnerHTML(e.availability_lb, str);
197         //
198         //  downloaded_lb
199         //
201         if(torrents.length < 1)
202             str = none;
203         else {
204             d = f = 0;
205             for(i=0; t=torrents[i]; ++i) {
206                 d = t.getDownloadedEver();
207                 f = t.getFailedEver();
208             }
209             if(f)
210                 str = fmt.size(d) + ' (' + fmt.size(f) + ' corrupt)';
211             else
212                 str = fmt.size(d);
213         }
214         setInnerHTML(e.downloaded_lb, str);
216         //
217         //  uploaded_lb
218         //
220         if(torrents.length < 1)
221             str = none;
222         else {
223             d = u = 0;
224             for(i=0; t=torrents[i]; ++i) {
225                 d = t.getDownloadedEver();
226                 u = t.getUploadedEver();
227             }
228             str = fmt.size(u) + ' (Ratio: ' + fmt.ratioString( Math.ratio(u,d))+')';
229         }
230         setInnerHTML(e.uploaded_lb, str);
232         //
233         // running time
234         //
236         if(torrents.length < 1)
237             str = none;
238         else {
239             allPaused = true;
240             baseline = torrents[0].getStartDate();
241             for(i=0; t=torrents[i]; ++i) {
242                 if(baseline != t.getStartDate())
243                     baseline = 0;
244                 if(!t.isStopped())
245                     allPaused = false;
246             }
247             if(allPaused)
248                 str = stateString; // paused || finished
249             else if(!baseline)
250                 str = mixed;
251             else
252                 str = fmt.timeInterval(now/1000 - baseline);
253         }
254         setInnerHTML(e.running_time_lb, str);
256         //
257         // remaining time
258         //
260         str = '';
261         if(torrents.length < 1)
262             str = none;
263         else {
264             baseline = torrents[0].getETA();
265             for(i=0; t=torrents[i]; ++i) {
266                 if(baseline != t.getETA()) {
267                     str = mixed;
268                     break;
269                 }
270             }
271         }
272         if(!str.length) {
273             if(baseline < 0)
274                 str = unknown;
275             else
276                 str = fmt.timeInterval(baseline);
277         }
278         setInnerHTML(e.remaining_time_lb, str);
280         //
281         // last activity
282         //
284         latest = -1;
285         if(torrents.length < 1)
286             str = none;
287         else {
288             baseline = torrents[0].getLastActivity();
289             for(i=0; t=torrents[i]; ++i) {
290                 d = t.getLastActivity();
291                 if(latest < d)
292                     latest = d;
293             }
294             d = now/1000 - latest; // seconds since last activity
295             if(d < 0)
296                 str = none;
297             else if(d < 5)
298                 str = 'Active now';
299             else
300                 str = fmt.timeInterval(d) + ' ago';
301         }
302         setInnerHTML(e.last_activity_lb, str);
304         //
305         // error
306         //
308         if(torrents.length < 1)
309             str = none;
310         else {
311             str = torrents[0].getErrorString();
312             for(i=0; t=torrents[i]; ++i) {
313                 if(str != t.getErrorString()) {
314                     str = mixed;
315                     break;
316                 }
317             }
318         }
319         setInnerHTML(e.error_lb, str || none);
321         //
322         // size
323         //
325         if(torrents.length < 1)
326             str = none;
327         else {
328             pieces = 0;
329             size = 0;
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())
335                     pieceSize = 0;
336             }
337             if(!size)
338                 str = none;
339             else if(pieceSize > 0)
340                 str = fmt.size(size) + ' (' + pieces.toStringWithCommas() + ' pieces @ ' + fmt.mem(pieceSize) + ')';
341             else
342                 str = fmt.size(size) + ' (' + pieces.toStringWithCommas() + ' pieces)';
343         }
344         setInnerHTML(e.size_lb, str);
346         //
347         //  hash
348         //
350         if(torrents.length < 1)
351             str = none;
352         else {
353             str = torrents[0].getHashString();
354             for(i=0; t=torrents[i]; ++i) {
355                 if(str != t.getHashString()) {
356                     str = mixed;
357                     break;
358                 }
359             }
360         }
361         setInnerHTML(e.hash_lb, str);
363         //
364         //  privacy
365         //
367         if(torrents.length < 1)
368             str = none;
369         else {
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()) {
374                     str = mixed;
375                     break;
376                 }
377             }
378         }
379         setInnerHTML(e.privacy_lb, str);
381         //
382         //  comment
383         //
385         if(torrents.length < 1)
386             str = none;
387         else {
388             str = torrents[0].getComment();
389             for(i=0; t=torrents[i]; ++i) {
390                 if(str != t.getComment()) {
391                     str = mixed;
392                     break;
393                 }
394             }
395         }
396         if(!str)
397             str = none;  
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>'));
400         //
401         //  origin
402         //
404         if(torrents.length < 1)
405             str = none;
406         else {
407             mixed_creator = false;
408             mixed_date = 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())
415                     mixed_date = true;
416             }
417             if(mixed_creator && mixed_date)
418                 str = mixed;
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();
423             else
424                 str = 'Created by ' + creator + ' on ' + (new Date(date*1000)).toDateString();
425         }
426         setInnerHTML(e.origin_lb, str);
428         //
429         //  foldername
430         //
432         if(torrents.length < 1)
433             str = none;
434         else {
435             str = torrents[0].getDownloadDir();
436             for(i=0; t=torrents[i]; ++i) {
437                 if(str != t.getDownloadDir()) {
438                     str = mixed;
439                     break;
440                 }
441             }
442         }
443         setInnerHTML(e.foldername_lb, str);
444     },
446     /****
447     *****  FILES PAGE
448     ****/
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);
454     },
456     selectAllFiles = function() {
457         changeFileCommand([], 'files-wanted');
458     },
460     deselectAllFiles = function() {
461         changeFileCommand([], 'files-unwanted');
462     },
464     onFileWantedToggled = function(ev, row, want) {
465         changeFileCommand([row], want?'files-wanted':'files-unwanted');
466     },
468     onFilePriorityToggled = function(ev, row, priority) {
469         var command;
470         switch(priority) {
471             case -1: command = 'priority-low'; break;
472             case  1: command = 'priority-high'; break;
473             default: command = 'priority-normal'; break;
474         }
475         changeFileCommand([row], command);
476     },
478     clearFileList = function() {
479         $(data.elements.file_list).empty();
480         delete data.file_torrent;
481         delete data.file_rows;
482     },
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) {
490             clearFileList();
491             return;
492         }
494         // build the file list
495         tor = torrents[0];
497         clearFileList();
498         data.file_torrent = tor;
499         n = tor.getFileCount();
500         data.file_rows = [];
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);
508         }
510         file_list.appendChild(fragment);
511     },
513     /****
514     *****  PEERS PAGE
515     ****/
517     updatePeersPage = function() {
518         var i, k, tor, peers, peer, parity,
519             html = [],
520             fmt = Transmission.fmt,
521             peers_list = data.elements.peers_list,
522             torrents = data.torrents;
524         for (k=0; tor=torrents[k]; ++k)
525         {
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>');
530             }
531             if (!peers || !peers.length) {
532                 html.push('<br></div>'); // firefox won't paint the top border if the div is empty
533                 continue;
534             }
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>',
544                    '</tr>');
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>',
556                        '</tr>');
557             }
558             html.push('</table></div>');
559         }
561         setInnerHTML(peers_list, html.join(''));
562     },
564     /****
565     *****  TRACKERS PAGE
566     ****/
568     getAnnounceState = function(tracker) {
569         var timeUntilAnnounce, s = '';
570         switch (tracker.announceState) {
571             case Torrent._TrackerActive:
572                 s = 'Announce in progress';
573                 break;
574             case Torrent._TrackerWaiting:
575                 timeUntilAnnounce = tracker.nextAnnounceTime - ((new Date()).getTime() / 1000);
576                 if (timeUntilAnnounce < 0) {
577                     timeUntilAnnounce = 0;
578                 }
579                 s = 'Next announce in ' + Transmission.fmt.timeInterval(timeUntilAnnounce);
580                 break;
581             case Torrent._TrackerQueued:
582                 s = 'Announce is queued';
583                 break;
584             case Torrent._TrackerInactive:
585                 s = tracker.isBackup ?
586                     'Tracker will be used as a backup' :
587                     'Announce not scheduled';
588                 break;
589             default:
590                 s = 'unknown announce state: ' + tracker.announceState;
591         }
592         return s;
593     },
595     lastAnnounceStatus = function(tracker) {
597         var lastAnnounceLabel = 'Last Announce',
598             lastAnnounce = [ 'N/A' ],
599         lastAnnounceTime;
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'), ')' ];
605             } else {
606                 lastAnnounceLabel = 'Announce error';
607                 lastAnnounce = [ (tracker.lastAnnounceResult ? (tracker.lastAnnounceResult + ' - ') : ''), lastAnnounceTime ];
608             }
609         }
610         return { 'label':lastAnnounceLabel, 'value':lastAnnounce.join('') };
611     },
613     lastScrapeStatus = function(tracker) {
615         var lastScrapeLabel = 'Last Scrape',
616             lastScrape = 'N/A',
617         lastScrapeTime;
619         if (tracker.hasScraped) {
620             lastScrapeTime = Transmission.fmt.timestamp(tracker.lastScrapeTime);
621             if (tracker.lastScrapeSucceeded) {
622                 lastScrape = lastScrapeTime;
623             } else {
624                 lastScrapeLabel = 'Scrape error';
625                 lastScrape = (tracker.lastScrapeResult ? tracker.lastScrapeResult + ' - ' : '') + lastScrapeTime;
626             }
627         }
628         return {'label':lastScrapeLabel, 'value':lastScrape};
629     },
631     updateTrackersPage = function() {
632         var i, j, tier, tracker, trackers, tor,
633             html, parity, lastAnnounceStatusHash,
634             announceState, lastScrapeStatusHash,
635             na = 'N/A',
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.
641         html = [];
642         for (i=0; tor=torrents[i]; ++i)
643         {
644             html.push ('<div class="inspector_group">');
646             if (torrents.length > 1)
647                 html.push('<div class="inspector_torrent_label">', tor.getName(), '</div>');
649             tier = -1;
650             trackers = tor.getTrackers();
651             for (j=0; tracker=trackers[j]; ++j)
652             {
653                 if (tier != tracker.tier)
654                 {
655                     if (tier !== -1) // close previous tier
656                         html.push('</ul></div>');
658                     tier = tracker.tier;
660                     html.push('<div class="inspector_group_label">',
661                           'Tier ', tier, '</div>',
662                           '<ul class="tier_list">');
663                 }
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>',
680                       '</table></li>');
681             }
682             if (tier !== -1) // close last tier
683                     html.push('</ul></div>');
685             html.push('</div>'); // inspector_group
686         }
688         setInnerHTML(trackers_list, html.join(''));
689     },
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
730         updateInspector();
731         updateInfoPage();
732         updatePeersPage();
733         updateTrackersPage();
734         updateFilesPage();
735     };
737     /****
738     *****  PUBLIC FUNCTIONS
739     ****/
741     this.setTorrents = function (torrents) {
742         var d = data;
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);
752         refreshTorrents();
754         // refresh the inspector's UI
755         updateInspector();
756     };
758     initialize (controller);