Add WebAPI for fetching torrent metadata

Signed-off-by: Thomas Piccirello <thomas@piccirello.com>
This commit is contained in:
Thomas Piccirello 2024-07-06 22:38:33 -07:00
parent ff07591a87
commit 5750de6270
No known key found for this signature in database
2 changed files with 298 additions and 1 deletions

View file

@ -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);
}

View file

@ -28,15 +28,30 @@
#pragma once
#include <QHash>
#include <QSet>
#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<QString, BitTorrent::InfoHash> m_torrentSourceCache;
QHash<BitTorrent::InfoHash, BitTorrent::TorrentDescriptor> m_torrentMetadataCache;
QSet<QString> m_requestedTorrentSource;
};