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"
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>
47 #include <QScopeGuard>
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
;
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
)
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
139 BitTorrent::TrackerEndpointState status
= BitTorrent::TrackerEndpointState::NotContacted
;
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
<
158 hashed_unique
<tag
<struct ByID
>, composite_key
<
160 member
<Item
, QString
, &Item::name
>,
161 member
<Item
, int, &Item::btVersion
>
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
>,
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
)
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
)
281 [[maybe_unused
]] const auto modelResetGuard
= qScopeGuard([this] { endResetModel(); });
291 m_announceRefreshTimer
->stop();
294 BitTorrent::Torrent
*TrackerListModel::torrent() const
299 void TrackerListModel::populate()
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
)
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())
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
;
356 itemsByPos
.modify((itemsByPos
.begin() + ROW_PEX
), [&seedsPeX
, &peersPeX
](std::shared_ptr
<Item
> &item
)
358 item
->numSeeds
= seedsPeX
;
359 item
->numLeeches
= peersPeX
;
362 itemsByPos
.modify((itemsByPos
.begin() + ROW_LSD
), [&seedsLSD
, &peersLSD
](std::shared_ptr
<Item
> &item
)
364 item
->numSeeds
= seedsLSD
;
365 item
->numLeeches
= peersLSD
;
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
));
388 void TrackerListModel::addTrackerItem(const BitTorrent::TrackerEntryStatus
&trackerEntryStatus
)
390 [[maybe_unused
]] const auto &[iter
, res
] = m_items
->emplace_back(createTrackerItem(trackerEntryStatus
));
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
);
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
))
427 const auto row
= std::distance(item
->childItems
.begin(), it
);
428 beginRemoveRows(trackerIndex
, row
, row
);
429 it
= item
->childItems
.erase(it
);
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
);
445 item
->fillFrom(trackerEntryStatus
);
446 emit
dataChanged(trackerIndex
, index(trackerRow
, (columnCount() - 1)));
449 void TrackerListModel::refreshAnnounceTimes()
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
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());
477 if (!item
) [[unlikely
]]
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
)
490 case Qt::DisplayRole
:
494 return tr("URL/Announce Endpoint");
498 return tr("BT Protocol");
506 return tr("Leeches");
507 case COL_TIMES_DOWNLOADED
:
508 return tr("Times Downloaded");
510 return tr("Message");
511 case COL_NEXT_ANNOUNCE
:
512 return tr("Next Announce");
513 case COL_MIN_ANNOUNCE
:
514 return tr("Min Announce");
519 case Qt::TextAlignmentRole
:
526 case COL_TIMES_DOWNLOADED
:
527 case COL_NEXT_ANNOUNCE
:
528 case COL_MIN_ANNOUNCE
:
529 return QVariant
{Qt::AlignRight
| Qt::AlignVCenter
};
539 QVariant
TrackerListModel::data(const QModelIndex
&index
, const int role
) const
541 if (!index
.isValid())
544 auto *itemPtr
= static_cast<Item
*>(index
.internalPointer());
546 if (!itemPtr
) [[unlikely
]]
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();
564 case Qt::TextAlignmentRole
:
565 switch (index
.column())
572 case COL_TIMES_DOWNLOADED
:
573 case COL_NEXT_ANNOUNCE
:
574 case COL_MIN_ANNOUNCE
:
575 return QVariant
{Qt::AlignRight
| Qt::AlignVCenter
};
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
;
586 case Qt::DisplayRole
:
587 case Qt::ToolTipRole
:
588 switch (index
.column())
591 return itemPtr
->name
;
593 return (isEndpoint
|| (index
.row() < STICKY_ROW_COUNT
)) ? QString() : QString::number(itemPtr
->tier
);
595 return isEndpoint
? (u
'v' + QString::number(itemPtr
->btVersion
)) : QString();
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
);
607 return prettyCount(itemPtr
->numPeers
);
609 return prettyCount(itemPtr
->numSeeds
);
611 return prettyCount(itemPtr
->numLeeches
);
612 case COL_TIMES_DOWNLOADED
:
613 return prettyCount(itemPtr
->numDownloaded
);
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
);
625 switch (index
.column())
628 return itemPtr
->name
;
630 return isEndpoint
? -1 : itemPtr
->tier
;
632 return isEndpoint
? itemPtr
->btVersion
: -1;
634 return toString(itemPtr
->status
);
636 return itemPtr
->numPeers
;
638 return itemPtr
->numSeeds
;
640 return itemPtr
->numLeeches
;
641 case COL_TIMES_DOWNLOADED
:
642 return itemPtr
->numDownloaded
;
644 return itemPtr
->message
;
645 case COL_NEXT_ANNOUNCE
:
646 return itemPtr
->secsToNextAnnounce
;
647 case COL_MIN_ANNOUNCE
:
648 return itemPtr
->secsToMinAnnounce
;
660 QModelIndex
TrackerListModel::index(const int row
, const int column
, const QModelIndex
&parent
) const
662 if ((column
< 0) || (column
>= columnCount()))
665 if ((row
< 0) || (row
>= rowCount(parent
)))
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())
679 const auto *item
= static_cast<Item
*>(index
.internalPointer());
681 if (!item
) [[unlikely
]]
684 const std::shared_ptr
<Item
> parentItem
= item
->parentItem
.lock();
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
]]
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
});
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
);
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
);
747 newTrackerItems
.emplace_back(createTrackerItem(trackerEntryStatus
));
751 auto it
= m_items
->begin();
752 while (it
!= m_items
->end())
754 if (trackerItemIDs
.contains((*it
)->name
))
760 const auto row
= std::distance(m_items
->begin(), it
);
761 beginRemoveRows({}, row
, row
);
762 it
= m_items
->erase(it
);
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
);
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
);