From 5750de6270db4b1ae512c27dfca6d4df6674ce4e Mon Sep 17 00:00:00 2001 From: Thomas Piccirello Date: Sat, 6 Jul 2024 22:38:33 -0700 Subject: [PATCH 1/5] Add WebAPI for fetching torrent metadata Signed-off-by: Thomas Piccirello --- src/webui/api/torrentscontroller.cpp | 272 +++++++++++++++++++++++++++ src/webui/api/torrentscontroller.h | 27 ++- 2 files changed, 298 insertions(+), 1 deletion(-) diff --git a/src/webui/api/torrentscontroller.cpp b/src/webui/api/torrentscontroller.cpp index 9fe658f95..063998b0b 100644 --- a/src/webui/api/torrentscontroller.cpp +++ b/src/webui/api/torrentscontroller.cpp @@ -54,12 +54,15 @@ #include "base/interfaces/iapplication.h" #include "base/global.h" #include "base/logger.h" +#include "base/net/downloadmanager.h" +#include "base/preferences.h" #include "base/torrentfilter.h" #include "base/utils/datetime.h" #include "base/utils/fs.h" #include "base/utils/sslkey.h" #include "base/utils/string.h" #include "apierror.h" +#include "apistatus.h" #include "serialize/serialize_torrent.h" // Tracker keys @@ -133,6 +136,16 @@ const QString KEY_FILE_IS_SEED = u"is_seed"_s; const QString KEY_FILE_PIECE_RANGE = u"piece_range"_s; const QString KEY_FILE_AVAILABILITY = u"availability"_s; +// Torrent info +const QString KEY_TORRENTINFO_FILE_LENGTH = u"length"_s; +const QString KEY_TORRENTINFO_FILE_PATH = u"path"_s; +const QString KEY_TORRENTINFO_FILES = u"files"_s; +const QString KEY_TORRENTINFO_INFO = u"info"_s; +const QString KEY_TORRENTINFO_LENGTH = u"length"_s; +const QString KEY_TORRENTINFO_PIECE_LENGTH = u"piece_length"_s; +const QString KEY_TORRENTINFO_TRACKERS = u"trackers"_s; +const QString KEY_TORRENTINFO_WEBSEEDS = u"webseeds"_s; + namespace { using Utils::String::parseBool; @@ -343,6 +356,112 @@ namespace return url; } + + QJsonObject serializeInfoHash(const BitTorrent::InfoHash &infoHash) + { + return { + {KEY_TORRENT_INFOHASHV1, infoHash.v1().toString()}, + {KEY_TORRENT_INFOHASHV2, infoHash.v2().toString()}, + {KEY_TORRENT_ID, infoHash.toTorrentID().toString()}, + }; + } + + QJsonObject serializeTorrentInfo(const BitTorrent::TorrentInfo &info) + { + qlonglong torrentSize = 0; + QJsonArray files; + for (int fileIndex = 0; fileIndex < info.filesCount(); ++fileIndex) + { + const qlonglong fileSize = info.fileSize(fileIndex); + torrentSize += fileSize; + files << QJsonObject + { + // use platform-independent separators + {KEY_TORRENTINFO_FILE_PATH, info.filePath(fileIndex).data()}, + {KEY_TORRENTINFO_FILE_LENGTH, fileSize} + }; + } + + const BitTorrent::InfoHash infoHash = info.infoHash(); + + return { + {KEY_TORRENT_INFOHASHV1, infoHash.v1().toString()}, + {KEY_TORRENT_INFOHASHV2, infoHash.v2().toString()}, + {KEY_TORRENT_ID, infoHash.toTorrentID().toString()}, + {KEY_TORRENTINFO_INFO, QJsonObject { + {KEY_TORRENTINFO_FILES, files}, + {KEY_TORRENTINFO_LENGTH, torrentSize}, + {KEY_TORRENT_NAME, info.name()}, + {KEY_TORRENTINFO_PIECE_LENGTH, info.pieceLength()}, + {KEY_PROP_PIECES_NUM, info.piecesCount()}, + {KEY_PROP_PRIVATE, info.isPrivate()}, + }}, + }; + } + + QJsonObject serializeTorrentInfo(const BitTorrent::TorrentDescriptor &torrentDescr) + { + QJsonObject info = serializeTorrentInfo(torrentDescr.info().value()); + + QJsonArray trackers; + for (const BitTorrent::TrackerEntry &tracker : asConst(torrentDescr.trackers())) + { + trackers << QJsonObject + { + {KEY_TRACKER_URL, tracker.url}, + {KEY_TRACKER_TIER, tracker.tier} + }; + } + info.insert(KEY_TORRENTINFO_TRACKERS, trackers); + + QJsonArray webseeds; + for (const QUrl &webseed : asConst(torrentDescr.urlSeeds())) + { + webseeds << webseed.toString(); + } + info.insert(KEY_TORRENTINFO_WEBSEEDS, webseeds); + + info.insert(KEY_PROP_CREATED_BY, torrentDescr.creator()); + info.insert(KEY_PROP_CREATION_DATE, Utils::DateTime::toSecsSinceEpoch(torrentDescr.creationDate())); + info.insert(KEY_PROP_COMMENT, torrentDescr.comment()); + + return info; + } + + QJsonObject serializeTorrentInfo(const BitTorrent::Torrent &torrent) + { + QJsonObject info = serializeTorrentInfo(torrent.info()); + + QJsonArray trackers; + for (const BitTorrent::TrackerEntryStatus &tracker : asConst(torrent.trackers())) + { + trackers << QJsonObject + { + {KEY_TRACKER_URL, tracker.url}, + {KEY_TRACKER_TIER, tracker.tier} + }; + } + info.insert(KEY_TORRENTINFO_TRACKERS, trackers); + + QJsonArray webseeds; + for (const QUrl &webseed : asConst(torrent.urlSeeds())) + { + webseeds << webseed.toString(); + } + info.insert(KEY_TORRENTINFO_WEBSEEDS, webseeds); + + info.insert(KEY_PROP_CREATED_BY, torrent.creator()); + info.insert(KEY_PROP_CREATION_DATE, Utils::DateTime::toSecsSinceEpoch(torrent.creationDate())); + info.insert(KEY_PROP_COMMENT, torrent.comment()); + + return info; + } +} + +TorrentsController::TorrentsController(IApplication *app, QObject *parent) + : APIController(app, parent) +{ + connect(BitTorrent::Session::instance(), &BitTorrent::Session::metadataDownloaded, this, &TorrentsController::onMetadataDownloaded); } void TorrentsController::countAction() @@ -1768,3 +1887,156 @@ void TorrentsController::setSSLParametersAction() setResult(QString()); } + +void TorrentsController::fetchMetadataAction() +{ + requireParams({u"source"_s}); + + const QString sourceParam = params()[u"source"_s].trimmed(); + // must provide some value to parse + if (sourceParam.isEmpty()) + throw APIError(APIErrorType::BadParams, tr("Must specify URI or hash")); + + const QString source = QUrl::fromPercentEncoding(sourceParam.toLatin1()); + const auto sourceTorrentDescr = BitTorrent::TorrentDescriptor::parse(source); + + const BitTorrent::InfoHash infoHash = sourceTorrentDescr ? sourceTorrentDescr.value().infoHash() : m_torrentSourceCache.value(source); + if (infoHash.isValid()) + { + // check metadata cache + if (const BitTorrent::TorrentDescriptor &torrentDescr = m_torrentMetadataCache.value(infoHash); + torrentDescr.info().has_value()) + { + setResult(serializeTorrentInfo(torrentDescr)); + } + // check transfer list + else if (const BitTorrent::Torrent *const torrent = BitTorrent::Session::instance()->findTorrent(infoHash); + torrent && torrent->info().isValid()) + { + setResult(serializeTorrentInfo(*torrent)); + } + // check request cache + else if (BitTorrent::Session::instance()->isKnownTorrent(infoHash)) + { + setResult(serializeInfoHash(infoHash)); + setStatus(APIStatus::Async); + } + // request torrent's metadata + else + { + if (!BitTorrent::Session::instance()->downloadMetadata(sourceTorrentDescr.value())) [[unlikely]] + throw APIError(APIErrorType::BadParams, tr("Unable to download metadata for '%1'").arg(infoHash.toTorrentID().toString())); + + m_torrentMetadataCache.insert(infoHash, sourceTorrentDescr.value()); + + setResult(serializeInfoHash(infoHash)); + setStatus(APIStatus::Async); + } + } + // http(s) url + else if (Net::DownloadManager::hasSupportedScheme(source)) + { + if (!m_requestedTorrentSource.contains(source)) + { + const auto *pref = Preferences::instance(); + + Net::DownloadManager::instance()->download(Net::DownloadRequest(source).limit(pref->getTorrentFileSizeLimit()) + , pref->useProxyForGeneralPurposes(), this, &TorrentsController::onDownloadFinished); + + m_requestedTorrentSource.insert(source); + } + + setResult(QJsonObject {}); + setStatus(APIStatus::Async); + } + else + { + throw APIError(APIErrorType::BadParams, tr("Unable to parse '%1'").arg(source)); + } +} + +void TorrentsController::parseMetadataAction() +{ + const DataMap &uploadedTorrents = data(); + // must provide some value to parse + if (uploadedTorrents.isEmpty()) + throw APIError(APIErrorType::BadParams, tr("Must specify torrent file(s)")); + + QJsonObject result; + for (auto it = uploadedTorrents.constBegin(); it != uploadedTorrents.constEnd(); ++it) + { + if (const auto loadResult = BitTorrent::TorrentDescriptor::load(it.value())) + { + const BitTorrent::TorrentDescriptor &torrentDescr = loadResult.value(); + m_torrentMetadataCache.insert(torrentDescr.infoHash(), torrentDescr); + + const QString &fileName = it.key(); + result.insert(fileName, serializeTorrentInfo(torrentDescr)); + } + else + { + throw APIError(APIErrorType::BadData, tr("'%1' is not a valid torrent file.").arg(it.key())); + } + } + + setResult(result); +} + +void TorrentsController::onDownloadFinished(const Net::DownloadResult &result) +{ + const QString source = result.url; + m_requestedTorrentSource.remove(source); + + switch (result.status) + { + case Net::DownloadStatus::Success: + // use the info directly from the .torrent file + if (const auto loadResult = BitTorrent::TorrentDescriptor::load(result.data)) + { + const BitTorrent::TorrentDescriptor &torrentDescr = loadResult.value(); + const BitTorrent::InfoHash infoHash = torrentDescr.infoHash(); + m_torrentSourceCache.insert(source, infoHash); + m_torrentMetadataCache.insert(infoHash, torrentDescr); + } + else + { + LogMsg(tr("Parse torrent failed. URL: \"%1\". Error: \"%2\".").arg(source, loadResult.error()), Log::WARNING); + m_torrentSourceCache.remove(source); + } + break; + + case Net::DownloadStatus::RedirectedToMagnet: + if (const auto parseResult = BitTorrent::TorrentDescriptor::parse(result.magnetURI)) + { + const BitTorrent::TorrentDescriptor &torrentDescr = parseResult.value(); + const BitTorrent::InfoHash infoHash = torrentDescr.infoHash(); + m_torrentSourceCache.insert(source, infoHash); + + if (!m_torrentMetadataCache.contains(infoHash) && !BitTorrent::Session::instance()->isKnownTorrent(infoHash)) + { + if (BitTorrent::Session::instance()->downloadMetadata(torrentDescr)) + m_torrentMetadataCache.insert(infoHash, torrentDescr); + } + } + else + { + LogMsg(tr("Parse magnet URI failed. URI: \"%1\". Error: \"%2\".").arg(result.magnetURI, parseResult.error()), Log::WARNING); + m_torrentSourceCache.remove(source); + } + break; + + default: + break; + } +} + +void TorrentsController::onMetadataDownloaded(const BitTorrent::TorrentInfo &info) +{ + Q_ASSERT(info.isValid()); + if (!info.isValid()) [[unlikely]] + return; + + const BitTorrent::InfoHash infoHash = info.infoHash(); + if (auto iter = m_torrentMetadataCache.find(infoHash); iter != m_torrentMetadataCache.end()) + iter.value().setTorrentInfo(info); +} diff --git a/src/webui/api/torrentscontroller.h b/src/webui/api/torrentscontroller.h index a233048ac..bbbbc57c6 100644 --- a/src/webui/api/torrentscontroller.h +++ b/src/webui/api/torrentscontroller.h @@ -28,15 +28,30 @@ #pragma once +#include +#include + +#include "base/bittorrent/torrentdescriptor.h" #include "apicontroller.h" +namespace BitTorrent +{ + class InfoHash; + class TorrentInfo; +} + +namespace Net +{ + struct DownloadResult; +} + class TorrentsController : public APIController { Q_OBJECT Q_DISABLE_COPY_MOVE(TorrentsController) public: - using APIController::APIController; + explicit TorrentsController(IApplication *app, QObject *parent = nullptr); private slots: void countAction(); @@ -95,4 +110,14 @@ private slots: void exportAction(); void SSLParametersAction(); void setSSLParametersAction(); + void fetchMetadataAction(); + void parseMetadataAction(); + +private: + void onDownloadFinished(const Net::DownloadResult &result); + void onMetadataDownloaded(const BitTorrent::TorrentInfo &info); + + QHash m_torrentSourceCache; + QHash m_torrentMetadataCache; + QSet m_requestedTorrentSource; }; From e45ca3fde7134bbf99bf10bbc8790385d19eb25b Mon Sep 17 00:00:00 2001 From: Thomas Piccirello Date: Sat, 6 Jul 2024 22:39:18 -0700 Subject: [PATCH 2/5] Support downloading torrent from previously fetched metadata Signed-off-by: Thomas Piccirello --- src/webui/api/torrentscontroller.cpp | 63 +++++++++++++++++++++++++--- 1 file changed, 58 insertions(+), 5 deletions(-) diff --git a/src/webui/api/torrentscontroller.cpp b/src/webui/api/torrentscontroller.cpp index 063998b0b..6cb903d33 100644 --- a/src/webui/api/torrentscontroller.cpp +++ b/src/webui/api/torrentscontroller.cpp @@ -958,7 +958,7 @@ void TorrentsController::pieceStatesAction() void TorrentsController::addAction() { - const QString urls = params()[u"urls"_s]; + const QStringList urls = params()[u"urls"_s].split(u'\n', Qt::SkipEmptyParts); const bool skipChecking = parseBool(params()[u"skip_checking"_s]).value_or(false); const bool seqDownload = parseBool(params()[u"sequentialDownload"_s]).value_or(false); @@ -990,7 +990,32 @@ void TorrentsController::addAction() ? Utils::String::toEnum(contentLayoutParam, BitTorrent::TorrentContentLayout::Original) : std::optional {}); - const BitTorrent::AddTorrentParams addTorrentParams + const DataMap &torrents = data(); + + QList filePriorities; + const QStringList filePrioritiesParam = params()[u"filePriorities"_s].split(u',', Qt::SkipEmptyParts); + if (!filePrioritiesParam.isEmpty()) + { + if (urls.size() > 1) + throw APIError(APIErrorType::BadParams, tr("You cannot specify filePriorities when adding multiple torrents")); + if (!torrents.isEmpty()) + throw APIError(APIErrorType::BadParams, tr("You cannot specify filePriorities when uploading torrent files")); + + filePriorities.reserve(filePrioritiesParam.size()); + for (const QString &priorityStr : filePrioritiesParam) + { + bool ok = false; + const auto priority = static_cast(priorityStr.toInt(&ok)); + if (!ok) + throw APIError(APIErrorType::BadParams, tr("Priority must be an integer")); + if (!BitTorrent::isValidDownloadPriority(priority)) + throw APIError(APIErrorType::BadParams, tr("Priority is not valid")); + + filePriorities << priority; + } + } + + BitTorrent::AddTorrentParams addTorrentParams { // TODO: Check if destination actually exists .name = torrentName, @@ -1024,17 +1049,45 @@ void TorrentsController::addAction() } }; + bool partialSuccess = false; - for (QString url : asConst(urls.split(u'\n'))) + for (QString url : urls) { url = url.trimmed(); - if (!url.isEmpty()) + if (url.isEmpty()) + continue; + + BitTorrent::InfoHash infoHash; + if (const auto iter = m_torrentSourceCache.constFind(url); iter != m_torrentSourceCache.constEnd()) + infoHash = iter.value(); + else if (const auto sourceTorrentDescr = BitTorrent::TorrentDescriptor::parse(url)) + infoHash = sourceTorrentDescr.value().infoHash(); + + if (const BitTorrent::TorrentDescriptor &torrentDescr = m_torrentMetadataCache.value(infoHash); torrentDescr.info().has_value()) { + if (!filePriorities.isEmpty()) + { + const BitTorrent::TorrentInfo &info = torrentDescr.info().value(); + if (filePriorities.size() != info.filesCount()) + throw APIError(APIErrorType::BadParams, tr("Length of filePriorities must equal number of files in torrent")); + + addTorrentParams.filePriorities = filePriorities; + } + + partialSuccess |= BitTorrent::Session::instance()->addTorrent(torrentDescr, addTorrentParams); + } + else + { + if (!filePriorities.isEmpty()) + throw APIError(APIErrorType::BadParams, tr("filePriorities may only be specified when metadata has already been fetched")); + partialSuccess |= app()->addTorrentManager()->addTorrent(url, addTorrentParams); } + m_torrentSourceCache.remove(url); + m_torrentMetadataCache.remove(infoHash); } - const DataMap &torrents = data(); + // process uploaded .torrent files for (auto it = torrents.constBegin(); it != torrents.constEnd(); ++it) { if (const auto loadResult = BitTorrent::TorrentDescriptor::load(it.value())) From 69bf31f4e9fd67dc1a652d3ab814392ae2cd9d5f Mon Sep 17 00:00:00 2001 From: Thomas Piccirello Date: Wed, 18 Sep 2024 10:18:13 -0700 Subject: [PATCH 3/5] Add WebAPI for downloading torrent metadata Signed-off-by: Thomas Piccirello --- src/base/bittorrent/torrentdescriptor.cpp | 16 +++++++++++ src/base/bittorrent/torrentdescriptor.h | 1 + src/webui/api/torrentscontroller.cpp | 34 +++++++++++++++++++++-- src/webui/api/torrentscontroller.h | 1 + 4 files changed, 50 insertions(+), 2 deletions(-) diff --git a/src/base/bittorrent/torrentdescriptor.cpp b/src/base/bittorrent/torrentdescriptor.cpp index 437c6557c..a8945d205 100644 --- a/src/base/bittorrent/torrentdescriptor.cpp +++ b/src/base/bittorrent/torrentdescriptor.cpp @@ -141,6 +141,22 @@ catch (const lt::system_error &err) return nonstd::make_unexpected(QString::fromLocal8Bit(err.what())); } +nonstd::expected BitTorrent::TorrentDescriptor::saveToBuffer() const +try +{ + const lt::entry torrentEntry = lt::write_torrent_file(m_ltAddTorrentParams); + // usually torrent size should be smaller than 1 MB, + // however there are >100 MB v2/hybrid torrent files out in the wild + QByteArray buffer; + buffer.reserve(1024 * 1024); + lt::bencode(std::back_inserter(buffer), torrentEntry); + return buffer; +} +catch (const lt::system_error &err) +{ + return nonstd::make_unexpected(QString::fromLocal8Bit(err.what())); +} + BitTorrent::TorrentDescriptor::TorrentDescriptor(lt::add_torrent_params ltAddTorrentParams) : m_ltAddTorrentParams {std::move(ltAddTorrentParams)} { diff --git a/src/base/bittorrent/torrentdescriptor.h b/src/base/bittorrent/torrentdescriptor.h index f41140fed..74c971c8c 100644 --- a/src/base/bittorrent/torrentdescriptor.h +++ b/src/base/bittorrent/torrentdescriptor.h @@ -69,6 +69,7 @@ namespace BitTorrent static nonstd::expected loadFromFile(const Path &path) noexcept; static nonstd::expected parse(const QString &str) noexcept; nonstd::expected saveToFile(const Path &path) const; + nonstd::expected saveToBuffer() const; const lt::add_torrent_params <AddTorrentParams() const; diff --git a/src/webui/api/torrentscontroller.cpp b/src/webui/api/torrentscontroller.cpp index 6cb903d33..6f74fee36 100644 --- a/src/webui/api/torrentscontroller.cpp +++ b/src/webui/api/torrentscontroller.cpp @@ -997,9 +997,9 @@ void TorrentsController::addAction() if (!filePrioritiesParam.isEmpty()) { if (urls.size() > 1) - throw APIError(APIErrorType::BadParams, tr("You cannot specify filePriorities when adding multiple torrents")); + throw APIError(APIErrorType::BadParams, tr("Cannot specify filePriorities when adding multiple torrents")); if (!torrents.isEmpty()) - throw APIError(APIErrorType::BadParams, tr("You cannot specify filePriorities when uploading torrent files")); + throw APIError(APIErrorType::BadParams, tr("Cannot specify filePriorities when uploading torrent files")); filePriorities.reserve(filePrioritiesParam.size()); for (const QString &priorityStr : filePrioritiesParam) @@ -2035,6 +2035,36 @@ void TorrentsController::parseMetadataAction() setResult(result); } +void TorrentsController::saveMetadataAction() +{ + requireParams({u"source"_s}); + + const QString sourceParam = params()[u"source"_s].trimmed(); + if (sourceParam.isEmpty()) + throw APIError(APIErrorType::BadParams, tr("Must specify URI or hash")); + + const QString source = QUrl::fromPercentEncoding(sourceParam.toLatin1()); + + BitTorrent::InfoHash infoHash; + if (const auto iter = m_torrentSourceCache.constFind(source); iter != m_torrentSourceCache.constEnd()) + infoHash = iter.value(); + else if (const auto sourceTorrentDescr = BitTorrent::TorrentDescriptor::parse(source)) + infoHash = sourceTorrentDescr.value().infoHash(); + + if (!infoHash.isValid()) + throw APIError(APIErrorType::NotFound); + + const BitTorrent::TorrentDescriptor &torrentDescr = m_torrentMetadataCache.value(infoHash); + if (!torrentDescr.info().has_value()) + throw APIError(APIErrorType::Conflict, tr("Metadata is not yet available")); + + const nonstd::expected result = torrentDescr.saveToBuffer(); + if (!result) + throw APIError(APIErrorType::Conflict, tr("Unable to export torrent metadata. Error: %1").arg(result.error())); + + setResult(result.value(), u"application/x-bittorrent"_s, (infoHash.toTorrentID().toString() + u".torrent")); +} + void TorrentsController::onDownloadFinished(const Net::DownloadResult &result) { const QString source = result.url; diff --git a/src/webui/api/torrentscontroller.h b/src/webui/api/torrentscontroller.h index bbbbc57c6..92f93055b 100644 --- a/src/webui/api/torrentscontroller.h +++ b/src/webui/api/torrentscontroller.h @@ -112,6 +112,7 @@ private slots: void setSSLParametersAction(); void fetchMetadataAction(); void parseMetadataAction(); + void saveMetadataAction(); private: void onDownloadFinished(const Net::DownloadResult &result); From 4c190b0d4f1b908ac5fd97e61c86cf439f054354 Mon Sep 17 00:00:00 2001 From: Thomas Piccirello Date: Thu, 5 Jun 2025 21:15:44 -0700 Subject: [PATCH 4/5] Move priority parsing into helper function --- src/webui/api/torrentscontroller.cpp | 34 ++++++++++++++++------------ 1 file changed, 20 insertions(+), 14 deletions(-) diff --git a/src/webui/api/torrentscontroller.cpp b/src/webui/api/torrentscontroller.cpp index 6f74fee36..f7c6118a0 100644 --- a/src/webui/api/torrentscontroller.cpp +++ b/src/webui/api/torrentscontroller.cpp @@ -456,6 +456,17 @@ namespace return info; } + + nonstd::expected parseDownloadPriority(const QString &priorityStr) + { + bool ok = false; + const auto priority = static_cast(priorityStr.toInt(&ok)); + if (!ok) + return nonstd::make_unexpected(TorrentsController::tr("Priority must be an integer")); + if (!isValidDownloadPriority(priority)) + return nonstd::make_unexpected(TorrentsController::tr("Priority is not valid")); + return priority; + } } TorrentsController::TorrentsController(IApplication *app, QObject *parent) @@ -1004,14 +1015,10 @@ void TorrentsController::addAction() filePriorities.reserve(filePrioritiesParam.size()); for (const QString &priorityStr : filePrioritiesParam) { - bool ok = false; - const auto priority = static_cast(priorityStr.toInt(&ok)); - if (!ok) - throw APIError(APIErrorType::BadParams, tr("Priority must be an integer")); - if (!BitTorrent::isValidDownloadPriority(priority)) - throw APIError(APIErrorType::BadParams, tr("Priority is not valid")); - - filePriorities << priority; + const nonstd::expected result = parseDownloadPriority(priorityStr); + if (!result) + throw APIError(APIErrorType::BadParams, result.error()); + filePriorities << result.value(); } } @@ -1275,13 +1282,11 @@ void TorrentsController::filePrioAction() requireParams({u"hash"_s, u"id"_s, u"priority"_s}); const auto id = BitTorrent::TorrentID::fromString(params()[u"hash"_s]); - bool ok = false; - const auto priority = static_cast(params()[u"priority"_s].toInt(&ok)); - if (!ok) - throw APIError(APIErrorType::BadParams, tr("Priority must be an integer")); + const nonstd::expected result = parseDownloadPriority(params()[u"priority"_s]); + if (!result) + throw APIError(APIErrorType::BadParams, result.error()); - if (!BitTorrent::isValidDownloadPriority(priority)) - throw APIError(APIErrorType::BadParams, tr("Priority is not valid")); + const BitTorrent::DownloadPriority priority = result.value(); BitTorrent::Torrent *const torrent = BitTorrent::Session::instance()->getTorrent(id); if (!torrent) @@ -1294,6 +1299,7 @@ void TorrentsController::filePrioAction() bool priorityChanged = false; for (const QString &fileID : params()[u"id"_s].split(u'|')) { + bool ok = false; const int id = fileID.toInt(&ok); if (!ok) throw APIError(APIErrorType::BadParams, tr("File IDs must be integers")); From 7ac160a481e8cd185f6623cde1362dd3011ea028 Mon Sep 17 00:00:00 2001 From: Thomas Piccirello Date: Tue, 10 Jun 2025 12:48:08 -0700 Subject: [PATCH 5/5] Bump WebAPI version --- WebAPI_Changelog.md | 9 +++++++++ src/webui/webapplication.h | 2 +- 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/WebAPI_Changelog.md b/WebAPI_Changelog.md index c2164f65a..23272bb5a 100644 --- a/WebAPI_Changelog.md +++ b/WebAPI_Changelog.md @@ -1,5 +1,14 @@ # WebAPI Changelog +## 2.11.9 + +* [#21015](https://github.com/qbittorrent/qBittorrent/pull/21015) + * Add `torrents/fetchMetadata` endpoint for retrieving torrent metadata associated with a URL + * Add `torrents/parseMetadata` endpoint for retrieving torrent metadata associated with a .torrent file + * Add `torrents/saveMetadata` endpoint for saving retrieved torrent metadata to a .torrent file + * `torrents/add` allows adding a torrent with metadata previously retrieved via `torrents/fetchMetadata` or `torrents/parseMetadata` + * `torrents/add` allows specifying a torrent's file priorities + ## 2.11.8 * [#21349](https://github.com/qbittorrent/qBittorrent/pull/21349) diff --git a/src/webui/webapplication.h b/src/webui/webapplication.h index e39140eff..6ceb28593 100644 --- a/src/webui/webapplication.h +++ b/src/webui/webapplication.h @@ -53,7 +53,7 @@ #include "base/utils/version.h" #include "api/isessionmanager.h" -inline const Utils::Version<3, 2> API_VERSION {2, 11, 8}; +inline const Utils::Version<3, 2> API_VERSION {2, 11, 9}; class APIController; class AuthController;