Skip to content

Commit a4a295a

Browse files
committed
Add internal BitTorrent v2 support
1 parent 572e334 commit a4a295a

File tree

13 files changed

+255
-91
lines changed

13 files changed

+255
-91
lines changed

src/tribler/core/libtorrent/download_manager/download.py

+8-2
Original file line numberDiff line numberDiff line change
@@ -434,7 +434,8 @@ def on_save_resume_data_alert(self, alert: lt.save_resume_data_alert) -> None:
434434
self.config.set_engineresumedata(resume_data)
435435

436436
# Save it to file
437-
basename = hexlify(resume_data[b"info-hash"]).decode() + ".conf"
437+
# Note resume_data[b"info-hash"] can be b"\x00" * 32, so we use the tdef.
438+
basename = hexlify(self.tdef.get_infohash()).decode() + ".conf"
438439
Path(self.download_manager.get_checkpoint_dir()).mkdir(parents=True, exist_ok=True)
439440
filename = self.download_manager.get_checkpoint_dir() / basename
440441
self.config.config["download_defaults"]["name"] = self.tdef.get_name_as_unicode() # store name (for debugging)
@@ -523,7 +524,7 @@ def on_metadata_received_alert(self, alert: lt.metadata_received_alert) -> None:
523524
metadata[b"announce"] = tracker_urls[0]
524525

525526
try:
526-
self.tdef = TorrentDef.load_from_dict(metadata)
527+
self.set_def(TorrentDef.load_from_dict(metadata))
527528
with suppress(RuntimeError):
528529
# Try to load the torrent info in the background if we have a loop.
529530
get_running_loop().run_in_executor(None, self.tdef.load_torrent_info)
@@ -895,6 +896,11 @@ def set_def(self, tdef: TorrentDef) -> None:
895896
"""
896897
Set the torrent definition for this download.
897898
"""
899+
if (isinstance(self.tdef, TorrentDefNoMetainfo) and not isinstance(tdef, TorrentDefNoMetainfo)
900+
and len(self.tdef.infohash) != 20):
901+
# We store SHA-1 conf files. v2 torrents start with SHA-256 infohashes.
902+
basename = hexlify(self.tdef.get_infohash()).decode() + ".conf"
903+
Path(self.download_manager.get_checkpoint_dir() / basename).unlink(missing_ok=True)
898904
self.tdef = tdef
899905

900906
@check_handle(None)

src/tribler/core/libtorrent/download_manager/download_manager.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -1024,7 +1024,7 @@ async def load_checkpoints(self) -> None:
10241024
Load the checkpoint files in the checkpoint directory.
10251025
"""
10261026
self._logger.info("Load checkpoints...")
1027-
checkpoint_filenames = list(self.get_checkpoint_dir().glob("*.conf"))
1027+
checkpoint_filenames = sorted(self.get_checkpoint_dir().glob("*.conf"), key=lambda p: len(p.parts[-1]))
10281028
self.checkpoints_count = len(checkpoint_filenames)
10291029
for i, filename in enumerate(checkpoint_filenames, start=1):
10301030
await self.load_checkpoint(filename)

src/tribler/core/libtorrent/download_manager/download_state.py

+6
Original file line numberDiff line numberDiff line change
@@ -287,6 +287,12 @@ def get_files_completion(self) -> list[tuple[Path, float]]:
287287
for index, (path, size) in enumerate(files):
288288
completion_frac = (float(progress[index]) / size) if size > 0 else 1
289289
completion.append((path, completion_frac))
290+
elif progress and len(progress) > len(files) and self.download.tdef.torrent_info_loaded():
291+
# We need to remap
292+
remapping = self.download.tdef.get_file_indices()
293+
for index, (path, size) in enumerate(files):
294+
completion_frac = (float(progress[remapping[index]]) / size) if size > 0 else 1
295+
completion.append((path, completion_frac))
290296

291297
return completion
292298

src/tribler/core/libtorrent/restapi/downloads_endpoint.py

+6-5
Original file line numberDiff line numberDiff line change
@@ -158,13 +158,14 @@ def get_files_info_json(download: Download) -> list[JSONFilesInfo]:
158158
files_json = []
159159
files_completion = dict(download.get_state().get_files_completion())
160160
selected_files = download.config.get_selected_files()
161+
index_mapping = download.get_def().get_file_indices()
161162
for file_index, (fn, size) in enumerate(download.get_def().get_files_with_length()):
162163
files_json.append(cast(JSONFilesInfo, {
163-
"index": file_index,
164+
"index": index_mapping[file_index],
164165
# We always return files in Posix format to make GUI independent of Core and simplify testing
165166
"name": str(PurePosixPath(fn)),
166167
"size": size,
167-
"included": (file_index in selected_files or not selected_files),
168+
"included": (index_mapping[file_index] in selected_files or not selected_files),
168169
"progress": files_completion.get(fn, 0.0)
169170
}))
170171
return files_json
@@ -581,8 +582,8 @@ async def update_download(self, request: Request) -> RESTResponse: # noqa: C901
581582

582583
if "selected_files" in parameters:
583584
selected_files_list = parameters["selected_files"]
584-
num_files = len(download.tdef.get_files())
585-
if not all(0 <= index < num_files for index in selected_files_list):
585+
max_index = max(download.tdef.get_file_indices())
586+
if not all(0 <= index <= max_index for index in selected_files_list):
586587
return RESTResponse({"error": {
587588
"handled": True,
588589
"message": "index out of range"
@@ -1115,7 +1116,7 @@ async def stream(self, request: Request) -> web.StreamResponse:
11151116
return DownloadsEndpoint.return_404()
11161117

11171118
file_index = int(request.match_info["fileindex"])
1118-
if not 0 <= file_index < len(download.get_def().get_files()):
1119+
if not 0 <= file_index <= max(download.get_def().get_file_indices()):
11191120
return DownloadsEndpoint.return_404()
11201121

11211122
return TorrentStreamResponse(download, file_index)

src/tribler/core/libtorrent/restapi/torrentinfo_endpoint.py

+23-13
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,10 @@
11
from __future__ import annotations
22

3-
import json
43
import logging
54
from asyncio.exceptions import TimeoutError as AsyncTimeoutError
65
from binascii import hexlify, unhexlify
7-
from copy import deepcopy
86
from ssl import SSLError
9-
from typing import TYPE_CHECKING
7+
from typing import TYPE_CHECKING, TypedDict
108

119
import libtorrent as lt
1210
from aiohttp import (
@@ -46,6 +44,16 @@
4644
logger = logging.getLogger(__name__)
4745

4846

47+
class JSONMiniFileInfo(TypedDict):
48+
"""
49+
A minimal JSON dict to describe file info.
50+
"""
51+
52+
index: int
53+
name: str
54+
size: int
55+
56+
4957
def recursive_unicode(obj: Iterable, ignore_errors: bool = False) -> Iterable:
5058
"""
5159
Converts any bytes within a data structure to unicode strings. Bytes are assumed to be UTF-8 encoded text.
@@ -102,6 +110,14 @@ def __init__(self, download_manager: DownloadManager) -> None:
102110
self.app.add_routes([web.get("", self.get_torrent_info),
103111
web.put("", self.get_torrent_info_from_file)])
104112

113+
def get_files(self, tdef: TorrentDef) -> list[JSONMiniFileInfo]:
114+
"""
115+
Get a list of files from the given torrent definition.
116+
"""
117+
remapped_indices = tdef.get_file_indices()
118+
return [{"index": remapped_indices[i], "name": str(f[0]), "size": f[1]}
119+
for i, f in enumerate(tdef.get_files_with_length())]
120+
105121
@docs(
106122
tags=["Libtorrent"],
107123
summary="Return metainfo from a torrent found at a provided URI.",
@@ -269,13 +285,8 @@ async def get_torrent_info(self, request: Request) -> RESTResponse: # noqa: C90
269285
metainfo_download = metainfo_lookup.download if metainfo_lookup else None
270286
download_is_metainfo_request = download == metainfo_download
271287

272-
# Check if the torrent is already in the downloads
273-
encoded_metainfo = deepcopy(metainfo)
274-
275-
ready_for_unicode = recursive_unicode(encoded_metainfo, ignore_errors=True)
276-
json_dump = json.dumps(ready_for_unicode, ensure_ascii=False)
277-
278-
return RESTResponse({"metainfo": hexlify(json_dump.encode()).decode(),
288+
return RESTResponse({"files": self.get_files(torrent_def),
289+
"name": torrent_def.get_name_utf8(),
279290
"download_exists": download and not download_is_metainfo_request,
280291
"valid_certificate": valid_cert})
281292

@@ -302,8 +313,7 @@ async def get_torrent_info_from_file(self, request: web.Request) -> RESTResponse
302313
metainfo_download = metainfo_lookup.download if metainfo_lookup else None
303314
requesting_metainfo = download == metainfo_download
304315

305-
metainfo_unicode = recursive_unicode(deepcopy(tdef.get_metainfo()), ignore_errors=True)
306-
metainfo_json = json.dumps(metainfo_unicode, ensure_ascii=False)
307316
return RESTResponse({"infohash": hexlify(infohash).decode(),
308-
"metainfo": hexlify(metainfo_json.encode('utf-8')).decode(),
317+
"files": self.get_files(tdef),
318+
"name": tdef.get_name_utf8(),
309319
"download_exists": download and not requesting_metainfo})

0 commit comments

Comments
 (0)