From 9f88cee7ab856a3eaf98a3480924ac8db4399565 Mon Sep 17 00:00:00 2001 From: qstokkink Date: Fri, 30 Aug 2024 11:42:22 +0200 Subject: [PATCH] Added option to add trackers to a download --- .../libtorrent/download_manager/download.py | 10 +- .../libtorrent/restapi/downloads_endpoint.py | 121 ++++++++++++++++ src/tribler/ui/public/locales/en_US.json | 7 +- src/tribler/ui/public/locales/es_ES.json | 7 +- src/tribler/ui/public/locales/pt_BR.json | 7 +- src/tribler/ui/public/locales/ru_RU.json | 7 +- src/tribler/ui/public/locales/zh_CN.json | 7 +- .../ui/src/pages/Downloads/Trackers.tsx | 136 ++++++++++++++++-- .../ui/src/services/tribler.service.ts | 25 ++++ 9 files changed, 303 insertions(+), 24 deletions(-) diff --git a/src/tribler/core/libtorrent/download_manager/download.py b/src/tribler/core/libtorrent/download_manager/download.py index 0a2f5f0c42..0dd1bde6ea 100644 --- a/src/tribler/core/libtorrent/download_manager/download.py +++ b/src/tribler/core/libtorrent/download_manager/download.py @@ -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") diff --git a/src/tribler/core/libtorrent/restapi/downloads_endpoint.py b/src/tribler/core/libtorrent/restapi/downloads_endpoint.py index 56841c8d80..6acba7f7c5 100644 --- a/src/tribler/core/libtorrent/restapi/downloads_endpoint.py +++ b/src/tribler/core/libtorrent/restapi/downloads_endpoint.py @@ -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), @@ -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.", diff --git a/src/tribler/ui/public/locales/en_US.json b/src/tribler/ui/public/locales/en_US.json index ddbbfaa480..37354cb9bc 100644 --- a/src/tribler/ui/public/locales/en_US.json +++ b/src/tribler/ui/public/locales/en_US.json @@ -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:", @@ -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!" } diff --git a/src/tribler/ui/public/locales/es_ES.json b/src/tribler/ui/public/locales/es_ES.json index ecf1f753f9..f4a3747fb1 100644 --- a/src/tribler/ui/public/locales/es_ES.json +++ b/src/tribler/ui/public/locales/es_ES.json @@ -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:", @@ -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!" } diff --git a/src/tribler/ui/public/locales/pt_BR.json b/src/tribler/ui/public/locales/pt_BR.json index 0e34012aed..381743e5a8 100644 --- a/src/tribler/ui/public/locales/pt_BR.json +++ b/src/tribler/ui/public/locales/pt_BR.json @@ -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:", @@ -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!" } diff --git a/src/tribler/ui/public/locales/ru_RU.json b/src/tribler/ui/public/locales/ru_RU.json index dc96084b7a..25fcdc6900 100644 --- a/src/tribler/ui/public/locales/ru_RU.json +++ b/src/tribler/ui/public/locales/ru_RU.json @@ -106,6 +106,8 @@ "MagnetDialogInputLabel": "Пожалуйста, введите URL/magnet-ссылку в поле ниже:", "MagnetDialogHeader": "Добавить торренты по URL/magnet-ссылке", "MagnetDialogError": "Не удалось обработать URL/магнитную ссылку", + "TrackerDialogInputLabel": "Введите URL-адрес трекера в поле ниже:", + "TrackerDialogHeader": "Добавить торрент с URL-адреса трекера", "Add": "ДОБАВИТЬ", "FilterByName": "Фильтровать по имени", "WithSelected": "С выбранным:", @@ -159,5 +161,8 @@ "ToastErrorDownloadSetHops": "Не удалось изменить анонимность загрузки!", "ToastErrorDownloadSetFiles": "Не удалось установить файлы для скачивания!", "ToastErrorUpgradeFailed": "Обновление не удалось!", - "ToastErrorRemoveVersion": "Не удалось удалить версию {{version}}!" + "ToastErrorRemoveVersion": "Не удалось удалить версию {{version}}!", + "ToastErrorTrackerAdd": "Не удалось добавить трекер!", + "ToastErrorTrackerRemove": "Не удалось удалить трекер!", + "ToastErrorTrackerCheck": "Не удалось проверить трекер!" } diff --git a/src/tribler/ui/public/locales/zh_CN.json b/src/tribler/ui/public/locales/zh_CN.json index c57c078f78..ef00d6aba0 100644 --- a/src/tribler/ui/public/locales/zh_CN.json +++ b/src/tribler/ui/public/locales/zh_CN.json @@ -105,6 +105,8 @@ "MagnetDialogInputLabel": "请在下面字段输入 URL/磁力链接:", "MagnetDialogHeader": "从 URL/磁力链接添加种子文件", "MagnetDialogError": "无法处理 URL/磁力链接", + "TrackerDialogInputLabel": "请在下面的字段中输入跟踪器 URL:", + "TrackerDialogHeader": "从跟踪器 URL 添加 torrent", "Add": "添加", "FilterByName": "按名称过滤", "WithSelected": "已选择:", @@ -158,5 +160,8 @@ "ToastErrorDownloadSetHops": "更改下载匿名失败!", "ToastErrorDownloadSetFiles": "设置文件下载失败!", "ToastErrorUpgradeFailed": "升级失败!", - "ToastErrorRemoveVersion": "删除版本失败 {{version}}!" + "ToastErrorRemoveVersion": "删除版本失败 {{version}}!", + "ToastErrorTrackerAdd": "添加追踪器失败!", + "ToastErrorTrackerRemove": "删除跟踪器失败!", + "ToastErrorTrackerCheck": "检查追踪器失败!" } diff --git a/src/tribler/ui/src/pages/Downloads/Trackers.tsx b/src/tribler/ui/src/pages/Downloads/Trackers.tsx index 47a9d7ce89..488cfb308b 100644 --- a/src/tribler/ui/src/pages/Downloads/Trackers.tsx +++ b/src/tribler/ui/src/pages/Downloads/Trackers.tsx @@ -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[] = [ - { - 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 + const { t } = useTranslation(); + + const [trackerDialogOpen, setTrackerDialogOpen] = useState(false); + const [trackerInput, setTrackerInput] = useState(''); + + const trackerColumns: ColumnDef[] = [ + { + 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) ? <> : + ) + } + }, + { + header: "", + accessorKey: "removeButton", + cell: (props) => { + return (["[DHT]", "[PeX]"].includes(props.row.original.url) ? <> : + ) + } + } + ] + + return ( +
+
+ +
+ +
+ + + + {t('TrackerDialogHeader')} + +
+ {t('TrackerDialogInputLabel')} +
+ setTrackerInput(event.target.value)} + /> +
+
+ + + + + + +
+
+
+ ) } diff --git a/src/tribler/ui/src/services/tribler.service.ts b/src/tribler/ui/src/services/tribler.service.ts index 60d3fefffe..f4b6c687ee 100644 --- a/src/tribler/ui/src/services/tribler.service.ts +++ b/src/tribler/ui/src/services/tribler.service.ts @@ -141,6 +141,31 @@ export class TriblerService { } } + async addDownloadTracker(infohash: string, trackerUrl: string): Promise { + try { + return (await this.http.put(`/downloads/${infohash}/trackers`, { url: trackerUrl })).data.added; + } catch (error) { + return formatAxiosError(error as Error | AxiosError); + } + } + + async removeDownloadTracker(infohash: string, trackerUrl: string): Promise { + try { + return (await this.http.delete(`/downloads/${infohash}/trackers`, { data: { url: trackerUrl } })).data.removed; + } catch (error) { + return formatAxiosError(error as Error | AxiosError); + } + } + + async forceCheckDownloadTracker(infohash: string, trackerUrl: string): Promise { + try { + return (await this.http.put(`/downloads/${infohash}/tracker_force_announce`, { url: trackerUrl })).data.forced; + } catch (error) { + return formatAxiosError(error as Error | AxiosError); + } + + } + // Statistics async getIPv8Statistics(): Promise {