From cef257d2960b5e4418a03a6d676748857249e86b Mon Sep 17 00:00:00 2001 From: DjLegolas Date: Sat, 30 Apr 2022 13:15:45 +0300 Subject: [PATCH 1/3] [Torrent] Add trackers cache The current way the trackers are being update is either by adding a new torrent to the session (on startup or new one), or by changing the trackers. This leads to data not being updated in the `torrent.trackers` dict. To solve this, a cache mechanism was added which will update the dict on changes or every 5 seconds. This will help us also get an update regarding `lt.announce_endpoint` in each of the trackers. --- deluge/core/torrent.py | 49 ++++++++++++++++++++++++++++++++--- deluge/core/torrentmanager.py | 2 +- deluge/tests/test_torrent.py | 28 ++++++++++++++++++++ 3 files changed, 75 insertions(+), 4 deletions(-) diff --git a/deluge/core/torrent.py b/deluge/core/torrent.py index dbcf8f101c..a7b5f73590 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 Optional, List 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 @@ -578,7 +580,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,12 +601,53 @@ 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 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): """Sets the tracker status. diff --git a/deluge/core/torrentmanager.py b/deluge/core/torrentmanager.py index 1233553a8f..144b53b170 100644 --- a/deluge/core/torrentmanager.py +++ b/deluge/core/torrentmanager.py @@ -1389,7 +1389,7 @@ def on_alert_tracker_error(self, alert): ) # 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 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': ''}] From 0ca8f4dda6404c481525e95c2ca7857e7dd84d81 Mon Sep 17 00:00:00 2001 From: DjLegolas Date: Sat, 26 Feb 2022 12:54:59 +0200 Subject: [PATCH 2/3] [Tracker] Add a status for all trackers in a torrent As part of the changes needed for a more informative trackers tab in the UIs, we need the info for each of the trackers in the torrent. For this, new status keys were added, and `tracker_status` is now considered deprecated. For tracker info, the keys are: `trackers_status` - a dict of tracker_url->{status, message} `trackers_peers` - a dict of tracker_url->number of peers in the tracker `trackers_status` will contain the status of the torrent and also the message from the relevant alert. --- deluge/core/torrent.py | 44 ++++++++++++++++++++++++++++++----- deluge/core/torrentmanager.py | 18 +++++++++----- 2 files changed, 50 insertions(+), 12 deletions(-) diff --git a/deluge/core/torrent.py b/deluge/core/torrent.py index a7b5f73590..727d513048 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, List +from typing import List, Optional from urllib.parse import urlparse from twisted.internet.defer import Deferred, DeferredList @@ -265,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 @@ -648,11 +650,13 @@ def trackers(self, trackers: List['lt.announce_entry']) -> None: self._trackers = trackers self._trackers_last_update = time.time() - def set_tracker_status(self, status): + 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. @@ -661,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( @@ -709,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)) @@ -1200,7 +1229,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 144b53b170..0da4db1e76 100644 --- a/deluge/core/torrentmanager.py +++ b/deluge/core/torrentmanager.py @@ -1348,7 +1348,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() @@ -1363,7 +1365,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""" @@ -1372,7 +1374,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""" @@ -1385,7 +1391,7 @@ 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 @@ -1395,9 +1401,9 @@ def on_alert_tracker_error(self, alert): 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): From 99dd3c2fdcd43ad9757fecc4f7c1b72038e68208 Mon Sep 17 00:00:00 2001 From: DjLegolas Date: Sat, 26 Feb 2022 12:56:58 +0200 Subject: [PATCH 3/3] [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); + }, + }); +})();