[Core] Fix prefetch magnets missing trackers

When adding magnets that have been prefetched the tracker details were
lost. A result of returning only the lt.torrent_info.metadata which
does not contain full torrent details, such as trackers.

- Modified torrentmanager prefetch_metadata to return dict instead of
  base64 encoded bencoded metadata dict...
  - Used a namedtuple to ease identifying tuple contents.
  - Updated tests to reflect changes with mock trackers added to
    test_torrent.file.torrent.

- Refactor TorrentInfo to accept dict instead of bytes and add
  a class method to accept metadata dict with lists of trackers.
  - Rename class arg from metainfo to torrent_file, matching
    lt.torrent_info.
  - Rename metadata property to correct name; metainfo.
  - Simplify class variable naming with _filedata and _metainfo for
    torrent file contents encoded and decoded respectively.

- Update GTK Add torrent dialog to pass trackers to TorrentInfo.
This commit is contained in:
Calum Lind 2019-05-20 15:31:26 +01:00
parent 6a5bb44d5b
commit c6b6902e9f
6 changed files with 107 additions and 90 deletions

View File

@ -437,22 +437,22 @@ class Core(component.Component):
@export @export
def prefetch_magnet_metadata(self, magnet, timeout=30): 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: Args:
magnet (str): The magnet uri. 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: Returns:
Deferred: A tuple of (torrent_id (str), metadata (bytes)) for the magnet. Deferred: A tuple of (torrent_id (str), metadata (dict)) for the magnet.
The metadata is base64 encoded.
""" """
def on_metadata(result, result_d): def on_metadata(result, result_d):
torrent_id, metadata = result """Return result of torrent_id and metadata"""
result_d.callback((torrent_id, b64encode(metadata))) result_d.callback(result)
return result return result
d = self.torrentmanager.prefetch_metadata(magnet, timeout) d = self.torrentmanager.prefetch_metadata(magnet, timeout)

View File

@ -15,6 +15,7 @@ import logging
import operator import operator
import os import os
import time import time
from collections import namedtuple
from tempfile import gettempdir from tempfile import gettempdir
import six.moves.cPickle as pickle # noqa: N813 import six.moves.cPickle as pickle # noqa: N813
@ -339,21 +340,20 @@ class TorrentManager(component.Component):
return torrent_info return torrent_info
def prefetch_metadata(self, magnet, timeout): def prefetch_metadata(self, magnet, timeout):
"""Download metadata for a magnet uri. """Download the metadata for a magnet uri.
Args: Args:
magnet (str): A magnet uri to download the metadata for. magnet (str): A magnet uri to download the metadata for.
timeout (int): How long timeout (int): Number of seconds to wait before cancelling.
Returns: 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'] torrent_id = get_magnet_info(magnet)['info_hash']
if torrent_id in self.prefetching_metadata: if torrent_id in self.prefetching_metadata:
d = self.prefetching_metadata[torrent_id][0] return self.prefetching_metadata[torrent_id].defer
return d
add_torrent_params = {} add_torrent_params = {}
add_torrent_params['save_path'] = gettempdir() add_torrent_params['save_path'] = gettempdir()
@ -374,7 +374,8 @@ class TorrentManager(component.Component):
# Cancel the defer if timeout reached. # Cancel the defer if timeout reached.
defer_timeout = self.callLater(timeout, d.cancel) defer_timeout = self.callLater(timeout, d.cancel)
d.addBoth(self.on_prefetch_metadata, torrent_id, defer_timeout) 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 return d
def on_prefetch_metadata(self, torrent_info, torrent_id, defer_timeout): def on_prefetch_metadata(self, torrent_info, torrent_id, defer_timeout):
@ -384,18 +385,18 @@ class TorrentManager(component.Component):
except error.AlreadyCalled: except error.AlreadyCalled:
pass pass
log.debug('remove magnet from session') log.debug('remove prefetch magnet from session')
try: try:
torrent_handle = self.prefetching_metadata.pop(torrent_id)[1] torrent_handle = self.prefetching_metadata.pop(torrent_id).handle
except KeyError: except KeyError:
pass pass
else: else:
self.session.remove_torrent(torrent_handle, 1) self.session.remove_torrent(torrent_handle, 1)
metadata = b'' metadata = None
if isinstance(torrent_info, lt.torrent_info): if isinstance(torrent_info, lt.torrent_info):
log.debug('metadata received') log.debug('prefetch metadata received')
metadata = torrent_info.metadata() metadata = lt.bdecode(torrent_info.metadata())
return torrent_id, metadata return torrent_id, metadata
@ -447,8 +448,7 @@ class TorrentManager(component.Component):
raise AddTorrentError('Torrent already being added (%s).' % torrent_id) raise AddTorrentError('Torrent already being added (%s).' % torrent_id)
elif torrent_id in self.prefetching_metadata: elif torrent_id in self.prefetching_metadata:
# Cancel and remove metadata fetching torrent. # Cancel and remove metadata fetching torrent.
d = self.prefetching_metadata[torrent_id][0] self.prefetching_metadata[torrent_id].defer.cancel()
d.cancel()
# Check for renamed files and if so, rename them in the torrent_info before adding. # Check for renamed files and if so, rename them in the torrent_info before adding.
if options['mapped_files'] and torrent_info: if options['mapped_files'] and torrent_info:
@ -1545,7 +1545,7 @@ class TorrentManager(component.Component):
# Try callback to prefetch_metadata method. # Try callback to prefetch_metadata method.
try: try:
d = self.prefetching_metadata[torrent_id][0] d = self.prefetching_metadata[torrent_id].defer
except KeyError: except KeyError:
pass pass
else: else:

View File

@ -314,7 +314,7 @@ class CoreTestCase(BaseTestCase):
def test_prefetch_metadata_existing(self): def test_prefetch_metadata_existing(self):
"""Check another call with same magnet returns existing deferred.""" """Check another call with same magnet returns existing deferred."""
magnet = 'magnet:?xt=urn:btih:ab570cdd5a17ea1b61e970bb72047de141bce173' magnet = 'magnet:?xt=urn:btih:ab570cdd5a17ea1b61e970bb72047de141bce173'
expected = ('ab570cdd5a17ea1b61e970bb72047de141bce173', b'') expected = ('ab570cdd5a17ea1b61e970bb72047de141bce173', None)
def on_result(result): def on_result(result):
self.assertEqual(result, expected) self.assertEqual(result, expected)

View File

@ -15,7 +15,6 @@ import pytest
from twisted.internet import defer, task from twisted.internet import defer, task
from deluge import component from deluge import component
from deluge.bencode import bencode
from deluge.core.core import Core from deluge.core.core import Core
from deluge.core.rpcserver import RPCServer from deluge.core.rpcserver import RPCServer
from deluge.error import InvalidTorrentError from deluge.error import InvalidTorrentError
@ -72,36 +71,34 @@ class TorrentmanagerTestCase(BaseTestCase):
expected = ( expected = (
'ab570cdd5a17ea1b61e970bb72047de141bce173', 'ab570cdd5a17ea1b61e970bb72047de141bce173',
bencode( {
{ b'piece length': 32768,
'piece length': 32768, b'sha1': (
'sha1': ( b'2\xce\xb6\xa8"\xd7\xf0\xd4\xbf\xdc^K\xba\x1bh'
b'2\xce\xb6\xa8"\xd7\xf0\xd4\xbf\xdc^K\xba\x1bh' b'\x9d\xc5\xb7\xac\xdd'
b'\x9d\xc5\xb7\xac\xdd' ),
), b'name': b'azcvsupdater_2.6.2.jar',
'name': 'azcvsupdater_2.6.2.jar', b'private': 0,
'private': 0, b'pieces': (
'pieces': ( b'\xdb\x04B\x05\xc3\'\xdab\xb8su97\xa9u'
b'\xdb\x04B\x05\xc3\'\xdab\xb8su97\xa9u' b'\xca<w\\\x1ef\xd4\x9b\x16\xa9}\xc0\x9f:\xfd'
b'\xca<w\\\x1ef\xd4\x9b\x16\xa9}\xc0\x9f:\xfd' b'\x97qv\x83\xa2"\xef\x9d7\x0by!\rl\xe5v\xb7'
b'\x97qv\x83\xa2"\xef\x9d7\x0by!\rl\xe5v\xb7' b'\x18{\xf7/"P\xe9\x8d\x01D\x9e8\xbd\x16\xe3'
b'\x18{\xf7/"P\xe9\x8d\x01D\x9e8\xbd\x16\xe3' b'\xfb-\x9d\xaa\xbcM\x11\xba\x92\xfc\x13F\xf0'
b'\xfb-\x9d\xaa\xbcM\x11\xba\x92\xfc\x13F\xf0' b'\x1c\x86x+\xc8\xd0S\xa9\x90`\xa1\xe4\x82\xe8'
b'\x1c\x86x+\xc8\xd0S\xa9\x90`\xa1\xe4\x82\xe8' b'\xfc\x08\xf7\xe3\xe5\xf6\x85\x1c%\xe7%\n\xed'
b'\xfc\x08\xf7\xe3\xe5\xf6\x85\x1c%\xe7%\n\xed' b'\xc0\x1f\xa1;\x9a\xea\xcf\x90\x0c/F>\xdf\xdagA'
b'\xc0\x1f\xa1;\x9a\xea\xcf\x90\x0c/F>\xdf\xdagA' b'\xc42|\xda\x82\xf5\xa6b\xa1\xb8#\x80wI\xd8f'
b'\xc42|\xda\x82\xf5\xa6b\xa1\xb8#\x80wI\xd8f' b'\xf8\xbd\xacW\xab\xc3s\xe0\xbbw\xf2K\xbe\xee'
b'\xf8\xbd\xacW\xab\xc3s\xe0\xbbw\xf2K\xbe\xee' b'\xa8rG\xe1W\xe8\xb7\xc2i\xf3\xd8\xaf\x9d\xdc'
b'\xa8rG\xe1W\xe8\xb7\xc2i\xf3\xd8\xaf\x9d\xdc' b'\xd0#\xf4\xc1\x12u\xcd\x0bE?:\xe8\x9c\x1cu'
b'\xd0#\xf4\xc1\x12u\xcd\x0bE?:\xe8\x9c\x1cu' b'\xabb(oj\r^\xd5\xd5A\x83\x88\x9a\xa1J\x1c?'
b'\xabb(oj\r^\xd5\xd5A\x83\x88\x9a\xa1J\x1c?' b'\xa1\xd6\x8c\x83\x9e&'
b'\xa1\xd6\x8c\x83\x9e&' ),
), b'length': 307949,
'length': 307949, b'name.utf-8': b'azcvsupdater_2.6.2.jar',
'name.utf-8': b'azcvsupdater_2.6.2.jar', b'ed2k': b'>p\xefl\xfa]\x95K\x1b^\xc2\\;;e\xb7',
'ed2k': b'>p\xefl\xfa]\x95K\x1b^\xc2\\;;e\xb7', },
}
),
) )
self.assertEqual(expected, self.successResultOf(d)) self.assertEqual(expected, self.successResultOf(d))
@ -109,7 +106,7 @@ class TorrentmanagerTestCase(BaseTestCase):
magnet = 'magnet:?xt=urn:btih:ab570cdd5a17ea1b61e970bb72047de141bce173' magnet = 'magnet:?xt=urn:btih:ab570cdd5a17ea1b61e970bb72047de141bce173'
d = self.tm.prefetch_metadata(magnet, 30) d = self.tm.prefetch_metadata(magnet, 30)
self.clock.advance(30) self.clock.advance(30)
expected = ('ab570cdd5a17ea1b61e970bb72047de141bce173', b'') expected = ('ab570cdd5a17ea1b61e970bb72047de141bce173', None)
return d.addCallback(self.assertEqual, expected) return d.addCallback(self.assertEqual, expected)
@pytest.mark.todo @pytest.mark.todo

View File

@ -173,38 +173,36 @@ class TorrentInfo(object):
"""Collects information about a torrent file. """Collects information about a torrent file.
Args: 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). filetree (int, optional): The version of filetree to create (defaults to 1).
metainfo (bytes, optional): A bencoded filedump from a .torrent file. torrent_file (dict, optional): A bdecoded .torrent file contents.
metadata (bytes, optional): A bencoded metadata info_dict.
""" """
def __init__(self, filename='', filetree=1, metainfo=None, metadata=None): def __init__(self, filename='', filetree=1, torrent_file=None):
# Get the torrent metainfo from the torrent file self._filedata = None
if metadata: if torrent_file:
self._metainfo_dict = {b'info': bencode.bdecode(metadata)} self._metainfo = torrent_file
elif filename:
self._metainfo = bencode.bencode(self._metainfo_dict) log.debug('Attempting to open %s.', filename)
else: try:
self._metainfo = metainfo with open(filename, 'rb') as _file:
if filename and not self._metainfo: self._filedata = _file.read()
log.debug('Attempting to open %s.', filename) except IOError as ex:
try: log.warning('Unable to open %s: %s', filename, ex)
with open(filename, 'rb') as _file: return
self._metainfo = _file.read()
except IOError as ex:
log.warning('Unable to open %s: %s', filename, ex)
return
try: try:
self._metainfo_dict = bencode.bdecode(self._metainfo) self._metainfo = bencode.bdecode(self._filedata)
except bencode.BTFailure as ex: except bencode.BTFailure as ex:
log.warning('Failed to decode %s: %s', filename, ex) log.warning('Failed to decode %s: %s', filename, ex)
return return
else:
log.warning('Requires valid arguments.')
return
# info_dict with keys decoded. # info_dict with keys decoded to unicode.
info_dict = {k.decode(): v for k, v in self._metainfo_dict[b'info'].items()} info_dict = {k.decode(): v for k, v in self._metainfo[b'info'].items()}
self._info_hash = sha(bencode.bencode(info_dict)).hexdigest() self._info_hash = sha(bencode.bencode(info_dict)).hexdigest()
# Get encoding from torrent file if available # Get encoding from torrent file if available
@ -299,6 +297,25 @@ class TorrentInfo(object):
else: else:
self._files_tree = {self._name: (0, info_dict['length'], True)} 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): def as_dict(self, *keys):
"""The torrent info as a dictionary, filtered by keys. """The torrent info as a dictionary, filtered by keys.
@ -358,24 +375,28 @@ class TorrentInfo(object):
return self._files_tree return self._files_tree
@property @property
def metadata(self): def metainfo(self):
"""The torrents metainfo dictionary. """Returns the torrent metainfo dictionary.
This is the bdecoded torrent file contents.
Returns: Returns:
dict: The bdecoded metainfo dictionary. dict: The metainfo dictionary.
""" """
return self._metainfo_dict return self._metainfo
@property @property
def filedata(self): def filedata(self):
"""The contents of the .torrent file. """The contents of the .torrent file.
Returns: 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): class FileTree2(object):

View File

@ -11,7 +11,7 @@ from __future__ import division, unicode_literals
import logging import logging
import os 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 escape as xml_escape
from xml.sax.saxutils import unescape as xml_unescape from xml.sax.saxutils import unescape as xml_unescape
@ -251,16 +251,15 @@ class AddTorrentDialog(component.Component):
if already_added: if already_added:
self.show_already_added_dialog(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.""" """Process prefetched metadata to allow file priority selection."""
info_hash, b64_metadata = result info_hash, metadata = result
log.debug('on_uri_metadata for %s (%s)', uri, info_hash) log.debug('magnet metadata for %s (%s)', uri, info_hash)
if info_hash not in self.prefetching_magnets: if info_hash not in self.prefetching_magnets:
return return
if b64_metadata: if metadata:
metadata = b64decode(b64_metadata) info = TorrentInfo.from_metadata(metadata, [[t] for t in trackers])
info = TorrentInfo(metadata=metadata)
self.files[info_hash] = info.files self.files[info_hash] = info.files
self.infos[info_hash] = info.filedata self.infos[info_hash] = info.filedata
else: else:
@ -313,7 +312,7 @@ class AddTorrentDialog(component.Component):
self.prefetching_magnets.append(torrent_id) self.prefetching_magnets.append(torrent_id)
self.prefetch_waiting_message(torrent_id, None) self.prefetch_waiting_message(torrent_id, None)
d = client.core.prefetch_magnet_metadata(uri) 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) d.addErrback(self._on_uri_metadata_fail, torrent_id)
if already_added: if already_added: