mirror of
https://github.com/qbittorrent/qBittorrent.git
synced 2025-12-17 06:01:33 -06:00
WebUI/WebAPI: Support downloading torrent via search plugin
This adds support for downloading a torrent via a search plugin's `download_torrent` function. This primarily affects torrents that use a private tracker requiring a login. Closes #18334. PR #23163.
This commit is contained in:
committed by
GitHub
parent
becfd19e34
commit
4fa433a728
@@ -1,5 +1,10 @@
|
|||||||
# WebAPI Changelog
|
# 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
|
## 2.13.0
|
||||||
* [#23045](https://github.com/qbittorrent/qBittorrent/pull/23045)
|
* [#23045](https://github.com/qbittorrent/qBittorrent/pull/23045)
|
||||||
* `torrents/trackers` returns three new fields: `next_announce`, `min_announce` and `endpoints`
|
* `torrents/trackers` returns three new fields: `next_announce`, `min_announce` and `endpoints`
|
||||||
|
|||||||
@@ -58,9 +58,12 @@
|
|||||||
#include "base/logger.h"
|
#include "base/logger.h"
|
||||||
#include "base/net/downloadmanager.h"
|
#include "base/net/downloadmanager.h"
|
||||||
#include "base/preferences.h"
|
#include "base/preferences.h"
|
||||||
|
#include "base/search/searchdownloadhandler.h"
|
||||||
|
#include "base/search/searchpluginmanager.h"
|
||||||
#include "base/torrentfilter.h"
|
#include "base/torrentfilter.h"
|
||||||
#include "base/utils/datetime.h"
|
#include "base/utils/datetime.h"
|
||||||
#include "base/utils/fs.h"
|
#include "base/utils/fs.h"
|
||||||
|
#include "base/utils/io.h"
|
||||||
#include "base/utils/sslkey.h"
|
#include "base/utils/sslkey.h"
|
||||||
#include "base/utils/string.h"
|
#include "base/utils/string.h"
|
||||||
#include "apierror.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
|
BitTorrent::AddTorrentParams addTorrentParams
|
||||||
{
|
{
|
||||||
.name = torrentName,
|
.name = torrentName,
|
||||||
@@ -1118,10 +1125,24 @@ void TorrentsController::addAction()
|
|||||||
|
|
||||||
partialSuccess |= BitTorrent::Session::instance()->addTorrent(torrentDescr, addTorrentParams);
|
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
|
else
|
||||||
{
|
{
|
||||||
if (!filePriorities.isEmpty())
|
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);
|
partialSuccess |= app()->addTorrentManager()->addTorrent(url, addTorrentParams);
|
||||||
}
|
}
|
||||||
@@ -2032,6 +2053,10 @@ void TorrentsController::fetchMetadataAction()
|
|||||||
if (sourceParam.isEmpty())
|
if (sourceParam.isEmpty())
|
||||||
throw APIError(APIErrorType::BadParams, tr("Must specify URI or hash"));
|
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 QString source = QUrl::fromPercentEncoding(sourceParam.toLatin1());
|
||||||
const auto sourceTorrentDescr = BitTorrent::TorrentDescriptor::parse(source);
|
const auto sourceTorrentDescr = BitTorrent::TorrentDescriptor::parse(source);
|
||||||
|
|
||||||
@@ -2074,11 +2099,23 @@ void TorrentsController::fetchMetadataAction()
|
|||||||
{
|
{
|
||||||
if (!m_requestedTorrentSource.contains(source))
|
if (!m_requestedTorrentSource.contains(source))
|
||||||
{
|
{
|
||||||
const auto *pref = Preferences::instance();
|
if (!downloaderParam.isEmpty())
|
||||||
|
{
|
||||||
Net::DownloadManager::instance()->download(Net::DownloadRequest(source).limit(pref->getTorrentFileSizeLimit())
|
SearchDownloadHandler *downloadHandler = SearchPluginManager::instance()->downloadTorrent(downloaderParam, source);
|
||||||
, pref->useProxyForGeneralPurposes(), this, &TorrentsController::onDownloadFinished);
|
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);
|
m_requestedTorrentSource.insert(source);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2156,33 +2193,13 @@ void TorrentsController::onDownloadFinished(const Net::DownloadResult &result)
|
|||||||
{
|
{
|
||||||
case Net::DownloadStatus::Success:
|
case Net::DownloadStatus::Success:
|
||||||
// use the info directly from the .torrent file
|
// use the info directly from the .torrent file
|
||||||
if (const auto loadResult = BitTorrent::TorrentDescriptor::load(result.data))
|
cacheTorrentFile(source, 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);
|
|
||||||
}
|
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case Net::DownloadStatus::RedirectedToMagnet:
|
case Net::DownloadStatus::RedirectedToMagnet:
|
||||||
if (const auto parseResult = BitTorrent::TorrentDescriptor::parse(result.magnetURI))
|
if (const auto parseResult = BitTorrent::TorrentDescriptor::parse(result.magnetURI))
|
||||||
{
|
{
|
||||||
const BitTorrent::TorrentDescriptor &torrentDescr = parseResult.value();
|
cacheMagnetURI(source, 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);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
@@ -2215,3 +2232,53 @@ void TorrentsController::onMetadataDownloaded(const BitTorrent::TorrentInfo &inf
|
|||||||
iter.value().setTorrentInfo(info);
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -34,6 +34,8 @@
|
|||||||
#include "base/bittorrent/torrentdescriptor.h"
|
#include "base/bittorrent/torrentdescriptor.h"
|
||||||
#include "apicontroller.h"
|
#include "apicontroller.h"
|
||||||
|
|
||||||
|
class QByteArray;
|
||||||
|
|
||||||
namespace BitTorrent
|
namespace BitTorrent
|
||||||
{
|
{
|
||||||
class InfoHash;
|
class InfoHash;
|
||||||
@@ -119,6 +121,9 @@ private slots:
|
|||||||
private:
|
private:
|
||||||
void onDownloadFinished(const Net::DownloadResult &result);
|
void onDownloadFinished(const Net::DownloadResult &result);
|
||||||
void onMetadataDownloaded(const BitTorrent::TorrentInfo &info);
|
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<QString, BitTorrent::InfoHash> m_torrentSourceCache;
|
QHash<QString, BitTorrent::InfoHash> m_torrentSourceCache;
|
||||||
QHash<BitTorrent::TorrentID, BitTorrent::TorrentDescriptor> m_torrentMetadataCache;
|
QHash<BitTorrent::TorrentID, BitTorrent::TorrentDescriptor> m_torrentMetadataCache;
|
||||||
|
|||||||
@@ -53,7 +53,7 @@
|
|||||||
#include "base/utils/version.h"
|
#include "base/utils/version.h"
|
||||||
#include "api/isessionmanager.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 APIController;
|
||||||
class AuthController;
|
class AuthController;
|
||||||
|
|||||||
@@ -49,6 +49,13 @@
|
|||||||
const windowId = searchParams.get("windowId");
|
const windowId = searchParams.get("windowId");
|
||||||
window.qBittorrent.AddTorrent.setWindowId(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) => {
|
document.getElementById("uploadForm").addEventListener("submit", (event) => {
|
||||||
submitted = true;
|
submitted = true;
|
||||||
window.qBittorrent.AddTorrent.submitForm();
|
window.qBittorrent.AddTorrent.submitForm();
|
||||||
@@ -81,7 +88,7 @@
|
|||||||
window.qBittorrent.TorrentContent.init("addTorrentFilesTableDiv", window.qBittorrent.DynamicTable.AddTorrentFilesTable);
|
window.qBittorrent.TorrentContent.init("addTorrentFilesTableDiv", window.qBittorrent.DynamicTable.AddTorrentFilesTable);
|
||||||
|
|
||||||
if (fetchMetadata)
|
if (fetchMetadata)
|
||||||
window.qBittorrent.AddTorrent.loadMetadata(source);
|
window.qBittorrent.AddTorrent.loadMetadata(source, downloader);
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -144,6 +151,7 @@
|
|||||||
<form action="api/v2/torrents/add" enctype="multipart/form-data" method="post" id="uploadForm" style="text-align: center; padding: 10px 12px;" target="upload_frame" autocorrect="off" autocapitalize="none">
|
<form action="api/v2/torrents/add" enctype="multipart/form-data" method="post" id="uploadForm" style="text-align: center; padding: 10px 12px;" target="upload_frame" autocorrect="off" autocapitalize="none">
|
||||||
<input type="hidden" id="urls" name="urls">
|
<input type="hidden" id="urls" name="urls">
|
||||||
<input type="hidden" id="filePriorities" name="filePriorities">
|
<input type="hidden" id="filePriorities" name="filePriorities">
|
||||||
|
<input type="hidden" id="downloader" name="downloader" disabled>
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<div class="column">
|
<div class="column">
|
||||||
<fieldset class="settings" style="text-align: left;">
|
<fieldset class="settings" style="text-align: left;">
|
||||||
|
|||||||
@@ -42,6 +42,7 @@ window.qBittorrent.AddTorrent ??= (() => {
|
|||||||
let defaultTempPathEnabled = false;
|
let defaultTempPathEnabled = false;
|
||||||
let windowId = "";
|
let windowId = "";
|
||||||
let source = "";
|
let source = "";
|
||||||
|
let downloader = "";
|
||||||
|
|
||||||
const localPreferences = new window.qBittorrent.LocalPreferences.LocalPreferences();
|
const localPreferences = new window.qBittorrent.LocalPreferences.LocalPreferences();
|
||||||
|
|
||||||
@@ -214,14 +215,17 @@ window.qBittorrent.AddTorrent ??= (() => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
let loadMetadataTimer = -1;
|
let loadMetadataTimer = -1;
|
||||||
const loadMetadata = (sourceUrl = undefined) => {
|
const loadMetadata = (sourceUrl = undefined, downloaderName = undefined) => {
|
||||||
if (sourceUrl !== undefined)
|
if (sourceUrl !== undefined)
|
||||||
source = sourceUrl;
|
source = sourceUrl;
|
||||||
|
if (downloaderName !== undefined)
|
||||||
|
downloader = downloaderName;
|
||||||
|
|
||||||
fetch("api/v2/torrents/fetchMetadata", {
|
fetch("api/v2/torrents/fetchMetadata", {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
body: new URLSearchParams({
|
body: new URLSearchParams({
|
||||||
source: source
|
source: source,
|
||||||
|
downloader: downloader
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
.then(async (response) => {
|
.then(async (response) => {
|
||||||
|
|||||||
@@ -130,7 +130,7 @@ window.qBittorrent.Client ??= (() => {
|
|||||||
return showingLogViewer;
|
return showingLogViewer;
|
||||||
};
|
};
|
||||||
|
|
||||||
const createAddTorrentWindow = (title, source, metadata = undefined) => {
|
const createAddTorrentWindow = (title, source, metadata = undefined, downloader = undefined) => {
|
||||||
const isFirefox = navigator.userAgent.includes("Firefox");
|
const isFirefox = navigator.userAgent.includes("Firefox");
|
||||||
const isSafari = navigator.userAgent.includes("AppleWebKit") && !navigator.userAgent.includes("Chrome");
|
const isSafari = navigator.userAgent.includes("AppleWebKit") && !navigator.userAgent.includes("Chrome");
|
||||||
let height = 855;
|
let height = 855;
|
||||||
@@ -147,7 +147,8 @@ window.qBittorrent.Client ??= (() => {
|
|||||||
v: "${CACHEID}",
|
v: "${CACHEID}",
|
||||||
source: source,
|
source: source,
|
||||||
fetch: metadata === undefined,
|
fetch: metadata === undefined,
|
||||||
windowId: id
|
windowId: id,
|
||||||
|
downloader: downloader ?? ""
|
||||||
});
|
});
|
||||||
|
|
||||||
new MochaUI.Window({
|
new MochaUI.Window({
|
||||||
|
|||||||
@@ -561,8 +561,8 @@ window.qBittorrent.Search ??= (() => {
|
|||||||
|
|
||||||
const downloadSearchTorrent = () => {
|
const downloadSearchTorrent = () => {
|
||||||
for (const rowID of searchResultsTable.selectedRowsIds()) {
|
for (const rowID of searchResultsTable.selectedRowsIds()) {
|
||||||
const { fileName, fileUrl } = searchResultsTable.getRow(rowID).full_data;
|
const { engineName, fileName, fileUrl } = searchResultsTable.getRow(rowID).full_data;
|
||||||
qBittorrent.Client.createAddTorrentWindow(fileName, fileUrl);
|
qBittorrent.Client.createAddTorrentWindow(fileName, fileUrl, undefined, engineName);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user