WebUI: Add support for tracker status filter

PR #22166.
This commit is contained in:
Vladimir Golovnev 2025-05-30 08:32:46 +03:00 committed by GitHub
commit 28c1ba869b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
12 changed files with 211 additions and 41 deletions

View file

@ -120,6 +120,28 @@ namespace
const QString KEY_FULL_UPDATE = u"full_update"_s;
const QString KEY_RESPONSE_ID = u"rid"_s;
const QString KEY_TORRENT_HAS_TRACKER_WARNING = u"has_tracker_warning"_s;
const QString KEY_TORRENT_HAS_TRACKER_ERROR = u"has_tracker_error"_s;
const QString KEY_TORRENT_HAS_OTHER_ANNOUNCE_ERROR = u"has_other_announce_error"_s;
QStringList asStrings(const QSet<BitTorrent::TorrentID> &torrentIDs)
{
QStringList result;
result.reserve(torrentIDs.size());
for (const BitTorrent::TorrentID &torrentID : torrentIDs)
result.emplaceBack(torrentID.toString());
return result;
}
bool hasWarningMessage(const BitTorrent::TrackerEntryStatus &status)
{
return std::ranges::any_of(status.endpoints, [](const BitTorrent::TrackerEndpointStatus &endpointEntry)
{
return (endpointEntry.state == BitTorrent::TrackerEndpointState::Working) && !endpointEntry.message.isEmpty();
});
}
QVariantMap processMap(const QVariantMap &prevData, const QVariantMap &data);
std::pair<QVariantMap, QVariantList> processHash(QVariantHash prevData, const QVariantHash &data);
std::pair<QVariantList, QVariantList> processList(QVariantList prevData, const QVariantList &data);
@ -384,6 +406,39 @@ namespace
return QJsonObject::fromVariantMap(syncData);
}
void addAnnounceStats(QVariantMap &serializedTorrent, const BitTorrent::Torrent *torrent)
{
bool hasTrackerWarning = false;
bool hasTrackerError = false;
bool hasOtherAnnounceError = false;
for (const BitTorrent::TrackerEntryStatus &status : asConst(torrent->trackers()))
{
switch (status.state)
{
case BitTorrent::TrackerEndpointState::Working:
if (!hasTrackerWarning && hasWarningMessage(status))
hasTrackerWarning = true;
break;
case BitTorrent::TrackerEndpointState::TrackerError:
hasTrackerError = true;
break;
case BitTorrent::TrackerEndpointState::NotWorking:
case BitTorrent::TrackerEndpointState::Unreachable:
hasOtherAnnounceError = true;
break;
default:
break;
}
if (hasTrackerWarning && hasTrackerError && hasOtherAnnounceError)
break;
}
serializedTorrent[KEY_TORRENT_HAS_TRACKER_WARNING] = hasTrackerWarning;
serializedTorrent[KEY_TORRENT_HAS_TRACKER_ERROR] = hasTrackerError;
serializedTorrent[KEY_TORRENT_HAS_OTHER_ANNOUNCE_ERROR] = hasOtherAnnounceError;
}
}
SyncController::SyncController(IApplication *app, QObject *parent)
@ -445,6 +500,9 @@ void SyncController::updateFreeDiskSpace(const qint64 freeDiskSpace)
// - "seen_complete": Indicates the time when the torrent was last seen complete/whole
// - "last_activity": Last time when a chunk was downloaded/uploaded
// - "total_size": Size including unwanted data
// - "has_tracker_warning": the torrent has working tracker that has a message
// - "has_tracker_error": the torrent has a tracker error
// - "has_other_announce_error": the torrent has other problems announcing to a tracker
// Server state map may contain the following keys:
// - "connection_status": connection status
// - "dht_nodes": DHT nodes count
@ -488,6 +546,7 @@ void SyncController::maindataAction()
connect(btSession, &BitTorrent::Session::trackersAdded, this, &SyncController::onTorrentTrackersChanged);
connect(btSession, &BitTorrent::Session::trackersRemoved, this, &SyncController::onTorrentTrackersChanged);
connect(btSession, &BitTorrent::Session::trackersChanged, this, &SyncController::onTorrentTrackersChanged);
connect(btSession, &BitTorrent::Session::trackerEntryStatusesUpdated, this, &SyncController::onTorrentTrackerEntryStatusesUpdated);
}
const int acceptedID = params()[u"rid"_s].toInt();
@ -526,6 +585,7 @@ void SyncController::makeMaindataSnapshot()
QVariantMap serializedTorrent = serialize(*torrent);
serializedTorrent.remove(KEY_TORRENT_ID);
addAnnounceStats(serializedTorrent, torrent);
for (const BitTorrent::TrackerEntryStatus &status : asConst(torrent->trackers()))
m_knownTrackers[status.url].insert(torrentID);
@ -547,14 +607,8 @@ void SyncController::makeMaindataSnapshot()
for (const Tag &tag : asConst(session->tags()))
m_maindataSnapshot.tags.append(tag.toString());
for (auto trackersIter = m_knownTrackers.cbegin(); trackersIter != m_knownTrackers.cend(); ++trackersIter)
{
QStringList torrentIDs;
for (const BitTorrent::TorrentID &torrentID : asConst(trackersIter.value()))
torrentIDs.append(torrentID.toString());
m_maindataSnapshot.trackers[trackersIter.key()] = torrentIDs;
}
for (const auto &[tracker, torrentIDs] : m_knownTrackers.asKeyValueRange())
m_maindataSnapshot.trackers[tracker] = asStrings(torrentIDs);
m_maindataSnapshot.serverState = getTransferInfo();
m_maindataSnapshot.serverState[KEY_TRANSFER_FREESPACEONDISK] = m_freeDiskSpace;
@ -579,8 +633,12 @@ QJsonObject SyncController::generateMaindataSyncData(const int id, const bool fu
for (const BitTorrent::TorrentID &torrentID : asConst(m_updatedTorrents))
m_maindataSyncBuf.removedTorrents.removeOne(torrentID.toString());
for (const BitTorrent::TorrentID &torrentID : asConst(m_removedTorrents))
m_maindataSyncBuf.torrents.remove(torrentID.toString());
{
const QString torrentIDStr = torrentID.toString();
m_maindataSyncBuf.torrents.remove(torrentIDStr);
}
for (const QString &tracker : asConst(m_updatedTrackers))
m_maindataSyncBuf.removedTrackers.removeOne(tracker);
@ -635,30 +693,64 @@ QJsonObject SyncController::generateMaindataSyncData(const int id, const bool fu
QVariantMap serializedTorrent = serialize(*torrent);
serializedTorrent.remove(KEY_TORRENT_ID);
auto &torrentSnapshot = m_maindataSnapshot.torrents[torrentID.toString()];
const QString torrentIDStr = torrentID.toString();
auto &torrentSnapshot = m_maindataSnapshot.torrents[torrentIDStr];
if (m_announcedTorrents.contains(torrentID))
{
addAnnounceStats(serializedTorrent, torrent);
}
else
{
serializedTorrent[KEY_TORRENT_HAS_TRACKER_WARNING] = torrentSnapshot[KEY_TORRENT_HAS_TRACKER_WARNING];
serializedTorrent[KEY_TORRENT_HAS_TRACKER_ERROR] = torrentSnapshot[KEY_TORRENT_HAS_TRACKER_ERROR];
serializedTorrent[KEY_TORRENT_HAS_OTHER_ANNOUNCE_ERROR] = torrentSnapshot[KEY_TORRENT_HAS_OTHER_ANNOUNCE_ERROR];
}
if (const QVariantMap syncData = processMap(torrentSnapshot, serializedTorrent); !syncData.isEmpty())
{
m_maindataSyncBuf.torrents[torrentID.toString()] = syncData;
m_maindataSyncBuf.torrents[torrentIDStr] = syncData;
torrentSnapshot = serializedTorrent;
}
}
for (const BitTorrent::TorrentID &torrentID : asConst(m_announcedTorrents))
{
if (m_updatedTorrents.contains(torrentID))
continue;
const BitTorrent::Torrent *torrent = session->getTorrent(torrentID);
Q_ASSERT(torrent);
const QString torrentIDStr = torrentID.toString();
auto &torrentSnapshot = m_maindataSnapshot.torrents[torrentIDStr];
// Only announce stats are changed so don't need to serialize torrent again
QVariantMap serializedTorrent = torrentSnapshot;
addAnnounceStats(serializedTorrent, torrent);
if (const QVariantMap syncData = processMap(torrentSnapshot, serializedTorrent); !syncData.isEmpty())
{
m_maindataSyncBuf.torrents[torrentIDStr] = syncData;
torrentSnapshot = serializedTorrent;
}
}
m_updatedTorrents.clear();
m_announcedTorrents.clear();
for (const BitTorrent::TorrentID &torrentID : asConst(m_removedTorrents))
{
m_maindataSyncBuf.removedTorrents.append(torrentID.toString());
m_maindataSnapshot.torrents.remove(torrentID.toString());
const QString torrentIDStr = torrentID.toString();
m_maindataSyncBuf.removedTorrents.append(torrentIDStr);
m_maindataSnapshot.torrents.remove(torrentIDStr);
}
m_removedTorrents.clear();
for (const QString &tracker : asConst(m_updatedTrackers))
{
const QSet<BitTorrent::TorrentID> torrentIDs = m_knownTrackers[tracker];
QStringList serializedTorrentIDs;
serializedTorrentIDs.reserve(torrentIDs.size());
for (const BitTorrent::TorrentID &torrentID : torrentIDs)
serializedTorrentIDs.append(torrentID.toString());
const QStringList serializedTorrentIDs = asStrings(m_knownTrackers[tracker]);
m_maindataSyncBuf.trackers[tracker] = serializedTorrentIDs;
m_maindataSnapshot.trackers[tracker] = serializedTorrentIDs;
@ -847,6 +939,7 @@ void SyncController::onTorrentAdded(BitTorrent::Torrent *torrent)
m_removedTorrents.remove(torrentID);
m_updatedTorrents.insert(torrentID);
m_announcedTorrents.insert(torrentID);
for (const BitTorrent::TrackerEntryStatus &status : asConst(torrent->trackers()))
{
@ -860,6 +953,7 @@ void SyncController::onTorrentAboutToBeRemoved(BitTorrent::Torrent *torrent)
{
const BitTorrent::TorrentID torrentID = torrent->id();
m_announcedTorrents.remove(torrentID);
m_updatedTorrents.remove(torrentID);
m_removedTorrents.insert(torrentID);
@ -899,6 +993,7 @@ void SyncController::onTorrentMetadataReceived(BitTorrent::Torrent *torrent)
void SyncController::onTorrentStopped(BitTorrent::Torrent *torrent)
{
m_updatedTorrents.insert(torrent->id());
m_announcedTorrents.insert(torrent->id());
}
void SyncController::onTorrentStarted(BitTorrent::Torrent *torrent)
@ -981,4 +1076,12 @@ void SyncController::onTorrentTrackersChanged(BitTorrent::Torrent *torrent)
m_removedTrackers.remove(currentTracker);
}
}
m_announcedTorrents.insert(torrentID);
}
void SyncController::onTorrentTrackerEntryStatusesUpdated(const BitTorrent::Torrent *torrent
, [[maybe_unused]] const QHash<QString, BitTorrent::TrackerEntryStatus> &updatedTrackers)
{
m_announcedTorrents.insert(torrent->id());
}

View file

@ -1,6 +1,6 @@
/*
* Bittorrent Client using Qt and libtorrent.
* Copyright (C) 2018-2023 Vladimir Golovnev <glassez@yandex.ru>
* Copyright (C) 2018-2025 Vladimir Golovnev <glassez@yandex.ru>
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License
@ -38,6 +38,7 @@
namespace BitTorrent
{
class Torrent;
struct TrackerEntryStatus;
}
class SyncController : public APIController
@ -79,6 +80,8 @@ private:
void onTorrentTagRemoved(BitTorrent::Torrent *torrent, const Tag &tag);
void onTorrentsUpdated(const QList<BitTorrent::Torrent *> &torrents);
void onTorrentTrackersChanged(BitTorrent::Torrent *torrent);
void onTorrentTrackerEntryStatusesUpdated(const BitTorrent::Torrent *torrent
, const QHash<QString, BitTorrent::TrackerEntryStatus> &updatedTrackers);
qint64 m_freeDiskSpace = 0;
@ -94,20 +97,24 @@ private:
QSet<QString> m_updatedTrackers;
QSet<QString> m_removedTrackers;
QSet<BitTorrent::TorrentID> m_updatedTorrents;
QSet<BitTorrent::TorrentID> m_announcedTorrents;
QSet<BitTorrent::TorrentID> m_removedTorrents;
struct MaindataSyncBuf
{
QHash<QString, QVariantMap> categories;
QVariantList tags;
QHash<QString, QVariantMap> torrents;
QHash<QString, QStringList> trackers;
QVariantMap serverState;
QStringList removedCategories;
QVariantList tags;
QStringList removedTags;
QHash<QString, QVariantMap> torrents;
QStringList removedTorrents;
QHash<QString, QStringList> trackers;
QStringList removedTrackers;
QVariantMap serverState;
};
MaindataSyncBuf m_maindataSnapshot;

View file

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

View file

@ -0,0 +1 @@
<svg height="32" viewBox="0 0 32 32" width="32" xmlns="http://www.w3.org/2000/svg"><path d="m16.000002 1c-6.3509517 0-11.5000025 4.7958103-11.5000025 10.711067 0 .369709.033485.710286.072579 1.070355.031509.358271.083753.723168.1451609 1.074112 1.316651 7.528273 6.6824576 12.860221 11.2822626 17.144466 4.599805-4.284245 9.965566-9.61598 11.282261-17.144466.061409-.350944.113619-.715841.145157-1.074112.03932-.360069.07258-.700646.07258-1.070355 0-5.9152567-5.149052-10.711067-11.499998-10.711067zm0 6.4296445c2.540473 0 4.600808 1.9152985 4.600808 4.2814225s-2.060404 4.285178-4.600808 4.285178-4.600807-1.919054-4.600807-4.285178 2.060403-4.2814225 4.600807-4.2814225z" fill="#f00" stroke-width="2.59043"/></svg>

After

Width:  |  Height:  |  Size: 716 B

View file

@ -0,0 +1 @@
<svg height="32" viewBox="0 0 32 32" width="32" xmlns="http://www.w3.org/2000/svg"><path d="m16.000002 1c-6.3509517 0-11.5000025 4.7958103-11.5000025 10.711067 0 .369709.033485.710286.072579 1.070355.031509.358271.083753.723168.1451609 1.074112 1.316651 7.528273 6.6824576 12.860221 11.2822626 17.144466 4.599805-4.284245 9.965566-9.61598 11.282261-17.144466.061409-.350944.113619-.715841.145157-1.074112.03932-.360069.07258-.700646.07258-1.070355 0-5.9152567-5.149052-10.711067-11.499998-10.711067zm0 6.4296445c2.540473 0 4.600808 1.9152985 4.600808 4.2814225s-2.060404 4.285178-4.600808 4.285178-4.600807-1.919054-4.600807-4.285178 2.060403-4.2814225 4.600807-4.2814225z" fill="#ff8c00" stroke-width="2.59043"/></svg>

After

Width:  |  Height:  |  Size: 719 B

View file

@ -0,0 +1 @@
<svg height="32" viewBox="0 0 32 32" width="32" xmlns="http://www.w3.org/2000/svg"><path d="m16.000002 1c-6.3509517 0-11.5000025 4.7958103-11.5000025 10.711067 0 .369709.033485.710286.072579 1.070355.031509.358271.083753.723168.1451609 1.074112 1.316651 7.528273 6.6824576 12.860221 11.2822626 17.144466 4.599805-4.284245 9.965566-9.61598 11.282261-17.144466.061409-.350944.113619-.715841.145157-1.074112.03932-.360069.07258-.700646.07258-1.070355 0-5.9152567-5.149052-10.711067-11.499998-10.711067zm0 6.4296445c2.540473 0 4.600808 1.9152985 4.600808 4.2814225s-2.060404 4.285178-4.600808 4.285178-4.600807-1.919054-4.600807-4.285178 2.060403-4.2814225 4.600807-4.2814225z" fill="#808080" stroke-width="2.59043"/></svg>

After

Width:  |  Height:  |  Size: 719 B

View file

@ -161,7 +161,10 @@ let setTagFilter = () => {};
/* Trackers filter */
const TRACKERS_ALL = "b4af0e4c-e76d-4bac-a392-46cbc18d9655";
const TRACKERS_ANNOUNCE_ERROR = "d0b4cad2-9f6f-4e7f-8d4b-f80a103dd436";
const TRACKERS_ERROR = "b551cfc3-64e9-4393-bc88-5d6ea2fab5cc";
const TRACKERS_TRACKERLESS = "e24bd469-ea22-404c-8e2e-a17c82f37ea0";
const TRACKERS_WARNING = "82a702c5-210c-412b-829f-97632d7557e9";
// Map<trackerHost: String, Map<trackerURL: String, torrents: Set>>
const trackerMap = new Map();
@ -685,17 +688,41 @@ window.addEventListener("DOMContentLoaded", (event) => {
const span = trackerFilterItem.firstElementChild;
span.lastChild.textContent = `${text} (${count})`;
switch (host) {
case TRACKERS_ANNOUNCE_ERROR:
case TRACKERS_ERROR:
span.lastElementChild.src = "images/tracker-error.svg";
break;
case TRACKERS_TRACKERLESS:
span.lastElementChild.src = "images/trackerless.svg";
break;
case TRACKERS_WARNING:
span.lastElementChild.src = "images/tracker-warning.svg";
break;
}
return trackerFilterItem;
};
let trackerlessTorrentsCount = 0;
for (const { full_data: { trackers_count: trackersCount } } of torrentsTable.getRowValues()) {
if (trackersCount === 0)
trackerlessTorrentsCount += 1;
let trackerlessCount = 0;
let trackerErrorCount = 0;
let announceErrorCount = 0;
let trackerWarningCount = 0;
for (const { full_data } of torrentsTable.getRowValues()) {
if (full_data.trackers_count === 0)
trackerlessCount += 1;
// counting bools by adding them
trackerErrorCount += full_data.has_tracker_error;
announceErrorCount += full_data.has_other_announce_error;
trackerWarningCount += full_data.has_tracker_warning;
}
trackerFilterList.appendChild(createLink(TRACKERS_ALL, "QBT_TR(All)QBT_TR[CONTEXT=TrackerFiltersList]", torrentsTable.getRowSize()));
trackerFilterList.appendChild(createLink(TRACKERS_TRACKERLESS, "QBT_TR(Trackerless)QBT_TR[CONTEXT=TrackerFiltersList]", trackerlessTorrentsCount));
trackerFilterList.appendChild(createLink(TRACKERS_TRACKERLESS, "QBT_TR(Trackerless)QBT_TR[CONTEXT=TrackerFiltersList]", trackerlessCount));
trackerFilterList.appendChild(createLink(TRACKERS_ERROR, "QBT_TR(Tracker error)QBT_TR[CONTEXT=TrackerFiltersList]", trackerErrorCount));
trackerFilterList.appendChild(createLink(TRACKERS_ANNOUNCE_ERROR, "QBT_TR(Other error)QBT_TR[CONTEXT=TrackerFiltersList]", announceErrorCount));
trackerFilterList.appendChild(createLink(TRACKERS_WARNING, "QBT_TR(Warning)QBT_TR[CONTEXT=TrackerFiltersList]", trackerWarningCount));
// Sort trackers by hostname
const sortedList = [];

View file

@ -620,11 +620,18 @@ window.qBittorrent.ContextMenu ??= (() => {
class TrackersFilterContextMenu extends FilterListContextMenu {
updateMenuItems() {
const id = this.options.element.id;
if ((id !== TRACKERS_ALL) && (id !== TRACKERS_TRACKERLESS))
this.showItem("deleteTracker");
else
this.hideItem("deleteTracker");
switch (this.options.element.id) {
case TRACKERS_ALL:
case TRACKERS_ANNOUNCE_ERROR:
case TRACKERS_ERROR:
case TRACKERS_TRACKERLESS:
case TRACKERS_WARNING:
this.hideItem("deleteTracker");
break;
default:
this.showItem("deleteTracker");
break;
}
this.updateTorrentActions();
}

View file

@ -1558,7 +1558,7 @@ window.qBittorrent.DynamicTable ??= (() => {
};
}
applyFilter(row, filterName, category, tag, tracker, filterTerms) {
applyFilter(row, filterName, category, tag, trackerHost, filterTerms) {
const state = row["full_data"].state;
let inactive = false;
@ -1666,17 +1666,32 @@ window.qBittorrent.DynamicTable ??= (() => {
}
}
switch (tracker) {
switch (trackerHost) {
case TRACKERS_ALL:
break; // do nothing
case TRACKERS_ANNOUNCE_ERROR:
if (!row["full_data"]["has_other_announce_error"])
return false;
break;
case TRACKERS_ERROR:
if (!row["full_data"]["has_tracker_error"])
return false;
break;
case TRACKERS_TRACKERLESS:
if (row["full_data"].trackers_count > 0)
return false;
break;
case TRACKERS_WARNING:
if (!row["full_data"]["has_tracker_warning"])
return false;
break;
default: {
const trackerTorrentMap = trackerMap.get(tracker);
const trackerTorrentMap = trackerMap.get(trackerHost);
if (trackerTorrentMap !== undefined) {
let found = false;
for (const torrents of trackerTorrentMap.values()) {

View file

@ -1093,7 +1093,11 @@ const initializeWindows = () => {
};
deleteTrackerFN = (trackerHost) => {
if ((trackerHost === TRACKERS_ALL) || (trackerHost === TRACKERS_TRACKERLESS))
if ((trackerHost === TRACKERS_ALL)
|| (trackerHost === TRACKERS_ANNOUNCE_ERROR)
|| (trackerHost === TRACKERS_ERROR)
|| (trackerHost === TRACKERS_TRACKERLESS)
|| (trackerHost === TRACKERS_WARNING))
return;
const contentURL = new URL("confirmtrackerdeletion.html", window.location);

View file

@ -74,10 +74,11 @@ window.qBittorrent.PropTrackers ??= (() => {
return;
const selectedTrackers = torrentTrackersTable.selectedRowsIds();
torrentTrackersTable.clear();
const trackers = await response.json();
if (trackers) {
torrentTrackersTable.clear();
trackers.each((tracker) => {
let status;
switch (tracker.status) {
@ -122,7 +123,7 @@ window.qBittorrent.PropTrackers ??= (() => {
})
.finally(() => {
clearTimeout(loadTrackersDataTimer);
loadTrackersDataTimer = loadTrackersData.delay(10000);
loadTrackersDataTimer = loadTrackersData.delay(window.qBittorrent.Client.getSyncMainDataInterval());
});
};

View file

@ -372,6 +372,9 @@
<file>private/images/torrent-start-forced.svg</file>
<file>private/images/torrent-start.svg</file>
<file>private/images/torrent-stop.svg</file>
<file>private/images/tracker-error.svg</file>
<file>private/images/tracker-warning.svg</file>
<file>private/images/trackerless.svg</file>
<file>private/images/trackers.svg</file>
<file>private/images/upload.svg</file>
<file>private/images/view-categories.svg</file>