From be7cfb78dee872d3b6daac6d5fc85955d444dec8 Mon Sep 17 00:00:00 2001 From: Vladimir Golovnev Date: Mon, 4 Jul 2022 12:48:21 +0300 Subject: [PATCH] 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. --- src/app/application.cpp | 119 ++-- src/app/application.h | 16 +- src/base/CMakeLists.txt | 1 + src/base/base.pri | 1 + .../bittorrent/bencoderesumedatastorage.cpp | 122 ++-- .../bittorrent/bencoderesumedatastorage.h | 8 +- src/base/bittorrent/dbresumedatastorage.cpp | 169 +++-- src/base/bittorrent/dbresumedatastorage.h | 8 +- src/base/bittorrent/resumedatastorage.cpp | 76 ++ src/base/bittorrent/resumedatastorage.h | 43 +- src/base/bittorrent/session.cpp | 658 ++++++++++-------- src/base/bittorrent/session.h | 19 +- src/base/rss/rss_autodownloader.cpp | 19 +- src/base/torrentfileswatcher.cpp | 50 +- src/base/torrentfileswatcher.h | 1 + src/gui/categoryfiltermodel.cpp | 16 +- src/gui/categoryfiltermodel.h | 10 +- src/gui/mainwindow.cpp | 7 +- src/gui/search/searchjobwidget.cpp | 10 +- src/gui/tagfiltermodel.cpp | 20 +- src/gui/tagfiltermodel.h | 10 +- src/gui/transferlistfilterswidget.cpp | 64 +- src/gui/transferlistfilterswidget.h | 8 +- src/gui/transferlistmodel.cpp | 21 +- src/gui/transferlistmodel.h | 2 +- 25 files changed, 926 insertions(+), 552 deletions(-) create mode 100644 src/base/bittorrent/resumedatastorage.cpp diff --git a/src/app/application.cpp b/src/app/application.cpp index 1a43049c7..5b32738f2 100644 --- a/src/app/application.cpp +++ b/src/app/application.cpp @@ -309,10 +309,18 @@ void Application::setFileLoggerAgeType(const int value) void Application::processMessage(const QString &message) { - const QStringList params = message.split(PARAMS_SEPARATOR, Qt::SkipEmptyParts); - // If Application is not running (i.e., other - // components are not ready) store params - if (m_running) +#ifndef DISABLE_GUI + if (message.isEmpty()) + { + 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); else m_paramsQueue.append(params); @@ -516,21 +524,10 @@ bool Application::sendParams(const QStringList ¶ms) return m_instanceManager->sendMessage(params.join(PARAMS_SEPARATOR)); } -// As program parameters, we can get paths or urls. -// This function parse the parameters and call -// the right addTorrent function, considering -// the parameter type. -void Application::processParams(const QStringList ¶ms) +Application::AddTorrentParams Application::parseParams(const QStringList ¶ms) const { -#ifndef DISABLE_GUI - if (params.isEmpty()) - { - m_window->activate(); // show UI - return; - } -#endif - BitTorrent::AddTorrentParams torrentParams; - std::optional skipTorrentDialog; + AddTorrentParams parsedParams; + BitTorrent::AddTorrentParams &torrentParams = parsedParams.torrentParams; for (QString param : params) { @@ -576,23 +573,31 @@ void Application::processParams(const QStringList ¶ms) if (param.startsWith(u"@skipDialog=")) { - skipTorrentDialog = (QStringView(param).mid(12).toInt() != 0); + parsedParams.skipTorrentDialog = (QStringView(param).mid(12).toInt() != 0); continue; } + parsedParams.torrentSource = param; + break; + } + + return parsedParams; +} + +void Application::processParams(const AddTorrentParams ¶ms) +{ #ifndef DISABLE_GUI - // There are two circumstances in which we want to show the torrent - // dialog. One is when the application settings specify that it should - // be shown and skipTorrentDialog is undefined. The other is when - // skipTorrentDialog is false, meaning that the application setting - // should be overridden. - const bool showDialogForThisTorrent = !skipTorrentDialog.value_or(!AddNewTorrentDialog::isEnabled()); - if (showDialogForThisTorrent) - AddNewTorrentDialog::show(param, torrentParams, m_window); - else + // There are two circumstances in which we want to show the torrent + // dialog. One is when the application settings specify that it should + // be shown and skipTorrentDialog is undefined. The other is when + // skipTorrentDialog is false, meaning that the application setting + // should be overridden. + const bool showDialogForThisTorrent = !params.skipTorrentDialog.value_or(!AddNewTorrentDialog::isEnabled()); + if (showDialogForThisTorrent) + AddNewTorrentDialog::show(params.torrentSource, params.torrentParams, m_window); + else #endif - BitTorrent::Session::instance()->addTorrent(param, torrentParams); - } + BitTorrent::Session::instance()->addTorrent(params.torrentSource, params.torrentParams); } int Application::exec(const QStringList ¶ms) @@ -612,21 +617,28 @@ int Application::exec(const QStringList ¶ms) try { 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 ¶ms : m_paramsQueue) + processParams(params); + m_paramsQueue.clear(); + }); connect(BitTorrent::Session::instance(), &BitTorrent::Session::torrentFinished, this, &Application::torrentFinished); connect(BitTorrent::Session::instance(), &BitTorrent::Session::allTorrentsFinished, this, &Application::allTorrentsFinished, Qt::QueuedConnection); Net::GeoIPManager::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::AutoDownloader; // create RSS::AutoDownloader singleton } @@ -669,17 +681,9 @@ int Application::exec(const QStringList ¶ms) m_window = new MainWindow(this); #endif // DISABLE_GUI - m_running = true; - - // Now UI is ready to process signals from Session - BitTorrent::Session::instance()->startUpTorrents(); + if (!params.isEmpty()) + m_paramsQueue.append(parseParams(params)); - m_paramsQueue = params + m_paramsQueue; - if (!m_paramsQueue.isEmpty()) - { - processParams(m_paramsQueue); - m_paramsQueue.clear(); - } return BaseApplication::exec(); } @@ -699,16 +703,19 @@ bool Application::event(QEvent *ev) // Get the url instead path = static_cast(ev)->url().toString(); 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 - m_paramsQueue.append(path); + m_paramsQueue.append(params); + return true; } - else - { - return BaseApplication::event(ev); - } + + return BaseApplication::event(ev); } #endif // Q_OS_MACOS #endif // DISABLE_GUI diff --git a/src/app/application.h b/src/app/application.h index 392e59912..bf6ba52c1 100644 --- a/src/app/application.h +++ b/src/app/application.h @@ -41,6 +41,7 @@ #include #endif +#include "base/bittorrent/addtorrentparams.h" #include "base/interfaces/iapplication.h" #include "base/path.h" #include "base/settingvalue.h" @@ -132,8 +133,16 @@ private slots: #endif private: + struct AddTorrentParams + { + QString torrentSource; + BitTorrent::AddTorrentParams torrentParams; + std::optional skipTorrentDialog; + }; + void initializeTranslation(); - void processParams(const QStringList ¶ms); + AddTorrentParams parseParams(const QStringList ¶ms) const; + void processParams(const AddTorrentParams ¶ms); void runExternalProgram(const BitTorrent::Torrent *torrent) const; void sendNotificationEmail(const BitTorrent::Torrent *torrent); @@ -152,8 +161,8 @@ private: #endif ApplicationInstanceManager *m_instanceManager = nullptr; - bool m_running = false; QAtomicInt m_isCleanupRun; + bool m_isProcessingParamsAllowed = false; ShutdownDialogAction m_shutdownAct; QBtCommandLineParameters m_commandLineArgs; @@ -162,7 +171,8 @@ private: QTranslator m_qtTranslator; QTranslator m_translator; - QStringList m_paramsQueue; + + QList m_paramsQueue; SettingValue m_storeFileLoggerEnabled; SettingValue m_storeFileLoggerBackup; diff --git a/src/base/CMakeLists.txt b/src/base/CMakeLists.txt index 3e8cd98e2..d94cc27b1 100644 --- a/src/base/CMakeLists.txt +++ b/src/base/CMakeLists.txt @@ -124,6 +124,7 @@ add_library(qbt_base STATIC bittorrent/peeraddress.cpp bittorrent/peerinfo.cpp bittorrent/portforwarderimpl.cpp + bittorrent/resumedatastorage.cpp bittorrent/session.cpp bittorrent/speedmonitor.cpp bittorrent/statistics.cpp diff --git a/src/base/base.pri b/src/base/base.pri index 1f74e8288..79d37b9ec 100644 --- a/src/base/base.pri +++ b/src/base/base.pri @@ -124,6 +124,7 @@ SOURCES += \ $$PWD/bittorrent/peeraddress.cpp \ $$PWD/bittorrent/peerinfo.cpp \ $$PWD/bittorrent/portforwarderimpl.cpp \ + $$PWD/bittorrent/resumedatastorage.cpp \ $$PWD/bittorrent/session.cpp \ $$PWD/bittorrent/speedmonitor.cpp \ $$PWD/bittorrent/statistics.cpp \ diff --git a/src/base/bittorrent/bencoderesumedatastorage.cpp b/src/base/bittorrent/bencoderesumedatastorage.cpp index d5ff8932d..415867a95 100644 --- a/src/base/bittorrent/bencoderesumedatastorage.cpp +++ b/src/base/bittorrent/bencoderesumedatastorage.cpp @@ -90,21 +90,20 @@ namespace } BitTorrent::BencodeResumeDataStorage::BencodeResumeDataStorage(const Path &path, QObject *parent) - : ResumeDataStorage {parent} - , m_resumeDataPath {path} - , m_ioThread {new QThread {this}} - , m_asyncWorker {new Worker(m_resumeDataPath)} + : ResumeDataStorage(path, parent) + , m_ioThread {new QThread(this)} + , m_asyncWorker {new Worker(path)} { 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\"") - .arg(m_resumeDataPath.toString())); + .arg(path.toString())); } 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()); for (const QString &filename : filenames) @@ -114,7 +113,7 @@ BitTorrent::BencodeResumeDataStorage::BencodeResumeDataStorage(const Path &path, 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(); @@ -134,25 +133,19 @@ QVector BitTorrent::BencodeResumeDataStorage::registeredT return m_registeredTorrents; } -std::optional BitTorrent::BencodeResumeDataStorage::load(const TorrentID &id) const +BitTorrent::LoadResumeDataResult BitTorrent::BencodeResumeDataStorage::load(const TorrentID &id) const { const QString idString = id.toString(); - const Path fastresumePath = m_resumeDataPath / Path(idString + u".fastresume"); - const Path torrentFilePath = m_resumeDataPath / Path(idString + u".torrent"); + const Path fastresumePath = path() / Path(idString + u".fastresume"); + const Path torrentFilePath = path() / Path(idString + u".torrent"); QFile resumeDataFile {fastresumePath.data()}; if (!resumeDataFile.open(QIODevice::ReadOnly)) - { - LogMsg(tr("Cannot read file %1: %2").arg(fastresumePath.toString(), resumeDataFile.errorString()), Log::WARNING); - return std::nullopt; - } + return nonstd::make_unexpected(tr("Cannot read file %1: %2").arg(fastresumePath.toString(), resumeDataFile.errorString())); QFile metadataFile {torrentFilePath.data()}; if (metadataFile.exists() && !metadataFile.open(QIODevice::ReadOnly)) - { - LogMsg(tr("Cannot read file %1: %2").arg(torrentFilePath.toString(), metadataFile.errorString()), Log::WARNING); - return std::nullopt; - } + return nonstd::make_unexpected(tr("Cannot read file %1: %2").arg(torrentFilePath.toString(), metadataFile.errorString())); const QByteArray data = resumeDataFile.readAll(); const QByteArray metadata = (metadataFile.isOpen() ? metadataFile.readAll() : ""); @@ -160,16 +153,64 @@ std::optional BitTorrent::BencodeResumeDataStorag return loadTorrentResumeData(data, metadata); } -std::optional BitTorrent::BencodeResumeDataStorage::loadTorrentResumeData( - const QByteArray &data, const QByteArray &metadata) const +void BitTorrent::BencodeResumeDataStorage::doLoadAll() const +{ + qDebug() << "Loading torrents count: " << m_registeredTorrents.size(); + + emit const_cast(this)->loadStarted(m_registeredTorrents); + + for (const TorrentID &torrentID : asConst(m_registeredTorrents)) + onResumeDataLoaded(torrentID, load(torrentID)); + + emit const_cast(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()) ? data : (data.chopped(1) + metadata.mid(1))); lt::error_code ec; const lt::bdecode_node root = lt::bdecode(allData, ec); - if (ec || (root.type() != lt::bdecode_node::dict_t)) - return std::nullopt; + if (ec) + 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; torrentParams.restored = true; @@ -247,7 +288,7 @@ std::optional BitTorrent::BencodeResumeDataStorag const bool hasMetadata = (p.ti && p.ti->is_valid()); 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; } @@ -276,39 +317,6 @@ void BitTorrent::BencodeResumeDataStorage::storeQueue(const QVector & }); } -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) : m_resumeDataDir {resumeDataDir} { diff --git a/src/base/bittorrent/bencoderesumedatastorage.h b/src/base/bittorrent/bencoderesumedatastorage.h index b7f558927..3ce84a32b 100644 --- a/src/base/bittorrent/bencoderesumedatastorage.h +++ b/src/base/bittorrent/bencoderesumedatastorage.h @@ -31,7 +31,7 @@ #include #include -#include "base/path.h" +#include "base/pathfwd.h" #include "resumedatastorage.h" class QByteArray; @@ -49,16 +49,16 @@ namespace BitTorrent ~BencodeResumeDataStorage() override; QVector registeredTorrents() const override; - std::optional load(const TorrentID &id) const override; + LoadResumeDataResult load(const TorrentID &id) const override; void store(const TorrentID &id, const LoadTorrentParams &resumeData) const override; void remove(const TorrentID &id) const override; void storeQueue(const QVector &queue) const override; private: + void doLoadAll() const override; void loadQueue(const Path &queueFilename); - std::optional loadTorrentResumeData(const QByteArray &data, const QByteArray &metadata) const; + LoadResumeDataResult loadTorrentResumeData(const QByteArray &data, const QByteArray &metadata) const; - const Path m_resumeDataPath; QVector m_registeredTorrents; QThread *m_ioThread = nullptr; diff --git a/src/base/bittorrent/dbresumedatastorage.cpp b/src/base/bittorrent/dbresumedatastorage.cpp index 90c1bf975..3bf2ccce4 100644 --- a/src/base/bittorrent/dbresumedatastorage.cpp +++ b/src/base/bittorrent/dbresumedatastorage.cpp @@ -175,7 +175,7 @@ namespace BitTorrent Q_DISABLE_COPY_MOVE(Worker) public: - Worker(const Path &dbPath, const QString &dbConnectionName); + Worker(const Path &dbPath, const QString &dbConnectionName, QReadWriteLock &dbLock); void openDatabase() const; void closeDatabase() const; @@ -187,11 +187,64 @@ namespace BitTorrent private: const Path m_path; 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( + query.value(DB_COLUMN_CONTENT_LAYOUT.name).toString(), TorrentContentLayout::Original); + resumeData.operatingMode = Utils::String::toEnum( + 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) - : ResumeDataStorage {parent} + : ResumeDataStorage(dbPath, parent) , m_ioThread {new QThread(this)} { const bool needCreateDB = !dbPath.exists(); @@ -212,7 +265,7 @@ BitTorrent::DBResumeDataStorage::DBResumeDataStorage(const Path &dbPath, QObject updateDBFromVersion1(); } - m_asyncWorker = new Worker(dbPath, u"ResumeDataStorageWorker"_qs); + m_asyncWorker = new Worker(dbPath, u"ResumeDataStorageWorker"_qs, m_dbLock); m_asyncWorker->moveToThread(m_ioThread); connect(m_ioThread, &QThread::finished, m_asyncWorker, &QObject::deleteLater); m_ioThread->start(); @@ -262,7 +315,7 @@ QVector BitTorrent::DBResumeDataStorage::registeredTorren return registeredTorrents; } -std::optional 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 .arg(quoted(DB_TABLE_TORRENTS), quoted(DB_COLUMN_TORRENT_ID.name), DB_COLUMN_TORRENT_ID.placeholder); @@ -283,56 +336,11 @@ std::optional BitTorrent::DBResumeDataStorage::lo } catch (const RuntimeError &err) { - LogMsg(tr("Couldn't load resume data of torrent '%1'. Error: %2") - .arg(id.toString(), err.message()), Log::CRITICAL); - return std::nullopt; + return nonstd::make_unexpected(tr("Couldn't load resume data of torrent '%1'. Error: %2") + .arg(id.toString(), err.message())); } - 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( - query.value(DB_COLUMN_CONTENT_LAYOUT.name).toString(), TorrentContentLayout::Original); - resumeData.operatingMode = Utils::String::toEnum( - 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; + return parseQueryResultRow(query); } void BitTorrent::DBResumeDataStorage::store(const TorrentID &id, const LoadTorrentParams &resumeData) const @@ -359,6 +367,49 @@ void BitTorrent::DBResumeDataStorage::storeQueue(const QVector &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 registeredTorrents; + registeredTorrents.reserve(query.size()); + while (query.next()) + registeredTorrents.append(TorrentID::fromString(query.value(0).toString())); + + emit const_cast(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(this)->loadFinished(); + + QSqlDatabase::removeDatabase(connectionName); +} + int BitTorrent::DBResumeDataStorage::currentDBVersion() const { 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); + const QReadLocker locker {&m_dbLock}; + if (!query.exec()) throw RuntimeError(query.lastError().text()); @@ -390,6 +443,8 @@ void BitTorrent::DBResumeDataStorage::createDB() const { auto db = QSqlDatabase::database(DB_CONNECTION_NAME); + const QWriteLocker locker {&m_dbLock}; + if (!db.transaction()) throw RuntimeError(db.lastError().text()); @@ -453,6 +508,8 @@ void BitTorrent::DBResumeDataStorage::updateDBFromVersion1() const { auto db = QSqlDatabase::database(DB_CONNECTION_NAME); + const QWriteLocker locker {&m_dbLock}; + if (!db.transaction()) 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_connectionName {dbConnectionName} + , m_dbLock {dbLock} { } @@ -612,6 +670,7 @@ void BitTorrent::DBResumeDataStorage::Worker::store(const TorrentID &id, const L if (!bencodedMetadata.isEmpty()) query.bindValue(DB_COLUMN_METADATA.placeholder, bencodedMetadata); + const QWriteLocker locker {&m_dbLock}; if (!query.exec()) throw RuntimeError(query.lastError().text()); } @@ -636,6 +695,8 @@ void BitTorrent::DBResumeDataStorage::Worker::remove(const TorrentID &id) const throw RuntimeError(query.lastError().text()); query.bindValue(DB_COLUMN_TORRENT_ID.placeholder, id.toString()); + + const QWriteLocker locker {&m_dbLock}; if (!query.exec()) throw RuntimeError(query.lastError().text()); } @@ -656,6 +717,8 @@ void BitTorrent::DBResumeDataStorage::Worker::storeQueue(const QVector + #include "base/pathfwd.h" #include "resumedatastorage.h" @@ -45,12 +47,14 @@ namespace BitTorrent ~DBResumeDataStorage() override; QVector registeredTorrents() const override; - std::optional load(const TorrentID &id) const override; + LoadResumeDataResult load(const TorrentID &id) const override; + void store(const TorrentID &id, const LoadTorrentParams &resumeData) const override; void remove(const TorrentID &id) const override; void storeQueue(const QVector &queue) const override; private: + void doLoadAll() const override; int currentDBVersion() const; void createDB() const; void updateDBFromVersion1() const; @@ -59,5 +63,7 @@ namespace BitTorrent class Worker; Worker *m_asyncWorker = nullptr; + + mutable QReadWriteLock m_dbLock; }; } diff --git a/src/base/bittorrent/resumedatastorage.cpp b/src/base/bittorrent/resumedatastorage.cpp new file mode 100644 index 000000000..0b640a95e --- /dev/null +++ b/src/base/bittorrent/resumedatastorage.cpp @@ -0,0 +1,76 @@ +/* + * Bittorrent Client using Qt and libtorrent. + * Copyright (C) 2015-2022 Vladimir Golovnev + * + * 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 + +#include +#include +#include + +const int TORRENTIDLIST_TYPEID = qRegisterMetaType>(); + +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::ResumeDataStorage::fetchLoadedResumeData() const +{ + const QMutexLocker locker {&m_loadedResumeDataMutex}; + + const QVector 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}); +} diff --git a/src/base/bittorrent/resumedatastorage.h b/src/base/bittorrent/resumedatastorage.h index 0b5f436aa..3faf63494 100644 --- a/src/base/bittorrent/resumedatastorage.h +++ b/src/base/bittorrent/resumedatastorage.h @@ -1,6 +1,6 @@ /* * Bittorrent Client using Qt and libtorrent. - * Copyright (C) 2015, 2018 Vladimir Golovnev + * Copyright (C) 2015-2022 Vladimir Golovnev * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License @@ -28,15 +28,25 @@ #pragma once -#include - #include +#include #include +#include + +#include "base/3rdparty/expected.hpp" +#include "base/path.h" +#include "infohash.h" +#include "loadtorrentparams.h" namespace BitTorrent { - class TorrentID; - struct LoadTorrentParams; + using LoadResumeDataResult = nonstd::expected; + + struct LoadedResumeData + { + TorrentID torrentID; + LoadResumeDataResult result; + }; class ResumeDataStorage : public QObject { @@ -44,12 +54,31 @@ namespace BitTorrent Q_DISABLE_COPY_MOVE(ResumeDataStorage) public: - using QObject::QObject; + explicit ResumeDataStorage(const Path &path, QObject *parent = nullptr); + + Path path() const; virtual QVector registeredTorrents() const = 0; - virtual std::optional 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 remove(const TorrentID &id) const = 0; virtual void storeQueue(const QVector &queue) const = 0; + + void loadAll() const; + QVector fetchLoadedResumeData() const; + + signals: + void loadStarted(const QVector &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 m_loadedResumeData; + mutable QMutex m_loadedResumeDataMutex; }; } diff --git a/src/base/bittorrent/session.cpp b/src/base/bittorrent/session.cpp index d4ea39fd3..46e7f9d0f 100644 --- a/src/base/bittorrent/session.cpp +++ b/src/base/bittorrent/session.cpp @@ -104,6 +104,7 @@ #include "magneturi.h" #include "nativesessionextension.h" #include "portforwarderimpl.h" +#include "resumedatastorage.h" #include "statistics.h" #include "torrentimpl.h" #include "tracker.h" @@ -112,6 +113,7 @@ using namespace std::chrono_literals; using namespace BitTorrent; const Path CATEGORIES_FILE_NAME {u"categories.json"_qs}; +const int MAX_PROCESSING_RESUMEDATA_COUNT = 50; namespace { @@ -297,6 +299,23 @@ namespace #endif } +struct BitTorrent::Session::ResumeSessionContext final : public QObject +{ + using QObject::QObject; + + ResumeDataStorage *startupStorage = nullptr; + ResumeDataStorageType currentStorageType = ResumeDataStorageType::Legacy; + QVector loadedResumeData; + int processingResumeDataCount = 0; + bool isLoadFinished = false; + bool isLoadedResumeDataHandlingEnqueued = false; + QSet recoveredCategories; +#ifdef QBT_USES_LIBTORRENT2 + QSet indexedTorrents; + QSet skippedIDs; +#endif +}; + const int addTorrentParamsId = qRegisterMetaType(); // Session @@ -467,7 +486,6 @@ Session::Session(QObject *parent) const QStringList storedTags = m_storedTags.get(); m_tags = {storedTags.cbegin(), storedTags.cend()}; - enqueueRefresh(); updateSeedingLimitTimer(); populateAdditionalTrackers(); if (isExcludedFileNamesEnabled()) @@ -494,19 +512,12 @@ Session::Session(QObject *parent) 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 - new PortForwarderImpl {m_nativeSession}; + new PortForwarderImpl(m_nativeSession); initMetrics(); + + prepareStartup(); } bool Session::isDHTEnabled() const @@ -1062,6 +1073,285 @@ void Session::configureComponents() #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 &torrents) + { +#ifdef QBT_USES_LIBTORRENT2 + context->indexedTorrents = QSet(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 &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 &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 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 loadPreferredResumeDataResult = context->startupStorage->load(torrentID); + if (loadPreferredResumeDataResult) + { + std::shared_ptr 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() { 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; } #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("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); if (loadingTorrentsIter != m_loadingTorrents.end()) { - LoadTorrentParams params = loadingTorrentsIter.value(); - m_loadingTorrents.erase(loadingTorrentsIter); - + LoadTorrentParams ¶ms = loadingTorrentsIter.value(); lt::add_torrent_params &p = params.ltAddTorrentParams; 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) 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 ¶ms) { // `source`: .torrent file path/url or magnet uri + if (!isRestored()) + return false; + if (Net::DownloadManager::hasSupportedScheme(source)) { LogMsg(tr("Downloading torrent, please wait... Source: \"%1\"").arg(source)); @@ -2083,13 +2374,20 @@ bool Session::addTorrent(const QString &source, const AddTorrentParams ¶ms) bool Session::addTorrent(const MagnetUri &magnetUri, const AddTorrentParams ¶ms) { - if (!magnetUri.isValid()) return false; + if (!isRestored()) + return false; + + if (!magnetUri.isValid()) + return false; return addTorrent_impl(magnetUri, params); } bool Session::addTorrent(const TorrentInfo &torrentInfo, const AddTorrentParams ¶ms) { + if (!isRestored()) + return false; + return addTorrent_impl(torrentInfo, params); } @@ -2152,6 +2450,8 @@ LoadTorrentParams Session::initLoadTorrentParams(const AddTorrentParams &addTorr // Add a torrent to the BitTorrent session bool Session::addTorrent_impl(const std::variant &source, const AddTorrentParams &addTorrentParams) { + Q_ASSERT(isRestored()); + const bool hasMetadata = std::holds_alternative(source); const auto id = TorrentID::fromInfoHash(hasMetadata ? std::get(source).infoHash() : std::get(source).infoHash()); @@ -2332,36 +2632,18 @@ bool Session::addTorrent_impl(const std::variant &source 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 p.max_connections = maxConnectionsPerTorrent(); p.max_uploads = maxUploadsPerTorrent(); - const bool hasMetadata = (p.ti && p.ti->is_valid()); -#ifdef QBT_USES_LIBTORRENT2 - const auto id = TorrentID::fromInfoHash(hasMetadata ? p.ti->info_hashes() : p.info_hashes); -#else - const auto id = TorrentID::fromInfoHash(hasMetadata ? p.ti->info_hash() : p.info_hash); + p.userdata = LTClientData(new ExtensionData); +#ifndef QBT_USES_LIBTORRENT2 + p.storage = customStorageConstructor; #endif - m_loadingTorrents.insert(id, params); - // Adding torrent to BitTorrent session - m_nativeSession->async_add_torrent(p); + m_loadingTorrents.insert(id, loadTorrentParams); + if (!isFindingIncompleteFiles) + m_nativeSession->async_add_torrent(p); return true; } @@ -3207,6 +3489,11 @@ QStringList Session::bannedIPs() const return m_bannedIPs; } +bool Session::isRestored() const +{ + return m_isRestored; +} + #if defined(Q_OS_WIN) OSMemoryPriority Session::getOSMemoryPriority() const { @@ -4537,206 +4824,6 @@ const CacheStatus &Session::cacheStatus() const 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 torrents = startupStorage->registeredTorrents(); - int resumedTorrentsCount = 0; - QVector queue; - QSet recoveredCategories; -#ifdef QBT_USES_LIBTORRENT2 - const QSet indexedTorrents {torrents.cbegin(), torrents.cend()}; - QSet skippedIDs; -#endif - for (TorrentID torrentID : torrents) - { -#ifdef QBT_USES_LIBTORRENT2 - if (skippedIDs.contains(torrentID)) - continue; -#endif - - const std::optional 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 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 loadPreferredResumeDataResult = startupStorage->load(torrentID); - if (loadPreferredResumeDataResult) - { - std::shared_ptr 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 { @@ -4807,12 +4894,63 @@ void Session::setTorrentContentLayout(const TorrentContentLayout value) void Session::readAlerts() { const std::vector alerts = getPendingAlerts(); + handleAddTorrentAlerts(alerts); for (const lt::alert *a : alerts) handleAlert(a); processTrackerStatuses(); } +void Session::handleAddTorrentAlerts(const std::vector &alerts) +{ + QVector 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(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 ¶ms = 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) { try @@ -4852,7 +4990,7 @@ void Session::handleAlert(const lt::alert *a) handleFileErrorAlert(static_cast(a)); break; case lt::add_torrent_alert::alert_type: - handleAddTorrentAlert(static_cast(a)); + // handled separately break; case lt::torrent_removed_alert::alert_type: handleTorrentRemovedAlert(static_cast(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 ¶ms) { #ifdef QBT_USES_LIBTORRENT2 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()); #endif - Q_ASSERT(m_loadingTorrents.contains(torrentID)); - - const LoadTorrentParams params = m_loadingTorrents.take(torrentID); - auto *const torrent = new TorrentImpl(this, m_nativeSession, nativeHandle, params); m_torrents.insert(torrent->id(), torrent); - const bool hasMetadata = torrent->hasMetadata(); - if (!params.restored) { m_resumeDataStorage->store(torrent->id(), params); // The following is useless for newly added magnet - if (hasMetadata) + if (torrent->hasMetadata()) { if (!torrentExportDirectory().isEmpty()) exportTorrentFile(torrent, torrentExportDirectory()); @@ -4959,9 +5091,6 @@ void Session::createTorrent(const lt::torrent_handle &nativeHandle) m_seedingLimitTimer->start(); } - // Send torrent addition signal - emit torrentLoaded(torrent); - // Send new torrent signal if (params.restored) { 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 if (torrent->hasError()) LogMsg(tr("Torrent errored. Torrent: \"%1\". Error: \"%2\"").arg(torrent->name(), torrent->error()), Log::WARNING); -} -void Session::handleAddTorrentAlert(const lt::add_torrent_alert *p) -{ - 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 ¶ms = 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); - } + return torrent; } void Session::handleTorrentRemovedAlert(const lt::torrent_removed_alert *p) diff --git a/src/base/bittorrent/session.h b/src/base/bittorrent/session.h index 69b74392a..6b588504d 100644 --- a/src/base/bittorrent/session.h +++ b/src/base/bittorrent/session.h @@ -469,7 +469,8 @@ namespace BitTorrent void setOSMemoryPriority(OSMemoryPriority priority); #endif - void startUpTorrents(); + bool isRestored() const; + Torrent *findTorrent(const TorrentID &id) const; QVector torrents() const; qsizetype torrentsCount() const; @@ -539,6 +540,7 @@ namespace BitTorrent void loadTorrentFailed(const QString &error); void metadataDownloaded(const TorrentInfo &info); void recursiveTorrentDownloadPossible(Torrent *torrent); + void restored(); void speedLimitModeChanged(bool alternative); void statsUpdated(); void subcategoriesSupportChanged(); @@ -549,12 +551,12 @@ namespace BitTorrent void torrentCategoryChanged(Torrent *torrent, const QString &oldCategory); void torrentFinished(Torrent *torrent); void torrentFinishedChecking(Torrent *torrent); - void torrentLoaded(Torrent *torrent); void torrentMetadataReceived(Torrent *torrent); void torrentPaused(Torrent *torrent); void torrentResumed(Torrent *torrent); void torrentSavePathChanged(Torrent *torrent); void torrentSavingModeChanged(Torrent *torrent); + void torrentsLoaded(const QVector &torrents); void torrentsUpdated(const QVector &torrents); void torrentTagAdded(Torrent *torrent, const QString &tag); void torrentTagRemoved(Torrent *torrent, const QString &tag); @@ -585,6 +587,8 @@ namespace BitTorrent #endif private: + struct ResumeSessionContext; + struct MoveStorageJob { lt::torrent_handle torrentHandle; @@ -630,8 +634,11 @@ namespace BitTorrent #endif void processTrackerStatuses(); 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); bool addTorrent_impl(const std::variant &source, const AddTorrentParams &addTorrentParams); @@ -639,8 +646,8 @@ namespace BitTorrent void exportTorrentFile(const Torrent *torrent, const Path &folderPath); void handleAlert(const lt::alert *a); + void handleAddTorrentAlerts(const std::vector &alerts); void dispatchTorrentAlert(const lt::alert *a); - void handleAddTorrentAlert(const lt::add_torrent_alert *p); void handleStateUpdateAlert(const lt::state_update_alert *p); void handleMetadataReceivedAlert(const lt::metadata_received_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 handleTrackerAlert(const lt::tracker_alert *a); - void createTorrent(const lt::torrent_handle &nativeHandle); + TorrentImpl *createTorrent(const lt::torrent_handle &nativeHandle, const LoadTorrentParams ¶ms); void saveResumeData(); void saveTorrentsQueue() const; @@ -792,6 +799,8 @@ namespace BitTorrent CachedSettingValue m_OSMemoryPriority; #endif + bool m_isRestored = false; + // Order is important. This needs to be declared after its CachedSettingsValue // counterpart, because it uses it for initialization in the constructor // initialization list. diff --git a/src/base/rss/rss_autodownloader.cpp b/src/base/rss/rss_autodownloader.cpp index 26498174f..7c9a8316a 100644 --- a/src/base/rss/rss_autodownloader.cpp +++ b/src/base/rss/rss_autodownloader.cpp @@ -138,8 +138,20 @@ AutoDownloader::AutoDownloader() m_processingTimer->setSingleShot(true); connect(m_processingTimer, &QTimer::timeout, this, &AutoDownloader::process); - if (isProcessingEnabled()) - startProcessing(); + const auto *btSession = BitTorrent::Session::instance(); + if (btSession->isRestored()) + { + if (isProcessingEnabled()) + startProcessing(); + } + else + { + connect(btSession, &BitTorrent::Session::restored, this, [this]() + { + if (isProcessingEnabled()) + startProcessing(); + }); + } } AutoDownloader::~AutoDownloader() @@ -506,7 +518,8 @@ void AutoDownloader::setProcessingEnabled(const bool enabled) m_storeProcessingEnabled = enabled; if (enabled) { - startProcessing(); + if (BitTorrent::Session::instance()->isRestored()) + startProcessing(); } else { diff --git a/src/base/torrentfileswatcher.cpp b/src/base/torrentfileswatcher.cpp index 8b28ed449..dbc6390f4 100644 --- a/src/base/torrentfileswatcher.cpp +++ b/src/base/torrentfileswatcher.cpp @@ -255,13 +255,12 @@ TorrentFilesWatcher *TorrentFilesWatcher::instance() TorrentFilesWatcher::TorrentFilesWatcher(QObject *parent) : QObject {parent} , m_ioThread {new QThread(this)} - , 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(); + const auto *btSession = BitTorrent::Session::instance(); + if (btSession->isRestored()) + initWorker(); + else + connect(btSession, &BitTorrent::Session::restored, this, &TorrentFilesWatcher::initWorker); load(); } @@ -273,6 +272,27 @@ TorrentFilesWatcher::~TorrentFilesWatcher() 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() { 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; - 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); } @@ -411,10 +434,13 @@ void TorrentFilesWatcher::removeWatchedFolder(const Path &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); diff --git a/src/base/torrentfileswatcher.h b/src/base/torrentfileswatcher.h index 7ba6eebdd..6b8ff828d 100644 --- a/src/base/torrentfileswatcher.h +++ b/src/base/torrentfileswatcher.h @@ -78,6 +78,7 @@ private: explicit TorrentFilesWatcher(QObject *parent = nullptr); ~TorrentFilesWatcher() override; + void initWorker(); void load(); void loadLegacy(); void store() const; diff --git a/src/gui/categoryfiltermodel.cpp b/src/gui/categoryfiltermodel.cpp index b8e70bab7..a188356ed 100644 --- a/src/gui/categoryfiltermodel.cpp +++ b/src/gui/categoryfiltermodel.cpp @@ -32,7 +32,6 @@ #include #include "base/bittorrent/session.h" -#include "base/bittorrent/torrent.h" #include "base/global.h" #include "uithememanager.h" @@ -181,7 +180,7 @@ CategoryFilterModel::CategoryFilterModel(QObject *parent) connect(session, &Session::categoryRemoved, this, &CategoryFilterModel::categoryRemoved); connect(session, &Session::torrentCategoryChanged, this, &CategoryFilterModel::torrentCategoryChanged); 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); populate(); @@ -333,13 +332,16 @@ void CategoryFilterModel::categoryRemoved(const QString &categoryName) } } -void CategoryFilterModel::torrentAdded(BitTorrent::Torrent *const torrent) +void CategoryFilterModel::torrentsLoaded(const QVector &torrents) { - CategoryModelItem *item = findItem(torrent->category()); - Q_ASSERT(item); + for (const BitTorrent::Torrent *torrent : torrents) + { + CategoryModelItem *item = findItem(torrent->category()); + Q_ASSERT(item); - item->increaseTorrentsCount(); - m_rootItem->childAt(0)->increaseTorrentsCount(); + item->increaseTorrentsCount(); + m_rootItem->childAt(0)->increaseTorrentsCount(); + } } void CategoryFilterModel::torrentAboutToBeRemoved(BitTorrent::Torrent *const torrent) diff --git a/src/gui/categoryfiltermodel.h b/src/gui/categoryfiltermodel.h index bade1f6ce..744f6b319 100644 --- a/src/gui/categoryfiltermodel.h +++ b/src/gui/categoryfiltermodel.h @@ -28,17 +28,15 @@ #pragma once +#include #include +#include "base/bittorrent/torrent.h" + class QModelIndex; class CategoryModelItem; -namespace BitTorrent -{ - class Torrent; -} - class CategoryFilterModel final : public QAbstractItemModel { Q_OBJECT @@ -64,7 +62,7 @@ public: private slots: void categoryAdded(const QString &categoryName); void categoryRemoved(const QString &categoryName); - void torrentAdded(BitTorrent::Torrent *const torrent); + void torrentsLoaded(const QVector &torrents); void torrentAboutToBeRemoved(BitTorrent::Torrent *const torrent); void torrentCategoryChanged(BitTorrent::Torrent *const torrent, const QString &oldCategory); void subcategoriesSupportChanged(); diff --git a/src/gui/mainwindow.cpp b/src/gui/mainwindow.cpp index e5f16f9f6..1bccb34e4 100644 --- a/src/gui/mainwindow.cpp +++ b/src/gui/mainwindow.cpp @@ -1205,12 +1205,12 @@ void MainWindow::keyPressEvent(QKeyEvent *event) { if (event->matches(QKeySequence::Paste)) { - const QMimeData *mimeData {QGuiApplication::clipboard()->mimeData()}; + const QMimeData *mimeData = QGuiApplication::clipboard()->mimeData(); if (mimeData->hasText()) { - const bool useTorrentAdditionDialog {AddNewTorrentDialog::isEnabled()}; - const QStringList lines {mimeData->text().split(u'\n', Qt::SkipEmptyParts)}; + const bool useTorrentAdditionDialog = AddNewTorrentDialog::isEnabled(); + const QStringList lines = mimeData->text().split(u'\n', Qt::SkipEmptyParts); for (QString line : lines) { @@ -1438,6 +1438,7 @@ void MainWindow::dragEnterEvent(QDragEnterEvent *event) { for (const QString &mime : asConst(event->mimeData()->formats())) qDebug("mimeData: %s", mime.toLocal8Bit().data()); + if (event->mimeData()->hasFormat(u"text/plain"_qs) || event->mimeData()->hasFormat(u"text/uri-list"_qs)) event->acceptProposedAction(); } diff --git a/src/gui/search/searchjobwidget.cpp b/src/gui/search/searchjobwidget.cpp index 45b998b1a..79c9b27ab 100644 --- a/src/gui/search/searchjobwidget.cpp +++ b/src/gui/search/searchjobwidget.cpp @@ -208,7 +208,7 @@ void SearchJobWidget::cancelSearch() 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) downloadTorrent(rowIndex, option); } @@ -390,10 +390,10 @@ void SearchJobWidget::contextMenuEvent(QContextMenuEvent *event) auto *menu = new QMenu(this); menu->setAttribute(Qt::WA_DeleteOnClose); - menu->addAction(UIThemeManager::instance()->getIcon(u"kt-set-max-download-speed"_qs), tr("Open download window") - , this, [this]() { downloadTorrents(AddTorrentOption::ShowDialog); }); - menu->addAction(UIThemeManager::instance()->getIcon(u"downloading"_qs), tr("Download") - , this, [this]() { downloadTorrents(AddTorrentOption::SkipDialog); }); + menu->addAction(UIThemeManager::instance()->getIcon(u"kt-set-max-download-speed"_qs) + , tr("Open download window"), this, [this]() { downloadTorrents(AddTorrentOption::ShowDialog); }); + menu->addAction(UIThemeManager::instance()->getIcon(u"downloading"_qs) + , tr("Download"), this, [this]() { downloadTorrents(AddTorrentOption::SkipDialog); }); menu->addSeparator(); menu->addAction(UIThemeManager::instance()->getIcon(u"application-x-mswinurl"_qs), tr("Open description page") , this, &SearchJobWidget::openTorrentPages); diff --git a/src/gui/tagfiltermodel.cpp b/src/gui/tagfiltermodel.cpp index 560d9c46d..702f9c9e7 100644 --- a/src/gui/tagfiltermodel.cpp +++ b/src/gui/tagfiltermodel.cpp @@ -33,7 +33,6 @@ #include #include "base/bittorrent/session.h" -#include "base/bittorrent/torrent.h" #include "base/global.h" #include "uithememanager.h" @@ -99,7 +98,7 @@ TagFilterModel::TagFilterModel(QObject *parent) connect(session, &Session::tagRemoved, this, &TagFilterModel::tagRemoved); connect(session, &Session::torrentTagAdded, this, &TagFilterModel::torrentTagAdded); 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); populate(); } @@ -230,16 +229,19 @@ void TagFilterModel::torrentTagRemoved(BitTorrent::Torrent *const torrent, const emit dataChanged(i, i); } -void TagFilterModel::torrentAdded(BitTorrent::Torrent *const torrent) +void TagFilterModel::torrentsLoaded(const QVector &torrents) { - allTagsItem()->increaseTorrentsCount(); + for (const BitTorrent::Torrent *torrent : torrents) + { + allTagsItem()->increaseTorrentsCount(); - const QVector items = findItems(torrent->tags()); - if (items.isEmpty()) - untaggedItem()->increaseTorrentsCount(); + const QVector items = findItems(torrent->tags()); + if (items.isEmpty()) + untaggedItem()->increaseTorrentsCount(); - for (TagModelItem *item : items) - item->increaseTorrentsCount(); + for (TagModelItem *item : items) + item->increaseTorrentsCount(); + } } void TagFilterModel::torrentAboutToBeRemoved(BitTorrent::Torrent *const torrent) diff --git a/src/gui/tagfiltermodel.h b/src/gui/tagfiltermodel.h index 4e264e7a7..d51addb1b 100644 --- a/src/gui/tagfiltermodel.h +++ b/src/gui/tagfiltermodel.h @@ -28,20 +28,16 @@ #pragma once -#include #include +#include +#include "base/bittorrent/torrent.h" #include "base/tagset.h" class QModelIndex; class TagModelItem; -namespace BitTorrent -{ - class Torrent; -} - class TagFilterModel final : public QAbstractListModel { Q_OBJECT @@ -67,7 +63,7 @@ private slots: void tagRemoved(const QString &tag); void torrentTagAdded(BitTorrent::Torrent *const torrent, const QString &tag); void torrentTagRemoved(BitTorrent::Torrent *const, const QString &tag); - void torrentAdded(BitTorrent::Torrent *const torrent); + void torrentsLoaded(const QVector &torrents); void torrentAboutToBeRemoved(BitTorrent::Torrent *const torrent); private: diff --git a/src/gui/transferlistfilterswidget.cpp b/src/gui/transferlistfilterswidget.cpp index 61ead143d..5856bd0d6 100644 --- a/src/gui/transferlistfilterswidget.cpp +++ b/src/gui/transferlistfilterswidget.cpp @@ -134,8 +134,8 @@ BaseFilterWidget::BaseFilterWidget(QWidget *parent, TransferListWidget *transfer connect(this, &BaseFilterWidget::customContextMenuRequested, this, &BaseFilterWidget::showMenu); connect(this, &BaseFilterWidget::currentRowChanged, this, &BaseFilterWidget::applyFilter); - connect(BitTorrent::Session::instance(), &BitTorrent::Session::torrentLoaded - , this, &BaseFilterWidget::handleNewTorrent); + connect(BitTorrent::Session::instance(), &BitTorrent::Session::torrentsLoaded + , this, &BaseFilterWidget::handleTorrentsLoaded); connect(BitTorrent::Session::instance(), &BitTorrent::Session::torrentAboutToBeRemoved , this, &BaseFilterWidget::torrentAboutToBeDeleted); } @@ -318,9 +318,11 @@ void StatusFilterWidget::applyFilter(int row) transferList->applyStatusFilter(row); } -void StatusFilterWidget::handleNewTorrent(BitTorrent::Torrent *const torrent) +void StatusFilterWidget::handleTorrentsLoaded(const QVector &torrents) { - updateTorrentStatus(torrent); + for (const BitTorrent::Torrent *torrent : torrents) + updateTorrentStatus(torrent); + updateTexts(); } @@ -376,6 +378,8 @@ TrackerFiltersList::TrackerFiltersList(QWidget *parent, TransferListWidget *tran m_trackers[NULL_HOST] = {{}, noTracker}; + handleTorrentsLoaded(BitTorrent::Session::instance()->torrents()); + setCurrentRow(0, QItemSelectionModel::SelectCurrent); toggleFilter(Preferences::instance()->getTrackerFilterState()); } @@ -390,7 +394,7 @@ void TrackerFiltersList::addTrackers(const BitTorrent::Torrent *torrent, const Q { const BitTorrent::TorrentID torrentID = torrent->id(); for (const BitTorrent::TrackerEntry &tracker : trackers) - addItem(tracker.url, torrentID); + addItems(tracker.url, {torrentID}); } 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(); if (isTrackerless) { - addItem(NULL_HOST, torrentID); + addItems(NULL_HOST, {torrentID}); } else { for (const BitTorrent::TrackerEntry &trackerEntry : trackerEntries) - addItem(trackerEntry.url, torrentID); + addItems(trackerEntry.url, {torrentID}); } updateGeometry(); @@ -445,23 +449,20 @@ void TrackerFiltersList::refreshTrackers(const BitTorrent::Torrent *torrent) void TrackerFiltersList::changeTrackerless(const BitTorrent::Torrent *torrent, const bool trackerless) { if (trackerless) - addItem(NULL_HOST, torrent->id()); + addItems(NULL_HOST, {torrent->id()}); else removeItem(NULL_HOST, torrent->id()); } -void TrackerFiltersList::addItem(const QString &tracker, const BitTorrent::TorrentID &id) +void TrackerFiltersList::addItems(const QString &trackerURL, const QVector &torrents) { - const QString host = getHost(tracker); + const QString host = getHost(trackerURL); auto trackersIt = m_trackers.find(host); const bool exists = (trackersIt != m_trackers.end()); QListWidgetItem *trackerItem = nullptr; if (exists) { - if (trackersIt->torrents.contains(id)) - return; - trackerItem = trackersIt->item; } else @@ -469,17 +470,18 @@ void TrackerFiltersList::addItem(const QString &tracker, const BitTorrent::Torre trackerItem = new QListWidgetItem(); trackerItem->setData(Qt::DecorationRole, UIThemeManager::instance()->getIcon(u"trackers"_qs)); - TrackerData trackerData {{}, trackerItem}; + const TrackerData trackerData {{}, trackerItem}; 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)); } Q_ASSERT(trackerItem); QSet &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()))); if (exists) @@ -724,18 +726,30 @@ void TrackerFiltersList::applyFilter(const int row) transferList->applyTrackerFilter(getTorrentIDs(row)); } -void TrackerFiltersList::handleNewTorrent(BitTorrent::Torrent *const torrent) +void TrackerFiltersList::handleTorrentsLoaded(const QVector &torrents) { - const BitTorrent::TorrentID torrentID = torrent->id(); - const QVector trackers = torrent->trackers(); - for (const BitTorrent::TrackerEntry &tracker : trackers) - addItem(tracker.url, torrentID); + QHash> torrentsPerTracker; + for (const BitTorrent::Torrent *torrent : torrents) + { + const BitTorrent::TorrentID torrentID = torrent->id(); + const QVector trackers = torrent->trackers(); + for (const BitTorrent::TrackerEntry &tracker : trackers) + torrentsPerTracker[tracker.url].append(torrentID); - // Check for trackerless torrent - if (trackers.isEmpty()) - addItem(NULL_HOST, torrentID); + // Check for trackerless torrent + if (trackers.isEmpty()) + torrentsPerTracker[NULL_HOST].append(torrentID); + } + + for (auto it = torrentsPerTracker.cbegin(); it != torrentsPerTracker.cend(); ++it) + { + const QString &trackerURL = it.key(); + const QVector &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) diff --git a/src/gui/transferlistfilterswidget.h b/src/gui/transferlistfilterswidget.h index 32c5ba6be..986feeaf1 100644 --- a/src/gui/transferlistfilterswidget.h +++ b/src/gui/transferlistfilterswidget.h @@ -71,7 +71,7 @@ protected: private slots: virtual void showMenu() = 0; virtual void applyFilter(int row) = 0; - virtual void handleNewTorrent(BitTorrent::Torrent *const) = 0; + virtual void handleTorrentsLoaded(const QVector &torrents) = 0; virtual void torrentAboutToBeDeleted(BitTorrent::Torrent *const) = 0; }; @@ -92,7 +92,7 @@ private: // No need to redeclare them here as slots. void showMenu() override; void applyFilter(int row) override; - void handleNewTorrent(BitTorrent::Torrent *const) override; + void handleTorrentsLoaded(const QVector &torrents) override; void torrentAboutToBeDeleted(BitTorrent::Torrent *const) override; void populate(); @@ -139,10 +139,10 @@ private: // No need to redeclare them here as slots. void showMenu() override; void applyFilter(int row) override; - void handleNewTorrent(BitTorrent::Torrent *const torrent) override; + void handleTorrentsLoaded(const QVector &torrents) override; void torrentAboutToBeDeleted(BitTorrent::Torrent *const torrent) override; - void addItem(const QString &tracker, const BitTorrent::TorrentID &id); + void addItems(const QString &trackerURL, const QVector &torrents); void removeItem(const QString &trackerURL, const BitTorrent::TorrentID &id); QString trackerFromRow(int row) const; int rowFromTracker(const QString &tracker) const; diff --git a/src/gui/transferlistmodel.cpp b/src/gui/transferlistmodel.cpp index 307d96de1..4a963a32f 100644 --- a/src/gui/transferlistmodel.cpp +++ b/src/gui/transferlistmodel.cpp @@ -166,11 +166,10 @@ TransferListModel::TransferListModel(QObject *parent) // Load the torrents using namespace BitTorrent; - for (Torrent *const torrent : asConst(Session::instance()->torrents())) - addTorrent(torrent); + addTorrents(Session::instance()->torrents()); // 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::torrentsUpdated, this, &TransferListModel::handleTorrentsUpdated); @@ -599,15 +598,19 @@ bool TransferListModel::setData(const QModelIndex &index, const QVariant &value, return true; } -void TransferListModel::addTorrent(BitTorrent::Torrent *const torrent) +void TransferListModel::addTorrents(const QVector &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(); } diff --git a/src/gui/transferlistmodel.h b/src/gui/transferlistmodel.h index d5f1550d1..dbe8e32e8 100644 --- a/src/gui/transferlistmodel.h +++ b/src/gui/transferlistmodel.h @@ -103,7 +103,7 @@ public: BitTorrent::Torrent *torrentHandle(const QModelIndex &index) const; private slots: - void addTorrent(BitTorrent::Torrent *const torrent); + void addTorrents(const QVector &torrents); void handleTorrentAboutToBeRemoved(BitTorrent::Torrent *const torrent); void handleTorrentStatusUpdated(BitTorrent::Torrent *const torrent); void handleTorrentsUpdated(const QVector &torrents);