/* * Copyright © Dave Perrett and Malcolm Jarvis * This code is licensed under the GPL version 2. * For details, see http://www.gnu.org/licenses/old-licenses/gpl-2.0.html * * Class Torrent */ function Torrent( transferListParent, fileListParent, controller, data) { this.initialize( transferListParent, fileListParent, controller, data); } // Constants Torrent._StatusWaitingToCheck = 1; Torrent._StatusChecking = 2; Torrent._StatusDownloading = 4; Torrent._StatusSeeding = 8; Torrent._StatusPaused = 16; Torrent._InfiniteTimeRemaining = 215784000; // 999 Hours - may as well be infinite Torrent._RatioUseGlobal = 0; Torrent._RatioUseLocal = 1; Torrent._RatioUnlimited = 2; Torrent._ErrNone = 0; Torrent._ErrTrackerWarning = 1; Torrent._ErrTrackerError = 2; Torrent._ErrLocalError = 3; Torrent._TrackerInactive = 0; Torrent._TrackerWaiting = 1; Torrent._TrackerQueued = 2; Torrent._TrackerActive = 3; Torrent._StaticFields = [ 'hashString', 'id' ] Torrent._MetaDataFields = [ 'addedDate', 'comment', 'creator', 'dateCreated', 'isPrivate', 'name', 'totalSize', 'pieceCount', 'pieceSize' ] Torrent._DynamicFields = [ 'downloadedEver', 'error', 'errorString', 'eta', 'haveUnchecked', 'haveValid', 'leftUntilDone', 'metadataPercentComplete', 'peers', 'peersConnected', 'peersGettingFromUs', 'peersSendingToUs', 'rateDownload', 'rateUpload', 'recheckProgress', 'sizeWhenDone', 'status', 'trackerStats', 'desiredAvailable', 'uploadedEver', 'uploadRatio', 'seedRatioLimit', 'seedRatioMode', 'downloadDir', 'isFinished' ] Torrent.prototype = { initMetaData: function( data ) { this._date = data.addedDate; this._comment = data.comment; this._creator = data.creator; this._creator_date = data.dateCreated; this._is_private = data.isPrivate; this._name = data.name; this._name_lc = this._name.toLowerCase( ); this._size = data.totalSize; this._pieceCount = data.pieceCount; this._pieceSize = data.pieceSize; if( data.files ) { for( var i=0, row; row=data.files[i]; ++i ) { this._file_model[i] = { 'index': i, 'torrent': this, 'length': row.length, 'name': row.name }; } } }, /* * Constructor */ initialize: function( transferListParent, fileListParent, controller, data) { this._id = data.id; this._hashString = data.hashString; this._sizeWhenDone = data.sizeWhenDone; this._trackerStats = this.buildTrackerStats(data.trackerStats); this._file_model = [ ]; this._file_view = [ ]; this.initMetaData( data ); // Create a new
  • element var top_e = document.createElement( 'li' ); top_e.className = 'torrent'; top_e.id = 'torrent_' + data.id; top_e._torrent = this; var element = $(top_e); $(element).bind('dblclick', function(e) { transmission.toggleInspector(); }); element._torrent = this; element._id = this._id; this._element = element; this._controller = controller; controller._rows.push( element ); // Create the 'name'
    var e = document.createElement( 'div' ); e.className = 'torrent_name'; top_e.appendChild( e ); element._name_container = e; // Create the 'peer details'
    e = document.createElement( 'div' ); e.className = 'torrent_peer_details'; top_e.appendChild( e ); element._peer_details_container = e; //Create a progress bar container top_a = document.createElement( 'div' ); top_a.className = 'torrent_progress_bar_container'; element._progress_bar_container = top_a; // Create the 'in progress' bar e = document.createElement( 'div' ); e.className = 'torrent_progress_bar incomplete'; e.style.width = '0%'; top_a.appendChild( e ); element._progress_complete_container = e; // Create the 'incomplete' bar (initially hidden) e = document.createElement( 'div' ); e.className = 'torrent_progress_bar incomplete'; e.style.display = 'none'; top_a.appendChild( e ); element._progress_incomplete_container = e; //Add the progress bar container to the torrent top_e.appendChild(top_a); // Add the pause/resume button - don't specify the // image or alt text until the 'refresh()' function // (depends on torrent state) var image = document.createElement( 'div' ); image.className = 'torrent_pause'; e = document.createElement( 'a' ); e.appendChild( image ); top_e.appendChild( e ); element._pause_resume_button_image = image; if (!iPhone) $(e).bind('click', function(e) { element._torrent.clickPauseResumeButton(e); }); // Create the 'progress details'
    e = document.createElement( 'div' ); e.className = 'torrent_progress_details'; top_e.appendChild( e ); element._progress_details_container = e; // Set the torrent click observer element.bind('click', function(e){ element._torrent.clickTorrent(e) }); // Safari hack - first torrent needs to be moved down for some reason. Seems to be ok when // using
  • 's in straight html, but adding through the DOM gets a bit odd. if ($.browser.safari) this._element.css('margin-top', '7px'); this.initializeTorrentFilesInspectorGroup( fileListParent ); // Update all the labels etc this.refresh(data); // insert the element transferListParent.appendChild(top_e); }, initializeTorrentFilesInspectorGroup: function( fileListParent ) { var e = document.createElement( 'ul' ); e.className = 'inspector_torrent_file_list inspector_group'; e.style.display = 'none'; fileListParent.appendChild( e ); this._fileList = e; }, fileList: function() { return $(this._fileList); }, buildTrackerStats: function(trackerStats) { result = []; for( var i=0, tracker; tracker=trackerStats[i]; ++i ) { tier = result[tracker.tier] || []; tier[tier.length] = { 'host': tracker.host, 'announce': tracker.announce, 'hasAnnounced': tracker.hasAnnounced, 'lastAnnounceTime': tracker.lastAnnounceTime, 'lastAnnounceSucceeded': tracker.lastAnnounceSucceeded, 'lastAnnounceResult': tracker.lastAnnounceResult, 'lastAnnouncePeerCount': tracker.lastAnnouncePeerCount, 'announceState': tracker.announceState, 'nextAnnounceTime': tracker.nextAnnounceTime, 'isBackup': tracker.isBackup, 'hasScraped': tracker.hasScraped, 'lastScrapeTime': tracker.lastScrapeTime, 'lastScrapeSucceeded': tracker.lastScrapeSucceeded, 'lastScrapeResult': tracker.lastScrapeResult, 'seederCount': tracker.seederCount, 'leecherCount': tracker.leecherCount, 'downloadCount': tracker.downloadCount }; result[tracker.tier] = tier; } return result; }, /*-------------------------------------------- * * S E T T E R S / G E T T E R S * *--------------------------------------------*/ /* Return the DOM element for this torrent (a
  • element) */ element: function() { return this._element; }, setElement: function( element ) { this._element = element; element._torrent = this; element[0]._torrent = this; this.refreshHTML( ); }, activity: function() { return this._download_speed + this._upload_speed; }, comment: function() { return this._comment; }, completed: function() { return this._completed; }, creator: function() { return this._creator; }, dateAdded: function() { return this._date; }, downloadSpeed: function() { return this._download_speed; }, downloadTotal: function() { return this._download_total; }, hash: function() { return this._hashString; }, id: function() { return this._id; }, isActiveFilter: function() { return this.peersGettingFromUs() > 0 || this.peersSendingToUs() > 0 || this.webseedsSendingToUs() > 0 || this.state() == Torrent._StatusChecking; }, isActive: function() { return this.state() != Torrent._StatusPaused; }, isDownloading: function() { return this.state() == Torrent._StatusDownloading; }, isFinished: function() { return this._isFinishedSeeding; }, isSeeding: function() { return this.state() == Torrent._StatusSeeding; }, name: function() { return this._name; }, webseedsSendingToUs: function() { return this._webseeds_sending_to_us; }, peersSendingToUs: function() { return this._peers_sending_to_us; }, peersGettingFromUs: function() { return this._peers_getting_from_us; }, needsMetaData: function(){ return this._metadataPercentComplete < 1 }, getPercentDone: function() { if( !this._sizeWhenDone ) return 1.0; if( !this._leftUntilDone ) return 1.0; return ( this._sizeWhenDone - this._leftUntilDone ) / this._sizeWhenDone; }, getPercentDoneStr: function() { return Transmission.fmt.percentString( 100 * this.getPercentDone() ); }, size: function() { return this._size; }, state: function() { return this._state; }, stateStr: function() { switch( this.state() ) { case Torrent._StatusSeeding: return 'Seeding'; case Torrent._StatusDownloading: return 'Downloading'; case Torrent._StatusPaused: return this.isFinished() ? 'Seeding complete' : 'Paused'; case Torrent._StatusChecking: return 'Verifying local data'; case Torrent._StatusWaitingToCheck: return 'Waiting to verify'; default: return 'error'; } }, trackerStats: function() { return this._trackerStats; }, uploadSpeed: function() { return this._upload_speed; }, uploadTotal: function() { return this._upload_total; }, showFileList: function() { if(this.fileList().is(':visible')) return; this.ensureFileListExists(); this.refreshFileView(); this.fileList().show(); }, hideFileList: function() { this.fileList().hide(); }, seedRatioLimit: function(){ switch( this._seed_ratio_mode ) { case Torrent._RatioUseGlobal: return this._controller.seedRatioLimit(); case Torrent._RatioUseLocal: return this._seed_ratio_limit; default: return -1; } }, /*-------------------------------------------- * * E V E N T F U N C T I O N S * *--------------------------------------------*/ /* * Process a click event on this torrent */ clickTorrent: function( event ) { // Prevents click carrying to parent element // which deselects all on click event.stopPropagation(); // but still hide the context menu if it is showing $('#jqContextMenu').hide(); var torrent = this; // 'Apple' button emulation on PC : // Need settable meta-key and ctrl-key variables for mac emulation var meta_key = event.metaKey; var ctrl_key = event.ctrlKey; if (event.ctrlKey && navigator.appVersion.toLowerCase().indexOf("mac") == -1) { meta_key = true; ctrl_key = false; } // Shift-Click - Highlight a range between this torrent and the last-clicked torrent if (iPhone) { if ( torrent.isSelected() ) torrent._controller.showInspector(); torrent._controller.setSelectedTorrent( torrent, true ); } else if (event.shiftKey) { torrent._controller.selectRange( torrent, true ); // Need to deselect any selected text window.focus(); // Apple-Click, not selected } else if (!torrent.isSelected() && meta_key) { torrent._controller.selectTorrent( torrent, true ); // Regular Click, not selected } else if (!torrent.isSelected()) { torrent._controller.setSelectedTorrent( torrent, true ); // Apple-Click, selected } else if (torrent.isSelected() && meta_key) { torrent._controller.deselectTorrent( torrent, true ); // Regular Click, selected } else if (torrent.isSelected()) { torrent._controller.setSelectedTorrent( torrent, true ); } torrent._controller.setLastTorrentClicked(torrent); }, /* * Process a click event on the pause/resume button */ clickPauseResumeButton: function( event ) { // prevent click event resulting in selection of torrent event.stopPropagation(); // either stop or start the torrent var torrent = this; if( torrent.isActive( ) ) torrent._controller.stopTorrent( torrent ); else torrent._controller.startTorrent( torrent ); }, /*-------------------------------------------- * * I N T E R F A C E F U N C T I O N S * *--------------------------------------------*/ refreshMetaData: function(data) { this.initMetaData( data ); this.ensureFileListExists(); this.refreshFileView(); this.refreshHTML( ); }, refresh: function(data) { this.refreshData( data ); this.refreshHTML( ); }, /* * Refresh display */ refreshData: function(data) { if( this.needsMetaData() && ( data.metadataPercentComplete >= 1 ) ) transmission.refreshMetaData( [ this._id ] ); this._completed = data.haveUnchecked + data.haveValid; this._verified = data.haveValid; this._leftUntilDone = data.leftUntilDone; this._download_total = data.downloadedEver; this._upload_total = data.uploadedEver; this._upload_ratio = data.uploadRatio; this._seed_ratio_limit = data.seedRatioLimit; this._seed_ratio_mode = data.seedRatioMode; this._download_speed = data.rateDownload; this._upload_speed = data.rateUpload; this._peers = data.peers; this._peers_connected = data.peersConnected; this._peers_getting_from_us = data.peersGettingFromUs; this._peers_sending_to_us = data.peersSendingToUs; this._webseeds_sending_to_us = data.webseedsSendingToUs; this._sizeWhenDone = data.sizeWhenDone; this._recheckProgress = data.recheckProgress; this._error = data.error; this._error_string = data.errorString; this._eta = data.eta; this._trackerStats = this.buildTrackerStats(data.trackerStats); this._state = data.status; this._download_dir = data.downloadDir; this._metadataPercentComplete = data.metadataPercentComplete; this._isFinishedSeeding = data.isFinished; this._desiredAvailable = data.desiredAvailable; if (data.fileStats) this.refreshFileModel( data ); }, refreshFileModel: function(data) { for( var i=0; i 0; // Fix for situation // when a verifying/downloading torrent gets state seeding if( this._state === Torrent._StatusSeeding ) notDone = false ; if( this.needsMetaData() ){ var metaPercentComplete = this._metadataPercentComplete * 100; progress_details = [ "Magnetized transfer - retrieving metadata (", Transmission.fmt.percentString( metaPercentComplete ), "%)" ].join(''); var empty = ""; if(metaPercentComplete == 0) empty = "empty"; root._progress_complete_container.style.width = metaPercentComplete + "%"; root._progress_complete_container.className = 'torrent_progress_bar in_progress meta ' + empty+compact; root._progress_incomplete_container.style.width = 100 - metaPercentComplete + "%" root._progress_incomplete_container.className = 'torrent_progress_bar incomplete compact meta'+compact; root._progress_incomplete_container.style.display = 'block'; } else if( notDone ) { // Create the 'progress details' label // Eg: '101 MiB of 631 MiB (16.02%) - 2 hr remaining' c = [ Transmission.fmt.size( this._sizeWhenDone - this._leftUntilDone ), ' of ', Transmission.fmt.size( this._sizeWhenDone ), ' (', this.getPercentDoneStr(), '%)' ]; if( this.isActive( ) ) { c.push( ' - ' ); if (this._eta < 0 || this._eta >= Torrent._InfiniteTimeRemaining ) c.push( 'remaining time unknown' ); else c.push( Transmission.fmt.timeInterval(this._eta) + ' remaining' ); } progress_details = c.join(''); // Figure out the percent completed var css_completed_width = ( this.getPercentDone() * MaxBarWidth ).toTruncFixed( 2 ); // Update the 'in progress' bar e = root._progress_complete_container; c = [ 'torrent_progress_bar'+compact, this.isActive() ? 'in_progress' : 'incomplete_stopped' ]; if(css_completed_width === 0) { c.push( 'empty' ); } e.className = c.join(' '); e.style.width = css_completed_width + '%'; // Update the 'incomplete' bar e = root._progress_incomplete_container; e.className = 'torrent_progress_bar incomplete' e.style.width = (MaxBarWidth - css_completed_width) + '%'; e.style.display = 'block'; } else { // Create the 'progress details' label if( this._size == this._sizeWhenDone ) { // seed: '698.05 MiB' c = [ Transmission.fmt.size( this._size ) ]; } else { // partial seed: '127.21 MiB of 698.05 MiB (18.2%)' c = [ Transmission.fmt.size( this._sizeWhenDone ), ' of ', Transmission.fmt.size( this._size ), ' (', Transmission.fmt.percentString( 100.0 * this._sizeWhenDone / this._size ), '%)' ]; } // append UL stats: ', uploaded 8.59 GiB (Ratio: 12.3)' c.push( ', uploaded ', Transmission.fmt.size( this._upload_total ), ' (Ratio ', Transmission.fmt.ratioString( this._upload_ratio ), ')' ); // maybe append remaining time if( this.isActive( ) && this.seedRatioLimit( ) > 0 ) { c.push(' - '); if (this._eta < 0 || this._eta >= Torrent._InfiniteTimeRemaining ) c.push( 'remaining time unknown' ); else c.push( Transmission.fmt.timeInterval(this._eta), ' remaining' ); } progress_details = c.join(''); var status = this.isActive() ? 'complete' : 'complete_stopped'; if(this.isActive() && this.seedRatioLimit() > 0){ status = 'complete seeding' var seedRatioRatio = this._upload_ratio / this.seedRatioLimit(); var seedRatioPercent = Math.round( seedRatioRatio * 100 * MaxBarWidth ) / 100; // Set progress to percent seeded root._progress_complete_container.style.width = seedRatioPercent + '%'; // Update the 'incomplete' bar root._progress_incomplete_container.className = 'torrent_progress_bar incomplete seeding' root._progress_incomplete_container.style.display = 'block'; root._progress_incomplete_container.style.width = MaxBarWidth - seedRatioPercent + '%'; } else { // Hide the 'incomplete' bar root._progress_incomplete_container.className = 'torrent_progress_bar incomplete' root._progress_incomplete_container.style.display = 'none'; // Set progress to maximum root._progress_complete_container.style.width = MaxBarWidth + '%'; } // Update the 'in progress' bar e = root._progress_complete_container; e.className = 'torrent_progress_bar ' + status; } var hasError = this.getErrorMessage( ) != undefined; // Update the progress details if(compact_mode){ progress_details = this.getPeerDetails(); $(root._progress_details_container).toggleClass('error',hasError); } else { $(root._peer_details_container).toggleClass('error',hasError); } setInnerHTML( root._progress_details_container, progress_details ); if( compact ){ var width = root._progress_details_container.offsetLeft - root._name_container.offsetLeft; root._name_container.style.width = width + 'px'; } else { root._name_container.style.width = '100%'; } // Update the peer details and pause/resume button e = root._pause_resume_button_image; if ( this.state() === Torrent._StatusPaused ) { e.alt = 'Resume'; e.className = "torrent_resume"+compact; } else { e.alt = 'Pause'; e.className = "torrent_pause"+compact; } setInnerHTML( root._peer_details_container, this.getPeerDetails( ) ); this.refreshFileView( ); }, refreshFileView: function() { if( this._file_view.length ) for( var i=0; i= this._size; }, isEditable: function () { return (this._torrent._file_model.length>1) && !this.isDone(); }, readAttributes: function(file_data) { if( file_data.index !== undefined && file_data.index !== this._index ) { this._index = file_data.index; this._dirty = true; } if( file_data.bytesCompleted !== undefined && file_data.bytesCompleted !== this._done ) { this._done = file_data.bytesCompleted; this._dirty = true; } if( file_data.length !== undefined && file_data.length !== this._size ) { this._size = file_data.length; this._dirty = true; } if( file_data.priority !== undefined && file_data.priority !== this._prio ) { this._prio = file_data.priority; this._dirty = true; } if( file_data.wanted !== undefined && file_data.wanted !== this._wanted ) { this._wanted = file_data.wanted; this._dirty = true; } }, element: function() { return $(this._element); }, domElement: function() { return this._element; }, setPriority: function(prio) { if (this.isEditable()) { var cmd; switch( prio ) { case 1: cmd = 'priority-high'; break; case -1: cmd = 'priority-low'; break; default: cmd = 'priority-normal'; break; } this._prio = prio; this._dirty = true; this._torrent._controller.changeFileCommand( cmd, this._torrent, this ); } }, setWanted: function(wanted, process) { this._dirty = true; this._wanted = wanted; if(!iPhone) this.element().toggleClass( 'skip', !wanted ); if (process) { var command = wanted ? 'files-wanted' : 'files-unwanted'; this._torrent._controller.changeFileCommand(command, this._torrent, this); } }, toggleWanted: function() { if (this.isEditable()) this.setWanted( !this._wanted, true ); }, refreshHTML: function() { if( this._dirty ) { this._dirty = false; this.refreshProgressHTML(); this.refreshWantedHTML(); this.refreshPriorityHTML(); } }, refreshProgressHTML: function() { var c = [ Transmission.fmt.size(this._done), ' of ', Transmission.fmt.size(this._size), ' (', this._size ? Transmission.fmt.percentString(100 * this._done / this._size) : '100', '%)' ].join(''); setInnerHTML(this._progress[0], c); }, refreshWantedHTML: function() { var e = this.domElement(); var c = [ e.classNameConst ]; if(!this._wanted) { c.push( 'skip' ); } if(this.isDone()) { c.push( 'complete' ); } e.className = c.join(' '); }, refreshPriorityHTML: function() { var e = this._priority_control; var c = [ e.classNameConst ]; switch( this._prio ) { case 1 : c.push( 'high' ); break; case -1 : c.push( 'low' ); break; default : c.push( 'normal' ); break; } e.className = c.join(' '); }, fileWantedControlClicked: function(event) { this.toggleWanted(); }, filePriorityControlClicked: function(event, element) { var x = event.pageX; while (element !== null) { x = x - element.offsetLeft; element = element.offsetParent; } var prio; if(iPhone) { if( x < 8 ) prio = -1; else if( x < 27 ) prio = 0; else prio = 1; } else { if( x < 12 ) prio = -1; else if( x < 23 ) prio = 0; else prio = 1; } this.setPriority( prio ); } };