From 99dd3c2fdcd43ad9757fecc4f7c1b72038e68208 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 * peers count * additional message Third, moved the private torrent info to the details tab. closes: https://dev.deluge-torrent.org/ticket/1015 --- deluge/common.py | 7 +- deluge/ui/gtk3/details_tab.py | 3 +- .../glade/main_window.tabs.menu_trackers.ui | 27 ++ deluge/ui/gtk3/glade/main_window.tabs.ui | 224 +++-------------- deluge/ui/gtk3/mainwindow.py | 1 + deluge/ui/gtk3/trackers_tab.py | 235 ++++++++++++++++-- deluge/ui/web/js/deluge-all/Keys.js | 12 + deluge/ui/web/js/deluge-all/UI.js | 7 + .../web/js/deluge-all/data/TrackerRecord.js | 40 +++ .../web/js/deluge-all/details/DetailsPanel.js | 1 + .../web/js/deluge-all/details/TrackersTab.js | 174 +++++++++++++ 11 files changed, 509 insertions(+), 222 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/common.py b/deluge/common.py index 7b76d245c2..638522bfea 100644 --- a/deluge/common.py +++ b/deluge/common.py @@ -721,14 +721,17 @@ def parse_human_size(size): raise InvalidSize(msg % (size, tokens)) -def anchorify_urls(text: str) -> str: +def anchorify_urls(text: str, as_hyperlink: bool = True) -> str: """ Wrap all occurrences of text URLs with HTML """ url_pattern = r'((htt)|(ft)|(ud))ps?://\S+' html_href_pattern = r'\g<0>' + markup_pattern = r'\g<0>' - return re.sub(url_pattern, html_href_pattern, text) + return re.sub( + url_pattern, html_href_pattern if as_hyperlink else markup_pattern, text + ) def is_url(url): diff --git a/deluge/ui/gtk3/details_tab.py b/deluge/ui/gtk3/details_tab.py index 95b4ab8e36..468fcca95a 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 anchorify_urls, decode_bytes, fdate, fsize -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 5fad631e4f..65d00b1a63 100644 --- a/deluge/ui/gtk3/trackers_tab.py +++ b/deluge/ui/gtk3/trackers_tab.py @@ -7,11 +7,23 @@ # import logging +import webbrowser + +from gi.repository.Gdk import EventType +from gi.repository.Gtk import ( + CellRendererText, + ListStore, + SortType, + TreeView, + TreeViewColumn, +) import deluge.component as component -from deluge.common import anchorify_urls, ftime +from deluge.common import anchorify_urls, is_url +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,49 +33,218 @@ 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: TreeView = self.main_builder.get_object('trackers_listview') + self.listview.props.has_tooltip = True + self.listview.connect('button-press-event', self._on_button_press_event) + self.listview.connect('row-activated', self._on_row_activated) + self.listview.props.activate_on_single_click = True + + # 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, 'markup', 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, 'markup', 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: - if 'tracker_status' in widget.status_keys: - widget.obj.set_markup(anchorify_urls(txt)) - else: - 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 = anchorify_urls(tracker_status, as_hyperlink=False) + tracker_status_message = ftranslate(stacker_status_dict.get('message', '')) + tracker_status_message = anchorify_urls( + tracker_status_message, as_hyperlink=False + ) + 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('') + self.liststore.clear() + + def _on_button_press_event(self, widget, event): + """This is a callback for handling click events.""" + log.debug('on_button_press_event') + 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_row_activated(self, treeview, path, column): + """THis is a callback for handling link click""" + log.debug('on_row_activated') + model = treeview.get_model() + tree_iter = model.get_iter(path) + + # Get the index of the clicked column from the TreeViewColumn + clicked_column_index = self._get_column_index(column) + if clicked_column_index is None: + log.warning(f'column {column.get_title()} not selected') + return + + # Retrieve the value from the correct column based on the clicked column + cell_value = model.get_value(tree_iter, clicked_column_index) + + if '') + 1 + end_index = cell_value[start_index:].index('<') + start_index + url = cell_value[start_index:end_index] + if is_url(url): + webbrowser.open_new(url) + + def _get_column_index(self, column): + for index, col in enumerate(self.listview.get_columns()): + if col == column: + return index + return None - def on_button_edit_trackers_clicked(self, 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); + }, + }); +})();