diff options
Diffstat (limited to 'deluge/core/torrent.py')
-rw-r--r-- | deluge/core/torrent.py | 196 |
1 files changed, 129 insertions, 67 deletions
diff --git a/deluge/core/torrent.py b/deluge/core/torrent.py index a8e178f..57ec26f 100644 --- a/deluge/core/torrent.py +++ b/deluge/core/torrent.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # # Copyright (C) 2007-2009 Andrew Resch <andrewresch@gmail.com> # @@ -14,11 +13,12 @@ Attributes: """ -from __future__ import division, unicode_literals - import logging import os import socket +import time +from typing import Optional +from urllib.parse import urlparse from twisted.internet.defer import Deferred, DeferredList @@ -34,18 +34,6 @@ from deluge.event import ( TorrentTrackerStatusEvent, ) -try: - from urllib.parse import urlparse -except ImportError: - # PY2 fallback - from urlparse import urlparse # pylint: disable=ungrouped-imports - -try: - from future_builtins import zip -except ImportError: - # Ignore on Py3. - pass - log = logging.getLogger(__name__) LT_TORRENT_STATE_MAP = { @@ -94,7 +82,7 @@ def convert_lt_files(files): """Indexes and decodes files from libtorrent get_files(). Args: - files (list): The libtorrent torrent files. + files (file_storage): The libtorrent torrent files. Returns: list of dict: The files. @@ -109,18 +97,18 @@ def convert_lt_files(files): } """ filelist = [] - for index, _file in enumerate(files): + for index in range(files.num_files()): try: - file_path = _file.path.decode('utf8') + file_path = files.file_path(index).decode('utf8') except AttributeError: - file_path = _file.path + file_path = files.file_path(index) filelist.append( { 'index': index, 'path': file_path.replace('\\', '/'), - 'size': _file.size, - 'offset': _file.offset, + 'size': files.file_size(index), + 'offset': files.file_offset(index), } ) @@ -161,7 +149,7 @@ class TorrentOptions(dict): """ def __init__(self): - super(TorrentOptions, self).__init__() + super().__init__() config = ConfigManager('core.conf').config options_conf_map = { 'add_paused': 'add_paused', @@ -191,14 +179,14 @@ class TorrentOptions(dict): self['seed_mode'] = False -class TorrentError(object): +class TorrentError: def __init__(self, error_message, was_paused=False, restart_to_resume=False): self.error_message = error_message self.was_paused = was_paused self.restart_to_resume = restart_to_resume -class Torrent(object): +class Torrent: """Torrent holds information about torrents added to the libtorrent session. Args: @@ -206,12 +194,12 @@ class Torrent(object): options (dict): The torrent options. state (TorrentState): The torrent state. filename (str): The filename of the torrent file. - magnet (str): The magnet uri. + magnet (str): The magnet URI. Attributes: torrent_id (str): The torrent_id for this torrent handle: Holds the libtorrent torrent handle - magnet (str): The magnet uri used to add this torrent (if available). + magnet (str): The magnet URI used to add this torrent (if available). status: Holds status info so that we don"t need to keep getting it from libtorrent. torrent_info: store the torrent info. has_metadata (bool): True if the metadata for the torrent is available, False otherwise. @@ -248,9 +236,10 @@ class Torrent(object): self.handle = handle self.magnet = magnet - self.status = self.handle.status() + self._status: Optional['lt.torrent_status'] = None + self._status_last_update: float = 0.0 - self.torrent_info = self.handle.get_torrent_info() + self.torrent_info = self.handle.torrent_file() self.has_metadata = self.status.has_metadata self.options = TorrentOptions() @@ -266,6 +255,9 @@ class Torrent(object): self.is_finished = False self.filename = filename + if not self.filename: + self.filename = '' + self.forced_error = None self.statusmsg = None self.state = None @@ -278,7 +270,6 @@ class Torrent(object): self.prev_status = {} self.waiting_on_folder_rename = [] - self.update_status(self.handle.status()) self._create_status_funcs() self.set_options(self.options) self.update_state() @@ -286,6 +277,18 @@ class Torrent(object): if log.isEnabledFor(logging.DEBUG): log.debug('Torrent object created.') + def _set_handle_flags(self, flag: lt.torrent_flags, set_flag: bool): + """set or unset a flag to the lt handle + + Args: + flag (lt.torrent_flags): the flag to set/unset + set_flag (bool): True for setting the flag, False for unsetting it + """ + if set_flag: + self.handle.set_flags(flag) + else: + self.handle.unset_flags(flag) + def on_metadata_received(self): """Process the metadata received alert for this torrent""" self.has_metadata = True @@ -370,7 +373,7 @@ class Torrent(object): """Sets maximum download speed for this torrent. Args: - m_up_speed (float): Maximum download speed in KiB/s. + m_down_speed (float): Maximum download speed in KiB/s. """ self.options['max_download_speed'] = m_down_speed if m_down_speed < 0: @@ -402,7 +405,7 @@ class Torrent(object): return # A list of priorities for each piece in the torrent - priorities = self.handle.piece_priorities() + priorities = self.handle.get_piece_priorities() def get_file_piece(idx, byte_offset): return self.torrent_info.map_file(idx, byte_offset, 0).piece @@ -428,14 +431,17 @@ class Torrent(object): # Setting the priorites for all the pieces of this torrent self.handle.prioritize_pieces(priorities) - def set_sequential_download(self, set_sequencial): + def set_sequential_download(self, sequential): """Sets whether to download the pieces of the torrent in order. Args: - set_sequencial (bool): Enable sequencial downloading. + sequential (bool): Enable sequential downloading. """ - self.options['sequential_download'] = set_sequencial - self.handle.set_sequential_download(set_sequencial) + self.options['sequential_download'] = sequential + self._set_handle_flags( + flag=lt.torrent_flags.sequential_download, + set_flag=sequential, + ) def set_auto_managed(self, auto_managed): """Set auto managed mode, i.e. will be started or queued automatically. @@ -445,7 +451,10 @@ class Torrent(object): """ self.options['auto_managed'] = auto_managed if not (self.status.paused and not self.status.auto_managed): - self.handle.auto_managed(auto_managed) + self._set_handle_flags( + flag=lt.torrent_flags.auto_managed, + set_flag=auto_managed, + ) self.update_state() def set_super_seeding(self, super_seeding): @@ -455,7 +464,10 @@ class Torrent(object): super_seeding (bool): Enable super seeding. """ self.options['super_seeding'] = super_seeding - self.handle.super_seeding(super_seeding) + self._set_handle_flags( + flag=lt.torrent_flags.super_seeding, + set_flag=super_seeding, + ) def set_stop_ratio(self, stop_ratio): """The seeding ratio to stop (or remove) the torrent at. @@ -516,7 +528,7 @@ class Torrent(object): self.handle.prioritize_files(file_priorities) else: log.debug('Unable to set new file priorities.') - file_priorities = self.handle.file_priorities() + file_priorities = self.handle.get_file_priorities() if 0 in self.options['file_priorities']: # Previously marked a file 'skip' so check for any 0's now >0. @@ -566,7 +578,7 @@ class Torrent(object): trackers (list of dicts): A list of trackers. """ if trackers is None: - self.trackers = [tracker for tracker in self.handle.trackers()] + self.trackers = list(self.handle.trackers()) self.tracker_host = None return @@ -631,7 +643,7 @@ class Torrent(object): def update_state(self): """Updates the state, based on libtorrent's torrent state""" - status = self.handle.status() + status = self.get_lt_status() session_paused = component.get('Core').session.is_paused() old_state = self.state self.set_status_message() @@ -643,7 +655,10 @@ class Torrent(object): elif status_error: self.state = 'Error' # auto-manage status will be reverted upon resuming. - self.handle.auto_managed(False) + self._set_handle_flags( + flag=lt.torrent_flags.auto_managed, + set_flag=False, + ) self.set_status_message(decode_bytes(status_error)) elif status.moving_storage: self.state = 'Moving' @@ -696,8 +711,11 @@ class Torrent(object): restart_to_resume (bool, optional): Prevent resuming clearing the error, only restarting session can resume. """ - status = self.handle.status() - self.handle.auto_managed(False) + status = self.get_lt_status() + self._set_handle_flags( + flag=lt.torrent_flags.auto_managed, + set_flag=False, + ) self.forced_error = TorrentError(message, status.paused, restart_to_resume) if not status.paused: self.handle.pause() @@ -711,7 +729,10 @@ class Torrent(object): log.error('Restart deluge to clear this torrent error') if not self.forced_error.was_paused and self.options['auto_managed']: - self.handle.auto_managed(True) + self._set_handle_flags( + flag=lt.torrent_flags.auto_managed, + set_flag=True, + ) self.forced_error = None self.set_status_message('OK') if update_state: @@ -810,7 +831,11 @@ class Torrent(object): if peer.flags & peer.connecting or peer.flags & peer.handshake: continue - client = decode_bytes(peer.client) + try: + client = decode_bytes(peer.client) + except UnicodeDecodeError: + # libtorrent on Py3 can raise UnicodeDecodeError for peer_info.client + client = 'unknown' try: country = component.get('Core').geoip_instance.country_code_by_addr( @@ -831,7 +856,7 @@ class Torrent(object): 'client': client, 'country': country, 'down_speed': peer.payload_down_speed, - 'ip': '%s:%s' % (peer.ip[0], peer.ip[1]), + 'ip': f'{peer.ip[0]}:{peer.ip[1]}', 'progress': peer.progress, 'seed': peer.flags & peer.seed, 'up_speed': peer.payload_up_speed, @@ -850,7 +875,7 @@ class Torrent(object): def get_file_priorities(self): """Return the file priorities""" - if not self.handle.has_metadata(): + if not self.handle.status().has_metadata: return [] if not self.options['file_priorities']: @@ -867,11 +892,18 @@ class Torrent(object): """ if not self.has_metadata: return [] - return [ - progress / _file.size if _file.size else 0.0 - for progress, _file in zip( + + try: + files_progresses = zip( self.handle.file_progress(), self.torrent_info.files() ) + except Exception: + # Handle libtorrent >=2.0.0,<=2.0.4 file_progress error + files_progresses = zip(iter(lambda: 0, 1), self.torrent_info.files()) + + return [ + progress / _file.size if _file.size else 0.0 + for progress, _file in files_progresses ] def get_tracker_host(self): @@ -896,7 +928,7 @@ class Torrent(object): # Check if hostname is an IP address and just return it if that's the case try: socket.inet_aton(host) - except socket.error: + except OSError: pass else: # This is an IP address because an exception wasn't raised @@ -913,7 +945,7 @@ class Torrent(object): return '' def get_magnet_uri(self): - """Returns a magnet uri for this torrent""" + """Returns a magnet URI for this torrent""" return lt.make_magnet_uri(self.handle) def get_name(self): @@ -932,10 +964,10 @@ class Torrent(object): if self.has_metadata: # Use the top-level folder as torrent name. - filename = decode_bytes(self.torrent_info.file_at(0).path) + filename = decode_bytes(self.torrent_info.files().file_path(0)) name = filename.replace('\\', '/', 1).split('/', 1)[0] else: - name = decode_bytes(self.handle.name()) + name = decode_bytes(self.handle.status().name) if not name: name = self.torrent_id @@ -987,12 +1019,14 @@ class Torrent(object): call to get_status based on the session_id update (bool): If True the status will be updated from libtorrent if False, the cached values will be returned + all_keys (bool): If True return all keys while ignoring the keys param + if False, return only the requested keys Returns: dict: a dictionary of the status keys and their values """ if update: - self.update_status(self.handle.status()) + self.get_lt_status() if all_keys: keys = list(self.status_funcs) @@ -1022,13 +1056,35 @@ class Torrent(object): return status_dict - def update_status(self, status): + def get_lt_status(self) -> 'lt.torrent_status': + """Get the torrent status fresh, not from cache. + + This should be used when a guaranteed fresh status is needed rather than + `torrent.handle.status()` because it will update the cache as well. + """ + self.status = self.handle.status() + return self.status + + @property + def status(self) -> 'lt.torrent_status': + """Cached copy of the libtorrent status for this torrent. + + If it has not been updated within the last five seconds, it will be + automatically refreshed. + """ + if self._status_last_update < (time.time() - 5): + self.status = self.handle.status() + return self._status + + @status.setter + def status(self, status: 'lt.torrent_status') -> None: """Updates the cached status. Args: - status (libtorrent.torrent_status): a libtorrent torrent status + status: a libtorrent torrent status """ - self.status = status + self._status = status + self._status_last_update = time.time() def _create_status_funcs(self): """Creates the functions for getting torrent status""" @@ -1150,7 +1206,10 @@ class Torrent(object): """ # Turn off auto-management so the torrent will not be unpaused by lt queueing - self.handle.auto_managed(False) + self._set_handle_flags( + flag=lt.torrent_flags.auto_managed, + set_flag=False, + ) if self.state == 'Error': log.debug('Unable to pause torrent while in Error state') elif self.status.paused: @@ -1185,7 +1244,10 @@ class Torrent(object): else: # Check if torrent was originally being auto-managed. if self.options['auto_managed']: - self.handle.auto_managed(True) + self._set_handle_flags( + flag=lt.torrent_flags.auto_managed, + set_flag=True, + ) try: self.handle.resume() except RuntimeError as ex: @@ -1208,8 +1270,8 @@ class Torrent(object): bool: True is successful, otherwise False """ try: - self.handle.connect_peer((peer_ip, peer_port), 0) - except RuntimeError as ex: + self.handle.connect_peer((peer_ip, int(peer_port)), 0) + except (RuntimeError, ValueError) as ex: log.debug('Unable to connect to peer: %s', ex) return False return True @@ -1289,7 +1351,7 @@ class Torrent(object): try: with open(filepath, 'wb') as save_file: save_file.write(filedump) - except IOError as ex: + except OSError as ex: log.error('Unable to save torrent file to: %s', ex) filepath = os.path.join(get_config_dir(), 'state', self.torrent_id + '.torrent') @@ -1312,7 +1374,7 @@ class Torrent(object): torrent_files = [ os.path.join(get_config_dir(), 'state', self.torrent_id + '.torrent') ] - if delete_copies: + if delete_copies and self.filename: torrent_files.append( os.path.join(self.config['torrentfiles_location'], self.filename) ) @@ -1336,8 +1398,8 @@ class Torrent(object): def scrape_tracker(self): """Scrape the tracker - A scrape request queries the tracker for statistics such as total - number of incomplete peers, complete peers, number of downloads etc. + A scrape request queries the tracker for statistics such as total + number of incomplete peers, complete peers, number of downloads etc. """ try: self.handle.scrape_tracker() @@ -1384,7 +1446,7 @@ class Torrent(object): This basically does a file rename on all of the folders children. Args: - folder (str): The orignal folder name + folder (str): The original folder name new_folder (str): The new folder name Returns: |