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