diff --git a/WebAPI_Changelog.md b/WebAPI_Changelog.md index 67bc7af85..3c85fadd4 100644 --- a/WebAPI_Changelog.md +++ b/WebAPI_Changelog.md @@ -1,5 +1,10 @@ # WebAPI Changelog +## 2.13.1 +* [#23163](https://github.com/qbittorrent/qBittorrent/pull/23163) + * `torrents/add` endpoint now supports downloading from a search plugin via the `downloader` parameter + * `torrents/fetchMetadata` endpoint now supports fetching from a search plugin via the `downloader` parameter + ## 2.13.0 * [#23045](https://github.com/qbittorrent/qBittorrent/pull/23045) * `torrents/trackers` returns three new fields: `next_announce`, `min_announce` and `endpoints` diff --git a/src/webui/api/torrentscontroller.cpp b/src/webui/api/torrentscontroller.cpp index afdde04c3..76e1ca29b 100644 --- a/src/webui/api/torrentscontroller.cpp +++ b/src/webui/api/torrentscontroller.cpp @@ -58,9 +58,12 @@ #include "base/logger.h" #include "base/net/downloadmanager.h" #include "base/preferences.h" +#include "base/search/searchdownloadhandler.h" +#include "base/search/searchpluginmanager.h" #include "base/torrentfilter.h" #include "base/utils/datetime.h" #include "base/utils/fs.h" +#include "base/utils/io.h" #include "base/utils/sslkey.h" #include "base/utils/string.h" #include "apierror.h" @@ -1057,6 +1060,10 @@ void TorrentsController::addAction() } } + const QString downloaderParam = params()[u"downloader"_s]; + if (!downloaderParam.isEmpty() && !SearchPluginManager::instance()->allPlugins().contains(downloaderParam)) + throw APIError(APIErrorType::BadParams, tr("`downloader` must be a valid search plugin")); + BitTorrent::AddTorrentParams addTorrentParams { .name = torrentName, @@ -1118,10 +1125,24 @@ void TorrentsController::addAction() partialSuccess |= BitTorrent::Session::instance()->addTorrent(torrentDescr, addTorrentParams); } + else if (!downloaderParam.isEmpty()) + { + if (!filePriorities.isEmpty()) + throw APIError(APIErrorType::BadParams, tr("`filePriorities` may only be specified when metadata has already been fetched")); + + SearchDownloadHandler *downloadHandler = SearchPluginManager::instance()->downloadTorrent(downloaderParam, url); + connect(downloadHandler, &SearchDownloadHandler::downloadFinished + , this, [this, addTorrentParams](const QString &torrentFilePath) + { + app()->addTorrentManager()->addTorrent(torrentFilePath, addTorrentParams); + }); + connect(downloadHandler, &SearchDownloadHandler::downloadFinished, downloadHandler, &SearchDownloadHandler::deleteLater); + partialSuccess = true; + } else { if (!filePriorities.isEmpty()) - throw APIError(APIErrorType::BadParams, tr("filePriorities may only be specified when metadata has already been fetched")); + throw APIError(APIErrorType::BadParams, tr("`filePriorities` may only be specified when metadata has already been fetched")); partialSuccess |= app()->addTorrentManager()->addTorrent(url, addTorrentParams); } @@ -2032,6 +2053,10 @@ void TorrentsController::fetchMetadataAction() if (sourceParam.isEmpty()) throw APIError(APIErrorType::BadParams, tr("Must specify URI or hash")); + const QString downloaderParam = params()[u"downloader"_s]; + if (!downloaderParam.isEmpty() && !SearchPluginManager::instance()->allPlugins().contains(downloaderParam)) + throw APIError(APIErrorType::BadParams, tr("downloader must be a valid search plugin")); + const QString source = QUrl::fromPercentEncoding(sourceParam.toLatin1()); const auto sourceTorrentDescr = BitTorrent::TorrentDescriptor::parse(source); @@ -2074,11 +2099,23 @@ void TorrentsController::fetchMetadataAction() { 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); + if (!downloaderParam.isEmpty()) + { + SearchDownloadHandler *downloadHandler = SearchPluginManager::instance()->downloadTorrent(downloaderParam, source); + connect(downloadHandler, &SearchDownloadHandler::downloadFinished + , this, [this, source](const QString &data) + { + onSearchPluginTorrentDownloaded(source, data); + }); + connect(downloadHandler, &SearchDownloadHandler::downloadFinished, downloadHandler, &SearchDownloadHandler::deleteLater); + } + else + { + const auto *pref = Preferences::instance(); + Net::DownloadManager::instance()->download(Net::DownloadRequest(source).limit(pref->getTorrentFileSizeLimit()) + , pref->useProxyForGeneralPurposes(), this, &TorrentsController::onDownloadFinished); + } m_requestedTorrentSource.insert(source); } @@ -2156,33 +2193,13 @@ void TorrentsController::onDownloadFinished(const Net::DownloadResult &result) { 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.toTorrentID(), torrentDescr); - } - else - { - LogMsg(tr("Parse torrent failed. URL: \"%1\". Error: \"%2\".").arg(source, loadResult.error()), Log::WARNING); - m_torrentSourceCache.remove(source); - } + cacheTorrentFile(source, result.data); 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(); - const BitTorrent::TorrentID torrentID = infoHash.toTorrentID(); - m_torrentSourceCache.insert(source, infoHash); - - if (!m_torrentMetadataCache.contains(torrentID) && !BitTorrent::Session::instance()->isKnownTorrent(infoHash)) - { - if (BitTorrent::Session::instance()->downloadMetadata(torrentDescr)) - m_torrentMetadataCache.insert(torrentID, torrentDescr); - } + cacheMagnetURI(source, parseResult.value()); } else { @@ -2215,3 +2232,53 @@ void TorrentsController::onMetadataDownloaded(const BitTorrent::TorrentInfo &inf iter.value().setTorrentInfo(info); } } + +void TorrentsController::onSearchPluginTorrentDownloaded(const QString &source, const QString &data) +{ + m_requestedTorrentSource.remove(source); + + // magnet URI + if (const auto parseResult = BitTorrent::TorrentDescriptor::parse(data)) + { + cacheMagnetURI(source, parseResult.value()); + } + // path to .torrent file + else if (const auto readResult = Utils::IO::readFile(Path(data), Preferences::instance()->getTorrentFileSizeLimit())) + { + cacheTorrentFile(source, readResult.value()); + } + else + { + LogMsg(tr("Reading downloaded torrent data failed. Data: \"%1\". Error: \"%2\".").arg(data, readResult.error().message), Log::WARNING); + m_torrentSourceCache.remove(source); + } +} + +void TorrentsController::cacheTorrentFile(const QString &source, const QByteArray &data) +{ + if (const auto loadResult = BitTorrent::TorrentDescriptor::load(data)) + { + const BitTorrent::TorrentDescriptor &torrentDescr = loadResult.value(); + const BitTorrent::InfoHash infoHash = torrentDescr.infoHash(); + m_torrentSourceCache.insert(source, infoHash); + m_torrentMetadataCache.insert(infoHash.toTorrentID(), torrentDescr); + } + else + { + LogMsg(tr("Parse torrent failed. URL: \"%1\". Error: \"%2\".").arg(source, loadResult.error()), Log::WARNING); + m_torrentSourceCache.remove(source); + } +} + +void TorrentsController::cacheMagnetURI(const QString &source, const BitTorrent::TorrentDescriptor &torrentDescr) +{ + const BitTorrent::InfoHash infoHash = torrentDescr.infoHash(); + const BitTorrent::TorrentID torrentID = infoHash.toTorrentID(); + m_torrentSourceCache.insert(source, infoHash); + + if (!m_torrentMetadataCache.contains(torrentID) && !BitTorrent::Session::instance()->isKnownTorrent(infoHash)) + { + if (BitTorrent::Session::instance()->downloadMetadata(torrentDescr)) + m_torrentMetadataCache.insert(torrentID, torrentDescr); + } +} diff --git a/src/webui/api/torrentscontroller.h b/src/webui/api/torrentscontroller.h index 7f81ec15a..3c4705fb7 100644 --- a/src/webui/api/torrentscontroller.h +++ b/src/webui/api/torrentscontroller.h @@ -34,6 +34,8 @@ #include "base/bittorrent/torrentdescriptor.h" #include "apicontroller.h" +class QByteArray; + namespace BitTorrent { class InfoHash; @@ -119,6 +121,9 @@ private slots: private: void onDownloadFinished(const Net::DownloadResult &result); void onMetadataDownloaded(const BitTorrent::TorrentInfo &info); + void onSearchPluginTorrentDownloaded(const QString &source, const QString &data); + void cacheTorrentFile(const QString &source, const QByteArray &data); + void cacheMagnetURI(const QString &source, const BitTorrent::TorrentDescriptor &torrentDescr); QHash m_torrentSourceCache; QHash m_torrentMetadataCache; diff --git a/src/webui/webapplication.h b/src/webui/webapplication.h index 889da879b..f30d5a06f 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, 13, 0}; +inline const Utils::Version<3, 2> API_VERSION {2, 13, 1}; class APIController; class AuthController; diff --git a/src/webui/www/private/addtorrent.html b/src/webui/www/private/addtorrent.html index ccd0e2c09..04b3dfcfc 100644 --- a/src/webui/www/private/addtorrent.html +++ b/src/webui/www/private/addtorrent.html @@ -49,6 +49,13 @@ const windowId = searchParams.get("windowId"); window.qBittorrent.AddTorrent.setWindowId(windowId); + const downloader = searchParams.get("downloader"); + if (downloader?.length > 0) { + const downloaderInput = document.getElementById("downloader"); + downloaderInput.value = downloader; + downloaderInput.disabled = false; + } + document.getElementById("uploadForm").addEventListener("submit", (event) => { submitted = true; window.qBittorrent.AddTorrent.submitForm(); @@ -81,7 +88,7 @@ window.qBittorrent.TorrentContent.init("addTorrentFilesTableDiv", window.qBittorrent.DynamicTable.AddTorrentFilesTable); if (fetchMetadata) - window.qBittorrent.AddTorrent.loadMetadata(source); + window.qBittorrent.AddTorrent.loadMetadata(source, downloader); }); @@ -144,6 +151,7 @@
+
diff --git a/src/webui/www/private/scripts/addtorrent.js b/src/webui/www/private/scripts/addtorrent.js index b36631041..dd026d8e9 100644 --- a/src/webui/www/private/scripts/addtorrent.js +++ b/src/webui/www/private/scripts/addtorrent.js @@ -42,6 +42,7 @@ window.qBittorrent.AddTorrent ??= (() => { let defaultTempPathEnabled = false; let windowId = ""; let source = ""; + let downloader = ""; const localPreferences = new window.qBittorrent.LocalPreferences.LocalPreferences(); @@ -214,14 +215,17 @@ window.qBittorrent.AddTorrent ??= (() => { }; let loadMetadataTimer = -1; - const loadMetadata = (sourceUrl = undefined) => { + const loadMetadata = (sourceUrl = undefined, downloaderName = undefined) => { if (sourceUrl !== undefined) source = sourceUrl; + if (downloaderName !== undefined) + downloader = downloaderName; fetch("api/v2/torrents/fetchMetadata", { method: "POST", body: new URLSearchParams({ - source: source + source: source, + downloader: downloader }) }) .then(async (response) => { diff --git a/src/webui/www/private/scripts/client.js b/src/webui/www/private/scripts/client.js index 4b3ac11a7..2c6126e6c 100644 --- a/src/webui/www/private/scripts/client.js +++ b/src/webui/www/private/scripts/client.js @@ -130,7 +130,7 @@ window.qBittorrent.Client ??= (() => { return showingLogViewer; }; - const createAddTorrentWindow = (title, source, metadata = undefined) => { + const createAddTorrentWindow = (title, source, metadata = undefined, downloader = undefined) => { const isFirefox = navigator.userAgent.includes("Firefox"); const isSafari = navigator.userAgent.includes("AppleWebKit") && !navigator.userAgent.includes("Chrome"); let height = 855; @@ -147,7 +147,8 @@ window.qBittorrent.Client ??= (() => { v: "${CACHEID}", source: source, fetch: metadata === undefined, - windowId: id + windowId: id, + downloader: downloader ?? "" }); new MochaUI.Window({ diff --git a/src/webui/www/private/scripts/search.js b/src/webui/www/private/scripts/search.js index 7bfb8cbb3..c3ab21539 100644 --- a/src/webui/www/private/scripts/search.js +++ b/src/webui/www/private/scripts/search.js @@ -561,8 +561,8 @@ window.qBittorrent.Search ??= (() => { const downloadSearchTorrent = () => { for (const rowID of searchResultsTable.selectedRowsIds()) { - const { fileName, fileUrl } = searchResultsTable.getRow(rowID).full_data; - qBittorrent.Client.createAddTorrentWindow(fileName, fileUrl); + const { engineName, fileName, fileUrl } = searchResultsTable.getRow(rowID).full_data; + qBittorrent.Client.createAddTorrentWindow(fileName, fileUrl, undefined, engineName); } };