[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:
parent
6a5bb44d5b
commit
c6b6902e9f
|
@ -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)
|
||||||
|
|
|
@ -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:
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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,16 +71,15 @@ class TorrentmanagerTestCase(BaseTestCase):
|
||||||
|
|
||||||
expected = (
|
expected = (
|
||||||
'ab570cdd5a17ea1b61e970bb72047de141bce173',
|
'ab570cdd5a17ea1b61e970bb72047de141bce173',
|
||||||
bencode(
|
|
||||||
{
|
{
|
||||||
'piece length': 32768,
|
b'piece length': 32768,
|
||||||
'sha1': (
|
b'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'
|
||||||
),
|
),
|
||||||
'name': 'azcvsupdater_2.6.2.jar',
|
b'name': b'azcvsupdater_2.6.2.jar',
|
||||||
'private': 0,
|
b'private': 0,
|
||||||
'pieces': (
|
b'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'
|
||||||
|
@ -97,11 +95,10 @@ class TorrentmanagerTestCase(BaseTestCase):
|
||||||
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&'
|
||||||
),
|
),
|
||||||
'length': 307949,
|
b'length': 307949,
|
||||||
'name.utf-8': b'azcvsupdater_2.6.2.jar',
|
b'name.utf-8': b'azcvsupdater_2.6.2.jar',
|
||||||
'ed2k': b'>p\xefl\xfa]\x95K\x1b^\xc2\\;;e\xb7',
|
b'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
|
||||||
|
|
|
@ -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)
|
|
||||||
else:
|
|
||||||
self._metainfo = metainfo
|
|
||||||
if filename and not self._metainfo:
|
|
||||||
log.debug('Attempting to open %s.', filename)
|
log.debug('Attempting to open %s.', filename)
|
||||||
try:
|
try:
|
||||||
with open(filename, 'rb') as _file:
|
with open(filename, 'rb') as _file:
|
||||||
self._metainfo = _file.read()
|
self._filedata = _file.read()
|
||||||
except IOError as ex:
|
except IOError as ex:
|
||||||
log.warning('Unable to open %s: %s', filename, ex)
|
log.warning('Unable to open %s: %s', filename, ex)
|
||||||
return
|
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):
|
||||||
|
|
|
@ -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:
|
||||||
|
|
Loading…
Reference in New Issue