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 @@
+
+
+
+
+
+
+
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
+
+
+
+ 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();
+ });
+ },
+ });
+})();