Browse Source

Revise 'Add trackers' dialog

* Now it allow user to add tracker to different tier.
* The downloaded trackers are now displayed as is (without modifying).
* Now the dialog remember dialog size and last used URL.

Closes #17692.
adaptive-webui-19844
Chocobo1 2 years ago
parent
commit
e692a191ed
No known key found for this signature in database
GPG Key ID: 210D9C873253A68C
  1. 5
      src/base/bittorrent/torrentimpl.cpp
  2. 28
      src/base/bittorrent/trackerentry.cpp
  3. 4
      src/base/bittorrent/trackerentry.h
  4. 18
      src/gui/properties/trackerlistwidget.cpp
  5. 2
      src/gui/properties/trackerlistwidget.h
  6. 121
      src/gui/properties/trackersadditiondialog.cpp
  7. 20
      src/gui/properties/trackersadditiondialog.h
  8. 6
      src/gui/properties/trackersadditiondialog.ui
  9. 22
      src/gui/trackerentriesdialog.cpp
  10. 10
      src/webui/api/torrentscontroller.cpp
  11. 1
      test/CMakeLists.txt
  12. 143
      test/testbittorrenttrackerentry.cpp

5
src/base/bittorrent/torrentimpl.cpp

@ -540,6 +540,7 @@ void TorrentImpl::addTrackers(QVector<TrackerEntry> trackers)
m_trackerEntries.append(trackers); m_trackerEntries.append(trackers);
std::sort(m_trackerEntries.begin(), m_trackerEntries.end() std::sort(m_trackerEntries.begin(), m_trackerEntries.end()
, [](const TrackerEntry &lhs, const TrackerEntry &rhs) { return lhs.tier < rhs.tier; }); , [](const TrackerEntry &lhs, const TrackerEntry &rhs) { return lhs.tier < rhs.tier; });
m_session->handleTorrentNeedSaveResumeData(this); m_session->handleTorrentNeedSaveResumeData(this);
m_session->handleTorrentTrackersAdded(this, trackers); m_session->handleTorrentTrackersAdded(this, trackers);
} }
@ -561,6 +562,7 @@ void TorrentImpl::removeTrackers(const QStringList &trackers)
if (!removedTrackers.isEmpty()) if (!removedTrackers.isEmpty())
{ {
m_nativeHandle.replace_trackers(nativeTrackers); m_nativeHandle.replace_trackers(nativeTrackers);
m_session->handleTorrentNeedSaveResumeData(this); m_session->handleTorrentNeedSaveResumeData(this);
m_session->handleTorrentTrackersRemoved(this, removedTrackers); m_session->handleTorrentTrackersRemoved(this, removedTrackers);
} }
@ -579,12 +581,13 @@ void TorrentImpl::replaceTrackers(QVector<TrackerEntry> trackers)
nativeTrackers.emplace_back(makeNativeAnnounceEntry(tracker.url, tracker.tier)); nativeTrackers.emplace_back(makeNativeAnnounceEntry(tracker.url, tracker.tier));
m_nativeHandle.replace_trackers(nativeTrackers); m_nativeHandle.replace_trackers(nativeTrackers);
m_trackerEntries = trackers;
// Clear the peer list if it's a private torrent since // Clear the peer list if it's a private torrent since
// we do not want to keep connecting with peers from old tracker. // we do not want to keep connecting with peers from old tracker.
if (isPrivate()) if (isPrivate())
clearPeers(); clearPeers();
m_trackerEntries = trackers;
m_session->handleTorrentNeedSaveResumeData(this); m_session->handleTorrentNeedSaveResumeData(this);
m_session->handleTorrentTrackersChanged(this); m_session->handleTorrentTrackersChanged(this);
} }

28
src/base/bittorrent/trackerentry.cpp

@ -28,6 +28,34 @@
#include "trackerentry.h" #include "trackerentry.h"
#include <QList>
#include <QVector>
QVector<BitTorrent::TrackerEntry> BitTorrent::parseTrackerEntries(const QStringView str)
{
const QList<QStringView> trackers = str.split(u'\n'); // keep the empty parts to track tracker tier
QVector<BitTorrent::TrackerEntry> entries;
entries.reserve(trackers.size());
int trackerTier = 0;
for (QStringView tracker : trackers)
{
tracker = tracker.trimmed();
if (tracker.isEmpty())
{
if (trackerTier < std::numeric_limits<decltype(trackerTier)>::max()) // prevent overflow
++trackerTier;
continue;
}
entries.append({tracker.toString(), trackerTier});
}
return entries;
}
bool BitTorrent::operator==(const TrackerEntry &left, const TrackerEntry &right) bool BitTorrent::operator==(const TrackerEntry &left, const TrackerEntry &right)
{ {
return (left.url == right.url); return (left.url == right.url);

4
src/base/bittorrent/trackerentry.h

@ -30,10 +30,12 @@
#include <libtorrent/socket.hpp> #include <libtorrent/socket.hpp>
#include <QtContainerFwd>
#include <QtGlobal> #include <QtGlobal>
#include <QHash> #include <QHash>
#include <QMap> #include <QMap>
#include <QString> #include <QString>
#include <QStringView>
namespace BitTorrent namespace BitTorrent
{ {
@ -74,6 +76,8 @@ namespace BitTorrent
QString message {}; QString message {};
}; };
QVector<TrackerEntry> parseTrackerEntries(QStringView str);
bool operator==(const TrackerEntry &left, const TrackerEntry &right); bool operator==(const TrackerEntry &left, const TrackerEntry &right);
#if (QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)) #if (QT_VERSION >= QT_VERSION_CHECK(6, 0, 0))
std::size_t qHash(const TrackerEntry &key, std::size_t seed = 0); std::size_t qHash(const TrackerEntry &key, std::size_t seed = 0);

18
src/gui/properties/trackerlistwidget.cpp

@ -423,17 +423,15 @@ void TrackerListWidget::loadTrackers()
delete m_trackerItems.take(tracker); delete m_trackerItems.take(tracker);
} }
// Ask the user for new trackers and add them to the torrent void TrackerListWidget::openAddTrackersDialog()
void TrackerListWidget::askForTrackers()
{ {
BitTorrent::Torrent *const torrent = m_properties->getCurrentTorrent(); BitTorrent::Torrent *torrent = m_properties->getCurrentTorrent();
if (!torrent) return; if (!torrent)
return;
QVector<BitTorrent::TrackerEntry> trackers;
for (const QString &tracker : asConst(TrackersAdditionDialog::askForTrackers(this, torrent)))
trackers.append({tracker});
torrent->addTrackers(trackers); const auto dialog = new TrackersAdditionDialog(this, torrent);
dialog->setAttribute(Qt::WA_DeleteOnClose);
dialog->open();
} }
void TrackerListWidget::copyTrackerUrl() void TrackerListWidget::copyTrackerUrl()
@ -568,7 +566,7 @@ void TrackerListWidget::showTrackerListMenu()
// Add actions // Add actions
menu->addAction(UIThemeManager::instance()->getIcon(u"list-add"_qs), tr("Add trackers...") menu->addAction(UIThemeManager::instance()->getIcon(u"list-add"_qs), tr("Add trackers...")
, this, &TrackerListWidget::askForTrackers); , this, &TrackerListWidget::openAddTrackersDialog);
if (!getSelectedTrackerItems().isEmpty()) if (!getSelectedTrackerItems().isEmpty())
{ {

2
src/gui/properties/trackerlistwidget.h

@ -70,7 +70,6 @@ public slots:
void clear(); void clear();
void loadStickyItems(const BitTorrent::Torrent *torrent); void loadStickyItems(const BitTorrent::Torrent *torrent);
void loadTrackers(); void loadTrackers();
void askForTrackers();
void copyTrackerUrl(); void copyTrackerUrl();
void reannounceSelected(); void reannounceSelected();
void deleteSelectedTrackers(); void deleteSelectedTrackers();
@ -83,6 +82,7 @@ protected:
QVector<QTreeWidgetItem *> getSelectedTrackerItems() const; QVector<QTreeWidgetItem *> getSelectedTrackerItems() const;
private slots: private slots:
void openAddTrackersDialog();
void displayColumnHeaderMenu(); void displayColumnHeaderMenu();
private: private:

121
src/gui/properties/trackersadditiondialog.cpp

@ -1,5 +1,6 @@
/* /*
* Bittorrent Client using Qt and libtorrent. * Bittorrent Client using Qt and libtorrent.
* Copyright (C) 2022 Mike Tzou (Chocobo1)
* Copyright (C) 2006 Christophe Dumez <chris@qbittorrent.org> * Copyright (C) 2006 Christophe Dumez <chris@qbittorrent.org>
* *
* This program is free software; you can redistribute it and/or * This program is free software; you can redistribute it and/or
@ -28,9 +29,10 @@
#include "trackersadditiondialog.h" #include "trackersadditiondialog.h"
#include <QBuffer>
#include <QMessageBox> #include <QMessageBox>
#include <QStringList> #include <QSize>
#include <QStringView>
#include <QVector>
#include "base/bittorrent/torrent.h" #include "base/bittorrent/torrent.h"
#include "base/bittorrent/trackerentry.h" #include "base/bittorrent/trackerentry.h"
@ -39,102 +41,87 @@
#include "gui/uithememanager.h" #include "gui/uithememanager.h"
#include "ui_trackersadditiondialog.h" #include "ui_trackersadditiondialog.h"
#define SETTINGS_KEY(name) u"AddTrackersDialog/" name
TrackersAdditionDialog::TrackersAdditionDialog(QWidget *parent, BitTorrent::Torrent *const torrent) TrackersAdditionDialog::TrackersAdditionDialog(QWidget *parent, BitTorrent::Torrent *const torrent)
: QDialog(parent) : QDialog(parent)
, m_ui(new Ui::TrackersAdditionDialog()) , m_ui(new Ui::TrackersAdditionDialog)
, m_torrent(torrent) , m_torrent(torrent)
, m_storeDialogSize(SETTINGS_KEY(u"Size"_qs))
, m_storeTrackersListURL(SETTINGS_KEY(u"TrackersListURL"_qs))
{ {
m_ui->setupUi(this); m_ui->setupUi(this);
// Icons
m_ui->uTorrentListButton->setIcon(UIThemeManager::instance()->getIcon(u"downloading"_qs)); m_ui->downloadButton->setIcon(UIThemeManager::instance()->getIcon(u"downloading"_qs));
m_ui->buttonBox->button(QDialogButtonBox::Ok)->setText(tr("Add"));
connect(m_ui->downloadButton, &QAbstractButton::clicked, this, &TrackersAdditionDialog::onDownloadButtonClicked);
connect(this, &QDialog::accepted, this, &TrackersAdditionDialog::onAccepted);
loadSettings();
} }
TrackersAdditionDialog::~TrackersAdditionDialog() TrackersAdditionDialog::~TrackersAdditionDialog()
{ {
saveSettings();
delete m_ui; delete m_ui;
} }
QStringList TrackersAdditionDialog::newTrackers() const void TrackersAdditionDialog::onAccepted() const
{ {
const QString plainText = m_ui->textEditTrackersList->toPlainText(); const QVector<BitTorrent::TrackerEntry> entries = BitTorrent::parseTrackerEntries(m_ui->textEditTrackersList->toPlainText());
m_torrent->addTrackers(entries);
}
QStringList cleanTrackers; void TrackersAdditionDialog::onDownloadButtonClicked()
for (QStringView url : asConst(QStringView(plainText).split(u'\n'))) {
const QString url = m_ui->lineEditListURL->text();
if (url.isEmpty())
{ {
url = url.trimmed(); QMessageBox::warning(this, tr("Trackers list URL error"), tr("The trackers list URL cannot be empty"));
if (!url.isEmpty()) return;
cleanTrackers << url.toString();
} }
return cleanTrackers;
}
void TrackersAdditionDialog::on_uTorrentListButton_clicked()
{
m_ui->uTorrentListButton->setEnabled(false);
Net::DownloadManager::instance()->download(m_ui->lineEditListURL->text()
, this, &TrackersAdditionDialog::torrentListDownloadFinished);
// Just to show that it takes times // Just to show that it takes times
m_ui->downloadButton->setEnabled(false);
setCursor(Qt::WaitCursor); setCursor(Qt::WaitCursor);
Net::DownloadManager::instance()->download(url, this, &TrackersAdditionDialog::onTorrentListDownloadFinished);
} }
void TrackersAdditionDialog::torrentListDownloadFinished(const Net::DownloadResult &result) void TrackersAdditionDialog::onTorrentListDownloadFinished(const Net::DownloadResult &result)
{ {
// Restore the cursor, buttons...
m_ui->downloadButton->setEnabled(true);
setCursor(Qt::ArrowCursor);
if (result.status != Net::DownloadStatus::Success) if (result.status != Net::DownloadStatus::Success)
{ {
// To restore the cursor ... QMessageBox::warning(this, tr("Download trackers list error")
setCursor(Qt::ArrowCursor); , tr("Error occurred when downloading the trackers list. Reason: \"%1\"").arg(result.errorString));
m_ui->uTorrentListButton->setEnabled(true);
QMessageBox::warning(
this, tr("Download error")
, tr("The trackers list could not be downloaded, reason: %1")
.arg(result.errorString), QMessageBox::Ok);
return; return;
} }
const QStringList trackersFromUser = m_ui->textEditTrackersList->toPlainText().split(u'\n'); // Add fetched trackers to the list
QVector<BitTorrent::TrackerEntry> existingTrackers = m_torrent->trackers(); const QString existingText = m_ui->textEditTrackersList->toPlainText();
existingTrackers.reserve(trackersFromUser.size()); if (!existingText.isEmpty() && !existingText.endsWith(u'\n'))
for (const QString &userURL : trackersFromUser)
{
const BitTorrent::TrackerEntry userTracker {userURL};
if (!existingTrackers.contains(userTracker))
existingTrackers << userTracker;
}
// Add new trackers to the list
if (!m_ui->textEditTrackersList->toPlainText().isEmpty() && !m_ui->textEditTrackersList->toPlainText().endsWith(u'\n'))
m_ui->textEditTrackersList->insertPlainText(u"\n"_qs); m_ui->textEditTrackersList->insertPlainText(u"\n"_qs);
int nb = 0;
QBuffer buffer;
buffer.setData(result.data);
buffer.open(QBuffer::ReadOnly);
while (!buffer.atEnd())
{
const auto line = QString::fromUtf8(buffer.readLine().trimmed());
if (line.isEmpty()) continue;
BitTorrent::TrackerEntry newTracker {line};
if (!existingTrackers.contains(newTracker))
{
m_ui->textEditTrackersList->insertPlainText(line + u'\n');
++nb;
}
}
// To restore the cursor ... // append the data as-is
setCursor(Qt::ArrowCursor); const auto trackers = QString::fromUtf8(result.data).trimmed();
m_ui->uTorrentListButton->setEnabled(true); m_ui->textEditTrackersList->insertPlainText(trackers);
// Display information message if necessary
if (nb == 0)
QMessageBox::information(this, tr("No change"), tr("No additional trackers were found."), QMessageBox::Ok);
} }
QStringList TrackersAdditionDialog::askForTrackers(QWidget *parent, BitTorrent::Torrent *const torrent) void TrackersAdditionDialog::saveSettings()
{ {
QStringList trackers; m_storeDialogSize = size();
TrackersAdditionDialog dlg(parent, torrent); m_storeTrackersListURL = m_ui->lineEditListURL->text();
if (dlg.exec() == QDialog::Accepted) }
return dlg.newTrackers();
return trackers; void TrackersAdditionDialog::loadSettings()
{
if (const QSize dialogSize = m_storeDialogSize; dialogSize.isValid())
resize(dialogSize);
m_ui->lineEditListURL->setText(m_storeTrackersListURL);
} }

20
src/gui/properties/trackersadditiondialog.h

@ -1,5 +1,6 @@
/* /*
* Bittorrent Client using Qt and libtorrent. * Bittorrent Client using Qt and libtorrent.
* Copyright (C) 2022 Mike Tzou (Chocobo1)
* Copyright (C) 2006 Christophe Dumez <chris@qbittorrent.org> * Copyright (C) 2006 Christophe Dumez <chris@qbittorrent.org>
* *
* This program is free software; you can redistribute it and/or * This program is free software; you can redistribute it and/or
@ -29,9 +30,8 @@
#pragma once #pragma once
#include <QDialog> #include <QDialog>
#include <QtContainerFwd>
class QString; #include "base/settingvalue.h"
namespace BitTorrent namespace BitTorrent
{ {
@ -57,14 +57,18 @@ public:
TrackersAdditionDialog(QWidget *parent, BitTorrent::Torrent *const torrent); TrackersAdditionDialog(QWidget *parent, BitTorrent::Torrent *const torrent);
~TrackersAdditionDialog(); ~TrackersAdditionDialog();
QStringList newTrackers() const; private slots:
static QStringList askForTrackers(QWidget *parent, BitTorrent::Torrent *const torrent); void onAccepted() const;
void onDownloadButtonClicked();
public slots: void onTorrentListDownloadFinished(const Net::DownloadResult &result);
void on_uTorrentListButton_clicked();
void torrentListDownloadFinished(const Net::DownloadResult &result);
private: private:
void saveSettings();
void loadSettings();
Ui::TrackersAdditionDialog *m_ui = nullptr; Ui::TrackersAdditionDialog *m_ui = nullptr;
BitTorrent::Torrent *const m_torrent = nullptr; BitTorrent::Torrent *const m_torrent = nullptr;
SettingValue<QSize> m_storeDialogSize;
SettingValue<QString> m_storeTrackersListURL;
}; };

6
src/gui/properties/trackersadditiondialog.ui

@ -44,7 +44,11 @@
<widget class="QLineEdit" name="lineEditListURL"/> <widget class="QLineEdit" name="lineEditListURL"/>
</item> </item>
<item> <item>
<widget class="QPushButton" name="uTorrentListButton"/> <widget class="QPushButton" name="downloadButton">
<property name="toolTip">
<string>Download trackers list</string>
</property>
</widget>
</item> </item>
</layout> </layout>
</item> </item>

22
src/gui/trackerentriesdialog.cpp

@ -80,27 +80,7 @@ void TrackerEntriesDialog::setTrackers(const QVector<BitTorrent::TrackerEntry> &
QVector<BitTorrent::TrackerEntry> TrackerEntriesDialog::trackers() const QVector<BitTorrent::TrackerEntry> TrackerEntriesDialog::trackers() const
{ {
const QString plainText = m_ui->plainTextEdit->toPlainText(); return BitTorrent::parseTrackerEntries(m_ui->plainTextEdit->toPlainText());
const QList<QStringView> lines = QStringView(plainText).split(u'\n');
QVector<BitTorrent::TrackerEntry> entries;
entries.reserve(lines.size());
int tier = 0;
for (QStringView line : lines)
{
line = line.trimmed();
if (line.isEmpty())
{
++tier;
continue;
}
entries.append({line.toString(), tier});
}
return entries;
} }
void TrackerEntriesDialog::saveSettings() void TrackerEntriesDialog::saveSettings()

10
src/webui/api/torrentscontroller.cpp

@ -745,14 +745,8 @@ void TorrentsController::addTrackersAction()
if (!torrent) if (!torrent)
throw APIError(APIErrorType::NotFound); throw APIError(APIErrorType::NotFound);
QVector<BitTorrent::TrackerEntry> trackers; const QVector<BitTorrent::TrackerEntry> entries = BitTorrent::parseTrackerEntries(params()[u"urls"_qs]);
for (const QString &urlStr : asConst(params()[u"urls"_qs].split(u'\n'))) torrent->addTrackers(entries);
{
const QUrl url {urlStr.trimmed()};
if (url.isValid())
trackers.append({url.toString()});
}
torrent->addTrackers(trackers);
} }
void TorrentsController::editTrackerAction() void TorrentsController::editTrackerAction()

1
test/CMakeLists.txt

@ -11,6 +11,7 @@ include_directories("../src")
set(testFiles set(testFiles
testalgorithm.cpp testalgorithm.cpp
testbittorrenttrackerentry.cpp
testorderedset.cpp testorderedset.cpp
testpath.cpp testpath.cpp
testutilscompare.cpp testutilscompare.cpp

143
test/testbittorrenttrackerentry.cpp

@ -0,0 +1,143 @@
/*
* Bittorrent Client using Qt and libtorrent.
* Copyright (C) 2022 Mike Tzou (Chocobo1)
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License
* as published by the Free Software Foundation; either version 2
* of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*
* In addition, as a special exception, the copyright holders give permission to
* link this program with the OpenSSL project's "OpenSSL" library (or with
* modified versions of it that use the same license as the "OpenSSL" library),
* and distribute the linked executables. You must obey the GNU General Public
* License in all respects for all of the code used other than "OpenSSL". If you
* modify file(s), you may extend this exception to your version of the file(s),
* but you are not obligated to do so. If you do not wish to do so, delete this
* exception statement from your version.
*/
#include <algorithm>
#include <QTest>
#include <QVector>
#include "base/bittorrent/trackerentry.h"
#include "base/global.h"
class TestBittorrentTrackerEntry final : public QObject
{
Q_OBJECT
Q_DISABLE_COPY_MOVE(TestBittorrentTrackerEntry)
public:
TestBittorrentTrackerEntry() = default;
private slots:
void testParseTrackerEntries() const
{
using Entries = QVector<BitTorrent::TrackerEntry>;
const auto isEqual = [](const Entries &left, const Entries &right) -> bool
{
return std::equal(left.begin(), left.end(), right.begin(), right.end()
, [](const BitTorrent::TrackerEntry &leftEntry, const BitTorrent::TrackerEntry &rightEntry)
{
return (leftEntry.url == rightEntry.url)
&& (leftEntry.tier == rightEntry.tier);
});
};
{
const QString input;
const Entries output;
QVERIFY(isEqual(BitTorrent::parseTrackerEntries(input), output));
}
{
const QString input = u"http://localhost:1234"_qs;
const Entries output = {{u"http://localhost:1234"_qs, 0}};
QVERIFY(isEqual(BitTorrent::parseTrackerEntries(input), output));
}
{
const QString input = u" http://localhost:1234 "_qs;
const Entries output = {{u"http://localhost:1234"_qs, 0}};
QVERIFY(isEqual(BitTorrent::parseTrackerEntries(input), output));
}
{
const QString input = u"\nhttp://localhost:1234"_qs;
const Entries output = {{u"http://localhost:1234"_qs, 1}};
QVERIFY(isEqual(BitTorrent::parseTrackerEntries(input), output));
}
{
const QString input = u"http://localhost:1234\n"_qs;
const Entries output = {{u"http://localhost:1234"_qs, 0}};
QVERIFY(isEqual(BitTorrent::parseTrackerEntries(input), output));
}
{
const QString input = u"http://localhost:1234 \n http://[::1]:4567"_qs;
const Entries output =
{
{u"http://localhost:1234"_qs, 0},
{u"http://[::1]:4567"_qs, 0}
};
QVERIFY(isEqual(BitTorrent::parseTrackerEntries(input), output));
}
{
const QString input = u"\n http://localhost:1234 \n http://[::1]:4567"_qs;
const Entries output =
{
{u"http://localhost:1234"_qs, 1},
{u"http://[::1]:4567"_qs, 1}
};
QVERIFY(isEqual(BitTorrent::parseTrackerEntries(input), output));
}
{
const QString input = u"http://localhost:1234 \n http://[::1]:4567 \n \n \n"_qs;
const Entries output =
{
{u"http://localhost:1234"_qs, 0},
{u"http://[::1]:4567"_qs, 0}
};
QVERIFY(isEqual(BitTorrent::parseTrackerEntries(input), output));
}
{
const QString input = u"http://localhost:1234 \n \n http://[::1]:4567"_qs;
const Entries output =
{
{u"http://localhost:1234"_qs, 0},
{u"http://[::1]:4567"_qs, 1}
};
QVERIFY(isEqual(BitTorrent::parseTrackerEntries(input), output));
}
{
const QString input = u"\n \n \n http://localhost:1234 \n \n \n \n http://[::1]:4567 \n \n \n"_qs;
const Entries output =
{
{u"http://localhost:1234"_qs, 3},
{u"http://[::1]:4567"_qs, 6}
};
QVERIFY(isEqual(BitTorrent::parseTrackerEntries(input), output));
}
}
};
QTEST_APPLESS_MAIN(TestBittorrentTrackerEntry)
#include "testbittorrenttrackerentry.moc"
Loading…
Cancel
Save