From 14907417c0fc0120d1802be73d3f4df9e747b1a9 Mon Sep 17 00:00:00 2001 From: Thomas Piccirello Date: Sun, 30 Jun 2024 16:43:27 -0700 Subject: [PATCH] Add WebAPI for fetching torrent metadata --- src/webui/api/apicontroller.h | 8 +- src/webui/api/torrentscontroller.cpp | 139 +++++++++++++++++++++++++++ src/webui/api/torrentscontroller.h | 10 ++ 3 files changed, 153 insertions(+), 4 deletions(-) diff --git a/src/webui/api/apicontroller.h b/src/webui/api/apicontroller.h index 93a726cdb1ca..b59e104183f7 100644 --- a/src/webui/api/apicontroller.h +++ b/src/webui/api/apicontroller.h @@ -63,10 +63,10 @@ class APIController : public ApplicationComponent const DataMap &data() const; void requireParams(const QVector &requiredParams) const; - void setResult(const QString &result, const int statusCode = 200); - void setResult(const QJsonArray &result, const int statusCode = 200); - void setResult(const QJsonObject &result, const int statusCode = 200); - void setResult(const QByteArray &result, const QString &mimeType = {}, const QString &filename = {}, const int statusCode = 200); + void setResult(const QString &result, int statusCode = 200); + void setResult(const QJsonArray &result, int statusCode = 200); + void setResult(const QJsonObject &result, int statusCode = 200); + void setResult(const QByteArray &result, const QString &mimeType = {}, const QString &filename = {}, int statusCode = 200); private: StringMap m_params; diff --git a/src/webui/api/torrentscontroller.cpp b/src/webui/api/torrentscontroller.cpp index 8a36fb81d165..8937aa62a24e 100644 --- a/src/webui/api/torrentscontroller.cpp +++ b/src/webui/api/torrentscontroller.cpp @@ -31,6 +31,7 @@ #include #include +#include #include #include #include @@ -127,6 +128,11 @@ 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; +// Metadata keys +const QString KEY_METADATA_TRACKERS = u"trackers"_s; +const QString KEY_METADATA_FILES = u"files"_s; +const QString KEY_METADATA_WEBSEEDS = u"webseeds"_s; + namespace { using Utils::String::parseBool; @@ -250,6 +256,63 @@ namespace idList << BitTorrent::TorrentID::fromString(hash); return idList; } + + QJsonObject serializeTorrentMetadata(const BitTorrent::TorrentInfo &metadata, const QVector &trackers) + { + QJsonArray trackersArr; + for (const auto &tracker : trackers) + { + trackersArr << QJsonObject + { + {KEY_TRACKER_URL, tracker.url}, + {KEY_TRACKER_TIER, tracker.tier} + }; + } + + QJsonArray files; + for (int fileIndex = 0; fileIndex < metadata.filesCount(); ++fileIndex) + { + files << QJsonObject + { + {KEY_FILE_INDEX, fileIndex}, + {KEY_FILE_NAME, metadata.filePath(fileIndex).toString()}, + {KEY_FILE_SIZE, metadata.fileSize(fileIndex)} + }; + } + + QJsonArray webseeds; + for (const QUrl &webseed : metadata.urlSeeds()) + { + webseeds << QJsonObject + { + {KEY_WEBSEED_URL, webseed.toString()} + }; + } + + // based on torrent_info https://www.libtorrent.org/reference-Torrent_Info.html#torrent_info + return QJsonObject { + {KEY_TORRENT_INFOHASHV1, metadata.infoHash().v1().toString()}, + {KEY_TORRENT_INFOHASHV2, metadata.infoHash().v2().toString()}, + {KEY_TORRENT_NAME, metadata.name()}, + {KEY_TORRENT_ID, metadata.infoHash().toTorrentID().toString()}, + {KEY_METADATA_TRACKERS, trackersArr}, + {KEY_PROP_TOTAL_SIZE, metadata.totalSize()}, + {KEY_PROP_PIECES_NUM, metadata.piecesCount()}, + {KEY_PROP_PIECE_SIZE, metadata.pieceLength()}, + {KEY_PROP_CREATED_BY, metadata.creator()}, + {KEY_PROP_PRIVATE, metadata.isPrivate()}, + {KEY_PROP_CREATION_DATE, Utils::DateTime::toSecsSinceEpoch(metadata.creationDate())}, + {KEY_PROP_COMMENT, metadata.comment()}, + {KEY_METADATA_FILES, files}, + {KEY_METADATA_WEBSEEDS, webseeds} + }; + } +} + +TorrentsController::TorrentsController(IApplication *app, QObject *parent) + : APIController(app, parent) +{ + connect(BitTorrent::Session::instance(), &BitTorrent::Session::metadataDownloaded, this, &TorrentsController::onMetadataDownloaded); } void TorrentsController::countAction() @@ -1523,3 +1586,79 @@ void TorrentsController::setSSLParametersAction() torrent->setSSLParameters(sslParams); } + +void TorrentsController::metadataAction() +{ + const QString source {params()[u"source"_s]}; + + if (Net::DownloadManager::hasSupportedScheme(source)) + throw APIError(APIErrorType::BadParams, tr("Source must be magnet URI or info hash")); + + if (const auto parseResult = BitTorrent::TorrentDescriptor::parse(source)) + { + auto *session = BitTorrent::Session::instance(); + const BitTorrent::TorrentDescriptor &torrentDescr = parseResult.value(); + const BitTorrent::InfoHash &infoHash = torrentDescr.infoHash(); + + const BitTorrent::Torrent *const torrent = BitTorrent::Session::instance()->findTorrent(infoHash); + // torrent has already been added and has metadata + if (torrent && torrent->info().isValid()) + { + // the TorrentInfo returned by Torrent::info() contains an empty list of trackers + // we must fetch the trackers directly via Torrent::trackers() + QVector trackers; + for (const BitTorrent::TrackerEntryStatus &tracker : torrent->trackers()) + trackers << BitTorrent::TrackerEntry + { + .url = tracker.url, + .tier = tracker.tier + }; + + setResult(serializeTorrentMetadata(torrent->info(), trackers)); + return; + } + + const auto iter = m_torrentMetadata.find(infoHash); + + // metadata has already been downloaded + if (iter != m_torrentMetadata.end() && iter.value().isValid()) + { + const BitTorrent::TorrentInfo &metadata = iter.value(); + setResult(serializeTorrentMetadata(metadata, metadata.trackers())); + return; + } + + if (!session->isKnownTorrent(infoHash) && iter == m_torrentMetadata.end()) + { + qDebug("Fetching metadata for %s", qUtf8Printable(infoHash.toTorrentID().toString())); + if (!session->downloadMetadata(torrentDescr)) + throw APIError(APIErrorType::BadParams, tr("Unable to download metadata")); + + m_torrentMetadata.insert(infoHash, BitTorrent::TorrentInfo()); + } + + setResult(QJsonObject {}, 202); + return; + } + + throw APIError(APIErrorType::BadParams, tr("Unable to parse torrent")); +} + +void TorrentsController::onMetadataDownloaded(const BitTorrent::TorrentInfo &metadata) +{ + + const auto iter = m_torrentMetadata.find(metadata.infoHash()); + // only process if a lookup was explicitly initiated via API + if (iter != m_torrentMetadata.end()) + { + Q_ASSERT(metadata.isValid()); + if (!metadata.isValid()) [[unlikely]] + { + // reattempt the download on the next request + m_torrentMetadata.remove(metadata.infoHash()); + return; + } + + m_torrentMetadata.insert(metadata.infoHash(), metadata); + } +} diff --git a/src/webui/api/torrentscontroller.h b/src/webui/api/torrentscontroller.h index d731b0de8f5c..8e734dc14841 100644 --- a/src/webui/api/torrentscontroller.h +++ b/src/webui/api/torrentscontroller.h @@ -28,6 +28,8 @@ #pragma once +#include "base/bittorrent/torrent.h" +#include "base/bittorrent/torrentinfo.h" #include "apicontroller.h" class TorrentsController : public APIController @@ -38,6 +40,8 @@ class TorrentsController : public APIController public: using APIController::APIController; + explicit TorrentsController(IApplication *app, QObject *parent = nullptr); + private slots: void countAction(); void infoAction(); @@ -91,4 +95,10 @@ private slots: void exportAction(); void SSLParametersAction(); void setSSLParametersAction(); + void metadataAction(); + +private: + void onMetadataDownloaded(const BitTorrent::TorrentInfo &metadata); + + QHash m_torrentMetadata; };