/* * Bittorrent Client using Qt and libtorrent. * Copyright (C) 2018 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 "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/torrent.h" #include "base/bittorrent/torrentinfo.h" #include "base/bittorrent/trackerentry.h" #include "base/global.h" #include "base/logger.h" #include "base/net/downloadmanager.h" #include "base/torrentfilter.h" #include "base/utils/fs.h" #include "base/utils/string.h" #include "apierror.h" #include "serialize/serialize_torrent.h" // Tracker keys const QString KEY_TRACKER_URL = u"url"_qs; const QString KEY_TRACKER_STATUS = u"status"_qs; const QString KEY_TRACKER_TIER = u"tier"_qs; const QString KEY_TRACKER_MSG = u"msg"_qs; const QString KEY_TRACKER_PEERS_COUNT = u"num_peers"_qs; const QString KEY_TRACKER_SEEDS_COUNT = u"num_seeds"_qs; const QString KEY_TRACKER_LEECHES_COUNT = u"num_leeches"_qs; const QString KEY_TRACKER_DOWNLOADED_COUNT = u"num_downloaded"_qs; // Web seed keys const QString KEY_WEBSEED_URL = u"url"_qs; // Torrent keys (Properties) const QString KEY_PROP_TIME_ELAPSED = u"time_elapsed"_qs; const QString KEY_PROP_SEEDING_TIME = u"seeding_time"_qs; const QString KEY_PROP_ETA = u"eta"_qs; const QString KEY_PROP_CONNECT_COUNT = u"nb_connections"_qs; const QString KEY_PROP_CONNECT_COUNT_LIMIT = u"nb_connections_limit"_qs; const QString KEY_PROP_DOWNLOADED = u"total_downloaded"_qs; const QString KEY_PROP_DOWNLOADED_SESSION = u"total_downloaded_session"_qs; const QString KEY_PROP_UPLOADED = u"total_uploaded"_qs; const QString KEY_PROP_UPLOADED_SESSION = u"total_uploaded_session"_qs; const QString KEY_PROP_DL_SPEED = u"dl_speed"_qs; const QString KEY_PROP_DL_SPEED_AVG = u"dl_speed_avg"_qs; const QString KEY_PROP_UP_SPEED = u"up_speed"_qs; const QString KEY_PROP_UP_SPEED_AVG = u"up_speed_avg"_qs; const QString KEY_PROP_DL_LIMIT = u"dl_limit"_qs; const QString KEY_PROP_UP_LIMIT = u"up_limit"_qs; const QString KEY_PROP_WASTED = u"total_wasted"_qs; const QString KEY_PROP_SEEDS = u"seeds"_qs; const QString KEY_PROP_SEEDS_TOTAL = u"seeds_total"_qs; const QString KEY_PROP_PEERS = u"peers"_qs; const QString KEY_PROP_PEERS_TOTAL = u"peers_total"_qs; const QString KEY_PROP_RATIO = u"share_ratio"_qs; const QString KEY_PROP_REANNOUNCE = u"reannounce"_qs; const QString KEY_PROP_TOTAL_SIZE = u"total_size"_qs; const QString KEY_PROP_PIECES_NUM = u"pieces_num"_qs; const QString KEY_PROP_PIECE_SIZE = u"piece_size"_qs; const QString KEY_PROP_PIECES_HAVE = u"pieces_have"_qs; const QString KEY_PROP_CREATED_BY = u"created_by"_qs; const QString KEY_PROP_LAST_SEEN = u"last_seen"_qs; const QString KEY_PROP_ADDITION_DATE = u"addition_date"_qs; const QString KEY_PROP_COMPLETION_DATE = u"completion_date"_qs; const QString KEY_PROP_CREATION_DATE = u"creation_date"_qs; const QString KEY_PROP_SAVE_PATH = u"save_path"_qs; const QString KEY_PROP_DOWNLOAD_PATH = u"download_path"_qs; const QString KEY_PROP_COMMENT = u"comment"_qs; const QString KEY_PROP_ISPRIVATE = u"is_private"_qs; // File keys const QString KEY_FILE_INDEX = u"index"_qs; const QString KEY_FILE_NAME = u"name"_qs; const QString KEY_FILE_SIZE = u"size"_qs; const QString KEY_FILE_PROGRESS = u"progress"_qs; const QString KEY_FILE_PRIORITY = u"priority"_qs; const QString KEY_FILE_IS_SEED = u"is_seed"_qs; const QString KEY_FILE_PIECE_RANGE = u"piece_range"_qs; const QString KEY_FILE_AVAILABILITY = u"availability"_qs; namespace { using Utils::String::parseBool; using Utils::String::parseInt; using Utils::String::parseDouble; void applyToTorrents(const QStringList &idList, const std::function &func) { 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(); } QJsonArray getStickyTrackers(const BitTorrent::Torrent *const torrent) { int seedsDHT = 0, seedsPeX = 0, seedsLSD = 0, leechesDHT = 0, leechesPeX = 0, leechesLSD = 0; for (const BitTorrent::PeerInfo &peer : asConst(torrent->peers())) { 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::TrackerEntry::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] **"_qs}, {KEY_TRACKER_TIER, -1}, {KEY_TRACKER_MSG, (isTorrentPrivate ? privateMsg : u""_qs)}, {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] **"_qs}, {KEY_TRACKER_TIER, -1}, {KEY_TRACKER_MSG, (isTorrentPrivate ? privateMsg : u""_qs)}, {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] **"_qs}, {KEY_TRACKER_TIER, -1}, {KEY_TRACKER_MSG, (isTorrentPrivate ? privateMsg : u""_qs)}, {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}; } QVector toTorrentIDs(const QStringList &idStrings) { QVector idList; idList.reserve(idStrings.size()); for (const QString &hash : idStrings) idList << BitTorrent::TorrentID::fromString(hash); return idList; } } // 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, paused, resumed, 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 | // - 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"_qs]}; const std::optional category = getOptionalString(params(), u"category"_qs); const std::optional tag = getOptionalString(params(), u"tag"_qs); const QString sortedColumn {params()[u"sort"_qs]}; const bool reverse {parseBool(params()[u"reverse"_qs]).value_or(false)}; int limit {params()[u"limit"_qs].toInt()}; int offset {params()[u"offset"_qs].toInt()}; const QStringList hashes {params()[u"hashes"_qs].split(u'|', Qt::SkipEmptyParts)}; 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}; QVariantList torrentList; for (const BitTorrent::Torrent *torrent : asConst(BitTorrent::Session::instance()->torrents())) { if (torrentFilter.match(torrent)) torrentList.append(serialize(*torrent)); } 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.type() == right.type()); switch (static_cast(left.type())) { 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.type() , QMetaType::typeName(left.type())); 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; if ((offset >= size) || (offset < 0)) offset = 0; // 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 // - "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"_qs}); const auto id = BitTorrent::TorrentID::fromString(params()[u"hash"_qs]); BitTorrent::Torrent *const torrent = BitTorrent::Session::instance()->getTorrent(id); if (!torrent) throw APIError(APIErrorType::NotFound); QJsonObject dataDict; dataDict[KEY_TORRENT_INFOHASHV1] = torrent->infoHash().v1().toString(); dataDict[KEY_TORRENT_INFOHASHV2] = torrent->infoHash().v2().toString(); dataDict[KEY_TORRENT_NAME] = torrent->name(); dataDict[KEY_TORRENT_ID] = torrent->id().toString(); dataDict[KEY_PROP_TIME_ELAPSED] = torrent->activeTime(); dataDict[KEY_PROP_SEEDING_TIME] = torrent->finishedTime(); dataDict[KEY_PROP_ETA] = static_cast(torrent->eta()); dataDict[KEY_PROP_CONNECT_COUNT] = torrent->connectionsCount(); dataDict[KEY_PROP_CONNECT_COUNT_LIMIT] = torrent->connectionsLimit(); dataDict[KEY_PROP_DOWNLOADED] = torrent->totalDownload(); dataDict[KEY_PROP_DOWNLOADED_SESSION] = torrent->totalPayloadDownload(); dataDict[KEY_PROP_UPLOADED] = torrent->totalUpload(); dataDict[KEY_PROP_UPLOADED_SESSION] = torrent->totalPayloadUpload(); dataDict[KEY_PROP_DL_SPEED] = torrent->downloadPayloadRate(); const qlonglong dlDuration = torrent->activeTime() - torrent->finishedTime(); dataDict[KEY_PROP_DL_SPEED_AVG] = torrent->totalDownload() / ((dlDuration == 0) ? -1 : dlDuration); dataDict[KEY_PROP_UP_SPEED] = torrent->uploadPayloadRate(); const qlonglong ulDuration = torrent->activeTime(); dataDict[KEY_PROP_UP_SPEED_AVG] = torrent->totalUpload() / ((ulDuration == 0) ? -1 : ulDuration); dataDict[KEY_PROP_DL_LIMIT] = torrent->downloadLimit() <= 0 ? -1 : torrent->downloadLimit(); dataDict[KEY_PROP_UP_LIMIT] = torrent->uploadLimit() <= 0 ? -1 : torrent->uploadLimit(); dataDict[KEY_PROP_WASTED] = torrent->wastedSize(); dataDict[KEY_PROP_SEEDS] = torrent->seedsCount(); dataDict[KEY_PROP_SEEDS_TOTAL] = torrent->totalSeedsCount(); dataDict[KEY_PROP_PEERS] = torrent->leechsCount(); dataDict[KEY_PROP_PEERS_TOTAL] = torrent->totalLeechersCount(); const qreal ratio = torrent->realRatio(); dataDict[KEY_PROP_RATIO] = ratio > BitTorrent::Torrent::MAX_RATIO ? -1 : ratio; dataDict[KEY_PROP_REANNOUNCE] = torrent->nextAnnounce(); dataDict[KEY_PROP_TOTAL_SIZE] = torrent->totalSize(); dataDict[KEY_PROP_PIECES_NUM] = torrent->piecesCount(); dataDict[KEY_PROP_PIECE_SIZE] = torrent->pieceLength(); dataDict[KEY_PROP_PIECES_HAVE] = torrent->piecesHave(); dataDict[KEY_PROP_CREATED_BY] = torrent->creator(); dataDict[KEY_PROP_ISPRIVATE] = torrent->isPrivate(); dataDict[KEY_PROP_ADDITION_DATE] = static_cast(torrent->addedTime().toSecsSinceEpoch()); if (torrent->hasMetadata()) { dataDict[KEY_PROP_LAST_SEEN] = torrent->lastSeenComplete().isValid() ? torrent->lastSeenComplete().toSecsSinceEpoch() : -1; dataDict[KEY_PROP_COMPLETION_DATE] = torrent->completedTime().isValid() ? torrent->completedTime().toSecsSinceEpoch() : -1; dataDict[KEY_PROP_CREATION_DATE] = static_cast(torrent->creationDate().toSecsSinceEpoch()); } else { dataDict[KEY_PROP_LAST_SEEN] = -1; dataDict[KEY_PROP_COMPLETION_DATE] = -1; dataDict[KEY_PROP_CREATION_DATE] = -1; } dataDict[KEY_PROP_SAVE_PATH] = torrent->savePath().toString(); dataDict[KEY_PROP_DOWNLOAD_PATH] = torrent->downloadPath().toString(); dataDict[KEY_PROP_COMMENT] = torrent->comment(); setResult(dataDict); } // 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"_qs}); const auto id = BitTorrent::TorrentID::fromString(params()[u"hash"_qs]); const BitTorrent::Torrent *const torrent = BitTorrent::Session::instance()->getTorrent(id); if (!torrent) throw APIError(APIErrorType::NotFound); QJsonArray trackerList = getStickyTrackers(torrent); for (const BitTorrent::TrackerEntry &tracker : asConst(torrent->trackers())) { trackerList << QJsonObject { {KEY_TRACKER_URL, tracker.url}, {KEY_TRACKER_TIER, tracker.tier}, {KEY_TRACKER_STATUS, static_cast(tracker.status)}, {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} }; } setResult(trackerList); } // 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"_qs}); const auto id = BitTorrent::TorrentID::fromString(params()[u"hash"_qs]); 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); } // 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"_qs}); const auto id = BitTorrent::TorrentID::fromString(params()[u"hash"_qs]); const BitTorrent::Torrent *const torrent = BitTorrent::Session::instance()->getTorrent(id); if (!torrent) throw APIError(APIErrorType::NotFound); const int filesCount = torrent->filesCount(); QVector fileIndexes; const auto idxIt = params().constFind(u"indexes"_qs); if (idxIt != params().cend()) { 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; }); } else { fileIndexes.reserve(filesCount); for (int i = 0; i < filesCount; ++i) fileIndexes.append(i); } QJsonArray fileList; if (torrent->hasMetadata()) { const QVector priorities = torrent->filePriorities(); const QVector fp = torrent->filesProgress(); const QVector fileAvailability = torrent->availableFileFractions(); const BitTorrent::TorrentInfo info = torrent->info(); for (const int index : asConst(fileIndexes)) { 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()} }; const BitTorrent::TorrentInfo::PieceRange idx = info.filePieces(index); fileDict[KEY_FILE_PIECE_RANGE] = QJsonArray {idx.first(), idx.last()}; if (index == 0) fileDict[KEY_FILE_IS_SEED] = torrent->isFinished(); fileList.append(fileDict); } } 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"_qs}); const auto id = BitTorrent::TorrentID::fromString(params()[u"hash"_qs]); BitTorrent::Torrent *const torrent = BitTorrent::Session::instance()->getTorrent(id); if (!torrent) throw APIError(APIErrorType::NotFound); QJsonArray pieceHashes; if (torrent->hasMetadata()) { const QVector 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"_qs}); const auto id = BitTorrent::TorrentID::fromString(params()[u"hash"_qs]); 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->downloadingPieces(); for (int i = 0; i < states.size(); ++i) { if (dlstates[i]) pieceStates[i] = 1; } setResult(pieceStates); } void TorrentsController::addAction() { const QString urls = params()[u"urls"_qs]; const QString cookie = params()[u"cookie"_qs]; const bool skipChecking = parseBool(params()[u"skip_checking"_qs]).value_or(false); const bool seqDownload = parseBool(params()[u"sequentialDownload"_qs]).value_or(false); const bool firstLastPiece = parseBool(params()[u"firstLastPiecePrio"_qs]).value_or(false); const std::optional addToQueueTop = parseBool(params()[u"addToTopOfQueue"_qs]); const std::optional addPaused = parseBool(params()[u"paused"_qs]); const QString savepath = params()[u"savepath"_qs].trimmed(); const QString downloadPath = params()[u"downloadPath"_qs].trimmed(); const std::optional useDownloadPath = parseBool(params()[u"useDownloadPath"_qs]); const QString category = params()[u"category"_qs]; const QStringList tags = params()[u"tags"_qs].split(u',', Qt::SkipEmptyParts); const QString torrentName = params()[u"rename"_qs].trimmed(); const int upLimit = parseInt(params()[u"upLimit"_qs]).value_or(-1); const int dlLimit = parseInt(params()[u"dlLimit"_qs]).value_or(-1); const double ratioLimit = parseDouble(params()[u"ratioLimit"_qs]).value_or(BitTorrent::Torrent::USE_GLOBAL_RATIO); const int seedingTimeLimit = parseInt(params()[u"seedingTimeLimit"_qs]).value_or(BitTorrent::Torrent::USE_GLOBAL_SEEDING_TIME); const std::optional autoTMM = parseBool(params()[u"autoTMM"_qs]); const QString stopConditionParam = params()[u"stopCondition"_qs]; const std::optional stopCondition = (!stopConditionParam.isEmpty() ? Utils::String::toEnum(stopConditionParam, BitTorrent::Torrent::StopCondition::None) : std::optional {}); const QString contentLayoutParam = params()[u"contentLayout"_qs]; const std::optional contentLayout = (!contentLayoutParam.isEmpty() ? Utils::String::toEnum(contentLayoutParam, BitTorrent::TorrentContentLayout::Original) : std::optional {}); QList cookies; if (!cookie.isEmpty()) { const QStringList cookiesStr = cookie.split(u"; "_qs); for (QString cookieStr : cookiesStr) { cookieStr = cookieStr.trimmed(); int index = cookieStr.indexOf(u'='); if (index > 1) { QByteArray name = cookieStr.left(index).toLatin1(); QByteArray value = cookieStr.right(cookieStr.length() - index - 1).toLatin1(); cookies += QNetworkCookie(name, value); } } } BitTorrent::AddTorrentParams addTorrentParams; // TODO: Check if destination actually exists addTorrentParams.skipChecking = skipChecking; addTorrentParams.sequential = seqDownload; addTorrentParams.firstLastPiecePriority = firstLastPiece; addTorrentParams.addToQueueTop = addToQueueTop; addTorrentParams.addPaused = addPaused; addTorrentParams.stopCondition = stopCondition; addTorrentParams.contentLayout = contentLayout; addTorrentParams.savePath = Path(savepath); addTorrentParams.downloadPath = Path(downloadPath); addTorrentParams.useDownloadPath = useDownloadPath; addTorrentParams.category = category; addTorrentParams.tags.insert(tags.cbegin(), tags.cend()); addTorrentParams.name = torrentName; addTorrentParams.uploadLimit = upLimit; addTorrentParams.downloadLimit = dlLimit; addTorrentParams.seedingTimeLimit = seedingTimeLimit; addTorrentParams.ratioLimit = ratioLimit; addTorrentParams.useAutoTMM = autoTMM; bool partialSuccess = false; for (QString url : asConst(urls.split(u'\n'))) { url = url.trimmed(); if (!url.isEmpty()) { Net::DownloadManager::instance()->setCookiesFromUrl(cookies, QUrl::fromEncoded(url.toUtf8())); partialSuccess |= BitTorrent::Session::instance()->addTorrent(url, addTorrentParams); } } const DataMap torrents = data(); for (auto it = torrents.constBegin(); it != torrents.constEnd(); ++it) { const nonstd::expected result = BitTorrent::TorrentInfo::load(it.value()); if (!result) { throw APIError(APIErrorType::BadData , tr("Error: '%1' is not a valid torrent file.").arg(it.key())); } partialSuccess |= BitTorrent::Session::instance()->addTorrent(result.value(), addTorrentParams); } if (partialSuccess) setResult(u"Ok."_qs); else setResult(u"Fails."_qs); } void TorrentsController::addTrackersAction() { requireParams({u"hash"_qs, u"urls"_qs}); const auto id = BitTorrent::TorrentID::fromString(params()[u"hash"_qs]); BitTorrent::Torrent *const torrent = BitTorrent::Session::instance()->getTorrent(id); if (!torrent) throw APIError(APIErrorType::NotFound); const QVector entries = BitTorrent::parseTrackerEntries(params()[u"urls"_qs]); torrent->addTrackers(entries); } void TorrentsController::editTrackerAction() { requireParams({u"hash"_qs, u"origUrl"_qs, u"newUrl"_qs}); const auto id = BitTorrent::TorrentID::fromString(params()[u"hash"_qs]); const QString origUrl = params()[u"origUrl"_qs]; const QString newUrl = params()[u"newUrl"_qs]; 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"_qs); QVector trackers = torrent->trackers(); bool match = false; for (BitTorrent::TrackerEntry &tracker : trackers) { const QUrl trackerUrl {tracker.url}; if (trackerUrl == newTrackerUrl) throw APIError(APIErrorType::Conflict, u"New tracker URL already exists"_qs); if (trackerUrl == origTrackerUrl) { match = true; tracker.url = newTrackerUrl.toString(); } } if (!match) throw APIError(APIErrorType::Conflict, u"Tracker not found"_qs); torrent->replaceTrackers(trackers); if (!torrent->isPaused()) torrent->forceReannounce(); } void TorrentsController::removeTrackersAction() { requireParams({u"hash"_qs, u"urls"_qs}); const auto id = BitTorrent::TorrentID::fromString(params()[u"hash"_qs]); BitTorrent::Torrent *const torrent = BitTorrent::Session::instance()->getTorrent(id); if (!torrent) throw APIError(APIErrorType::NotFound); const QStringList urls = params()[u"urls"_qs].split(u'|'); torrent->removeTrackers(urls); if (!torrent->isPaused()) torrent->forceReannounce(); } void TorrentsController::addPeersAction() { requireParams({u"hashes"_qs, u"peers"_qs}); const QStringList hashes = params()[u"hashes"_qs].split(u'|'); const QStringList peers = params()[u"peers"_qs].split(u'|'); QVector 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"_qs); 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"_qs, peersAdded}, {u"failed"_qs, (peers.size() - peersAdded)} }; }); setResult(results); } void TorrentsController::pauseAction() { requireParams({u"hashes"_qs}); const QStringList hashes = params()[u"hashes"_qs].split(u'|'); applyToTorrents(hashes, [](BitTorrent::Torrent *const torrent) { torrent->pause(); }); } void TorrentsController::resumeAction() { requireParams({u"hashes"_qs}); const QStringList idStrings = params()[u"hashes"_qs].split(u'|'); applyToTorrents(idStrings, [](BitTorrent::Torrent *const torrent) { torrent->resume(); }); } void TorrentsController::filePrioAction() { requireParams({u"hash"_qs, u"id"_qs, u"priority"_qs}); const auto id = BitTorrent::TorrentID::fromString(params()[u"hash"_qs]); bool ok = false; const auto priority = static_cast(params()[u"priority"_qs].toInt(&ok)); if (!ok) throw APIError(APIErrorType::BadParams, tr("Priority must be an integer")); if (!BitTorrent::isValidDownloadPriority(priority)) throw APIError(APIErrorType::BadParams, tr("Priority is not valid")); 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(); QVector priorities = torrent->filePriorities(); bool priorityChanged = false; for (const QString &fileID : params()[u"id"_qs].split(u'|')) { 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); } void TorrentsController::uploadLimitAction() { requireParams({u"hashes"_qs}); const QStringList idList {params()[u"hashes"_qs].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"_qs}); const QStringList idList {params()[u"hashes"_qs].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"_qs, u"limit"_qs}); qlonglong limit = params()[u"limit"_qs].toLongLong(); if (limit == 0) limit = -1; const QStringList hashes {params()[u"hashes"_qs].split(u'|')}; applyToTorrents(hashes, [limit](BitTorrent::Torrent *const torrent) { torrent->setUploadLimit(limit); }); } void TorrentsController::setDownloadLimitAction() { requireParams({u"hashes"_qs, u"limit"_qs}); qlonglong limit = params()[u"limit"_qs].toLongLong(); if (limit == 0) limit = -1; const QStringList hashes {params()[u"hashes"_qs].split(u'|')}; applyToTorrents(hashes, [limit](BitTorrent::Torrent *const torrent) { torrent->setDownloadLimit(limit); }); } void TorrentsController::setShareLimitsAction() { requireParams({u"hashes"_qs, u"ratioLimit"_qs, u"seedingTimeLimit"_qs}); const qreal ratioLimit = params()[u"ratioLimit"_qs].toDouble(); const qlonglong seedingTimeLimit = params()[u"seedingTimeLimit"_qs].toLongLong(); const QStringList hashes = params()[u"hashes"_qs].split(u'|'); applyToTorrents(hashes, [ratioLimit, seedingTimeLimit](BitTorrent::Torrent *const torrent) { torrent->setRatioLimit(ratioLimit); torrent->setSeedingTimeLimit(seedingTimeLimit); }); } void TorrentsController::toggleSequentialDownloadAction() { requireParams({u"hashes"_qs}); const QStringList hashes {params()[u"hashes"_qs].split(u'|')}; applyToTorrents(hashes, [](BitTorrent::Torrent *const torrent) { torrent->toggleSequentialDownload(); }); } void TorrentsController::toggleFirstLastPiecePrioAction() { requireParams({u"hashes"_qs}); const QStringList hashes {params()[u"hashes"_qs].split(u'|')}; applyToTorrents(hashes, [](BitTorrent::Torrent *const torrent) { torrent->toggleFirstLastPiecePriority(); }); } void TorrentsController::setSuperSeedingAction() { requireParams({u"hashes"_qs, u"value"_qs}); const bool value {parseBool(params()[u"value"_qs]).value_or(false)}; const QStringList hashes {params()[u"hashes"_qs].split(u'|')}; applyToTorrents(hashes, [value](BitTorrent::Torrent *const torrent) { torrent->setSuperSeeding(value); }); } void TorrentsController::setForceStartAction() { requireParams({u"hashes"_qs, u"value"_qs}); const bool value {parseBool(params()[u"value"_qs]).value_or(false)}; const QStringList hashes {params()[u"hashes"_qs].split(u'|')}; applyToTorrents(hashes, [value](BitTorrent::Torrent *const torrent) { torrent->resume(value ? BitTorrent::TorrentOperatingMode::Forced : BitTorrent::TorrentOperatingMode::AutoManaged); }); } void TorrentsController::deleteAction() { requireParams({u"hashes"_qs, u"deleteFiles"_qs}); const QStringList hashes {params()[u"hashes"_qs].split(u'|')}; const DeleteOption deleteOption = parseBool(params()[u"deleteFiles"_qs]).value_or(false) ? DeleteTorrentAndFiles : DeleteTorrent; applyToTorrents(hashes, [deleteOption](const BitTorrent::Torrent *torrent) { BitTorrent::Session::instance()->deleteTorrent(torrent->id(), deleteOption); }); } void TorrentsController::increasePrioAction() { requireParams({u"hashes"_qs}); if (!BitTorrent::Session::instance()->isQueueingSystemEnabled()) throw APIError(APIErrorType::Conflict, tr("Torrent queueing must be enabled")); const QStringList hashes {params()[u"hashes"_qs].split(u'|')}; BitTorrent::Session::instance()->increaseTorrentsQueuePos(toTorrentIDs(hashes)); } void TorrentsController::decreasePrioAction() { requireParams({u"hashes"_qs}); if (!BitTorrent::Session::instance()->isQueueingSystemEnabled()) throw APIError(APIErrorType::Conflict, tr("Torrent queueing must be enabled")); const QStringList hashes {params()[u"hashes"_qs].split(u'|')}; BitTorrent::Session::instance()->decreaseTorrentsQueuePos(toTorrentIDs(hashes)); } void TorrentsController::topPrioAction() { requireParams({u"hashes"_qs}); if (!BitTorrent::Session::instance()->isQueueingSystemEnabled()) throw APIError(APIErrorType::Conflict, tr("Torrent queueing must be enabled")); const QStringList hashes {params()[u"hashes"_qs].split(u'|')}; BitTorrent::Session::instance()->topTorrentsQueuePos(toTorrentIDs(hashes)); } void TorrentsController::bottomPrioAction() { requireParams({u"hashes"_qs}); if (!BitTorrent::Session::instance()->isQueueingSystemEnabled()) throw APIError(APIErrorType::Conflict, tr("Torrent queueing must be enabled")); const QStringList hashes {params()[u"hashes"_qs].split(u'|')}; BitTorrent::Session::instance()->bottomTorrentsQueuePos(toTorrentIDs(hashes)); } void TorrentsController::setLocationAction() { requireParams({u"hashes"_qs, u"location"_qs}); const QStringList hashes {params()[u"hashes"_qs].split(u'|')}; const Path newLocation {params()[u"location"_qs].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); }); } void TorrentsController::setSavePathAction() { requireParams({u"id"_qs, u"path"_qs}); const QStringList ids {params()[u"id"_qs].split(u'|')}; const Path newPath {params()[u"path"_qs]}; 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); }); } void TorrentsController::setDownloadPathAction() { requireParams({u"id"_qs, u"path"_qs}); const QStringList ids {params()[u"id"_qs].split(u'|')}; const Path newPath {params()[u"path"_qs]}; 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); }); } void TorrentsController::renameAction() { requireParams({u"hash"_qs, u"name"_qs}); const auto id = BitTorrent::TorrentID::fromString(params()[u"hash"_qs]); QString name = params()[u"name"_qs].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"_qs), u" "_qs); torrent->setName(name); } void TorrentsController::setAutoManagementAction() { requireParams({u"hashes"_qs, u"enable"_qs}); const QStringList hashes {params()[u"hashes"_qs].split(u'|')}; const bool isEnabled {parseBool(params()[u"enable"_qs]).value_or(false)}; applyToTorrents(hashes, [isEnabled](BitTorrent::Torrent *const torrent) { torrent->setAutoTMMEnabled(isEnabled); }); } void TorrentsController::recheckAction() { requireParams({u"hashes"_qs}); const QStringList hashes {params()[u"hashes"_qs].split(u'|')}; applyToTorrents(hashes, [](BitTorrent::Torrent *const torrent) { torrent->forceRecheck(); }); } void TorrentsController::reannounceAction() { requireParams({u"hashes"_qs}); const QStringList hashes {params()[u"hashes"_qs].split(u'|')}; applyToTorrents(hashes, [](BitTorrent::Torrent *const torrent) { torrent->forceReannounce(); }); } void TorrentsController::setCategoryAction() { requireParams({u"hashes"_qs, u"category"_qs}); const QStringList hashes {params()[u"hashes"_qs].split(u'|')}; const QString category {params()[u"category"_qs]}; applyToTorrents(hashes, [category](BitTorrent::Torrent *const torrent) { if (!torrent->setCategory(category)) throw APIError(APIErrorType::Conflict, tr("Incorrect category name")); }); } void TorrentsController::createCategoryAction() { requireParams({u"category"_qs}); const QString category = params()[u"category"_qs]; 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"_qs]}; const auto useDownloadPath = parseBool(params()[u"downloadPathEnabled"_qs]); BitTorrent::CategoryOptions categoryOptions; categoryOptions.savePath = savePath; if (useDownloadPath.has_value()) { const Path downloadPath {params()[u"downloadPath"_qs]}; categoryOptions.downloadPath = {useDownloadPath.value(), downloadPath}; } if (!BitTorrent::Session::instance()->addCategory(category, categoryOptions)) throw APIError(APIErrorType::Conflict, tr("Unable to create category")); } void TorrentsController::editCategoryAction() { requireParams({u"category"_qs, u"savePath"_qs}); const QString category = params()[u"category"_qs]; if (category.isEmpty()) throw APIError(APIErrorType::BadParams, tr("Category cannot be empty")); const Path savePath {params()[u"savePath"_qs]}; const auto useDownloadPath = parseBool(params()[u"downloadPathEnabled"_qs]); BitTorrent::CategoryOptions categoryOptions; categoryOptions.savePath = savePath; if (useDownloadPath.has_value()) { const Path downloadPath {params()[u"downloadPath"_qs]}; categoryOptions.downloadPath = {useDownloadPath.value(), downloadPath}; } if (!BitTorrent::Session::instance()->editCategory(category, categoryOptions)) throw APIError(APIErrorType::Conflict, tr("Unable to edit category")); } void TorrentsController::removeCategoriesAction() { requireParams({u"categories"_qs}); const QStringList categories {params()[u"categories"_qs].split(u'\n')}; for (const QString &category : categories) BitTorrent::Session::instance()->removeCategory(category); } 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"_qs] = category.take(u"save_path"_qs); category.insert(u"name"_qs, categoryName); categories[categoryName] = category; } setResult(categories); } void TorrentsController::addTagsAction() { requireParams({u"hashes"_qs, u"tags"_qs}); const QStringList hashes {params()[u"hashes"_qs].split(u'|')}; const QStringList tags {params()[u"tags"_qs].split(u',', Qt::SkipEmptyParts)}; for (const QString &tag : tags) { const QString tagTrimmed {tag.trimmed()}; applyToTorrents(hashes, [&tagTrimmed](BitTorrent::Torrent *const torrent) { torrent->addTag(tagTrimmed); }); } } void TorrentsController::removeTagsAction() { requireParams({u"hashes"_qs}); const QStringList hashes {params()[u"hashes"_qs].split(u'|')}; const QStringList tags {params()[u"tags"_qs].split(u',', Qt::SkipEmptyParts)}; for (const QString &tag : tags) { const QString tagTrimmed {tag.trimmed()}; applyToTorrents(hashes, [&tagTrimmed](BitTorrent::Torrent *const torrent) { torrent->removeTag(tagTrimmed); }); } if (tags.isEmpty()) { applyToTorrents(hashes, [](BitTorrent::Torrent *const torrent) { torrent->removeAllTags(); }); } } void TorrentsController::createTagsAction() { requireParams({u"tags"_qs}); const QStringList tags {params()[u"tags"_qs].split(u',', Qt::SkipEmptyParts)}; for (const QString &tag : tags) BitTorrent::Session::instance()->addTag(tag.trimmed()); } void TorrentsController::deleteTagsAction() { requireParams({u"tags"_qs}); const QStringList tags {params()[u"tags"_qs].split(u',', Qt::SkipEmptyParts)}; for (const QString &tag : tags) BitTorrent::Session::instance()->removeTag(tag.trimmed()); } void TorrentsController::tagsAction() { QJsonArray result; for (const QString &tag : asConst(BitTorrent::Session::instance()->tags())) result << tag; setResult(result); } void TorrentsController::renameFileAction() { requireParams({u"hash"_qs, u"oldPath"_qs, u"newPath"_qs}); const auto id = BitTorrent::TorrentID::fromString(params()[u"hash"_qs]); BitTorrent::Torrent *const torrent = BitTorrent::Session::instance()->getTorrent(id); if (!torrent) throw APIError(APIErrorType::NotFound); const Path oldPath {params()[u"oldPath"_qs]}; const Path newPath {params()[u"newPath"_qs]}; try { torrent->renameFile(oldPath, newPath); } catch (const RuntimeError &error) { throw APIError(APIErrorType::Conflict, error.message()); } } void TorrentsController::renameFolderAction() { requireParams({u"hash"_qs, u"oldPath"_qs, u"newPath"_qs}); const auto id = BitTorrent::TorrentID::fromString(params()[u"hash"_qs]); BitTorrent::Torrent *const torrent = BitTorrent::Session::instance()->getTorrent(id); if (!torrent) throw APIError(APIErrorType::NotFound); const Path oldPath {params()[u"oldPath"_qs]}; const Path newPath {params()[u"newPath"_qs]}; try { torrent->renameFolder(oldPath, newPath); } catch (const RuntimeError &error) { throw APIError(APIErrorType::Conflict, error.message()); } } void TorrentsController::exportAction() { requireParams({u"hash"_qs}); const auto id = BitTorrent::TorrentID::fromString(params()[u"hash"_qs]); 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()); }