diff --git a/deluge/core/core.py b/deluge/core/core.py index 91f9b8a38..9a19e3057 100644 --- a/deluge/core/core.py +++ b/deluge/core/core.py @@ -437,22 +437,22 @@ class Core(component.Component): @export def prefetch_magnet_metadata(self, magnet, timeout=30): - """Download the metadata for the magnet uri without adding torrent to deluge session. + """Download magnet metadata without adding to Deluge session. + + Used by UIs to get magnet files for selection before adding to session. Args: magnet (str): The magnet uri. - timeout (int): Time to wait to recieve metadata from peers. + timeout (int): Number of seconds to wait before cancelling request. Returns: - Deferred: A tuple of (torrent_id (str), metadata (bytes)) for the magnet. - - The metadata is base64 encoded. + Deferred: A tuple of (torrent_id (str), metadata (dict)) for the magnet. """ def on_metadata(result, result_d): - torrent_id, metadata = result - result_d.callback((torrent_id, b64encode(metadata))) + """Return result of torrent_id and metadata""" + result_d.callback(result) return result d = self.torrentmanager.prefetch_metadata(magnet, timeout) diff --git a/deluge/core/torrentmanager.py b/deluge/core/torrentmanager.py index c92939c06..a7df5012b 100644 --- a/deluge/core/torrentmanager.py +++ b/deluge/core/torrentmanager.py @@ -15,6 +15,7 @@ import logging import operator import os import time +from collections import namedtuple from tempfile import gettempdir import six.moves.cPickle as pickle # noqa: N813 @@ -339,21 +340,20 @@ class TorrentManager(component.Component): return torrent_info def prefetch_metadata(self, magnet, timeout): - """Download metadata for a magnet uri. + """Download the metadata for a magnet uri. Args: magnet (str): A magnet uri to download the metadata for. - timeout (int): How long + timeout (int): Number of seconds to wait before cancelling. Returns: - Deferred: A tuple of (torrent_id (str), bencoded metadata (bytes)) + Deferred: A tuple of (torrent_id (str), metadata (dict)) """ torrent_id = get_magnet_info(magnet)['info_hash'] if torrent_id in self.prefetching_metadata: - d = self.prefetching_metadata[torrent_id][0] - return d + return self.prefetching_metadata[torrent_id].defer add_torrent_params = {} add_torrent_params['save_path'] = gettempdir() @@ -374,7 +374,8 @@ class TorrentManager(component.Component): # Cancel the defer if timeout reached. defer_timeout = self.callLater(timeout, d.cancel) d.addBoth(self.on_prefetch_metadata, torrent_id, defer_timeout) - self.prefetching_metadata[torrent_id] = (d, torrent_handle) + Prefetch = namedtuple('Prefetch', 'defer handle') + self.prefetching_metadata[torrent_id] = Prefetch(defer=d, handle=torrent_handle) return d def on_prefetch_metadata(self, torrent_info, torrent_id, defer_timeout): @@ -384,18 +385,18 @@ class TorrentManager(component.Component): except error.AlreadyCalled: pass - log.debug('remove magnet from session') + log.debug('remove prefetch magnet from session') try: - torrent_handle = self.prefetching_metadata.pop(torrent_id)[1] + torrent_handle = self.prefetching_metadata.pop(torrent_id).handle except KeyError: pass else: self.session.remove_torrent(torrent_handle, 1) - metadata = b'' + metadata = None if isinstance(torrent_info, lt.torrent_info): - log.debug('metadata received') - metadata = torrent_info.metadata() + log.debug('prefetch metadata received') + metadata = lt.bdecode(torrent_info.metadata()) return torrent_id, metadata @@ -447,8 +448,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. - d = self.prefetching_metadata[torrent_id][0] - d.cancel() + self.prefetching_metadata[torrent_id].defer.cancel() # Check for renamed files and if so, rename them in the torrent_info before adding. if options['mapped_files'] and torrent_info: @@ -1545,7 +1545,7 @@ class TorrentManager(component.Component): # Try callback to prefetch_metadata method. try: - d = self.prefetching_metadata[torrent_id][0] + d = self.prefetching_metadata[torrent_id].defer except KeyError: pass else: diff --git a/deluge/tests/test_core.py b/deluge/tests/test_core.py index c0da54da5..4f64c73e6 100644 --- a/deluge/tests/test_core.py +++ b/deluge/tests/test_core.py @@ -314,7 +314,7 @@ class CoreTestCase(BaseTestCase): def test_prefetch_metadata_existing(self): """Check another call with same magnet returns existing deferred.""" magnet = 'magnet:?xt=urn:btih:ab570cdd5a17ea1b61e970bb72047de141bce173' - expected = ('ab570cdd5a17ea1b61e970bb72047de141bce173', b'') + expected = ('ab570cdd5a17ea1b61e970bb72047de141bce173', None) def on_result(result): self.assertEqual(result, expected) diff --git a/deluge/tests/test_torrentmanager.py b/deluge/tests/test_torrentmanager.py index 84ad5a5e9..bf84f451b 100644 --- a/deluge/tests/test_torrentmanager.py +++ b/deluge/tests/test_torrentmanager.py @@ -15,7 +15,6 @@ import pytest from twisted.internet import defer, task from deluge import component -from deluge.bencode import bencode from deluge.core.core import Core from deluge.core.rpcserver import RPCServer from deluge.error import InvalidTorrentError @@ -72,36 +71,34 @@ class TorrentmanagerTestCase(BaseTestCase): expected = ( 'ab570cdd5a17ea1b61e970bb72047de141bce173', - bencode( - { - 'piece length': 32768, - 'sha1': ( - b'2\xce\xb6\xa8"\xd7\xf0\xd4\xbf\xdc^K\xba\x1bh' - b'\x9d\xc5\xb7\xac\xdd' - ), - 'name': 'azcvsupdater_2.6.2.jar', - 'private': 0, - 'pieces': ( - b'\xdb\x04B\x05\xc3\'\xdab\xb8su97\xa9u' - b'\xca\xdf\xdagA' - b'\xc42|\xda\x82\xf5\xa6b\xa1\xb8#\x80wI\xd8f' - b'\xf8\xbd\xacW\xab\xc3s\xe0\xbbw\xf2K\xbe\xee' - b'\xa8rG\xe1W\xe8\xb7\xc2i\xf3\xd8\xaf\x9d\xdc' - b'\xd0#\xf4\xc1\x12u\xcd\x0bE?:\xe8\x9c\x1cu' - b'\xabb(oj\r^\xd5\xd5A\x83\x88\x9a\xa1J\x1c?' - b'\xa1\xd6\x8c\x83\x9e&' - ), - 'length': 307949, - 'name.utf-8': b'azcvsupdater_2.6.2.jar', - 'ed2k': b'>p\xefl\xfa]\x95K\x1b^\xc2\\;;e\xb7', - } - ), + { + b'piece length': 32768, + b'sha1': ( + b'2\xce\xb6\xa8"\xd7\xf0\xd4\xbf\xdc^K\xba\x1bh' + b'\x9d\xc5\xb7\xac\xdd' + ), + b'name': b'azcvsupdater_2.6.2.jar', + b'private': 0, + b'pieces': ( + b'\xdb\x04B\x05\xc3\'\xdab\xb8su97\xa9u' + b'\xca\xdf\xdagA' + b'\xc42|\xda\x82\xf5\xa6b\xa1\xb8#\x80wI\xd8f' + b'\xf8\xbd\xacW\xab\xc3s\xe0\xbbw\xf2K\xbe\xee' + b'\xa8rG\xe1W\xe8\xb7\xc2i\xf3\xd8\xaf\x9d\xdc' + b'\xd0#\xf4\xc1\x12u\xcd\x0bE?:\xe8\x9c\x1cu' + b'\xabb(oj\r^\xd5\xd5A\x83\x88\x9a\xa1J\x1c?' + b'\xa1\xd6\x8c\x83\x9e&' + ), + b'length': 307949, + b'name.utf-8': b'azcvsupdater_2.6.2.jar', + b'ed2k': b'>p\xefl\xfa]\x95K\x1b^\xc2\\;;e\xb7', + }, ) self.assertEqual(expected, self.successResultOf(d)) @@ -109,7 +106,7 @@ class TorrentmanagerTestCase(BaseTestCase): magnet = 'magnet:?xt=urn:btih:ab570cdd5a17ea1b61e970bb72047de141bce173' d = self.tm.prefetch_metadata(magnet, 30) self.clock.advance(30) - expected = ('ab570cdd5a17ea1b61e970bb72047de141bce173', b'') + expected = ('ab570cdd5a17ea1b61e970bb72047de141bce173', None) return d.addCallback(self.assertEqual, expected) @pytest.mark.todo diff --git a/deluge/ui/common.py b/deluge/ui/common.py index 38c27d8cf..21bcafd5f 100644 --- a/deluge/ui/common.py +++ b/deluge/ui/common.py @@ -173,38 +173,36 @@ class TorrentInfo(object): """Collects information about a torrent file. Args: - filename (str): The path to the .torrent file. + filename (str, optional): The path to the .torrent file. filetree (int, optional): The version of filetree to create (defaults to 1). - metainfo (bytes, optional): A bencoded filedump from a .torrent file. - metadata (bytes, optional): A bencoded metadata info_dict. + torrent_file (dict, optional): A bdecoded .torrent file contents. """ - def __init__(self, filename='', filetree=1, metainfo=None, metadata=None): - # Get the torrent metainfo from the torrent file - if metadata: - self._metainfo_dict = {b'info': bencode.bdecode(metadata)} - - self._metainfo = bencode.bencode(self._metainfo_dict) - else: - self._metainfo = metainfo - if filename and not self._metainfo: - log.debug('Attempting to open %s.', filename) - try: - with open(filename, 'rb') as _file: - self._metainfo = _file.read() - except IOError as ex: - log.warning('Unable to open %s: %s', filename, ex) - return + def __init__(self, filename='', filetree=1, torrent_file=None): + self._filedata = None + if torrent_file: + self._metainfo = torrent_file + elif filename: + log.debug('Attempting to open %s.', filename) + try: + with open(filename, 'rb') as _file: + self._filedata = _file.read() + except IOError as ex: + log.warning('Unable to open %s: %s', filename, ex) + return try: - self._metainfo_dict = bencode.bdecode(self._metainfo) + self._metainfo = bencode.bdecode(self._filedata) except bencode.BTFailure as ex: log.warning('Failed to decode %s: %s', filename, ex) return + else: + log.warning('Requires valid arguments.') + return - # info_dict with keys decoded. - info_dict = {k.decode(): v for k, v in self._metainfo_dict[b'info'].items()} + # info_dict with keys decoded to unicode. + info_dict = {k.decode(): v for k, v in self._metainfo[b'info'].items()} self._info_hash = sha(bencode.bencode(info_dict)).hexdigest() # Get encoding from torrent file if available @@ -299,6 +297,25 @@ class TorrentInfo(object): else: self._files_tree = {self._name: (0, info_dict['length'], True)} + @classmethod + def from_metadata(cls, metadata, trackers=None): + """Create a TorrentInfo from metadata and trackers + + Args: + metadata (dict): A bdecoded info section of torrent file. + trackers (list of lists, optional): The trackers to include. + + """ + if not isinstance(metadata, dict): + return + + metainfo = {b'info': metadata} + if trackers: + metainfo[b'announce'] = trackers[0][0].encode('utf-8') + trackers_utf8 = [[t.encode('utf-8') for t in tier] for tier in trackers] + metainfo[b'announce-list'] = trackers_utf8 + return cls(torrent_file=metainfo) + def as_dict(self, *keys): """The torrent info as a dictionary, filtered by keys. @@ -358,24 +375,28 @@ class TorrentInfo(object): return self._files_tree @property - def metadata(self): - """The torrents metainfo dictionary. + def metainfo(self): + """Returns the torrent metainfo dictionary. + + This is the bdecoded torrent file contents. Returns: - dict: The bdecoded metainfo dictionary. + dict: The metainfo dictionary. """ - return self._metainfo_dict + return self._metainfo @property def filedata(self): """The contents of the .torrent file. Returns: - str: The metainfo bencoded dictionary from a torrent file. + bytes: The bencoded metainfo. """ - return self._metainfo + if not self._filedata: + self._filedata = bencode.bencode(self._metainfo) + return self._filedata class FileTree2(object): diff --git a/deluge/ui/gtk3/addtorrentdialog.py b/deluge/ui/gtk3/addtorrentdialog.py index d164c2cc5..e7066f9ba 100644 --- a/deluge/ui/gtk3/addtorrentdialog.py +++ b/deluge/ui/gtk3/addtorrentdialog.py @@ -11,7 +11,7 @@ from __future__ import division, unicode_literals import logging import os -from base64 import b64decode, b64encode +from base64 import b64encode from xml.sax.saxutils import escape as xml_escape from xml.sax.saxutils import unescape as xml_unescape @@ -251,16 +251,15 @@ class AddTorrentDialog(component.Component): if already_added: self.show_already_added_dialog(already_added) - def _on_uri_metadata(self, result, uri): + def _on_uri_metadata(self, result, uri, trackers): """Process prefetched metadata to allow file priority selection.""" - info_hash, b64_metadata = result - log.debug('on_uri_metadata for %s (%s)', uri, info_hash) + info_hash, metadata = result + log.debug('magnet metadata for %s (%s)', uri, info_hash) if info_hash not in self.prefetching_magnets: return - if b64_metadata: - metadata = b64decode(b64_metadata) - info = TorrentInfo(metadata=metadata) + if metadata: + info = TorrentInfo.from_metadata(metadata, [[t] for t in trackers]) self.files[info_hash] = info.files self.infos[info_hash] = info.filedata else: @@ -313,7 +312,7 @@ class AddTorrentDialog(component.Component): self.prefetching_magnets.append(torrent_id) self.prefetch_waiting_message(torrent_id, None) d = client.core.prefetch_magnet_metadata(uri) - d.addCallback(self._on_uri_metadata, uri) + d.addCallback(self._on_uri_metadata, uri, magnet['trackers']) d.addErrback(self._on_uri_metadata_fail, torrent_id) if already_added: