From 7dd35b3edb47efca3cbba7478cc4a9ac4a2f83fd Mon Sep 17 00:00:00 2001 From: DjLegolas Date: Sat, 26 Feb 2022 12:56:58 +0200 Subject: [PATCH] [UI] Add and improve trackers tab First, added trackers tab to the WebUI. Second, now we can view all the trackers and view each: * status * number of peers * additional message Third, information about DHT, PeX and LSD is also added. closes: https://dev.deluge-torrent.org/ticket/1015 --- .../glade/main_window.tabs.menu_trackers.ui | 27 +++ deluge/ui/gtk3/glade/main_window.tabs.ui | 30 +++ deluge/ui/gtk3/mainwindow.py | 1 + deluge/ui/gtk3/trackers_tab.py | 197 ++++++++++++++--- deluge/ui/web/js/deluge-all/Keys.js | 6 + .../web/js/deluge-all/data/TrackerRecord.js | 40 ++++ .../web/js/deluge-all/details/DetailsPanel.js | 1 + .../web/js/deluge-all/details/TrackersTab.js | 199 ++++++++++++++++++ 8 files changed, 475 insertions(+), 26 deletions(-) create mode 100644 deluge/ui/gtk3/glade/main_window.tabs.menu_trackers.ui create mode 100644 deluge/ui/web/js/deluge-all/data/TrackerRecord.js create mode 100644 deluge/ui/web/js/deluge-all/details/TrackersTab.js 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 d4984dd9d8..d9146805f6 100644 --- a/deluge/ui/gtk3/glade/main_window.tabs.ui +++ b/deluge/ui/gtk3/glade/main_window.tabs.ui @@ -1661,6 +1661,36 @@ False + + + True + True + + + True + True + + + + + + + + 6 + + + + + True + False + _Trackers + True + + + 7 + False + + diff --git a/deluge/ui/gtk3/mainwindow.py b/deluge/ui/gtk3/mainwindow.py index d11ff317aa..972fc1fbe8 100644 --- a/deluge/ui/gtk3/mainwindow.py +++ b/deluge/ui/gtk3/mainwindow.py @@ -96,6 +96,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..b14477e1ed 100644 --- a/deluge/ui/gtk3/trackers_tab.py +++ b/deluge/ui/gtk3/trackers_tab.py @@ -8,59 +8,204 @@ import logging +from gi.repository.Gtk import CellRendererText, ListStore, SortType, TreeViewColumn + import deluge.component as component -from deluge.common import ftime -from .tab_data_funcs import fcount, ftranslate, fyes_no from .torrentdetails import Tab log = logging.getLogger(__name__) -class TrackersTab(Tab): +class Trackers2Tab(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',)) + super().__init__('Trackers2', 'trackers2_tab', 'trackers2_tab_label') + 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.constant_rows = {} + + # 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 + + self._fill_constant_rows() + + def _fill_constant_rows(self): + for item in ['DHT', 'PeX', 'LSD']: + row = self.liststore.append( + [ + f'*** {item} ***', + '', + 0, + '', + ] + ) + + self.constant_rows[item.lower()] = row + def 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() + self._fill_constant_rows() 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._fill_constant_rows() + self.torrent_id = torrent_id + session = component.get('SessionProxy') - session.get_torrent_status(selected, self.status_keys).addCallback( - self._on_get_torrent_status + + tracker_keys = [ + 'trackers', + 'trackers_status', + 'trackers_peers', + ] + + session.get_torrent_status(torrent_id, tracker_keys).addCallback( + self._on_get_torrent_tracker_status + ) + session.get_torrent_status(torrent_id, ['peers_source']).addCallback( + self._on_get_peers_source_status ) - def _on_get_torrent_status(self, 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) + new_trackers = set() + for tracker in status['trackers']: + new_trackers.add(tracker['url']) + tracker_url = tracker['url'] + tracker_status = status['trackers_status'].get(tracker_url, '') + tracker_peers = status['trackers_peers'].get(tracker_url, 0) + tracker_message = tracker.get('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 _on_get_peers_source_status(self, status): + # Check to see if we got valid data from the core + if not status: + return + + for const_values in status['peers_source']: + row = self.constant_rows[const_values['name']] + old_peers_value = self.liststore.get(row, 2)[0] + status = 'Working' if const_values['enabled'] else 'Disabled' + peers_count = const_values['count'] + self.liststore.set_value(row, 1, status) + if peers_count != old_peers_value: + self.liststore.set_value(row, 2, peers_count) - def clear(self): - for widget in self.tab_widgets.values(): - widget.obj.set_text('') - def on_button_edit_trackers_clicked(self, button): + def clear(self): + self.liststore.clear() + self._fill_constant_rows() + + 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 + + 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..8c01273e9b 100644 --- a/deluge/ui/web/js/deluge-all/Keys.js +++ b/deluge/ui/web/js/deluge-all/Keys.js @@ -94,6 +94,12 @@ Deluge.Keys = { */ Peers: ['peers'], + /** + * Keys used in the trackers tab of the statistics panel. + *
['trackers']
+ */ + Trackers: ['trackers', 'trackers_status', 'trackers_peers'], + /** * Keys used in the details tab of the statistics panel. */ 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..51d0a727b4 --- /dev/null +++ b/deluge/ui/web/js/deluge-all/details/TrackersTab.js @@ -0,0 +1,199 @@ +/** + * 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: {}, + constantRows: {}, + + 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 + ); + // this.constantRows = {}; + this._fillConstantRows(); + }, + + _fillConstantRows: function () { + var constRows = []; + var tmpConstantRows = {}; + + Ext.each(['DHT', 'PeX', 'LSD'], function (constRowName) { + constRows.push( + new Deluge.data.Tracker( + { + tracker: '*** ' + constRowName + ' ***', + status: '', + peers: 0, + message: '', + }, + constRowName.toLowerCase() + ) + ); + tmpConstantRows[constRowName.toLowerCase()] = true; + }); + + this.constantRows = tmpConstantRows; + var store = this.getStore(); + store.add(constRows); + store.commitChanges(); + }, + + clear: function () { + this.getStore().removeAll(); + this._fillConstantRows(); + this.trackers = {}; + }, + + update: function (torrentId) { + deluge.client.web.get_torrent_status( + torrentId, + Deluge.Keys.Trackers, + { + success: this.onTrackersRequestComplete, + scope: this, + } + ); + deluge.client.web.get_torrent_status(torrentId, ['peers_source'], { + success: this.onPeersSourceRequestComplete, + scope: this, + }); + }, + + onTrackersRequestComplete: function (status, options) { + if (!status) return; + + var store = this.getStore(); + var newTrackers = []; + var addresses = {}; + + // Go through the trackers updating and creating tracker records + Ext.each( + status.trackers, + function (tracker) { + var url = tracker.url; + var tracker_data = { + tracker: url, + status: + url in status.trackers_status + ? status.trackers_status[url] + : '', + peers: + url in status.trackers_peers + ? status.trackers_peers[url] + : 0, + message: tracker.message ? tracker.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); + }, + + onPeersSourceRequestComplete: function (status, options) { + if (!status) return; + + var store = this.getStore(); + Ext.each(status.peers_source, function (source) { + var record = store.getById(source.name); + var source_data = { + status: source.enabled ? 'Working' : 'Disabled', + peers: source.count, + }; + record.beginEdit(); + for (var k in source_data) { + if (record.get(k) != source_data[k]) { + record.set(k, source_data[k]); + } + } + record.endEdit(); + }); + }, + }); +})();