WebUI: Implement missing tracker list features

Implemented: Tracker endpoints in the list, missing "Tracker Error" and "Unreachable" status, "Next Announce" and "Min Announce" column and double click to edit tracker url.

PR #23045.
This commit is contained in:
tehcneko 2025-08-11 16:20:58 +08:00 committed by GitHub
commit a265ba7fd2
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 271 additions and 39 deletions

View file

@ -1,5 +1,12 @@
# WebAPI Changelog # WebAPI Changelog
## 2.13.0
* [#23045](https://github.com/qbittorrent/qBittorrent/pull/23045)
* `torrents/trackers` returns three new fields: `next_announce`, `min_announce` and `endpoints`
* `endpoints` is an array of tracker endpoints, each with `name`, `updating`, `status`, `msg`, `bt_version`, `num_peers`, `num_peers`, `num_leeches`, `num_downloaded`, `next_announce` and `min_announce` fields
* `torrents/trackers` now returns `5` and `6` in `status` field as possible values
* `5` for `Tracker error` and `6` for `Unreachable`
## 2.12.1 ## 2.12.1
* [#23031](https://github.com/qbittorrent/qBittorrent/pull/23031) * [#23031](https://github.com/qbittorrent/qBittorrent/pull/23031)
* Add `torrents/setComment` endpoint with parameters `hashes` and `comment` for setting a new torrent comment * Add `torrents/setComment` endpoint with parameters `hashes` and `comment` for setting a new torrent comment

View file

@ -28,6 +28,8 @@
#include "torrentscontroller.h" #include "torrentscontroller.h"
#include <algorithm>
#include <chrono>
#include <concepts> #include <concepts>
#include <functional> #include <functional>
@ -67,14 +69,19 @@
// Tracker keys // Tracker keys
const QString KEY_TRACKER_URL = u"url"_s; const QString KEY_TRACKER_URL = u"url"_s;
const QString KEY_TRACKER_NAME = u"name"_s;
const QString KEY_TRACKER_UPDATING = u"updating"_s; const QString KEY_TRACKER_UPDATING = u"updating"_s;
const QString KEY_TRACKER_STATUS = u"status"_s; const QString KEY_TRACKER_STATUS = u"status"_s;
const QString KEY_TRACKER_TIER = u"tier"_s; const QString KEY_TRACKER_TIER = u"tier"_s;
const QString KEY_TRACKER_MSG = u"msg"_s; const QString KEY_TRACKER_MSG = u"msg"_s;
const QString KEY_TRACKER_BT_VERSION = u"bt_version"_s;
const QString KEY_TRACKER_PEERS_COUNT = u"num_peers"_s; const QString KEY_TRACKER_PEERS_COUNT = u"num_peers"_s;
const QString KEY_TRACKER_SEEDS_COUNT = u"num_seeds"_s; const QString KEY_TRACKER_SEEDS_COUNT = u"num_seeds"_s;
const QString KEY_TRACKER_LEECHES_COUNT = u"num_leeches"_s; const QString KEY_TRACKER_LEECHES_COUNT = u"num_leeches"_s;
const QString KEY_TRACKER_DOWNLOADED_COUNT = u"num_downloaded"_s; const QString KEY_TRACKER_DOWNLOADED_COUNT = u"num_downloaded"_s;
const QString KEY_TRACKER_NEXT_ANNOUNCE = u"next_announce"_s;
const QString KEY_TRACKER_MIN_ANNOUNCE = u"min_announce"_s;
const QString KEY_TRACKER_ENDPOINTS = u"endpoints"_s;
// Web seed keys // Web seed keys
const QString KEY_WEBSEED_URL = u"url"_s; const QString KEY_WEBSEED_URL = u"url"_s;
@ -269,24 +276,52 @@ namespace
QJsonArray getTrackers(const BitTorrent::Torrent *const torrent) QJsonArray getTrackers(const BitTorrent::Torrent *const torrent)
{ {
const auto now = std::chrono::system_clock::now();
const auto timepointNow = BitTorrent::AnnounceTimePoint::clock::now();
const auto toSecondsSinceEpoch = [&now, &timepointNow](const BitTorrent::AnnounceTimePoint &time) -> qint64
{
const auto timeEpoch = (now + (time - timepointNow)).time_since_epoch();
return std::chrono::duration_cast<std::chrono::seconds>(timeEpoch).count();
};
QJsonArray trackerList; QJsonArray trackerList;
for (const BitTorrent::TrackerEntryStatus &tracker : asConst(torrent->trackers())) for (const BitTorrent::TrackerEntryStatus &tracker : asConst(torrent->trackers()))
{ {
const bool isNotWorking = (tracker.state == BitTorrent::TrackerEndpointState::NotWorking) QJsonArray endpointsList;
|| (tracker.state == BitTorrent::TrackerEndpointState::TrackerError)
|| (tracker.state == BitTorrent::TrackerEndpointState::Unreachable); for (const BitTorrent::TrackerEndpointStatus &endpoint : tracker.endpoints)
{
endpointsList << QJsonObject
{
{KEY_TRACKER_NAME, endpoint.name},
{KEY_TRACKER_UPDATING, endpoint.isUpdating},
{KEY_TRACKER_STATUS, static_cast<int>(endpoint.state)},
{KEY_TRACKER_MSG, endpoint.message},
{KEY_TRACKER_BT_VERSION, static_cast<int>(endpoint.btVersion)},
{KEY_TRACKER_PEERS_COUNT, endpoint.numPeers},
{KEY_TRACKER_SEEDS_COUNT, endpoint.numSeeds},
{KEY_TRACKER_LEECHES_COUNT, endpoint.numLeeches},
{KEY_TRACKER_DOWNLOADED_COUNT, endpoint.numDownloaded},
{KEY_TRACKER_NEXT_ANNOUNCE, toSecondsSinceEpoch(endpoint.nextAnnounceTime)},
{KEY_TRACKER_MIN_ANNOUNCE, toSecondsSinceEpoch(endpoint.minAnnounceTime)}
};
}
trackerList << QJsonObject trackerList << QJsonObject
{ {
{KEY_TRACKER_URL, tracker.url}, {KEY_TRACKER_URL, tracker.url},
{KEY_TRACKER_TIER, tracker.tier}, {KEY_TRACKER_TIER, tracker.tier},
{KEY_TRACKER_UPDATING, tracker.isUpdating}, {KEY_TRACKER_UPDATING, tracker.isUpdating},
{KEY_TRACKER_STATUS, static_cast<int>((isNotWorking ? BitTorrent::TrackerEndpointState::NotWorking : tracker.state))}, {KEY_TRACKER_STATUS, static_cast<int>(tracker.state)},
{KEY_TRACKER_MSG, tracker.message}, {KEY_TRACKER_MSG, tracker.message},
{KEY_TRACKER_PEERS_COUNT, tracker.numPeers}, {KEY_TRACKER_PEERS_COUNT, tracker.numPeers},
{KEY_TRACKER_SEEDS_COUNT, tracker.numSeeds}, {KEY_TRACKER_SEEDS_COUNT, tracker.numSeeds},
{KEY_TRACKER_LEECHES_COUNT, tracker.numLeeches}, {KEY_TRACKER_LEECHES_COUNT, tracker.numLeeches},
{KEY_TRACKER_DOWNLOADED_COUNT, tracker.numDownloaded} {KEY_TRACKER_DOWNLOADED_COUNT, tracker.numDownloaded},
{KEY_TRACKER_NEXT_ANNOUNCE, toSecondsSinceEpoch(tracker.nextAnnounceTime)},
{KEY_TRACKER_MIN_ANNOUNCE, toSecondsSinceEpoch(tracker.minAnnounceTime)},
{KEY_TRACKER_ENDPOINTS, endpointsList}
}; };
} }

View file

@ -53,7 +53,7 @@
#include "base/utils/version.h" #include "base/utils/version.h"
#include "api/isessionmanager.h" #include "api/isessionmanager.h"
inline const Utils::Version<3, 2> API_VERSION {2, 12, 1}; inline const Utils::Version<3, 2> API_VERSION {2, 13, 0};
class APIController; class APIController;
class AuthController; class AuthController;

View file

@ -259,11 +259,11 @@
</ul> </ul>
<ul id="torrentTrackersMenu" class="contextMenu"> <ul id="torrentTrackersMenu" class="contextMenu">
<li><a href="#AddTracker"><img src="images/list-add.svg" alt="QBT_TR(Add trackers...)QBT_TR[CONTEXT=TrackerListWidget]"> QBT_TR(Add trackers...)QBT_TR[CONTEXT=TrackerListWidget]</a></li> <li><a href="#AddTracker"><img src="images/list-add.svg" alt="QBT_TR(Add trackers...)QBT_TR[CONTEXT=TrackerListWidget]"> QBT_TR(Add trackers...)QBT_TR[CONTEXT=TrackerListWidget]</a></li>
<li class="separator"><a href="#EditTracker"><img src="images/edit-rename.svg" alt="QBT_TR(Edit tracker URL...)QBT_TR[CONTEXT=TrackerListWidget]"> QBT_TR(Edit tracker URL...)QBT_TR[CONTEXT=TrackerListWidget]</a></li> <li><a href="#EditTracker"><img src="images/edit-rename.svg" alt="QBT_TR(Edit tracker URL...)QBT_TR[CONTEXT=TrackerListWidget]"> QBT_TR(Edit tracker URL...)QBT_TR[CONTEXT=TrackerListWidget]</a></li>
<li><a href="#RemoveTracker"><img src="images/list-remove.svg" alt="QBT_TR(Remove tracker)QBT_TR[CONTEXT=TrackerListWidget]"> QBT_TR(Remove tracker)QBT_TR[CONTEXT=TrackerListWidget]</a></li> <li><a href="#RemoveTracker"><img src="images/list-remove.svg" alt="QBT_TR(Remove tracker)QBT_TR[CONTEXT=TrackerListWidget]"> QBT_TR(Remove tracker)QBT_TR[CONTEXT=TrackerListWidget]</a></li>
<li><a href="#CopyTrackerUrl" id="CopyTrackerUrl"><img src="images/edit-copy.svg" alt="QBT_TR(Copy tracker URL)QBT_TR[CONTEXT=TrackerListWidget]"> QBT_TR(Copy tracker URL)QBT_TR[CONTEXT=TrackerListWidget]</a></li> <li><a href="#CopyTrackerUrl" id="CopyTrackerUrl"><img src="images/edit-copy.svg" alt="QBT_TR(Copy tracker URL)QBT_TR[CONTEXT=TrackerListWidget]"> QBT_TR(Copy tracker URL)QBT_TR[CONTEXT=TrackerListWidget]</a></li>
<li><a href="#ReannounceTrackers" id="ReannounceTrackers"><img src="images/view-refresh.svg" alt="QBT_TR(Copy tracker URL)QBT_TR[CONTEXT=TrackerListWidget]"> QBT_TR(Force reannounce to selected tracker(s))QBT_TR[CONTEXT=TrackerListWidget]</a></li> <li><a href="#ReannounceTrackers" id="ReannounceTrackers"><img src="images/reannounce.svg" alt="QBT_TR(Force reannounce to selected tracker(s))QBT_TR[CONTEXT=TrackerListWidget]"> QBT_TR(Force reannounce to selected tracker(s))QBT_TR[CONTEXT=TrackerListWidget]</a></li>
<li class="separator"><a href="#ReannounceAllTrackers" id="ReannounceAllTrackers"><img src="images/view-refresh.svg" alt="QBT_TR(Copy tracker URL)QBT_TR[CONTEXT=TrackerListWidget]"> QBT_TR(Force reannounce to all trackers)QBT_TR[CONTEXT=TrackerListWidget]</a></li> <li class="separator"><a href="#ReannounceAllTrackers" id="ReannounceAllTrackers"><img src="images/reannounce.svg" alt="QBT_TR(Force reannounce to all trackers)QBT_TR[CONTEXT=TrackerListWidget]"> QBT_TR(Force reannounce to all trackers)QBT_TR[CONTEXT=TrackerListWidget]</a></li>
</ul> </ul>
<ul id="torrentPeersMenu" class="contextMenu"> <ul id="torrentPeersMenu" class="contextMenu">
<li><a href="#addPeer"><img src="images/peers-add.svg" alt="QBT_TR(Add peers...)QBT_TR[CONTEXT=PeerListWidget]"> QBT_TR(Add peers...)QBT_TR[CONTEXT=PeerListWidget]</a></li> <li><a href="#addPeer"><img src="images/peers-add.svg" alt="QBT_TR(Add peers...)QBT_TR[CONTEXT=PeerListWidget]"> QBT_TR(Add peers...)QBT_TR[CONTEXT=PeerListWidget]</a></li>

View file

@ -2078,15 +2078,68 @@ window.qBittorrent.DynamicTable ??= (() => {
} }
class TorrentTrackersTable extends DynamicTable { class TorrentTrackersTable extends DynamicTable {
collapseState = new Map(); // { rowId: String, isCollapsed: bool }
isTrackerCollapsed(id) {
return this.collapseState.get(id) ?? true;
}
toggleTrackerCollapsed(id) {
this.collapseState.set(id, !this.isTrackerCollapsed(id));
this.#updateTrackerRowState(id, this.isTrackerCollapsed(id));
}
#updateEndpointVisibility(endpoint, shouldHide) {
const span = document.getElementById(`trackersTableTrackerUrl${endpoint}`);
// span won't exist if row has been filtered out
if (span === null)
return;
const tr = span.parentElement.parentElement;
tr.classList.toggle("invisible", shouldHide);
}
#updateTrackerCollapseIcon(tracker, isCollapsed) {
const span = document.getElementById(`trackersTableTrackerUrl${tracker}`);
// span won't exist if row has been filtered out
if (span === null)
return;
const td = span.parentElement;
// rotate the collapse icon
const collapseIcon = td.firstElementChild;
collapseIcon.classList.toggle("rotate", isCollapsed);
}
#updateTrackerRowState(id, shouldCollapse) {
// collapsed rows will be filtered out when using virtual list
if (this.useVirtualList)
return;
this.#updateTrackerCollapseIcon(id, shouldCollapse);
for (const row of this.getRowValues()) {
const parentId = row.full_data._tracker;
if (parentId === id)
this.#updateEndpointVisibility(row.rowId, shouldCollapse);
}
}
clearCollapseState() {
this.collapseState.clear();
}
initColumns() { initColumns() {
this.newColumn("url", "", "QBT_TR(URL/Announce Endpoint)QBT_TR[CONTEXT=TrackerListWidget]", 250, true);
this.newColumn("tier", "", "QBT_TR(Tier)QBT_TR[CONTEXT=TrackerListWidget]", 35, true); this.newColumn("tier", "", "QBT_TR(Tier)QBT_TR[CONTEXT=TrackerListWidget]", 35, true);
this.newColumn("url", "", "QBT_TR(URL)QBT_TR[CONTEXT=TrackerListWidget]", 250, true); this.newColumn("btVersion", "", "QBT_TR(BT Protocol)QBT_TR[CONTEXT=TrackerListWidget]", 35, true);
this.newColumn("status", "", "QBT_TR(Status)QBT_TR[CONTEXT=TrackerListWidget]", 125, true); this.newColumn("status", "", "QBT_TR(Status)QBT_TR[CONTEXT=TrackerListWidget]", 125, true);
this.newColumn("peers", "", "QBT_TR(Peers)QBT_TR[CONTEXT=TrackerListWidget]", 75, true); this.newColumn("peers", "", "QBT_TR(Peers)QBT_TR[CONTEXT=TrackerListWidget]", 75, true);
this.newColumn("seeds", "", "QBT_TR(Seeds)QBT_TR[CONTEXT=TrackerListWidget]", 75, true); this.newColumn("seeds", "", "QBT_TR(Seeds)QBT_TR[CONTEXT=TrackerListWidget]", 75, true);
this.newColumn("leeches", "", "QBT_TR(Leeches)QBT_TR[CONTEXT=TrackerListWidget]", 75, true); this.newColumn("leeches", "", "QBT_TR(Leeches)QBT_TR[CONTEXT=TrackerListWidget]", 75, true);
this.newColumn("downloaded", "", "QBT_TR(Times Downloaded)QBT_TR[CONTEXT=TrackerListWidget]", 100, true); this.newColumn("downloaded", "", "QBT_TR(Times Downloaded)QBT_TR[CONTEXT=TrackerListWidget]", 100, true);
this.newColumn("message", "", "QBT_TR(Message)QBT_TR[CONTEXT=TrackerListWidget]", 250, true); this.newColumn("message", "", "QBT_TR(Message)QBT_TR[CONTEXT=TrackerListWidget]", 250, true);
this.newColumn("nextAnnounce", "", "QBT_TR(Next Announce)QBT_TR[CONTEXT=TrackerListWidget]", 150, true);
this.newColumn("minAnnounce", "", "QBT_TR(Min Announce)QBT_TR[CONTEXT=TrackerListWidget]", 150, true);
this.initColumnsFunctions(); this.initColumnsFunctions();
} }
@ -2101,6 +2154,43 @@ window.qBittorrent.DynamicTable ??= (() => {
return window.qBittorrent.Misc.naturalSortCollator.compare(value1, value2); return window.qBittorrent.Misc.naturalSortCollator.compare(value1, value2);
}; };
this.columns["url"].updateTd = (td, row) => {
const id = row.rowId;
const data = row.full_data;
let collapseIcon = td.firstElementChild;
if (collapseIcon === null) {
collapseIcon = document.createElement("img");
collapseIcon.src = "images/go-down.svg";
collapseIcon.className = "filesTableCollapseIcon";
collapseIcon.addEventListener("click", (e) => {
const id = collapseIcon.dataset.id;
this.toggleTrackerCollapsed(id);
if (this.useVirtualList)
this.rerender();
});
td.append(collapseIcon);
}
if (data._isTracker) {
collapseIcon.style.display = "inline";
collapseIcon.style.visibility = data._hasEndpoints ? "visible" : "hidden";
collapseIcon.dataset.id = id;
collapseIcon.classList.toggle("rotate", this.isTrackerCollapsed(id));
}
else {
collapseIcon.style.display = "none";
}
let span = td.children[1];
if (span === undefined) {
span = document.createElement("span");
td.append(span);
}
span.id = `trackersTableTrackerUrl${id}`;
span.textContent = data.url;
span.style.marginLeft = data._isTracker ? "0" : "20px";
};
this.columns["url"].compareRows = naturalSort; this.columns["url"].compareRows = naturalSort;
this.columns["status"].compareRows = naturalSort; this.columns["status"].compareRows = naturalSort;
this.columns["message"].compareRows = naturalSort; this.columns["message"].compareRows = naturalSort;
@ -2155,6 +2245,8 @@ window.qBittorrent.DynamicTable ??= (() => {
statusClass = "trackerUpdating"; statusClass = "trackerUpdating";
break; break;
case "QBT_TR(Not working)QBT_TR[CONTEXT=TrackerListWidget]": case "QBT_TR(Not working)QBT_TR[CONTEXT=TrackerListWidget]":
case "QBT_TR(Tracker error)QBT_TR[CONTEXT=TrackerListWidget]":
case "QBT_TR(Unreachable)QBT_TR[CONTEXT=TrackerListWidget]":
statusClass = "trackerNotWorking"; statusClass = "trackerNotWorking";
break; break;
} }
@ -2167,6 +2259,76 @@ window.qBittorrent.DynamicTable ??= (() => {
td.textContent = status; td.textContent = status;
td.title = status; td.title = status;
}; };
const friendlyDuration = function(td, row) {
const value = this.getRowValue(row) ?? 0;
const seconds = Math.max(value - (Date.now() / 1000), 0);
const duration = window.qBittorrent.Misc.friendlyDuration(seconds, window.qBittorrent.Misc.MAX_ETA);
td.textContent = duration;
td.title = duration;
};
this.columns["nextAnnounce"].updateTd = friendlyDuration;
this.columns["minAnnounce"].updateTd = friendlyDuration;
}
getFilteredAndSortedRows() {
const trackers = [];
const trakcerEndpoints = new Map();
for (const row of this.getRowValues()) {
const tracker = row.full_data._tracker;
if (tracker) {
if (this.useVirtualList && this.isTrackerCollapsed(tracker))
continue;
const endpoints = trakcerEndpoints.get(tracker);
if (endpoints === undefined)
trakcerEndpoints.set(tracker, [row]);
else
endpoints.push(row);
}
else {
trackers.push(row);
}
}
const column = this.columns[this.sortedColumn];
const isReverseSort = this.reverseSort === "0";
const sortRows = (row1, row2) => {
const result = column.compareRows(row1, row2);
return isReverseSort ? result : -result;
};
const result = [];
for (const tracker of trackers.sort(sortRows)) {
result.push(tracker);
const endpoints = trakcerEndpoints.get(tracker.rowId) || [];
result.push(...endpoints.sort(sortRows));
}
return result;
}
updateTable(fullUpdate = false) {
super.updateTable(fullUpdate);
if (!this.useVirtualList) {
for (const row of this.getRowValues()) {
if (row.full_data._isTracker)
continue;
this.#updateEndpointVisibility(row.rowId, this.isTrackerCollapsed(row.full_data._tracker));
}
}
}
setupCommonEvents() {
super.setupCommonEvents();
this.dynamicTableDiv.addEventListener("dblclick", (e) => {
const tr = e.target.closest("tr");
if (!tr || (tr.rowId.startsWith("** [") || tr.rowId.startsWith("endpoint|")))
return;
window.qBittorrent.PropTrackers.editTracker(tr);
});
} }
} }

View file

@ -32,6 +32,7 @@ window.qBittorrent ??= {};
window.qBittorrent.PropTrackers ??= (() => { window.qBittorrent.PropTrackers ??= (() => {
const exports = () => { const exports = () => {
return { return {
editTracker: editTrackerFN,
updateData: updateData, updateData: updateData,
clear: clear clear: clear
}; };
@ -42,6 +43,25 @@ window.qBittorrent.PropTrackers ??= (() => {
const torrentTrackersTable = new window.qBittorrent.DynamicTable.TorrentTrackersTable(); const torrentTrackersTable = new window.qBittorrent.DynamicTable.TorrentTrackersTable();
let loadTrackersDataTimer = -1; let loadTrackersDataTimer = -1;
const trackerStatusText = (tracker) => {
if (tracker.updating)
return "QBT_TR(Updating...)QBT_TR[CONTEXT=TrackerListWidget]";
switch (tracker.status) {
case 0:
return "QBT_TR(Disabled)QBT_TR[CONTEXT=TrackerListWidget]";
case 1:
return "QBT_TR(Not contacted yet)QBT_TR[CONTEXT=TrackerListWidget]";
case 2:
return "QBT_TR(Working)QBT_TR[CONTEXT=TrackerListWidget]";
case 4:
return "QBT_TR(Not working)QBT_TR[CONTEXT=TrackerListWidget]";
case 5:
return "QBT_TR(Tracker error)QBT_TR[CONTEXT=TrackerListWidget]";
case 6:
return "QBT_TR(Unreachable)QBT_TR[CONTEXT=TrackerListWidget]";
}
};
const loadTrackersData = () => { const loadTrackersData = () => {
if (document.hidden) if (document.hidden)
return; return;
@ -53,11 +73,13 @@ window.qBittorrent.PropTrackers ??= (() => {
const new_hash = torrentsTable.getCurrentTorrentID(); const new_hash = torrentsTable.getCurrentTorrentID();
if (new_hash === "") { if (new_hash === "") {
torrentTrackersTable.clear(); torrentTrackersTable.clear();
torrentTrackersTable.clearCollapseState();
clearTimeout(loadTrackersDataTimer); clearTimeout(loadTrackersDataTimer);
return; return;
} }
if (new_hash !== current_hash) { if (new_hash !== current_hash) {
torrentTrackersTable.clear(); torrentTrackersTable.clear();
torrentTrackersTable.clearCollapseState();
current_hash = new_hash; current_hash = new_hash;
} }
@ -79,43 +101,50 @@ window.qBittorrent.PropTrackers ??= (() => {
if (trackers) { if (trackers) {
torrentTrackersTable.clear(); torrentTrackersTable.clear();
const notApplicable = "QBT_TR(N/A)QBT_TR[CONTEXT=TrackerListWidget]";
trackers.each((tracker) => { trackers.each((tracker) => {
let status;
if (tracker.updating) {
status = "QBT_TR(Updating...)QBT_TR[CONTEXT=TrackerListWidget]";
}
else {
switch (tracker.status) {
case 0:
status = "QBT_TR(Disabled)QBT_TR[CONTEXT=TrackerListWidget]";
break;
case 1:
status = "QBT_TR(Not contacted yet)QBT_TR[CONTEXT=TrackerListWidget]";
break;
case 2:
status = "QBT_TR(Working)QBT_TR[CONTEXT=TrackerListWidget]";
break;
case 4:
status = "QBT_TR(Not working)QBT_TR[CONTEXT=TrackerListWidget]";
break;
}
}
const row = { const row = {
rowId: tracker.url, rowId: tracker.url,
tier: (tracker.tier >= 0) ? tracker.tier : "", tier: (tracker.tier >= 0) ? tracker.tier : "",
btVersion: "",
url: tracker.url, url: tracker.url,
status: status, status: trackerStatusText(tracker),
peers: (tracker.num_peers >= 0) ? tracker.num_peers : "QBT_TR(N/A)QBT_TR[CONTEXT=TrackerListWidget]", peers: (tracker.num_peers >= 0) ? tracker.num_peers : notApplicable,
seeds: (tracker.num_seeds >= 0) ? tracker.num_seeds : "QBT_TR(N/A)QBT_TR[CONTEXT=TrackerListWidget]", seeds: (tracker.num_seeds >= 0) ? tracker.num_seeds : notApplicable,
leeches: (tracker.num_leeches >= 0) ? tracker.num_leeches : "QBT_TR(N/A)QBT_TR[CONTEXT=TrackerListWidget]", leeches: (tracker.num_leeches >= 0) ? tracker.num_leeches : notApplicable,
downloaded: (tracker.num_downloaded >= 0) ? tracker.num_downloaded : "QBT_TR(N/A)QBT_TR[CONTEXT=TrackerListWidget]", downloaded: (tracker.num_downloaded >= 0) ? tracker.num_downloaded : notApplicable,
message: tracker.msg, message: tracker.msg,
nextAnnounce: tracker.next_announce,
minAnnounce: tracker.min_announce,
_isTracker: true,
_hasEndpoints: tracker.endpoints && (tracker.endpoints.length > 0),
_sortable: !tracker.url.startsWith("** [") _sortable: !tracker.url.startsWith("** [")
}; };
torrentTrackersTable.updateRowData(row); torrentTrackersTable.updateRowData(row);
if (tracker.endpoints !== undefined) {
for (const endpoint of tracker.endpoints) {
const row = {
rowId: `endpoint|${tracker.url}|${endpoint.name}|${endpoint.bt_version}`,
tier: "",
btVersion: `v${endpoint.bt_version}`,
url: endpoint.name,
status: trackerStatusText(endpoint),
peers: (endpoint.num_peers >= 0) ? endpoint.num_peers : notApplicable,
seeds: (endpoint.num_seeds >= 0) ? endpoint.num_seeds : notApplicable,
leeches: (endpoint.num_leeches >= 0) ? endpoint.num_leeches : notApplicable,
downloaded: (endpoint.num_downloaded >= 0) ? endpoint.num_downloaded : notApplicable,
message: endpoint.msg,
nextAnnounce: endpoint.next_announce,
minAnnounce: endpoint.min_announce,
_isTracker: false,
_tracker: tracker.url,
_sortable: true,
};
torrentTrackersTable.updateRowData(row);
}
}
}); });
torrentTrackersTable.updateTable(false); torrentTrackersTable.updateTable(false);
@ -163,7 +192,7 @@ window.qBittorrent.PropTrackers ??= (() => {
onShow: function() { onShow: function() {
const selectedTrackers = torrentTrackersTable.selectedRowsIds(); const selectedTrackers = torrentTrackersTable.selectedRowsIds();
const containsStaticTracker = selectedTrackers.some((tracker) => { const containsStaticTracker = selectedTrackers.some((tracker) => {
return tracker.startsWith("** ["); return tracker.startsWith("** [") || tracker.startsWith("endpoint|");
}); });
if (containsStaticTracker || (selectedTrackers.length === 0)) { if (containsStaticTracker || (selectedTrackers.length === 0)) {
@ -171,7 +200,6 @@ window.qBittorrent.PropTrackers ??= (() => {
this.hideItem("RemoveTracker"); this.hideItem("RemoveTracker");
this.hideItem("CopyTrackerUrl"); this.hideItem("CopyTrackerUrl");
this.hideItem("ReannounceTrackers"); this.hideItem("ReannounceTrackers");
this.hideItem("ReannounceAllTrackers");
} }
else { else {
if (selectedTrackers.length === 1) if (selectedTrackers.length === 1)