From cf5c899a13fa7a58367494893814ade4b51690df Mon Sep 17 00:00:00 2001 From: Thomas Piccirello Date: Tue, 1 Jul 2025 08:43:42 -0700 Subject: [PATCH] WebUI: Support reannouncing individual trackers --- WebAPI_Changelog.md | 2 + src/webui/api/torrentscontroller.cpp | 26 ++++++++++-- src/webui/www/private/index.html | 2 + src/webui/www/private/scripts/dynamicTable.js | 5 +++ .../www/private/scripts/prop-trackers.js | 40 +++++++++++++++++++ 5 files changed, 72 insertions(+), 3 deletions(-) diff --git a/WebAPI_Changelog.md b/WebAPI_Changelog.md index 4dd39c4a8..ac372fa79 100644 --- a/WebAPI_Changelog.md +++ b/WebAPI_Changelog.md @@ -4,6 +4,8 @@ * [#22932](https://github.com/qbittorrent/qBittorrent/pull/22932) * `torrents/categories` and `sync/maindata` now serialize categories' `downloadPath` to `null`, rather than `undefined` +* [#22954](https://github.com/qbittorrent/qBittorrent/pull/22954) + * `torrents/reannounce` supports specifying individual trackers via `trackers` field ## 2.11.9 diff --git a/src/webui/api/torrentscontroller.cpp b/src/webui/api/torrentscontroller.cpp index bb3e98a31..310f7c405 100644 --- a/src/webui/api/torrentscontroller.cpp +++ b/src/webui/api/torrentscontroller.cpp @@ -1618,10 +1618,30 @@ void TorrentsController::reannounceAction() requireParams({u"hashes"_s}); const QStringList hashes {params()[u"hashes"_s].split(u'|')}; - applyToTorrents(hashes, [](BitTorrent::Torrent *const torrent) + const QStringList urlsParam {params()[u"urls"_s].split(u'|', Qt::SkipEmptyParts)}; + + QSet urls; + urls.reserve(urlsParam.size()); + for (const QString &urlStr : urlsParam) + urls << QUrl::fromPercentEncoding(urlStr.toLatin1()); + + applyToTorrents(hashes, [&urls](BitTorrent::Torrent *const torrent) { - torrent->forceReannounce(); - torrent->forceDHTAnnounce(); + if (urls.isEmpty()) + { + torrent->forceReannounce(); + torrent->forceDHTAnnounce(); + } + else + { + const QList &trackers = torrent->trackers(); + for (qsizetype i = 0; i < trackers.size(); ++i) + { + const BitTorrent::TrackerEntryStatus &status = trackers.at(i); + if (urls.contains(status.url)) + torrent->forceReannounce(i); + } + } }); setResult(QString()); diff --git a/src/webui/www/private/index.html b/src/webui/www/private/index.html index cabeb72d0..8ba14dd6a 100644 --- a/src/webui/www/private/index.html +++ b/src/webui/www/private/index.html @@ -248,6 +248,8 @@
  • QBT_TR(Edit tracker URL...)QBT_TR[CONTEXT=TrackerListWidget] QBT_TR(Edit tracker URL...)QBT_TR[CONTEXT=TrackerListWidget]
  • QBT_TR(Remove tracker)QBT_TR[CONTEXT=TrackerListWidget] QBT_TR(Remove tracker)QBT_TR[CONTEXT=TrackerListWidget]
  • QBT_TR(Copy tracker URL)QBT_TR[CONTEXT=TrackerListWidget] QBT_TR(Copy tracker URL)QBT_TR[CONTEXT=TrackerListWidget]
  • +
  • QBT_TR(Copy tracker URL)QBT_TR[CONTEXT=TrackerListWidget] QBT_TR(Force reannounce to selected tracker(s))QBT_TR[CONTEXT=TrackerListWidget]
  • +
  • QBT_TR(Copy tracker URL)QBT_TR[CONTEXT=TrackerListWidget] QBT_TR(Force reannounce to all trackers)QBT_TR[CONTEXT=TrackerListWidget]
    • QBT_TR(Add peers...)QBT_TR[CONTEXT=PeerListWidget] QBT_TR(Add peers...)QBT_TR[CONTEXT=PeerListWidget]
    • diff --git a/src/webui/www/private/scripts/dynamicTable.js b/src/webui/www/private/scripts/dynamicTable.js index 032a79a1e..3d3f656a8 100644 --- a/src/webui/www/private/scripts/dynamicTable.js +++ b/src/webui/www/private/scripts/dynamicTable.js @@ -1818,6 +1818,11 @@ window.qBittorrent.DynamicTable ??= (() => { onSelectedRowChanged() { updatePropertiesPanel(); } + + isStopped(hash) { + const row = this.getRow(hash); + return (row === undefined) ? true : row.full_data.state.includes("stopped"); + } } class TorrentPeersTable extends DynamicTable { diff --git a/src/webui/www/private/scripts/prop-trackers.js b/src/webui/www/private/scripts/prop-trackers.js index 079a61673..688ee6e61 100644 --- a/src/webui/www/private/scripts/prop-trackers.js +++ b/src/webui/www/private/scripts/prop-trackers.js @@ -148,6 +148,12 @@ window.qBittorrent.PropTrackers ??= (() => { }, RemoveTracker: (element, ref) => { removeTrackerFN(element); + }, + ReannounceTrackers: (element, ref) => { + reannounceTrackersFN(element, torrentTrackersTable.selectedRowsIds()); + }, + ReannounceAllTrackers: (element, ref) => { + reannounceTrackersFN(element, []); } }, offsets: { @@ -164,6 +170,8 @@ window.qBittorrent.PropTrackers ??= (() => { this.hideItem("EditTracker"); this.hideItem("RemoveTracker"); this.hideItem("CopyTrackerUrl"); + this.hideItem("ReannounceTrackers"); + this.hideItem("ReannounceAllTrackers"); } else { if (selectedTrackers.length === 1) @@ -173,6 +181,16 @@ window.qBittorrent.PropTrackers ??= (() => { this.showItem("RemoveTracker"); this.showItem("CopyTrackerUrl"); + + const torrentHash = torrentsTable.getCurrentTorrentID(); + if (torrentsTable.isStopped(torrentHash)) { + this.hideItem("ReannounceTrackers"); + this.hideItem("ReannounceAllTrackers"); + } + else { + this.showItem("ReannounceTrackers"); + this.showItem("ReannounceAllTrackers"); + } } } }); @@ -253,6 +271,28 @@ window.qBittorrent.PropTrackers ??= (() => { }); }; + const reannounceTrackersFN = (element, trackers) => { + if (current_hash.length === 0) + return; + + const body = new URLSearchParams({ + hashes: current_hash + }); + if (trackers.length > 0) + body.set("urls", trackers.map(encodeURIComponent).join("|")); + + fetch("api/v2/torrents/reannounce", { + method: "POST", + body: body + }) + .then((response) => { + if (!response.ok) + return; + + updateData(); + }); + }; + const clear = () => { torrentTrackersTable.clear(); };