Skip to content

Commit

Permalink
Added option to add trackers to a download
Browse files Browse the repository at this point in the history
  • Loading branch information
qstokkink committed Sep 23, 2024
1 parent cb8e537 commit 9f88cee
Show file tree
Hide file tree
Showing 9 changed files with 303 additions and 24 deletions.
10 changes: 6 additions & 4 deletions src/tribler/core/libtorrent/download_manager/download.py
Original file line number Diff line number Diff line change
Expand Up @@ -749,10 +749,12 @@ def get_tracker_status(self) -> dict[str, tuple[int, str]]:
self.handle = cast(lt.torrent_handle, self.handle)
# Make sure all trackers are in the tracker_status dict
try:
for announce_entry in self.handle.trackers():
url = announce_entry["url"]
if url not in self.tracker_status:
self.tracker_status[url] = (0, "Not contacted yet")
tracker_urls = {tracker["url"] for tracker in self.handle.trackers()}
for removed in (set(self.tracker_status.keys()) - tracker_urls):
self.tracker_status.pop(removed)
for tracker_url in tracker_urls:
if tracker_url not in self.tracker_status:
self.tracker_status[tracker_url] = (0, "Not contacted yet")
except UnicodeDecodeError:
self._logger.warning("UnicodeDecodeError in get_tracker_status")

Expand Down
121 changes: 121 additions & 0 deletions src/tribler/core/libtorrent/restapi/downloads_endpoint.py
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,9 @@ def __init__(self, download_manager: DownloadManager, metadata_store: MetadataSt
web.delete("/{infohash}", self.delete_download),
web.patch("/{infohash}", self.update_download),
web.get("/{infohash}/torrent", self.get_torrent),
web.put("/{infohash}/trackers", self.add_tracker),
web.delete("/{infohash}/trackers", self.remove_tracker),
web.put("/{infohash}/tracker_force_announce", self.tracker_force_announce),
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),
Expand Down Expand Up @@ -612,6 +615,124 @@ async def get_torrent(self, request: Request) -> RESTResponse:
"Content-Disposition": f"attachment; filename={hexlify(infohash).decode()}.torrent"
})

@docs(
tags=["Libtorrent"],
summary="Add a tracker to the specified torrent.",
parameters=[{
"in": "path",
"name": "infohash",
"description": "Infohash of the download to add the given tracker to",
"type": "string",
"required": True
}],
responses={
200: {
"schema": schema(AddTrackerResponse={"added": Boolean}),
"examples": {"added": True}
}
}
)
@json_schema(schema(AddTrackerRequest={
"url": (String, "The tracker URL to insert"),
}))
async def add_tracker(self, request: Request) -> RESTResponse:
"""
Return the .torrent file associated with the specified download.
"""
infohash = unhexlify(request.match_info["infohash"])
download = self.download_manager.get_download(infohash)
if not download:
return DownloadsEndpoint.return_404()

parameters = await request.json()
url = parameters.get("url")
if not url:
return RESTResponse({"error": "url parameter missing"}, status=HTTP_BAD_REQUEST)

download.add_trackers([url])
download.handle.force_reannounce(0, len(download.handle.trackers()) - 1)

return RESTResponse({"added": True})

@docs(
tags=["Libtorrent"],
summary="Remove a tracker from the specified torrent.",
parameters=[{
"in": "path",
"name": "infohash",
"description": "Infohash of the download to remove the given tracker from",
"type": "string",
"required": True
}],
responses={
200: {
"schema": schema(AddTrackerResponse={"removed": Boolean}),
"examples": {"removed": True}
}
}
)
@json_schema(schema(AddTrackerRequest={
"url": (String, "The tracker URL to remove"),
}))
async def remove_tracker(self, request: Request) -> RESTResponse:
"""
Return the .torrent file associated with the specified download.
"""
infohash = unhexlify(request.match_info["infohash"])
download = self.download_manager.get_download(infohash)
if not download:
return DownloadsEndpoint.return_404()

parameters = await request.json()
url = parameters.get("url")
if not url:
return RESTResponse({"error": "url parameter missing"}, status=HTTP_BAD_REQUEST)

download.handle.replace_trackers([tracker for tracker in download.handle.trackers() if tracker["url"] != url])

return RESTResponse({"removed": True})

@docs(
tags=["Libtorrent"],
summary="Forcefully announce to the given tracker.",
parameters=[{
"in": "path",
"name": "infohash",
"description": "Infohash of the download to force the tracker announce for",
"type": "string",
"required": True
}],
responses={
200: {
"schema": schema(AddTrackerResponse={"forced": Boolean}),
"examples": {"forced": True}
}
}
)
@json_schema(schema(AddTrackerRequest={
"url": (String, "The tracker URL to query"),
}))
async def tracker_force_announce(self, request: Request) -> RESTResponse:
"""
Forcefully announce to the given tracker.
"""
infohash = unhexlify(request.match_info["infohash"])
download = self.download_manager.get_download(infohash)
if not download:
return DownloadsEndpoint.return_404()

parameters = await request.json()
url = parameters.get("url")
if not url:
return RESTResponse({"error": "url parameter missing"}, status=HTTP_BAD_REQUEST)

for i, tracker in enumerate(download.handle.trackers()):
if tracker["url"] == url:
download.handle.force_reannounce(0, i)
break

return RESTResponse({"forced": True})

@docs(
tags=["Libtorrent"],
summary="Return file information of a specific download.",
Expand Down
7 changes: 6 additions & 1 deletion src/tribler/ui/public/locales/en_US.json
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,8 @@
"MagnetDialogInputLabel": "Please enter the URL/magnet link in the field below:",
"MagnetDialogHeader": "Add torrent from URL/magnet link",
"MagnetDialogError": "Could not process URL/magnet link",
"TrackerDialogInputLabel": "Please enter the tracker URL in the field below:",
"TrackerDialogHeader": "Add torrent from tracker URL",
"Add": "Add",
"FilterByName": "Filter by name",
"WithSelected": "With selected:",
Expand Down Expand Up @@ -159,5 +161,8 @@
"ToastErrorDownloadSetHops": "Failed change the anonymity of download!",
"ToastErrorDownloadSetFiles": "Failed to set files for download!",
"ToastErrorUpgradeFailed": "Upgrade failed!",
"ToastErrorRemoveVersion": "Failed to remove version {{version}}!"
"ToastErrorRemoveVersion": "Failed to remove version {{version}}!",
"ToastErrorTrackerAdd": "Failed to add tracker!",
"ToastErrorTrackerRemove": "Failed to remove tracker!",
"ToastErrorTrackerCheck": "Failed to check tracker!"
}
7 changes: 6 additions & 1 deletion src/tribler/ui/public/locales/es_ES.json
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,8 @@
"MagnetDialogInputLabel": "Introduzca el enlace URL/magnet en el siguiente recuadro:",
"MagnetDialogHeader": "Añadir torrent desde URL/enlace magnet",
"MagnetDialogError": "No se pudo procesar la URL/enlace magnético",
"TrackerDialogInputLabel": "Introduzca la URL del rastreador en el campo siguiente:",
"TrackerDialogHeader": "Agregar torrent desde la URL del rastreador",
"Add": "AÑADIR",
"FilterByName": "Filtrar por nombre",
"WithSelected": "Con seleccionado:",
Expand Down Expand Up @@ -159,5 +161,8 @@
"ToastErrorDownloadSetHops": "¡Error al cambiar el anonimato de la descarga!",
"ToastErrorDownloadSetFiles": "¡No se pudieron configurar los archivos para descargar!",
"ToastErrorUpgradeFailed": "¡La actualización falló!",
"ToastErrorRemoveVersion": "No se pudo eliminar la versión {{version}}!"
"ToastErrorRemoveVersion": "No se pudo eliminar la versión {{version}}!",
"ToastErrorTrackerAdd": "¡No se pudo agregar el rastreador!",
"ToastErrorTrackerRemove": "¡No se pudo eliminar el rastreador!",
"ToastErrorTrackerCheck": "¡No se pudo verificar el rastreador!"
}
7 changes: 6 additions & 1 deletion src/tribler/ui/public/locales/pt_BR.json
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,8 @@
"MagnetDialogInputLabel": "Por favor adicione o link URL/magnet no campo abaixo:",
"MagnetDialogHeader": "Adicionar torrent de um link URL/magnet",
"MagnetDialogError": "Não foi possível processar URL/link magnético",
"TrackerDialogInputLabel": "Insira o URL do rastreador no campo abaixo:",
"TrackerDialogHeader": "Adicionar torrent do URL do rastreador",
"Add": "ADICIONAR",
"FilterByName": "Filtrar por nome",
"WithSelected": "Com selecionado:",
Expand Down Expand Up @@ -151,5 +153,8 @@
"ToastErrorDownloadSetHops": "Falha ao alterar o anonimato do download!",
"ToastErrorDownloadSetFiles": "Falha ao definir arquivos para download!",
"ToastErrorUpgradeFailed": "Falha na atualização!",
"ToastErrorRemoveVersion": "Falha ao remover a versão {{version}}!"
"ToastErrorRemoveVersion": "Falha ao remover a versão {{version}}!",
"ToastErrorTrackerAdd": "Falha ao adicionar rastreador!",
"ToastErrorTrackerRemove": "Falha ao remover o rastreador!",
"ToastErrorTrackerCheck": "Falha ao verificar o rastreador!"
}
7 changes: 6 additions & 1 deletion src/tribler/ui/public/locales/ru_RU.json
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,8 @@
"MagnetDialogInputLabel": "Пожалуйста, введите URL/magnet-ссылку в поле ниже:",
"MagnetDialogHeader": "Добавить торренты по URL/magnet-ссылке",
"MagnetDialogError": "Не удалось обработать URL/магнитную ссылку",
"TrackerDialogInputLabel": "Введите URL-адрес трекера в поле ниже:",
"TrackerDialogHeader": "Добавить торрент с URL-адреса трекера",
"Add": "ДОБАВИТЬ",
"FilterByName": "Фильтровать по имени",
"WithSelected": "С выбранным:",
Expand Down Expand Up @@ -159,5 +161,8 @@
"ToastErrorDownloadSetHops": "Не удалось изменить анонимность загрузки!",
"ToastErrorDownloadSetFiles": "Не удалось установить файлы для скачивания!",
"ToastErrorUpgradeFailed": "Обновление не удалось!",
"ToastErrorRemoveVersion": "Не удалось удалить версию {{version}}!"
"ToastErrorRemoveVersion": "Не удалось удалить версию {{version}}!",
"ToastErrorTrackerAdd": "Не удалось добавить трекер!",
"ToastErrorTrackerRemove": "Не удалось удалить трекер!",
"ToastErrorTrackerCheck": "Не удалось проверить трекер!"
}
7 changes: 6 additions & 1 deletion src/tribler/ui/public/locales/zh_CN.json
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,8 @@
"MagnetDialogInputLabel": "请在下面字段输入 URL/磁力链接:",
"MagnetDialogHeader": "从 URL/磁力链接添加种子文件",
"MagnetDialogError": "无法处理 URL/磁力链接",
"TrackerDialogInputLabel": "请在下面的字段中输入跟踪器 URL:",
"TrackerDialogHeader": "从跟踪器 URL 添加 torrent",
"Add": "添加",
"FilterByName": "按名称过滤",
"WithSelected": "已选择:",
Expand Down Expand Up @@ -158,5 +160,8 @@
"ToastErrorDownloadSetHops": "更改下载匿名失败!",
"ToastErrorDownloadSetFiles": "设置文件下载失败!",
"ToastErrorUpgradeFailed": "升级失败!",
"ToastErrorRemoveVersion": "删除版本失败 {{version}}!"
"ToastErrorRemoveVersion": "删除版本失败 {{version}}!",
"ToastErrorTrackerAdd": "添加追踪器失败!",
"ToastErrorTrackerRemove": "删除跟踪器失败!",
"ToastErrorTrackerCheck": "检查追踪器失败!"
}
136 changes: 121 additions & 15 deletions src/tribler/ui/src/pages/Downloads/Trackers.tsx
Original file line number Diff line number Diff line change
@@ -1,25 +1,131 @@
import { useState } from "react";
import toast from 'react-hot-toast';
import SimpleTable from "@/components/ui/simple-table";
import { Dialog, DialogClose, DialogContent, DialogFooter, DialogHeader, DialogTitle } from "@/components/ui/dialog";
import { translateHeader } from "@/lib/utils";
import { Download } from "@/models/download.model";
import { Tracker } from "@/models/tracker.model ";
import { ColumnDef } from "@tanstack/react-table";
import { Input } from "@/components/ui/input";
import { Button } from "@/components/ui/button";
import { triblerService } from "@/services/tribler.service";
import { isErrorDict } from "@/services/reporting";
import { useTranslation } from "react-i18next";


const trackerColumns: ColumnDef<Tracker>[] = [
{
accessorKey: "url",
header: translateHeader('Name'),
},
{
accessorKey: "status",
header: translateHeader('Status'),
},
{
accessorKey: "peers",
header: translateHeader('Peers'),
},
]
interface TrackerRow extends Tracker {
recheckButton: typeof Button;
removeButton: typeof Button;
}

export default function Trackers({ download }: { download: Download }) {
return <SimpleTable data={download.trackers} columns={trackerColumns} />
const { t } = useTranslation();

const [trackerDialogOpen, setTrackerDialogOpen] = useState<boolean>(false);
const [trackerInput, setTrackerInput] = useState('');

const trackerColumns: ColumnDef<TrackerRow>[] = [
{
accessorKey: "url",
header: translateHeader('Name'),
},
{
accessorKey: "status",
header: translateHeader('Status'),
},
{
accessorKey: "peers",
header: translateHeader('Peers'),
},
{
header: "",
accessorKey: "recheckButton",
cell: (props) => {
return (["[DHT]", "[PeX]"].includes(props.row.original.url) ? <></> :
<Button variant="secondary" className="max-h-6" onClick={(event) => {
triblerService.forceCheckDownloadTracker(download.infohash, props.row.original.url).then((response) => {
if (response === undefined) {
toast.error(`${"ToastErrorTrackerCheck"} ${"ToastErrorGenNetworkErr"}`);
} else if (isErrorDict(response)){
toast.error(`${"ToastErrorTrackerCheck"} ${response.error}`);
}
});
}}>{t("ForceRecheck")}</Button>)
}
},
{
header: "",
accessorKey: "removeButton",
cell: (props) => {
return (["[DHT]", "[PeX]"].includes(props.row.original.url) ? <></> :
<Button variant="secondary" className="max-h-6" onClick={(event) => {
triblerService.removeDownloadTracker(download.infohash, props.row.original.url).then((response) => {
if (response === undefined) {
toast.error(`${"ToastErrorTrackerRemove"} ${"ToastErrorGenNetworkErr"}`);
} else if (isErrorDict(response)){
toast.error(`${"ToastErrorTrackerRemove"} ${response.error}`);
} else {
download.trackers = download.trackers.filter(tracker => {return tracker.url != props.row.original.url});
var button = event.target as HTMLButtonElement;
button.disabled = true;
button.classList.add("cursor-not-allowed");
button.classList.add("opacity-50");
}
});
}}></Button>)
}
}
]

return (
<div>
<div className="flex-col items-center grid-cols-1">
<SimpleTable data={download.trackers as TrackerRow[]} columns={trackerColumns} />
<div className="flex-none h-1 min-h-1 max-h-1 bg-secondary"></div>
<Button className="flex-none mx-4 my-2 min-w-24 max-h-8" variant="secondary" onClick={() => {setTrackerDialogOpen(true)}}>{t('Add')}</Button>
</div>
<Dialog open={trackerDialogOpen} onOpenChange={setTrackerDialogOpen}>
<DialogContent>
<DialogHeader>
<DialogTitle>{t('TrackerDialogHeader')}</DialogTitle>
</DialogHeader>
<div className="grid gap-1 py-4 text-sm">
{t('TrackerDialogInputLabel')}
<div className="grid grid-cols-6 items-center gap-4">
<Input
id="uri"
className="col-span-5 pt-0"
value={trackerInput}
onChange={(event) => setTrackerInput(event.target.value)}
/>
</div>
</div>
<DialogFooter>
<Button
variant="outline"
type="submit"
onClick={() => {
if (trackerInput) {
triblerService.addDownloadTracker(download.infohash, trackerInput).then((response) => {
if (response === undefined) {
toast.error(`${"ToastErrorTrackerAdd"} ${"ToastErrorGenNetworkErr"}`);
} else if (isErrorDict(response)){
toast.error(`${"ToastErrorTrackerAdd"} ${response.error}`);
}
});
setTrackerDialogOpen(false);
}
}}>
{t('Add')}
</Button>
<DialogClose asChild>
<Button variant="outline" type="button">
{t('Cancel')}
</Button>
</DialogClose>
</DialogFooter>
</DialogContent>
</Dialog>
</div>
)
}
Loading

0 comments on commit 9f88cee

Please sign in to comment.