Skip to content

Commit 54d7741

Browse files
authored
Added support for a custom list of default trackers (#8446)
2 parents f6ac7cc + 0faab7b commit 54d7741

File tree

11 files changed

+111
-4
lines changed

11 files changed

+111
-4
lines changed

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

+35-2
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,12 @@
11
from __future__ import annotations
22

3+
import logging
34
import mimetypes
45
from asyncio import get_event_loop, shield
56
from binascii import hexlify, unhexlify
7+
from functools import lru_cache
68
from pathlib import Path, PurePosixPath
9+
from time import time
710
from typing import TYPE_CHECKING, Any, Optional, TypedDict, cast
811

912
import libtorrent as lt
@@ -39,6 +42,7 @@
3942
TOTAL = "total"
4043
LOADED = "loaded"
4144
ALL_LOADED = "all_loaded"
45+
logger = logging.getLogger(__name__)
4246

4347

4448
class JSONFilesInfo(TypedDict):
@@ -53,6 +57,21 @@ class JSONFilesInfo(TypedDict):
5357
progress: float
5458

5559

60+
@lru_cache(maxsize=1)
61+
def cached_read(tracker_file: str, _: int) -> list[bytes]:
62+
"""
63+
Keep one cache for one tracker file at a time (by default: for a max of 120 seconds, see caller).
64+
65+
When adding X torrents at once, this avoids reading the same file X times.
66+
"""
67+
try:
68+
with open(tracker_file, "rb") as f:
69+
return [line.rstrip() for line in f if line.rstrip()] # uTorrent format contains blank lines between URLs
70+
except OSError:
71+
logger.exception("Failed to read tracker file!")
72+
return []
73+
74+
5675
class DownloadsEndpoint(RESTEndpoint):
5776
"""
5877
This endpoint is responsible for all requests regarding downloads. Examples include getting all downloads,
@@ -359,6 +378,17 @@ async def get_downloads(self, request: Request) -> RESTResponse: # noqa: C901
359378
result.append(info)
360379
return RESTResponse({"downloads": result, "checkpoints": checkpoints})
361380

381+
def _get_default_trackers(self) -> list[bytes]:
382+
"""
383+
Get the default trackers from the configured tracker file.
384+
385+
Tracker file format is "(<TRACKER><NEWLINE><NEWLINE>)*". We assume "<TRACKER>" does not include newlines.
386+
"""
387+
tracker_file = self.download_manager.config.get("libtorrent/download_defaults/trackers_file")
388+
if not tracker_file:
389+
return []
390+
return cached_read(tracker_file, int(time())//120)
391+
362392
@docs(
363393
tags=["Libtorrent"],
364394
summary="Start a download from a provided URI.",
@@ -397,7 +427,7 @@ async def get_downloads(self, request: Request) -> RESTResponse: # noqa: C901
397427
"uri*": (String, "The URI of the torrent file that should be downloaded. This URI can either represent a file "
398428
"location, a magnet link or a HTTP(S) url."),
399429
}))
400-
async def add_download(self, request: Request) -> RESTResponse: # noqa: C901
430+
async def add_download(self, request: Request) -> RESTResponse: # noqa: C901, PLR0912
401431
"""
402432
Start a download from a provided URI.
403433
"""
@@ -436,8 +466,11 @@ async def add_download(self, request: Request) -> RESTResponse: # noqa: C901
436466
try:
437467
if tdef:
438468
download = await self.download_manager.start_download(tdef=tdef, config=download_config)
439-
elif uri:
469+
else: # guaranteed to have uri
440470
download = await self.download_manager.start_download_from_uri(uri, config=download_config)
471+
if self.download_manager.config.get("libtorrent/download_defaults/trackers_file"):
472+
await download.get_handle() # We can only add trackers to a valid handle, wait for it.
473+
download.add_trackers(self._get_default_trackers())
441474
except Exception as e:
442475
return RESTResponse({"error": {
443476
"handled": True,

src/tribler/test_unit/core/libtorrent/restapi/test_downloads_endpoint.py

+43-1
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55

66
from io import StringIO
77
from pathlib import Path
8-
from unittest.mock import AsyncMock, Mock, call, patch
8+
from unittest.mock import AsyncMock, Mock, call, mock_open, patch
99

1010
from aiohttp.web_urldispatcher import UrlMappingMatchInfo
1111
from configobj import ConfigObj
@@ -309,6 +309,48 @@ async def test_add_download_failed(self) -> None:
309309
self.assertEqual(HTTP_INTERNAL_SERVER_ERROR, response.status)
310310
self.assertEqual("invalid uri", response_body_json["error"]["message"])
311311

312+
async def test_add_download_with_default_trackers_bad_file(self) -> None:
313+
"""
314+
Test if a bad trackers file is simply ignored.
315+
"""
316+
download = self.create_mock_download()
317+
download.get_handle = AsyncMock(return_value=None)
318+
self.download_manager.start_download_from_uri = AsyncMock(return_value=download)
319+
self.download_manager.config = MockTriblerConfigManager()
320+
self.download_manager.config.set("libtorrent/download_defaults/trackers_file", "testpath.txt")
321+
request = MockRequest("/api/downloads", "PUT", {"uri": "http://127.0.0.1/file"})
322+
323+
with patch("tribler.core.libtorrent.download_manager.download_config.DownloadConfig.from_defaults",
324+
lambda _: download.config):
325+
response = await self.endpoint.add_download(request)
326+
response_body_json = await response_to_json(response)
327+
328+
self.assertEqual(200, response.status)
329+
self.assertTrue(response_body_json["started"])
330+
331+
async def test_add_download_with_default_trackers(self) -> None:
332+
"""
333+
Test if the default trackers are added when adding a download.
334+
"""
335+
download = self.create_mock_download()
336+
download.handle = Mock(is_valid=Mock(return_value=True))
337+
download.get_handle = AsyncMock(return_value=download.handle)
338+
self.download_manager.start_download_from_uri = AsyncMock(return_value=download)
339+
self.download_manager.config = MockTriblerConfigManager()
340+
self.download_manager.config.set("libtorrent/download_defaults/trackers_file", "testpath.txt")
341+
request = MockRequest("/api/downloads", "PUT", {"uri": "http://127.0.0.1/file"})
342+
343+
with patch("tribler.core.libtorrent.download_manager.download_config.DownloadConfig.from_defaults",
344+
lambda _: download.config), patch("builtins.open", mock_open(read_data=b"http://1\n\nudp://2/\n\n")):
345+
response = await self.endpoint.add_download(request)
346+
response_body_json = await response_to_json(response)
347+
348+
self.assertEqual(200, response.status)
349+
self.assertTrue(response_body_json["started"])
350+
self.assertListEqual([call({"url": b"http://1", "verified": False}),
351+
call({"url": b"udp://2/", "verified": False})],
352+
download.handle.add_tracker.call_args_list)
353+
312354
async def test_delete_download_no_remove_data(self) -> None:
313355
"""
314356
Test if a graceful error is returned when no remove data is supplied when deleting a download.

src/tribler/tribler_config.py

+3-1
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,7 @@ class DownloadDefaultsConfig(TypedDict):
7777
seeding_time: float
7878
channel_download: bool
7979
add_download_to_channel: bool
80+
trackers_file: str
8081

8182

8283
class LibtorrentConfig(TypedDict):
@@ -231,7 +232,8 @@ class TriblerConfig(TypedDict):
231232
seeding_ratio=2.0,
232233
seeding_time=60.0,
233234
channel_download=False,
234-
add_download_to_channel=False)
235+
add_download_to_channel=False,
236+
trackers_file="")
235237
),
236238
"recommender": RecommenderConfig(enabled=True),
237239
"rendezvous": RendezvousConfig(enabled=True),

src/tribler/ui/public/locales/en_US.json

+1
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
"CreateTorrent": "Create torrent from file(s)",
2323
"DefaultDownloadSettings": "Default download settings",
2424
"SaveFilesTo": "Save files to:",
25+
"DefaultTrackersFile": "Default trackers file:",
2526
"AlwaysAsk": "Always ask download settings",
2627
"DownloadAnon": "Download anonymously using proxies",
2728
"SeedAnon": "Encrypted anonymous seeding using proxies",

src/tribler/ui/public/locales/es_ES.json

+1
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
"CreateTorrent": "Crear torrent a partir de archivo(s)",
2323
"DefaultDownloadSettings": "Configuración de descargas por defecto",
2424
"SaveFilesTo": "Guardar archivos en:",
25+
"DefaultTrackersFile": "Archivo de rastreadores predeterminados:",
2526
"AlwaysAsk": "¿Preguntar siempre donde guardar las descargas?",
2627
"DownloadAnon": "Descarga anónima mediante proxies",
2728
"SeedAnon": "Siembra anónima cifrada mediante proxies",

src/tribler/ui/public/locales/ko_KR.json

+1
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
"CreateTorrent": "파일에서 토렌트 만들기",
2323
"DefaultDownloadSettings": "기본 내려받기 설정",
2424
"SaveFilesTo": "파일을 저장할 위치 :",
25+
"DefaultTrackersFile": "기본 추적기 파일 :",
2526
"AlwaysAsk": "항상 내려받기 설정 확인",
2627
"DownloadAnon": "프록시를 사용하여 익명으로 내려받기",
2728
"SeedAnon": "프록시를 사용하여 암호화된 익명 시딩",

src/tribler/ui/public/locales/pt_BR.json

+1
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
"CreateTorrent": "Criar torrent de um arquivo(s)",
2323
"DefaultDownloadSettings": "Configuração de download padrão",
2424
"SaveFilesTo": "Salvar arquivos em:",
25+
"DefaultTrackersFile": "Arquivo de rastreadores padrão:",
2526
"AlwaysAsk": "Sempre peça configurações de download",
2627
"DownloadAnon": "Baixe anonimamente usando proxies",
2728
"SeedAnon": "Semear anonimamente usando proxies",

src/tribler/ui/public/locales/ru_RU.json

+1
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
"CreateTorrent": "Создать торрент",
2323
"DefaultDownloadSettings": "Настройки закачки по умолчанию",
2424
"SaveFilesTo": "Сохранять файлы в:",
25+
"DefaultTrackersFile": "Файл трекеров по умолчанию:",
2526
"AlwaysAsk": "Всегда спрашивать настройки закачки",
2627
"DownloadAnon": "Загружать торренты анонимно (через других участников)",
2728
"SeedAnon": "Раздавать торренты анонимно (через других участников)",

src/tribler/ui/public/locales/zh_CN.json

+1
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
"CreateTorrent": "从文件创建种子",
2323
"DefaultDownloadSettings": "默认下载设置",
2424
"SaveFilesTo": "保存文件到:",
25+
"DefaultTrackersFile": "默认跟踪器文件:",
2526
"AlwaysAsk": "总是询问下载设置",
2627
"DownloadAnon": "使用代理匿名下载",
2728
"SeedAnon": "使用代理加密地匿名做种",

src/tribler/ui/src/models/settings.model.tsx

+1
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,7 @@ export interface Settings {
7878
seeding_time: number;
7979
channel_download: boolean;
8080
add_download_to_channel: boolean;
81+
trackers_file: string;
8182
},
8283
},
8384
rendezvous: {

src/tribler/ui/src/pages/Settings/General.tsx

+23
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,29 @@ export default function General() {
8484
}}
8585
/>
8686
</div>
87+
<div className="py-2 flex items-center">
88+
<Label htmlFor="trackers_file" className="whitespace-nowrap pr-5">
89+
{t('DefaultTrackersFile')}
90+
</Label>
91+
<PathInput
92+
path={settings?.libtorrent?.download_defaults?.trackers_file}
93+
directory={false}
94+
onPathChange={(path) => {
95+
if (settings) {
96+
setSettings({
97+
...settings,
98+
libtorrent: {
99+
...settings.libtorrent,
100+
download_defaults: {
101+
...settings.libtorrent.download_defaults,
102+
trackers_file: path
103+
}
104+
}
105+
});
106+
}
107+
}}
108+
/>
109+
</div>
87110
<div className="flex items-center">
88111
<div className="w-64 flex items-center">
89112
<Checkbox

0 commit comments

Comments
 (0)