diff --git a/deluge/ui/client.py b/deluge/ui/client.py
index 6b657d5ca5..c2cfc540db 100644
--- a/deluge/ui/client.py
+++ b/deluge/ui/client.py
@@ -741,6 +741,12 @@ def connection_info(self):
return None
+ def connection_version(self):
+ if self.connected():
+ return self._daemon_proxy.daemon_info
+
+ return ''
+
def register_event_handler(self, event, handler):
"""
Registers a handler that will be called when an event is received from the daemon.
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 @@
+
+
+
+
+
+
+
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
+
+
+
+ 4
+ 5
+
+
+
+
+
+ 3
+ 5
+
+
2
- 2
- 3
+ 1
+ 4
@@ -843,12 +869,6 @@
-
-
-
-
-
-
@@ -1446,191 +1466,11 @@
True
True
-
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..48db862df0 100644
--- a/deluge/ui/gtk3/trackers_tab.py
+++ b/deluge/ui/gtk3/trackers_tab.py
@@ -8,10 +8,15 @@
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.common import VersionSplit
+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 +26,221 @@ 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.constant_rows = {}
+
+ 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
+
+ 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):
+ if client.is_standalone():
+ self._can_get_trackers_info = True
+ else:
+ self._can_get_trackers_info = (
+ VersionSplit(client.connection_version()) > VersionSplit('2.0.5')
+ )
+ 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()
+ 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
- )
- 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',
+ ]
+
+ peers_sources = await session.get_torrent_status(torrent_id, ['peers_source'])
+ self._on_get_peers_source_status(peers_sources)
+
+ 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
+
+ if not self._can_get_trackers_info:
+ status['trackers'] = [
+ {
+ 'url': status['tracker_host'],
+ 'message': ''
+ }
+ ]
+ status['trackers_status'] = {
+ status['tracker_host']: status['tracker_status']
+ }
+ status['trackers_peers'] = {}
+
+ new_trackers = set()
+ for tracker in status['trackers']:
+ new_trackers.add(tracker['url'])
+ tracker_url = tracker['url']
+ tracker_status = ftranslate(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
- # 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)
+ 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):
+ 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
+ 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..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();
+ });
+ },
+ });
+})();