Browse Source

Restore BitTorrent session asynchronously

Reduce the total startup time of the application and maintain sufficient responsiveness of the UI during startup due to the following:
1. Load resume data from disk asynchronously in separate thread;
2. Split handling of loaded resume data in chunks;
3. Reduce the number of emitting signals.

PR #16840.
adaptive-webui-19844
Vladimir Golovnev 2 years ago committed by GitHub
parent
commit
be7cfb78de
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 119
      src/app/application.cpp
  2. 16
      src/app/application.h
  3. 1
      src/base/CMakeLists.txt
  4. 1
      src/base/base.pri
  5. 122
      src/base/bittorrent/bencoderesumedatastorage.cpp
  6. 8
      src/base/bittorrent/bencoderesumedatastorage.h
  7. 169
      src/base/bittorrent/dbresumedatastorage.cpp
  8. 8
      src/base/bittorrent/dbresumedatastorage.h
  9. 76
      src/base/bittorrent/resumedatastorage.cpp
  10. 43
      src/base/bittorrent/resumedatastorage.h
  11. 658
      src/base/bittorrent/session.cpp
  12. 19
      src/base/bittorrent/session.h
  13. 19
      src/base/rss/rss_autodownloader.cpp
  14. 50
      src/base/torrentfileswatcher.cpp
  15. 1
      src/base/torrentfileswatcher.h
  16. 16
      src/gui/categoryfiltermodel.cpp
  17. 10
      src/gui/categoryfiltermodel.h
  18. 7
      src/gui/mainwindow.cpp
  19. 10
      src/gui/search/searchjobwidget.cpp
  20. 20
      src/gui/tagfiltermodel.cpp
  21. 10
      src/gui/tagfiltermodel.h
  22. 64
      src/gui/transferlistfilterswidget.cpp
  23. 8
      src/gui/transferlistfilterswidget.h
  24. 21
      src/gui/transferlistmodel.cpp
  25. 2
      src/gui/transferlistmodel.h

119
src/app/application.cpp

@ -309,10 +309,18 @@ void Application::setFileLoggerAgeType(const int value)
void Application::processMessage(const QString &message) void Application::processMessage(const QString &message)
{ {
const QStringList params = message.split(PARAMS_SEPARATOR, Qt::SkipEmptyParts); #ifndef DISABLE_GUI
// If Application is not running (i.e., other if (message.isEmpty())
// components are not ready) store params {
if (m_running) m_window->activate(); // show UI
return;
}
#endif
const AddTorrentParams params = parseParams(message.split(PARAMS_SEPARATOR, Qt::SkipEmptyParts));
// If Application is not allowed to process params immediately
// (i.e., other components are not ready) store params
if (m_isProcessingParamsAllowed)
processParams(params); processParams(params);
else else
m_paramsQueue.append(params); m_paramsQueue.append(params);
@ -516,21 +524,10 @@ bool Application::sendParams(const QStringList &params)
return m_instanceManager->sendMessage(params.join(PARAMS_SEPARATOR)); return m_instanceManager->sendMessage(params.join(PARAMS_SEPARATOR));
} }
// As program parameters, we can get paths or urls. Application::AddTorrentParams Application::parseParams(const QStringList &params) const
// This function parse the parameters and call
// the right addTorrent function, considering
// the parameter type.
void Application::processParams(const QStringList &params)
{ {
#ifndef DISABLE_GUI AddTorrentParams parsedParams;
if (params.isEmpty()) BitTorrent::AddTorrentParams &torrentParams = parsedParams.torrentParams;
{
m_window->activate(); // show UI
return;
}
#endif
BitTorrent::AddTorrentParams torrentParams;
std::optional<bool> skipTorrentDialog;
for (QString param : params) for (QString param : params)
{ {
@ -576,23 +573,31 @@ void Application::processParams(const QStringList &params)
if (param.startsWith(u"@skipDialog=")) if (param.startsWith(u"@skipDialog="))
{ {
skipTorrentDialog = (QStringView(param).mid(12).toInt() != 0); parsedParams.skipTorrentDialog = (QStringView(param).mid(12).toInt() != 0);
continue; continue;
} }
parsedParams.torrentSource = param;
break;
}
return parsedParams;
}
void Application::processParams(const AddTorrentParams &params)
{
#ifndef DISABLE_GUI #ifndef DISABLE_GUI
// There are two circumstances in which we want to show the torrent // There are two circumstances in which we want to show the torrent
// dialog. One is when the application settings specify that it should // dialog. One is when the application settings specify that it should
// be shown and skipTorrentDialog is undefined. The other is when // be shown and skipTorrentDialog is undefined. The other is when
// skipTorrentDialog is false, meaning that the application setting // skipTorrentDialog is false, meaning that the application setting
// should be overridden. // should be overridden.
const bool showDialogForThisTorrent = !skipTorrentDialog.value_or(!AddNewTorrentDialog::isEnabled()); const bool showDialogForThisTorrent = !params.skipTorrentDialog.value_or(!AddNewTorrentDialog::isEnabled());
if (showDialogForThisTorrent) if (showDialogForThisTorrent)
AddNewTorrentDialog::show(param, torrentParams, m_window); AddNewTorrentDialog::show(params.torrentSource, params.torrentParams, m_window);
else else
#endif #endif
BitTorrent::Session::instance()->addTorrent(param, torrentParams); BitTorrent::Session::instance()->addTorrent(params.torrentSource, params.torrentParams);
}
} }
int Application::exec(const QStringList &params) int Application::exec(const QStringList &params)
@ -612,21 +617,28 @@ int Application::exec(const QStringList &params)
try try
{ {
BitTorrent::Session::initInstance(); BitTorrent::Session::initInstance();
connect(BitTorrent::Session::instance(), &BitTorrent::Session::restored, this, [this]()
{
#ifndef DISABLE_WEBUI
m_webui = new WebUI(this);
#ifdef DISABLE_GUI
if (m_webui->isErrored())
QCoreApplication::exit(1);
connect(m_webui, &WebUI::fatalError, this, []() { QCoreApplication::exit(1); });
#endif // DISABLE_GUI
#endif // DISABLE_WEBUI
m_isProcessingParamsAllowed = true;
for (const AddTorrentParams &params : m_paramsQueue)
processParams(params);
m_paramsQueue.clear();
});
connect(BitTorrent::Session::instance(), &BitTorrent::Session::torrentFinished, this, &Application::torrentFinished); connect(BitTorrent::Session::instance(), &BitTorrent::Session::torrentFinished, this, &Application::torrentFinished);
connect(BitTorrent::Session::instance(), &BitTorrent::Session::allTorrentsFinished, this, &Application::allTorrentsFinished, Qt::QueuedConnection); connect(BitTorrent::Session::instance(), &BitTorrent::Session::allTorrentsFinished, this, &Application::allTorrentsFinished, Qt::QueuedConnection);
Net::GeoIPManager::initInstance(); Net::GeoIPManager::initInstance();
TorrentFilesWatcher::initInstance(); TorrentFilesWatcher::initInstance();
#ifndef DISABLE_WEBUI
m_webui = new WebUI(this);
#ifdef DISABLE_GUI
if (m_webui->isErrored())
return 1;
connect(m_webui, &WebUI::fatalError, this, []() { QCoreApplication::exit(1); });
#endif // DISABLE_GUI
#endif // DISABLE_WEBUI
new RSS::Session; // create RSS::Session singleton new RSS::Session; // create RSS::Session singleton
new RSS::AutoDownloader; // create RSS::AutoDownloader singleton new RSS::AutoDownloader; // create RSS::AutoDownloader singleton
} }
@ -669,17 +681,9 @@ int Application::exec(const QStringList &params)
m_window = new MainWindow(this); m_window = new MainWindow(this);
#endif // DISABLE_GUI #endif // DISABLE_GUI
m_running = true; if (!params.isEmpty())
m_paramsQueue.append(parseParams(params));
// Now UI is ready to process signals from Session
BitTorrent::Session::instance()->startUpTorrents();
m_paramsQueue = params + m_paramsQueue;
if (!m_paramsQueue.isEmpty())
{
processParams(m_paramsQueue);
m_paramsQueue.clear();
}
return BaseApplication::exec(); return BaseApplication::exec();
} }
@ -699,16 +703,19 @@ bool Application::event(QEvent *ev)
// Get the url instead // Get the url instead
path = static_cast<QFileOpenEvent *>(ev)->url().toString(); path = static_cast<QFileOpenEvent *>(ev)->url().toString();
qDebug("Received a mac file open event: %s", qUtf8Printable(path)); qDebug("Received a mac file open event: %s", qUtf8Printable(path));
if (m_running)
processParams(QStringList(path)); const AddTorrentParams params = parseParams({path});
// If Application is not allowed to process params immediately
// (i.e., other components are not ready) store params
if (m_isProcessingParamsAllowed)
processParams(params);
else else
m_paramsQueue.append(path); m_paramsQueue.append(params);
return true; return true;
} }
else
{ return BaseApplication::event(ev);
return BaseApplication::event(ev);
}
} }
#endif // Q_OS_MACOS #endif // Q_OS_MACOS
#endif // DISABLE_GUI #endif // DISABLE_GUI

16
src/app/application.h

@ -41,6 +41,7 @@
#include <QApplication> #include <QApplication>
#endif #endif
#include "base/bittorrent/addtorrentparams.h"
#include "base/interfaces/iapplication.h" #include "base/interfaces/iapplication.h"
#include "base/path.h" #include "base/path.h"
#include "base/settingvalue.h" #include "base/settingvalue.h"
@ -132,8 +133,16 @@ private slots:
#endif #endif
private: private:
struct AddTorrentParams
{
QString torrentSource;
BitTorrent::AddTorrentParams torrentParams;
std::optional<bool> skipTorrentDialog;
};
void initializeTranslation(); void initializeTranslation();
void processParams(const QStringList &params); AddTorrentParams parseParams(const QStringList &params) const;
void processParams(const AddTorrentParams &params);
void runExternalProgram(const BitTorrent::Torrent *torrent) const; void runExternalProgram(const BitTorrent::Torrent *torrent) const;
void sendNotificationEmail(const BitTorrent::Torrent *torrent); void sendNotificationEmail(const BitTorrent::Torrent *torrent);
@ -152,8 +161,8 @@ private:
#endif #endif
ApplicationInstanceManager *m_instanceManager = nullptr; ApplicationInstanceManager *m_instanceManager = nullptr;
bool m_running = false;
QAtomicInt m_isCleanupRun; QAtomicInt m_isCleanupRun;
bool m_isProcessingParamsAllowed = false;
ShutdownDialogAction m_shutdownAct; ShutdownDialogAction m_shutdownAct;
QBtCommandLineParameters m_commandLineArgs; QBtCommandLineParameters m_commandLineArgs;
@ -162,7 +171,8 @@ private:
QTranslator m_qtTranslator; QTranslator m_qtTranslator;
QTranslator m_translator; QTranslator m_translator;
QStringList m_paramsQueue;
QList<AddTorrentParams> m_paramsQueue;
SettingValue<bool> m_storeFileLoggerEnabled; SettingValue<bool> m_storeFileLoggerEnabled;
SettingValue<bool> m_storeFileLoggerBackup; SettingValue<bool> m_storeFileLoggerBackup;

1
src/base/CMakeLists.txt

@ -124,6 +124,7 @@ add_library(qbt_base STATIC
bittorrent/peeraddress.cpp bittorrent/peeraddress.cpp
bittorrent/peerinfo.cpp bittorrent/peerinfo.cpp
bittorrent/portforwarderimpl.cpp bittorrent/portforwarderimpl.cpp
bittorrent/resumedatastorage.cpp
bittorrent/session.cpp bittorrent/session.cpp
bittorrent/speedmonitor.cpp bittorrent/speedmonitor.cpp
bittorrent/statistics.cpp bittorrent/statistics.cpp

1
src/base/base.pri

@ -124,6 +124,7 @@ SOURCES += \
$$PWD/bittorrent/peeraddress.cpp \ $$PWD/bittorrent/peeraddress.cpp \
$$PWD/bittorrent/peerinfo.cpp \ $$PWD/bittorrent/peerinfo.cpp \
$$PWD/bittorrent/portforwarderimpl.cpp \ $$PWD/bittorrent/portforwarderimpl.cpp \
$$PWD/bittorrent/resumedatastorage.cpp \
$$PWD/bittorrent/session.cpp \ $$PWD/bittorrent/session.cpp \
$$PWD/bittorrent/speedmonitor.cpp \ $$PWD/bittorrent/speedmonitor.cpp \
$$PWD/bittorrent/statistics.cpp \ $$PWD/bittorrent/statistics.cpp \

122
src/base/bittorrent/bencoderesumedatastorage.cpp

@ -90,21 +90,20 @@ namespace
} }
BitTorrent::BencodeResumeDataStorage::BencodeResumeDataStorage(const Path &path, QObject *parent) BitTorrent::BencodeResumeDataStorage::BencodeResumeDataStorage(const Path &path, QObject *parent)
: ResumeDataStorage {parent} : ResumeDataStorage(path, parent)
, m_resumeDataPath {path} , m_ioThread {new QThread(this)}
, m_ioThread {new QThread {this}} , m_asyncWorker {new Worker(path)}
, m_asyncWorker {new Worker(m_resumeDataPath)}
{ {
Q_ASSERT(path.isAbsolute()); Q_ASSERT(path.isAbsolute());
if (!m_resumeDataPath.exists() && !Utils::Fs::mkpath(m_resumeDataPath)) if (!path.exists() && !Utils::Fs::mkpath(path))
{ {
throw RuntimeError(tr("Cannot create torrent resume folder: \"%1\"") throw RuntimeError(tr("Cannot create torrent resume folder: \"%1\"")
.arg(m_resumeDataPath.toString())); .arg(path.toString()));
} }
const QRegularExpression filenamePattern {u"^([A-Fa-f0-9]{40})\\.fastresume$"_qs}; const QRegularExpression filenamePattern {u"^([A-Fa-f0-9]{40})\\.fastresume$"_qs};
const QStringList filenames = QDir(m_resumeDataPath.data()).entryList(QStringList(u"*.fastresume"_qs), QDir::Files, QDir::Unsorted); const QStringList filenames = QDir(path.data()).entryList(QStringList(u"*.fastresume"_qs), QDir::Files, QDir::Unsorted);
m_registeredTorrents.reserve(filenames.size()); m_registeredTorrents.reserve(filenames.size());
for (const QString &filename : filenames) for (const QString &filename : filenames)
@ -114,7 +113,7 @@ BitTorrent::BencodeResumeDataStorage::BencodeResumeDataStorage(const Path &path,
m_registeredTorrents.append(TorrentID::fromString(rxMatch.captured(1))); m_registeredTorrents.append(TorrentID::fromString(rxMatch.captured(1)));
} }
loadQueue(m_resumeDataPath / Path(u"queue"_qs)); loadQueue(path / Path(u"queue"_qs));
qDebug() << "Registered torrents count: " << m_registeredTorrents.size(); qDebug() << "Registered torrents count: " << m_registeredTorrents.size();
@ -134,25 +133,19 @@ QVector<BitTorrent::TorrentID> BitTorrent::BencodeResumeDataStorage::registeredT
return m_registeredTorrents; return m_registeredTorrents;
} }
std::optional<BitTorrent::LoadTorrentParams> BitTorrent::BencodeResumeDataStorage::load(const TorrentID &id) const BitTorrent::LoadResumeDataResult BitTorrent::BencodeResumeDataStorage::load(const TorrentID &id) const
{ {
const QString idString = id.toString(); const QString idString = id.toString();
const Path fastresumePath = m_resumeDataPath / Path(idString + u".fastresume"); const Path fastresumePath = path() / Path(idString + u".fastresume");
const Path torrentFilePath = m_resumeDataPath / Path(idString + u".torrent"); const Path torrentFilePath = path() / Path(idString + u".torrent");
QFile resumeDataFile {fastresumePath.data()}; QFile resumeDataFile {fastresumePath.data()};
if (!resumeDataFile.open(QIODevice::ReadOnly)) if (!resumeDataFile.open(QIODevice::ReadOnly))
{ return nonstd::make_unexpected(tr("Cannot read file %1: %2").arg(fastresumePath.toString(), resumeDataFile.errorString()));
LogMsg(tr("Cannot read file %1: %2").arg(fastresumePath.toString(), resumeDataFile.errorString()), Log::WARNING);
return std::nullopt;
}
QFile metadataFile {torrentFilePath.data()}; QFile metadataFile {torrentFilePath.data()};
if (metadataFile.exists() && !metadataFile.open(QIODevice::ReadOnly)) if (metadataFile.exists() && !metadataFile.open(QIODevice::ReadOnly))
{ return nonstd::make_unexpected(tr("Cannot read file %1: %2").arg(torrentFilePath.toString(), metadataFile.errorString()));
LogMsg(tr("Cannot read file %1: %2").arg(torrentFilePath.toString(), metadataFile.errorString()), Log::WARNING);
return std::nullopt;
}
const QByteArray data = resumeDataFile.readAll(); const QByteArray data = resumeDataFile.readAll();
const QByteArray metadata = (metadataFile.isOpen() ? metadataFile.readAll() : ""); const QByteArray metadata = (metadataFile.isOpen() ? metadataFile.readAll() : "");
@ -160,16 +153,64 @@ std::optional<BitTorrent::LoadTorrentParams> BitTorrent::BencodeResumeDataStorag
return loadTorrentResumeData(data, metadata); return loadTorrentResumeData(data, metadata);
} }
std::optional<BitTorrent::LoadTorrentParams> BitTorrent::BencodeResumeDataStorage::loadTorrentResumeData( void BitTorrent::BencodeResumeDataStorage::doLoadAll() const
const QByteArray &data, const QByteArray &metadata) const {
qDebug() << "Loading torrents count: " << m_registeredTorrents.size();
emit const_cast<BencodeResumeDataStorage *>(this)->loadStarted(m_registeredTorrents);
for (const TorrentID &torrentID : asConst(m_registeredTorrents))
onResumeDataLoaded(torrentID, load(torrentID));
emit const_cast<BencodeResumeDataStorage *>(this)->loadFinished();
}
void BitTorrent::BencodeResumeDataStorage::loadQueue(const Path &queueFilename)
{
QFile queueFile {queueFilename.data()};
if (!queueFile.exists())
return;
if (!queueFile.open(QFile::ReadOnly))
{
LogMsg(tr("Couldn't load torrents queue: %1").arg(queueFile.errorString()), Log::WARNING);
return;
}
const QRegularExpression hashPattern {u"^([A-Fa-f0-9]{40})$"_qs};
int start = 0;
while (true)
{
const auto line = QString::fromLatin1(queueFile.readLine().trimmed());
if (line.isEmpty())
break;
const QRegularExpressionMatch rxMatch = hashPattern.match(line);
if (rxMatch.hasMatch())
{
const auto torrentID = BitTorrent::TorrentID::fromString(rxMatch.captured(1));
const int pos = m_registeredTorrents.indexOf(torrentID, start);
if (pos != -1)
{
std::swap(m_registeredTorrents[start], m_registeredTorrents[pos]);
++start;
}
}
}
}
BitTorrent::LoadResumeDataResult BitTorrent::BencodeResumeDataStorage::loadTorrentResumeData(const QByteArray &data, const QByteArray &metadata) const
{ {
const QByteArray allData = ((metadata.isEmpty() || data.isEmpty()) const QByteArray allData = ((metadata.isEmpty() || data.isEmpty())
? data : (data.chopped(1) + metadata.mid(1))); ? data : (data.chopped(1) + metadata.mid(1)));
lt::error_code ec; lt::error_code ec;
const lt::bdecode_node root = lt::bdecode(allData, ec); const lt::bdecode_node root = lt::bdecode(allData, ec);
if (ec || (root.type() != lt::bdecode_node::dict_t)) if (ec)
return std::nullopt; return nonstd::make_unexpected(tr("Cannot parse resume data: %1").arg(QString::fromStdString(ec.message())));
if (root.type() != lt::bdecode_node::dict_t)
return nonstd::make_unexpected(tr("Cannot parse resume data: invalid foormat"));
LoadTorrentParams torrentParams; LoadTorrentParams torrentParams;
torrentParams.restored = true; torrentParams.restored = true;
@ -247,7 +288,7 @@ std::optional<BitTorrent::LoadTorrentParams> BitTorrent::BencodeResumeDataStorag
const bool hasMetadata = (p.ti && p.ti->is_valid()); const bool hasMetadata = (p.ti && p.ti->is_valid());
if (!hasMetadata && !root.dict_find("info-hash")) if (!hasMetadata && !root.dict_find("info-hash"))
return std::nullopt; return nonstd::make_unexpected(tr("Resume data is invalid: neither metadata nor info-hash was found"));
return torrentParams; return torrentParams;
} }
@ -276,39 +317,6 @@ void BitTorrent::BencodeResumeDataStorage::storeQueue(const QVector<TorrentID> &
}); });
} }
void BitTorrent::BencodeResumeDataStorage::loadQueue(const Path &queueFilename)
{
QFile queueFile {queueFilename.data()};
if (!queueFile.exists())
return;
if (queueFile.open(QFile::ReadOnly))
{
const QRegularExpression hashPattern {u"^([A-Fa-f0-9]{40})$"_qs};
QString line;
int start = 0;
while (!(line = QString::fromLatin1(queueFile.readLine().trimmed())).isEmpty())
{
const QRegularExpressionMatch rxMatch = hashPattern.match(line);
if (rxMatch.hasMatch())
{
const auto torrentID = TorrentID::fromString(rxMatch.captured(1));
const int pos = m_registeredTorrents.indexOf(torrentID, start);
if (pos != -1)
{
std::swap(m_registeredTorrents[start], m_registeredTorrents[pos]);
++start;
}
}
}
}
else
{
LogMsg(tr("Couldn't load torrents queue from '%1'. Error: %2")
.arg(queueFile.fileName(), queueFile.errorString()), Log::WARNING);
}
}
BitTorrent::BencodeResumeDataStorage::Worker::Worker(const Path &resumeDataDir) BitTorrent::BencodeResumeDataStorage::Worker::Worker(const Path &resumeDataDir)
: m_resumeDataDir {resumeDataDir} : m_resumeDataDir {resumeDataDir}
{ {

8
src/base/bittorrent/bencoderesumedatastorage.h

@ -31,7 +31,7 @@
#include <QDir> #include <QDir>
#include <QVector> #include <QVector>
#include "base/path.h" #include "base/pathfwd.h"
#include "resumedatastorage.h" #include "resumedatastorage.h"
class QByteArray; class QByteArray;
@ -49,16 +49,16 @@ namespace BitTorrent
~BencodeResumeDataStorage() override; ~BencodeResumeDataStorage() override;
QVector<TorrentID> registeredTorrents() const override; QVector<TorrentID> registeredTorrents() const override;
std::optional<LoadTorrentParams> load(const TorrentID &id) const override; LoadResumeDataResult load(const TorrentID &id) const override;
void store(const TorrentID &id, const LoadTorrentParams &resumeData) const override; void store(const TorrentID &id, const LoadTorrentParams &resumeData) const override;
void remove(const TorrentID &id) const override; void remove(const TorrentID &id) const override;
void storeQueue(const QVector<TorrentID> &queue) const override; void storeQueue(const QVector<TorrentID> &queue) const override;
private: private:
void doLoadAll() const override;
void loadQueue(const Path &queueFilename); void loadQueue(const Path &queueFilename);
std::optional<LoadTorrentParams> loadTorrentResumeData(const QByteArray &data, const QByteArray &metadata) const; LoadResumeDataResult loadTorrentResumeData(const QByteArray &data, const QByteArray &metadata) const;
const Path m_resumeDataPath;
QVector<TorrentID> m_registeredTorrents; QVector<TorrentID> m_registeredTorrents;
QThread *m_ioThread = nullptr; QThread *m_ioThread = nullptr;

169
src/base/bittorrent/dbresumedatastorage.cpp

@ -175,7 +175,7 @@ namespace BitTorrent
Q_DISABLE_COPY_MOVE(Worker) Q_DISABLE_COPY_MOVE(Worker)
public: public:
Worker(const Path &dbPath, const QString &dbConnectionName); Worker(const Path &dbPath, const QString &dbConnectionName, QReadWriteLock &dbLock);
void openDatabase() const; void openDatabase() const;
void closeDatabase() const; void closeDatabase() const;
@ -187,11 +187,64 @@ namespace BitTorrent
private: private:
const Path m_path; const Path m_path;
const QString m_connectionName; const QString m_connectionName;
QReadWriteLock &m_dbLock;
}; };
namespace
{
LoadTorrentParams parseQueryResultRow(const QSqlQuery &query)
{
LoadTorrentParams resumeData;
resumeData.restored = true;
resumeData.name = query.value(DB_COLUMN_NAME.name).toString();
resumeData.category = query.value(DB_COLUMN_CATEGORY.name).toString();
const QString tagsData = query.value(DB_COLUMN_TAGS.name).toString();
if (!tagsData.isEmpty())
{
const QStringList tagList = tagsData.split(u',');
resumeData.tags.insert(tagList.cbegin(), tagList.cend());
}
resumeData.hasSeedStatus = query.value(DB_COLUMN_HAS_SEED_STATUS.name).toBool();
resumeData.firstLastPiecePriority = query.value(DB_COLUMN_HAS_OUTER_PIECES_PRIORITY.name).toBool();
resumeData.ratioLimit = query.value(DB_COLUMN_RATIO_LIMIT.name).toInt() / 1000.0;
resumeData.seedingTimeLimit = query.value(DB_COLUMN_SEEDING_TIME_LIMIT.name).toInt();
resumeData.contentLayout = Utils::String::toEnum<TorrentContentLayout>(
query.value(DB_COLUMN_CONTENT_LAYOUT.name).toString(), TorrentContentLayout::Original);
resumeData.operatingMode = Utils::String::toEnum<TorrentOperatingMode>(
query.value(DB_COLUMN_OPERATING_MODE.name).toString(), TorrentOperatingMode::AutoManaged);
resumeData.stopped = query.value(DB_COLUMN_STOPPED.name).toBool();
resumeData.savePath = Profile::instance()->fromPortablePath(
Path(query.value(DB_COLUMN_TARGET_SAVE_PATH.name).toString()));
resumeData.useAutoTMM = resumeData.savePath.isEmpty();
if (!resumeData.useAutoTMM)
{
resumeData.downloadPath = Profile::instance()->fromPortablePath(
Path(query.value(DB_COLUMN_DOWNLOAD_PATH.name).toString()));
}
const QByteArray bencodedResumeData = query.value(DB_COLUMN_RESUMEDATA.name).toByteArray();
const QByteArray bencodedMetadata = query.value(DB_COLUMN_METADATA.name).toByteArray();
const QByteArray allData = ((bencodedMetadata.isEmpty() || bencodedResumeData.isEmpty())
? bencodedResumeData
: (bencodedResumeData.chopped(1) + bencodedMetadata.mid(1)));
lt::error_code ec;
const lt::bdecode_node root = lt::bdecode(allData, ec);
lt::add_torrent_params &p = resumeData.ltAddTorrentParams;
p = lt::read_resume_data(root, ec);
p.save_path = Profile::instance()->fromPortablePath(Path(fromLTString(p.save_path)))
.toString().toStdString();
return resumeData;
}
}
} }
BitTorrent::DBResumeDataStorage::DBResumeDataStorage(const Path &dbPath, QObject *parent) BitTorrent::DBResumeDataStorage::DBResumeDataStorage(const Path &dbPath, QObject *parent)
: ResumeDataStorage {parent} : ResumeDataStorage(dbPath, parent)
, m_ioThread {new QThread(this)} , m_ioThread {new QThread(this)}
{ {
const bool needCreateDB = !dbPath.exists(); const bool needCreateDB = !dbPath.exists();
@ -212,7 +265,7 @@ BitTorrent::DBResumeDataStorage::DBResumeDataStorage(const Path &dbPath, QObject
updateDBFromVersion1(); updateDBFromVersion1();
} }
m_asyncWorker = new Worker(dbPath, u"ResumeDataStorageWorker"_qs); m_asyncWorker = new Worker(dbPath, u"ResumeDataStorageWorker"_qs, m_dbLock);
m_asyncWorker->moveToThread(m_ioThread); m_asyncWorker->moveToThread(m_ioThread);
connect(m_ioThread, &QThread::finished, m_asyncWorker, &QObject::deleteLater); connect(m_ioThread, &QThread::finished, m_asyncWorker, &QObject::deleteLater);
m_ioThread->start(); m_ioThread->start();
@ -262,7 +315,7 @@ QVector<BitTorrent::TorrentID> BitTorrent::DBResumeDataStorage::registeredTorren
return registeredTorrents; return registeredTorrents;
} }
std::optional<BitTorrent::LoadTorrentParams> BitTorrent::DBResumeDataStorage::load(const TorrentID &id) const BitTorrent::LoadResumeDataResult BitTorrent::DBResumeDataStorage::load(const TorrentID &id) const
{ {
const QString selectTorrentStatement = u"SELECT * FROM %1 WHERE %2 = %3;"_qs const QString selectTorrentStatement = u"SELECT * FROM %1 WHERE %2 = %3;"_qs
.arg(quoted(DB_TABLE_TORRENTS), quoted(DB_COLUMN_TORRENT_ID.name), DB_COLUMN_TORRENT_ID.placeholder); .arg(quoted(DB_TABLE_TORRENTS), quoted(DB_COLUMN_TORRENT_ID.name), DB_COLUMN_TORRENT_ID.placeholder);
@ -283,56 +336,11 @@ std::optional<BitTorrent::LoadTorrentParams> BitTorrent::DBResumeDataStorage::lo
} }
catch (const RuntimeError &err) catch (const RuntimeError &err)
{ {
LogMsg(tr("Couldn't load resume data of torrent '%1'. Error: %2") return nonstd::make_unexpected(tr("Couldn't load resume data of torrent '%1'. Error: %2")
.arg(id.toString(), err.message()), Log::CRITICAL); .arg(id.toString(), err.message()));
return std::nullopt;
} }
LoadTorrentParams resumeData; return parseQueryResultRow(query);
resumeData.restored = true;
resumeData.name = query.value(DB_COLUMN_NAME.name).toString();
resumeData.category = query.value(DB_COLUMN_CATEGORY.name).toString();
const QString tagsData = query.value(DB_COLUMN_TAGS.name).toString();
if (!tagsData.isEmpty())
{
const QStringList tagList = tagsData.split(u',');
resumeData.tags.insert(tagList.cbegin(), tagList.cend());
}
resumeData.hasSeedStatus = query.value(DB_COLUMN_HAS_SEED_STATUS.name).toBool();
resumeData.firstLastPiecePriority = query.value(DB_COLUMN_HAS_OUTER_PIECES_PRIORITY.name).toBool();
resumeData.ratioLimit = query.value(DB_COLUMN_RATIO_LIMIT.name).toInt() / 1000.0;
resumeData.seedingTimeLimit = query.value(DB_COLUMN_SEEDING_TIME_LIMIT.name).toInt();
resumeData.contentLayout = Utils::String::toEnum<TorrentContentLayout>(
query.value(DB_COLUMN_CONTENT_LAYOUT.name).toString(), TorrentContentLayout::Original);
resumeData.operatingMode = Utils::String::toEnum<TorrentOperatingMode>(
query.value(DB_COLUMN_OPERATING_MODE.name).toString(), TorrentOperatingMode::AutoManaged);
resumeData.stopped = query.value(DB_COLUMN_STOPPED.name).toBool();
resumeData.savePath = Profile::instance()->fromPortablePath(
Path(query.value(DB_COLUMN_TARGET_SAVE_PATH.name).toString()));
resumeData.useAutoTMM = resumeData.savePath.isEmpty();
if (!resumeData.useAutoTMM)
{
resumeData.downloadPath = Profile::instance()->fromPortablePath(
Path(query.value(DB_COLUMN_DOWNLOAD_PATH.name).toString()));
}
const QByteArray bencodedResumeData = query.value(DB_COLUMN_RESUMEDATA.name).toByteArray();
const QByteArray bencodedMetadata = query.value(DB_COLUMN_METADATA.name).toByteArray();
const QByteArray allData = ((bencodedMetadata.isEmpty() || bencodedResumeData.isEmpty())
? bencodedResumeData
: (bencodedResumeData.chopped(1) + bencodedMetadata.mid(1)));
lt::error_code ec;
const lt::bdecode_node root = lt::bdecode(allData, ec);
lt::add_torrent_params &p = resumeData.ltAddTorrentParams;
p = lt::read_resume_data(root, ec);
p.save_path = Profile::instance()->fromPortablePath(Path(fromLTString(p.save_path)))
.toString().toStdString();
return resumeData;
} }
void BitTorrent::DBResumeDataStorage::store(const TorrentID &id, const LoadTorrentParams &resumeData) const void BitTorrent::DBResumeDataStorage::store(const TorrentID &id, const LoadTorrentParams &resumeData) const
@ -359,6 +367,49 @@ void BitTorrent::DBResumeDataStorage::storeQueue(const QVector<TorrentID> &queue
}); });
} }
void BitTorrent::DBResumeDataStorage::doLoadAll() const
{
const QString connectionName = u"ResumeDataStorageLoadAll"_qs;
{
auto db = QSqlDatabase::addDatabase(u"QSQLITE"_qs, connectionName);
db.setDatabaseName(path().data());
if (!db.open())
throw RuntimeError(db.lastError().text());
QSqlQuery query {db};
const auto selectTorrentIDStatement = u"SELECT %1 FROM %2 ORDER BY %3;"_qs
.arg(quoted(DB_COLUMN_TORRENT_ID.name), quoted(DB_TABLE_TORRENTS), quoted(DB_COLUMN_QUEUE_POSITION.name));
const QReadLocker locker {&m_dbLock};
if (!query.exec(selectTorrentIDStatement))
throw RuntimeError(query.lastError().text());
QVector<TorrentID> registeredTorrents;
registeredTorrents.reserve(query.size());
while (query.next())
registeredTorrents.append(TorrentID::fromString(query.value(0).toString()));
emit const_cast<DBResumeDataStorage *>(this)->loadStarted(registeredTorrents);
const auto selectStatement = u"SELECT * FROM %1 ORDER BY %2;"_qs.arg(quoted(DB_TABLE_TORRENTS), quoted(DB_COLUMN_QUEUE_POSITION.name));
if (!query.exec(selectStatement))
throw RuntimeError(query.lastError().text());
while (query.next())
{
const auto torrentID = TorrentID::fromString(query.value(DB_COLUMN_TORRENT_ID.name).toString());
onResumeDataLoaded(torrentID, parseQueryResultRow(query));
}
}
emit const_cast<DBResumeDataStorage *>(this)->loadFinished();
QSqlDatabase::removeDatabase(connectionName);
}
int BitTorrent::DBResumeDataStorage::currentDBVersion() const int BitTorrent::DBResumeDataStorage::currentDBVersion() const
{ {
const auto selectDBVersionStatement = u"SELECT %1 FROM %2 WHERE %3 = %4;"_qs const auto selectDBVersionStatement = u"SELECT %1 FROM %2 WHERE %3 = %4;"_qs
@ -372,6 +423,8 @@ int BitTorrent::DBResumeDataStorage::currentDBVersion() const
query.bindValue(DB_COLUMN_NAME.placeholder, META_VERSION); query.bindValue(DB_COLUMN_NAME.placeholder, META_VERSION);
const QReadLocker locker {&m_dbLock};
if (!query.exec()) if (!query.exec())
throw RuntimeError(query.lastError().text()); throw RuntimeError(query.lastError().text());
@ -390,6 +443,8 @@ void BitTorrent::DBResumeDataStorage::createDB() const
{ {
auto db = QSqlDatabase::database(DB_CONNECTION_NAME); auto db = QSqlDatabase::database(DB_CONNECTION_NAME);
const QWriteLocker locker {&m_dbLock};
if (!db.transaction()) if (!db.transaction())
throw RuntimeError(db.lastError().text()); throw RuntimeError(db.lastError().text());
@ -453,6 +508,8 @@ void BitTorrent::DBResumeDataStorage::updateDBFromVersion1() const
{ {
auto db = QSqlDatabase::database(DB_CONNECTION_NAME); auto db = QSqlDatabase::database(DB_CONNECTION_NAME);
const QWriteLocker locker {&m_dbLock};
if (!db.transaction()) if (!db.transaction())
throw RuntimeError(db.lastError().text()); throw RuntimeError(db.lastError().text());
@ -485,9 +542,10 @@ void BitTorrent::DBResumeDataStorage::updateDBFromVersion1() const
} }
} }
BitTorrent::DBResumeDataStorage::Worker::Worker(const Path &dbPath, const QString &dbConnectionName) BitTorrent::DBResumeDataStorage::Worker::Worker(const Path &dbPath, const QString &dbConnectionName, QReadWriteLock &dbLock)
: m_path {dbPath} : m_path {dbPath}
, m_connectionName {dbConnectionName} , m_connectionName {dbConnectionName}
, m_dbLock {dbLock}
{ {
} }
@ -612,6 +670,7 @@ void BitTorrent::DBResumeDataStorage::Worker::store(const TorrentID &id, const L
if (!bencodedMetadata.isEmpty()) if (!bencodedMetadata.isEmpty())
query.bindValue(DB_COLUMN_METADATA.placeholder, bencodedMetadata); query.bindValue(DB_COLUMN_METADATA.placeholder, bencodedMetadata);
const QWriteLocker locker {&m_dbLock};
if (!query.exec()) if (!query.exec())
throw RuntimeError(query.lastError().text()); throw RuntimeError(query.lastError().text());
} }
@ -636,6 +695,8 @@ void BitTorrent::DBResumeDataStorage::Worker::remove(const TorrentID &id) const
throw RuntimeError(query.lastError().text()); throw RuntimeError(query.lastError().text());
query.bindValue(DB_COLUMN_TORRENT_ID.placeholder, id.toString()); query.bindValue(DB_COLUMN_TORRENT_ID.placeholder, id.toString());
const QWriteLocker locker {&m_dbLock};
if (!query.exec()) if (!query.exec())
throw RuntimeError(query.lastError().text()); throw RuntimeError(query.lastError().text());
} }
@ -656,6 +717,8 @@ void BitTorrent::DBResumeDataStorage::Worker::storeQueue(const QVector<TorrentID
try try
{ {
const QWriteLocker locker {&m_dbLock};
if (!db.transaction()) if (!db.transaction())
throw RuntimeError(db.lastError().text()); throw RuntimeError(db.lastError().text());

8
src/base/bittorrent/dbresumedatastorage.h

@ -28,6 +28,8 @@
#pragma once #pragma once
#include <QReadWriteLock>
#include "base/pathfwd.h" #include "base/pathfwd.h"
#include "resumedatastorage.h" #include "resumedatastorage.h"
@ -45,12 +47,14 @@ namespace BitTorrent
~DBResumeDataStorage() override; ~DBResumeDataStorage() override;
QVector<TorrentID> registeredTorrents() const override; QVector<TorrentID> registeredTorrents() const override;
std::optional<LoadTorrentParams> load(const TorrentID &id) const override; LoadResumeDataResult load(const TorrentID &id) const override;
void store(const TorrentID &id, const LoadTorrentParams &resumeData) const override; void store(const TorrentID &id, const LoadTorrentParams &resumeData) const override;
void remove(const TorrentID &id) const override; void remove(const TorrentID &id) const override;
void storeQueue(const QVector<TorrentID> &queue) const override; void storeQueue(const QVector<TorrentID> &queue) const override;
private: private:
void doLoadAll() const override;
int currentDBVersion() const; int currentDBVersion() const;
void createDB() const; void createDB() const;
void updateDBFromVersion1() const; void updateDBFromVersion1() const;
@ -59,5 +63,7 @@ namespace BitTorrent
class Worker; class Worker;
Worker *m_asyncWorker = nullptr; Worker *m_asyncWorker = nullptr;
mutable QReadWriteLock m_dbLock;
}; };
} }

76
src/base/bittorrent/resumedatastorage.cpp

@ -0,0 +1,76 @@
/*
* Bittorrent Client using Qt and libtorrent.
* Copyright (C) 2015-2022 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
* 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 "resumedatastorage.h"
#include <utility>
#include <QMetaObject>
#include <QMutexLocker>
#include <QThread>
const int TORRENTIDLIST_TYPEID = qRegisterMetaType<QVector<BitTorrent::TorrentID>>();
BitTorrent::ResumeDataStorage::ResumeDataStorage(const Path &path, QObject *parent)
: QObject(parent)
, m_path {path}
{
}
Path BitTorrent::ResumeDataStorage::path() const
{
return m_path;
}
void BitTorrent::ResumeDataStorage::loadAll() const
{
m_loadedResumeData.reserve(1024);
auto *loadingThread = QThread::create([this]()
{
doLoadAll();
});
connect(loadingThread, &QThread::finished, loadingThread, &QObject::deleteLater);
loadingThread->start();
}
QVector<BitTorrent::LoadedResumeData> BitTorrent::ResumeDataStorage::fetchLoadedResumeData() const
{
const QMutexLocker locker {&m_loadedResumeDataMutex};
const QVector<BitTorrent::LoadedResumeData> loadedResumeData = m_loadedResumeData;
m_loadedResumeData.clear();
return loadedResumeData;
}
void BitTorrent::ResumeDataStorage::onResumeDataLoaded(const TorrentID &torrentID, const LoadResumeDataResult &loadResumeDataResult) const
{
const QMutexLocker locker {&m_loadedResumeDataMutex};
m_loadedResumeData.append({torrentID, loadResumeDataResult});
}

43
src/base/bittorrent/resumedatastorage.h

@ -1,6 +1,6 @@
/* /*
* Bittorrent Client using Qt and libtorrent. * Bittorrent Client using Qt and libtorrent.
* Copyright (C) 2015, 2018 Vladimir Golovnev <glassez@yandex.ru> * Copyright (C) 2015-2022 Vladimir Golovnev <glassez@yandex.ru>
* *
* This program is free software; you can redistribute it and/or * This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License * modify it under the terms of the GNU General Public License
@ -28,15 +28,25 @@
#pragma once #pragma once
#include <optional>
#include <QtContainerFwd> #include <QtContainerFwd>
#include <QMutex>
#include <QObject> #include <QObject>
#include <QVector>
#include "base/3rdparty/expected.hpp"
#include "base/path.h"
#include "infohash.h"
#include "loadtorrentparams.h"
namespace BitTorrent namespace BitTorrent
{ {
class TorrentID; using LoadResumeDataResult = nonstd::expected<LoadTorrentParams, QString>;
struct LoadTorrentParams;
struct LoadedResumeData
{
TorrentID torrentID;
LoadResumeDataResult result;
};
class ResumeDataStorage : public QObject class ResumeDataStorage : public QObject
{ {
@ -44,12 +54,31 @@ namespace BitTorrent
Q_DISABLE_COPY_MOVE(ResumeDataStorage) Q_DISABLE_COPY_MOVE(ResumeDataStorage)
public: public:
using QObject::QObject; explicit ResumeDataStorage(const Path &path, QObject *parent = nullptr);
Path path() const;
virtual QVector<TorrentID> registeredTorrents() const = 0; virtual QVector<TorrentID> registeredTorrents() const = 0;
virtual std::optional<LoadTorrentParams> load(const TorrentID &id) const = 0; virtual LoadResumeDataResult load(const TorrentID &id) const = 0;
virtual void store(const TorrentID &id, const LoadTorrentParams &resumeData) const = 0; virtual void store(const TorrentID &id, const LoadTorrentParams &resumeData) const = 0;
virtual void remove(const TorrentID &id) const = 0; virtual void remove(const TorrentID &id) const = 0;
virtual void storeQueue(const QVector<TorrentID> &queue) const = 0; virtual void storeQueue(const QVector<TorrentID> &queue) const = 0;
void loadAll() const;
QVector<LoadedResumeData> fetchLoadedResumeData() const;
signals:
void loadStarted(const QVector<BitTorrent::TorrentID> &torrents);
void loadFinished();
protected:
void onResumeDataLoaded(const TorrentID &torrentID, const LoadResumeDataResult &loadResumeDataResult) const;
private:
virtual void doLoadAll() const = 0;
const Path m_path;
mutable QVector<LoadedResumeData> m_loadedResumeData;
mutable QMutex m_loadedResumeDataMutex;
}; };
} }

658
src/base/bittorrent/session.cpp

@ -104,6 +104,7 @@
#include "magneturi.h" #include "magneturi.h"
#include "nativesessionextension.h" #include "nativesessionextension.h"
#include "portforwarderimpl.h" #include "portforwarderimpl.h"
#include "resumedatastorage.h"
#include "statistics.h" #include "statistics.h"
#include "torrentimpl.h" #include "torrentimpl.h"
#include "tracker.h" #include "tracker.h"
@ -112,6 +113,7 @@ using namespace std::chrono_literals;
using namespace BitTorrent; using namespace BitTorrent;
const Path CATEGORIES_FILE_NAME {u"categories.json"_qs}; const Path CATEGORIES_FILE_NAME {u"categories.json"_qs};
const int MAX_PROCESSING_RESUMEDATA_COUNT = 50;
namespace namespace
{ {
@ -297,6 +299,23 @@ namespace
#endif #endif
} }
struct BitTorrent::Session::ResumeSessionContext final : public QObject
{
using QObject::QObject;
ResumeDataStorage *startupStorage = nullptr;
ResumeDataStorageType currentStorageType = ResumeDataStorageType::Legacy;
QVector<LoadedResumeData> loadedResumeData;
int processingResumeDataCount = 0;
bool isLoadFinished = false;
bool isLoadedResumeDataHandlingEnqueued = false;
QSet<QString> recoveredCategories;
#ifdef QBT_USES_LIBTORRENT2
QSet<TorrentID> indexedTorrents;
QSet<TorrentID> skippedIDs;
#endif
};
const int addTorrentParamsId = qRegisterMetaType<AddTorrentParams>(); const int addTorrentParamsId = qRegisterMetaType<AddTorrentParams>();
// Session // Session
@ -467,7 +486,6 @@ Session::Session(QObject *parent)
const QStringList storedTags = m_storedTags.get(); const QStringList storedTags = m_storedTags.get();
m_tags = {storedTags.cbegin(), storedTags.cend()}; m_tags = {storedTags.cbegin(), storedTags.cend()};
enqueueRefresh();
updateSeedingLimitTimer(); updateSeedingLimitTimer();
populateAdditionalTrackers(); populateAdditionalTrackers();
if (isExcludedFileNamesEnabled()) if (isExcludedFileNamesEnabled())
@ -494,19 +512,12 @@ Session::Session(QObject *parent)
m_ioThread->start(); m_ioThread->start();
// Regular saving of fastresume data
connect(m_resumeDataTimer, &QTimer::timeout, this, [this]() { generateResumeData(); });
const int saveInterval = saveResumeDataInterval();
if (saveInterval > 0)
{
m_resumeDataTimer->setInterval(std::chrono::minutes(saveInterval));
m_resumeDataTimer->start();
}
// initialize PortForwarder instance // initialize PortForwarder instance
new PortForwarderImpl {m_nativeSession}; new PortForwarderImpl(m_nativeSession);
initMetrics(); initMetrics();
prepareStartup();
} }
bool Session::isDHTEnabled() const bool Session::isDHTEnabled() const
@ -1062,6 +1073,285 @@ void Session::configureComponents()
#endif #endif
} }
void Session::prepareStartup()
{
qDebug("Initializing torrents resume data storage...");
const Path dbPath = specialFolderLocation(SpecialFolder::Data) / Path(u"torrents.db"_qs);
const bool dbStorageExists = dbPath.exists();
auto *context = new ResumeSessionContext(this);
context->currentStorageType = resumeDataStorageType();
if (context->currentStorageType == ResumeDataStorageType::SQLite)
{
m_resumeDataStorage = new DBResumeDataStorage(dbPath, this);
if (!dbStorageExists)
{
const Path dataPath = specialFolderLocation(SpecialFolder::Data) / Path(u"BT_backup"_qs);
context->startupStorage = new BencodeResumeDataStorage(dataPath, this);
}
}
else
{
const Path dataPath = specialFolderLocation(SpecialFolder::Data) / Path(u"BT_backup"_qs);
m_resumeDataStorage = new BencodeResumeDataStorage(dataPath, this);
if (dbStorageExists)
context->startupStorage = new DBResumeDataStorage(dbPath, this);
}
if (!context->startupStorage)
context->startupStorage = m_resumeDataStorage;
connect(context->startupStorage, &ResumeDataStorage::loadStarted, context
, [this, context](const QVector<TorrentID> &torrents)
{
#ifdef QBT_USES_LIBTORRENT2
context->indexedTorrents = QSet<TorrentID>(torrents.cbegin(), torrents.cend());
#endif
handleLoadedResumeData(context);
});
connect(context->startupStorage, &ResumeDataStorage::loadFinished, context, [context]()
{
context->isLoadFinished = true;
});
connect(this, &Session::torrentsLoaded, context, [this, context](const QVector<Torrent *> &torrents)
{
context->processingResumeDataCount -= torrents.count();
if (!context->isLoadedResumeDataHandlingEnqueued)
{
QMetaObject::invokeMethod(this, [this, context]() { handleLoadedResumeData(context); }, Qt::QueuedConnection);
context->isLoadedResumeDataHandlingEnqueued = true;
}
m_nativeSession->post_torrent_updates();
m_refreshEnqueued = true;
});
context->startupStorage->loadAll();
}
void Session::handleLoadedResumeData(ResumeSessionContext *context)
{
context->isLoadedResumeDataHandlingEnqueued = false;
while (context->processingResumeDataCount < MAX_PROCESSING_RESUMEDATA_COUNT)
{
if (context->loadedResumeData.isEmpty())
context->loadedResumeData = context->startupStorage->fetchLoadedResumeData();
if (context->loadedResumeData.isEmpty())
{
if (context->processingResumeDataCount == 0)
{
if (context->isLoadFinished)
{
endStartup(context);
}
else if (!context->isLoadedResumeDataHandlingEnqueued)
{
QMetaObject::invokeMethod(this, [this, context]() { handleLoadedResumeData(context); }, Qt::QueuedConnection);
context->isLoadedResumeDataHandlingEnqueued = true;
}
}
break;
}
processNextResumeData(context);
}
}
void Session::processNextResumeData(ResumeSessionContext *context)
{
const LoadedResumeData loadedResumeDataItem = context->loadedResumeData.takeFirst();
TorrentID torrentID = loadedResumeDataItem.torrentID;
#ifdef QBT_USES_LIBTORRENT2
if (context->skippedIDs.contains(torrentID))
return;
#endif
const nonstd::expected<LoadTorrentParams, QString> &loadResumeDataResult = loadedResumeDataItem.result;
if (!loadResumeDataResult)
{
LogMsg(tr("Failed to resume torrent. Torrent: \"%1\". Reason: \"%2\"")
.arg(torrentID.toString(), loadResumeDataResult.error()), Log::CRITICAL);
return;
}
LoadTorrentParams resumeData = *loadResumeDataResult;
bool needStore = false;
#ifdef QBT_USES_LIBTORRENT2
const lt::info_hash_t infoHash = (resumeData.ltAddTorrentParams.ti
? resumeData.ltAddTorrentParams.ti->info_hashes()
: resumeData.ltAddTorrentParams.info_hashes);
const bool isHybrid = infoHash.has_v1() && infoHash.has_v2();
const auto torrentIDv2 = TorrentID::fromInfoHash(infoHash);
const auto torrentIDv1 = TorrentID::fromInfoHash(lt::info_hash_t(infoHash.v1));
if (torrentID == torrentIDv2)
{
if (isHybrid && context->indexedTorrents.contains(torrentIDv1))
{
// if we don't have metadata, try to find it in alternative "resume data"
if (!resumeData.ltAddTorrentParams.ti)
{
const nonstd::expected<LoadTorrentParams, QString> loadAltResumeDataResult = context->startupStorage->load(torrentIDv1);
if (loadAltResumeDataResult)
resumeData.ltAddTorrentParams.ti = loadAltResumeDataResult->ltAddTorrentParams.ti;
}
// remove alternative "resume data" and skip the attempt to load it
m_resumeDataStorage->remove(torrentIDv1);
context->skippedIDs.insert(torrentIDv1);
}
}
else if (torrentID == torrentIDv1)
{
torrentID = torrentIDv2;
needStore = true;
m_resumeDataStorage->remove(torrentIDv1);
if (context->indexedTorrents.contains(torrentID))
{
context->skippedIDs.insert(torrentID);
const nonstd::expected<LoadTorrentParams, QString> loadPreferredResumeDataResult = context->startupStorage->load(torrentID);
if (loadPreferredResumeDataResult)
{
std::shared_ptr<lt::torrent_info> ti = resumeData.ltAddTorrentParams.ti;
resumeData = *loadPreferredResumeDataResult;
if (!resumeData.ltAddTorrentParams.ti)
resumeData.ltAddTorrentParams.ti = ti;
}
}
}
else
{
LogMsg(tr("Failed to resume torrent: inconsistent torrent ID is detected. Torrent: \"%1\"")
.arg(torrentID.toString()), Log::WARNING);
return;
}
#else
const lt::sha1_hash infoHash = (resumeData.ltAddTorrentParams.ti
? resumeData.ltAddTorrentParams.ti->info_hash()
: resumeData.ltAddTorrentParams.info_hash);
if (torrentID != TorrentID::fromInfoHash(infoHash))
{
LogMsg(tr("Failed to resume torrent: inconsistent torrent ID is detected. Torrent: \"%1\"")
.arg(torrentID.toString()), Log::WARNING);
return;
}
#endif
if (m_resumeDataStorage != context->startupStorage)
needStore = true;
// TODO: Remove the following upgrade code in v4.6
// == BEGIN UPGRADE CODE ==
if (!needStore)
{
if (m_needUpgradeDownloadPath && isDownloadPathEnabled() && !resumeData.useAutoTMM)
{
resumeData.downloadPath = downloadPath();
needStore = true;
}
}
// == END UPGRADE CODE ==
if (needStore)
m_resumeDataStorage->store(torrentID, resumeData);
const QString category = resumeData.category;
bool isCategoryRecovered = context->recoveredCategories.contains(category);
if (!category.isEmpty() && (isCategoryRecovered || !m_categories.contains(category)))
{
if (!isCategoryRecovered)
{
if (addCategory(category))
{
context->recoveredCategories.insert(category);
isCategoryRecovered = true;
LogMsg(tr("Detected inconsistent data: category is missing from the configuration file."
" Category will be recovered but its settings will be reset to default."
" Torrent: \"%1\". Category: \"%2\"").arg(torrentID.toString(), category), Log::WARNING);
}
else
{
resumeData.category.clear();
LogMsg(tr("Detected inconsistent data: invalid category. Torrent: \"%1\". Category: \"%2\"")
.arg(torrentID.toString(), category), Log::WARNING);
}
}
// We should check isCategoryRecovered again since the category
// can be just recovered by the code above
if (isCategoryRecovered && resumeData.useAutoTMM)
{
const Path storageLocation {resumeData.ltAddTorrentParams.save_path};
if ((storageLocation != categorySavePath(resumeData.category)) && (storageLocation != categoryDownloadPath(resumeData.category)))
{
resumeData.useAutoTMM = false;
resumeData.savePath = storageLocation;
resumeData.downloadPath = {};
LogMsg(tr("Detected mismatch between the save paths of the recovered category and the current save path of the torrent."
" Torrent is now switched to Manual mode."
" Torrent: \"%1\". Category: \"%2\"").arg(torrentID.toString(), category), Log::WARNING);
}
}
}
resumeData.ltAddTorrentParams.userdata = LTClientData(new ExtensionData);
#ifndef QBT_USES_LIBTORRENT2
resumeData.ltAddTorrentParams.storage = customStorageConstructor;
#endif
qDebug() << "Starting up torrent" << torrentID.toString() << "...";
m_loadingTorrents.insert(torrentID, resumeData);
m_nativeSession->async_add_torrent(resumeData.ltAddTorrentParams);
++context->processingResumeDataCount;
}
void Session::endStartup(ResumeSessionContext *context)
{
if (m_resumeDataStorage != context->startupStorage)
{
if (isQueueingSystemEnabled())
saveTorrentsQueue();
const Path dbPath = context->startupStorage->path();
delete context->startupStorage;
if (context->currentStorageType == ResumeDataStorageType::Legacy)
Utils::Fs::removeFile(dbPath);
}
context->deleteLater();
m_nativeSession->resume();
if (m_refreshEnqueued)
m_refreshEnqueued = false;
else
enqueueRefresh();
// Regular saving of fastresume data
connect(m_resumeDataTimer, &QTimer::timeout, this, &Session::generateResumeData);
const int saveInterval = saveResumeDataInterval();
if (saveInterval > 0)
{
m_resumeDataTimer->setInterval(std::chrono::minutes(saveInterval));
m_resumeDataTimer->start();
}
m_isRestored = true;
emit restored();
}
void Session::initializeNativeSession() void Session::initializeNativeSession()
{ {
const std::string peerId = lt::generate_fingerprint(PEER_ID, QBT_VERSION_MAJOR, QBT_VERSION_MINOR, QBT_VERSION_BUGFIX, QBT_VERSION_BUILD); const std::string peerId = lt::generate_fingerprint(PEER_ID, QBT_VERSION_MAJOR, QBT_VERSION_MINOR, QBT_VERSION_BUGFIX, QBT_VERSION_BUILD);
@ -1100,7 +1390,7 @@ void Session::initializeNativeSession()
break; break;
} }
#endif #endif
m_nativeSession = new lt::session {sessionParams}; m_nativeSession = new lt::session(sessionParams, lt::session::paused);
LogMsg(tr("Peer ID: \"%1\"").arg(QString::fromStdString(peerId)), Log::INFO); LogMsg(tr("Peer ID: \"%1\"").arg(QString::fromStdString(peerId)), Log::INFO);
LogMsg(tr("HTTP User-Agent: \"%1\"").arg(USER_AGENT), Log::INFO); LogMsg(tr("HTTP User-Agent: \"%1\"").arg(USER_AGENT), Log::INFO);
@ -1764,9 +2054,7 @@ void Session::fileSearchFinished(const TorrentID &id, const Path &savePath, cons
const auto loadingTorrentsIter = m_loadingTorrents.find(id); const auto loadingTorrentsIter = m_loadingTorrents.find(id);
if (loadingTorrentsIter != m_loadingTorrents.end()) if (loadingTorrentsIter != m_loadingTorrents.end())
{ {
LoadTorrentParams params = loadingTorrentsIter.value(); LoadTorrentParams &params = loadingTorrentsIter.value();
m_loadingTorrents.erase(loadingTorrentsIter);
lt::add_torrent_params &p = params.ltAddTorrentParams; lt::add_torrent_params &p = params.ltAddTorrentParams;
p.save_path = savePath.toString().toStdString(); p.save_path = savePath.toString().toStdString();
@ -1775,7 +2063,7 @@ void Session::fileSearchFinished(const TorrentID &id, const Path &savePath, cons
for (int i = 0; i < fileNames.size(); ++i) for (int i = 0; i < fileNames.size(); ++i)
p.renamed_files[nativeIndexes[i]] = fileNames[i].toString().toStdString(); p.renamed_files[nativeIndexes[i]] = fileNames[i].toString().toStdString();
loadTorrent(params); m_nativeSession->async_add_torrent(p);
} }
} }
@ -2054,6 +2342,9 @@ bool Session::addTorrent(const QString &source, const AddTorrentParams &params)
{ {
// `source`: .torrent file path/url or magnet uri // `source`: .torrent file path/url or magnet uri
if (!isRestored())
return false;
if (Net::DownloadManager::hasSupportedScheme(source)) if (Net::DownloadManager::hasSupportedScheme(source))
{ {
LogMsg(tr("Downloading torrent, please wait... Source: \"%1\"").arg(source)); LogMsg(tr("Downloading torrent, please wait... Source: \"%1\"").arg(source));
@ -2083,13 +2374,20 @@ bool Session::addTorrent(const QString &source, const AddTorrentParams &params)
bool Session::addTorrent(const MagnetUri &magnetUri, const AddTorrentParams &params) bool Session::addTorrent(const MagnetUri &magnetUri, const AddTorrentParams &params)
{ {
if (!magnetUri.isValid()) return false; if (!isRestored())
return false;
if (!magnetUri.isValid())
return false;
return addTorrent_impl(magnetUri, params); return addTorrent_impl(magnetUri, params);
} }
bool Session::addTorrent(const TorrentInfo &torrentInfo, const AddTorrentParams &params) bool Session::addTorrent(const TorrentInfo &torrentInfo, const AddTorrentParams &params)
{ {
if (!isRestored())
return false;
return addTorrent_impl(torrentInfo, params); return addTorrent_impl(torrentInfo, params);
} }
@ -2152,6 +2450,8 @@ LoadTorrentParams Session::initLoadTorrentParams(const AddTorrentParams &addTorr
// Add a torrent to the BitTorrent session // Add a torrent to the BitTorrent session
bool Session::addTorrent_impl(const std::variant<MagnetUri, TorrentInfo> &source, const AddTorrentParams &addTorrentParams) bool Session::addTorrent_impl(const std::variant<MagnetUri, TorrentInfo> &source, const AddTorrentParams &addTorrentParams)
{ {
Q_ASSERT(isRestored());
const bool hasMetadata = std::holds_alternative<TorrentInfo>(source); const bool hasMetadata = std::holds_alternative<TorrentInfo>(source);
const auto id = TorrentID::fromInfoHash(hasMetadata ? std::get<TorrentInfo>(source).infoHash() : std::get<MagnetUri>(source).infoHash()); const auto id = TorrentID::fromInfoHash(hasMetadata ? std::get<TorrentInfo>(source).infoHash() : std::get<MagnetUri>(source).infoHash());
@ -2332,36 +2632,18 @@ bool Session::addTorrent_impl(const std::variant<MagnetUri, TorrentInfo> &source
p.added_time = std::time(nullptr); p.added_time = std::time(nullptr);
if (!isFindingIncompleteFiles)
return loadTorrent(loadTorrentParams);
m_loadingTorrents.insert(id, loadTorrentParams);
return true;
}
// Add a torrent to the BitTorrent session
bool Session::loadTorrent(LoadTorrentParams params)
{
lt::add_torrent_params &p = params.ltAddTorrentParams;
p.userdata = LTClientData(new ExtensionData);
#ifndef QBT_USES_LIBTORRENT2
p.storage = customStorageConstructor;
#endif
// Limits // Limits
p.max_connections = maxConnectionsPerTorrent(); p.max_connections = maxConnectionsPerTorrent();
p.max_uploads = maxUploadsPerTorrent(); p.max_uploads = maxUploadsPerTorrent();
const bool hasMetadata = (p.ti && p.ti->is_valid()); p.userdata = LTClientData(new ExtensionData);
#ifdef QBT_USES_LIBTORRENT2 #ifndef QBT_USES_LIBTORRENT2
const auto id = TorrentID::fromInfoHash(hasMetadata ? p.ti->info_hashes() : p.info_hashes); p.storage = customStorageConstructor;
#else
const auto id = TorrentID::fromInfoHash(hasMetadata ? p.ti->info_hash() : p.info_hash);
#endif #endif
m_loadingTorrents.insert(id, params);
// Adding torrent to BitTorrent session m_loadingTorrents.insert(id, loadTorrentParams);
m_nativeSession->async_add_torrent(p); if (!isFindingIncompleteFiles)
m_nativeSession->async_add_torrent(p);
return true; return true;
} }
@ -3207,6 +3489,11 @@ QStringList Session::bannedIPs() const
return m_bannedIPs; return m_bannedIPs;
} }
bool Session::isRestored() const
{
return m_isRestored;
}
#if defined(Q_OS_WIN) #if defined(Q_OS_WIN)
OSMemoryPriority Session::getOSMemoryPriority() const OSMemoryPriority Session::getOSMemoryPriority() const
{ {
@ -4537,206 +4824,6 @@ const CacheStatus &Session::cacheStatus() const
return m_cacheStatus; return m_cacheStatus;
} }
void Session::startUpTorrents()
{
qDebug("Initializing torrents resume data storage...");
const Path dbPath = specialFolderLocation(SpecialFolder::Data) / Path(u"torrents.db"_qs);
const bool dbStorageExists = dbPath.exists();
ResumeDataStorage *startupStorage = nullptr;
if (resumeDataStorageType() == ResumeDataStorageType::SQLite)
{
m_resumeDataStorage = new DBResumeDataStorage(dbPath, this);
if (!dbStorageExists)
{
const Path dataPath = specialFolderLocation(SpecialFolder::Data) / Path(u"BT_backup"_qs);
startupStorage = new BencodeResumeDataStorage(dataPath, this);
}
}
else
{
const Path dataPath = specialFolderLocation(SpecialFolder::Data) / Path(u"BT_backup"_qs);
m_resumeDataStorage = new BencodeResumeDataStorage(dataPath, this);
if (dbStorageExists)
startupStorage = new DBResumeDataStorage(dbPath, this);
}
if (!startupStorage)
startupStorage = m_resumeDataStorage;
qDebug("Starting up torrents...");
const QVector<TorrentID> torrents = startupStorage->registeredTorrents();
int resumedTorrentsCount = 0;
QVector<TorrentID> queue;
QSet<QString> recoveredCategories;
#ifdef QBT_USES_LIBTORRENT2
const QSet<TorrentID> indexedTorrents {torrents.cbegin(), torrents.cend()};
QSet<TorrentID> skippedIDs;
#endif
for (TorrentID torrentID : torrents)
{
#ifdef QBT_USES_LIBTORRENT2
if (skippedIDs.contains(torrentID))
continue;
#endif
const std::optional<LoadTorrentParams> loadResumeDataResult = startupStorage->load(torrentID);
if (!loadResumeDataResult)
{
LogMsg(tr("Failed to resume torrent. Torrent: \"%1\"").arg(torrentID.toString()), Log::CRITICAL);
continue;
}
LoadTorrentParams resumeData = *loadResumeDataResult;
bool needStore = false;
#ifdef QBT_USES_LIBTORRENT2
const lt::info_hash_t infoHash = (resumeData.ltAddTorrentParams.ti
? resumeData.ltAddTorrentParams.ti->info_hashes()
: resumeData.ltAddTorrentParams.info_hashes);
const bool isHybrid = infoHash.has_v1() && infoHash.has_v2();
const auto torrentIDv2 = TorrentID::fromInfoHash(infoHash);
const auto torrentIDv1 = TorrentID::fromInfoHash(lt::info_hash_t(infoHash.v1));
if (torrentID == torrentIDv2)
{
if (isHybrid && indexedTorrents.contains(torrentIDv1))
{
// if we don't have metadata, try to find it in alternative "resume data"
if (!resumeData.ltAddTorrentParams.ti)
{
const std::optional<LoadTorrentParams> loadAltResumeDataResult = startupStorage->load(torrentIDv1);
if (loadAltResumeDataResult)
resumeData.ltAddTorrentParams.ti = loadAltResumeDataResult->ltAddTorrentParams.ti;
}
// remove alternative "resume data" and skip the attempt to load it
m_resumeDataStorage->remove(torrentIDv1);
skippedIDs.insert(torrentIDv1);
}
}
else if (torrentID == torrentIDv1)
{
torrentID = torrentIDv2;
needStore = true;
m_resumeDataStorage->remove(torrentIDv1);
if (indexedTorrents.contains(torrentID))
{
skippedIDs.insert(torrentID);
const std::optional<LoadTorrentParams> loadPreferredResumeDataResult = startupStorage->load(torrentID);
if (loadPreferredResumeDataResult)
{
std::shared_ptr<lt::torrent_info> ti = resumeData.ltAddTorrentParams.ti;
resumeData = *loadPreferredResumeDataResult;
if (!resumeData.ltAddTorrentParams.ti)
resumeData.ltAddTorrentParams.ti = ti;
}
}
}
else
{
LogMsg(tr("Failed to resume torrent: inconsistent torrent ID is detected. Torrent: \"%1\"")
.arg(torrentID.toString()), Log::WARNING);
continue;
}
#else
const lt::sha1_hash infoHash = (resumeData.ltAddTorrentParams.ti
? resumeData.ltAddTorrentParams.ti->info_hash()
: resumeData.ltAddTorrentParams.info_hash);
if (torrentID != TorrentID::fromInfoHash(infoHash))
{
LogMsg(tr("Failed to resume torrent: inconsistent torrent ID is detected. Torrent: \"%1\"")
.arg(torrentID.toString()), Log::WARNING);
continue;
}
#endif
if (m_resumeDataStorage != startupStorage)
{
needStore = true;
if (isQueueingSystemEnabled() && !resumeData.hasSeedStatus)
queue.append(torrentID);
}
// TODO: Remove the following upgrade code in v4.6
// == BEGIN UPGRADE CODE ==
if (m_needUpgradeDownloadPath && isDownloadPathEnabled())
{
if (!resumeData.useAutoTMM)
{
resumeData.downloadPath = downloadPath();
needStore = true;
}
}
// == END UPGRADE CODE ==
if (needStore)
m_resumeDataStorage->store(torrentID, resumeData);
const QString category = resumeData.category;
bool isCategoryRecovered = recoveredCategories.contains(category);
if (!category.isEmpty() && (isCategoryRecovered || !m_categories.contains(category)))
{
if (!isCategoryRecovered)
{
if (addCategory(category))
{
recoveredCategories.insert(category);
isCategoryRecovered = true;
LogMsg(tr("Detected inconsistent data: category is missing from the configuration file."
" Category will be recovered but its settings will be reset to default."
" Torrent: \"%1\". Category: \"%2\"").arg(torrentID.toString(), category), Log::WARNING);
}
else
{
resumeData.category.clear();
LogMsg(tr("Detected inconsistent data: invalid category. Torrent: \"%1\". Category: \"%2\"")
.arg(torrentID.toString(), category), Log::WARNING);
}
}
// We should check isCategoryRecovered again since the category
// can be just recovered by the code above
if (isCategoryRecovered && resumeData.useAutoTMM)
{
const Path storageLocation {resumeData.ltAddTorrentParams.save_path};
if ((storageLocation != categorySavePath(resumeData.category)) && (storageLocation != categoryDownloadPath(resumeData.category)))
{
resumeData.useAutoTMM = false;
resumeData.savePath = storageLocation;
resumeData.downloadPath = {};
LogMsg(tr("Detected mismatch between the save paths of the recovered category and the current save path of the torrent."
" Torrent is now switched to Manual mode."
" Torrent: \"%1\". Category: \"%2\"").arg(torrentID.toString(), category), Log::WARNING);
}
}
}
qDebug() << "Starting up torrent" << torrentID.toString() << "...";
if (!loadTorrent(resumeData))
LogMsg(tr("Failed to resume torrent. Torrent: \"%1\"").arg(torrentID.toString()), Log::CRITICAL);
// process add torrent messages before message queue overflow
if ((resumedTorrentsCount % 100) == 0) readAlerts();
++resumedTorrentsCount;
}
if (m_resumeDataStorage != startupStorage)
{
delete startupStorage;
if (resumeDataStorageType() == ResumeDataStorageType::Legacy)
Utils::Fs::removeFile(dbPath);
if (isQueueingSystemEnabled())
m_resumeDataStorage->storeQueue(queue);
}
}
qint64 Session::getAlltimeDL() const qint64 Session::getAlltimeDL() const
{ {
@ -4807,12 +4894,63 @@ void Session::setTorrentContentLayout(const TorrentContentLayout value)
void Session::readAlerts() void Session::readAlerts()
{ {
const std::vector<lt::alert *> alerts = getPendingAlerts(); const std::vector<lt::alert *> alerts = getPendingAlerts();
handleAddTorrentAlerts(alerts);
for (const lt::alert *a : alerts) for (const lt::alert *a : alerts)
handleAlert(a); handleAlert(a);
processTrackerStatuses(); processTrackerStatuses();
} }
void Session::handleAddTorrentAlerts(const std::vector<lt::alert *> &alerts)
{
QVector<Torrent *> loadedTorrents;
if (!isRestored())
loadedTorrents.reserve(MAX_PROCESSING_RESUMEDATA_COUNT);
for (const lt::alert *a : alerts)
{
if (a->type() != lt::add_torrent_alert::alert_type)
continue;
auto alert = static_cast<const lt::add_torrent_alert *>(a);
if (alert->error)
{
const QString msg = QString::fromStdString(alert->message());
LogMsg(tr("Failed to load torrent. Reason: \"%1\"").arg(msg), Log::WARNING);
emit loadTorrentFailed(msg);
const lt::add_torrent_params &params = alert->params;
const bool hasMetadata = (params.ti && params.ti->is_valid());
#ifdef QBT_USES_LIBTORRENT2
const auto torrentID = TorrentID::fromInfoHash(hasMetadata ? params.ti->info_hashes() : params.info_hashes);
#else
const auto torrentID = TorrentID::fromInfoHash(hasMetadata ? params.ti->info_hash() : params.info_hash);
#endif
m_loadingTorrents.remove(torrentID);
return;
}
#ifdef QBT_USES_LIBTORRENT2
const auto torrentID = TorrentID::fromInfoHash(alert->handle.info_hashes());
#else
const auto torrentID = TorrentID::fromInfoHash(alert->handle.info_hash());
#endif
const auto loadingTorrentsIter = m_loadingTorrents.find(torrentID);
if (loadingTorrentsIter != m_loadingTorrents.end())
{
LoadTorrentParams params = loadingTorrentsIter.value();
m_loadingTorrents.erase(loadingTorrentsIter);
Torrent *torrent = createTorrent(alert->handle, params);
loadedTorrents.append(torrent);
}
}
if (!loadedTorrents.isEmpty())
emit torrentsLoaded(loadedTorrents);
}
void Session::handleAlert(const lt::alert *a) void Session::handleAlert(const lt::alert *a)
{ {
try try
@ -4852,7 +4990,7 @@ void Session::handleAlert(const lt::alert *a)
handleFileErrorAlert(static_cast<const lt::file_error_alert*>(a)); handleFileErrorAlert(static_cast<const lt::file_error_alert*>(a));
break; break;
case lt::add_torrent_alert::alert_type: case lt::add_torrent_alert::alert_type:
handleAddTorrentAlert(static_cast<const lt::add_torrent_alert*>(a)); // handled separately
break; break;
case lt::torrent_removed_alert::alert_type: case lt::torrent_removed_alert::alert_type:
handleTorrentRemovedAlert(static_cast<const lt::torrent_removed_alert*>(a)); handleTorrentRemovedAlert(static_cast<const lt::torrent_removed_alert*>(a));
@ -4924,7 +5062,7 @@ void Session::dispatchTorrentAlert(const lt::alert *a)
} }
} }
void Session::createTorrent(const lt::torrent_handle &nativeHandle) TorrentImpl *Session::createTorrent(const lt::torrent_handle &nativeHandle, const LoadTorrentParams &params)
{ {
#ifdef QBT_USES_LIBTORRENT2 #ifdef QBT_USES_LIBTORRENT2
const auto torrentID = TorrentID::fromInfoHash(nativeHandle.info_hashes()); const auto torrentID = TorrentID::fromInfoHash(nativeHandle.info_hashes());
@ -4932,21 +5070,15 @@ void Session::createTorrent(const lt::torrent_handle &nativeHandle)
const auto torrentID = TorrentID::fromInfoHash(nativeHandle.info_hash()); const auto torrentID = TorrentID::fromInfoHash(nativeHandle.info_hash());
#endif #endif
Q_ASSERT(m_loadingTorrents.contains(torrentID));
const LoadTorrentParams params = m_loadingTorrents.take(torrentID);
auto *const torrent = new TorrentImpl(this, m_nativeSession, nativeHandle, params); auto *const torrent = new TorrentImpl(this, m_nativeSession, nativeHandle, params);
m_torrents.insert(torrent->id(), torrent); m_torrents.insert(torrent->id(), torrent);
const bool hasMetadata = torrent->hasMetadata();
if (!params.restored) if (!params.restored)
{ {
m_resumeDataStorage->store(torrent->id(), params); m_resumeDataStorage->store(torrent->id(), params);
// The following is useless for newly added magnet // The following is useless for newly added magnet
if (hasMetadata) if (torrent->hasMetadata())
{ {
if (!torrentExportDirectory().isEmpty()) if (!torrentExportDirectory().isEmpty())
exportTorrentFile(torrent, torrentExportDirectory()); exportTorrentFile(torrent, torrentExportDirectory());
@ -4959,9 +5091,6 @@ void Session::createTorrent(const lt::torrent_handle &nativeHandle)
m_seedingLimitTimer->start(); m_seedingLimitTimer->start();
} }
// Send torrent addition signal
emit torrentLoaded(torrent);
// Send new torrent signal
if (params.restored) if (params.restored)
{ {
LogMsg(tr("Restored torrent. Torrent: \"%1\"").arg(torrent->name())); LogMsg(tr("Restored torrent. Torrent: \"%1\"").arg(torrent->name()));
@ -4975,29 +5104,8 @@ void Session::createTorrent(const lt::torrent_handle &nativeHandle)
// Torrent could have error just after adding to libtorrent // Torrent could have error just after adding to libtorrent
if (torrent->hasError()) if (torrent->hasError())
LogMsg(tr("Torrent errored. Torrent: \"%1\". Error: \"%2\"").arg(torrent->name(), torrent->error()), Log::WARNING); LogMsg(tr("Torrent errored. Torrent: \"%1\". Error: \"%2\"").arg(torrent->name(), torrent->error()), Log::WARNING);
}
void Session::handleAddTorrentAlert(const lt::add_torrent_alert *p) return torrent;
{
if (p->error)
{
const QString msg = QString::fromStdString(p->message());
LogMsg(tr("Failed to load torrent. Reason: \"%1\"").arg(msg), Log::WARNING);
emit loadTorrentFailed(msg);
const lt::add_torrent_params &params = p->params;
const bool hasMetadata = (params.ti && params.ti->is_valid());
#ifdef QBT_USES_LIBTORRENT2
const auto id = TorrentID::fromInfoHash(hasMetadata ? params.ti->info_hashes() : params.info_hashes);
#else
const auto id = TorrentID::fromInfoHash(hasMetadata ? params.ti->info_hash() : params.info_hash);
#endif
m_loadingTorrents.remove(id);
}
else if (m_loadingTorrents.contains(p->handle.info_hash()))
{
createTorrent(p->handle);
}
} }
void Session::handleTorrentRemovedAlert(const lt::torrent_removed_alert *p) void Session::handleTorrentRemovedAlert(const lt::torrent_removed_alert *p)

19
src/base/bittorrent/session.h

@ -469,7 +469,8 @@ namespace BitTorrent
void setOSMemoryPriority(OSMemoryPriority priority); void setOSMemoryPriority(OSMemoryPriority priority);
#endif #endif
void startUpTorrents(); bool isRestored() const;
Torrent *findTorrent(const TorrentID &id) const; Torrent *findTorrent(const TorrentID &id) const;
QVector<Torrent *> torrents() const; QVector<Torrent *> torrents() const;
qsizetype torrentsCount() const; qsizetype torrentsCount() const;
@ -539,6 +540,7 @@ namespace BitTorrent
void loadTorrentFailed(const QString &error); void loadTorrentFailed(const QString &error);
void metadataDownloaded(const TorrentInfo &info); void metadataDownloaded(const TorrentInfo &info);
void recursiveTorrentDownloadPossible(Torrent *torrent); void recursiveTorrentDownloadPossible(Torrent *torrent);
void restored();
void speedLimitModeChanged(bool alternative); void speedLimitModeChanged(bool alternative);
void statsUpdated(); void statsUpdated();
void subcategoriesSupportChanged(); void subcategoriesSupportChanged();
@ -549,12 +551,12 @@ namespace BitTorrent
void torrentCategoryChanged(Torrent *torrent, const QString &oldCategory); void torrentCategoryChanged(Torrent *torrent, const QString &oldCategory);
void torrentFinished(Torrent *torrent); void torrentFinished(Torrent *torrent);
void torrentFinishedChecking(Torrent *torrent); void torrentFinishedChecking(Torrent *torrent);
void torrentLoaded(Torrent *torrent);
void torrentMetadataReceived(Torrent *torrent); void torrentMetadataReceived(Torrent *torrent);
void torrentPaused(Torrent *torrent); void torrentPaused(Torrent *torrent);
void torrentResumed(Torrent *torrent); void torrentResumed(Torrent *torrent);
void torrentSavePathChanged(Torrent *torrent); void torrentSavePathChanged(Torrent *torrent);
void torrentSavingModeChanged(Torrent *torrent); void torrentSavingModeChanged(Torrent *torrent);
void torrentsLoaded(const QVector<Torrent *> &torrents);
void torrentsUpdated(const QVector<Torrent *> &torrents); void torrentsUpdated(const QVector<Torrent *> &torrents);
void torrentTagAdded(Torrent *torrent, const QString &tag); void torrentTagAdded(Torrent *torrent, const QString &tag);
void torrentTagRemoved(Torrent *torrent, const QString &tag); void torrentTagRemoved(Torrent *torrent, const QString &tag);
@ -585,6 +587,8 @@ namespace BitTorrent
#endif #endif
private: private:
struct ResumeSessionContext;
struct MoveStorageJob struct MoveStorageJob
{ {
lt::torrent_handle torrentHandle; lt::torrent_handle torrentHandle;
@ -630,8 +634,11 @@ namespace BitTorrent
#endif #endif
void processTrackerStatuses(); void processTrackerStatuses();
void populateExcludedFileNamesRegExpList(); void populateExcludedFileNamesRegExpList();
void prepareStartup();
void handleLoadedResumeData(ResumeSessionContext *context);
void processNextResumeData(ResumeSessionContext *context);
void endStartup(ResumeSessionContext *context);
bool loadTorrent(LoadTorrentParams params);
LoadTorrentParams initLoadTorrentParams(const AddTorrentParams &addTorrentParams); LoadTorrentParams initLoadTorrentParams(const AddTorrentParams &addTorrentParams);
bool addTorrent_impl(const std::variant<MagnetUri, TorrentInfo> &source, const AddTorrentParams &addTorrentParams); bool addTorrent_impl(const std::variant<MagnetUri, TorrentInfo> &source, const AddTorrentParams &addTorrentParams);
@ -639,8 +646,8 @@ namespace BitTorrent
void exportTorrentFile(const Torrent *torrent, const Path &folderPath); void exportTorrentFile(const Torrent *torrent, const Path &folderPath);
void handleAlert(const lt::alert *a); void handleAlert(const lt::alert *a);
void handleAddTorrentAlerts(const std::vector<lt::alert *> &alerts);
void dispatchTorrentAlert(const lt::alert *a); void dispatchTorrentAlert(const lt::alert *a);
void handleAddTorrentAlert(const lt::add_torrent_alert *p);
void handleStateUpdateAlert(const lt::state_update_alert *p); void handleStateUpdateAlert(const lt::state_update_alert *p);
void handleMetadataReceivedAlert(const lt::metadata_received_alert *p); void handleMetadataReceivedAlert(const lt::metadata_received_alert *p);
void handleFileErrorAlert(const lt::file_error_alert *p); void handleFileErrorAlert(const lt::file_error_alert *p);
@ -662,7 +669,7 @@ namespace BitTorrent
void handleSocks5Alert(const lt::socks5_alert *p) const; void handleSocks5Alert(const lt::socks5_alert *p) const;
void handleTrackerAlert(const lt::tracker_alert *a); void handleTrackerAlert(const lt::tracker_alert *a);
void createTorrent(const lt::torrent_handle &nativeHandle); TorrentImpl *createTorrent(const lt::torrent_handle &nativeHandle, const LoadTorrentParams &params);
void saveResumeData(); void saveResumeData();
void saveTorrentsQueue() const; void saveTorrentsQueue() const;
@ -792,6 +799,8 @@ namespace BitTorrent
CachedSettingValue<OSMemoryPriority> m_OSMemoryPriority; CachedSettingValue<OSMemoryPriority> m_OSMemoryPriority;
#endif #endif
bool m_isRestored = false;
// Order is important. This needs to be declared after its CachedSettingsValue // Order is important. This needs to be declared after its CachedSettingsValue
// counterpart, because it uses it for initialization in the constructor // counterpart, because it uses it for initialization in the constructor
// initialization list. // initialization list.

19
src/base/rss/rss_autodownloader.cpp

@ -138,8 +138,20 @@ AutoDownloader::AutoDownloader()
m_processingTimer->setSingleShot(true); m_processingTimer->setSingleShot(true);
connect(m_processingTimer, &QTimer::timeout, this, &AutoDownloader::process); connect(m_processingTimer, &QTimer::timeout, this, &AutoDownloader::process);
if (isProcessingEnabled()) const auto *btSession = BitTorrent::Session::instance();
startProcessing(); if (btSession->isRestored())
{
if (isProcessingEnabled())
startProcessing();
}
else
{
connect(btSession, &BitTorrent::Session::restored, this, [this]()
{
if (isProcessingEnabled())
startProcessing();
});
}
} }
AutoDownloader::~AutoDownloader() AutoDownloader::~AutoDownloader()
@ -506,7 +518,8 @@ void AutoDownloader::setProcessingEnabled(const bool enabled)
m_storeProcessingEnabled = enabled; m_storeProcessingEnabled = enabled;
if (enabled) if (enabled)
{ {
startProcessing(); if (BitTorrent::Session::instance()->isRestored())
startProcessing();
} }
else else
{ {

50
src/base/torrentfileswatcher.cpp

@ -255,13 +255,12 @@ TorrentFilesWatcher *TorrentFilesWatcher::instance()
TorrentFilesWatcher::TorrentFilesWatcher(QObject *parent) TorrentFilesWatcher::TorrentFilesWatcher(QObject *parent)
: QObject {parent} : QObject {parent}
, m_ioThread {new QThread(this)} , m_ioThread {new QThread(this)}
, m_asyncWorker {new TorrentFilesWatcher::Worker}
{ {
connect(m_asyncWorker, &TorrentFilesWatcher::Worker::magnetFound, this, &TorrentFilesWatcher::onMagnetFound); const auto *btSession = BitTorrent::Session::instance();
connect(m_asyncWorker, &TorrentFilesWatcher::Worker::torrentFound, this, &TorrentFilesWatcher::onTorrentFound); if (btSession->isRestored())
initWorker();
m_asyncWorker->moveToThread(m_ioThread); else
m_ioThread->start(); connect(btSession, &BitTorrent::Session::restored, this, &TorrentFilesWatcher::initWorker);
load(); load();
} }
@ -273,6 +272,27 @@ TorrentFilesWatcher::~TorrentFilesWatcher()
delete m_asyncWorker; delete m_asyncWorker;
} }
void TorrentFilesWatcher::initWorker()
{
Q_ASSERT(!m_asyncWorker);
m_asyncWorker = new TorrentFilesWatcher::Worker;
connect(m_asyncWorker, &TorrentFilesWatcher::Worker::magnetFound, this, &TorrentFilesWatcher::onMagnetFound);
connect(m_asyncWorker, &TorrentFilesWatcher::Worker::torrentFound, this, &TorrentFilesWatcher::onTorrentFound);
m_asyncWorker->moveToThread(m_ioThread);
m_ioThread->start();
for (auto it = m_watchedFolders.cbegin(); it != m_watchedFolders.cend(); ++it)
{
QMetaObject::invokeMethod(m_asyncWorker, [this, path = it.key(), options = it.value()]()
{
m_asyncWorker->setWatchedFolder(path, options);
});
}
}
void TorrentFilesWatcher::load() void TorrentFilesWatcher::load()
{ {
QFile confFile {(specialFolderLocation(SpecialFolder::Config) / Path(CONF_FILE_NAME)).data()}; QFile confFile {(specialFolderLocation(SpecialFolder::Config) / Path(CONF_FILE_NAME)).data()};
@ -399,10 +419,13 @@ void TorrentFilesWatcher::doSetWatchedFolder(const Path &path, const WatchedFold
m_watchedFolders[path] = options; m_watchedFolders[path] = options;
QMetaObject::invokeMethod(m_asyncWorker, [this, path, options]() if (m_asyncWorker)
{ {
m_asyncWorker->setWatchedFolder(path, options); QMetaObject::invokeMethod(m_asyncWorker, [this, path, options]()
}); {
m_asyncWorker->setWatchedFolder(path, options);
});
}
emit watchedFolderSet(path, options); emit watchedFolderSet(path, options);
} }
@ -411,10 +434,13 @@ void TorrentFilesWatcher::removeWatchedFolder(const Path &path)
{ {
if (m_watchedFolders.remove(path)) if (m_watchedFolders.remove(path))
{ {
QMetaObject::invokeMethod(m_asyncWorker, [this, path]() if (m_asyncWorker)
{ {
m_asyncWorker->removeWatchedFolder(path); QMetaObject::invokeMethod(m_asyncWorker, [this, path]()
}); {
m_asyncWorker->removeWatchedFolder(path);
});
}
emit watchedFolderRemoved(path); emit watchedFolderRemoved(path);

1
src/base/torrentfileswatcher.h

@ -78,6 +78,7 @@ private:
explicit TorrentFilesWatcher(QObject *parent = nullptr); explicit TorrentFilesWatcher(QObject *parent = nullptr);
~TorrentFilesWatcher() override; ~TorrentFilesWatcher() override;
void initWorker();
void load(); void load();
void loadLegacy(); void loadLegacy();
void store() const; void store() const;

16
src/gui/categoryfiltermodel.cpp

@ -32,7 +32,6 @@
#include <QIcon> #include <QIcon>
#include "base/bittorrent/session.h" #include "base/bittorrent/session.h"
#include "base/bittorrent/torrent.h"
#include "base/global.h" #include "base/global.h"
#include "uithememanager.h" #include "uithememanager.h"
@ -181,7 +180,7 @@ CategoryFilterModel::CategoryFilterModel(QObject *parent)
connect(session, &Session::categoryRemoved, this, &CategoryFilterModel::categoryRemoved); connect(session, &Session::categoryRemoved, this, &CategoryFilterModel::categoryRemoved);
connect(session, &Session::torrentCategoryChanged, this, &CategoryFilterModel::torrentCategoryChanged); connect(session, &Session::torrentCategoryChanged, this, &CategoryFilterModel::torrentCategoryChanged);
connect(session, &Session::subcategoriesSupportChanged, this, &CategoryFilterModel::subcategoriesSupportChanged); connect(session, &Session::subcategoriesSupportChanged, this, &CategoryFilterModel::subcategoriesSupportChanged);
connect(session, &Session::torrentLoaded, this, &CategoryFilterModel::torrentAdded); connect(session, &Session::torrentsLoaded, this, &CategoryFilterModel::torrentsLoaded);
connect(session, &Session::torrentAboutToBeRemoved, this, &CategoryFilterModel::torrentAboutToBeRemoved); connect(session, &Session::torrentAboutToBeRemoved, this, &CategoryFilterModel::torrentAboutToBeRemoved);
populate(); populate();
@ -333,13 +332,16 @@ void CategoryFilterModel::categoryRemoved(const QString &categoryName)
} }
} }
void CategoryFilterModel::torrentAdded(BitTorrent::Torrent *const torrent) void CategoryFilterModel::torrentsLoaded(const QVector<BitTorrent::Torrent *> &torrents)
{ {
CategoryModelItem *item = findItem(torrent->category()); for (const BitTorrent::Torrent *torrent : torrents)
Q_ASSERT(item); {
CategoryModelItem *item = findItem(torrent->category());
Q_ASSERT(item);
item->increaseTorrentsCount(); item->increaseTorrentsCount();
m_rootItem->childAt(0)->increaseTorrentsCount(); m_rootItem->childAt(0)->increaseTorrentsCount();
}
} }
void CategoryFilterModel::torrentAboutToBeRemoved(BitTorrent::Torrent *const torrent) void CategoryFilterModel::torrentAboutToBeRemoved(BitTorrent::Torrent *const torrent)

10
src/gui/categoryfiltermodel.h

@ -28,17 +28,15 @@
#pragma once #pragma once
#include <QtContainerFwd>
#include <QAbstractItemModel> #include <QAbstractItemModel>
#include "base/bittorrent/torrent.h"
class QModelIndex; class QModelIndex;
class CategoryModelItem; class CategoryModelItem;
namespace BitTorrent
{
class Torrent;
}
class CategoryFilterModel final : public QAbstractItemModel class CategoryFilterModel final : public QAbstractItemModel
{ {
Q_OBJECT Q_OBJECT
@ -64,7 +62,7 @@ public:
private slots: private slots:
void categoryAdded(const QString &categoryName); void categoryAdded(const QString &categoryName);
void categoryRemoved(const QString &categoryName); void categoryRemoved(const QString &categoryName);
void torrentAdded(BitTorrent::Torrent *const torrent); void torrentsLoaded(const QVector<BitTorrent::Torrent *> &torrents);
void torrentAboutToBeRemoved(BitTorrent::Torrent *const torrent); void torrentAboutToBeRemoved(BitTorrent::Torrent *const torrent);
void torrentCategoryChanged(BitTorrent::Torrent *const torrent, const QString &oldCategory); void torrentCategoryChanged(BitTorrent::Torrent *const torrent, const QString &oldCategory);
void subcategoriesSupportChanged(); void subcategoriesSupportChanged();

7
src/gui/mainwindow.cpp

@ -1205,12 +1205,12 @@ void MainWindow::keyPressEvent(QKeyEvent *event)
{ {
if (event->matches(QKeySequence::Paste)) if (event->matches(QKeySequence::Paste))
{ {
const QMimeData *mimeData {QGuiApplication::clipboard()->mimeData()}; const QMimeData *mimeData = QGuiApplication::clipboard()->mimeData();
if (mimeData->hasText()) if (mimeData->hasText())
{ {
const bool useTorrentAdditionDialog {AddNewTorrentDialog::isEnabled()}; const bool useTorrentAdditionDialog = AddNewTorrentDialog::isEnabled();
const QStringList lines {mimeData->text().split(u'\n', Qt::SkipEmptyParts)}; const QStringList lines = mimeData->text().split(u'\n', Qt::SkipEmptyParts);
for (QString line : lines) for (QString line : lines)
{ {
@ -1438,6 +1438,7 @@ void MainWindow::dragEnterEvent(QDragEnterEvent *event)
{ {
for (const QString &mime : asConst(event->mimeData()->formats())) for (const QString &mime : asConst(event->mimeData()->formats()))
qDebug("mimeData: %s", mime.toLocal8Bit().data()); qDebug("mimeData: %s", mime.toLocal8Bit().data());
if (event->mimeData()->hasFormat(u"text/plain"_qs) || event->mimeData()->hasFormat(u"text/uri-list"_qs)) if (event->mimeData()->hasFormat(u"text/plain"_qs) || event->mimeData()->hasFormat(u"text/uri-list"_qs))
event->acceptProposedAction(); event->acceptProposedAction();
} }

10
src/gui/search/searchjobwidget.cpp

@ -208,7 +208,7 @@ void SearchJobWidget::cancelSearch()
void SearchJobWidget::downloadTorrents(const AddTorrentOption option) void SearchJobWidget::downloadTorrents(const AddTorrentOption option)
{ {
const QModelIndexList rows {m_ui->resultsBrowser->selectionModel()->selectedRows()}; const QModelIndexList rows = m_ui->resultsBrowser->selectionModel()->selectedRows();
for (const QModelIndex &rowIndex : rows) for (const QModelIndex &rowIndex : rows)
downloadTorrent(rowIndex, option); downloadTorrent(rowIndex, option);
} }
@ -390,10 +390,10 @@ void SearchJobWidget::contextMenuEvent(QContextMenuEvent *event)
auto *menu = new QMenu(this); auto *menu = new QMenu(this);
menu->setAttribute(Qt::WA_DeleteOnClose); menu->setAttribute(Qt::WA_DeleteOnClose);
menu->addAction(UIThemeManager::instance()->getIcon(u"kt-set-max-download-speed"_qs), tr("Open download window") menu->addAction(UIThemeManager::instance()->getIcon(u"kt-set-max-download-speed"_qs)
, this, [this]() { downloadTorrents(AddTorrentOption::ShowDialog); }); , tr("Open download window"), this, [this]() { downloadTorrents(AddTorrentOption::ShowDialog); });
menu->addAction(UIThemeManager::instance()->getIcon(u"downloading"_qs), tr("Download") menu->addAction(UIThemeManager::instance()->getIcon(u"downloading"_qs)
, this, [this]() { downloadTorrents(AddTorrentOption::SkipDialog); }); , tr("Download"), this, [this]() { downloadTorrents(AddTorrentOption::SkipDialog); });
menu->addSeparator(); menu->addSeparator();
menu->addAction(UIThemeManager::instance()->getIcon(u"application-x-mswinurl"_qs), tr("Open description page") menu->addAction(UIThemeManager::instance()->getIcon(u"application-x-mswinurl"_qs), tr("Open description page")
, this, &SearchJobWidget::openTorrentPages); , this, &SearchJobWidget::openTorrentPages);

20
src/gui/tagfiltermodel.cpp

@ -33,7 +33,6 @@
#include <QVector> #include <QVector>
#include "base/bittorrent/session.h" #include "base/bittorrent/session.h"
#include "base/bittorrent/torrent.h"
#include "base/global.h" #include "base/global.h"
#include "uithememanager.h" #include "uithememanager.h"
@ -99,7 +98,7 @@ TagFilterModel::TagFilterModel(QObject *parent)
connect(session, &Session::tagRemoved, this, &TagFilterModel::tagRemoved); connect(session, &Session::tagRemoved, this, &TagFilterModel::tagRemoved);
connect(session, &Session::torrentTagAdded, this, &TagFilterModel::torrentTagAdded); connect(session, &Session::torrentTagAdded, this, &TagFilterModel::torrentTagAdded);
connect(session, &Session::torrentTagRemoved, this, &TagFilterModel::torrentTagRemoved); connect(session, &Session::torrentTagRemoved, this, &TagFilterModel::torrentTagRemoved);
connect(session, &Session::torrentLoaded, this, &TagFilterModel::torrentAdded); connect(session, &Session::torrentsLoaded, this, &TagFilterModel::torrentsLoaded);
connect(session, &Session::torrentAboutToBeRemoved, this, &TagFilterModel::torrentAboutToBeRemoved); connect(session, &Session::torrentAboutToBeRemoved, this, &TagFilterModel::torrentAboutToBeRemoved);
populate(); populate();
} }
@ -230,16 +229,19 @@ void TagFilterModel::torrentTagRemoved(BitTorrent::Torrent *const torrent, const
emit dataChanged(i, i); emit dataChanged(i, i);
} }
void TagFilterModel::torrentAdded(BitTorrent::Torrent *const torrent) void TagFilterModel::torrentsLoaded(const QVector<BitTorrent::Torrent *> &torrents)
{ {
allTagsItem()->increaseTorrentsCount(); for (const BitTorrent::Torrent *torrent : torrents)
{
allTagsItem()->increaseTorrentsCount();
const QVector<TagModelItem *> items = findItems(torrent->tags()); const QVector<TagModelItem *> items = findItems(torrent->tags());
if (items.isEmpty()) if (items.isEmpty())
untaggedItem()->increaseTorrentsCount(); untaggedItem()->increaseTorrentsCount();
for (TagModelItem *item : items) for (TagModelItem *item : items)
item->increaseTorrentsCount(); item->increaseTorrentsCount();
}
} }
void TagFilterModel::torrentAboutToBeRemoved(BitTorrent::Torrent *const torrent) void TagFilterModel::torrentAboutToBeRemoved(BitTorrent::Torrent *const torrent)

10
src/gui/tagfiltermodel.h

@ -28,20 +28,16 @@
#pragma once #pragma once
#include <QAbstractListModel>
#include <QtContainerFwd> #include <QtContainerFwd>
#include <QAbstractItemModel>
#include "base/bittorrent/torrent.h"
#include "base/tagset.h" #include "base/tagset.h"
class QModelIndex; class QModelIndex;
class TagModelItem; class TagModelItem;
namespace BitTorrent
{
class Torrent;
}
class TagFilterModel final : public QAbstractListModel class TagFilterModel final : public QAbstractListModel
{ {
Q_OBJECT Q_OBJECT
@ -67,7 +63,7 @@ private slots:
void tagRemoved(const QString &tag); void tagRemoved(const QString &tag);
void torrentTagAdded(BitTorrent::Torrent *const torrent, const QString &tag); void torrentTagAdded(BitTorrent::Torrent *const torrent, const QString &tag);
void torrentTagRemoved(BitTorrent::Torrent *const, const QString &tag); void torrentTagRemoved(BitTorrent::Torrent *const, const QString &tag);
void torrentAdded(BitTorrent::Torrent *const torrent); void torrentsLoaded(const QVector<BitTorrent::Torrent *> &torrents);
void torrentAboutToBeRemoved(BitTorrent::Torrent *const torrent); void torrentAboutToBeRemoved(BitTorrent::Torrent *const torrent);
private: private:

64
src/gui/transferlistfilterswidget.cpp

@ -134,8 +134,8 @@ BaseFilterWidget::BaseFilterWidget(QWidget *parent, TransferListWidget *transfer
connect(this, &BaseFilterWidget::customContextMenuRequested, this, &BaseFilterWidget::showMenu); connect(this, &BaseFilterWidget::customContextMenuRequested, this, &BaseFilterWidget::showMenu);
connect(this, &BaseFilterWidget::currentRowChanged, this, &BaseFilterWidget::applyFilter); connect(this, &BaseFilterWidget::currentRowChanged, this, &BaseFilterWidget::applyFilter);
connect(BitTorrent::Session::instance(), &BitTorrent::Session::torrentLoaded connect(BitTorrent::Session::instance(), &BitTorrent::Session::torrentsLoaded
, this, &BaseFilterWidget::handleNewTorrent); , this, &BaseFilterWidget::handleTorrentsLoaded);
connect(BitTorrent::Session::instance(), &BitTorrent::Session::torrentAboutToBeRemoved connect(BitTorrent::Session::instance(), &BitTorrent::Session::torrentAboutToBeRemoved
, this, &BaseFilterWidget::torrentAboutToBeDeleted); , this, &BaseFilterWidget::torrentAboutToBeDeleted);
} }
@ -318,9 +318,11 @@ void StatusFilterWidget::applyFilter(int row)
transferList->applyStatusFilter(row); transferList->applyStatusFilter(row);
} }
void StatusFilterWidget::handleNewTorrent(BitTorrent::Torrent *const torrent) void StatusFilterWidget::handleTorrentsLoaded(const QVector<BitTorrent::Torrent *> &torrents)
{ {
updateTorrentStatus(torrent); for (const BitTorrent::Torrent *torrent : torrents)
updateTorrentStatus(torrent);
updateTexts(); updateTexts();
} }
@ -376,6 +378,8 @@ TrackerFiltersList::TrackerFiltersList(QWidget *parent, TransferListWidget *tran
m_trackers[NULL_HOST] = {{}, noTracker}; m_trackers[NULL_HOST] = {{}, noTracker};
handleTorrentsLoaded(BitTorrent::Session::instance()->torrents());
setCurrentRow(0, QItemSelectionModel::SelectCurrent); setCurrentRow(0, QItemSelectionModel::SelectCurrent);
toggleFilter(Preferences::instance()->getTrackerFilterState()); toggleFilter(Preferences::instance()->getTrackerFilterState());
} }
@ -390,7 +394,7 @@ void TrackerFiltersList::addTrackers(const BitTorrent::Torrent *torrent, const Q
{ {
const BitTorrent::TorrentID torrentID = torrent->id(); const BitTorrent::TorrentID torrentID = torrent->id();
for (const BitTorrent::TrackerEntry &tracker : trackers) for (const BitTorrent::TrackerEntry &tracker : trackers)
addItem(tracker.url, torrentID); addItems(tracker.url, {torrentID});
} }
void TrackerFiltersList::removeTrackers(const BitTorrent::Torrent *torrent, const QStringList &trackers) void TrackerFiltersList::removeTrackers(const BitTorrent::Torrent *torrent, const QStringList &trackers)
@ -431,12 +435,12 @@ void TrackerFiltersList::refreshTrackers(const BitTorrent::Torrent *torrent)
const bool isTrackerless = trackerEntries.isEmpty(); const bool isTrackerless = trackerEntries.isEmpty();
if (isTrackerless) if (isTrackerless)
{ {
addItem(NULL_HOST, torrentID); addItems(NULL_HOST, {torrentID});
} }
else else
{ {
for (const BitTorrent::TrackerEntry &trackerEntry : trackerEntries) for (const BitTorrent::TrackerEntry &trackerEntry : trackerEntries)
addItem(trackerEntry.url, torrentID); addItems(trackerEntry.url, {torrentID});
} }
updateGeometry(); updateGeometry();
@ -445,23 +449,20 @@ void TrackerFiltersList::refreshTrackers(const BitTorrent::Torrent *torrent)
void TrackerFiltersList::changeTrackerless(const BitTorrent::Torrent *torrent, const bool trackerless) void TrackerFiltersList::changeTrackerless(const BitTorrent::Torrent *torrent, const bool trackerless)
{ {
if (trackerless) if (trackerless)
addItem(NULL_HOST, torrent->id()); addItems(NULL_HOST, {torrent->id()});
else else
removeItem(NULL_HOST, torrent->id()); removeItem(NULL_HOST, torrent->id());
} }
void TrackerFiltersList::addItem(const QString &tracker, const BitTorrent::TorrentID &id) void TrackerFiltersList::addItems(const QString &trackerURL, const QVector<BitTorrent::TorrentID> &torrents)
{ {
const QString host = getHost(tracker); const QString host = getHost(trackerURL);
auto trackersIt = m_trackers.find(host); auto trackersIt = m_trackers.find(host);
const bool exists = (trackersIt != m_trackers.end()); const bool exists = (trackersIt != m_trackers.end());
QListWidgetItem *trackerItem = nullptr; QListWidgetItem *trackerItem = nullptr;
if (exists) if (exists)
{ {
if (trackersIt->torrents.contains(id))
return;
trackerItem = trackersIt->item; trackerItem = trackersIt->item;
} }
else else
@ -469,17 +470,18 @@ void TrackerFiltersList::addItem(const QString &tracker, const BitTorrent::Torre
trackerItem = new QListWidgetItem(); trackerItem = new QListWidgetItem();
trackerItem->setData(Qt::DecorationRole, UIThemeManager::instance()->getIcon(u"trackers"_qs)); trackerItem->setData(Qt::DecorationRole, UIThemeManager::instance()->getIcon(u"trackers"_qs));
TrackerData trackerData {{}, trackerItem}; const TrackerData trackerData {{}, trackerItem};
trackersIt = m_trackers.insert(host, trackerData); trackersIt = m_trackers.insert(host, trackerData);
const QString scheme = getScheme(tracker); const QString scheme = getScheme(trackerURL);
downloadFavicon(u"%1://%2/favicon.ico"_qs.arg((scheme.startsWith(u"http") ? scheme : u"http"_qs), host)); downloadFavicon(u"%1://%2/favicon.ico"_qs.arg((scheme.startsWith(u"http") ? scheme : u"http"_qs), host));
} }
Q_ASSERT(trackerItem); Q_ASSERT(trackerItem);
QSet<BitTorrent::TorrentID> &torrentIDs = trackersIt->torrents; QSet<BitTorrent::TorrentID> &torrentIDs = trackersIt->torrents;
torrentIDs.insert(id); for (const BitTorrent::TorrentID &torrentID : torrents)
torrentIDs.insert(torrentID);
trackerItem->setText(u"%1 (%2)"_qs.arg(((host == NULL_HOST) ? tr("Trackerless") : host), QString::number(torrentIDs.size()))); trackerItem->setText(u"%1 (%2)"_qs.arg(((host == NULL_HOST) ? tr("Trackerless") : host), QString::number(torrentIDs.size())));
if (exists) if (exists)
@ -724,18 +726,30 @@ void TrackerFiltersList::applyFilter(const int row)
transferList->applyTrackerFilter(getTorrentIDs(row)); transferList->applyTrackerFilter(getTorrentIDs(row));
} }
void TrackerFiltersList::handleNewTorrent(BitTorrent::Torrent *const torrent) void TrackerFiltersList::handleTorrentsLoaded(const QVector<BitTorrent::Torrent *> &torrents)
{ {
const BitTorrent::TorrentID torrentID = torrent->id(); QHash<QString, QVector<BitTorrent::TorrentID>> torrentsPerTracker;
const QVector<BitTorrent::TrackerEntry> trackers = torrent->trackers(); for (const BitTorrent::Torrent *torrent : torrents)
for (const BitTorrent::TrackerEntry &tracker : trackers) {
addItem(tracker.url, torrentID); const BitTorrent::TorrentID torrentID = torrent->id();
const QVector<BitTorrent::TrackerEntry> trackers = torrent->trackers();
for (const BitTorrent::TrackerEntry &tracker : trackers)
torrentsPerTracker[tracker.url].append(torrentID);
// Check for trackerless torrent // Check for trackerless torrent
if (trackers.isEmpty()) if (trackers.isEmpty())
addItem(NULL_HOST, torrentID); torrentsPerTracker[NULL_HOST].append(torrentID);
}
for (auto it = torrentsPerTracker.cbegin(); it != torrentsPerTracker.cend(); ++it)
{
const QString &trackerURL = it.key();
const QVector<BitTorrent::TorrentID> &torrents = it.value();
addItems(trackerURL, torrents);
}
item(ALL_ROW)->setText(tr("All (%1)", "this is for the tracker filter").arg(++m_totalTorrents)); m_totalTorrents += torrents.count();
item(ALL_ROW)->setText(tr("All (%1)", "this is for the tracker filter").arg(m_totalTorrents));
} }
void TrackerFiltersList::torrentAboutToBeDeleted(BitTorrent::Torrent *const torrent) void TrackerFiltersList::torrentAboutToBeDeleted(BitTorrent::Torrent *const torrent)

8
src/gui/transferlistfilterswidget.h

@ -71,7 +71,7 @@ protected:
private slots: private slots:
virtual void showMenu() = 0; virtual void showMenu() = 0;
virtual void applyFilter(int row) = 0; virtual void applyFilter(int row) = 0;
virtual void handleNewTorrent(BitTorrent::Torrent *const) = 0; virtual void handleTorrentsLoaded(const QVector<BitTorrent::Torrent *> &torrents) = 0;
virtual void torrentAboutToBeDeleted(BitTorrent::Torrent *const) = 0; virtual void torrentAboutToBeDeleted(BitTorrent::Torrent *const) = 0;
}; };
@ -92,7 +92,7 @@ private:
// No need to redeclare them here as slots. // No need to redeclare them here as slots.
void showMenu() override; void showMenu() override;
void applyFilter(int row) override; void applyFilter(int row) override;
void handleNewTorrent(BitTorrent::Torrent *const) override; void handleTorrentsLoaded(const QVector<BitTorrent::Torrent *> &torrents) override;
void torrentAboutToBeDeleted(BitTorrent::Torrent *const) override; void torrentAboutToBeDeleted(BitTorrent::Torrent *const) override;
void populate(); void populate();
@ -139,10 +139,10 @@ private:
// No need to redeclare them here as slots. // No need to redeclare them here as slots.
void showMenu() override; void showMenu() override;
void applyFilter(int row) override; void applyFilter(int row) override;
void handleNewTorrent(BitTorrent::Torrent *const torrent) override; void handleTorrentsLoaded(const QVector<BitTorrent::Torrent *> &torrents) override;
void torrentAboutToBeDeleted(BitTorrent::Torrent *const torrent) override; void torrentAboutToBeDeleted(BitTorrent::Torrent *const torrent) override;
void addItem(const QString &tracker, const BitTorrent::TorrentID &id); void addItems(const QString &trackerURL, const QVector<BitTorrent::TorrentID> &torrents);
void removeItem(const QString &trackerURL, const BitTorrent::TorrentID &id); void removeItem(const QString &trackerURL, const BitTorrent::TorrentID &id);
QString trackerFromRow(int row) const; QString trackerFromRow(int row) const;
int rowFromTracker(const QString &tracker) const; int rowFromTracker(const QString &tracker) const;

21
src/gui/transferlistmodel.cpp

@ -166,11 +166,10 @@ TransferListModel::TransferListModel(QObject *parent)
// Load the torrents // Load the torrents
using namespace BitTorrent; using namespace BitTorrent;
for (Torrent *const torrent : asConst(Session::instance()->torrents())) addTorrents(Session::instance()->torrents());
addTorrent(torrent);
// Listen for torrent changes // Listen for torrent changes
connect(Session::instance(), &Session::torrentLoaded, this, &TransferListModel::addTorrent); connect(Session::instance(), &Session::torrentsLoaded, this, &TransferListModel::addTorrents);
connect(Session::instance(), &Session::torrentAboutToBeRemoved, this, &TransferListModel::handleTorrentAboutToBeRemoved); connect(Session::instance(), &Session::torrentAboutToBeRemoved, this, &TransferListModel::handleTorrentAboutToBeRemoved);
connect(Session::instance(), &Session::torrentsUpdated, this, &TransferListModel::handleTorrentsUpdated); connect(Session::instance(), &Session::torrentsUpdated, this, &TransferListModel::handleTorrentsUpdated);
@ -599,15 +598,19 @@ bool TransferListModel::setData(const QModelIndex &index, const QVariant &value,
return true; return true;
} }
void TransferListModel::addTorrent(BitTorrent::Torrent *const torrent) void TransferListModel::addTorrents(const QVector<BitTorrent::Torrent *> &torrents)
{ {
Q_ASSERT(!m_torrentMap.contains(torrent)); int row = m_torrentList.size();
beginInsertRows({}, row, (row + torrents.size()));
const int row = m_torrentList.size(); for (BitTorrent::Torrent *torrent : torrents)
{
Q_ASSERT(!m_torrentMap.contains(torrent));
m_torrentList.append(torrent);
m_torrentMap[torrent] = row++;
}
beginInsertRows({}, row, row);
m_torrentList << torrent;
m_torrentMap[torrent] = row;
endInsertRows(); endInsertRows();
} }

2
src/gui/transferlistmodel.h

@ -103,7 +103,7 @@ public:
BitTorrent::Torrent *torrentHandle(const QModelIndex &index) const; BitTorrent::Torrent *torrentHandle(const QModelIndex &index) const;
private slots: private slots:
void addTorrent(BitTorrent::Torrent *const torrent); void addTorrents(const QVector<BitTorrent::Torrent *> &torrents);
void handleTorrentAboutToBeRemoved(BitTorrent::Torrent *const torrent); void handleTorrentAboutToBeRemoved(BitTorrent::Torrent *const torrent);
void handleTorrentStatusUpdated(BitTorrent::Torrent *const torrent); void handleTorrentStatusUpdated(BitTorrent::Torrent *const torrent);
void handleTorrentsUpdated(const QVector<BitTorrent::Torrent *> &torrents); void handleTorrentsUpdated(const QVector<BitTorrent::Torrent *> &torrents);

Loading…
Cancel
Save