Revise Tracker related classes
[qBittorrent.git] / src / gui / trackerlist / trackerlistmodel.cpp
blob92fb8acb30513298ae8681176c1d1429517118ec
1 /*
2 * Bittorrent Client using Qt and libtorrent.
3 * Copyright (C) 2023 Vladimir Golovnev <glassez@yandex.ru>
4 * Copyright (C) 2006 Christophe Dumez <chris@qbittorrent.org>
6 * This program is free software; you can redistribute it and/or
7 * modify it under the terms of the GNU General Public License
8 * as published by the Free Software Foundation; either version 2
9 * of the License, or (at your option) any later version.
11 * This program is distributed in the hope that it will be useful,
12 * but WITHOUT ANY WARRANTY; without even the implied warranty of
13 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 * GNU General Public License for more details.
16 * You should have received a copy of the GNU General Public License
17 * along with this program; if not, write to the Free Software
18 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
20 * In addition, as a special exception, the copyright holders give permission to
21 * link this program with the OpenSSL project's "OpenSSL" library (or with
22 * modified versions of it that use the same license as the "OpenSSL" library),
23 * and distribute the linked executables. You must obey the GNU General Public
24 * License in all respects for all of the code used other than "OpenSSL". If you
25 * modify file(s), you may extend this exception to your version of the file(s),
26 * but you are not obligated to do so. If you do not wish to do so, delete this
27 * exception statement from your version.
30 #include "trackerlistmodel.h"
32 #include <chrono>
34 #include <boost/multi_index_container.hpp>
35 #include <boost/multi_index/composite_key.hpp>
36 #include <boost/multi_index/hashed_index.hpp>
37 #include <boost/multi_index/indexed_by.hpp>
38 #include <boost/multi_index/member.hpp>
39 #include <boost/multi_index/random_access_index.hpp>
40 #include <boost/multi_index/tag.hpp>
42 #include <QColor>
43 #include <QList>
44 #include <QPointer>
45 #include <QScopeGuard>
46 #include <QTimer>
48 #include "base/bittorrent/peerinfo.h"
49 #include "base/bittorrent/session.h"
50 #include "base/bittorrent/torrent.h"
51 #include "base/bittorrent/trackerentry.h"
52 #include "base/global.h"
53 #include "base/utils/misc.h"
55 using namespace std::chrono_literals;
56 using namespace boost::multi_index;
58 namespace
60 const std::chrono::milliseconds ANNOUNCE_TIME_REFRESH_INTERVAL = 4s;
62 const char STR_WORKING[] = QT_TRANSLATE_NOOP("TrackerListModel", "Working");
63 const char STR_DISABLED[] = QT_TRANSLATE_NOOP("TrackerListModel", "Disabled");
64 const char STR_TORRENT_DISABLED[] = QT_TRANSLATE_NOOP("TrackerListModel", "Disabled for this torrent");
65 const char STR_PRIVATE_MSG[] = QT_TRANSLATE_NOOP("TrackerListModel", "This torrent is private");
67 QString prettyCount(const int val)
69 return (val > -1) ? QString::number(val) : TrackerListModel::tr("N/A");
72 QString toString(const BitTorrent::TrackerEndpointState state)
74 switch (state)
76 case BitTorrent::TrackerEndpointState::Working:
77 return TrackerListModel::tr(STR_WORKING);
78 case BitTorrent::TrackerEndpointState::Updating:
79 return TrackerListModel::tr("Updating...");
80 case BitTorrent::TrackerEndpointState::NotWorking:
81 return TrackerListModel::tr("Not working");
82 case BitTorrent::TrackerEndpointState::TrackerError:
83 return TrackerListModel::tr("Tracker error");
84 case BitTorrent::TrackerEndpointState::Unreachable:
85 return TrackerListModel::tr("Unreachable");
86 case BitTorrent::TrackerEndpointState::NotContacted:
87 return TrackerListModel::tr("Not contacted yet");
89 return TrackerListModel::tr("Invalid state!");
92 QString statusDHT(const BitTorrent::Torrent *torrent)
94 if (!torrent->session()->isDHTEnabled())
95 return TrackerListModel::tr(STR_DISABLED);
97 if (torrent->isPrivate() || torrent->isDHTDisabled())
98 return TrackerListModel::tr(STR_TORRENT_DISABLED);
100 return TrackerListModel::tr(STR_WORKING);
103 QString statusPeX(const BitTorrent::Torrent *torrent)
105 if (!torrent->session()->isPeXEnabled())
106 return TrackerListModel::tr(STR_DISABLED);
108 if (torrent->isPrivate() || torrent->isPEXDisabled())
109 return TrackerListModel::tr(STR_TORRENT_DISABLED);
111 return TrackerListModel::tr(STR_WORKING);
114 QString statusLSD(const BitTorrent::Torrent *torrent)
116 if (!torrent->session()->isLSDEnabled())
117 return TrackerListModel::tr(STR_DISABLED);
119 if (torrent->isPrivate() || torrent->isLSDDisabled())
120 return TrackerListModel::tr(STR_TORRENT_DISABLED);
122 return TrackerListModel::tr(STR_WORKING);
126 std::size_t hash_value(const QString &string)
128 return qHash(string);
131 struct TrackerListModel::Item final
133 QString name {};
134 int tier = -1;
135 int btVersion = -1;
136 BitTorrent::TrackerEndpointState status = BitTorrent::TrackerEndpointState::NotContacted;
137 QString message {};
139 int numPeers = -1;
140 int numSeeds = -1;
141 int numLeeches = -1;
142 int numDownloaded = -1;
144 QDateTime nextAnnounceTime {};
145 QDateTime minAnnounceTime {};
147 qint64 secsToNextAnnounce = 0;
148 qint64 secsToMinAnnounce = 0;
149 QDateTime announceTimestamp;
151 std::weak_ptr<Item> parentItem {};
153 multi_index_container<std::shared_ptr<Item>, indexed_by<
154 random_access<>,
155 hashed_unique<tag<struct ByID>, composite_key<
156 Item,
157 member<Item, QString, &Item::name>,
158 member<Item, int, &Item::btVersion>
160 >> childItems {};
162 Item(QStringView name, QStringView message);
163 explicit Item(const BitTorrent::TrackerEntryStatus &trackerEntryStatus);
164 Item(const std::shared_ptr<Item> &parentItem, const BitTorrent::TrackerEndpointStatus &endpointStatus);
166 void fillFrom(const BitTorrent::TrackerEntryStatus &trackerEntryStatus);
167 void fillFrom(const BitTorrent::TrackerEndpointStatus &endpointStatus);
170 class TrackerListModel::Items final : public multi_index_container<
171 std::shared_ptr<Item>,
172 indexed_by<
173 random_access<>,
174 hashed_unique<tag<struct ByName>, member<Item, QString, &Item::name>>>>
178 TrackerListModel::Item::Item(const QStringView name, const QStringView message)
179 : name {name.toString()}
180 , message {message.toString()}
184 TrackerListModel::Item::Item(const BitTorrent::TrackerEntryStatus &trackerEntryStatus)
185 : name {trackerEntryStatus.url}
187 fillFrom(trackerEntryStatus);
190 TrackerListModel::Item::Item(const std::shared_ptr<Item> &parentItem, const BitTorrent::TrackerEndpointStatus &endpointStatus)
191 : name {endpointStatus.name}
192 , btVersion {endpointStatus.btVersion}
193 , parentItem {parentItem}
195 fillFrom(endpointStatus);
198 void TrackerListModel::Item::fillFrom(const BitTorrent::TrackerEntryStatus &trackerEntryStatus)
200 Q_ASSERT(parentItem.expired());
201 Q_ASSERT(trackerEntryStatus.url == name);
203 tier = trackerEntryStatus.tier;
204 status = trackerEntryStatus.state;
205 message = trackerEntryStatus.message;
206 numPeers = trackerEntryStatus.numPeers;
207 numSeeds = trackerEntryStatus.numSeeds;
208 numLeeches = trackerEntryStatus.numLeeches;
209 numDownloaded = trackerEntryStatus.numDownloaded;
210 nextAnnounceTime = trackerEntryStatus.nextAnnounceTime;
211 minAnnounceTime = trackerEntryStatus.minAnnounceTime;
212 secsToNextAnnounce = 0;
213 secsToMinAnnounce = 0;
214 announceTimestamp = QDateTime();
217 void TrackerListModel::Item::fillFrom(const BitTorrent::TrackerEndpointStatus &endpointStatus)
219 Q_ASSERT(!parentItem.expired());
220 Q_ASSERT(endpointStatus.name == name);
221 Q_ASSERT(endpointStatus.btVersion == btVersion);
223 status = endpointStatus.state;
224 message = endpointStatus.message;
225 numPeers = endpointStatus.numPeers;
226 numSeeds = endpointStatus.numSeeds;
227 numLeeches = endpointStatus.numLeeches;
228 numDownloaded = endpointStatus.numDownloaded;
229 nextAnnounceTime = endpointStatus.nextAnnounceTime;
230 minAnnounceTime = endpointStatus.minAnnounceTime;
231 secsToNextAnnounce = 0;
232 secsToMinAnnounce = 0;
233 announceTimestamp = QDateTime();
236 TrackerListModel::TrackerListModel(BitTorrent::Session *btSession, QObject *parent)
237 : QAbstractItemModel(parent)
238 , m_btSession {btSession}
239 , m_items {std::make_unique<Items>()}
240 , m_announceRefreshTimer {new QTimer(this)}
242 Q_ASSERT(m_btSession);
244 m_announceRefreshTimer->setSingleShot(true);
245 connect(m_announceRefreshTimer, &QTimer::timeout, this, &TrackerListModel::refreshAnnounceTimes);
247 connect(m_btSession, &BitTorrent::Session::trackersAdded, this
248 , [this](BitTorrent::Torrent *torrent, const QList<BitTorrent::TrackerEntry> &newTrackers)
250 if (torrent == m_torrent)
251 onTrackersAdded(newTrackers);
253 connect(m_btSession, &BitTorrent::Session::trackersRemoved, this
254 , [this](BitTorrent::Torrent *torrent, const QStringList &deletedTrackers)
256 if (torrent == m_torrent)
257 onTrackersRemoved(deletedTrackers);
259 connect(m_btSession, &BitTorrent::Session::trackersChanged, this
260 , [this](BitTorrent::Torrent *torrent)
262 if (torrent == m_torrent)
263 onTrackersChanged();
265 connect(m_btSession, &BitTorrent::Session::trackerEntryStatusesUpdated, this
266 , [this](BitTorrent::Torrent *torrent, const QHash<QString, BitTorrent::TrackerEntryStatus> &updatedTrackers)
268 if (torrent == m_torrent)
269 onTrackersUpdated(updatedTrackers);
273 TrackerListModel::~TrackerListModel() = default;
275 void TrackerListModel::setTorrent(BitTorrent::Torrent *torrent)
277 beginResetModel();
278 [[maybe_unused]] const auto modelResetGuard = qScopeGuard([this] { endResetModel(); });
280 if (m_torrent)
281 m_items->clear();
283 m_torrent = torrent;
285 if (m_torrent)
286 populate();
287 else
288 m_announceRefreshTimer->stop();
291 BitTorrent::Torrent *TrackerListModel::torrent() const
293 return m_torrent;
296 void TrackerListModel::populate()
298 Q_ASSERT(m_torrent);
300 const QList<BitTorrent::TrackerEntryStatus> trackers = m_torrent->trackers();
301 m_items->reserve(trackers.size() + STICKY_ROW_COUNT);
303 const QString &privateTorrentMessage = m_torrent->isPrivate() ? tr(STR_PRIVATE_MSG) : u""_s;
304 m_items->emplace_back(std::make_shared<Item>(u"** [DHT] **", privateTorrentMessage));
305 m_items->emplace_back(std::make_shared<Item>(u"** [PeX] **", privateTorrentMessage));
306 m_items->emplace_back(std::make_shared<Item>(u"** [LSD] **", privateTorrentMessage));
308 using TorrentPtr = QPointer<const BitTorrent::Torrent>;
309 m_torrent->fetchPeerInfo([this, torrent = TorrentPtr(m_torrent)](const QList<BitTorrent::PeerInfo> &peers)
311 if (torrent != m_torrent)
312 return;
314 // XXX: libtorrent should provide this info...
315 // Count peers from DHT, PeX, LSD
316 uint seedsDHT = 0, seedsPeX = 0, seedsLSD = 0, peersDHT = 0, peersPeX = 0, peersLSD = 0;
317 for (const BitTorrent::PeerInfo &peer : peers)
319 if (peer.isConnecting())
320 continue;
322 if (peer.isSeed())
324 if (peer.fromDHT())
325 ++seedsDHT;
327 if (peer.fromPeX())
328 ++seedsPeX;
330 if (peer.fromLSD())
331 ++seedsLSD;
333 else
335 if (peer.fromDHT())
336 ++peersDHT;
338 if (peer.fromPeX())
339 ++peersPeX;
341 if (peer.fromLSD())
342 ++peersLSD;
346 auto &itemsByPos = m_items->get<0>();
347 itemsByPos.modify((itemsByPos.begin() + ROW_DHT), [&seedsDHT, &peersDHT](std::shared_ptr<Item> &item)
349 item->numSeeds = seedsDHT;
350 item->numLeeches = peersDHT;
351 return true;
353 itemsByPos.modify((itemsByPos.begin() + ROW_PEX), [&seedsPeX, &peersPeX](std::shared_ptr<Item> &item)
355 item->numSeeds = seedsPeX;
356 item->numLeeches = peersPeX;
357 return true;
359 itemsByPos.modify((itemsByPos.begin() + ROW_LSD), [&seedsLSD, &peersLSD](std::shared_ptr<Item> &item)
361 item->numSeeds = seedsLSD;
362 item->numLeeches = peersLSD;
363 return true;
366 emit dataChanged(index(ROW_DHT, COL_SEEDS), index(ROW_LSD, COL_LEECHES));
369 for (const BitTorrent::TrackerEntryStatus &status : trackers)
370 addTrackerItem(status);
372 m_announceTimestamp = QDateTime::currentDateTime();
373 m_announceRefreshTimer->start(ANNOUNCE_TIME_REFRESH_INTERVAL);
376 std::shared_ptr<TrackerListModel::Item> TrackerListModel::createTrackerItem(const BitTorrent::TrackerEntryStatus &trackerEntryStatus)
378 const auto item = std::make_shared<Item>(trackerEntryStatus);
379 for (const auto &[id, endpointStatus] : trackerEntryStatus.endpoints.asKeyValueRange())
380 item->childItems.emplace_back(std::make_shared<Item>(item, endpointStatus));
382 return item;
385 void TrackerListModel::addTrackerItem(const BitTorrent::TrackerEntryStatus &trackerEntryStatus)
387 [[maybe_unused]] const auto &[iter, res] = m_items->emplace_back(createTrackerItem(trackerEntryStatus));
388 Q_ASSERT(res);
391 void TrackerListModel::updateTrackerItem(const std::shared_ptr<Item> &item, const BitTorrent::TrackerEntryStatus &trackerEntryStatus)
393 QSet<std::pair<QString, int>> endpointItemIDs;
394 QList<std::shared_ptr<Item>> newEndpointItems;
395 for (const auto &[id, endpointStatus] : trackerEntryStatus.endpoints.asKeyValueRange())
397 endpointItemIDs.insert(id);
399 auto &itemsByID = item->childItems.get<ByID>();
400 if (const auto &iter = itemsByID.find(std::make_tuple(id.first, id.second)); iter != itemsByID.end())
402 (*iter)->fillFrom(endpointStatus);
404 else
406 newEndpointItems.emplace_back(std::make_shared<Item>(item, endpointStatus));
410 const auto &itemsByPos = m_items->get<0>();
411 const auto trackerRow = std::distance(itemsByPos.begin(), itemsByPos.iterator_to(item));
412 const auto trackerIndex = index(trackerRow, 0);
414 auto it = item->childItems.begin();
415 while (it != item->childItems.end())
417 if (const auto endpointItemID = std::make_pair((*it)->name, (*it)->btVersion)
418 ; endpointItemIDs.contains(endpointItemID))
420 ++it;
422 else
424 const auto row = std::distance(item->childItems.begin(), it);
425 beginRemoveRows(trackerIndex, row, row);
426 it = item->childItems.erase(it);
427 endRemoveRows();
431 const int numRows = rowCount(trackerIndex);
432 emit dataChanged(index(0, 0, trackerIndex), index((numRows - 1), (columnCount(trackerIndex) - 1), trackerIndex));
434 if (!newEndpointItems.isEmpty())
436 beginInsertRows(trackerIndex, numRows, (numRows + newEndpointItems.size() - 1));
437 for (const auto &newEndpointItem : asConst(newEndpointItems))
438 item->childItems.get<0>().push_back(newEndpointItem);
439 endInsertRows();
442 item->fillFrom(trackerEntryStatus);
443 emit dataChanged(trackerIndex, index(trackerRow, (columnCount() - 1)));
446 void TrackerListModel::refreshAnnounceTimes()
448 if (!m_torrent)
449 return;
451 m_announceTimestamp = QDateTime::currentDateTime();
452 emit dataChanged(index(0, COL_NEXT_ANNOUNCE), index((rowCount() - 1), COL_MIN_ANNOUNCE));
453 for (int i = 0; i < rowCount(); ++i)
455 const QModelIndex parentIndex = index(i, 0);
456 emit dataChanged(index(0, COL_NEXT_ANNOUNCE, parentIndex), index((rowCount(parentIndex) - 1), COL_MIN_ANNOUNCE, parentIndex));
459 m_announceRefreshTimer->start(ANNOUNCE_TIME_REFRESH_INTERVAL);
462 int TrackerListModel::columnCount([[maybe_unused]] const QModelIndex &parent) const
464 return COL_COUNT;
467 int TrackerListModel::rowCount(const QModelIndex &parent) const
469 if (!parent.isValid())
470 return m_items->size();
472 const auto *item = static_cast<Item *>(parent.internalPointer());
473 Q_ASSERT(item);
474 if (!item) [[unlikely]]
475 return 0;
477 return item->childItems.size();
480 QVariant TrackerListModel::headerData(const int section, const Qt::Orientation orientation, const int role) const
482 if (orientation != Qt::Horizontal)
483 return {};
485 switch (role)
487 case Qt::DisplayRole:
488 switch (section)
490 case COL_URL:
491 return tr("URL/Announce endpoint");
492 case COL_TIER:
493 return tr("Tier");
494 case COL_PROTOCOL:
495 return tr("Protocol");
496 case COL_STATUS:
497 return tr("Status");
498 case COL_PEERS:
499 return tr("Peers");
500 case COL_SEEDS:
501 return tr("Seeds");
502 case COL_LEECHES:
503 return tr("Leeches");
504 case COL_TIMES_DOWNLOADED:
505 return tr("Times Downloaded");
506 case COL_MSG:
507 return tr("Message");
508 case COL_NEXT_ANNOUNCE:
509 return tr("Next announce");
510 case COL_MIN_ANNOUNCE:
511 return tr("Min announce");
512 default:
513 return {};
516 case Qt::TextAlignmentRole:
517 switch (section)
519 case COL_TIER:
520 case COL_PEERS:
521 case COL_SEEDS:
522 case COL_LEECHES:
523 case COL_TIMES_DOWNLOADED:
524 case COL_NEXT_ANNOUNCE:
525 case COL_MIN_ANNOUNCE:
526 return QVariant {Qt::AlignRight | Qt::AlignVCenter};
527 default:
528 return {};
531 default:
532 return {};
536 QVariant TrackerListModel::data(const QModelIndex &index, const int role) const
538 if (!index.isValid())
539 return {};
541 auto *itemPtr = static_cast<Item *>(index.internalPointer());
542 Q_ASSERT(itemPtr);
543 if (!itemPtr) [[unlikely]]
544 return {};
546 if (itemPtr->announceTimestamp != m_announceTimestamp)
548 itemPtr->secsToNextAnnounce = std::max<qint64>(0, m_announceTimestamp.secsTo(itemPtr->nextAnnounceTime));
549 itemPtr->secsToMinAnnounce = std::max<qint64>(0, m_announceTimestamp.secsTo(itemPtr->minAnnounceTime));
550 itemPtr->announceTimestamp = m_announceTimestamp;
553 const bool isEndpoint = !itemPtr->parentItem.expired();
555 switch (role)
557 case Qt::TextAlignmentRole:
558 switch (index.column())
560 case COL_TIER:
561 case COL_PROTOCOL:
562 case COL_PEERS:
563 case COL_SEEDS:
564 case COL_LEECHES:
565 case COL_TIMES_DOWNLOADED:
566 case COL_NEXT_ANNOUNCE:
567 case COL_MIN_ANNOUNCE:
568 return QVariant {Qt::AlignRight | Qt::AlignVCenter};
569 default:
570 return {};
573 case Qt::ForegroundRole:
574 // TODO: Make me configurable via UI Theme
575 if (!index.parent().isValid() && (index.row() < STICKY_ROW_COUNT))
576 return QColorConstants::Svg::grey;
577 return {};
579 case Qt::DisplayRole:
580 case Qt::ToolTipRole:
581 switch (index.column())
583 case COL_URL:
584 return itemPtr->name;
585 case COL_TIER:
586 return (isEndpoint || (index.row() < STICKY_ROW_COUNT)) ? QString() : QString::number(itemPtr->tier);
587 case COL_PROTOCOL:
588 return isEndpoint ? tr("v%1").arg(itemPtr->btVersion) : QString();
589 case COL_STATUS:
590 if (isEndpoint)
591 return toString(itemPtr->status);
592 if (index.row() == ROW_DHT)
593 return statusDHT(m_torrent);
594 if (index.row() == ROW_PEX)
595 return statusPeX(m_torrent);
596 if (index.row() == ROW_LSD)
597 return statusLSD(m_torrent);
598 return toString(itemPtr->status);
599 case COL_PEERS:
600 return prettyCount(itemPtr->numPeers);
601 case COL_SEEDS:
602 return prettyCount(itemPtr->numSeeds);
603 case COL_LEECHES:
604 return prettyCount(itemPtr->numLeeches);
605 case COL_TIMES_DOWNLOADED:
606 return prettyCount(itemPtr->numDownloaded);
607 case COL_MSG:
608 return itemPtr->message;
609 case COL_NEXT_ANNOUNCE:
610 return Utils::Misc::userFriendlyDuration(itemPtr->secsToNextAnnounce, -1, Utils::Misc::TimeResolution::Seconds);
611 case COL_MIN_ANNOUNCE:
612 return Utils::Misc::userFriendlyDuration(itemPtr->secsToMinAnnounce, -1, Utils::Misc::TimeResolution::Seconds);
613 default:
614 return {};
617 case SortRole:
618 switch (index.column())
620 case COL_URL:
621 return itemPtr->name;
622 case COL_TIER:
623 return isEndpoint ? -1 : itemPtr->tier;
624 case COL_PROTOCOL:
625 return isEndpoint ? itemPtr->btVersion : -1;
626 case COL_STATUS:
627 return toString(itemPtr->status);
628 case COL_PEERS:
629 return itemPtr->numPeers;
630 case COL_SEEDS:
631 return itemPtr->numSeeds;
632 case COL_LEECHES:
633 return itemPtr->numLeeches;
634 case COL_TIMES_DOWNLOADED:
635 return itemPtr->numDownloaded;
636 case COL_MSG:
637 return itemPtr->message;
638 case COL_NEXT_ANNOUNCE:
639 return itemPtr->secsToNextAnnounce;
640 case COL_MIN_ANNOUNCE:
641 return itemPtr->secsToMinAnnounce;
642 default:
643 return {};
646 default:
647 break;
650 return {};
653 QModelIndex TrackerListModel::index(const int row, const int column, const QModelIndex &parent) const
655 if ((column < 0) || (column >= columnCount()))
656 return {};
658 if ((row < 0) || (row >= rowCount(parent)))
659 return {};
661 const std::shared_ptr<Item> item = parent.isValid()
662 ? m_items->at(static_cast<std::size_t>(parent.row()))->childItems.at(row)
663 : m_items->at(static_cast<std::size_t>(row));
664 return createIndex(row, column, item.get());
667 QModelIndex TrackerListModel::parent(const QModelIndex &index) const
669 if (!index.isValid())
670 return {};
672 const auto *item = static_cast<Item *>(index.internalPointer());
673 Q_ASSERT(item);
674 if (!item) [[unlikely]]
675 return {};
677 const std::shared_ptr<Item> parentItem = item->parentItem.lock();
678 if (!parentItem)
679 return {};
681 const auto &itemsByName = m_items->get<ByName>();
682 auto itemsByNameIter = itemsByName.find(parentItem->name);
683 Q_ASSERT(itemsByNameIter != itemsByName.end());
684 if (itemsByNameIter == itemsByName.end()) [[unlikely]]
685 return {};
687 const auto &itemsByPosIter = m_items->project<0>(itemsByNameIter);
688 const auto row = std::distance(m_items->get<0>().begin(), itemsByPosIter);
690 // From https://doc.qt.io/qt-6/qabstractitemmodel.html#parent:
691 // A common convention used in models that expose tree data structures is that only items
692 // in the first column have children. For that case, when reimplementing this function in
693 // a subclass the column of the returned QModelIndex would be 0.
694 return createIndex(row, 0, parentItem.get());
697 void TrackerListModel::onTrackersAdded(const QList<BitTorrent::TrackerEntry> &newTrackers)
699 const int row = rowCount();
700 beginInsertRows({}, row, (row + newTrackers.size() - 1));
701 for (const BitTorrent::TrackerEntry &entry : newTrackers)
702 addTrackerItem({entry.url, entry.tier});
703 endInsertRows();
706 void TrackerListModel::onTrackersRemoved(const QStringList &deletedTrackers)
708 for (const QString &trackerURL : deletedTrackers)
710 auto &itemsByName = m_items->get<ByName>();
711 if (auto iter = itemsByName.find(trackerURL); iter != itemsByName.end())
713 const auto &iterByPos = m_items->project<0>(iter);
714 const auto row = std::distance(m_items->get<0>().begin(), iterByPos);
715 beginRemoveRows({}, row, row);
716 itemsByName.erase(iter);
717 endRemoveRows();
722 void TrackerListModel::onTrackersChanged()
724 QSet<QString> trackerItemIDs;
725 for (int i = 0; i < STICKY_ROW_COUNT; ++i)
726 trackerItemIDs.insert(m_items->at(i)->name);
728 QList<std::shared_ptr<Item>> newTrackerItems;
729 for (const BitTorrent::TrackerEntryStatus &trackerEntryStatus : m_torrent->trackers())
731 trackerItemIDs.insert(trackerEntryStatus.url);
733 auto &itemsByName = m_items->get<ByName>();
734 if (const auto &iter = itemsByName.find(trackerEntryStatus.url); iter != itemsByName.end())
736 updateTrackerItem(*iter, trackerEntryStatus);
738 else
740 newTrackerItems.emplace_back(createTrackerItem(trackerEntryStatus));
744 auto it = m_items->begin();
745 while (it != m_items->end())
747 if (trackerItemIDs.contains((*it)->name))
749 ++it;
751 else
753 const auto row = std::distance(m_items->begin(), it);
754 beginRemoveRows({}, row, row);
755 it = m_items->erase(it);
756 endRemoveRows();
760 if (!newTrackerItems.isEmpty())
762 const int numRows = rowCount();
763 beginInsertRows({}, numRows, (numRows + newTrackerItems.size() - 1));
764 for (const auto &newTrackerItem : asConst(newTrackerItems))
765 m_items->get<0>().push_back(newTrackerItem);
766 endInsertRows();
770 void TrackerListModel::onTrackersUpdated(const QHash<QString, BitTorrent::TrackerEntryStatus> &updatedTrackers)
772 for (const auto &[url, tracker] : updatedTrackers.asKeyValueRange())
774 auto &itemsByName = m_items->get<ByName>();
775 if (const auto &iter = itemsByName.find(tracker.url); iter != itemsByName.end()) [[likely]]
777 updateTrackerItem(*iter, tracker);