diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2023-02-19 15:05:52 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2023-02-19 16:15:47 +0000 |
commit | b686174b07bd56af4e5ffaa23c24f27f417fc305 (patch) | |
tree | 1ce335620d99341d94e88c159c0b9b0f6f0de5a0 /deluge/core | |
parent | Adding debian version 2.0.3-4. (diff) | |
download | deluge-b686174b07bd56af4e5ffaa23c24f27f417fc305.tar.xz deluge-b686174b07bd56af4e5ffaa23c24f27f417fc305.zip |
Merging upstream version 2.1.1 (Closes: #1026291).
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'deluge/core')
-rw-r--r-- | deluge/core/alertmanager.py | 14 | ||||
-rw-r--r-- | deluge/core/authmanager.py | 28 | ||||
-rw-r--r-- | deluge/core/core.py | 476 | ||||
-rw-r--r-- | deluge/core/daemon.py | 18 | ||||
-rw-r--r-- | deluge/core/daemon_entry.py | 3 | ||||
-rw-r--r-- | deluge/core/eventmanager.py | 3 | ||||
-rw-r--r-- | deluge/core/filtermanager.py | 11 | ||||
-rw-r--r-- | deluge/core/pluginmanager.py | 3 | ||||
-rw-r--r-- | deluge/core/preferencesmanager.py | 36 | ||||
-rw-r--r-- | deluge/core/rpcserver.py | 80 | ||||
-rw-r--r-- | deluge/core/torrent.py | 196 | ||||
-rw-r--r-- | deluge/core/torrentmanager.py | 175 |
12 files changed, 506 insertions, 537 deletions
diff --git a/deluge/core/alertmanager.py b/deluge/core/alertmanager.py index 2fe4222..9a1ded5 100644 --- a/deluge/core/alertmanager.py +++ b/deluge/core/alertmanager.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # # Copyright (C) 2007-2009 Andrew Resch <andrewresch@gmail.com> # @@ -15,10 +14,8 @@ This should typically only be used by the Core. Plugins should utilize the `:mod:EventManager` for similar functionality. """ -from __future__ import unicode_literals - import logging -import types +from types import SimpleNamespace from twisted.internet import reactor @@ -28,14 +25,6 @@ from deluge.common import decode_bytes log = logging.getLogger(__name__) -try: - SimpleNamespace = types.SimpleNamespace # Python 3.3+ -except AttributeError: - - class SimpleNamespace(object): # Python 2.7 - def __init__(self, **attr): - self.__dict__.update(attr) - class AlertManager(component.Component): """AlertManager fetches and processes libtorrent alerts""" @@ -57,6 +46,7 @@ class AlertManager(component.Component): | lt.alert.category_t.status_notification | lt.alert.category_t.ip_block_notification | lt.alert.category_t.performance_warning + | lt.alert.category_t.file_progress_notification ) self.session.apply_settings({'alert_mask': alert_mask}) diff --git a/deluge/core/authmanager.py b/deluge/core/authmanager.py index 0d997c1..3ff8a3a 100644 --- a/deluge/core/authmanager.py +++ b/deluge/core/authmanager.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # # Copyright (C) 2009 Andrew Resch <andrewresch@gmail.com> # Copyright (C) 2011 Pedro Algarvio <pedro@algarvio.me> @@ -8,12 +7,9 @@ # See LICENSE for more details. # -from __future__ import unicode_literals - import logging import os import shutil -from io import open import deluge.component as component import deluge.configmanager as configmanager @@ -32,14 +28,14 @@ log = logging.getLogger(__name__) AUTH_LEVELS_MAPPING = { 'NONE': AUTH_LEVEL_NONE, 'READONLY': AUTH_LEVEL_READONLY, - 'DEFAULT': AUTH_LEVEL_NORMAL, - 'NORMAL': AUTH_LEVEL_DEFAULT, + 'DEFAULT': AUTH_LEVEL_DEFAULT, + 'NORMAL': AUTH_LEVEL_NORMAL, 'ADMIN': AUTH_LEVEL_ADMIN, } AUTH_LEVELS_MAPPING_REVERSE = {v: k for k, v in AUTH_LEVELS_MAPPING.items()} -class Account(object): +class Account: __slots__ = ('username', 'password', 'authlevel') def __init__(self, username, password, authlevel): @@ -56,10 +52,10 @@ class Account(object): } def __repr__(self): - return '<Account username="%(username)s" authlevel=%(authlevel)s>' % { - 'username': self.username, - 'authlevel': self.authlevel, - } + return '<Account username="{username}" authlevel={authlevel}>'.format( + username=self.username, + authlevel=self.authlevel, + ) class AuthManager(component.Component): @@ -101,7 +97,7 @@ class AuthManager(component.Component): int: The auth level for this user. Raises: - AuthenticationRequired: If aditional details are required to authenticate. + AuthenticationRequired: If additional details are required to authenticate. BadLoginError: If the username does not exist or password does not match. """ @@ -184,7 +180,7 @@ class AuthManager(component.Component): if os.path.isfile(filepath): log.debug('Creating backup of %s at: %s', filename, filepath_bak) shutil.copy2(filepath, filepath_bak) - except IOError as ex: + except OSError as ex: log.error('Unable to backup %s to %s: %s', filepath, filepath_bak, ex) else: log.info('Saving the %s at: %s', filename, filepath) @@ -198,7 +194,7 @@ class AuthManager(component.Component): _file.flush() os.fsync(_file.fileno()) shutil.move(filepath_tmp, filepath) - except IOError as ex: + except OSError as ex: log.error('Unable to save %s: %s', filename, ex) if os.path.isfile(filepath_bak): log.info('Restoring backup of %s from: %s', filename, filepath_bak) @@ -227,9 +223,9 @@ class AuthManager(component.Component): for _filepath in (auth_file, auth_file_bak): log.info('Opening %s for load: %s', filename, _filepath) try: - with open(_filepath, 'r', encoding='utf8') as _file: + with open(_filepath, encoding='utf8') as _file: file_data = _file.readlines() - except IOError as ex: + except OSError as ex: log.warning('Unable to load %s: %s', _filepath, ex) file_data = [] else: diff --git a/deluge/core/core.py b/deluge/core/core.py index 9a19e30..35cf019 100644 --- a/deluge/core/core.py +++ b/deluge/core/core.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # # Copyright (C) 2007-2009 Andrew Resch <andrewresch@gmail.com> # Copyright (C) 2011 Pedro Algarvio <pedro@algarvio.me> @@ -8,8 +7,6 @@ # See LICENSE for more details. # -from __future__ import division, unicode_literals - import glob import logging import os @@ -17,8 +14,9 @@ import shutil import tempfile import threading from base64 import b64decode, b64encode +from typing import Any, Dict, List, Optional, Tuple, Union +from urllib.request import URLError, urlopen -from six import string_types from twisted.internet import defer, reactor, task from twisted.web.client import Agent, readBody @@ -41,7 +39,7 @@ from deluge.core.pluginmanager import PluginManager from deluge.core.preferencesmanager import PreferencesManager from deluge.core.rpcserver import export from deluge.core.torrentmanager import TorrentManager -from deluge.decorators import deprecated +from deluge.decorators import deprecated, maybe_coroutine from deluge.error import ( AddTorrentError, DelugeError, @@ -56,12 +54,6 @@ from deluge.event import ( ) from deluge.httpdownloader import download_file -try: - from urllib.request import urlopen, URLError -except ImportError: - # PY2 fallback - from urllib2 import urlopen, URLError - log = logging.getLogger(__name__) DEPR_SESSION_STATUS_KEYS = { @@ -120,7 +112,7 @@ class Core(component.Component): component.Component.__init__(self, 'Core') # Start the libtorrent session. - user_agent = 'Deluge/{} libtorrent/{}'.format(DELUGE_VER, LT_VERSION) + user_agent = f'Deluge/{DELUGE_VER} libtorrent/{LT_VERSION}' peer_id = self._create_peer_id(DELUGE_VER) log.debug('Starting session (peer_id: %s, user_agent: %s)', peer_id, user_agent) settings_pack = { @@ -173,19 +165,25 @@ class Core(component.Component): # store the one in the config so we can restore it on shutdown self._old_listen_interface = None if listen_interface: - if deluge.common.is_ip(listen_interface): + if deluge.common.is_interface(listen_interface): self._old_listen_interface = self.config['listen_interface'] self.config['listen_interface'] = listen_interface else: log.error( - 'Invalid listen interface (must be IP Address): %s', + 'Invalid listen interface (must be IP Address or Interface Name): %s', listen_interface, ) self._old_outgoing_interface = None if outgoing_interface: - self._old_outgoing_interface = self.config['outgoing_interface'] - self.config['outgoing_interface'] = outgoing_interface + if deluge.common.is_interface(outgoing_interface): + self._old_outgoing_interface = self.config['outgoing_interface'] + self.config['outgoing_interface'] = outgoing_interface + else: + log.error( + 'Invalid outgoing interface (must be IP Address or Interface Name): %s', + outgoing_interface, + ) # New release check information self.__new_release = None @@ -243,13 +241,12 @@ class Core(component.Component): """Apply libtorrent session settings. Args: - settings (dict): A dict of lt session settings to apply. - + settings: A dict of lt session settings to apply. """ self.session.apply_settings(settings) @staticmethod - def _create_peer_id(version): + def _create_peer_id(version: str) -> str: """Create a peer_id fingerprint. This creates the peer_id and modifies the release char to identify @@ -264,11 +261,10 @@ class Core(component.Component): ``--DE201b--`` (beta pre-release of v2.0.1) Args: - version (str): The version string in PEP440 dotted notation. + version: The version string in PEP440 dotted notation. Returns: - str: The formattted peer_id with Deluge prefix e.g. '--DE200s--' - + The formatted peer_id with Deluge prefix e.g. '--DE200s--' """ split = deluge.common.VersionSplit(version) # Fill list with zeros to length of 4 and use lt to create fingerprint. @@ -301,7 +297,7 @@ class Core(component.Component): if os.path.isfile(filepath): log.debug('Creating backup of %s at: %s', filename, filepath_bak) shutil.copy2(filepath, filepath_bak) - except IOError as ex: + except OSError as ex: log.error('Unable to backup %s to %s: %s', filepath, filepath_bak, ex) else: log.info('Saving the %s at: %s', filename, filepath) @@ -311,18 +307,17 @@ class Core(component.Component): _file.flush() os.fsync(_file.fileno()) shutil.move(filepath_tmp, filepath) - except (IOError, EOFError) as ex: + except (OSError, EOFError) as ex: log.error('Unable to save %s: %s', filename, ex) if os.path.isfile(filepath_bak): log.info('Restoring backup of %s from: %s', filename, filepath_bak) shutil.move(filepath_bak, filepath) - def _load_session_state(self): + def _load_session_state(self) -> dict: """Loads the libtorrent session state Returns: - dict: A libtorrent sesion state, empty dict if unable to load it. - + A libtorrent sesion state, empty dict if unable to load it. """ filename = 'session.state' filepath = get_config_dir(filename) @@ -333,7 +328,7 @@ class Core(component.Component): try: with open(_filepath, 'rb') as _file: state = lt.bdecode(_file.read()) - except (IOError, EOFError, RuntimeError) as ex: + except (OSError, EOFError, RuntimeError) as ex: log.warning('Unable to load %s: %s', _filepath, ex) else: log.info('Successfully loaded %s: %s', filename, _filepath) @@ -358,8 +353,8 @@ class Core(component.Component): if blocks_read: self.session_status['read_hit_ratio'] = ( - self.session_status['disk.num_blocks_cache_hits'] / blocks_read - ) + blocks_read - self.session_status['disk.num_read_ops'] + ) / blocks_read else: self.session_status['read_hit_ratio'] = 0.0 @@ -404,18 +399,19 @@ class Core(component.Component): # Exported Methods @export - def add_torrent_file_async(self, filename, filedump, options, save_state=True): - """Adds a torrent file to the session asynchonously. + def add_torrent_file_async( + self, filename: str, filedump: str, options: dict, save_state: bool = True + ) -> 'defer.Deferred[Optional[str]]': + """Adds a torrent file to the session asynchronously. Args: - filename (str): The filename of the torrent. - filedump (str): A base64 encoded string of torrent file contents. - options (dict): The options to apply to the torrent upon adding. - save_state (bool): If the state should be saved after adding the file. + filename: The filename of the torrent. + filedump: A base64 encoded string of torrent file contents. + options: The options to apply to the torrent upon adding. + save_state: If the state should be saved after adding the file. Returns: - Deferred: The torrent ID or None. - + The torrent ID or None. """ try: filedump = b64decode(filedump) @@ -436,42 +432,39 @@ class Core(component.Component): return d @export - def prefetch_magnet_metadata(self, magnet, timeout=30): + @maybe_coroutine + async def prefetch_magnet_metadata( + self, magnet: str, timeout: int = 30 + ) -> Tuple[str, bytes]: """Download magnet metadata without adding to Deluge session. Used by UIs to get magnet files for selection before adding to session. + The metadata is bencoded and for transfer base64 encoded. + Args: - magnet (str): The magnet uri. - timeout (int): Number of seconds to wait before cancelling request. + magnet: The magnet URI. + timeout: Number of seconds to wait before canceling request. Returns: - Deferred: A tuple of (torrent_id (str), metadata (dict)) for the magnet. + A tuple of (torrent_id, metadata) for the magnet. """ - - def on_metadata(result, result_d): - """Return result of torrent_id and metadata""" - result_d.callback(result) - return result - - d = self.torrentmanager.prefetch_metadata(magnet, timeout) - # Use a seperate callback chain to handle existing prefetching magnet. - result_d = defer.Deferred() - d.addBoth(on_metadata, result_d) - return result_d + return await self.torrentmanager.prefetch_metadata(magnet, timeout) @export - def add_torrent_file(self, filename, filedump, options): + def add_torrent_file( + self, filename: str, filedump: Union[str, bytes], options: dict + ) -> Optional[str]: """Adds a torrent file to the session. Args: - filename (str): The filename of the torrent. - filedump (str): A base64 encoded string of the torrent file contents. - options (dict): The options to apply to the torrent upon adding. + filename: The filename of the torrent. + filedump: A base64 encoded string of the torrent file contents. + options: The options to apply to the torrent upon adding. Returns: - str: The torrent_id or None. + The torrent_id or None. """ try: filedump = b64decode(filedump) @@ -487,24 +480,26 @@ class Core(component.Component): raise @export - def add_torrent_files(self, torrent_files): - """Adds multiple torrent files to the session asynchonously. + def add_torrent_files( + self, torrent_files: List[Tuple[str, Union[str, bytes], dict]] + ) -> 'defer.Deferred[List[AddTorrentError]]': + """Adds multiple torrent files to the session asynchronously. Args: - torrent_files (list of tuples): Torrent files as tuple of (filename, filedump, options). + torrent_files: Torrent files as tuple of + ``(filename, filedump, options)``. Returns: - Deferred - + A list of errors (if there were any) """ - @defer.inlineCallbacks - def add_torrents(): + @maybe_coroutine + async def add_torrents(): errors = [] last_index = len(torrent_files) - 1 for idx, torrent in enumerate(torrent_files): try: - yield self.add_torrent_file_async( + await self.add_torrent_file_async( torrent[0], torrent[1], torrent[2], save_state=idx == last_index ) except AddTorrentError as ex: @@ -515,93 +510,89 @@ class Core(component.Component): return task.deferLater(reactor, 0, add_torrents) @export - def add_torrent_url(self, url, options, headers=None): - """ - Adds a torrent from a url. Deluge will attempt to fetch the torrent - from url prior to adding it to the session. + @maybe_coroutine + async def add_torrent_url( + self, url: str, options: dict, headers: dict = None + ) -> 'defer.Deferred[Optional[str]]': + """Adds a torrent from a URL. Deluge will attempt to fetch the torrent + from the URL prior to adding it to the session. - :param url: the url pointing to the torrent file - :type url: string - :param options: the options to apply to the torrent on add - :type options: dict - :param headers: any optional headers to send - :type headers: dict + Args: + url: the URL pointing to the torrent file + options: the options to apply to the torrent on add + headers: any optional headers to send - :returns: a Deferred which returns the torrent_id as a str or None + Returns: + a Deferred which returns the torrent_id as a str or None """ - log.info('Attempting to add url %s', url) + log.info('Attempting to add URL %s', url) - def on_download_success(filename): - # We got the file, so add it to the session + tmp_fd, tmp_file = tempfile.mkstemp(prefix='deluge_url.', suffix='.torrent') + try: + filename = await download_file( + url, tmp_file, headers=headers, force_filename=True + ) + except Exception: + log.error('Failed to add torrent from URL %s', url) + raise + else: with open(filename, 'rb') as _file: data = _file.read() + return self.add_torrent_file(filename, b64encode(data), options) + finally: try: - os.remove(filename) + os.close(tmp_fd) + os.remove(tmp_file) except OSError as ex: - log.warning('Could not remove temp file: %s', ex) - return self.add_torrent_file(filename, b64encode(data), options) - - def on_download_fail(failure): - # Log the error and pass the failure onto the client - log.error('Failed to add torrent from url %s', url) - return failure - - tmp_fd, tmp_file = tempfile.mkstemp(prefix='deluge_url.', suffix='.torrent') - os.close(tmp_fd) - d = download_file(url, tmp_file, headers=headers, force_filename=True) - d.addCallbacks(on_download_success, on_download_fail) - return d + log.warning(f'Unable to delete temp file {tmp_file}: , {ex}') @export - def add_torrent_magnet(self, uri, options): - """ - Adds a torrent from a magnet link. - - :param uri: the magnet link - :type uri: string - :param options: the options to apply to the torrent on add - :type options: dict + def add_torrent_magnet(self, uri: str, options: dict) -> str: + """Adds a torrent from a magnet link. - :returns: the torrent_id - :rtype: string + Args: + uri: the magnet link + options: the options to apply to the torrent on add + Returns: + the torrent_id """ - log.debug('Attempting to add by magnet uri: %s', uri) + log.debug('Attempting to add by magnet URI: %s', uri) return self.torrentmanager.add(magnet=uri, options=options) @export - def remove_torrent(self, torrent_id, remove_data): + def remove_torrent(self, torrent_id: str, remove_data: bool) -> bool: """Removes a single torrent from the session. Args: - torrent_id (str): The torrent ID to remove. - remove_data (bool): If True, also remove the downloaded data. + torrent_id: The torrent ID to remove. + remove_data: If True, also remove the downloaded data. Returns: - bool: True if removed successfully. + True if removed successfully. Raises: InvalidTorrentError: If the torrent ID does not exist in the session. - """ log.debug('Removing torrent %s from the core.', torrent_id) return self.torrentmanager.remove(torrent_id, remove_data) @export - def remove_torrents(self, torrent_ids, remove_data): + def remove_torrents( + self, torrent_ids: List[str], remove_data: bool + ) -> 'defer.Deferred[List[Tuple[str, str]]]': """Remove multiple torrents from the session. Args: - torrent_ids (list): The torrent IDs to remove. - remove_data (bool): If True, also remove the downloaded data. + torrent_ids: The torrent IDs to remove. + remove_data: If True, also remove the downloaded data. Returns: - list: An empty list if no errors occurred otherwise the list contains - tuples of strings, a torrent ID and an error message. For example: - - [('<torrent_id>', 'Error removing torrent')] + An empty list if no errors occurred otherwise the list contains + tuples of strings, a torrent ID and an error message. For example: + [('<torrent_id>', 'Error removing torrent')] """ log.info('Removing %d torrents from core.', len(torrent_ids)) @@ -625,17 +616,17 @@ class Core(component.Component): return task.deferLater(reactor, 0, do_remove_torrents) @export - def get_session_status(self, keys): + def get_session_status(self, keys: List[str]) -> Dict[str, Union[int, float]]: """Gets the session status values for 'keys', these keys are taking from libtorrent's session status. See: http://www.rasterbar.com/products/libtorrent/manual.html#status - :param keys: the keys for which we want values - :type keys: list - :returns: a dictionary of {key: value, ...} - :rtype: dict + Args: + keys: the keys for which we want values + Returns: + a dictionary of {key: value, ...} """ if not keys: return self.session_status @@ -652,26 +643,26 @@ class Core(component.Component): ) status[key] = self.session_status[new_key] else: - log.warning('Session status key not valid: %s', key) + log.debug('Session status key not valid: %s', key) return status @export - def force_reannounce(self, torrent_ids): + def force_reannounce(self, torrent_ids: List[str]) -> None: log.debug('Forcing reannouncment to: %s', torrent_ids) for torrent_id in torrent_ids: self.torrentmanager[torrent_id].force_reannounce() @export - def pause_torrent(self, torrent_id): + def pause_torrent(self, torrent_id: str) -> None: """Pauses a torrent""" log.debug('Pausing: %s', torrent_id) - if not isinstance(torrent_id, string_types): + if not isinstance(torrent_id, str): self.pause_torrents(torrent_id) else: self.torrentmanager[torrent_id].pause() @export - def pause_torrents(self, torrent_ids=None): + def pause_torrents(self, torrent_ids: List[str] = None) -> None: """Pauses a list of torrents""" if not torrent_ids: torrent_ids = self.torrentmanager.get_torrent_list() @@ -679,27 +670,27 @@ class Core(component.Component): self.pause_torrent(torrent_id) @export - def connect_peer(self, torrent_id, ip, port): + def connect_peer(self, torrent_id: str, ip: str, port: int): log.debug('adding peer %s to %s', ip, torrent_id) if not self.torrentmanager[torrent_id].connect_peer(ip, port): log.warning('Error adding peer %s:%s to %s', ip, port, torrent_id) @export - def move_storage(self, torrent_ids, dest): + def move_storage(self, torrent_ids: List[str], dest: str): log.debug('Moving storage %s to %s', torrent_ids, dest) for torrent_id in torrent_ids: if not self.torrentmanager[torrent_id].move_storage(dest): log.warning('Error moving torrent %s to %s', torrent_id, dest) @export - def pause_session(self): + def pause_session(self) -> None: """Pause the entire session""" if not self.session.is_paused(): self.session.pause() component.get('EventManager').emit(SessionPausedEvent()) @export - def resume_session(self): + def resume_session(self) -> None: """Resume the entire session""" if self.session.is_paused(): self.session.resume() @@ -708,21 +699,21 @@ class Core(component.Component): component.get('EventManager').emit(SessionResumedEvent()) @export - def is_session_paused(self): + def is_session_paused(self) -> bool: """Returns the activity of the session""" return self.session.is_paused() @export - def resume_torrent(self, torrent_id): + def resume_torrent(self, torrent_id: str) -> None: """Resumes a torrent""" log.debug('Resuming: %s', torrent_id) - if not isinstance(torrent_id, string_types): + if not isinstance(torrent_id, str): self.resume_torrents(torrent_id) else: self.torrentmanager[torrent_id].resume() @export - def resume_torrents(self, torrent_ids=None): + def resume_torrents(self, torrent_ids: List[str] = None) -> None: """Resumes a list of torrents""" if not torrent_ids: torrent_ids = self.torrentmanager.get_torrent_list() @@ -746,7 +737,7 @@ class Core(component.Component): import traceback traceback.print_exc() - # Torrent was probaly removed meanwhile + # Torrent was probably removed meanwhile return {} # Ask the plugin manager to fill in the plugin keys @@ -755,7 +746,9 @@ class Core(component.Component): return status @export - def get_torrent_status(self, torrent_id, keys, diff=False): + def get_torrent_status( + self, torrent_id: str, keys: List[str], diff: bool = False + ) -> dict: torrent_keys, plugin_keys = self.torrentmanager.separate_keys( keys, [torrent_id] ) @@ -769,57 +762,54 @@ class Core(component.Component): ) @export - def get_torrents_status(self, filter_dict, keys, diff=False): - """ - returns all torrents , optionally filtered by filter_dict. - """ + @maybe_coroutine + async def get_torrents_status( + self, filter_dict: dict, keys: List[str], diff: bool = False + ) -> dict: + """returns all torrents , optionally filtered by filter_dict.""" + all_keys = not keys torrent_ids = self.filtermanager.filter_torrent_ids(filter_dict) - d = self.torrentmanager.torrents_status_update(torrent_ids, keys, diff=diff) - - def add_plugin_fields(args): - status_dict, plugin_keys = args - # Ask the plugin manager to fill in the plugin keys - if len(plugin_keys) > 0: - for key in status_dict: - status_dict[key].update( - self.pluginmanager.get_status(key, plugin_keys) - ) - return status_dict - - d.addCallback(add_plugin_fields) - return d + status_dict, plugin_keys = await self.torrentmanager.torrents_status_update( + torrent_ids, keys, diff=diff + ) + # Ask the plugin manager to fill in the plugin keys + if len(plugin_keys) > 0 or all_keys: + for key in status_dict: + status_dict[key].update(self.pluginmanager.get_status(key, plugin_keys)) + return status_dict @export - def get_filter_tree(self, show_zero_hits=True, hide_cat=None): - """ - returns {field: [(value,count)] } + def get_filter_tree( + self, show_zero_hits: bool = True, hide_cat: List[str] = None + ) -> Dict: + """returns {field: [(value,count)] } for use in sidebar(s) """ return self.filtermanager.get_filter_tree(show_zero_hits, hide_cat) @export - def get_session_state(self): + def get_session_state(self) -> List[str]: """Returns a list of torrent_ids in the session.""" # Get the torrent list from the TorrentManager return self.torrentmanager.get_torrent_list() @export - def get_config(self): + def get_config(self) -> dict: """Get all the preferences as a dictionary""" return self.config.config @export - def get_config_value(self, key): + def get_config_value(self, key: str) -> Any: """Get the config value for key""" return self.config.get(key) @export - def get_config_values(self, keys): + def get_config_values(self, keys: List[str]) -> Dict[str, Any]: """Get the config values for the entered keys""" return {key: self.config.get(key) for key in keys} @export - def set_config(self, config): + def set_config(self, config: Dict[str, Any]): """Set the config with values from dictionary""" # Load all the values into the configuration for key in config: @@ -828,21 +818,20 @@ class Core(component.Component): self.config[key] = config[key] @export - def get_listen_port(self): + def get_listen_port(self) -> int: """Returns the active listen port""" return self.session.listen_port() @export - def get_proxy(self): + def get_proxy(self) -> Dict[str, Any]: """Returns the proxy settings Returns: - dict: Contains proxy settings. + Proxy settings. Notes: Proxy type names: 0: None, 1: Socks4, 2: Socks5, 3: Socks5 w Auth, 4: HTTP, 5: HTTP w Auth, 6: I2P - """ settings = self.session.get_settings() @@ -865,51 +854,60 @@ class Core(component.Component): return proxy_dict @export - def get_available_plugins(self): + def get_available_plugins(self) -> List[str]: """Returns a list of plugins available in the core""" return self.pluginmanager.get_available_plugins() @export - def get_enabled_plugins(self): + def get_enabled_plugins(self) -> List[str]: """Returns a list of enabled plugins in the core""" return self.pluginmanager.get_enabled_plugins() @export - def enable_plugin(self, plugin): + def enable_plugin(self, plugin: str) -> 'defer.Deferred[bool]': return self.pluginmanager.enable_plugin(plugin) @export - def disable_plugin(self, plugin): + def disable_plugin(self, plugin: str) -> 'defer.Deferred[bool]': return self.pluginmanager.disable_plugin(plugin) @export - def force_recheck(self, torrent_ids): + def force_recheck(self, torrent_ids: List[str]) -> None: """Forces a data recheck on torrent_ids""" for torrent_id in torrent_ids: self.torrentmanager[torrent_id].force_recheck() @export - def set_torrent_options(self, torrent_ids, options): + def set_torrent_options( + self, torrent_ids: List[str], options: Dict[str, Any] + ) -> None: """Sets the torrent options for torrent_ids Args: - torrent_ids (list): A list of torrent_ids to set the options for. - options (dict): A dict of torrent options to set. See torrent.TorrentOptions class for valid keys. + torrent_ids: A list of torrent_ids to set the options for. + options: A dict of torrent options to set. See + ``torrent.TorrentOptions`` class for valid keys. """ if 'owner' in options and not self.authmanager.has_account(options['owner']): raise DelugeError('Username "%s" is not known.' % options['owner']) - if isinstance(torrent_ids, string_types): + if isinstance(torrent_ids, str): torrent_ids = [torrent_ids] for torrent_id in torrent_ids: self.torrentmanager[torrent_id].set_options(options) @export - def set_torrent_trackers(self, torrent_id, trackers): - """Sets a torrents tracker list. trackers will be [{"url", "tier"}]""" + def set_torrent_trackers( + self, torrent_id: str, trackers: List[Dict[str, Any]] + ) -> None: + """Sets a torrents tracker list. trackers will be ``[{"url", "tier"}]``""" return self.torrentmanager[torrent_id].set_trackers(trackers) + @export + def get_magnet_uri(self, torrent_id: str) -> str: + return self.torrentmanager[torrent_id].get_magnet_uri() + @deprecated @export def set_torrent_max_connections(self, torrent_id, value): @@ -985,7 +983,7 @@ class Core(component.Component): @export def get_path_size(self, path): """Returns the size of the file or folder 'path' and -1 if the path is - unaccessible (non-existent or insufficient privs)""" + inaccessible (non-existent or insufficient privileges)""" return deluge.common.get_path_size(path) @export @@ -1055,11 +1053,11 @@ class Core(component.Component): self.add_torrent_file(os.path.split(target)[1], filedump, options) @export - def upload_plugin(self, filename, filedump): + def upload_plugin(self, filename: str, filedump: Union[str, bytes]) -> None: """This method is used to upload new plugins to the daemon. It is used when connecting to the daemon remotely and installing a new plugin on - the client side. 'plugin_data' is a xmlrpc.Binary object of the file data, - ie, plugin_file.read()""" + the client side. ``plugin_data`` is a ``xmlrpc.Binary`` object of the file data, + i.e. ``plugin_file.read()``""" try: filedump = b64decode(filedump) @@ -1073,26 +1071,24 @@ class Core(component.Component): component.get('CorePluginManager').scan_for_plugins() @export - def rescan_plugins(self): - """ - Rescans the plugin folders for new plugins - """ + def rescan_plugins(self) -> None: + """Re-scans the plugin folders for new plugins""" component.get('CorePluginManager').scan_for_plugins() @export - def rename_files(self, torrent_id, filenames): - """ - Rename files in torrent_id. Since this is an asynchronous operation by + def rename_files( + self, torrent_id: str, filenames: List[Tuple[int, str]] + ) -> defer.Deferred: + """Rename files in ``torrent_id``. Since this is an asynchronous operation by libtorrent, watch for the TorrentFileRenamedEvent to know when the files have been renamed. - :param torrent_id: the torrent_id to rename files - :type torrent_id: string - :param filenames: a list of index, filename pairs - :type filenames: ((index, filename), ...) - - :raises InvalidTorrentError: if torrent_id is invalid + Args: + torrent_id: the torrent_id to rename files + filenames: a list of index, filename pairs + Raises: + InvalidTorrentError: if torrent_id is invalid """ if torrent_id not in self.torrentmanager.torrents: raise InvalidTorrentError('torrent_id is not in session') @@ -1103,21 +1099,20 @@ class Core(component.Component): return task.deferLater(reactor, 0, rename) @export - def rename_folder(self, torrent_id, folder, new_folder): - """ - Renames the 'folder' to 'new_folder' in 'torrent_id'. Watch for the + def rename_folder( + self, torrent_id: str, folder: str, new_folder: str + ) -> defer.Deferred: + """Renames the 'folder' to 'new_folder' in 'torrent_id'. Watch for the TorrentFolderRenamedEvent which is emitted when the folder has been renamed successfully. - :param torrent_id: the torrent to rename folder in - :type torrent_id: string - :param folder: the folder to rename - :type folder: string - :param new_folder: the new folder name - :type new_folder: string - - :raises InvalidTorrentError: if the torrent_id is invalid + Args: + torrent_id: the torrent to rename folder in + folder: the folder to rename + new_folder: the new folder name + Raises: + InvalidTorrentError: if the torrent_id is invalid """ if torrent_id not in self.torrentmanager.torrents: raise InvalidTorrentError('torrent_id is not in session') @@ -1125,7 +1120,7 @@ class Core(component.Component): return self.torrentmanager[torrent_id].rename_folder(folder, new_folder) @export - def queue_top(self, torrent_ids): + def queue_top(self, torrent_ids: List[str]) -> None: log.debug('Attempting to queue %s to top', torrent_ids) # torrent_ids must be sorted in reverse before moving to preserve order for torrent_id in sorted( @@ -1139,7 +1134,7 @@ class Core(component.Component): log.warning('torrent_id: %s does not exist in the queue', torrent_id) @export - def queue_up(self, torrent_ids): + def queue_up(self, torrent_ids: List[str]) -> None: log.debug('Attempting to queue %s to up', torrent_ids) torrents = ( (self.torrentmanager.get_queue_position(torrent_id), torrent_id) @@ -1164,7 +1159,7 @@ class Core(component.Component): prev_queue_position = queue_position @export - def queue_down(self, torrent_ids): + def queue_down(self, torrent_ids: List[str]) -> None: log.debug('Attempting to queue %s to down', torrent_ids) torrents = ( (self.torrentmanager.get_queue_position(torrent_id), torrent_id) @@ -1189,7 +1184,7 @@ class Core(component.Component): prev_queue_position = queue_position @export - def queue_bottom(self, torrent_ids): + def queue_bottom(self, torrent_ids: List[str]) -> None: log.debug('Attempting to queue %s to bottom', torrent_ids) # torrent_ids must be sorted before moving to preserve order for torrent_id in sorted( @@ -1203,17 +1198,15 @@ class Core(component.Component): log.warning('torrent_id: %s does not exist in the queue', torrent_id) @export - def glob(self, path): + def glob(self, path: str) -> List[str]: return glob.glob(path) @export - def test_listen_port(self): - """ - Checks if the active port is open - - :returns: True if the port is open, False if not - :rtype: bool + def test_listen_port(self) -> 'defer.Deferred[Optional[bool]]': + """Checks if the active port is open + Returns: + True if the port is open, False if not """ port = self.get_listen_port() url = 'https://deluge-torrent.org/test_port.php?port=%s' % port @@ -1232,18 +1225,17 @@ class Core(component.Component): return d @export - def get_free_space(self, path=None): - """ - Returns the number of free bytes at path + def get_free_space(self, path: str = None) -> int: + """Returns the number of free bytes at path - :param path: the path to check free space at, if None, use the default download location - :type path: string - - :returns: the number of free bytes at path - :rtype: int + Args: + path: the path to check free space at, if None, use the default download location - :raises InvalidPathError: if the path is invalid + Returns: + the number of free bytes at path + Raises: + InvalidPathError: if the path is invalid """ if not path: path = self.config['download_location'] @@ -1256,46 +1248,40 @@ class Core(component.Component): self.external_ip = external_ip @export - def get_external_ip(self): - """ - Returns the external ip address recieved from libtorrent. - """ + def get_external_ip(self) -> str: + """Returns the external IP address received from libtorrent.""" return self.external_ip @export - def get_libtorrent_version(self): - """ - Returns the libtorrent version. - - :returns: the version - :rtype: string + def get_libtorrent_version(self) -> str: + """Returns the libtorrent version. + Returns: + the version """ return LT_VERSION @export - def get_completion_paths(self, args): - """ - Returns the available path completions for the input value. - """ + def get_completion_paths(self, args: Dict[str, Any]) -> Dict[str, Any]: + """Returns the available path completions for the input value.""" return path_chooser_common.get_completion_paths(args) @export(AUTH_LEVEL_ADMIN) - def get_known_accounts(self): + def get_known_accounts(self) -> List[Dict[str, Any]]: return self.authmanager.get_known_accounts() @export(AUTH_LEVEL_NONE) - def get_auth_levels_mappings(self): + def get_auth_levels_mappings(self) -> Tuple[Dict[str, int], Dict[int, str]]: return (AUTH_LEVELS_MAPPING, AUTH_LEVELS_MAPPING_REVERSE) @export(AUTH_LEVEL_ADMIN) - def create_account(self, username, password, authlevel): + def create_account(self, username: str, password: str, authlevel: str) -> bool: return self.authmanager.create_account(username, password, authlevel) @export(AUTH_LEVEL_ADMIN) - def update_account(self, username, password, authlevel): + def update_account(self, username: str, password: str, authlevel: str) -> bool: return self.authmanager.update_account(username, password, authlevel) @export(AUTH_LEVEL_ADMIN) - def remove_account(self, username): + def remove_account(self, username: str) -> bool: return self.authmanager.remove_account(username) diff --git a/deluge/core/daemon.py b/deluge/core/daemon.py index d7ab813..0185dd8 100644 --- a/deluge/core/daemon.py +++ b/deluge/core/daemon.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # # Copyright (C) 2007-2009 Andrew Resch <andrewresch@gmail.com> # @@ -8,8 +7,6 @@ # """The Deluge daemon""" -from __future__ import unicode_literals - import logging import os import socket @@ -44,8 +41,8 @@ def is_daemon_running(pid_file): try: with open(pid_file) as _file: - pid, port = [int(x) for x in _file.readline().strip().split(';')] - except (EnvironmentError, ValueError): + pid, port = (int(x) for x in _file.readline().strip().split(';')) + except (OSError, ValueError): return False if is_process_running(pid): @@ -53,7 +50,7 @@ def is_daemon_running(pid_file): _socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) try: _socket.connect(('127.0.0.1', port)) - except socket.error: + except OSError: # Can't connect, so pid is not a deluged process. return False else: @@ -62,7 +59,7 @@ def is_daemon_running(pid_file): return True -class Daemon(object): +class Daemon: """The Deluge Daemon class""" def __init__( @@ -156,7 +153,7 @@ class Daemon(object): pid = os.getpid() log.debug('Storing pid %s & port %s in: %s', pid, self.port, self.pid_file) with open(self.pid_file, 'w') as _file: - _file.write('%s;%s\n' % (pid, self.port)) + _file.write(f'{pid};{self.port}\n') component.start() @@ -200,6 +197,7 @@ class Daemon(object): if rpc not in self.get_method_list(): return False - return self.rpcserver.get_session_auth_level() >= self.rpcserver.get_rpc_auth_level( - rpc + return ( + self.rpcserver.get_session_auth_level() + >= self.rpcserver.get_rpc_auth_level(rpc) ) diff --git a/deluge/core/daemon_entry.py b/deluge/core/daemon_entry.py index 8b3746c..c49fd2a 100644 --- a/deluge/core/daemon_entry.py +++ b/deluge/core/daemon_entry.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # # Copyright (C) 2007 Andrew Resch <andrewresch@gmail.com> # Copyright (C) 2010 Pedro Algarvio <pedro@algarvio.me> @@ -7,8 +6,6 @@ # the additional special exception to link portions of this program with the OpenSSL library. # See LICENSE for more details. # -from __future__ import print_function, unicode_literals - import os import sys from logging import DEBUG, FileHandler, getLogger diff --git a/deluge/core/eventmanager.py b/deluge/core/eventmanager.py index 5ba2989..d43847a 100644 --- a/deluge/core/eventmanager.py +++ b/deluge/core/eventmanager.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # # Copyright (C) 2009 Andrew Resch <andrewresch@gmail.com> # @@ -7,8 +6,6 @@ # See LICENSE for more details. # -from __future__ import unicode_literals - import logging import deluge.component as component diff --git a/deluge/core/filtermanager.py b/deluge/core/filtermanager.py index 9d89646..a60cc5b 100644 --- a/deluge/core/filtermanager.py +++ b/deluge/core/filtermanager.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # # Copyright (C) 2008 Martijn Voncken <mvoncken@gmail.com> # @@ -7,12 +6,8 @@ # See LICENSE for more details. # -from __future__ import unicode_literals - import logging -from six import string_types - import deluge.component as component from deluge.common import TORRENT_STATE @@ -100,9 +95,7 @@ def tracker_error_filter(torrent_ids, values): class FilterManager(component.Component): - """FilterManager - - """ + """FilterManager""" def __init__(self, core): component.Component.__init__(self, 'FilterManager') @@ -138,7 +131,7 @@ class FilterManager(component.Component): # Sanitize input: filter-value must be a list of strings for key, value in filter_dict.items(): - if isinstance(value, string_types): + if isinstance(value, str): filter_dict[key] = [value] # Optimized filter for id diff --git a/deluge/core/pluginmanager.py b/deluge/core/pluginmanager.py index 7d2f3a1..0482b16 100644 --- a/deluge/core/pluginmanager.py +++ b/deluge/core/pluginmanager.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # # Copyright (C) 2007 Andrew Resch <andrewresch@gmail.com> # @@ -9,8 +8,6 @@ """PluginManager for Core""" -from __future__ import unicode_literals - import logging from twisted.internet import defer diff --git a/deluge/core/preferencesmanager.py b/deluge/core/preferencesmanager.py index db9556a..7e5c207 100644 --- a/deluge/core/preferencesmanager.py +++ b/deluge/core/preferencesmanager.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # # Copyright (C) 2008-2010 Andrew Resch <andrewresch@gmail.com> # @@ -8,13 +7,13 @@ # -from __future__ import unicode_literals - import logging import os import platform import random import threading +from urllib.parse import quote_plus +from urllib.request import urlopen from twisted.internet.task import LoopingCall @@ -24,17 +23,14 @@ import deluge.configmanager from deluge._libtorrent import lt from deluge.event import ConfigValueChangedEvent +GeoIP = None try: - import GeoIP -except ImportError: - GeoIP = None - -try: - from urllib.parse import quote_plus - from urllib.request import urlopen + from GeoIP import GeoIP except ImportError: - from urllib import quote_plus - from urllib2 import urlopen + try: + from pygeoip import GeoIP + except ImportError: + pass log = logging.getLogger(__name__) @@ -202,7 +198,7 @@ class PreferencesManager(component.Component): self.__set_listen_on() def __set_listen_on(self): - """ Set the ports and interface address to listen for incoming connections on.""" + """Set the ports and interface address to listen for incoming connections on.""" if self.config['random_port']: if not self.config['listen_random_port']: self.config['listen_random_port'] = random.randrange(49152, 65525) @@ -225,13 +221,13 @@ class PreferencesManager(component.Component): self.config['listen_use_sys_port'], ) interfaces = [ - '%s:%s' % (interface, port) + f'{interface}:{port}' for port in range(listen_ports[0], listen_ports[1] + 1) ] self.core.apply_session_settings( { 'listen_system_port_fallback': self.config['listen_use_sys_port'], - 'listen_interfaces': ''.join(interfaces), + 'listen_interfaces': ','.join(interfaces), } ) @@ -400,7 +396,7 @@ class PreferencesManager(component.Component): + quote_plus(':'.join(self.config['enabled_plugins'])) ) urlopen(url) - except IOError as ex: + except OSError as ex: log.debug('Network error while trying to send info: %s', ex) else: self.config['info_sent'] = now @@ -464,11 +460,9 @@ class PreferencesManager(component.Component): # Load the GeoIP DB for country look-ups if available if os.path.exists(geoipdb_path): try: - self.core.geoip_instance = GeoIP.open( - geoipdb_path, GeoIP.GEOIP_STANDARD - ) - except AttributeError: - log.warning('GeoIP Unavailable') + self.core.geoip_instance = GeoIP(geoipdb_path, 0) + except Exception as ex: + log.warning('GeoIP Unavailable: %s', ex) else: log.warning('Unable to find GeoIP database file: %s', geoipdb_path) diff --git a/deluge/core/rpcserver.py b/deluge/core/rpcserver.py index adb5219..d4ca5d1 100644 --- a/deluge/core/rpcserver.py +++ b/deluge/core/rpcserver.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # # Copyright (C) 2008,2009 Andrew Resch <andrewresch@gmail.com> # @@ -8,17 +7,14 @@ # """RPCServer Module""" -from __future__ import unicode_literals - import logging import os -import stat import sys import traceback from collections import namedtuple from types import FunctionType +from typing import Callable, TypeVar, overload -from OpenSSL import crypto from twisted.internet import defer, reactor from twisted.internet.protocol import Factory, connectionDone @@ -29,7 +25,7 @@ from deluge.core.authmanager import ( AUTH_LEVEL_DEFAULT, AUTH_LEVEL_NONE, ) -from deluge.crypto_utils import get_context_factory +from deluge.crypto_utils import check_ssl_keys, get_context_factory from deluge.error import ( DelugeError, IncompatibleClient, @@ -46,6 +42,18 @@ RPC_EVENT = 3 log = logging.getLogger(__name__) +TCallable = TypeVar('TCallable', bound=Callable) + + +@overload +def export(func: TCallable) -> TCallable: + ... + + +@overload +def export(auth_level: int) -> Callable[[TCallable], TCallable]: + ... + def export(auth_level=AUTH_LEVEL_DEFAULT): """ @@ -69,7 +77,7 @@ def export(auth_level=AUTH_LEVEL_DEFAULT): if func.__doc__: if func.__doc__.endswith(' '): indent = func.__doc__.split('\n')[-1] - func.__doc__ += '\n{}'.format(indent) + func.__doc__ += f'\n{indent}' else: func.__doc__ += '\n\n' func.__doc__ += rpc_text @@ -114,7 +122,7 @@ def format_request(call): class DelugeRPCProtocol(DelugeTransferProtocol): def __init__(self): - super(DelugeRPCProtocol, self).__init__() + super().__init__() # namedtuple subclass with auth_level, username for the connected session. self.AuthLevel = namedtuple('SessionAuthlevel', 'auth_level, username') @@ -588,59 +596,3 @@ class RPCServer(component.Component): def stop(self): self.factory.state = 'stopping' - - -def check_ssl_keys(): - """ - Check for SSL cert/key and create them if necessary - """ - ssl_dir = deluge.configmanager.get_config_dir('ssl') - if not os.path.exists(ssl_dir): - # The ssl folder doesn't exist so we need to create it - os.makedirs(ssl_dir) - generate_ssl_keys() - else: - for f in ('daemon.pkey', 'daemon.cert'): - if not os.path.exists(os.path.join(ssl_dir, f)): - generate_ssl_keys() - break - - -def generate_ssl_keys(): - """ - This method generates a new SSL key/cert. - """ - from deluge.common import PY2 - - digest = 'sha256' if not PY2 else b'sha256' - - # Generate key pair - pkey = crypto.PKey() - pkey.generate_key(crypto.TYPE_RSA, 2048) - - # Generate cert request - req = crypto.X509Req() - subj = req.get_subject() - setattr(subj, 'CN', 'Deluge Daemon') - req.set_pubkey(pkey) - req.sign(pkey, digest) - - # Generate certificate - cert = crypto.X509() - cert.set_serial_number(0) - cert.gmtime_adj_notBefore(0) - cert.gmtime_adj_notAfter(60 * 60 * 24 * 365 * 3) # Three Years - cert.set_issuer(req.get_subject()) - cert.set_subject(req.get_subject()) - cert.set_pubkey(req.get_pubkey()) - cert.sign(pkey, digest) - - # Write out files - ssl_dir = deluge.configmanager.get_config_dir('ssl') - with open(os.path.join(ssl_dir, 'daemon.pkey'), 'wb') as _file: - _file.write(crypto.dump_privatekey(crypto.FILETYPE_PEM, pkey)) - with open(os.path.join(ssl_dir, 'daemon.cert'), 'wb') as _file: - _file.write(crypto.dump_certificate(crypto.FILETYPE_PEM, cert)) - # Make the files only readable by this user - for f in ('daemon.pkey', 'daemon.cert'): - os.chmod(os.path.join(ssl_dir, f), stat.S_IREAD | stat.S_IWRITE) 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: diff --git a/deluge/core/torrentmanager.py b/deluge/core/torrentmanager.py index a7df501..5609df4 100644 --- a/deluge/core/torrentmanager.py +++ b/deluge/core/torrentmanager.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # # Copyright (C) 2007-2009 Andrew Resch <andrewresch@gmail.com> # @@ -8,27 +7,33 @@ # """TorrentManager handles Torrent objects""" -from __future__ import unicode_literals - import datetime import logging import operator import os +import pickle import time -from collections import namedtuple +from base64 import b64encode from tempfile import gettempdir +from typing import Dict, List, NamedTuple, Tuple -import six.moves.cPickle as pickle # noqa: N813 -from twisted.internet import defer, error, reactor, threads +from twisted.internet import defer, reactor, threads from twisted.internet.defer import Deferred, DeferredList from twisted.internet.task import LoopingCall import deluge.component as component -from deluge._libtorrent import lt -from deluge.common import archive_files, decode_bytes, get_magnet_info, is_magnet +from deluge._libtorrent import LT_VERSION, lt +from deluge.common import ( + VersionSplit, + archive_files, + decode_bytes, + get_magnet_info, + is_magnet, +) from deluge.configmanager import ConfigManager, get_config_dir from deluge.core.authmanager import AUTH_LEVEL_ADMIN from deluge.core.torrent import Torrent, TorrentOptions, sanitize_filepath +from deluge.decorators import maybe_coroutine from deluge.error import AddTorrentError, InvalidTorrentError from deluge.event import ( ExternalIPEvent, @@ -52,6 +57,11 @@ LT_DEFAULT_ADD_TORRENT_FLAGS = ( ) +class PrefetchQueueItem(NamedTuple): + alert_deferred: Deferred + result_queue: List[Deferred] + + class TorrentState: # pylint: disable=old-style-class """Create a torrent state. @@ -89,7 +99,7 @@ class TorrentState: # pylint: disable=old-style-class super_seeding=False, name=None, ): - # Build the class atrribute list from args + # Build the class attribute list from args for key, value in locals().items(): if key == 'self': continue @@ -129,7 +139,8 @@ class TorrentManager(component.Component): """ - callLater = reactor.callLater # noqa: N815 + # This is used in the test to mock out timeouts + clock = reactor def __init__(self): component.Component.__init__( @@ -158,7 +169,7 @@ class TorrentManager(component.Component): self.is_saving_state = False self.save_resume_data_file_lock = defer.DeferredLock() self.torrents_loading = {} - self.prefetching_metadata = {} + self.prefetching_metadata: Dict[str, PrefetchQueueItem] = {} # This is a map of torrent_ids to Deferreds used to track needed resume data. # The Deferreds will be completed when resume data has been saved. @@ -243,8 +254,8 @@ class TorrentManager(component.Component): self.save_resume_data_timer.start(190, False) self.prev_status_cleanup_loop.start(10) - @defer.inlineCallbacks - def stop(self): + @maybe_coroutine + async def stop(self): # Stop timers if self.save_state_timer.running: self.save_state_timer.stop() @@ -256,11 +267,11 @@ class TorrentManager(component.Component): self.prev_status_cleanup_loop.stop() # Save state on shutdown - yield self.save_state() + await self.save_state() self.session.pause() - result = yield self.save_resume_data(flush_disk_cache=True) + result = await self.save_resume_data(flush_disk_cache=True) # Remove the temp_file to signify successfully saved state if result and os.path.isfile(self.temp_file): os.remove(self.temp_file) @@ -274,11 +285,6 @@ class TorrentManager(component.Component): 'Paused', 'Queued', ): - # If the global setting is set, but the per-torrent isn't... - # Just skip to the next torrent. - # This is so that a user can turn-off the stop at ratio option on a per-torrent basis - if not torrent.options['stop_at_ratio']: - continue if ( torrent.get_ratio() >= torrent.options['stop_ratio'] and torrent.is_finished @@ -286,7 +292,7 @@ class TorrentManager(component.Component): if torrent.options['remove_at_ratio']: self.remove(torrent_id) break - if not torrent.handle.status().paused: + if not torrent.status.paused: torrent.pause() def __getitem__(self, torrent_id): @@ -339,26 +345,28 @@ class TorrentManager(component.Component): else: return torrent_info - def prefetch_metadata(self, magnet, timeout): - """Download the metadata for a magnet uri. + @maybe_coroutine + async def prefetch_metadata(self, magnet: str, timeout: int) -> Tuple[str, bytes]: + """Download the metadata for a magnet URI. Args: - magnet (str): A magnet uri to download the metadata for. - timeout (int): Number of seconds to wait before cancelling. + magnet: A magnet URI to download the metadata for. + timeout: Number of seconds to wait before canceling. Returns: - Deferred: A tuple of (torrent_id (str), metadata (dict)) + A tuple of (torrent_id, metadata) """ torrent_id = get_magnet_info(magnet)['info_hash'] if torrent_id in self.prefetching_metadata: - return self.prefetching_metadata[torrent_id].defer + d = Deferred() + self.prefetching_metadata[torrent_id].result_queue.append(d) + return await d - add_torrent_params = {} - add_torrent_params['save_path'] = gettempdir() - add_torrent_params['url'] = magnet.strip().encode('utf8') - add_torrent_params['flags'] = ( + add_torrent_params = lt.parse_magnet_uri(magnet) + add_torrent_params.save_path = gettempdir() + add_torrent_params.flags = ( ( LT_DEFAULT_ADD_TORRENT_FLAGS | lt.add_torrent_params_flags_t.flag_duplicate_is_error @@ -372,33 +380,29 @@ class TorrentManager(component.Component): d = Deferred() # Cancel the defer if timeout reached. - defer_timeout = self.callLater(timeout, d.cancel) - d.addBoth(self.on_prefetch_metadata, torrent_id, defer_timeout) - Prefetch = namedtuple('Prefetch', 'defer handle') - self.prefetching_metadata[torrent_id] = Prefetch(defer=d, handle=torrent_handle) - return d + d.addTimeout(timeout, self.clock) + self.prefetching_metadata[torrent_id] = PrefetchQueueItem(d, []) - def on_prefetch_metadata(self, torrent_info, torrent_id, defer_timeout): - # Cancel reactor.callLater. try: - defer_timeout.cancel() - except error.AlreadyCalled: - pass - - log.debug('remove prefetch magnet from session') - try: - torrent_handle = self.prefetching_metadata.pop(torrent_id).handle - except KeyError: - pass + torrent_info = await d + except (defer.TimeoutError, defer.CancelledError): + log.debug(f'Prefetching metadata for {torrent_id} timed out or cancelled.') + metadata = b'' else: - self.session.remove_torrent(torrent_handle, 1) - - metadata = None - if isinstance(torrent_info, lt.torrent_info): log.debug('prefetch metadata received') - metadata = lt.bdecode(torrent_info.metadata()) + if VersionSplit(LT_VERSION) < VersionSplit('2.0.0.0'): + metadata = torrent_info.metadata() + else: + metadata = torrent_info.info_section() + + log.debug('remove prefetch magnet from session') + result_queue = self.prefetching_metadata.pop(torrent_id).result_queue + self.session.remove_torrent(torrent_handle, 1) + result = torrent_id, b64encode(metadata) - return torrent_id, metadata + for d in result_queue: + d.callback(result) + return result def _build_torrent_options(self, options): """Load default options and update if needed.""" @@ -431,9 +435,10 @@ class TorrentManager(component.Component): elif magnet: magnet_info = get_magnet_info(magnet) if magnet_info: - add_torrent_params['url'] = magnet.strip().encode('utf8') add_torrent_params['name'] = magnet_info['name'] + add_torrent_params['trackers'] = list(magnet_info['trackers']) torrent_id = magnet_info['info_hash'] + add_torrent_params['info_hash'] = bytes(bytearray.fromhex(torrent_id)) else: raise AddTorrentError( 'Unable to add magnet, invalid magnet info: %s' % magnet @@ -448,7 +453,7 @@ class TorrentManager(component.Component): raise AddTorrentError('Torrent already being added (%s).' % torrent_id) elif torrent_id in self.prefetching_metadata: # Cancel and remove metadata fetching torrent. - self.prefetching_metadata[torrent_id].defer.cancel() + self.prefetching_metadata[torrent_id].alert_deferred.cancel() # Check for renamed files and if so, rename them in the torrent_info before adding. if options['mapped_files'] and torrent_info: @@ -509,7 +514,7 @@ class TorrentManager(component.Component): save_state (bool, optional): If True save the session state after adding torrent, defaults to True. filedump (str, optional): bencoded filedump of a torrent file. filename (str, optional): The filename of the torrent file. - magnet (str, optional): The magnet uri. + magnet (str, optional): The magnet URI. resume_data (lt.entry, optional): libtorrent fast resume data. Returns: @@ -574,7 +579,7 @@ class TorrentManager(component.Component): save_state (bool, optional): If True save the session state after adding torrent, defaults to True. filedump (str, optional): bencoded filedump of a torrent file. filename (str, optional): The filename of the torrent file. - magnet (str, optional): The magnet uri. + magnet (str, optional): The magnet URI. resume_data (lt.entry, optional): libtorrent fast resume data. Returns: @@ -642,7 +647,7 @@ class TorrentManager(component.Component): # Resume AlertManager if paused for adding torrent to libtorrent. component.resume('AlertManager') - # Store the orignal resume_data, in case of errors. + # Store the original resume_data, in case of errors. if resume_data: self.resume_data[torrent.torrent_id] = resume_data @@ -809,9 +814,9 @@ class TorrentManager(component.Component): try: with open(filepath, 'rb') as _file: - state = pickle.load(_file) - except (IOError, EOFError, pickle.UnpicklingError) as ex: - message = 'Unable to load {}: {}'.format(filepath, ex) + state = pickle.load(_file, encoding='utf8') + except (OSError, EOFError, pickle.UnpicklingError) as ex: + message = f'Unable to load {filepath}: {ex}' log.error(message) if not filepath.endswith('.bak'): self.archive_state(message) @@ -1022,7 +1027,7 @@ class TorrentManager(component.Component): ) def on_torrent_resume_save(dummy_result, torrent_id): - """Recieved torrent resume_data alert so remove from waiting list""" + """Received torrent resume_data alert so remove from waiting list""" self.waiting_on_resume_data.pop(torrent_id, None) deferreds = [] @@ -1067,7 +1072,7 @@ class TorrentManager(component.Component): try: with open(_filepath, 'rb') as _file: resume_data = lt.bdecode(_file.read()) - except (IOError, EOFError, RuntimeError) as ex: + except (OSError, EOFError, RuntimeError) as ex: if self.torrents: log.warning('Unable to load %s: %s', _filepath, ex) resume_data = None @@ -1240,7 +1245,7 @@ class TorrentManager(component.Component): def on_alert_add_torrent(self, alert): """Alert handler for libtorrent add_torrent_alert""" if not alert.handle.is_valid(): - log.warning('Torrent handle is invalid!') + log.warning('Torrent handle is invalid: %s', alert.error.message()) return try: @@ -1351,10 +1356,8 @@ class TorrentManager(component.Component): torrent.set_tracker_status('Announce OK') # Check for peer information from the tracker, if none then send a scrape request. - if ( - alert.handle.status().num_complete == -1 - or alert.handle.status().num_incomplete == -1 - ): + torrent.get_lt_status() + if torrent.status.num_complete == -1 or torrent.status.num_incomplete == -1: torrent.scrape_tracker() def on_alert_tracker_announce(self, alert): @@ -1389,7 +1392,18 @@ class TorrentManager(component.Component): log.debug( 'Tracker Error Alert: %s [%s]', decode_bytes(alert.message()), error_message ) - torrent.set_tracker_status('Error: ' + 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(): + if tracker['url'] == alert.url: + if any( + endpoint['last_error']['value'] == 0 + for endpoint in tracker['endpoints'] + ): + torrent.set_tracker_status('Announce OK') + else: + torrent.set_tracker_status('Error: ' + error_message) + break def on_alert_storage_moved(self, alert): """Alert handler for libtorrent storage_moved_alert""" @@ -1463,7 +1477,9 @@ class TorrentManager(component.Component): return if torrent_id in self.torrents: # libtorrent add_torrent expects bencoded resume_data. - self.resume_data[torrent_id] = lt.bencode(alert.resume_data) + self.resume_data[torrent_id] = lt.bencode( + lt.write_resume_data(alert.params) + ) if torrent_id in self.waiting_on_resume_data: self.waiting_on_resume_data[torrent_id].callback(None) @@ -1545,7 +1561,7 @@ class TorrentManager(component.Component): # Try callback to prefetch_metadata method. try: - d = self.prefetching_metadata[torrent_id].defer + d = self.prefetching_metadata[torrent_id].alert_deferred except KeyError: pass else: @@ -1591,23 +1607,14 @@ class TorrentManager(component.Component): except RuntimeError: continue if torrent_id in self.torrents: - self.torrents[torrent_id].update_status(t_status) + self.torrents[torrent_id].status = t_status self.handle_torrents_status_callback(self.torrents_status_requests.pop()) def on_alert_external_ip(self, alert): - """Alert handler for libtorrent external_ip_alert - - Note: - The alert.message IPv4 address format is: - 'external IP received: 0.0.0.0' - and IPv6 address format is: - 'external IP received: 0:0:0:0:0:0:0:0' - """ - - external_ip = decode_bytes(alert.message()).split(' ')[-1] - log.info('on_alert_external_ip: %s', external_ip) - component.get('EventManager').emit(ExternalIPEvent(external_ip)) + """Alert handler for libtorrent external_ip_alert""" + log.info('on_alert_external_ip: %s', alert.external_address) + component.get('EventManager').emit(ExternalIPEvent(alert.external_address)) def on_alert_performance(self, alert): """Alert handler for libtorrent performance_alert""" |