diff --git a/deluge/core/torrent.py b/deluge/core/torrent.py index 57ec26f37a..96e3231746 100644 --- a/deluge/core/torrent.py +++ b/deluge/core/torrent.py @@ -17,7 +17,7 @@ import os import socket import time -from typing import Optional +from typing import List, Optional from urllib.parse import urlparse from twisted.internet.defer import Deferred, DeferredList @@ -238,6 +238,8 @@ def __init__(self, handle, options, state=None, filename=None, magnet=None): self.magnet = magnet self._status: Optional['lt.torrent_status'] = None self._status_last_update: float = 0.0 + self._trackers: Optional[List['lt.announce_entry']] = None + self._trackers_last_update: float = 0.0 self.torrent_info = self.handle.torrent_file() self.has_metadata = self.status.has_metadata @@ -263,6 +265,8 @@ def __init__(self, handle, options, state=None, filename=None, magnet=None): self.state = None self.moving_storage_dest_path = None self.tracker_status = '' + self.trackers_status = {} + self.trackers_peers = {} self.tracker_host = None self.forcing_recheck = False self.forcing_recheck_paused = False @@ -578,7 +582,7 @@ def set_trackers(self, trackers=None): trackers (list of dicts): A list of trackers. """ if trackers is None: - self.trackers = list(self.handle.trackers()) + self.trackers = self.handle.trackers() self.tracker_host = None return @@ -599,17 +603,60 @@ def set_trackers(self, trackers=None): for tracker in self.handle.trackers(): log.debug(' [tier %s]: %s', tracker['tier'], tracker['url']) # Set the tracker list in the torrent object - self.trackers = trackers + self.trackers = self.handle.trackers() if len(trackers) > 0: # Force a re-announce if there is at least 1 tracker self.force_reannounce() self.tracker_host = None - def set_tracker_status(self, status): + def get_lt_trackers(self): + """Get the torrent trackers fresh, not from cache. + + This should be used when a guaranteed fresh trackers is needed rather than + `torrent.handle.tracker()` because it will update the cache as well. + """ + trackers = self.handle.trackers() + self.trackers = trackers + return trackers + + @property + def trackers(self) -> List[dict]: + """Cached copy of the libtorrent Trackers for this torrent. + + If it has not been updated within the last five seconds, it will be + automatically refreshed. + """ + if self._trackers_last_update < (time.time() - 5): + self.trackers = self.handle.trackers() + trackers_list = [] + for tracker in self._trackers: + torrent_tracker = { + 'url': '', + 'message': '', + 'tier': 0, + } + for data_key in torrent_tracker: + torrent_tracker[data_key] = tracker.get(data_key) + trackers_list.append(torrent_tracker) + return trackers_list + + @trackers.setter + def trackers(self, trackers: List['lt.announce_entry']) -> None: + """Updates the cached status. + + Args: + trackers: a libtorrent torrent trackers + """ + self._trackers = trackers + self._trackers_last_update = time.time() + + def set_tracker_status(self, status: str, tracker_url: str, message: str = ''): """Sets the tracker status. Args: - status (str): The tracker status. + status: The tracker status. + tracker_url: The tracker url. + message: The message from tracker error/warning alerts Emits: TorrentTrackerStatusEvent upon tracker status change. @@ -618,12 +665,35 @@ def set_tracker_status(self, status): self.tracker_host = None - if self.tracker_status != status: - self.tracker_status = status + if self.state == 'Paused': + return + + if self.trackers_status.get(tracker_url, {}).get('status') != status: + self.trackers_status[tracker_url] = { + 'status': status, + 'message': message, + } + self.tracker_status = f'{status}{f": {message}" if message else ""}' component.get('EventManager').emit( - TorrentTrackerStatusEvent(self.torrent_id, self.tracker_status) + TorrentTrackerStatusEvent( + self.torrent_id, + self.trackers_status[tracker_url], + ) ) + def set_tracker_peers(self, peers: int, tracker_url: str): + """Sets the tracker peers amount + + Args: + peers: The number of peers the tracker has. + tracker_url: The tracker url. + """ + if self.state == 'Paused': + return + + if self.trackers_peers.get(tracker_url) != peers: + self.trackers_peers[tracker_url] = peers + def merge_trackers(self, torrent_info): """Merges new trackers in torrent_info into torrent""" log.info( @@ -666,6 +736,8 @@ def update_state(self): self.state = 'Queued' elif session_paused or status.paused: self.state = 'Paused' + self.trackers_peers = {} + self.trackers_status = {} else: self.state = LT_TORRENT_STATE_MAP.get(str(status.state), str(status.state)) @@ -1158,7 +1230,10 @@ def _create_status_funcs(self): 'tracker': lambda: self.status.current_tracker, 'tracker_host': self.get_tracker_host, 'trackers': lambda: self.trackers, + # Deprecated: Use trackers_status 'tracker_status': lambda: self.tracker_status, + 'trackers_status': lambda: self.trackers_status, + 'trackers_peers': lambda: self.trackers_peers, 'upload_payload_rate': lambda: self.status.upload_payload_rate, 'comment': lambda: decode_bytes(self.torrent_info.comment()) if self.has_metadata diff --git a/deluge/core/torrentmanager.py b/deluge/core/torrentmanager.py index c43a7a262d..ee2119b42c 100644 --- a/deluge/core/torrentmanager.py +++ b/deluge/core/torrentmanager.py @@ -1347,7 +1347,9 @@ def on_alert_tracker_reply(self, alert): return # Set the tracker status for the torrent - torrent.set_tracker_status('Announce OK') + torrent.set_tracker_status('Announce OK', alert.url) + # Set the amount of peers of the tracker + torrent.set_tracker_peers(alert.num_peers, alert.url) # Check for peer information from the tracker, if none then send a scrape request. torrent.get_lt_status() @@ -1362,7 +1364,7 @@ def on_alert_tracker_announce(self, alert): return # Set the tracker status for the torrent - torrent.set_tracker_status('Announce Sent') + torrent.set_tracker_status('Announce Sent', alert.url) def on_alert_tracker_warning(self, alert): """Alert handler for libtorrent tracker_warning_alert""" @@ -1371,7 +1373,11 @@ def on_alert_tracker_warning(self, alert): except (RuntimeError, KeyError): return # Set the tracker status for the torrent - torrent.set_tracker_status('Warning: %s' % decode_bytes(alert.message())) + torrent.set_tracker_status( + 'Warning', + alert.url, + decode_bytes(alert.warning_message()), + ) def on_alert_tracker_error(self, alert): """Alert handler for libtorrent tracker_error_alert""" @@ -1384,19 +1390,19 @@ def on_alert_tracker_error(self, alert): if not error_message: error_message = decode_bytes(alert.error.message()) log.debug( - 'Tracker Error Alert: %s [%s]', decode_bytes(alert.message()), error_message + f'Tracker Error Alert: {decode_bytes(alert.message())} [{error_message}]' ) # libtorrent 1.2 added endpoint struct to each tracker. to prevent false updates # we will need to verify that at least one endpoint to the errored tracker is working - for tracker in torrent.handle.trackers(): + for tracker in torrent.get_lt_trackers(): if tracker['url'] == alert.url: if any( endpoint['last_error']['value'] == 0 for endpoint in tracker['endpoints'] ): - torrent.set_tracker_status('Announce OK') + torrent.set_tracker_status('Announce OK', alert.url) else: - torrent.set_tracker_status('Error: ' + error_message) + torrent.set_tracker_status('Error', alert.url, error_message) break def on_alert_storage_moved(self, alert): diff --git a/deluge/tests/test_torrent.py b/deluge/tests/test_torrent.py index 62886159ea..39620fe563 100644 --- a/deluge/tests/test_torrent.py +++ b/deluge/tests/test_torrent.py @@ -386,3 +386,31 @@ def test_status_cache(self): # Advance time and verify cache expires and updates mock_time.return_value += 10 assert torrent.status == 2 + + def test_trackers_cache(self): + atp = self.get_torrent_atp('test_torrent.file.torrent') + handle = self.session.add_torrent(atp) + mock_time = mock.Mock(return_value=time.time()) + with mock.patch('time.time', mock_time): + torrent = Torrent(handle, {}) + counter = itertools.count() + handle.trackers = mock.Mock( + side_effect=lambda: [ + {'tier': counter.__next__(), 'url': '', 'message': ''} + ] + ) + first_trackers = torrent.get_lt_trackers() + assert first_trackers == [ + {'tier': 0, 'url': '', 'message': ''} + ], 'sanity check' + assert first_trackers == torrent.trackers, 'cached trackers should be used' + torrent.set_trackers() + assert torrent.trackers == [ + {'tier': 1, 'url': '', 'message': ''} + ], 'trackers should update' + assert torrent.get_lt_trackers() == [ + {'tier': 2, 'url': '', 'message': ''} + ], 'trackers should update a second time' + # Advance time and verify cache expires and updates + mock_time.return_value += 10 + assert torrent.trackers == [{'tier': 3, 'url': '', 'message': ''}] diff --git a/deluge/ui/gtk3/details_tab.py b/deluge/ui/gtk3/details_tab.py index 04a5eabfe0..a029dc9948 100644 --- a/deluge/ui/gtk3/details_tab.py +++ b/deluge/ui/gtk3/details_tab.py @@ -12,7 +12,7 @@ import deluge.component as component from deluge.common import decode_bytes, fdate, fsize, is_url -from .tab_data_funcs import fdate_or_dash, fpieces_num_size +from .tab_data_funcs import fdate_or_dash, fpieces_num_size, fyes_no from .torrentdetails import Tab log = logging.getLogger(__name__) @@ -34,6 +34,7 @@ def __init__(self): self.add_tab_widget( 'summary_pieces', fpieces_num_size, ('num_pieces', 'piece_length') ) + self.add_tab_widget('summary_private', fyes_no, ('private',)) def update(self): # Get the first selected torrent diff --git a/deluge/ui/gtk3/glade/main_window.tabs.menu_trackers.ui b/deluge/ui/gtk3/glade/main_window.tabs.menu_trackers.ui new file mode 100644 index 0000000000..0c6d7ae08b --- /dev/null +++ b/deluge/ui/gtk3/glade/main_window.tabs.menu_trackers.ui @@ -0,0 +1,27 @@ + + + + + + True + False + list-add-symbolic + 1 + + + True + False + + + _Edit Trackers + True + False + Edit all trackers + True + image1 + False + + + + + diff --git a/deluge/ui/gtk3/glade/main_window.tabs.ui b/deluge/ui/gtk3/glade/main_window.tabs.ui index 7ecf618210..76cd772bde 100644 --- a/deluge/ui/gtk3/glade/main_window.tabs.ui +++ b/deluge/ui/gtk3/glade/main_window.tabs.ui @@ -583,6 +583,17 @@ 2 + + + True + False + start + + + 4 + 5 + + True @@ -801,6 +812,21 @@ 3 + + + True + False + start + Private Torrent: + + + + + + 3 + 5 + + True @@ -809,8 +835,8 @@ 2 - 2 - 3 + 1 + 4 @@ -843,12 +869,6 @@ - - - - - - @@ -1446,191 +1466,11 @@ True True - + True - False - none - - - True - False - 5 - 2 - 10 - 15 - - - True - False - 5 - 10 - - - True - False - start - Current Tracker: - - - - - - 0 - 1 - - - - - True - False - True - - - 1 - 1 - - - - - True - False - True - - - 1 - 3 - - - - - True - False - True - char - True - - - 1 - 2 - - - - - True - False - True - - - 1 - 0 - - - - - True - False - char - True - - - 1 - 4 - - - - - True - False - start - Total Trackers: - - - - - - 0 - 0 - - - - - True - False - start - Tracker Status: - - - - - - 0 - 2 - - - - - True - False - start - Next Announce: - - - - - - 0 - 3 - - - - - True - False - start - Private Torrent: - - - - - - 0 - 4 - - - - - True - False - start - 5 - - - True - True - True - - - - True - False - _Edit Trackers - True - - - - - - - 0 - 5 - - - - - - - - + True + + diff --git a/deluge/ui/gtk3/mainwindow.py b/deluge/ui/gtk3/mainwindow.py index 6c871d2d84..59e2bd603e 100644 --- a/deluge/ui/gtk3/mainwindow.py +++ b/deluge/ui/gtk3/mainwindow.py @@ -101,6 +101,7 @@ def patched_connect_signals(*a, **k): 'main_window.tabs.ui', 'main_window.tabs.menu_file.ui', 'main_window.tabs.menu_peer.ui', + 'main_window.tabs.menu_trackers.ui', ] for filename in ui_filenames: self.main_builder.add_from_file( diff --git a/deluge/ui/gtk3/trackers_tab.py b/deluge/ui/gtk3/trackers_tab.py index d671471b02..b733bd227e 100644 --- a/deluge/ui/gtk3/trackers_tab.py +++ b/deluge/ui/gtk3/trackers_tab.py @@ -8,10 +8,14 @@ import logging +from gi.repository.Gdk import EventType +from gi.repository.Gtk import CellRendererText, ListStore, SortType, TreeViewColumn + import deluge.component as component -from deluge.common import ftime +from deluge.decorators import maybe_coroutine +from deluge.ui.client import client -from .tab_data_funcs import fcount, ftranslate, fyes_no +from .tab_data_funcs import ftranslate from .torrentdetails import Tab log = logging.getLogger(__name__) @@ -21,46 +25,185 @@ class TrackersTab(Tab): def __init__(self): super().__init__('Trackers', 'trackers_tab', 'trackers_tab_label') - self.add_tab_widget('summary_next_announce', ftime, ('next_announce',)) - self.add_tab_widget('summary_tracker', None, ('tracker_host',)) - self.add_tab_widget('summary_tracker_status', ftranslate, ('tracker_status',)) - self.add_tab_widget('summary_tracker_total', fcount, ('trackers',)) - self.add_tab_widget('summary_private', fyes_no, ('private',)) - + self.trackers_menu = self.main_builder.get_object('menu_trackers_tab') component.get('MainWindow').connect_signals(self) + self.listview = self.main_builder.get_object('trackers_listview') + self.listview.props.has_tooltip = True + self.listview.connect('button-press-event', self._on_button_press_event) + + # url, status, peers, message + self.liststore = ListStore(str, str, int, str) + + # key is url, item is row iter + self.trackers = {} + + self._can_get_trackers_info = False + + # self.treeview.append_column( + # Gtk.TreeViewColumn(_('Tier'), Gtk.CellRendererText(), text=0) + # ) + column = TreeViewColumn(_('Tracker')) + render = CellRendererText() + column.pack_start(render, False) + column.add_attribute(render, 'text', 0) + column.set_clickable(True) + column.set_resizable(True) + column.set_expand(False) + column.set_min_width(150) + column.set_reorderable(True) + self.listview.append_column(column) + + column = TreeViewColumn(_('Status')) + render = CellRendererText() + column.pack_start(render, False) + column.add_attribute(render, 'text', 1) + column.set_clickable(True) + column.set_resizable(True) + column.set_expand(False) + column.set_min_width(50) + column.set_reorderable(True) + self.listview.append_column(column) + + column = TreeViewColumn(_('Peers')) + render = CellRendererText() + column.pack_start(render, False) + column.add_attribute(render, 'text', 2) + column.set_clickable(True) + column.set_resizable(True) + column.set_expand(False) + column.set_min_width(50) + column.set_reorderable(True) + self.listview.append_column(column) + + column = TreeViewColumn(_('Message')) + render = CellRendererText() + column.pack_start(render, False) + column.add_attribute(render, 'text', 3) + column.set_clickable(True) + column.set_resizable(True) + column.set_expand(False) + column.set_min_width(100) + column.set_reorderable(True) + self.listview.append_column(column) + + self.listview.set_model(self.liststore) + self.liststore.set_sort_column_id(0, SortType.ASCENDING) + + self.torrent_id = None + def update(self): + if client.is_standalone(): + self._can_get_trackers_info = True + else: + self._can_get_trackers_info = client.daemon_version_check_min('2.1.2') + self.do_update() + + @maybe_coroutine + async def do_update(self): # Get the first selected torrent - selected = component.get('TorrentView').get_selected_torrents() + torrent_id = component.get('TorrentView').get_selected_torrents() # Only use the first torrent in the list or return if None selected - if selected: - selected = selected[0] + if torrent_id: + torrent_id = torrent_id[0] else: - self.clear() + self.liststore.clear() return + if torrent_id != self.torrent_id: + # We only want to do this if the torrent_id has changed + self.liststore.clear() + self.trackers = {} + self.torrent_id = torrent_id + session = component.get('SessionProxy') - session.get_torrent_status(selected, self.status_keys).addCallback( - self._on_get_torrent_status - ) - def _on_get_torrent_status(self, status): + if not self._can_get_trackers_info: + tracker_keys = [ + 'tracker_host', + 'tracker_status', + ] + else: + tracker_keys = [ + 'trackers', + 'trackers_status', + 'trackers_peers', + ] + + status = await session.get_torrent_status(torrent_id, tracker_keys) + self._on_get_torrent_tracker_status(status) + + def _on_get_torrent_tracker_status(self, status): # Check to see if we got valid data from the core if not status: return - # Update all the tab label widgets - for widget in self.tab_widgets.values(): - txt = self.widget_status_as_fstr(widget, status) - if widget.obj.get_text() != txt: - widget.obj.set_text(txt) + if not self._can_get_trackers_info: + status['trackers'] = [{'url': status['tracker_host'], 'message': ''}] + status['trackers_status'] = { + status['tracker_host']: { + 'status': status['tracker_status'], + 'message': '', + } + } + status['trackers_peers'] = {} + + new_trackers = set() + for tracker in status['trackers']: + new_trackers.add(tracker['url']) + tracker_url = tracker['url'] + stacker_status_dict = status['trackers_status'].get(tracker_url, {}) + tracker_status = ftranslate(stacker_status_dict.get('status', '')) + tracker_status_message = ftranslate(stacker_status_dict.get('message', '')) + tracker_peers = status['trackers_peers'].get(tracker_url, 0) + tracker_message = tracker.get('message', '') + if not tracker_message and tracker_status_message: + tracker_message = tracker_status_message + if tracker['url'] in self.trackers: + row = self.trackers[tracker['url']] + if not self.liststore.iter_is_valid(row): + # This iter is invalid, delete it and continue to next iteration + del self.trackers[tracker['url']] + continue + values = self.liststore.get(row, 1, 2, 3) + if tracker_status != values[0]: + self.liststore.set_value(row, 1, tracker_status) + if tracker_peers != values[1]: + self.liststore.set_value(row, 2, tracker_peers) + if tracker_message != values[2]: + self.liststore.set_value(row, 3, tracker_message) + else: + row = self.liststore.append( + [ + tracker_url, + tracker_status, + tracker_peers, + tracker_message, + ] + ) + + self.trackers[tracker['url']] = row + + # Now we need to remove any tracker that were not in status['trackers'] list + for tracker in set(self.trackers).difference(new_trackers): + self.liststore.remove(self.trackers[tracker]) + del self.trackers[tracker] def clear(self): - for widget in self.tab_widgets.values(): - widget.obj.set_text('') - - def on_button_edit_trackers_clicked(self, button): + self.liststore.clear() + + def _on_button_press_event(self, widget, event): + """This is a callback for showing the right-click context menu.""" + log.debug('on_button_press_event') + # We only care about right-clicks + if event.button == 3: + self.trackers_menu.popup(None, None, None, None, event.button, event.time) + return True + elif event.type == EventType.DOUBLE_BUTTON_PRESS: + self.on_menuitem_edit_trackers_activate(event.button) + + def on_menuitem_edit_trackers_activate(self, button): torrent_id = component.get('TorrentView').get_selected_torrent() if torrent_id: from .edittrackersdialog import EditTrackersDialog diff --git a/deluge/ui/web/js/deluge-all/Keys.js b/deluge/ui/web/js/deluge-all/Keys.js index 7b3e3affca..775edd92da 100644 --- a/deluge/ui/web/js/deluge-all/Keys.js +++ b/deluge/ui/web/js/deluge-all/Keys.js @@ -94,6 +94,18 @@ Deluge.Keys = { */ Peers: ['peers'], + /** + * Keys used in the trackers tab of the statistics panel. + *
['trackers', 'trackers_status', 'trackers_peers']
+ */ + Trackers: ['trackers', 'trackers_status', 'trackers_peers'], + + /** + * Keys used in the trackers tab of the statistics panel for Deluge version <2.1.1. + *
['tracker_host', 'tracker_status']
+ */ + TrackersRedundant: ['tracker_host', 'tracker_status'], + /** * Keys used in the details tab of the statistics panel. */ diff --git a/deluge/ui/web/js/deluge-all/UI.js b/deluge/ui/web/js/deluge-all/UI.js index f7edc84b19..1b3c10a527 100644 --- a/deluge/ui/web/js/deluge-all/UI.js +++ b/deluge/ui/web/js/deluge-all/UI.js @@ -52,6 +52,7 @@ deluge.ui = { deluge.sidebar = new Deluge.Sidebar(); deluge.statusbar = new Deluge.Statusbar(); deluge.toolbar = new Deluge.Toolbar(); + deluge.server_version = ''; this.detailsPanel = new Ext.Panel({ id: 'detailsPanel', @@ -223,6 +224,11 @@ deluge.ui = { this.running = setTimeout(this.update, 2000); this.update(); } + deluge.client.daemon.get_version({ + success: function (server_version) { + deluge.server_version = server_version; + }, + }); deluge.client.web.get_plugins({ success: this.onGotPlugins, scope: this, @@ -234,6 +240,7 @@ deluge.ui = { * @private */ onDisconnect: function () { + deluge.server_version = ''; this.stop(); }, diff --git a/deluge/ui/web/js/deluge-all/data/TrackerRecord.js b/deluge/ui/web/js/deluge-all/data/TrackerRecord.js new file mode 100644 index 0000000000..f8d65b97d5 --- /dev/null +++ b/deluge/ui/web/js/deluge-all/data/TrackerRecord.js @@ -0,0 +1,40 @@ +/** + * Deluge.data.TrackerRecord.js + * + * Copyright (c) Damien Churchill 2009-2010 + * + * This file is part of Deluge and is licensed under GNU General Public License 3.0, or later, with + * the additional special exception to link portions of this program with the OpenSSL library. + * See LICENSE for more details. + */ +Ext.namespace('Deluge.data'); + +/** + * Deluge.data.Tracker record + * + * @author Damien Churchill + * @version 1.3 + * + * @class Deluge.data.Tracker + * @extends Ext.data.Record + * @constructor + * @param {Object} data The tracker data + */ +Deluge.data.Tracker = Ext.data.Record.create([ + { + name: 'tracker', + type: 'string', + }, + { + name: 'status', + type: 'string', + }, + { + name: 'peers', + type: 'int', + }, + { + name: 'message', + type: 'string', + }, +]); diff --git a/deluge/ui/web/js/deluge-all/details/DetailsPanel.js b/deluge/ui/web/js/deluge-all/details/DetailsPanel.js index 3f28b2576c..9a32e32fcc 100644 --- a/deluge/ui/web/js/deluge-all/details/DetailsPanel.js +++ b/deluge/ui/web/js/deluge-all/details/DetailsPanel.js @@ -21,6 +21,7 @@ Deluge.details.DetailsPanel = Ext.extend(Ext.TabPanel, { this.add(new Deluge.details.StatusTab()); this.add(new Deluge.details.DetailsTab()); this.add(new Deluge.details.FilesTab()); + this.add(new Deluge.details.TrackersTab()); this.add(new Deluge.details.PeersTab()); this.add(new Deluge.details.OptionsTab()); }, diff --git a/deluge/ui/web/js/deluge-all/details/TrackersTab.js b/deluge/ui/web/js/deluge-all/details/TrackersTab.js new file mode 100644 index 0000000000..0f137574d8 --- /dev/null +++ b/deluge/ui/web/js/deluge-all/details/TrackersTab.js @@ -0,0 +1,174 @@ +/** + * Deluge.details.TrackersTab.js + * + * Copyright (c) Damien Churchill 2009-2010 + * + * This file is part of Deluge and is licensed under GNU General Public License 3.0, or later, with + * the additional special exception to link portions of this program with the OpenSSL library. + * See LICENSE for more details. + */ + +(function () { + Deluge.details.TrackersTab = Ext.extend(Ext.grid.GridPanel, { + // fast way to figure out if we have a tracker already. + trackers: {}, + can_get_trackers_info: false, + + constructor: function (config) { + config = Ext.apply( + { + title: _('Trackers'), + cls: 'x-deluge-trackers', + store: new Ext.data.Store({ + reader: new Ext.data.JsonReader( + { + idProperty: 'ip', + root: 'peers', + }, + Deluge.data.Tracker + ), + }), + columns: [ + { + header: _('Tracker'), + width: 300, + sortable: true, + renderer: 'htmlEncode', + dataIndex: 'tracker', + }, + { + header: _('Status'), + width: 150, + sortable: true, + renderer: 'htmlEncode', + dataIndex: 'status', + }, + { + header: _('Peers'), + width: 100, + sortable: true, + renderer: 'htmlEncode', + dataIndex: 'peers', + }, + { + header: _('Message'), + width: 100, + renderer: 'htmlEncode', + dataIndex: 'message', + }, + ], + stripeRows: true, + deferredRender: false, + autoScroll: true, + }, + config + ); + Deluge.details.TrackersTab.superclass.constructor.call( + this, + config + ); + }, + + clear: function () { + this.getStore().removeAll(); + this.trackers = {}; + }, + + update: function (torrentId) { + this.can_get_trackers_info = deluge.server_version > '2.0.5'; + + var trackers_keys = this.can_get_trackers_info + ? Deluge.Keys.Trackers + : Deluge.Keys.TrackersRedundant; + + deluge.client.web.get_torrent_status(torrentId, trackers_keys, { + success: this.onTrackersRequestComplete, + scope: this, + }); + }, + + onTrackersRequestComplete: function (status, options) { + if (!status) return; + + var store = this.getStore(); + var newTrackers = []; + var addresses = {}; + + if (!this.can_get_trackers_info) { + status['trackers'] = [ + { + url: status['tracker_host'], + message: '', + }, + ]; + var tracker_host = status['tracker_host']; + status['trackers_status'] = { + tracker_host: { + status: status['tracker_status'], + message: '', + }, + }; + status['trackers_peers'] = {}; + } + + // Go through the trackers updating and creating tracker records + Ext.each( + status.trackers, + function (tracker) { + var url = tracker.url; + var tracker_status = + url in status.trackers_status + ? status.trackers_status[url] + : {}; + var message = tracker.message ? tracker.message : ''; + if (!message && 'message' in tracker_status) { + message = tracker_status['message']; + } + var tracker_data = { + tracker: url, + status: + 'status' in tracker_status + ? tracker_status['status'] + : '', + peers: + url in status.trackers_peers + ? status.trackers_peers[url] + : 0, + message: message, + }; + if (this.trackers[tracker.url]) { + var record = store.getById(tracker.url); + record.beginEdit(); + for (var k in tracker_data) { + if (record.get(k) != tracker_data[k]) { + record.set(k, tracker_data[k]); + } + } + record.endEdit(); + } else { + this.trackers[tracker.url] = 1; + newTrackers.push( + new Deluge.data.Tracker(tracker_data, tracker.url) + ); + } + addresses[tracker.url] = 1; + }, + this + ); + store.add(newTrackers); + + // Remove any trackers that should not be left in the store. + store.each(function (record) { + if (!addresses[record.id] && !this.constantRows[record.id]) { + store.remove(record); + delete this.trackers[record.id]; + } + }, this); + store.commitChanges(); + + var sortState = store.getSortState(); + if (!sortState) return; + store.sort(sortState.field, sortState.direction); + }, + }); +})();