Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[1015] improve trackers tab #379

Open
wants to merge 3 commits into
base: develop
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
91 changes: 83 additions & 8 deletions deluge/core/torrent.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
import os
import socket
import time
from typing import Optional
from typing import List, Optional
from urllib.parse import urlparse

from twisted.internet.defer import Deferred, DeferredList
Expand Down Expand Up @@ -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
Expand All @@ -263,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
Expand Down Expand Up @@ -578,7 +582,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

Expand All @@ -599,17 +603,60 @@ 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 set_tracker_status(self, status):
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: 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.
Expand All @@ -618,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(
Expand Down Expand Up @@ -666,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))

Expand Down Expand Up @@ -1158,7 +1230,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
Expand Down
20 changes: 13 additions & 7 deletions deluge/core/torrentmanager.py
Original file line number Diff line number Diff line change
Expand Up @@ -1347,7 +1347,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()
Expand All @@ -1362,7 +1364,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"""
Expand All @@ -1371,7 +1373,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"""
Expand All @@ -1384,19 +1390,19 @@ 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
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
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):
Expand Down
28 changes: 28 additions & 0 deletions deluge/tests/test_torrent.py
Original file line number Diff line number Diff line change
Expand Up @@ -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': ''}]
3 changes: 2 additions & 1 deletion deluge/ui/gtk3/details_tab.py
Original file line number Diff line number Diff line change
Expand Up @@ -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__)
Expand All @@ -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
Expand Down
27 changes: 27 additions & 0 deletions deluge/ui/gtk3/glade/main_window.tabs.menu_trackers.ui
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
<?xml version="1.0" encoding="UTF-8"?>
<!-- Generated with glade 3.22.1 -->
<interface>
<requires lib="gtk+" version="3.0"/>
<object class="GtkImage" id="image1">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="icon_name">list-add-symbolic</property>
<property name="icon_size">1</property>
</object>
<object class="GtkMenu" id="menu_trackers_tab">
<property name="visible">True</property>
<property name="can_focus">False</property>
<child>
<object class="GtkImageMenuItem" id="edit_trackers_menuitem">
<property name="label" translatable="yes">_Edit Trackers</property>
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="tooltip_text" translatable="yes">Edit all trackers</property>
<property name="use_underline">True</property>
<property name="image">image1</property>
<property name="use_stock">False</property>
<signal name="activate" handler="on_menuitem_edit_trackers_activate" swapped="no"/>
</object>
</child>
</object>
</interface>