summaryrefslogtreecommitdiffstats
path: root/deluge/core
diff options
context:
space:
mode:
Diffstat (limited to 'deluge/core')
-rw-r--r--deluge/core/alertmanager.py14
-rw-r--r--deluge/core/authmanager.py28
-rw-r--r--deluge/core/core.py476
-rw-r--r--deluge/core/daemon.py18
-rw-r--r--deluge/core/daemon_entry.py3
-rw-r--r--deluge/core/eventmanager.py3
-rw-r--r--deluge/core/filtermanager.py11
-rw-r--r--deluge/core/pluginmanager.py3
-rw-r--r--deluge/core/preferencesmanager.py36
-rw-r--r--deluge/core/rpcserver.py80
-rw-r--r--deluge/core/torrent.py196
-rw-r--r--deluge/core/torrentmanager.py175
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"""