[UI][Common] Add support for BitTorrent V2 file tree
In BEP52, a new tiles structure was introduce, a `file tree`. This change added support for this structure in the `TorrentInfo` class. Ref: https://www.bittorrent.org/beps/bep_0052.html Closes: https://github.com/deluge-torrent/deluge/pull/404
This commit is contained in:
parent
8001110625
commit
a459e78268
Binary file not shown.
Binary file not shown.
|
@ -44,7 +44,7 @@ class TestUICommon:
|
|||
ti = TorrentInfo(filename, filetree=1)
|
||||
assert ti.files_tree == files_tree
|
||||
|
||||
filestree2 = {
|
||||
files_tree2 = {
|
||||
'contents': {
|
||||
'torrent_filehash': {
|
||||
'type': 'dir',
|
||||
|
@ -71,7 +71,7 @@ class TestUICommon:
|
|||
'type': 'dir',
|
||||
}
|
||||
ti = TorrentInfo(filename, filetree=2)
|
||||
assert ti.files_tree == filestree2
|
||||
assert ti.files_tree == files_tree2
|
||||
|
||||
def test_hash_optional_md5sum(self):
|
||||
# Ensure `md5sum` key is not included in filetree output
|
||||
|
@ -173,3 +173,118 @@ class TestUICommon:
|
|||
}
|
||||
]
|
||||
assert ti.files == result_files
|
||||
|
||||
def test_bittorrent_v2_path(self):
|
||||
filename = common.get_test_data_file('v2_test.torrent')
|
||||
files_tree = {
|
||||
'torrent_test': {
|
||||
'small.txt': (0, 22, True),
|
||||
'還在一個人無聊嗎~還不趕緊上來聊天美.txt': (1, 32, True),
|
||||
}
|
||||
}
|
||||
ti = TorrentInfo(filename, filetree=1)
|
||||
assert ti.files_tree == files_tree
|
||||
|
||||
files_tree2 = {
|
||||
'contents': {
|
||||
'torrent_test': {
|
||||
'type': 'dir',
|
||||
'contents': {
|
||||
'small.txt': {
|
||||
'type': 'file',
|
||||
'path': 'torrent_test/small.txt',
|
||||
'length': 22,
|
||||
'index': 0,
|
||||
'download': True,
|
||||
},
|
||||
'還在一個人無聊嗎~還不趕緊上來聊天美.txt': {
|
||||
'type': 'file',
|
||||
'path': 'torrent_test/還在一個人無聊嗎~還不趕緊上來聊天美.txt',
|
||||
'length': 32,
|
||||
'index': 1,
|
||||
'download': True,
|
||||
},
|
||||
},
|
||||
'length': 54,
|
||||
'download': True,
|
||||
}
|
||||
},
|
||||
'type': 'dir',
|
||||
}
|
||||
ti = TorrentInfo(filename, filetree=2)
|
||||
assert ti.files_tree == files_tree2
|
||||
|
||||
def test_bittorrent_v2_hybrid_path(self):
|
||||
filename = common.get_test_data_file('v2_hybrid.torrent')
|
||||
files_tree = {
|
||||
'torrent_test': {
|
||||
'small.txt': (0, 22, True),
|
||||
'還在一個人無聊嗎~還不趕緊上來聊天美.txt': (2, 32, True),
|
||||
'.pad': {
|
||||
'16362': (1, 16362, True),
|
||||
'16352': (3, 16352, True),
|
||||
},
|
||||
}
|
||||
}
|
||||
ti = TorrentInfo(filename, filetree=1, force_bt_version=1)
|
||||
assert ti.files_tree == files_tree
|
||||
del files_tree['torrent_test']['.pad']
|
||||
files_tree['torrent_test']['還在一個人無聊嗎~還不趕緊上來聊天美.txt'] = (1, 32, True)
|
||||
ti = TorrentInfo(filename, filetree=1, force_bt_version=2)
|
||||
assert ti.files_tree == files_tree
|
||||
|
||||
files_tree2 = {
|
||||
'contents': {
|
||||
'torrent_test': {
|
||||
'type': 'dir',
|
||||
'contents': {
|
||||
'small.txt': {
|
||||
'type': 'file',
|
||||
'path': 'torrent_test/small.txt',
|
||||
'length': 22,
|
||||
'index': 0,
|
||||
'download': True,
|
||||
},
|
||||
'還在一個人無聊嗎~還不趕緊上來聊天美.txt': {
|
||||
'type': 'file',
|
||||
'path': 'torrent_test/還在一個人無聊嗎~還不趕緊上來聊天美.txt',
|
||||
'length': 32,
|
||||
'index': 2,
|
||||
'download': True,
|
||||
},
|
||||
'.pad': {
|
||||
'type': 'dir',
|
||||
'contents': {
|
||||
'16362': {
|
||||
'type': 'file',
|
||||
'path': 'torrent_test/.pad/16362',
|
||||
'length': 16362,
|
||||
'index': 1,
|
||||
'download': True,
|
||||
},
|
||||
'16352': {
|
||||
'type': 'file',
|
||||
'path': 'torrent_test/.pad/16352',
|
||||
'length': 16352,
|
||||
'index': 3,
|
||||
'download': True,
|
||||
},
|
||||
},
|
||||
'length': 32714,
|
||||
'download': True,
|
||||
},
|
||||
},
|
||||
'length': 32768,
|
||||
'download': True,
|
||||
}
|
||||
},
|
||||
'type': 'dir',
|
||||
}
|
||||
ti = TorrentInfo(filename, filetree=2, force_bt_version=1)
|
||||
assert ti.files_tree == files_tree2
|
||||
torrent_test = files_tree2['contents']['torrent_test']
|
||||
torrent_test['length'] -= torrent_test['contents']['.pad']['length']
|
||||
del torrent_test['contents']['.pad']
|
||||
torrent_test['contents']['還在一個人無聊嗎~還不趕緊上來聊天美.txt']['index'] = 1
|
||||
ti = TorrentInfo(filename, filetree=2, force_bt_version=2)
|
||||
assert ti.files_tree == files_tree2
|
||||
|
|
|
@ -13,6 +13,7 @@ The ui common module contains methods and classes that are deemed useful for all
|
|||
import logging
|
||||
import os
|
||||
from hashlib import sha1 as sha
|
||||
from typing import Tuple
|
||||
|
||||
from deluge import bencode
|
||||
from deluge.common import decode_bytes
|
||||
|
@ -171,10 +172,11 @@ class TorrentInfo:
|
|||
filename (str, optional): The path to the .torrent file.
|
||||
filetree (int, optional): The version of filetree to create (defaults to 1).
|
||||
torrent_file (dict, optional): A bdecoded .torrent file contents.
|
||||
force_bt_version (int, optional): The BitTorrent spec to use for parsing (defaults to 1).
|
||||
|
||||
"""
|
||||
|
||||
def __init__(self, filename='', filetree=1, torrent_file=None):
|
||||
def __init__(self, filename='', filetree=1, torrent_file=None, force_bt_version=1):
|
||||
self._filedata = None
|
||||
if torrent_file:
|
||||
self._metainfo = torrent_file
|
||||
|
@ -211,9 +213,24 @@ class TorrentInfo:
|
|||
else:
|
||||
self._name = decode_bytes(info_dict['name'], encoding)
|
||||
|
||||
meta_version = info_dict['meta version'] if 'meta version' in info_dict else -1
|
||||
is_hybrid = 'files' in info_dict and meta_version == 2
|
||||
|
||||
parse_v1 = False
|
||||
parse_v2 = False
|
||||
if is_hybrid:
|
||||
if force_bt_version == 1:
|
||||
parse_v1 = True
|
||||
elif force_bt_version == 2:
|
||||
parse_v2 = True
|
||||
elif 'files' in info_dict:
|
||||
parse_v1 = True
|
||||
elif meta_version == 2 and 'file tree' in info_dict:
|
||||
parse_v2 = True
|
||||
|
||||
# Get list of files from torrent info
|
||||
self._files = []
|
||||
if 'files' in info_dict:
|
||||
if parse_v1:
|
||||
paths = {}
|
||||
dirs = {}
|
||||
prefix = self._name
|
||||
|
@ -245,25 +262,67 @@ class TorrentInfo:
|
|||
|
||||
if filetree == 2:
|
||||
|
||||
def walk(path, item):
|
||||
def walk(full_path, item):
|
||||
if item['type'] == 'dir':
|
||||
item.update(dirs[path])
|
||||
item.update(dirs[full_path])
|
||||
else:
|
||||
item.update(paths[path])
|
||||
item.update(paths[full_path])
|
||||
item['download'] = True
|
||||
|
||||
file_tree = FileTree2(list(paths))
|
||||
file_tree.walk(walk)
|
||||
else:
|
||||
|
||||
def walk(path, item):
|
||||
def walk(full_path, item):
|
||||
if isinstance(item, dict):
|
||||
return item
|
||||
return [paths[path]['index'], paths[path]['length'], True]
|
||||
return [paths[full_path]['index'], paths[full_path]['length'], True]
|
||||
|
||||
file_tree = FileTree(paths)
|
||||
file_tree.walk(walk)
|
||||
self._files_tree = file_tree.get_tree()
|
||||
elif parse_v2:
|
||||
|
||||
def single_file_torrent(inner_info_dict):
|
||||
if len(inner_info_dict['file tree']) > 1:
|
||||
return False
|
||||
|
||||
file_name = [key for key in inner_info_dict['file tree']][0]
|
||||
return inner_info_dict['name'] == file_name
|
||||
|
||||
if not single_file_torrent(info_dict):
|
||||
info_dict['file tree'] = {info_dict['name']: info_dict['file tree']}
|
||||
|
||||
if filetree == 2:
|
||||
|
||||
def walk(full_path, item):
|
||||
if item['type'] == 'file':
|
||||
item['path'] = full_path
|
||||
self._files.append(
|
||||
{
|
||||
'path': full_path,
|
||||
'size': item['length'],
|
||||
'download': True,
|
||||
}
|
||||
)
|
||||
item['download'] = True
|
||||
|
||||
file_tree = FileTree2BTv2(info_dict['file tree'])
|
||||
file_tree.walk(walk)
|
||||
else:
|
||||
|
||||
def walk(full_path, item):
|
||||
if isinstance(item, dict):
|
||||
return item
|
||||
self._files.append(
|
||||
{'path': full_path, 'size': item[1], 'download': True}
|
||||
)
|
||||
return [item[0], item[1], True]
|
||||
|
||||
file_tree = FiletreeBTv2(info_dict['file tree'])
|
||||
file_tree.walk(walk)
|
||||
|
||||
self._files_tree = file_tree.get_tree()
|
||||
else:
|
||||
self._files.append(
|
||||
{'path': self._name, 'size': info_dict['length'], 'download': True}
|
||||
|
@ -386,13 +445,31 @@ class TorrentInfo:
|
|||
|
||||
class FileTree2:
|
||||
"""
|
||||
Converts a list of paths in to a file tree.
|
||||
Converts a list of paths, from a V1 torrent, into a file tree.
|
||||
|
||||
:param paths: The paths to be converted
|
||||
:type paths: list
|
||||
Each file will have the dictionary structure of:
|
||||
{ file_name: {type, path, index, length, download} }
|
||||
where:
|
||||
type (str): will always be "file"
|
||||
path (str): the absolute file path from the root the torrent
|
||||
index (int): the index of file in the torrent
|
||||
length (int): the size of the file, in bytes
|
||||
download (bool): marks the file to download
|
||||
|
||||
Folder will be dictionaries of files:
|
||||
{ dir1: type, contents: {file_name1: {...}, file_name2: {...}}, dir2: ... }
|
||||
where:
|
||||
type (str): will always be "dir"
|
||||
contents (dict): a dictionary of inner files and folders
|
||||
|
||||
The entire tree will start with a root dictionary:
|
||||
{ contents: {dirs...}, type: "dir" }
|
||||
|
||||
Args:
|
||||
paths (list): The paths to be converted.
|
||||
"""
|
||||
|
||||
def __init__(self, paths):
|
||||
def __init__(self, paths: list):
|
||||
self.tree = {'contents': {}, 'type': 'dir'}
|
||||
|
||||
def get_parent(path):
|
||||
|
@ -466,13 +543,23 @@ class FileTree2:
|
|||
|
||||
class FileTree:
|
||||
"""
|
||||
Convert a list of paths in a file tree.
|
||||
Converts a dict of paths, from a V1 torrent, into a file tree.
|
||||
|
||||
:param paths: The paths to be converted.
|
||||
:type paths: list
|
||||
Each file will have the dictionary structure of:
|
||||
{ file_name: [index, length, download] }
|
||||
Where:
|
||||
index (int): the index of file in the torrent
|
||||
length (int): the size of the file, in bytes
|
||||
download (bool): marks the file to download
|
||||
|
||||
Folder will be dictionaries of files:
|
||||
{ dir1: {file_name1: [...], file_name2: [...]}, dir2: ... }
|
||||
|
||||
Args:
|
||||
paths (dict): The paths to be converted.
|
||||
"""
|
||||
|
||||
def __init__(self, paths):
|
||||
def __init__(self, paths: dict):
|
||||
self.tree = {}
|
||||
|
||||
def get_parent(path):
|
||||
|
@ -498,8 +585,8 @@ class FileTree:
|
|||
"""
|
||||
Return the tree, after first converting all file lists to a tuple.
|
||||
|
||||
:returns: the file tree.
|
||||
:rtype: dictionary
|
||||
Returns:
|
||||
dict: the file tree.
|
||||
"""
|
||||
|
||||
def to_tuple(path, item):
|
||||
|
@ -515,10 +602,10 @@ class FileTree:
|
|||
Walk through the file tree calling the callback function on each item
|
||||
contained.
|
||||
|
||||
:param callback: The function to be used as a callback, it should have
|
||||
Args:
|
||||
callback (function): The function to be used as a callback, it should have
|
||||
the signature func(item, path) where item is a `tuple` for a file
|
||||
and `dict` for a directory.
|
||||
:type callback: function
|
||||
"""
|
||||
|
||||
def walk(directory, parent_path):
|
||||
|
@ -547,3 +634,94 @@ class FileTree:
|
|||
|
||||
self.walk(write)
|
||||
return '\n'.join(lines)
|
||||
|
||||
|
||||
class FiletreeBTv2(FileTree):
|
||||
"""
|
||||
Converts a dict of paths, from a V2 torrent, into a file tree.
|
||||
|
||||
Each file will have the dictionary structure of:
|
||||
{ file_name: [index, length, download] }
|
||||
Where:
|
||||
index (int): the index of file in the torrent
|
||||
length (int): the size of the file, in bytes
|
||||
download (bool): marks the file to download
|
||||
|
||||
Folder will be dictionaries of files:
|
||||
{ dir1: {file_name1: [...], file_name2: [...]}, dir2: ... }
|
||||
|
||||
Args:
|
||||
file_tree (dict): The paths to be converted.
|
||||
"""
|
||||
|
||||
def __init__(self, file_tree):
|
||||
self.tree = {}
|
||||
|
||||
def get_parent(curr_tree_dict, index, parent) -> int:
|
||||
for key, item in curr_tree_dict.items():
|
||||
key = decode_bytes(key)
|
||||
if b'' in item:
|
||||
parent[key] = [index, item[b''][b'length']]
|
||||
index += 1
|
||||
else:
|
||||
parent[key] = {}
|
||||
index = get_parent(item, index, parent[key])
|
||||
return index
|
||||
|
||||
get_parent(file_tree, 0, self.tree)
|
||||
|
||||
|
||||
class FileTree2BTv2(FileTree2):
|
||||
"""
|
||||
Converts a dict of paths, from a V2 torrent, into a file tree.
|
||||
|
||||
Each file will have the dictionary structure of:
|
||||
{ file_name: {type, path, index, length, download} }
|
||||
where:
|
||||
type (str): will always be "file"
|
||||
path (str): the absolute file path from the root the torrent
|
||||
index (int): the index of file in the torrent
|
||||
length (int): the size of the file, in bytes
|
||||
download (bool): marks the file to download
|
||||
|
||||
Folder will be dictionaries of files:
|
||||
{ dir1: type, contents: {file_name1: {...}, file_name2: {...}}, dir2: ... }
|
||||
where:
|
||||
type (str): will always be "dir"
|
||||
contents (dict): a dictionary of inner files and folders
|
||||
|
||||
The entire tree will start with a root dictionary:
|
||||
{ contents: {dirs...}, type: "dir" }
|
||||
|
||||
Args:
|
||||
file_tree (dict): The paths to be converted.
|
||||
"""
|
||||
|
||||
def __init__(self, file_tree):
|
||||
self.tree = {'contents': {}, 'type': 'dir'}
|
||||
|
||||
def get_parent(curr_tree_dict, index, parent) -> Tuple[int, int]:
|
||||
total_length = 0
|
||||
for key, item in curr_tree_dict.items():
|
||||
key = decode_bytes(key)
|
||||
if b'' in item:
|
||||
length = item[b''][b'length']
|
||||
total_length += length
|
||||
parent['contents'][key] = {
|
||||
'index': index,
|
||||
'length': length,
|
||||
'type': 'file',
|
||||
}
|
||||
index += 1
|
||||
else:
|
||||
parent['contents'][key] = {
|
||||
'contents': {},
|
||||
'type': 'dir',
|
||||
'length': 0,
|
||||
}
|
||||
index, length = get_parent(item, index, parent['contents'][key])
|
||||
parent['contents'][key]['length'] = length
|
||||
total_length += length
|
||||
return index, total_length
|
||||
|
||||
get_parent(file_tree, 0, self.tree)
|
||||
|
|
Loading…
Reference in New Issue