Don't add duplicate episodes to previously matched
[qBittorrent.git] / src / gui / trackerlist / trackerlistmodel.cpp
blob78465488c312ffdde601aa50c048a418166e205e
1 /*
2 * Bittorrent Client using Qt and libtorrent.
3 * Copyright (C) 2023-2024 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 <algorithm>
33 #include <chrono>
35 #include <boost/multi_index_container.hpp>
36 #include <boost/multi_index/composite_key.hpp>
37 #include <boost/multi_index/hashed_index.hpp>
38 #include <boost/multi_index/indexed_by.hpp>
39 #include <boost/multi_index/member.hpp>
40 #include <boost/multi_index/random_access_index.hpp>
41 #include <boost/multi_index/tag.hpp>
43 #include <QColor>
44 #include <QDateTime>
45 #include <QList>
46 #include <QPointer>
47 #include <QScopeGuard>
48 #include <QTimer>
50 #include "base/bittorrent/announcetimepoint.h"
51 #include "base/bittorrent/peerinfo.h"
52 #include "base/bittorrent/session.h"
53 #include "base/bittorrent/torrent.h"
54 #include "base/bittorrent/trackerentry.h"
55 #include "base/global.h"
56 #include "base/utils/misc.h"
58 using namespace std::chrono_literals;
59 using namespace boost::multi_index;
61 namespace
63 const std::chrono::milliseconds ANNOUNCE_TIME_REFRESH_INTERVAL = 4s;
65 const char STR_WORKING[] = QT_TRANSLATE_NOOP("TrackerListModel", "Working");
66 const char STR_DISABLED[] = QT_TRANSLATE_NOOP("TrackerListModel", "Disabled");
67 const char STR_TORRENT_DISABLED[] = QT_TRANSLATE_NOOP("TrackerListModel", "Disabled for this torrent");
68 const char STR_PRIVATE_MSG[] = QT_TRANSLATE_NOOP("TrackerListModel", "This torrent is private");
70 QString prettyCount(const int val)
72 return (val > -1) ? QString::number(val) : TrackerListModel::tr("N/A");
75 QString toString(const BitTorrent::TrackerEndpointState state)
77 switch (state)
79 case BitTorrent::TrackerEndpointState::Working:
80 return TrackerListModel::tr(STR_WORKING);
81 case BitTorrent::TrackerEndpointState::Updating:
82 return TrackerListModel::tr("Updating...");
83 case BitTorrent::TrackerEndpointState::NotWorking:
84 return TrackerListModel::tr("Not working");
85 case BitTorrent::TrackerEndpointState::TrackerError:
86 return TrackerListModel::tr("Tracker error");
87 case BitTorrent::TrackerEndpointState::Unreachable:
88 return TrackerListModel::tr("Unreachable");
89 case BitTorrent::TrackerEndpointState::NotContacted:
90 return TrackerListModel::tr("Not contacted yet");
92 return TrackerListModel::tr("Invalid state!");
95 QString statusDHT(const BitTorrent::Torrent *torrent)
97 if (!torrent->session()->isDHTEnabled())
98 return TrackerListModel::tr(STR_DISABLED);
100 if (torrent->isPrivate() || torrent->isDHTDisabled())
101 return TrackerListModel::tr(STR_TORRENT_DISABLED);
103 return TrackerListModel::tr(STR_WORKING);
106 QString statusPeX(const BitTorrent::Torrent *torrent)
108 if (!torrent->session()->isPeXEnabled())
109 return TrackerListModel::tr(STR_DISABLED);
111 if (torrent->isPrivate() || torrent->isPEXDisabled())
112 return TrackerListModel::tr(STR_TORRENT_DISABLED);
114 return TrackerListModel::tr(STR_WORKING);
117 QString statusLSD(const BitTorrent::Torrent *torrent)
119 if (!torrent->session()->isLSDEnabled())
120 return TrackerListModel::tr(STR_DISABLED);
122 if (torrent->isPrivate() || torrent->isLSDDisabled())
123 return TrackerListModel::tr(STR_TORRENT_DISABLED);
125 return TrackerListModel::tr(STR_WORKING);
129 std::size_t hash_value(const QString &string)
131 return qHash(string);
134 struct TrackerListModel::Item final
136 QString name {};
137 int tier = -1;
138 int btVersion = -1;
139 BitTorrent::TrackerEndpointState status = BitTorrent::TrackerEndpointState::NotContacted;
140 QString message {};
142 int numPeers = -1;
143 int numSeeds = -1;
144 int numLeeches = -1;
145 int numDownloaded = -1;
147 BitTorrent::AnnounceTimePoint nextAnnounceTime;
148 BitTorrent::AnnounceTimePoint minAnnounceTime;
150 qint64 secsToNextAnnounce = 0;
151 qint64 secsToMinAnnounce = 0;
152 BitTorrent::AnnounceTimePoint announceTimestamp;
154 std::weak_ptr<Item> parentItem {};
156 multi_index_container<std::shared_ptr<Item>, indexed_by<
157 random_access<>,
158 hashed_unique<tag<struct ByID>, composite_key<
159 Item,
160 member<Item, QString, &Item::name>,
161 member<Item, int, &Item::btVersion>
163 >> childItems {};
165 Item(QStringView name, QStringView message);
166 explicit Item(const BitTorrent::TrackerEntryStatus &trackerEntryStatus);
167 Item(const std::shared_ptr<Item> &parentItem, const BitTorrent::TrackerEndpointStatus &endpointStatus);
169 void fillFrom(const BitTorrent::TrackerEntryStatus &trackerEntryStatus);
170 void fillFrom(const BitTorrent::TrackerEndpointStatus &endpointStatus);
173 class TrackerListModel::Items final : public multi_index_container<
174 std::shared_ptr<Item>,
175 indexed_by<
176 random_access<>,
177 hashed_unique<tag<struct ByName>, member<Item, QString, &Item::name>>>>
181 TrackerListModel::Item::Item(const QStringView name, const QStringView message)
182 : name {name.toString()}
183 , message {message.toString()}
187 TrackerListModel::Item::Item(const BitTorrent::TrackerEntryStatus &trackerEntryStatus)
188 : name {trackerEntryStatus.url}
190 fillFrom(trackerEntryStatus);
193 TrackerListModel::Item::Item(const std::shared_ptr<Item> &parentItem, const BitTorrent::TrackerEndpointStatus &endpointStatus)
194 : name {endpointStatus.name}
195 , btVersion {endpointStatus.btVersion}
196 , parentItem {parentItem}
198 fillFrom(endpointStatus);
201 void TrackerListModel::Item::fillFrom(const BitTorrent::TrackerEntryStatus &trackerEntryStatus)
203 Q_ASSERT(parentItem.expired());
204 Q_ASSERT(trackerEntryStatus.url == name);
206 tier = trackerEntryStatus.tier;
207 status = trackerEntryStatus.state;
208 message = trackerEntryStatus.message;
209 numPeers = trackerEntryStatus.numPeers;
210 numSeeds = trackerEntryStatus.numSeeds;
211 numLeeches = trackerEntryStatus.numLeeches;
212 numDownloaded = trackerEntryStatus.numDownloaded;
213 nextAnnounceTime = trackerEntryStatus.nextAnnounceTime;
214 minAnnounceTime = trackerEntryStatus.minAnnounceTime;
215 secsToNextAnnounce = 0;
216 secsToMinAnnounce = 0;
217 announceTimestamp = {};
220 void TrackerListModel::Item::fillFrom(const BitTorrent::TrackerEndpointStatus &endpointStatus)
222 Q_ASSERT(!parentItem.expired());
223 Q_ASSERT(endpointStatus.name == name);
224 Q_ASSERT(endpointStatus.btVersion == btVersion);
226 status = endpointStatus.state;
227 message = endpointStatus.message;
228 numPeers = endpointStatus.numPeers;
229 numSeeds = endpointStatus.numSeeds;
230 numLeeches = endpointStatus.numLeeches;
231 numDownloaded = endpointStatus.numDownloaded;
232 nextAnnounceTime = endpointStatus.nextAnnounceTime;
233 minAnnounceTime = endpointStatus.minAnnounceTime;
234 secsToNextAnnounce = 0;
235 secsToMinAnnounce = 0;
236 announceTimestamp = {};
239 TrackerListModel::TrackerListModel(BitTorrent::Session *btSession, QObject *parent)
240 : QAbstractItemModel(parent)
241 , m_btSession {btSession}
242 , m_items {std::make_unique<Items>()}
243 , m_announceRefreshTimer {new QTimer(this)}
245 Q_ASSERT(m_btSession);
247 m_announceRefreshTimer->setSingleShot(true);
248 connect(m_announceRefreshTimer, &QTimer::timeout, this, &TrackerListModel::refreshAnnounceTimes);
250 connect(m_btSession, &BitTorrent::Session::trackersAdded, this
251 , [this](BitTorrent::Torrent *torrent, const QList<BitTorrent::TrackerEntry> &newTrackers)
253 if (torrent == m_torrent)
254 onTrackersAdded(newTrackers);
256 connect(m_btSession, &BitTorrent::Session::trackersRemoved, this
257 , [this](BitTorrent::Torrent *torrent, const QStringList &deletedTrackers)
259 if (torrent == m_torrent)
260 onTrackersRemoved(deletedTrackers);
262 connect(m_btSession, &BitTorrent::Session::trackersChanged, this
263 , [this](BitTorrent::Torrent *torrent)
265 if (torrent == m_torrent)
266 onTrackersChanged();
268 connect(m_btSession, &BitTorrent::Session::trackerEntryStatusesUpdated, this
269 , [this](BitTorrent::Torrent *torrent, const QHash<QString, BitTorrent::TrackerEntryStatus> &updatedTrackers)
271 if (torrent == m_torrent)
272 onTrackersUpdated(updatedTrackers);
276 TrackerListModel::~TrackerListModel() = default;
278 void TrackerListModel::setTorrent(BitTorrent::Torrent *torrent)
280 beginResetModel();
281 [[maybe_unused]] const auto modelResetGuard = qScopeGuard([this] { endResetModel(); });
283 if (m_torrent)
284 m_items->clear();
286 m_torrent = torrent;
288 if (m_torrent)
289 populate();
290 else
291 m_announceRefreshTimer->stop();
294 BitTorrent::Torrent *TrackerListModel::torrent() const
296 return m_torrent;
299 void TrackerListModel::populate()
301 Q_ASSERT(m_torrent);
303 const QList<BitTorrent::TrackerEntryStatus> trackers = m_torrent->trackers();
304 m_items->reserve(trackers.size() + STICKY_ROW_COUNT);
306 const QString &privateTorrentMessage = m_torrent->isPrivate() ? tr(STR_PRIVATE_MSG) : u""_s;
307 m_items->emplace_back(std::make_shared<Item>(u"** [DHT] **", privateTorrentMessage));
308 m_items->emplace_back(std::make_shared<Item>(u"** [PeX] **", privateTorrentMessage));
309 m_items->emplace_back(std::make_shared<Item>(u"** [LSD] **", privateTorrentMessage));
311 using TorrentPtr = QPointer<const BitTorrent::Torrent>;
312 m_torrent->fetchPeerInfo([this, torrent = TorrentPtr(m_torrent)](const QList<BitTorrent::PeerInfo> &peers)
314 if (torrent != m_torrent)
315 return;
317 // XXX: libtorrent should provide this info...
318 // Count peers from DHT, PeX, LSD
319 uint seedsDHT = 0, seedsPeX = 0, seedsLSD = 0, peersDHT = 0, peersPeX = 0, peersLSD = 0;
320 for (const BitTorrent::PeerInfo &peer : peers)
322 if (peer.isConnecting())
323 continue;
325 if (peer.isSeed())
327 if (peer.fromDHT())
328 ++seedsDHT;
330 if (peer.fromPeX())
331 ++seedsPeX;
333 if (peer.fromLSD())
334 ++seedsLSD;
336 else
338 if (peer.fromDHT())
339 ++peersDHT;
341 if (peer.fromPeX())
342 ++peersPeX;
344 if (peer.fromLSD())
345 ++peersLSD;
349 auto &itemsByPos = m_items->get<0>();
350 itemsByPos.modify((itemsByPos.begin() + ROW_DHT), [&seedsDHT, &peersDHT](std::shared_ptr<Item> &item)
352 item->numSeeds = seedsDHT;
353 item->numLeeches = peersDHT;
354 return true;
356 itemsByPos.modify((itemsByPos.begin() + ROW_PEX), [&seedsPeX, &peersPeX](std::shared_ptr<Item> &item)
358 item->numSeeds = seedsPeX;
359 item->numLeeches = peersPeX;
360 return true;
362 itemsByPos.modify((itemsByPos.begin() + ROW_LSD), [&seedsLSD, &peersLSD](std::shared_ptr<Item> &item)
364 item->numSeeds = seedsLSD;
365 item->numLeeches = peersLSD;
366 return true;
369 emit dataChanged(index(ROW_DHT, COL_SEEDS), index(ROW_LSD, COL_LEECHES));
372 for (const BitTorrent::TrackerEntryStatus &status : trackers)
373 addTrackerItem(status);
375 m_announceTimestamp = BitTorrent::AnnounceTimePoint::clock::now();
376 m_announceRefreshTimer->start(ANNOUNCE_TIME_REFRESH_INTERVAL);
379 std::shared_ptr<TrackerListModel::Item> TrackerListModel::createTrackerItem(const BitTorrent::TrackerEntryStatus &trackerEntryStatus)
381 const auto item = std::make_shared<Item>(trackerEntryStatus);
382 for (const auto &[id, endpointStatus] : trackerEntryStatus.endpoints.asKeyValueRange())
383 item->childItems.emplace_back(std::make_shared<Item>(item, endpointStatus));
385 return item;
388 void TrackerListModel::addTrackerItem(const BitTorrent::TrackerEntryStatus &trackerEntryStatus)
390 [[maybe_unused]] const auto &[iter, res] = m_items->emplace_back(createTrackerItem(trackerEntryStatus));
391 Q_ASSERT(res);
394 void TrackerListModel::updateTrackerItem(const std::shared_ptr<Item> &item, const BitTorrent::TrackerEntryStatus &trackerEntryStatus)
396 QSet<std::pair<QString, int>> endpointItemIDs;
397 QList<std::shared_ptr<Item>> newEndpointItems;
398 for (const auto &[id, endpointStatus] : trackerEntryStatus.endpoints.asKeyValueRange())
400 endpointItemIDs.insert(id);
402 auto &itemsByID = item->childItems.get<ByID>();
403 if (const auto &iter = itemsByID.find(std::make_tuple(id.first, id.second)); iter != itemsByID.end())
405 (*iter)->fillFrom(endpointStatus);
407 else
409 newEndpointItems.emplace_back(std::make_shared<Item>(item, endpointStatus));
413 const auto &itemsByPos = m_items->get<0>();
414 const auto trackerRow = std::distance(itemsByPos.begin(), itemsByPos.iterator_to(item));
415 const auto trackerIndex = index(trackerRow, 0);
417 auto it = item->childItems.begin();
418 while (it != item->childItems.end())
420 if (const auto endpointItemID = std::make_pair((*it)->name, (*it)->btVersion)
421 ; endpointItemIDs.contains(endpointItemID))
423 ++it;
425 else
427 const auto row = std::distance(item->childItems.begin(), it);
428 beginRemoveRows(trackerIndex, row, row);
429 it = item->childItems.erase(it);
430 endRemoveRows();
434 const int numRows = rowCount(trackerIndex);
435 emit dataChanged(index(0, 0, trackerIndex), index((numRows - 1), (columnCount(trackerIndex) - 1), trackerIndex));
437 if (!newEndpointItems.isEmpty())
439 beginInsertRows(trackerIndex, numRows, (numRows + newEndpointItems.size() - 1));
440 for (const auto &newEndpointItem : asConst(newEndpointItems))
441 item->childItems.get<0>().push_back(newEndpointItem);
442 endInsertRows();
445 item->fillFrom(trackerEntryStatus);
446 emit dataChanged(trackerIndex, index(trackerRow, (columnCount() - 1)));
449 void TrackerListModel::refreshAnnounceTimes()
451 if (!m_torrent)
452 return;
454 m_announceTimestamp = BitTorrent::AnnounceTimePoint::clock::now();
455 emit dataChanged(index(0, COL_NEXT_ANNOUNCE), index((rowCount() - 1), COL_MIN_ANNOUNCE));
456 for (int i = 0; i < rowCount(); ++i)
458 const QModelIndex parentIndex = index(i, 0);
459 emit dataChanged(index(0, COL_NEXT_ANNOUNCE, parentIndex), index((rowCount(parentIndex) - 1), COL_MIN_ANNOUNCE, parentIndex));
462 m_announceRefreshTimer->start(ANNOUNCE_TIME_REFRESH_INTERVAL);
465 int TrackerListModel::columnCount([[maybe_unused]] const QModelIndex &parent) const
467 return COL_COUNT;
470 int TrackerListModel::rowCount(const QModelIndex &parent) const
472 if (!parent.isValid())
473 return m_items->size();
475 const auto *item = static_cast<Item *>(parent.internalPointer());
476 Q_ASSERT(item);
477 if (!item) [[unlikely]]
478 return 0;
480 return item->childItems.size();
483 QVariant TrackerListModel::headerData(const int section, const Qt::Orientation orientation, const int role) const
485 if (orientation != Qt::Horizontal)
486 return {};
488 switch (role)
490 case Qt::DisplayRole:
491 switch (section)
493 case COL_URL:
494 return tr("URL/Announce Endpoint");
495 case COL_TIER:
496 return tr("Tier");
497 case COL_PROTOCOL:
498 return tr("BT Protocol");
499 case COL_STATUS:
500 return tr("Status");
501 case COL_PEERS:
502 return tr("Peers");
503 case COL_SEEDS:
504 return tr("Seeds");
505 case COL_LEECHES:
506 return tr("Leeches");
507 case COL_TIMES_DOWNLOADED:
508 return tr("Times Downloaded");
509 case COL_MSG:
510 return tr("Message");
511 case COL_NEXT_ANNOUNCE:
512 return tr("Next Announce");
513 case COL_MIN_ANNOUNCE:
514 return tr("Min Announce");
515 default:
516 return {};
519 case Qt::TextAlignmentRole:
520 switch (section)
522 case COL_TIER:
523 case COL_PEERS:
524 case COL_SEEDS:
525 case COL_LEECHES:
526 case COL_TIMES_DOWNLOADED:
527 case COL_NEXT_ANNOUNCE:
528 case COL_MIN_ANNOUNCE:
529 return QVariant {Qt::AlignRight | Qt::AlignVCenter};
530 default:
531 return {};
534 default:
535 return {};
539 QVariant TrackerListModel::data(const QModelIndex &index, const int role) const
541 if (!index.isValid())
542 return {};
544 auto *itemPtr = static_cast<Item *>(index.internalPointer());
545 Q_ASSERT(itemPtr);
546 if (!itemPtr) [[unlikely]]
547 return {};
549 if (itemPtr->announceTimestamp != m_announceTimestamp)
551 const auto timeToNextAnnounce = std::chrono::duration_cast<std::chrono::seconds>(itemPtr->nextAnnounceTime - m_announceTimestamp);
552 itemPtr->secsToNextAnnounce = std::max<qint64>(0, timeToNextAnnounce.count());
554 const auto timeToMinAnnounce = std::chrono::duration_cast<std::chrono::seconds>(itemPtr->minAnnounceTime - m_announceTimestamp);
555 itemPtr->secsToMinAnnounce = std::max<qint64>(0, timeToMinAnnounce.count());
557 itemPtr->announceTimestamp = m_announceTimestamp;
560 const bool isEndpoint = !itemPtr->parentItem.expired();
562 switch (role)
564 case Qt::TextAlignmentRole:
565 switch (index.column())
567 case COL_TIER:
568 case COL_PROTOCOL:
569 case COL_PEERS:
570 case COL_SEEDS:
571 case COL_LEECHES:
572 case COL_TIMES_DOWNLOADED:
573 case COL_NEXT_ANNOUNCE:
574 case COL_MIN_ANNOUNCE:
575 return QVariant {Qt::AlignRight | Qt::AlignVCenter};
576 default:
577 return {};
580 case Qt::ForegroundRole:
581 // TODO: Make me configurable via UI Theme
582 if (!index.parent().isValid() && (index.row() < STICKY_ROW_COUNT))
583 return QColorConstants::Svg::grey;
584 return {};
586 case Qt::DisplayRole:
587 case Qt::ToolTipRole:
588 switch (index.column())
590 case COL_URL:
591 return itemPtr->name;
592 case COL_TIER:
593 return (isEndpoint || (index.row() < STICKY_ROW_COUNT)) ? QString() : QString::number(itemPtr->tier);
594 case COL_PROTOCOL:
595 return isEndpoint ? (u'v' + QString::number(itemPtr->btVersion)) : QString();
596 case COL_STATUS:
597 if (isEndpoint)
598 return toString(itemPtr->status);
599 if (index.row() == ROW_DHT)
600 return statusDHT(m_torrent);
601 if (index.row() == ROW_PEX)
602 return statusPeX(m_torrent);
603 if (index.row() == ROW_LSD)
604 return statusLSD(m_torrent);
605 return toString(itemPtr->status);
606 case COL_PEERS:
607 return prettyCount(itemPtr->numPeers);
608 case COL_SEEDS:
609 return prettyCount(itemPtr->numSeeds);
610 case COL_LEECHES:
611 return prettyCount(itemPtr->numLeeches);
612 case COL_TIMES_DOWNLOADED:
613 return prettyCount(itemPtr->numDownloaded);
614 case COL_MSG:
615 return itemPtr->message;
616 case COL_NEXT_ANNOUNCE:
617 return Utils::Misc::userFriendlyDuration(itemPtr->secsToNextAnnounce, -1, Utils::Misc::TimeResolution::Seconds);
618 case COL_MIN_ANNOUNCE:
619 return Utils::Misc::userFriendlyDuration(itemPtr->secsToMinAnnounce, -1, Utils::Misc::TimeResolution::Seconds);
620 default:
621 return {};
624 case SortRole:
625 switch (index.column())
627 case COL_URL:
628 return itemPtr->name;
629 case COL_TIER:
630 return isEndpoint ? -1 : itemPtr->tier;
631 case COL_PROTOCOL:
632 return isEndpoint ? itemPtr->btVersion : -1;
633 case COL_STATUS:
634 return toString(itemPtr->status);
635 case COL_PEERS:
636 return itemPtr->numPeers;
637 case COL_SEEDS:
638 return itemPtr->numSeeds;
639 case COL_LEECHES:
640 return itemPtr->numLeeches;
641 case COL_TIMES_DOWNLOADED:
642 return itemPtr->numDownloaded;
643 case COL_MSG:
644 return itemPtr->message;
645 case COL_NEXT_ANNOUNCE:
646 return itemPtr->secsToNextAnnounce;
647 case COL_MIN_ANNOUNCE:
648 return itemPtr->secsToMinAnnounce;
649 default:
650 return {};
653 default:
654 break;
657 return {};
660 QModelIndex TrackerListModel::index(const int row, const int column, const QModelIndex &parent) const
662 if ((column < 0) || (column >= columnCount()))
663 return {};
665 if ((row < 0) || (row >= rowCount(parent)))
666 return {};
668 const std::shared_ptr<Item> item = parent.isValid()
669 ? m_items->at(static_cast<std::size_t>(parent.row()))->childItems.at(row)
670 : m_items->at(static_cast<std::size_t>(row));
671 return createIndex(row, column, item.get());
674 QModelIndex TrackerListModel::parent(const QModelIndex &index) const
676 if (!index.isValid())
677 return {};
679 const auto *item = static_cast<Item *>(index.internalPointer());
680 Q_ASSERT(item);
681 if (!item) [[unlikely]]
682 return {};
684 const std::shared_ptr<Item> parentItem = item->parentItem.lock();
685 if (!parentItem)
686 return {};
688 const auto &itemsByName = m_items->get<ByName>();
689 auto itemsByNameIter = itemsByName.find(parentItem->name);
690 Q_ASSERT(itemsByNameIter != itemsByName.end());
691 if (itemsByNameIter == itemsByName.end()) [[unlikely]]
692 return {};
694 const auto &itemsByPosIter = m_items->project<0>(itemsByNameIter);
695 const auto row = std::distance(m_items->get<0>().begin(), itemsByPosIter);
697 // From https://doc.qt.io/qt-6/qabstractitemmodel.html#parent:
698 // A common convention used in models that expose tree data structures is that only items
699 // in the first column have children. For that case, when reimplementing this function in
700 // a subclass the column of the returned QModelIndex would be 0.
701 return createIndex(row, 0, parentItem.get());
704 void TrackerListModel::onTrackersAdded(const QList<BitTorrent::TrackerEntry> &newTrackers)
706 const int row = rowCount();
707 beginInsertRows({}, row, (row + newTrackers.size() - 1));
708 for (const BitTorrent::TrackerEntry &entry : newTrackers)
709 addTrackerItem({entry.url, entry.tier});
710 endInsertRows();
713 void TrackerListModel::onTrackersRemoved(const QStringList &deletedTrackers)
715 for (const QString &trackerURL : deletedTrackers)
717 auto &itemsByName = m_items->get<ByName>();
718 if (auto iter = itemsByName.find(trackerURL); iter != itemsByName.end())
720 const auto &iterByPos = m_items->project<0>(iter);
721 const auto row = std::distance(m_items->get<0>().begin(), iterByPos);
722 beginRemoveRows({}, row, row);
723 itemsByName.erase(iter);
724 endRemoveRows();
729 void TrackerListModel::onTrackersChanged()
731 QSet<QString> trackerItemIDs;
732 for (int i = 0; i < STICKY_ROW_COUNT; ++i)
733 trackerItemIDs.insert(m_items->at(i)->name);
735 QList<std::shared_ptr<Item>> newTrackerItems;
736 for (const BitTorrent::TrackerEntryStatus &trackerEntryStatus : m_torrent->trackers())
738 trackerItemIDs.insert(trackerEntryStatus.url);
740 auto &itemsByName = m_items->get<ByName>();
741 if (const auto &iter = itemsByName.find(trackerEntryStatus.url); iter != itemsByName.end())
743 updateTrackerItem(*iter, trackerEntryStatus);
745 else
747 newTrackerItems.emplace_back(createTrackerItem(trackerEntryStatus));
751 auto it = m_items->begin();
752 while (it != m_items->end())
754 if (trackerItemIDs.contains((*it)->name))
756 ++it;
758 else
760 const auto row = std::distance(m_items->begin(), it);
761 beginRemoveRows({}, row, row);
762 it = m_items->erase(it);
763 endRemoveRows();
767 if (!newTrackerItems.isEmpty())
769 const int numRows = rowCount();
770 beginInsertRows({}, numRows, (numRows + newTrackerItems.size() - 1));
771 for (const auto &newTrackerItem : asConst(newTrackerItems))
772 m_items->get<0>().push_back(newTrackerItem);
773 endInsertRows();
777 void TrackerListModel::onTrackersUpdated(const QHash<QString, BitTorrent::TrackerEntryStatus> &updatedTrackers)
779 for (const auto &[url, tracker] : updatedTrackers.asKeyValueRange())
781 auto &itemsByName = m_items->get<ByName>();
782 if (const auto &iter = itemsByName.find(tracker.url); iter != itemsByName.end()) [[likely]]
784 updateTrackerItem(*iter, tracker);