/* * Bittorrent Client using Qt and libtorrent. * Copyright (C) 2018-2025 Vladimir Golovnev * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License * as published by the Free Software Foundation; either version 2 * of the License, or (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. * * In addition, as a special exception, the copyright holders give permission to * link this program with the OpenSSL project's "OpenSSL" library (or with * modified versions of it that use the same license as the "OpenSSL" library), * and distribute the linked executables. You must obey the GNU General Public * License in all respects for all of the code used other than "OpenSSL". If you * modify file(s), you may extend this exception to your version of the file(s), * but you are not obligated to do so. If you do not wish to do so, delete this * exception statement from your version. */ #include "torrentscontroller.h" #include #include #include #include #include #include #include #include #include #include "base/addtorrentmanager.h" #include "base/bittorrent/categoryoptions.h" #include "base/bittorrent/downloadpriority.h" #include "base/bittorrent/infohash.h" #include "base/bittorrent/peeraddress.h" #include "base/bittorrent/peerinfo.h" #include "base/bittorrent/session.h" #include "base/bittorrent/sslparameters.h" #include "base/bittorrent/torrent.h" #include "base/bittorrent/torrentdescriptor.h" #include "base/bittorrent/trackerentry.h" #include "base/bittorrent/trackerentrystatus.h" #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 const QString KEY_TRACKER_URL = u"url"_s; const QString KEY_TRACKER_UPDATING = u"updating"_s; const QString KEY_TRACKER_STATUS = u"status"_s; const QString KEY_TRACKER_TIER = u"tier"_s; const QString KEY_TRACKER_MSG = u"msg"_s; const QString KEY_TRACKER_PEERS_COUNT = u"num_peers"_s; const QString KEY_TRACKER_SEEDS_COUNT = u"num_seeds"_s; const QString KEY_TRACKER_LEECHES_COUNT = u"num_leeches"_s; const QString KEY_TRACKER_DOWNLOADED_COUNT = u"num_downloaded"_s; // Web seed keys const QString KEY_WEBSEED_URL = u"url"_s; // Torrent keys (Properties) const QString KEY_PROP_TIME_ELAPSED = u"time_elapsed"_s; const QString KEY_PROP_SEEDING_TIME = u"seeding_time"_s; const QString KEY_PROP_ETA = u"eta"_s; const QString KEY_PROP_CONNECT_COUNT = u"nb_connections"_s; const QString KEY_PROP_CONNECT_COUNT_LIMIT = u"nb_connections_limit"_s; const QString KEY_PROP_DOWNLOADED = u"total_downloaded"_s; const QString KEY_PROP_DOWNLOADED_SESSION = u"total_downloaded_session"_s; const QString KEY_PROP_UPLOADED = u"total_uploaded"_s; const QString KEY_PROP_UPLOADED_SESSION = u"total_uploaded_session"_s; const QString KEY_PROP_DL_SPEED = u"dl_speed"_s; const QString KEY_PROP_DL_SPEED_AVG = u"dl_speed_avg"_s; const QString KEY_PROP_UP_SPEED = u"up_speed"_s; const QString KEY_PROP_UP_SPEED_AVG = u"up_speed_avg"_s; const QString KEY_PROP_DL_LIMIT = u"dl_limit"_s; const QString KEY_PROP_UP_LIMIT = u"up_limit"_s; const QString KEY_PROP_WASTED = u"total_wasted"_s; const QString KEY_PROP_SEEDS = u"seeds"_s; const QString KEY_PROP_SEEDS_TOTAL = u"seeds_total"_s; const QString KEY_PROP_PEERS = u"peers"_s; const QString KEY_PROP_PEERS_TOTAL = u"peers_total"_s; const QString KEY_PROP_RATIO = u"share_ratio"_s; const QString KEY_PROP_POPULARITY = u"popularity"_s; const QString KEY_PROP_REANNOUNCE = u"reannounce"_s; const QString KEY_PROP_TOTAL_SIZE = u"total_size"_s; const QString KEY_PROP_PIECES_NUM = u"pieces_num"_s; const QString KEY_PROP_PIECE_SIZE = u"piece_size"_s; const QString KEY_PROP_PIECES_HAVE = u"pieces_have"_s; const QString KEY_PROP_CREATED_BY = u"created_by"_s; const QString KEY_PROP_LAST_SEEN = u"last_seen"_s; const QString KEY_PROP_ADDITION_DATE = u"addition_date"_s; const QString KEY_PROP_COMPLETION_DATE = u"completion_date"_s; const QString KEY_PROP_CREATION_DATE = u"creation_date"_s; const QString KEY_PROP_SAVE_PATH = u"save_path"_s; const QString KEY_PROP_DOWNLOAD_PATH = u"download_path"_s; const QString KEY_PROP_COMMENT = u"comment"_s; const QString KEY_PROP_IS_PRIVATE = u"is_private"_s; // deprecated, "private" should be used instead const QString KEY_PROP_PRIVATE = u"private"_s; const QString KEY_PROP_SSL_CERTIFICATE = u"ssl_certificate"_s; const QString KEY_PROP_SSL_PRIVATEKEY = u"ssl_private_key"_s; const QString KEY_PROP_SSL_DHPARAMS = u"ssl_dh_params"_s; const QString KEY_PROP_HAS_METADATA = u"has_metadata"_s; const QString KEY_PROP_PROGRESS = u"progress"_s; const QString KEY_PROP_FILES = u"files"_s; const QString KEY_PROP_TRACKERS = u"trackers"_s; // File keys const QString KEY_FILE_INDEX = u"index"_s; const QString KEY_FILE_NAME = u"name"_s; const QString KEY_FILE_SIZE = u"size"_s; const QString KEY_FILE_PROGRESS = u"progress"_s; const QString KEY_FILE_PRIORITY = u"priority"_s; 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; using Utils::String::parseInt; using Utils::String::parseDouble; const QSet SUPPORTED_WEB_SEED_SCHEMES {u"http"_s, u"https"_s, u"ftp"_s}; template void applyToTorrents(const QStringList &idList, Func func) requires std::invocable { if ((idList.size() == 1) && (idList[0] == u"all")) { for (BitTorrent::Torrent *const torrent : asConst(BitTorrent::Session::instance()->torrents())) func(torrent); } else { for (const QString &idString : idList) { const auto hash = BitTorrent::TorrentID::fromString(idString); BitTorrent::Torrent *const torrent = BitTorrent::Session::instance()->getTorrent(hash); if (torrent) func(torrent); } } } std::optional getOptionalString(const StringMap ¶ms, const QString &name) { const auto it = params.constFind(name); if (it == params.cend()) return std::nullopt; return it.value(); } std::optional getOptionalTag(const StringMap ¶ms, const QString &name) { const auto it = params.constFind(name); if (it == params.cend()) return std::nullopt; return Tag(it.value()); } QJsonArray getStickyTrackers(const BitTorrent::Torrent *const torrent) { int seedsDHT = 0, seedsPeX = 0, seedsLSD = 0, leechesDHT = 0, leechesPeX = 0, leechesLSD = 0; const QList peersList = torrent->fetchPeerInfo().takeResult(); for (const BitTorrent::PeerInfo &peer : peersList) { if (peer.isConnecting()) continue; if (peer.isSeed()) { if (peer.fromDHT()) ++seedsDHT; if (peer.fromPeX()) ++seedsPeX; if (peer.fromLSD()) ++seedsLSD; } else { if (peer.fromDHT()) ++leechesDHT; if (peer.fromPeX()) ++leechesPeX; if (peer.fromLSD()) ++leechesLSD; } } const int working = static_cast(BitTorrent::TrackerEndpointState::Working); const int disabled = 0; const QString privateMsg {QCoreApplication::translate("TrackerListWidget", "This torrent is private")}; const bool isTorrentPrivate = torrent->isPrivate(); const QJsonObject dht { {KEY_TRACKER_URL, u"** [DHT] **"_s}, {KEY_TRACKER_TIER, -1}, {KEY_TRACKER_MSG, (isTorrentPrivate ? privateMsg : u""_s)}, {KEY_TRACKER_STATUS, ((BitTorrent::Session::instance()->isDHTEnabled() && !isTorrentPrivate) ? working : disabled)}, {KEY_TRACKER_PEERS_COUNT, 0}, {KEY_TRACKER_DOWNLOADED_COUNT, 0}, {KEY_TRACKER_SEEDS_COUNT, seedsDHT}, {KEY_TRACKER_LEECHES_COUNT, leechesDHT} }; const QJsonObject pex { {KEY_TRACKER_URL, u"** [PeX] **"_s}, {KEY_TRACKER_TIER, -1}, {KEY_TRACKER_MSG, (isTorrentPrivate ? privateMsg : u""_s)}, {KEY_TRACKER_STATUS, ((BitTorrent::Session::instance()->isPeXEnabled() && !isTorrentPrivate) ? working : disabled)}, {KEY_TRACKER_PEERS_COUNT, 0}, {KEY_TRACKER_DOWNLOADED_COUNT, 0}, {KEY_TRACKER_SEEDS_COUNT, seedsPeX}, {KEY_TRACKER_LEECHES_COUNT, leechesPeX} }; const QJsonObject lsd { {KEY_TRACKER_URL, u"** [LSD] **"_s}, {KEY_TRACKER_TIER, -1}, {KEY_TRACKER_MSG, (isTorrentPrivate ? privateMsg : u""_s)}, {KEY_TRACKER_STATUS, ((BitTorrent::Session::instance()->isLSDEnabled() && !isTorrentPrivate) ? working : disabled)}, {KEY_TRACKER_PEERS_COUNT, 0}, {KEY_TRACKER_DOWNLOADED_COUNT, 0}, {KEY_TRACKER_SEEDS_COUNT, seedsLSD}, {KEY_TRACKER_LEECHES_COUNT, leechesLSD} }; return {dht, pex, lsd}; } QJsonArray getTrackers(const BitTorrent::Torrent *const torrent) { QJsonArray trackerList; for (const BitTorrent::TrackerEntryStatus &tracker : asConst(torrent->trackers())) { const bool isNotWorking = (tracker.state == BitTorrent::TrackerEndpointState::NotWorking) || (tracker.state == BitTorrent::TrackerEndpointState::TrackerError) || (tracker.state == BitTorrent::TrackerEndpointState::Unreachable); trackerList << QJsonObject { {KEY_TRACKER_URL, tracker.url}, {KEY_TRACKER_TIER, tracker.tier}, {KEY_TRACKER_UPDATING, tracker.isUpdating}, {KEY_TRACKER_STATUS, static_cast((isNotWorking ? BitTorrent::TrackerEndpointState::NotWorking : tracker.state))}, {KEY_TRACKER_MSG, tracker.message}, {KEY_TRACKER_PEERS_COUNT, tracker.numPeers}, {KEY_TRACKER_SEEDS_COUNT, tracker.numSeeds}, {KEY_TRACKER_LEECHES_COUNT, tracker.numLeeches}, {KEY_TRACKER_DOWNLOADED_COUNT, tracker.numDownloaded} }; } return trackerList; } QJsonArray getFiles(const BitTorrent::Torrent *const torrent, QList fileIndexes = {}) { Q_ASSERT(torrent->hasMetadata()); if (!torrent->hasMetadata()) [[unlikely]] return {}; if (fileIndexes.isEmpty()) { const int filesCount = torrent->filesCount(); fileIndexes.reserve(filesCount); for (int i = 0; i < filesCount; ++i) fileIndexes.append(i); } QJsonArray fileList; const QList priorities = torrent->filePriorities(); const QList fp = torrent->filesProgress(); const QList fileAvailability = torrent->fetchAvailableFileFractions().takeResult(); const BitTorrent::TorrentInfo info = torrent->info(); for (const int index : asConst(fileIndexes)) { const BitTorrent::TorrentInfo::PieceRange idx = info.filePieces(index); const QJsonObject fileDict = { {KEY_FILE_INDEX, index}, {KEY_FILE_PROGRESS, fp[index]}, {KEY_FILE_PRIORITY, static_cast(priorities[index])}, {KEY_FILE_SIZE, torrent->fileSize(index)}, {KEY_FILE_AVAILABILITY, fileAvailability[index]}, // need to provide paths using a platform-independent separator format {KEY_FILE_NAME, torrent->filePath(index).data()}, {KEY_FILE_PIECE_RANGE, QJsonArray {idx.first(), idx.last()}} }; fileList.append(fileDict); } return fileList; } QList toTorrentIDs(const QStringList &idStrings) { QList idList; idList.reserve(idStrings.size()); for (const QString &hash : idStrings) idList << BitTorrent::TorrentID::fromString(hash); return idList; } nonstd::expected validateWebSeedUrl(const QString &urlStr) { const QString normalizedUrlStr = QUrl::fromPercentEncoding(urlStr.toLatin1()); const QUrl url {normalizedUrlStr, QUrl::StrictMode}; if (!url.isValid()) return nonstd::make_unexpected(TorrentsController::tr("\"%1\" is not a valid URL").arg(normalizedUrlStr)); if (!SUPPORTED_WEB_SEED_SCHEMES.contains(url.scheme())) return nonstd::make_unexpected(TorrentsController::tr("URL scheme must be one of [%1]").arg(SUPPORTED_WEB_SEED_SCHEMES.values().join(u", "))); 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; } nonstd::expected parseDownloadPriority(const QString &priorityStr) { bool ok = false; const auto priority = static_cast(priorityStr.toInt(&ok)); if (!ok) return nonstd::make_unexpected(TorrentsController::tr("Priority must be an integer")); if (!isValidDownloadPriority(priority)) return nonstd::make_unexpected(TorrentsController::tr("Priority is not valid")); return priority; } } TorrentsController::TorrentsController(IApplication *app, QObject *parent) : APIController(app, parent) { connect(BitTorrent::Session::instance(), &BitTorrent::Session::metadataDownloaded, this, &TorrentsController::onMetadataDownloaded); } void TorrentsController::countAction() { setResult(QString::number(BitTorrent::Session::instance()->torrentsCount())); } // Returns all the torrents in JSON format. // The return value is a JSON-formatted list of dictionaries. // The dictionary keys are: // - "hash": Torrent hash (ID) // - "name": Torrent name // - "size": Torrent size // - "progress": Torrent progress // - "dlspeed": Torrent download speed // - "upspeed": Torrent upload speed // - "priority": Torrent queue position (-1 if queuing is disabled) // - "num_seeds": Torrent seeds connected to // - "num_complete": Torrent seeds in the swarm // - "num_leechs": Torrent leechers connected to // - "num_incomplete": Torrent leechers in the swarm // - "ratio": Torrent share ratio // - "eta": Torrent ETA // - "state": Torrent state // - "seq_dl": Torrent sequential download state // - "f_l_piece_prio": Torrent first last piece priority state // - "force_start": Torrent force start state // - "category": Torrent category // GET params: // - filter (string): all, downloading, seeding, completed, stopped, running, active, inactive, stalled, stalled_uploading, stalled_downloading // - category (string): torrent category for filtering by it (empty string means "uncategorized"; no "category" param presented means "any category") // - tag (string): torrent tag for filtering by it (empty string means "untagged"; no "tag" param presented means "any tag") // - hashes (string): filter by hashes, can contain multiple hashes separated by | // - private (bool): filter torrents that are from private trackers (true) or not (false). Empty means any torrent (no filtering) // - includeFiles (bool): include files in list output (true) or not (false). Empty means not included // - includeTrackers (bool): include trackers in list output (true) or not (false). Empty means not included // - sort (string): name of column for sorting by its value // - reverse (bool): enable reverse sorting // - limit (int): set limit number of torrents returned (if greater than 0, otherwise - unlimited) // - offset (int): set offset (if less than 0 - offset from end) void TorrentsController::infoAction() { const QString filter {params()[u"filter"_s]}; const std::optional category = getOptionalString(params(), u"category"_s); const std::optional tag = getOptionalTag(params(), u"tag"_s); const QString sortedColumn {params()[u"sort"_s]}; const bool reverse {parseBool(params()[u"reverse"_s]).value_or(false)}; int limit {params()[u"limit"_s].toInt()}; int offset {params()[u"offset"_s].toInt()}; const QStringList hashes {params()[u"hashes"_s].split(u'|', Qt::SkipEmptyParts)}; const std::optional isPrivate = parseBool(params()[u"private"_s]); const bool includeFiles = parseBool(params()[u"includeFiles"_s]).value_or(false); const bool includeTrackers = parseBool(params()[u"includeTrackers"_s]).value_or(false); std::optional idSet; if (!hashes.isEmpty()) { idSet = TorrentIDSet(); for (const QString &hash : hashes) idSet->insert(BitTorrent::TorrentID::fromString(hash)); } const TorrentFilter torrentFilter {filter, idSet, category, tag, isPrivate}; QVariantList torrentList; for (const BitTorrent::Torrent *torrent : asConst(BitTorrent::Session::instance()->torrents())) { if (!torrentFilter.match(torrent)) continue; QVariantMap serializedTorrent = serialize(*torrent); if (includeFiles && torrent->hasMetadata()) serializedTorrent.insert(KEY_PROP_FILES, getFiles(torrent)); if (includeTrackers) serializedTorrent.insert(KEY_PROP_TRACKERS, getTrackers(torrent)); torrentList.append(serializedTorrent); } if (torrentList.isEmpty()) { setResult(QJsonArray {}); return; } if (!sortedColumn.isEmpty()) { if (!torrentList[0].toMap().contains(sortedColumn)) throw APIError(APIErrorType::BadParams, tr("'sort' parameter is invalid")); const auto lessThan = [](const QVariant &left, const QVariant &right) -> bool { Q_ASSERT(left.userType() == right.userType()); switch (left.userType()) { case QMetaType::Bool: return left.value() < right.value(); case QMetaType::Double: return left.value() < right.value(); case QMetaType::Float: return left.value() < right.value(); case QMetaType::Int: return left.value() < right.value(); case QMetaType::LongLong: return left.value() < right.value(); case QMetaType::QString: return left.value() < right.value(); default: qWarning("Unhandled QVariant comparison, type: %d, name: %s" , left.userType(), left.metaType().name()); break; } return false; }; std::sort(torrentList.begin(), torrentList.end() , [reverse, &sortedColumn, &lessThan](const QVariant &torrent1, const QVariant &torrent2) { const QVariant value1 {torrent1.toMap().value(sortedColumn)}; const QVariant value2 {torrent2.toMap().value(sortedColumn)}; return reverse ? lessThan(value2, value1) : lessThan(value1, value2); }); } const int size = torrentList.size(); // normalize offset if (offset < 0) offset = size + offset; // normalize limit if (limit <= 0) limit = -1; // unlimited if ((limit > 0) || (offset > 0)) torrentList = torrentList.mid(offset, limit); setResult(QJsonArray::fromVariantList(torrentList)); } // Returns the properties for a torrent in JSON format. // The return value is a JSON-formatted dictionary. // The dictionary keys are: // - "time_elapsed": Torrent elapsed time // - "seeding_time": Torrent elapsed time while complete // - "eta": Torrent ETA // - "nb_connections": Torrent connection count // - "nb_connections_limit": Torrent connection count limit // - "total_downloaded": Total data uploaded for torrent // - "total_downloaded_session": Total data downloaded this session // - "total_uploaded": Total data uploaded for torrent // - "total_uploaded_session": Total data uploaded this session // - "dl_speed": Torrent download speed // - "dl_speed_avg": Torrent average download speed // - "up_speed": Torrent upload speed // - "up_speed_avg": Torrent average upload speed // - "dl_limit": Torrent download limit // - "up_limit": Torrent upload limit // - "total_wasted": Total data wasted for torrent // - "seeds": Torrent connected seeds // - "seeds_total": Torrent total number of seeds // - "peers": Torrent connected peers // - "peers_total": Torrent total number of peers // - "share_ratio": Torrent share ratio // - "popularity": Torrent popularity // - "reannounce": Torrent next reannounce time // - "total_size": Torrent total size // - "pieces_num": Torrent pieces count // - "piece_size": Torrent piece size // - "pieces_have": Torrent pieces have // - "created_by": Torrent creator // - "last_seen": Torrent last seen complete // - "addition_date": Torrent addition date // - "completion_date": Torrent completion date // - "creation_date": Torrent creation date // - "save_path": Torrent save path // - "download_path": Torrent download path // - "comment": Torrent comment // - "infohash_v1": Torrent v1 infohash (or empty string for v2 torrents) // - "infohash_v2": Torrent v2 infohash (or empty string for v1 torrents) // - "hash": Torrent TorrentID (infohashv1 for v1 torrents, truncated infohashv2 for v2/hybrid torrents) // - "name": Torrent name void TorrentsController::propertiesAction() { requireParams({u"hash"_s}); const auto id = BitTorrent::TorrentID::fromString(params()[u"hash"_s]); const BitTorrent::Torrent *const torrent = BitTorrent::Session::instance()->getTorrent(id); if (!torrent) throw APIError(APIErrorType::NotFound); const BitTorrent::InfoHash infoHash = torrent->infoHash(); const qlonglong totalDownload = torrent->totalDownload(); const qlonglong totalUpload = torrent->totalUpload(); const qlonglong dlDuration = torrent->activeTime() - torrent->finishedTime(); const qlonglong ulDuration = torrent->activeTime(); const int downloadLimit = torrent->downloadLimit(); const int uploadLimit = torrent->uploadLimit(); const qreal ratio = torrent->realRatio(); const qreal popularity = torrent->popularity(); const bool hasMetadata = torrent->hasMetadata(); const bool isPrivate = torrent->isPrivate(); const QJsonObject ret { {KEY_TORRENT_INFOHASHV1, infoHash.v1().toString()}, {KEY_TORRENT_INFOHASHV2, infoHash.v2().toString()}, {KEY_TORRENT_NAME, torrent->name()}, {KEY_TORRENT_ID, torrent->id().toString()}, {KEY_PROP_TIME_ELAPSED, torrent->activeTime()}, {KEY_PROP_SEEDING_TIME, torrent->finishedTime()}, {KEY_PROP_ETA, torrent->eta()}, {KEY_PROP_CONNECT_COUNT, torrent->connectionsCount()}, {KEY_PROP_CONNECT_COUNT_LIMIT, torrent->connectionsLimit()}, {KEY_PROP_DOWNLOADED, totalDownload}, {KEY_PROP_DOWNLOADED_SESSION, torrent->totalPayloadDownload()}, {KEY_PROP_UPLOADED, totalUpload}, {KEY_PROP_UPLOADED_SESSION, torrent->totalPayloadUpload()}, {KEY_PROP_DL_SPEED, torrent->downloadPayloadRate()}, {KEY_PROP_DL_SPEED_AVG, ((dlDuration > 0) ? (totalDownload / dlDuration) : -1)}, {KEY_PROP_UP_SPEED, torrent->uploadPayloadRate()}, {KEY_PROP_UP_SPEED_AVG, ((ulDuration > 0) ? (totalUpload / ulDuration) : -1)}, {KEY_PROP_DL_LIMIT, ((downloadLimit > 0) ? downloadLimit : -1)}, {KEY_PROP_UP_LIMIT, ((uploadLimit > 0) ? uploadLimit : -1)}, {KEY_PROP_WASTED, torrent->wastedSize()}, {KEY_PROP_SEEDS, torrent->seedsCount()}, {KEY_PROP_SEEDS_TOTAL, torrent->totalSeedsCount()}, {KEY_PROP_PEERS, torrent->leechsCount()}, {KEY_PROP_PEERS_TOTAL, torrent->totalLeechersCount()}, {KEY_PROP_RATIO, ((ratio >= BitTorrent::Torrent::MAX_RATIO) ? -1 : ratio)}, {KEY_PROP_POPULARITY, ((popularity >= BitTorrent::Torrent::MAX_RATIO) ? -1 : popularity)}, {KEY_PROP_REANNOUNCE, torrent->nextAnnounce()}, {KEY_PROP_TOTAL_SIZE, torrent->totalSize()}, {KEY_PROP_PIECES_NUM, torrent->piecesCount()}, {KEY_PROP_PIECE_SIZE, torrent->pieceLength()}, {KEY_PROP_PIECES_HAVE, torrent->piecesHave()}, {KEY_PROP_CREATED_BY, torrent->creator()}, {KEY_PROP_IS_PRIVATE, torrent->isPrivate()}, // used for maintaining backward compatibility {KEY_PROP_PRIVATE, (hasMetadata ? isPrivate : QJsonValue())}, {KEY_PROP_ADDITION_DATE, Utils::DateTime::toSecsSinceEpoch(torrent->addedTime())}, {KEY_PROP_LAST_SEEN, Utils::DateTime::toSecsSinceEpoch(torrent->lastSeenComplete())}, {KEY_PROP_COMPLETION_DATE, Utils::DateTime::toSecsSinceEpoch(torrent->completedTime())}, {KEY_PROP_CREATION_DATE, Utils::DateTime::toSecsSinceEpoch(torrent->creationDate())}, {KEY_PROP_SAVE_PATH, torrent->savePath().toString()}, {KEY_PROP_DOWNLOAD_PATH, torrent->downloadPath().toString()}, {KEY_PROP_COMMENT, torrent->comment()}, {KEY_PROP_HAS_METADATA, torrent->hasMetadata()}, {KEY_PROP_PROGRESS, torrent->progress()} }; setResult(ret); } // Returns the trackers for a torrent in JSON format. // The return value is a JSON-formatted list of dictionaries. // The dictionary keys are: // - "url": Tracker URL // - "status": Tracker status // - "tier": Tracker tier // - "num_peers": Number of peers this torrent is currently connected to // - "num_seeds": Number of peers that have the whole file // - "num_leeches": Number of peers that are still downloading // - "num_downloaded": Tracker downloaded count // - "msg": Tracker message (last) void TorrentsController::trackersAction() { requireParams({u"hash"_s}); const auto id = BitTorrent::TorrentID::fromString(params()[u"hash"_s]); const BitTorrent::Torrent *const torrent = BitTorrent::Session::instance()->getTorrent(id); if (!torrent) throw APIError(APIErrorType::NotFound); QJsonArray trackersList = getStickyTrackers(torrent); // merge QJsonArray for (const auto &tracker : asConst(getTrackers(torrent))) trackersList.append(tracker); setResult(trackersList); } // Returns the web seeds for a torrent in JSON format. // The return value is a JSON-formatted list of dictionaries. // The dictionary keys are: // - "url": Web seed URL void TorrentsController::webseedsAction() { requireParams({u"hash"_s}); const auto id = BitTorrent::TorrentID::fromString(params()[u"hash"_s]); BitTorrent::Torrent *const torrent = BitTorrent::Session::instance()->getTorrent(id); if (!torrent) throw APIError(APIErrorType::NotFound); QJsonArray webSeedList; for (const QUrl &webseed : asConst(torrent->urlSeeds())) { webSeedList.append(QJsonObject { {KEY_WEBSEED_URL, webseed.toString()} }); } setResult(webSeedList); } void TorrentsController::addWebSeedsAction() { requireParams({u"hash"_s, u"urls"_s}); const QStringList paramUrls = params()[u"urls"_s].split(u'|', Qt::SkipEmptyParts); const auto id = BitTorrent::TorrentID::fromString(params()[u"hash"_s]); BitTorrent::Torrent *const torrent = BitTorrent::Session::instance()->getTorrent(id); if (!torrent) throw APIError(APIErrorType::NotFound); QList urls; urls.reserve(paramUrls.size()); for (const QString &urlStr : paramUrls) { const auto result = validateWebSeedUrl(urlStr); if (!result) throw APIError(APIErrorType::BadParams, result.error()); urls << result.value(); } torrent->addUrlSeeds(urls); setResult(QString()); } void TorrentsController::editWebSeedAction() { requireParams({u"hash"_s, u"origUrl"_s, u"newUrl"_s}); const auto id = BitTorrent::TorrentID::fromString(params()[u"hash"_s]); const QString origUrlStr = params()[u"origUrl"_s]; const QString newUrlStr = params()[u"newUrl"_s]; BitTorrent::Torrent *const torrent = BitTorrent::Session::instance()->getTorrent(id); if (!torrent) throw APIError(APIErrorType::NotFound); const auto origUrlResult = validateWebSeedUrl(origUrlStr); if (!origUrlResult) throw APIError(APIErrorType::BadParams, origUrlResult.error()); const QUrl origUrl = origUrlResult.value(); const auto newUrlResult = validateWebSeedUrl(newUrlStr); if (!newUrlResult) throw APIError(APIErrorType::BadParams, newUrlResult.error()); const QUrl newUrl = newUrlResult.value(); if (newUrl != origUrl) { if (!torrent->urlSeeds().contains(origUrl)) throw APIError(APIErrorType::Conflict, tr("\"%1\" is not an existing URL").arg(origUrl.toString())); torrent->removeUrlSeeds({origUrl}); torrent->addUrlSeeds({newUrl}); } setResult(QString()); } void TorrentsController::removeWebSeedsAction() { requireParams({u"hash"_s, u"urls"_s}); const QStringList paramUrls = params()[u"urls"_s].split(u'|', Qt::SkipEmptyParts); const auto id = BitTorrent::TorrentID::fromString(params()[u"hash"_s]); BitTorrent::Torrent *const torrent = BitTorrent::Session::instance()->getTorrent(id); if (!torrent) throw APIError(APIErrorType::NotFound); QList urls; urls.reserve(paramUrls.size()); for (const QString &urlStr : paramUrls) { const auto result = validateWebSeedUrl(urlStr); if (!result) throw APIError(APIErrorType::BadParams, result.error()); urls << result.value(); } torrent->removeUrlSeeds(urls); setResult(QString()); } // Returns the files in a torrent in JSON format. // The return value is a JSON-formatted list of dictionaries. // The dictionary keys are: // - "index": File index // - "name": File name // - "size": File size // - "progress": File progress // - "priority": File priority // - "is_seed": Flag indicating if torrent is seeding/complete // - "piece_range": Piece index range, the first number is the starting piece index // and the second number is the ending piece index (inclusive) void TorrentsController::filesAction() { requireParams({u"hash"_s}); const auto id = BitTorrent::TorrentID::fromString(params()[u"hash"_s]); const BitTorrent::Torrent *const torrent = BitTorrent::Session::instance()->getTorrent(id); if (!torrent) throw APIError(APIErrorType::NotFound); if (!torrent->hasMetadata()) return setResult(QJsonArray{}); QList fileIndexes; const auto idxIt = params().constFind(u"indexes"_s); if (idxIt != params().cend()) { const int filesCount = torrent->filesCount(); const QStringList indexStrings = idxIt.value().split(u'|'); fileIndexes.reserve(indexStrings.size()); std::transform(indexStrings.cbegin(), indexStrings.cend(), std::back_inserter(fileIndexes) , [&filesCount](const QString &indexString) -> int { bool ok = false; const int index = indexString.toInt(&ok); if (!ok || (index < 0)) throw APIError(APIErrorType::Conflict, tr("\"%1\" is not a valid file index.").arg(indexString)); if (index >= filesCount) throw APIError(APIErrorType::Conflict, tr("Index %1 is out of bounds.").arg(indexString)); return index; }); } QJsonArray fileList = getFiles(torrent, fileIndexes); if (!fileList.isEmpty()) { QJsonObject firstFile = fileList[0].toObject(); firstFile[KEY_FILE_IS_SEED] = torrent->isFinished(); fileList[0] = firstFile; } setResult(fileList); } // Returns an array of hashes (of each pieces respectively) for a torrent in JSON format. // The return value is a JSON-formatted array of strings (hex strings). void TorrentsController::pieceHashesAction() { requireParams({u"hash"_s}); const auto id = BitTorrent::TorrentID::fromString(params()[u"hash"_s]); BitTorrent::Torrent *const torrent = BitTorrent::Session::instance()->getTorrent(id); if (!torrent) throw APIError(APIErrorType::NotFound); QJsonArray pieceHashes; if (torrent->hasMetadata()) { const QList hashes = torrent->info().pieceHashes(); for (const QByteArray &hash : hashes) pieceHashes.append(QString::fromLatin1(hash.toHex())); } setResult(pieceHashes); } // Returns an array of states (of each pieces respectively) for a torrent in JSON format. // The return value is a JSON-formatted array of ints. // 0: piece not downloaded // 1: piece requested or downloading // 2: piece already downloaded void TorrentsController::pieceStatesAction() { requireParams({u"hash"_s}); const auto id = BitTorrent::TorrentID::fromString(params()[u"hash"_s]); BitTorrent::Torrent *const torrent = BitTorrent::Session::instance()->getTorrent(id); if (!torrent) throw APIError(APIErrorType::NotFound); QJsonArray pieceStates; const QBitArray states = torrent->pieces(); for (int i = 0; i < states.size(); ++i) pieceStates.append(static_cast(states[i]) * 2); const QBitArray dlstates = torrent->fetchDownloadingPieces().takeResult(); for (int i = 0; i < states.size(); ++i) { if (dlstates[i]) pieceStates[i] = 1; } setResult(pieceStates); } void TorrentsController::addAction() { const QStringList urls = params()[u"urls"_s].split(u'\n', Qt::SkipEmptyParts); const bool skipChecking = parseBool(params()[u"skip_checking"_s]).value_or(false); const bool seqDownload = parseBool(params()[u"sequentialDownload"_s]).value_or(false); const bool firstLastPiece = parseBool(params()[u"firstLastPiecePrio"_s]).value_or(false); const bool addForced = parseBool(params()[u"forced"_s]).value_or(false); const std::optional addToQueueTop = parseBool(params()[u"addToTopOfQueue"_s]); const std::optional addStopped = parseBool(params()[u"stopped"_s]); const QString savepath = params()[u"savepath"_s].trimmed(); const QString downloadPath = params()[u"downloadPath"_s].trimmed(); const std::optional useDownloadPath = parseBool(params()[u"useDownloadPath"_s]); const QString category = params()[u"category"_s]; const QStringList tags = params()[u"tags"_s].split(u',', Qt::SkipEmptyParts); const QString torrentName = params()[u"rename"_s].trimmed(); const int upLimit = parseInt(params()[u"upLimit"_s]).value_or(-1); const int dlLimit = parseInt(params()[u"dlLimit"_s]).value_or(-1); const double ratioLimit = parseDouble(params()[u"ratioLimit"_s]).value_or(BitTorrent::Torrent::USE_GLOBAL_RATIO); const int seedingTimeLimit = parseInt(params()[u"seedingTimeLimit"_s]).value_or(BitTorrent::Torrent::USE_GLOBAL_SEEDING_TIME); const int inactiveSeedingTimeLimit = parseInt(params()[u"inactiveSeedingTimeLimit"_s]).value_or(BitTorrent::Torrent::USE_GLOBAL_INACTIVE_SEEDING_TIME); const BitTorrent::ShareLimitAction shareLimitAction = Utils::String::toEnum(params()[u"shareLimitAction"_s], BitTorrent::ShareLimitAction::Default); const std::optional autoTMM = parseBool(params()[u"autoTMM"_s]); const QString stopConditionParam = params()[u"stopCondition"_s]; const std::optional stopCondition = (!stopConditionParam.isEmpty() ? Utils::String::toEnum(stopConditionParam, BitTorrent::Torrent::StopCondition::None) : std::optional {}); const QString contentLayoutParam = params()[u"contentLayout"_s]; const std::optional contentLayout = (!contentLayoutParam.isEmpty() ? Utils::String::toEnum(contentLayoutParam, BitTorrent::TorrentContentLayout::Original) : std::optional {}); const DataMap &torrents = data(); QList filePriorities; const QStringList filePrioritiesParam = params()[u"filePriorities"_s].split(u',', Qt::SkipEmptyParts); if (!filePrioritiesParam.isEmpty()) { if (urls.size() > 1) throw APIError(APIErrorType::BadParams, tr("Cannot specify filePriorities when adding multiple torrents")); if (!torrents.isEmpty()) throw APIError(APIErrorType::BadParams, tr("Cannot specify filePriorities when uploading torrent files")); filePriorities.reserve(filePrioritiesParam.size()); for (const QString &priorityStr : filePrioritiesParam) { const nonstd::expected result = parseDownloadPriority(priorityStr); if (!result) throw APIError(APIErrorType::BadParams, result.error()); filePriorities << result.value(); } } BitTorrent::AddTorrentParams addTorrentParams { // TODO: Check if destination actually exists .name = torrentName, .category = category, .tags = {tags.cbegin(), tags.cend()}, .savePath = Path(savepath), .useDownloadPath = useDownloadPath, .downloadPath = Path(downloadPath), .sequential = seqDownload, .firstLastPiecePriority = firstLastPiece, .addForced = addForced, .addToQueueTop = addToQueueTop, .addStopped = addStopped, .stopCondition = stopCondition, .filePaths = {}, .filePriorities = {}, .skipChecking = skipChecking, .contentLayout = contentLayout, .useAutoTMM = autoTMM, .uploadLimit = upLimit, .downloadLimit = dlLimit, .seedingTimeLimit = seedingTimeLimit, .inactiveSeedingTimeLimit = inactiveSeedingTimeLimit, .ratioLimit = ratioLimit, .shareLimitAction = shareLimitAction, .sslParameters = { .certificate = QSslCertificate(params()[KEY_PROP_SSL_CERTIFICATE].toLatin1()), .privateKey = Utils::SSLKey::load(params()[KEY_PROP_SSL_PRIVATEKEY].toLatin1()), .dhParams = params()[KEY_PROP_SSL_DHPARAMS].toLatin1() } }; bool partialSuccess = false; for (QString url : urls) { url = url.trimmed(); if (url.isEmpty()) continue; BitTorrent::InfoHash infoHash; if (const auto iter = m_torrentSourceCache.constFind(url); iter != m_torrentSourceCache.constEnd()) infoHash = iter.value(); else if (const auto sourceTorrentDescr = BitTorrent::TorrentDescriptor::parse(url)) infoHash = sourceTorrentDescr.value().infoHash(); if (const BitTorrent::TorrentDescriptor &torrentDescr = m_torrentMetadataCache.value(infoHash); torrentDescr.info().has_value()) { if (!filePriorities.isEmpty()) { const BitTorrent::TorrentInfo &info = torrentDescr.info().value(); if (filePriorities.size() != info.filesCount()) throw APIError(APIErrorType::BadParams, tr("Length of filePriorities must equal number of files in torrent")); addTorrentParams.filePriorities = filePriorities; } partialSuccess |= BitTorrent::Session::instance()->addTorrent(torrentDescr, addTorrentParams); } else { if (!filePriorities.isEmpty()) throw APIError(APIErrorType::BadParams, tr("filePriorities may only be specified when metadata has already been fetched")); partialSuccess |= app()->addTorrentManager()->addTorrent(url, addTorrentParams); } m_torrentSourceCache.remove(url); m_torrentMetadataCache.remove(infoHash); } // process uploaded .torrent files for (auto it = torrents.constBegin(); it != torrents.constEnd(); ++it) { if (const auto loadResult = BitTorrent::TorrentDescriptor::load(it.value())) { partialSuccess |= BitTorrent::Session::instance()->addTorrent(loadResult.value(), addTorrentParams); } else { throw APIError(APIErrorType::BadData, tr("Error: '%1' is not a valid torrent file.").arg(it.key())); } } if (partialSuccess) setResult(u"Ok."_s); else setResult(u"Fails."_s); } void TorrentsController::addTrackersAction() { requireParams({u"hash"_s, u"urls"_s}); const QList trackers = BitTorrent::parseTrackerEntries(params()[u"urls"_s]); const QStringList idStrings = params()[u"hash"_s].split(u'|'); applyToTorrents(idStrings, [&trackers](BitTorrent::Torrent *const torrent) { torrent->addTrackers(trackers); }); } void TorrentsController::editTrackerAction() { requireParams({u"hash"_s, u"origUrl"_s, u"newUrl"_s}); const auto id = BitTorrent::TorrentID::fromString(params()[u"hash"_s]); const QString origUrl = params()[u"origUrl"_s]; const QString newUrl = params()[u"newUrl"_s]; BitTorrent::Torrent *const torrent = BitTorrent::Session::instance()->getTorrent(id); if (!torrent) throw APIError(APIErrorType::NotFound); const QUrl origTrackerUrl {origUrl}; const QUrl newTrackerUrl {newUrl}; if (origTrackerUrl == newTrackerUrl) return; if (!newTrackerUrl.isValid()) throw APIError(APIErrorType::BadParams, u"New tracker URL is invalid"_s); const QList currentTrackers = torrent->trackers(); QList entries; entries.reserve(currentTrackers.size()); bool match = false; for (const BitTorrent::TrackerEntryStatus &tracker : currentTrackers) { const QUrl trackerUrl {tracker.url}; if (trackerUrl == newTrackerUrl) throw APIError(APIErrorType::Conflict, u"New tracker URL already exists"_s); BitTorrent::TrackerEntry entry { .url = tracker.url, .tier = tracker.tier }; if (trackerUrl == origTrackerUrl) { match = true; entry.url = newTrackerUrl.toString(); } entries.append(entry); } if (!match) throw APIError(APIErrorType::Conflict, u"Tracker not found"_s); torrent->replaceTrackers(entries); if (!torrent->isStopped()) torrent->forceReannounce(); setResult(QString()); } void TorrentsController::removeTrackersAction() { requireParams({u"hash"_s, u"urls"_s}); QString hash = params()[u"hash"_s]; if (hash == u"*"_s) hash = u"all"_s; const QStringList idStrings = hash.split(u'|', Qt::SkipEmptyParts); const QStringList urlsParam = params()[u"urls"_s].split(u'|', Qt::SkipEmptyParts); QStringList urls; urls.reserve(urlsParam.size()); for (const QString &urlStr : urlsParam) urls << QUrl::fromPercentEncoding(urlStr.toLatin1()); applyToTorrents(idStrings, [&urls](BitTorrent::Torrent *const torrent) { torrent->removeTrackers(urls); }); } void TorrentsController::addPeersAction() { requireParams({u"hashes"_s, u"peers"_s}); const QStringList hashes = params()[u"hashes"_s].split(u'|'); const QStringList peers = params()[u"peers"_s].split(u'|'); QList peerList; peerList.reserve(peers.size()); for (const QString &peer : peers) { const BitTorrent::PeerAddress addr = BitTorrent::PeerAddress::parse(peer.trimmed()); if (!addr.ip.isNull()) peerList.append(addr); } if (peerList.isEmpty()) throw APIError(APIErrorType::BadParams, u"No valid peers were specified"_s); QJsonObject results; applyToTorrents(hashes, [peers, peerList, &results](BitTorrent::Torrent *const torrent) { const int peersAdded = std::count_if(peerList.cbegin(), peerList.cend(), [torrent](const BitTorrent::PeerAddress &peer) { return torrent->connectPeer(peer); }); results[torrent->id().toString()] = QJsonObject { {u"added"_s, peersAdded}, {u"failed"_s, (peers.size() - peersAdded)} }; }); setResult(results); } void TorrentsController::stopAction() { requireParams({u"hashes"_s}); const QStringList hashes = params()[u"hashes"_s].split(u'|'); applyToTorrents(hashes, [](BitTorrent::Torrent *const torrent) { torrent->stop(); }); setResult(QString()); } void TorrentsController::startAction() { requireParams({u"hashes"_s}); const QStringList idStrings = params()[u"hashes"_s].split(u'|'); applyToTorrents(idStrings, [](BitTorrent::Torrent *const torrent) { torrent->start(); }); setResult(QString()); } void TorrentsController::filePrioAction() { requireParams({u"hash"_s, u"id"_s, u"priority"_s}); const auto id = BitTorrent::TorrentID::fromString(params()[u"hash"_s]); const nonstd::expected result = parseDownloadPriority(params()[u"priority"_s]); if (!result) throw APIError(APIErrorType::BadParams, result.error()); const BitTorrent::DownloadPriority priority = result.value(); BitTorrent::Torrent *const torrent = BitTorrent::Session::instance()->getTorrent(id); if (!torrent) throw APIError(APIErrorType::NotFound); if (!torrent->hasMetadata()) throw APIError(APIErrorType::Conflict, tr("Torrent's metadata has not yet downloaded")); const int filesCount = torrent->filesCount(); QList priorities = torrent->filePriorities(); bool priorityChanged = false; for (const QString &fileID : params()[u"id"_s].split(u'|')) { bool ok = false; const int id = fileID.toInt(&ok); if (!ok) throw APIError(APIErrorType::BadParams, tr("File IDs must be integers")); if ((id < 0) || (id >= filesCount)) throw APIError(APIErrorType::Conflict, tr("File ID is not valid")); if (priorities[id] != priority) { priorities[id] = priority; priorityChanged = true; } } if (priorityChanged) torrent->prioritizeFiles(priorities); setResult(QString()); } void TorrentsController::uploadLimitAction() { requireParams({u"hashes"_s}); const QStringList idList {params()[u"hashes"_s].split(u'|')}; QJsonObject map; for (const QString &id : idList) { int limit = -1; const BitTorrent::Torrent *const torrent = BitTorrent::Session::instance()->getTorrent(BitTorrent::TorrentID::fromString(id)); if (torrent) limit = torrent->uploadLimit(); map[id] = limit; } setResult(map); } void TorrentsController::downloadLimitAction() { requireParams({u"hashes"_s}); const QStringList idList {params()[u"hashes"_s].split(u'|')}; QJsonObject map; for (const QString &id : idList) { int limit = -1; const BitTorrent::Torrent *const torrent = BitTorrent::Session::instance()->getTorrent(BitTorrent::TorrentID::fromString(id)); if (torrent) limit = torrent->downloadLimit(); map[id] = limit; } setResult(map); } void TorrentsController::setUploadLimitAction() { requireParams({u"hashes"_s, u"limit"_s}); qlonglong limit = params()[u"limit"_s].toLongLong(); if (limit == 0) limit = -1; const QStringList hashes {params()[u"hashes"_s].split(u'|')}; applyToTorrents(hashes, [limit](BitTorrent::Torrent *const torrent) { torrent->setUploadLimit(limit); }); setResult(QString()); } void TorrentsController::setDownloadLimitAction() { requireParams({u"hashes"_s, u"limit"_s}); qlonglong limit = params()[u"limit"_s].toLongLong(); if (limit == 0) limit = -1; const QStringList hashes {params()[u"hashes"_s].split(u'|')}; applyToTorrents(hashes, [limit](BitTorrent::Torrent *const torrent) { torrent->setDownloadLimit(limit); }); setResult(QString()); } void TorrentsController::setShareLimitsAction() { requireParams({u"hashes"_s, u"ratioLimit"_s, u"seedingTimeLimit"_s, u"inactiveSeedingTimeLimit"_s}); const qreal ratioLimit = params()[u"ratioLimit"_s].toDouble(); const qlonglong seedingTimeLimit = params()[u"seedingTimeLimit"_s].toLongLong(); const qlonglong inactiveSeedingTimeLimit = params()[u"inactiveSeedingTimeLimit"_s].toLongLong(); const QStringList hashes = params()[u"hashes"_s].split(u'|'); applyToTorrents(hashes, [ratioLimit, seedingTimeLimit, inactiveSeedingTimeLimit](BitTorrent::Torrent *const torrent) { torrent->setRatioLimit(ratioLimit); torrent->setSeedingTimeLimit(seedingTimeLimit); torrent->setInactiveSeedingTimeLimit(inactiveSeedingTimeLimit); }); setResult(QString()); } void TorrentsController::toggleSequentialDownloadAction() { requireParams({u"hashes"_s}); const QStringList hashes {params()[u"hashes"_s].split(u'|')}; applyToTorrents(hashes, [](BitTorrent::Torrent *const torrent) { torrent->toggleSequentialDownload(); }); setResult(QString()); } void TorrentsController::toggleFirstLastPiecePrioAction() { requireParams({u"hashes"_s}); const QStringList hashes {params()[u"hashes"_s].split(u'|')}; applyToTorrents(hashes, [](BitTorrent::Torrent *const torrent) { torrent->toggleFirstLastPiecePriority(); }); setResult(QString()); } void TorrentsController::setSuperSeedingAction() { requireParams({u"hashes"_s, u"value"_s}); const bool value {parseBool(params()[u"value"_s]).value_or(false)}; const QStringList hashes {params()[u"hashes"_s].split(u'|')}; applyToTorrents(hashes, [value](BitTorrent::Torrent *const torrent) { torrent->setSuperSeeding(value); }); setResult(QString()); } void TorrentsController::setForceStartAction() { requireParams({u"hashes"_s, u"value"_s}); const bool value {parseBool(params()[u"value"_s]).value_or(false)}; const QStringList hashes {params()[u"hashes"_s].split(u'|')}; applyToTorrents(hashes, [value](BitTorrent::Torrent *const torrent) { torrent->start(value ? BitTorrent::TorrentOperatingMode::Forced : BitTorrent::TorrentOperatingMode::AutoManaged); }); setResult(QString()); } void TorrentsController::deleteAction() { requireParams({u"hashes"_s, u"deleteFiles"_s}); const QStringList hashes {params()[u"hashes"_s].split(u'|')}; const BitTorrent::TorrentRemoveOption deleteOption = parseBool(params()[u"deleteFiles"_s]).value_or(false) ? BitTorrent::TorrentRemoveOption::RemoveContent : BitTorrent::TorrentRemoveOption::KeepContent; applyToTorrents(hashes, [deleteOption](const BitTorrent::Torrent *torrent) { BitTorrent::Session::instance()->removeTorrent(torrent->id(), deleteOption); }); setResult(QString()); } void TorrentsController::increasePrioAction() { requireParams({u"hashes"_s}); if (!BitTorrent::Session::instance()->isQueueingSystemEnabled()) throw APIError(APIErrorType::Conflict, tr("Torrent queueing must be enabled")); const QStringList hashes {params()[u"hashes"_s].split(u'|')}; BitTorrent::Session::instance()->increaseTorrentsQueuePos(toTorrentIDs(hashes)); setResult(QString()); } void TorrentsController::decreasePrioAction() { requireParams({u"hashes"_s}); if (!BitTorrent::Session::instance()->isQueueingSystemEnabled()) throw APIError(APIErrorType::Conflict, tr("Torrent queueing must be enabled")); const QStringList hashes {params()[u"hashes"_s].split(u'|')}; BitTorrent::Session::instance()->decreaseTorrentsQueuePos(toTorrentIDs(hashes)); setResult(QString()); } void TorrentsController::topPrioAction() { requireParams({u"hashes"_s}); if (!BitTorrent::Session::instance()->isQueueingSystemEnabled()) throw APIError(APIErrorType::Conflict, tr("Torrent queueing must be enabled")); const QStringList hashes {params()[u"hashes"_s].split(u'|')}; BitTorrent::Session::instance()->topTorrentsQueuePos(toTorrentIDs(hashes)); setResult(QString()); } void TorrentsController::bottomPrioAction() { requireParams({u"hashes"_s}); if (!BitTorrent::Session::instance()->isQueueingSystemEnabled()) throw APIError(APIErrorType::Conflict, tr("Torrent queueing must be enabled")); const QStringList hashes {params()[u"hashes"_s].split(u'|')}; BitTorrent::Session::instance()->bottomTorrentsQueuePos(toTorrentIDs(hashes)); setResult(QString()); } void TorrentsController::setLocationAction() { requireParams({u"hashes"_s, u"location"_s}); const QStringList hashes {params()[u"hashes"_s].split(u'|')}; const Path newLocation {params()[u"location"_s].trimmed()}; if (newLocation.isEmpty()) throw APIError(APIErrorType::BadParams, tr("Save path cannot be empty")); // try to create the location if it does not exist if (!Utils::Fs::mkpath(newLocation)) throw APIError(APIErrorType::Conflict, tr("Cannot make save path")); applyToTorrents(hashes, [newLocation](BitTorrent::Torrent *const torrent) { LogMsg(tr("WebUI Set location: moving \"%1\", from \"%2\" to \"%3\"") .arg(torrent->name(), torrent->savePath().toString(), newLocation.toString())); torrent->setAutoTMMEnabled(false); torrent->setSavePath(newLocation); }); setResult(QString()); } void TorrentsController::setSavePathAction() { requireParams({u"id"_s, u"path"_s}); const QStringList ids {params()[u"id"_s].split(u'|')}; const Path newPath {params()[u"path"_s]}; if (newPath.isEmpty()) throw APIError(APIErrorType::BadParams, tr("Save path cannot be empty")); // try to create the directory if it does not exist if (!Utils::Fs::mkpath(newPath)) throw APIError(APIErrorType::Conflict, tr("Cannot create target directory")); // check permissions if (!Utils::Fs::isWritable(newPath)) throw APIError(APIErrorType::AccessDenied, tr("Cannot write to directory")); applyToTorrents(ids, [&newPath](BitTorrent::Torrent *const torrent) { if (!torrent->isAutoTMMEnabled()) torrent->setSavePath(newPath); }); setResult(QString()); } void TorrentsController::setDownloadPathAction() { requireParams({u"id"_s, u"path"_s}); const QStringList ids {params()[u"id"_s].split(u'|')}; const Path newPath {params()[u"path"_s]}; if (!newPath.isEmpty()) { // try to create the directory if it does not exist if (!Utils::Fs::mkpath(newPath)) throw APIError(APIErrorType::Conflict, tr("Cannot create target directory")); // check permissions if (!Utils::Fs::isWritable(newPath)) throw APIError(APIErrorType::AccessDenied, tr("Cannot write to directory")); } applyToTorrents(ids, [&newPath](BitTorrent::Torrent *const torrent) { if (!torrent->isAutoTMMEnabled()) torrent->setDownloadPath(newPath); }); setResult(QString()); } void TorrentsController::renameAction() { requireParams({u"hash"_s, u"name"_s}); const auto id = BitTorrent::TorrentID::fromString(params()[u"hash"_s]); QString name = params()[u"name"_s].trimmed(); if (name.isEmpty()) throw APIError(APIErrorType::Conflict, tr("Incorrect torrent name")); BitTorrent::Torrent *const torrent = BitTorrent::Session::instance()->getTorrent(id); if (!torrent) throw APIError(APIErrorType::NotFound); name.replace(QRegularExpression(u"\r?\n|\r"_s), u" "_s); torrent->setName(name); setResult(QString()); } void TorrentsController::setAutoManagementAction() { requireParams({u"hashes"_s, u"enable"_s}); const QStringList hashes {params()[u"hashes"_s].split(u'|')}; const bool isEnabled {parseBool(params()[u"enable"_s]).value_or(false)}; applyToTorrents(hashes, [isEnabled](BitTorrent::Torrent *const torrent) { torrent->setAutoTMMEnabled(isEnabled); }); setResult(QString()); } void TorrentsController::recheckAction() { requireParams({u"hashes"_s}); const QStringList hashes {params()[u"hashes"_s].split(u'|')}; applyToTorrents(hashes, [](BitTorrent::Torrent *const torrent) { torrent->forceRecheck(); }); setResult(QString()); } void TorrentsController::reannounceAction() { requireParams({u"hashes"_s}); const QStringList hashes {params()[u"hashes"_s].split(u'|')}; applyToTorrents(hashes, [](BitTorrent::Torrent *const torrent) { torrent->forceReannounce(); }); setResult(QString()); } void TorrentsController::setCategoryAction() { requireParams({u"hashes"_s, u"category"_s}); const QStringList hashes {params()[u"hashes"_s].split(u'|')}; const QString category {params()[u"category"_s]}; applyToTorrents(hashes, [category](BitTorrent::Torrent *const torrent) { if (!torrent->setCategory(category)) throw APIError(APIErrorType::Conflict, tr("Incorrect category name")); }); setResult(QString()); } void TorrentsController::createCategoryAction() { requireParams({u"category"_s}); const QString category = params()[u"category"_s]; if (category.isEmpty()) throw APIError(APIErrorType::BadParams, tr("Category cannot be empty")); if (!BitTorrent::Session::isValidCategoryName(category)) throw APIError(APIErrorType::Conflict, tr("Incorrect category name")); const Path savePath {params()[u"savePath"_s]}; const auto useDownloadPath = parseBool(params()[u"downloadPathEnabled"_s]); BitTorrent::CategoryOptions categoryOptions; categoryOptions.savePath = savePath; if (useDownloadPath.has_value()) { const Path downloadPath {params()[u"downloadPath"_s]}; categoryOptions.downloadPath = {useDownloadPath.value(), downloadPath}; } if (!BitTorrent::Session::instance()->addCategory(category, categoryOptions)) throw APIError(APIErrorType::Conflict, tr("Unable to create category")); setResult(QString()); } void TorrentsController::editCategoryAction() { requireParams({u"category"_s, u"savePath"_s}); const QString category = params()[u"category"_s]; if (category.isEmpty()) throw APIError(APIErrorType::BadParams, tr("Category cannot be empty")); const Path savePath {params()[u"savePath"_s]}; const auto useDownloadPath = parseBool(params()[u"downloadPathEnabled"_s]); BitTorrent::CategoryOptions categoryOptions; categoryOptions.savePath = savePath; if (useDownloadPath.has_value()) { const Path downloadPath {params()[u"downloadPath"_s]}; categoryOptions.downloadPath = {useDownloadPath.value(), downloadPath}; } if (!BitTorrent::Session::instance()->editCategory(category, categoryOptions)) throw APIError(APIErrorType::Conflict, tr("Unable to edit category")); setResult(QString()); } void TorrentsController::removeCategoriesAction() { requireParams({u"categories"_s}); const QStringList categories {params()[u"categories"_s].split(u'\n')}; for (const QString &category : categories) BitTorrent::Session::instance()->removeCategory(category); setResult(QString()); } void TorrentsController::categoriesAction() { const auto *session = BitTorrent::Session::instance(); QJsonObject categories; const QStringList categoriesList = session->categories(); for (const auto &categoryName : categoriesList) { const BitTorrent::CategoryOptions categoryOptions = session->categoryOptions(categoryName); QJsonObject category = categoryOptions.toJSON(); // adjust it to be compatible with existing WebAPI category[u"savePath"_s] = category.take(u"save_path"_s); category.insert(u"name"_s, categoryName); categories[categoryName] = category; } setResult(categories); } void TorrentsController::addTagsAction() { requireParams({u"hashes"_s, u"tags"_s}); const QStringList hashes {params()[u"hashes"_s].split(u'|')}; const QStringList tags {params()[u"tags"_s].split(u',', Qt::SkipEmptyParts)}; for (const QString &tagStr : tags) { applyToTorrents(hashes, [&tagStr](BitTorrent::Torrent *const torrent) { torrent->addTag(Tag(tagStr)); }); } setResult(QString()); } void TorrentsController::setTagsAction() { requireParams({u"hashes"_s, u"tags"_s}); const QStringList hashes {params()[u"hashes"_s].split(u'|', Qt::SkipEmptyParts)}; const QStringList tags {params()[u"tags"_s].split(u',', Qt::SkipEmptyParts)}; const TagSet newTags {tags.begin(), tags.end()}; applyToTorrents(hashes, [&newTags](BitTorrent::Torrent *const torrent) { TagSet tmpTags {newTags}; for (const Tag &tag : asConst(torrent->tags())) { if (tmpTags.erase(tag) == 0) torrent->removeTag(tag); } for (const Tag &tag : tmpTags) torrent->addTag(tag); }); } void TorrentsController::removeTagsAction() { requireParams({u"hashes"_s}); const QStringList hashes {params()[u"hashes"_s].split(u'|')}; const QStringList tags {params()[u"tags"_s].split(u',', Qt::SkipEmptyParts)}; for (const QString &tagStr : tags) { applyToTorrents(hashes, [&tagStr](BitTorrent::Torrent *const torrent) { torrent->removeTag(Tag(tagStr)); }); } if (tags.isEmpty()) { applyToTorrents(hashes, [](BitTorrent::Torrent *const torrent) { torrent->removeAllTags(); }); } setResult(QString()); } void TorrentsController::createTagsAction() { requireParams({u"tags"_s}); const QStringList tags {params()[u"tags"_s].split(u',', Qt::SkipEmptyParts)}; for (const QString &tagStr : tags) BitTorrent::Session::instance()->addTag(Tag(tagStr)); setResult(QString()); } void TorrentsController::deleteTagsAction() { requireParams({u"tags"_s}); const QStringList tags {params()[u"tags"_s].split(u',', Qt::SkipEmptyParts)}; for (const QString &tagStr : tags) BitTorrent::Session::instance()->removeTag(Tag(tagStr)); setResult(QString()); } void TorrentsController::tagsAction() { QJsonArray result; for (const Tag &tag : asConst(BitTorrent::Session::instance()->tags())) result << tag.toString(); setResult(result); } void TorrentsController::renameFileAction() { requireParams({u"hash"_s, u"oldPath"_s, u"newPath"_s}); const auto id = BitTorrent::TorrentID::fromString(params()[u"hash"_s]); BitTorrent::Torrent *const torrent = BitTorrent::Session::instance()->getTorrent(id); if (!torrent) throw APIError(APIErrorType::NotFound); const Path oldPath {params()[u"oldPath"_s]}; const Path newPath {params()[u"newPath"_s]}; try { torrent->renameFile(oldPath, newPath); } catch (const RuntimeError &error) { throw APIError(APIErrorType::Conflict, error.message()); } setResult(QString()); } void TorrentsController::renameFolderAction() { requireParams({u"hash"_s, u"oldPath"_s, u"newPath"_s}); const auto id = BitTorrent::TorrentID::fromString(params()[u"hash"_s]); BitTorrent::Torrent *const torrent = BitTorrent::Session::instance()->getTorrent(id); if (!torrent) throw APIError(APIErrorType::NotFound); const Path oldPath {params()[u"oldPath"_s]}; const Path newPath {params()[u"newPath"_s]}; try { torrent->renameFolder(oldPath, newPath); } catch (const RuntimeError &error) { throw APIError(APIErrorType::Conflict, error.message()); } setResult(QString()); } void TorrentsController::exportAction() { requireParams({u"hash"_s}); const auto id = BitTorrent::TorrentID::fromString(params()[u"hash"_s]); const BitTorrent::Torrent *const torrent = BitTorrent::Session::instance()->getTorrent(id); if (!torrent) throw APIError(APIErrorType::NotFound); const nonstd::expected result = torrent->exportToBuffer(); if (!result) throw APIError(APIErrorType::Conflict, tr("Unable to export torrent file. Error: %1").arg(result.error())); setResult(result.value(), u"application/x-bittorrent"_s, (id.toString() + u".torrent")); } void TorrentsController::SSLParametersAction() { requireParams({u"hash"_s}); const auto id = BitTorrent::TorrentID::fromString(params()[u"hash"_s]); const BitTorrent::Torrent *torrent = BitTorrent::Session::instance()->getTorrent(id); if (!torrent) throw APIError(APIErrorType::NotFound); const BitTorrent::SSLParameters sslParams = torrent->getSSLParameters(); const QJsonObject ret { {KEY_PROP_SSL_CERTIFICATE, QString::fromLatin1(sslParams.certificate.toPem())}, {KEY_PROP_SSL_PRIVATEKEY, QString::fromLatin1(sslParams.privateKey.toPem())}, {KEY_PROP_SSL_DHPARAMS, QString::fromLatin1(sslParams.dhParams)} }; setResult(ret); } void TorrentsController::setSSLParametersAction() { requireParams({u"hash"_s, KEY_PROP_SSL_CERTIFICATE, KEY_PROP_SSL_PRIVATEKEY, KEY_PROP_SSL_DHPARAMS}); const auto id = BitTorrent::TorrentID::fromString(params()[u"hash"_s]); BitTorrent::Torrent *const torrent = BitTorrent::Session::instance()->getTorrent(id); if (!torrent) throw APIError(APIErrorType::NotFound); const BitTorrent::SSLParameters sslParams { .certificate = QSslCertificate(params()[KEY_PROP_SSL_CERTIFICATE].toLatin1()), .privateKey = Utils::SSLKey::load(params()[KEY_PROP_SSL_PRIVATEKEY].toLatin1()), .dhParams = params()[KEY_PROP_SSL_DHPARAMS].toLatin1() }; if (!sslParams.isValid()) throw APIError(APIErrorType::BadData); torrent->setSSLParameters(sslParams); 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::saveMetadataAction() { requireParams({u"source"_s}); const QString sourceParam = params()[u"source"_s].trimmed(); if (sourceParam.isEmpty()) throw APIError(APIErrorType::BadParams, tr("Must specify URI or hash")); const QString source = QUrl::fromPercentEncoding(sourceParam.toLatin1()); BitTorrent::InfoHash infoHash; if (const auto iter = m_torrentSourceCache.constFind(source); iter != m_torrentSourceCache.constEnd()) infoHash = iter.value(); else if (const auto sourceTorrentDescr = BitTorrent::TorrentDescriptor::parse(source)) infoHash = sourceTorrentDescr.value().infoHash(); if (!infoHash.isValid()) throw APIError(APIErrorType::NotFound); const BitTorrent::TorrentDescriptor &torrentDescr = m_torrentMetadataCache.value(infoHash); if (!torrentDescr.info().has_value()) throw APIError(APIErrorType::Conflict, tr("Metadata is not yet available")); const nonstd::expected result = torrentDescr.saveToBuffer(); if (!result) throw APIError(APIErrorType::Conflict, tr("Unable to export torrent metadata. Error: %1").arg(result.error())); setResult(result.value(), u"application/x-bittorrent"_s, (infoHash.toTorrentID().toString() + u".torrent")); } 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); }