[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
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)

View File

@ -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:

View File

@ -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)

View File

@ -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<w\\\x1ef\xd4\x9b\x16\xa9}\xc0\x9f:\xfd'
b'\x97qv\x83\xa2"\xef\x9d7\x0by!\rl\xe5v\xb7'
b'\x18{\xf7/"P\xe9\x8d\x01D\x9e8\xbd\x16\xe3'
b'\xfb-\x9d\xaa\xbcM\x11\xba\x92\xfc\x13F\xf0'
b'\x1c\x86x+\xc8\xd0S\xa9\x90`\xa1\xe4\x82\xe8'
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'\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<w\\\x1ef\xd4\x9b\x16\xa9}\xc0\x9f:\xfd'
b'\x97qv\x83\xa2"\xef\x9d7\x0by!\rl\xe5v\xb7'
b'\x18{\xf7/"P\xe9\x8d\x01D\x9e8\xbd\x16\xe3'
b'\xfb-\x9d\xaa\xbcM\x11\xba\x92\xfc\x13F\xf0'
b'\x1c\x86x+\xc8\xd0S\xa9\x90`\xa1\xe4\x82\xe8'
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'\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

View File

@ -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):

View File

@ -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: