Skip to content

Commit

Permalink
Add WebAPI for fetching torrent metadata
Browse files Browse the repository at this point in the history
  • Loading branch information
Piccirello committed Jul 1, 2024
1 parent 2a4f27a commit cec83e2
Show file tree
Hide file tree
Showing 4 changed files with 151 additions and 4 deletions.
8 changes: 4 additions & 4 deletions src/webui/api/apicontroller.h
Original file line number Diff line number Diff line change
Expand Up @@ -63,10 +63,10 @@ class APIController : public ApplicationComponent<QObject>
const DataMap &data() const;
void requireParams(const QVector<QString> &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;
Expand Down
3 changes: 3 additions & 0 deletions src/webui/api/serialize/serialize_torrent.h
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ inline const QString KEY_TORRENT_CONTENT_PATH = u"content_path"_s;
inline const QString KEY_TORRENT_ADDED_ON = u"added_on"_s;
inline const QString KEY_TORRENT_COMPLETION_ON = u"completion_on"_s;
inline const QString KEY_TORRENT_TRACKER = u"tracker"_s;
inline const QString KEY_TORRENT_TRACKERS = u"trackers"_s;
inline const QString KEY_TORRENT_TRACKERS_COUNT = u"trackers_count"_s;
inline const QString KEY_TORRENT_DL_LIMIT = u"dl_limit"_s;
inline const QString KEY_TORRENT_UP_LIMIT = u"up_limit"_s;
Expand All @@ -94,5 +95,7 @@ inline const QString KEY_TORRENT_AVAILABILITY = u"availability"_s;
inline const QString KEY_TORRENT_REANNOUNCE = u"reannounce"_s;
inline const QString KEY_TORRENT_COMMENT = u"comment"_s;
inline const QString KEY_TORRENT_PRIVATE = u"private"_s;
inline const QString KEY_TORRENT_FILES = u"files"_s;
inline const QString KEY_TORRENT_WEBSEEDS = u"webseeds"_s;

QVariantMap serialize(const BitTorrent::Torrent &torrent);
134 changes: 134 additions & 0 deletions src/webui/api/torrentscontroller.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
#include <functional>

#include <QBitArray>
#include <QHash>
#include <QJsonArray>
#include <QJsonObject>
#include <QList>
Expand Down Expand Up @@ -250,6 +251,63 @@ namespace
idList << BitTorrent::TorrentID::fromString(hash);
return idList;
}

QJsonObject serializeTorrentMetadata(const BitTorrent::TorrentInfo &metadata, const QVector<BitTorrent::TrackerEntry> &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_TORRENT_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_TORRENT_FILES, files},
{KEY_TORRENT_WEBSEEDS, webseeds}
};
}
}

TorrentsController::TorrentsController(IApplication *app, QObject *parent)
: APIController(app, parent)
{
connect(BitTorrent::Session::instance(), &BitTorrent::Session::metadataDownloaded, this, &TorrentsController::onMetadataDownloaded);
}

void TorrentsController::countAction()
Expand Down Expand Up @@ -1523,3 +1581,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<BitTorrent::TrackerEntry> 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);
}
}
10 changes: 10 additions & 0 deletions src/webui/api/torrentscontroller.h
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@

#pragma once

#include "base/bittorrent/torrent.h"
#include "base/bittorrent/torrentinfo.h"
#include "apicontroller.h"

class TorrentsController : public APIController
Expand All @@ -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();
Expand Down Expand Up @@ -91,4 +95,10 @@ private slots:
void exportAction();
void SSLParametersAction();
void setSSLParametersAction();
void metadataAction();

private:
void onMetadataDownloaded(const BitTorrent::TorrentInfo &metadata);

QHash<BitTorrent::InfoHash, BitTorrent::TorrentInfo> m_torrentMetadata;
};

0 comments on commit cec83e2

Please sign in to comment.