Skip to content

Commit

Permalink
Added torrent file trees to the download endpoint (#7705)
Browse files Browse the repository at this point in the history
  • Loading branch information
qstokkink committed Nov 27, 2023
1 parent 5c309d3 commit 4e25843
Show file tree
Hide file tree
Showing 11 changed files with 1,191 additions and 136 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -479,16 +479,21 @@ def set_selected_files(self, selected_files=None, prio: int = 4, force: bool = F
else:
self.config.set_selected_files(selected_files)

torrent_info = get_info_from_handle(self.handle)
if not torrent_info or not hasattr(torrent_info, 'files'):
self._logger.error("File info not available for torrent %s", hexlify(self.tdef.get_infohash()))
return
tree = self.tdef.torrent_file_tree
total_files = self.tdef.torrent_info.num_files()

filepriorities = []
torrent_storage = torrent_info.files()
for index, file_entry in enumerate(torrent_storage):
filepriorities.append(prio if index in selected_files or not selected_files else 0)
self.set_file_priorities(filepriorities)
if not selected_files:
selected_files = range(total_files)

def map_selected(index: int) -> int:
file_instance = tree.find(Path(tree.file_storage.file_path(index)))
if index in selected_files:
file_instance.selected = True
return prio
file_instance.selected = False
return 0

self.set_file_priorities(list(map(map_selected, range(total_files))))

@check_handle(False)
def move_storage(self, new_dir: Path):
Expand Down Expand Up @@ -799,6 +804,9 @@ def get_piece_priorities(self):
def set_file_priorities(self, file_priorities):
self.handle.prioritize_files(file_priorities)

def set_file_priority(self, file_index: int, prio: int = 4) -> None:
self.handle.file_priority(file_index, prio)

@check_handle(None)
def reset_piece_deadline(self, piece):
self.handle.reset_piece_deadline(piece)
Expand Down Expand Up @@ -829,6 +837,9 @@ def file_piece_range(self, file_path: Path) -> list[int]:
# There is no next file so the nex piece is the last piece index + 1 (num_pieces()).
next_piece = self.tdef.torrent_info.num_pieces()

if start_piece == next_piece:
# A single piece with multiple files.
return [start_piece]
return list(range(start_piece, next_piece))

@check_handle(0.0)
Expand Down Expand Up @@ -872,6 +883,21 @@ def get_file_index(self, path: Path) -> int:
else IllegalFileIndex.expanded_dir.value)
return IllegalFileIndex.unloaded.value

@check_handle(None)
def set_selected_file_or_dir(self, path: Path, selected: bool) -> None:
"""
Set a single file or directory to be selected or not.
"""
tree = self.tdef.torrent_file_tree
prio = 4 if selected else 0
for index in tree.set_selected(Path(path), selected):
self.set_file_priority(index, prio)
if not selected:
with suppress(ValueError):
self.config.get_selected_files().remove(index)
else:
self.config.get_selected_files().append(index)

def is_file_selected(self, file_path: Path) -> bool:
"""
Check if the given file path is selected.
Expand Down
205 changes: 203 additions & 2 deletions src/tribler/core/components/libtorrent/restapi/downloads_endpoint.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
from ipv8.messaging.anonymization.tunnel import CIRCUIT_ID_PORT, PEER_FLAG_EXIT_BT
from marshmallow.fields import Boolean, Float, Integer, List, String

from tribler.core.components.libtorrent.download_manager.download import Download, IllegalFileIndex
from tribler.core.components.libtorrent.download_manager.download_config import DownloadConfig
from tribler.core.components.libtorrent.download_manager.download_manager import DownloadManager
from tribler.core.components.libtorrent.download_manager.stream import STREAM_PAUSE_TIME, StreamChunk
Expand Down Expand Up @@ -95,6 +96,10 @@ def setup_routes(self):
web.patch('/{infohash}', self.update_download),
web.get('/{infohash}/torrent', self.get_torrent),
web.get('/{infohash}/files', self.get_files),
web.get('/{infohash}/files/expand', self.expand_tree_directory),
web.get('/{infohash}/files/collapse', self.collapse_tree_directory),
web.get('/{infohash}/files/select', self.select_tree_path),
web.get('/{infohash}/files/deselect', self.deselect_tree_path),
web.get('/{infohash}/stream/{fileindex}', self.stream, allow_head=False)])

@staticmethod
Expand Down Expand Up @@ -155,6 +160,37 @@ def get_files_info_json(download):
file_index += 1
return files_json

@staticmethod
def get_files_info_json_paged(download: Download, view_start: Path, view_size: int):
"""
Return file info, similar to get_files_info_json() but paged (based on view_start and view_size).
Note that the view_start path is not included in the return value.
:param view_start: The last-known path from which to fetch new paths.
:param view_size: The requested number of elements (though only less may be available).
"""
if not download.tdef.torrent_info_loaded():
download.tdef.load_torrent_info()
return [{
"index": IllegalFileIndex.unloaded.value,
"name": "loading...",
"size": 0,
"included": 0,
"progress": 0.0
}]
return [
{
"index": download.get_file_index(path),
"name": str(PurePosixPath(path_str)),
"size": download.get_file_length(path),
"included": download.is_file_selected(path),
"progress": download.get_file_completion(path)
}
for path_str in download.tdef.torrent_file_tree.view(view_start, view_size)
if (path := Path(path_str))
]

@docs(
tags=["Libtorrent"],
summary="Return all downloads, both active and inactive",
Expand Down Expand Up @@ -582,7 +618,21 @@ async def get_torrent(self, request):
'description': 'Infohash of the download to from which to get file information',
'type': 'string',
'required': True
}],
},
{
'in': 'query',
'name': 'view_start_path',
'description': 'Path of the file or directory to form a view for',
'type': 'string',
'required': False
},
{
'in': 'query',
'name': 'view_size',
'description': 'Number of files to include in the view',
'type': 'number',
'required': False
}],
responses={
200: {
"schema": schema(GetFilesResponse={"files": [schema(File={'index': Integer,
Expand All @@ -598,7 +648,158 @@ async def get_files(self, request):
download = self.download_manager.get_download(infohash)
if not download:
return DownloadsEndpoint.return_404(request)
return RESTResponse({"files": self.get_files_info_json(download)})

params = request.query
view_start_path = params.get('view_start_path')
if view_start_path is None:
return RESTResponse({
"infohash": request.match_info['infohash'],
"files": self.get_files_info_json(download)
})

view_size = int(params.get('view_size', '100'))
return RESTResponse({
"infohash": request.match_info['infohash'],
"query": view_start_path,
"files": self.get_files_info_json_paged(download, Path(view_start_path), view_size)
})

@docs(
tags=["Libtorrent"],
summary="Collapse a tree directory.",
parameters=[{
'in': 'path',
'name': 'infohash',
'description': 'Infohash of the download',
'type': 'string',
'required': True
},
{
'in': 'query',
'name': 'path',
'description': 'Path of the directory to collapse',
'type': 'string',
'required': True
}],
responses={
200: {
"schema": schema(File={'path': path})
}
}
)
async def collapse_tree_directory(self, request):
infohash = unhexlify(request.match_info['infohash'])
download = self.download_manager.get_download(infohash)
if not download:
return DownloadsEndpoint.return_404(request)

params = request.query
path = params.get('path')
download.tdef.torrent_file_tree.collapse(Path(path))

return RESTResponse({'path': path})


@docs(
tags=["Libtorrent"],
summary="Expand a tree directory.",
parameters=[{
'in': 'path',
'name': 'infohash',
'description': 'Infohash of the download',
'type': 'string',
'required': True
},
{
'in': 'query',
'name': 'path',
'description': 'Path of the directory to expand',
'type': 'string',
'required': True
}],
responses={
200: {
"schema": schema(File={'path': String})
}
}
)
async def expand_tree_directory(self, request):
infohash = unhexlify(request.match_info['infohash'])
download = self.download_manager.get_download(infohash)
if not download:
return DownloadsEndpoint.return_404(request)

params = request.query
path = params.get('path')
download.tdef.torrent_file_tree.expand(Path(path))

return RESTResponse({'path': path})

@docs(
tags=["Libtorrent"],
summary="Select a tree path.",
parameters=[{
'in': 'path',
'name': 'infohash',
'description': 'Infohash of the download',
'type': 'string',
'required': True
},
{
'in': 'query',
'name': 'path',
'description': 'Path of the directory to select',
'type': 'string',
'required': True
}],
responses={
200: {}
}
)
async def select_tree_path(self, request):
infohash = unhexlify(request.match_info['infohash'])
download = self.download_manager.get_download(infohash)
if not download:
return DownloadsEndpoint.return_404(request)

params = request.query
path = params.get('path')
download.set_selected_file_or_dir(Path(path), True)

return RESTResponse({})

@docs(
tags=["Libtorrent"],
summary="Deselect a tree path.",
parameters=[{
'in': 'path',
'name': 'infohash',
'description': 'Infohash of the download',
'type': 'string',
'required': True
},
{
'in': 'query',
'name': 'path',
'description': 'Path of the directory to deselect',
'type': 'string',
'required': True
}],
responses={
200: {}
}
)
async def deselect_tree_path(self, request):
infohash = unhexlify(request.match_info['infohash'])
download = self.download_manager.get_download(infohash)
if not download:
return DownloadsEndpoint.return_404(request)

params = request.query
path = params.get('path')
download.set_selected_file_or_dir(Path(path), False)

return RESTResponse({})

@docs(
tags=["Libtorrent"],
Expand Down

0 comments on commit 4e25843

Please sign in to comment.