31 #include <libtorrent/bdecode.hpp>
32 #include <libtorrent/entry.hpp>
33 #include <libtorrent/read_resume_data.hpp>
34 #include <libtorrent/torrent_info.hpp>
35 #include <libtorrent/write_resume_data.hpp>
39 #include <QRegularExpression>
58 Q_DISABLE_COPY_MOVE(
Worker)
61 explicit Worker(
const QDir &resumeDataDir);
65 void storeQueue(
const QVector<TorrentID> &queue)
const;
74 template <
typename LTStr>
77 return QString::fromUtf8(str.data(),
static_cast<int>(str.size()));
85 entryList.reserve(input.size());
86 for (
const QString &
setValue : input)
87 entryList.emplace_back(
setValue.toStdString());
94 , m_resumeDataDir {path}
95 , m_ioThread {new QThread {this}}
96 , m_asyncWorker {new
Worker {m_resumeDataDir}}
100 throw RuntimeError {tr(
"Cannot create torrent resume folder: \"%1\"")
104 const QRegularExpression filenamePattern {QLatin1String(
"^([A-Fa-f0-9]{40})\\.fastresume$")};
105 const QStringList filenames =
m_resumeDataDir.entryList(QStringList(QLatin1String(
"*.fastresume")), QDir::Files, QDir::Unsorted);
108 for (
const QString &filename : filenames)
110 const QRegularExpressionMatch rxMatch = filenamePattern.match(filename);
111 if (rxMatch.hasMatch())
132 return m_registeredTorrents;
137 const QString idString =
id.toString();
138 const QString fastresumePath = m_resumeDataDir.absoluteFilePath(QString::fromLatin1(
"%1.fastresume").arg(idString));
139 const QString torrentFilePath = m_resumeDataDir.absoluteFilePath(QString::fromLatin1(
"%1.torrent").arg(idString));
141 QFile resumeDataFile {fastresumePath};
142 if (!resumeDataFile.open(QIODevice::ReadOnly))
144 LogMsg(tr(
"Cannot read file %1: %2").arg(fastresumePath, resumeDataFile.errorString()),
Log::WARNING);
148 QFile metadataFile {torrentFilePath};
149 if (metadataFile.exists() && !metadataFile.open(QIODevice::ReadOnly))
151 LogMsg(tr(
"Cannot read file %1: %2").arg(torrentFilePath, metadataFile.errorString()),
Log::WARNING);
155 const QByteArray data = resumeDataFile.readAll();
156 const QByteArray metadata = (metadataFile.isOpen() ? metadataFile.readAll() :
"");
158 return loadTorrentResumeData(data, metadata);
162 const QByteArray &data,
const QByteArray &metadata)
const
164 const QByteArray allData = ((metadata.isEmpty() || data.isEmpty())
165 ? data : (data.chopped(1) + metadata.mid(1)));
168 const lt::bdecode_node root = lt::bdecode(allData, ec);
169 if (ec || (root.type() != lt::bdecode_node::dict_t))
176 torrentParams.
hasSeedStatus = root.dict_find_int_value(
"qBt-seedStatus");
191 const lt::bdecode_node contentLayoutNode = root.dict_find(
"qBt-contentLayout");
192 if (contentLayoutNode.type() == lt::bdecode_node::string_t)
194 const QString contentLayoutStr =
fromLTString(contentLayoutNode.string_value());
199 const bool hasRootFolder = root.dict_find_int_value(
"qBt-hasRootFolder");
200 torrentParams.
contentLayout = (hasRootFolder ? TorrentContentLayout::Original : TorrentContentLayout::NoSubfolder);
208 const lt::string_view ratioLimitString = root.dict_find_string_value(
"qBt-ratioLimit");
209 if (ratioLimitString.empty())
214 const lt::bdecode_node tagsNode = root.dict_find(
"qBt-tags");
215 if (tagsNode.type() == lt::bdecode_node::list_t)
217 for (
int i = 0; i < tagsNode.list_size(); ++i)
219 const QString tag =
fromLTString(tagsNode.list_string_value_at(i));
220 torrentParams.
tags.insert(tag);
226 p = lt::read_resume_data(root, ec);
229 if (p.flags & lt::torrent_flags::stop_when_ready)
233 torrentParams.
operatingMode = TorrentOperatingMode::AutoManaged;
235 p.flags &= ~lt::torrent_flags::paused;
236 p.flags |= lt::torrent_flags::auto_managed;
240 torrentParams.
stopped = (p.flags & lt::torrent_flags::paused) && !(p.flags & lt::torrent_flags::auto_managed);
241 torrentParams.
operatingMode = (p.flags & lt::torrent_flags::paused) || (p.flags & lt::torrent_flags::auto_managed)
242 ? TorrentOperatingMode::AutoManaged : TorrentOperatingMode::Forced;
245 const bool hasMetadata = (p.ti && p.ti->is_valid());
246 if (!hasMetadata && !root.dict_find(
"info-hash"))
249 return torrentParams;
254 QMetaObject::invokeMethod(m_asyncWorker, [
this,
id, resumeData]()
256 m_asyncWorker->store(
id, resumeData);
262 QMetaObject::invokeMethod(m_asyncWorker, [
this,
id]()
264 m_asyncWorker->remove(
id);
270 QMetaObject::invokeMethod(m_asyncWorker, [
this, queue]()
272 m_asyncWorker->storeQueue(queue);
278 QFile queueFile {queueFilename};
279 if (!queueFile.exists())
282 if (queueFile.open(QFile::ReadOnly))
284 const QRegularExpression hashPattern {QLatin1String(
"^([A-Fa-f0-9]{40})$")};
287 while (!(line = queueFile.readLine().trimmed()).isEmpty())
289 const QRegularExpressionMatch rxMatch = hashPattern.match(line);
290 if (rxMatch.hasMatch())
293 const int pos = m_registeredTorrents.indexOf(torrentID, start);
296 std::swap(m_registeredTorrents[start], m_registeredTorrents[pos]);
304 LogMsg(tr(
"Couldn't load torrents queue from '%1'. Error: %2")
305 .arg(queueFile.fileName(), queueFile.errorString()),
Log::WARNING);
310 : m_resumeDataDir {resumeDataDir}
321 p.flags |= lt::torrent_flags::paused;
322 p.flags &= ~lt::torrent_flags::auto_managed;
328 if (resumeData.
operatingMode == BitTorrent::TorrentOperatingMode::AutoManaged)
330 p.flags |= lt::torrent_flags::auto_managed;
334 p.flags &= ~lt::torrent_flags::paused;
335 p.flags &= ~lt::torrent_flags::auto_managed;
339 lt::entry data = lt::write_resume_data(p);
344 lt::entry::dictionary_type &dataDict = data.dict();
345 lt::entry metadata {lt::entry::dictionary_t};
346 lt::entry::dictionary_type &metadataDict = metadata.dict();
347 metadataDict.insert(dataDict.extract(
"info"));
348 metadataDict.insert(dataDict.extract(
"creation date"));
349 metadataDict.insert(dataDict.extract(
"created by"));
350 metadataDict.insert(dataDict.extract(
"comment"));
352 const QString torrentFilepath =
m_resumeDataDir.absoluteFilePath(QString::fromLatin1(
"%1.torrent").arg(
id.
toString()));
356 LogMsg(tr(
"Couldn't save torrent metadata to '%1'. Error: %2.")
362 data[
"qBt-ratioLimit"] =
static_cast<int>(resumeData.
ratioLimit * 1000);
364 data[
"qBt-category"] = resumeData.
category.toStdString();
366 data[
"qBt-name"] = resumeData.
name.toStdString();
377 const QString resumeFilepath =
m_resumeDataDir.absoluteFilePath(QString::fromLatin1(
"%1.fastresume").arg(
id.
toString()));
381 LogMsg(tr(
"Couldn't save torrent resume data to '%1'. Error: %2.")
388 const QString resumeFilename = QString::fromLatin1(
"%1.fastresume").arg(
id.
toString());
391 const QString torrentFilename = QString::fromLatin1(
"%1.torrent").arg(
id.
toString());
400 data += (torrentID.toString().toLatin1() +
'\n');
402 const QString filepath =
m_resumeDataDir.absoluteFilePath(QLatin1String(
"queue"));
406 LogMsg(tr(
"Couldn't save data to '%1'. Error: %2")
void remove(const TorrentID &id) const
const QDir m_resumeDataDir
void storeQueue(const QVector< TorrentID > &queue) const
void store(const TorrentID &id, const LoadTorrentParams &resumeData) const
Worker(const QDir &resumeDataDir)
QVector< TorrentID > registeredTorrents() const override
void loadQueue(const QString &queueFilename)
~BencodeResumeDataStorage() override
std::optional< LoadTorrentParams > load(const TorrentID &id) const override
void storeQueue(const QVector< TorrentID > &queue) const override
std::optional< LoadTorrentParams > loadTorrentResumeData(const QByteArray &data, const QByteArray &metadata) const
void remove(const TorrentID &id) const override
QVector< TorrentID > m_registeredTorrents
const QDir m_resumeDataDir
BencodeResumeDataStorage(const QString &path, QObject *parent=nullptr)
void store(const TorrentID &id, const LoadTorrentParams &resumeData) const override
static const int USE_GLOBAL_SEEDING_TIME
static const qreal USE_GLOBAL_RATIO
static TorrentID fromString(const QString &hashString)
static constexpr int length()
QString fromPortablePath(const QString &portablePath) const
QString toPortablePath(const QString &absolutePath) const
static const Profile * instance()
void LogMsg(const QString &message, const Log::MsgType &type)
QString toUniformPath(const QString &path)
bool forceRemove(const QString &filePath)
QString toNativePath(const QString &path)
nonstd::expected< void, QString > saveToFile(const QString &path, const QByteArray &data)
QString fromEnum(const T &value)
T toEnum(const QString &serializedValue, const T &defaultValue)
QString fromLTString(const LTStr &str)
lt::entry::list_type ListType
ListType setToEntryList(const TagSet &input)
void setValue(const QString &key, const T &value)
QString toString(const lt::socket_type_t socketType)
TorrentContentLayout contentLayout
bool firstLastPiecePriority
lt::add_torrent_params ltAddTorrentParams
TorrentOperatingMode operatingMode