mirror of
https://github.com/d47081/qBittorrent.git
synced 2025-01-10 14:57:52 +00:00
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.
This commit is contained in:
parent
ec1d2cba40
commit
be7cfb78de
@ -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<bool> 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;
|
||||
}
|
||||
|
||||
#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
|
||||
#endif
|
||||
BitTorrent::Session::instance()->addTorrent(param, torrentParams);
|
||||
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 = !params.skipTorrentDialog.value_or(!AddNewTorrentDialog::isEnabled());
|
||||
if (showDialogForThisTorrent)
|
||||
AddNewTorrentDialog::show(params.torrentSource, params.torrentParams, m_window);
|
||||
else
|
||||
#endif
|
||||
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;
|
||||
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();
|
||||
}
|
||||
|
||||
@ -699,16 +703,19 @@ bool Application::event(QEvent *ev)
|
||||
// Get the url instead
|
||||
path = static_cast<QFileOpenEvent *>(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
|
||||
|
@ -41,6 +41,7 @@
|
||||
#include <QApplication>
|
||||
#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<bool> 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<AddTorrentParams> m_paramsQueue;
|
||||
|
||||
SettingValue<bool> m_storeFileLoggerEnabled;
|
||||
SettingValue<bool> m_storeFileLoggerBackup;
|
||||
|
@ -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
|
||||
|
@ -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 \
|
||||
|
@ -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::TorrentID> BitTorrent::BencodeResumeDataStorage::registeredT
|
||||
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 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::LoadTorrentParams> BitTorrent::BencodeResumeDataStorag
|
||||
return loadTorrentResumeData(data, metadata);
|
||||
}
|
||||
|
||||
std::optional<BitTorrent::LoadTorrentParams> 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<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())
|
||||
? 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::LoadTorrentParams> 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<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)
|
||||
: m_resumeDataDir {resumeDataDir}
|
||||
{
|
||||
|
@ -31,7 +31,7 @@
|
||||
#include <QDir>
|
||||
#include <QVector>
|
||||
|
||||
#include "base/path.h"
|
||||
#include "base/pathfwd.h"
|
||||
#include "resumedatastorage.h"
|
||||
|
||||
class QByteArray;
|
||||
@ -49,16 +49,16 @@ namespace BitTorrent
|
||||
~BencodeResumeDataStorage() 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 remove(const TorrentID &id) const override;
|
||||
void storeQueue(const QVector<TorrentID> &queue) const override;
|
||||
|
||||
private:
|
||||
void doLoadAll() const override;
|
||||
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;
|
||||
QThread *m_ioThread = nullptr;
|
||||
|
||||
|
@ -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<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)
|
||||
: 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::TorrentID> BitTorrent::DBResumeDataStorage::registeredTorren
|
||||
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
|
||||
.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)
|
||||
{
|
||||
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<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;
|
||||
return parseQueryResultRow(query);
|
||||
}
|
||||
|
||||
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
|
||||
{
|
||||
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<TorrentID
|
||||
|
||||
try
|
||||
{
|
||||
const QWriteLocker locker {&m_dbLock};
|
||||
|
||||
if (!db.transaction())
|
||||
throw RuntimeError(db.lastError().text());
|
||||
|
||||
|
@ -28,6 +28,8 @@
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <QReadWriteLock>
|
||||
|
||||
#include "base/pathfwd.h"
|
||||
#include "resumedatastorage.h"
|
||||
|
||||
@ -45,12 +47,14 @@ namespace BitTorrent
|
||||
~DBResumeDataStorage() 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 remove(const TorrentID &id) const override;
|
||||
void storeQueue(const QVector<TorrentID> &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;
|
||||
};
|
||||
}
|
||||
|
76
src/base/bittorrent/resumedatastorage.cpp
Normal file
76
src/base/bittorrent/resumedatastorage.cpp
Normal file
@ -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});
|
||||
}
|
@ -1,6 +1,6 @@
|
||||
/*
|
||||
* 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
|
||||
* modify it under the terms of the GNU General Public License
|
||||
@ -28,15 +28,25 @@
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <optional>
|
||||
|
||||
#include <QtContainerFwd>
|
||||
#include <QMutex>
|
||||
#include <QObject>
|
||||
#include <QVector>
|
||||
|
||||
#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<LoadTorrentParams, QString>;
|
||||
|
||||
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<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 remove(const TorrentID &id) 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;
|
||||
};
|
||||
}
|
||||
|
@ -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> 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>();
|
||||
|
||||
// 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<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()
|
||||
{
|
||||
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<MagnetUri, TorrentInfo> &source, const AddTorrentParams &addTorrentParams)
|
||||
{
|
||||
Q_ASSERT(isRestored());
|
||||
|
||||
const bool hasMetadata = std::holds_alternative<TorrentInfo>(source);
|
||||
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);
|
||||
|
||||
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;
|
||||
// Limits
|
||||
p.max_connections = maxConnectionsPerTorrent();
|
||||
p.max_uploads = maxUploadsPerTorrent();
|
||||
|
||||
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);
|
||||
#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<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
|
||||
{
|
||||
@ -4807,12 +4894,63 @@ void Session::setTorrentContentLayout(const TorrentContentLayout value)
|
||||
void Session::readAlerts()
|
||||
{
|
||||
const std::vector<lt::alert *> alerts = getPendingAlerts();
|
||||
handleAddTorrentAlerts(alerts);
|
||||
for (const lt::alert *a : alerts)
|
||||
handleAlert(a);
|
||||
|
||||
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 ¶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<const lt::file_error_alert*>(a));
|
||||
break;
|
||||
case lt::add_torrent_alert::alert_type:
|
||||
handleAddTorrentAlert(static_cast<const lt::add_torrent_alert*>(a));
|
||||
// handled separately
|
||||
break;
|
||||
case lt::torrent_removed_alert::alert_type:
|
||||
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 ¶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)
|
||||
|
@ -469,7 +469,8 @@ namespace BitTorrent
|
||||
void setOSMemoryPriority(OSMemoryPriority priority);
|
||||
#endif
|
||||
|
||||
void startUpTorrents();
|
||||
bool isRestored() const;
|
||||
|
||||
Torrent *findTorrent(const TorrentID &id) const;
|
||||
QVector<Torrent *> 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<Torrent *> &torrents);
|
||||
void torrentsUpdated(const QVector<Torrent *> &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<MagnetUri, TorrentInfo> &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<lt::alert *> &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<OSMemoryPriority> 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.
|
||||
|
@ -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
|
||||
{
|
||||
|
@ -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);
|
||||
|
||||
|
@ -78,6 +78,7 @@ private:
|
||||
explicit TorrentFilesWatcher(QObject *parent = nullptr);
|
||||
~TorrentFilesWatcher() override;
|
||||
|
||||
void initWorker();
|
||||
void load();
|
||||
void loadLegacy();
|
||||
void store() const;
|
||||
|
@ -32,7 +32,6 @@
|
||||
#include <QIcon>
|
||||
|
||||
#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<BitTorrent::Torrent *> &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)
|
||||
|
@ -28,17 +28,15 @@
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <QtContainerFwd>
|
||||
#include <QAbstractItemModel>
|
||||
|
||||
#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<BitTorrent::Torrent *> &torrents);
|
||||
void torrentAboutToBeRemoved(BitTorrent::Torrent *const torrent);
|
||||
void torrentCategoryChanged(BitTorrent::Torrent *const torrent, const QString &oldCategory);
|
||||
void subcategoriesSupportChanged();
|
||||
|
@ -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();
|
||||
}
|
||||
|
@ -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);
|
||||
|
@ -33,7 +33,6 @@
|
||||
#include <QVector>
|
||||
|
||||
#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<BitTorrent::Torrent *> &torrents)
|
||||
{
|
||||
allTagsItem()->increaseTorrentsCount();
|
||||
for (const BitTorrent::Torrent *torrent : torrents)
|
||||
{
|
||||
allTagsItem()->increaseTorrentsCount();
|
||||
|
||||
const QVector<TagModelItem *> items = findItems(torrent->tags());
|
||||
if (items.isEmpty())
|
||||
untaggedItem()->increaseTorrentsCount();
|
||||
const QVector<TagModelItem *> 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)
|
||||
|
@ -28,20 +28,16 @@
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <QAbstractListModel>
|
||||
#include <QtContainerFwd>
|
||||
#include <QAbstractItemModel>
|
||||
|
||||
#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<BitTorrent::Torrent *> &torrents);
|
||||
void torrentAboutToBeRemoved(BitTorrent::Torrent *const torrent);
|
||||
|
||||
private:
|
||||
|
@ -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<BitTorrent::Torrent *> &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<BitTorrent::TorrentID> &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<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())));
|
||||
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<BitTorrent::Torrent *> &torrents)
|
||||
{
|
||||
const BitTorrent::TorrentID torrentID = torrent->id();
|
||||
const QVector<BitTorrent::TrackerEntry> trackers = torrent->trackers();
|
||||
for (const BitTorrent::TrackerEntry &tracker : trackers)
|
||||
addItem(tracker.url, torrentID);
|
||||
QHash<QString, QVector<BitTorrent::TorrentID>> torrentsPerTracker;
|
||||
for (const BitTorrent::Torrent *torrent : torrents)
|
||||
{
|
||||
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
|
||||
if (trackers.isEmpty())
|
||||
addItem(NULL_HOST, torrentID);
|
||||
// Check for trackerless torrent
|
||||
if (trackers.isEmpty())
|
||||
torrentsPerTracker[NULL_HOST].append(torrentID);
|
||||
}
|
||||
|
||||
item(ALL_ROW)->setText(tr("All (%1)", "this is for the tracker filter").arg(++m_totalTorrents));
|
||||
for (auto it = torrentsPerTracker.cbegin(); it != torrentsPerTracker.cend(); ++it)
|
||||
{
|
||||
const QString &trackerURL = it.key();
|
||||
const QVector<BitTorrent::TorrentID> &torrents = it.value();
|
||||
addItems(trackerURL, torrents);
|
||||
}
|
||||
|
||||
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)
|
||||
|
@ -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<BitTorrent::Torrent *> &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<BitTorrent::Torrent *> &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<BitTorrent::Torrent *> &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<BitTorrent::TorrentID> &torrents);
|
||||
void removeItem(const QString &trackerURL, const BitTorrent::TorrentID &id);
|
||||
QString trackerFromRow(int row) const;
|
||||
int rowFromTracker(const QString &tracker) const;
|
||||
|
@ -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<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();
|
||||
}
|
||||
|
||||
|
@ -103,7 +103,7 @@ public:
|
||||
BitTorrent::Torrent *torrentHandle(const QModelIndex &index) const;
|
||||
|
||||
private slots:
|
||||
void addTorrent(BitTorrent::Torrent *const torrent);
|
||||
void addTorrents(const QVector<BitTorrent::Torrent *> &torrents);
|
||||
void handleTorrentAboutToBeRemoved(BitTorrent::Torrent *const torrent);
|
||||
void handleTorrentStatusUpdated(BitTorrent::Torrent *const torrent);
|
||||
void handleTorrentsUpdated(const QVector<BitTorrent::Torrent *> &torrents);
|
||||
|
Loading…
Reference in New Issue
Block a user