WebUI: Fix reloading page after login
[qBittorrent.git] / src / webui / api / torrentscontroller.cpp
bloba34971da0148f35af155f8c45ee9b00338218d98
1 /*
2 * Bittorrent Client using Qt and libtorrent.
3 * Copyright (C) 2018-2023 Vladimir Golovnev <glassez@yandex.ru>
5 * This program is free software; you can redistribute it and/or
6 * modify it under the terms of the GNU General Public License
7 * as published by the Free Software Foundation; either version 2
8 * of the License, or (at your option) any later version.
10 * This program is distributed in the hope that it will be useful,
11 * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 * GNU General Public License for more details.
15 * You should have received a copy of the GNU General Public License
16 * along with this program; if not, write to the Free Software
17 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
19 * In addition, as a special exception, the copyright holders give permission to
20 * link this program with the OpenSSL project's "OpenSSL" library (or with
21 * modified versions of it that use the same license as the "OpenSSL" library),
22 * and distribute the linked executables. You must obey the GNU General Public
23 * License in all respects for all of the code used other than "OpenSSL". If you
24 * modify file(s), you may extend this exception to your version of the file(s),
25 * but you are not obligated to do so. If you do not wish to do so, delete this
26 * exception statement from your version.
29 #include "torrentscontroller.h"
31 #include <concepts>
32 #include <functional>
34 #include <QBitArray>
35 #include <QJsonArray>
36 #include <QJsonObject>
37 #include <QList>
38 #include <QRegularExpression>
39 #include <QUrl>
41 #include "base/addtorrentmanager.h"
42 #include "base/bittorrent/categoryoptions.h"
43 #include "base/bittorrent/downloadpriority.h"
44 #include "base/bittorrent/infohash.h"
45 #include "base/bittorrent/peeraddress.h"
46 #include "base/bittorrent/peerinfo.h"
47 #include "base/bittorrent/session.h"
48 #include "base/bittorrent/sslparameters.h"
49 #include "base/bittorrent/torrent.h"
50 #include "base/bittorrent/torrentdescriptor.h"
51 #include "base/bittorrent/trackerentry.h"
52 #include "base/bittorrent/trackerentrystatus.h"
53 #include "base/interfaces/iapplication.h"
54 #include "base/global.h"
55 #include "base/logger.h"
56 #include "base/torrentfilter.h"
57 #include "base/utils/datetime.h"
58 #include "base/utils/fs.h"
59 #include "base/utils/sslkey.h"
60 #include "base/utils/string.h"
61 #include "apierror.h"
62 #include "serialize/serialize_torrent.h"
64 // Tracker keys
65 const QString KEY_TRACKER_URL = u"url"_s;
66 const QString KEY_TRACKER_STATUS = u"status"_s;
67 const QString KEY_TRACKER_TIER = u"tier"_s;
68 const QString KEY_TRACKER_MSG = u"msg"_s;
69 const QString KEY_TRACKER_PEERS_COUNT = u"num_peers"_s;
70 const QString KEY_TRACKER_SEEDS_COUNT = u"num_seeds"_s;
71 const QString KEY_TRACKER_LEECHES_COUNT = u"num_leeches"_s;
72 const QString KEY_TRACKER_DOWNLOADED_COUNT = u"num_downloaded"_s;
74 // Web seed keys
75 const QString KEY_WEBSEED_URL = u"url"_s;
77 // Torrent keys (Properties)
78 const QString KEY_PROP_TIME_ELAPSED = u"time_elapsed"_s;
79 const QString KEY_PROP_SEEDING_TIME = u"seeding_time"_s;
80 const QString KEY_PROP_ETA = u"eta"_s;
81 const QString KEY_PROP_CONNECT_COUNT = u"nb_connections"_s;
82 const QString KEY_PROP_CONNECT_COUNT_LIMIT = u"nb_connections_limit"_s;
83 const QString KEY_PROP_DOWNLOADED = u"total_downloaded"_s;
84 const QString KEY_PROP_DOWNLOADED_SESSION = u"total_downloaded_session"_s;
85 const QString KEY_PROP_UPLOADED = u"total_uploaded"_s;
86 const QString KEY_PROP_UPLOADED_SESSION = u"total_uploaded_session"_s;
87 const QString KEY_PROP_DL_SPEED = u"dl_speed"_s;
88 const QString KEY_PROP_DL_SPEED_AVG = u"dl_speed_avg"_s;
89 const QString KEY_PROP_UP_SPEED = u"up_speed"_s;
90 const QString KEY_PROP_UP_SPEED_AVG = u"up_speed_avg"_s;
91 const QString KEY_PROP_DL_LIMIT = u"dl_limit"_s;
92 const QString KEY_PROP_UP_LIMIT = u"up_limit"_s;
93 const QString KEY_PROP_WASTED = u"total_wasted"_s;
94 const QString KEY_PROP_SEEDS = u"seeds"_s;
95 const QString KEY_PROP_SEEDS_TOTAL = u"seeds_total"_s;
96 const QString KEY_PROP_PEERS = u"peers"_s;
97 const QString KEY_PROP_PEERS_TOTAL = u"peers_total"_s;
98 const QString KEY_PROP_RATIO = u"share_ratio"_s;
99 const QString KEY_PROP_POPULARITY = u"popularity"_s;
100 const QString KEY_PROP_REANNOUNCE = u"reannounce"_s;
101 const QString KEY_PROP_TOTAL_SIZE = u"total_size"_s;
102 const QString KEY_PROP_PIECES_NUM = u"pieces_num"_s;
103 const QString KEY_PROP_PIECE_SIZE = u"piece_size"_s;
104 const QString KEY_PROP_PIECES_HAVE = u"pieces_have"_s;
105 const QString KEY_PROP_CREATED_BY = u"created_by"_s;
106 const QString KEY_PROP_LAST_SEEN = u"last_seen"_s;
107 const QString KEY_PROP_ADDITION_DATE = u"addition_date"_s;
108 const QString KEY_PROP_COMPLETION_DATE = u"completion_date"_s;
109 const QString KEY_PROP_CREATION_DATE = u"creation_date"_s;
110 const QString KEY_PROP_SAVE_PATH = u"save_path"_s;
111 const QString KEY_PROP_DOWNLOAD_PATH = u"download_path"_s;
112 const QString KEY_PROP_COMMENT = u"comment"_s;
113 const QString KEY_PROP_IS_PRIVATE = u"is_private"_s; // deprecated, "private" should be used instead
114 const QString KEY_PROP_PRIVATE = u"private"_s;
115 const QString KEY_PROP_SSL_CERTIFICATE = u"ssl_certificate"_s;
116 const QString KEY_PROP_SSL_PRIVATEKEY = u"ssl_private_key"_s;
117 const QString KEY_PROP_SSL_DHPARAMS = u"ssl_dh_params"_s;
118 const QString KEY_PROP_HAS_METADATA = u"has_metadata"_s;
119 const QString KEY_PROP_PROGRESS = u"progress"_s;
122 // File keys
123 const QString KEY_FILE_INDEX = u"index"_s;
124 const QString KEY_FILE_NAME = u"name"_s;
125 const QString KEY_FILE_SIZE = u"size"_s;
126 const QString KEY_FILE_PROGRESS = u"progress"_s;
127 const QString KEY_FILE_PRIORITY = u"priority"_s;
128 const QString KEY_FILE_IS_SEED = u"is_seed"_s;
129 const QString KEY_FILE_PIECE_RANGE = u"piece_range"_s;
130 const QString KEY_FILE_AVAILABILITY = u"availability"_s;
132 namespace
134 using Utils::String::parseBool;
135 using Utils::String::parseInt;
136 using Utils::String::parseDouble;
138 const QSet<QString> SUPPORTED_WEB_SEED_SCHEMES {u"http"_s, u"https"_s, u"ftp"_s};
140 template <typename Func>
141 void applyToTorrents(const QStringList &idList, Func func)
142 requires std::invocable<Func, BitTorrent::Torrent *>
144 if ((idList.size() == 1) && (idList[0] == u"all"))
146 for (BitTorrent::Torrent *const torrent : asConst(BitTorrent::Session::instance()->torrents()))
147 func(torrent);
149 else
151 for (const QString &idString : idList)
153 const auto hash = BitTorrent::TorrentID::fromString(idString);
154 BitTorrent::Torrent *const torrent = BitTorrent::Session::instance()->getTorrent(hash);
155 if (torrent)
156 func(torrent);
161 std::optional<QString> getOptionalString(const StringMap &params, const QString &name)
163 const auto it = params.constFind(name);
164 if (it == params.cend())
165 return std::nullopt;
167 return it.value();
170 std::optional<Tag> getOptionalTag(const StringMap &params, const QString &name)
172 const auto it = params.constFind(name);
173 if (it == params.cend())
174 return std::nullopt;
176 return Tag(it.value());
179 QJsonArray getStickyTrackers(const BitTorrent::Torrent *const torrent)
181 int seedsDHT = 0, seedsPeX = 0, seedsLSD = 0, leechesDHT = 0, leechesPeX = 0, leechesLSD = 0;
182 for (const BitTorrent::PeerInfo &peer : asConst(torrent->peers()))
184 if (peer.isConnecting()) continue;
186 if (peer.isSeed())
188 if (peer.fromDHT())
189 ++seedsDHT;
190 if (peer.fromPeX())
191 ++seedsPeX;
192 if (peer.fromLSD())
193 ++seedsLSD;
195 else
197 if (peer.fromDHT())
198 ++leechesDHT;
199 if (peer.fromPeX())
200 ++leechesPeX;
201 if (peer.fromLSD())
202 ++leechesLSD;
206 const int working = static_cast<int>(BitTorrent::TrackerEndpointState::Working);
207 const int disabled = 0;
209 const QString privateMsg {QCoreApplication::translate("TrackerListWidget", "This torrent is private")};
210 const bool isTorrentPrivate = torrent->isPrivate();
212 const QJsonObject dht
214 {KEY_TRACKER_URL, u"** [DHT] **"_s},
215 {KEY_TRACKER_TIER, -1},
216 {KEY_TRACKER_MSG, (isTorrentPrivate ? privateMsg : u""_s)},
217 {KEY_TRACKER_STATUS, ((BitTorrent::Session::instance()->isDHTEnabled() && !isTorrentPrivate) ? working : disabled)},
218 {KEY_TRACKER_PEERS_COUNT, 0},
219 {KEY_TRACKER_DOWNLOADED_COUNT, 0},
220 {KEY_TRACKER_SEEDS_COUNT, seedsDHT},
221 {KEY_TRACKER_LEECHES_COUNT, leechesDHT}
224 const QJsonObject pex
226 {KEY_TRACKER_URL, u"** [PeX] **"_s},
227 {KEY_TRACKER_TIER, -1},
228 {KEY_TRACKER_MSG, (isTorrentPrivate ? privateMsg : u""_s)},
229 {KEY_TRACKER_STATUS, ((BitTorrent::Session::instance()->isPeXEnabled() && !isTorrentPrivate) ? working : disabled)},
230 {KEY_TRACKER_PEERS_COUNT, 0},
231 {KEY_TRACKER_DOWNLOADED_COUNT, 0},
232 {KEY_TRACKER_SEEDS_COUNT, seedsPeX},
233 {KEY_TRACKER_LEECHES_COUNT, leechesPeX}
236 const QJsonObject lsd
238 {KEY_TRACKER_URL, u"** [LSD] **"_s},
239 {KEY_TRACKER_TIER, -1},
240 {KEY_TRACKER_MSG, (isTorrentPrivate ? privateMsg : u""_s)},
241 {KEY_TRACKER_STATUS, ((BitTorrent::Session::instance()->isLSDEnabled() && !isTorrentPrivate) ? working : disabled)},
242 {KEY_TRACKER_PEERS_COUNT, 0},
243 {KEY_TRACKER_DOWNLOADED_COUNT, 0},
244 {KEY_TRACKER_SEEDS_COUNT, seedsLSD},
245 {KEY_TRACKER_LEECHES_COUNT, leechesLSD}
248 return {dht, pex, lsd};
251 QList<BitTorrent::TorrentID> toTorrentIDs(const QStringList &idStrings)
253 QList<BitTorrent::TorrentID> idList;
254 idList.reserve(idStrings.size());
255 for (const QString &hash : idStrings)
256 idList << BitTorrent::TorrentID::fromString(hash);
257 return idList;
260 nonstd::expected<QUrl, QString> validateWebSeedUrl(const QString &urlStr)
262 const QString normalizedUrlStr = QUrl::fromPercentEncoding(urlStr.toLatin1());
264 const QUrl url {normalizedUrlStr, QUrl::StrictMode};
265 if (!url.isValid())
266 return nonstd::make_unexpected(TorrentsController::tr("\"%1\" is not a valid URL").arg(normalizedUrlStr));
268 if (!SUPPORTED_WEB_SEED_SCHEMES.contains(url.scheme()))
269 return nonstd::make_unexpected(TorrentsController::tr("URL scheme must be one of [%1]").arg(SUPPORTED_WEB_SEED_SCHEMES.values().join(u", ")));
271 return url;
275 void TorrentsController::countAction()
277 setResult(QString::number(BitTorrent::Session::instance()->torrentsCount()));
280 // Returns all the torrents in JSON format.
281 // The return value is a JSON-formatted list of dictionaries.
282 // The dictionary keys are:
283 // - "hash": Torrent hash (ID)
284 // - "name": Torrent name
285 // - "size": Torrent size
286 // - "progress": Torrent progress
287 // - "dlspeed": Torrent download speed
288 // - "upspeed": Torrent upload speed
289 // - "priority": Torrent queue position (-1 if queuing is disabled)
290 // - "num_seeds": Torrent seeds connected to
291 // - "num_complete": Torrent seeds in the swarm
292 // - "num_leechs": Torrent leechers connected to
293 // - "num_incomplete": Torrent leechers in the swarm
294 // - "ratio": Torrent share ratio
295 // - "eta": Torrent ETA
296 // - "state": Torrent state
297 // - "seq_dl": Torrent sequential download state
298 // - "f_l_piece_prio": Torrent first last piece priority state
299 // - "force_start": Torrent force start state
300 // - "category": Torrent category
301 // GET params:
302 // - filter (string): all, downloading, seeding, completed, stopped, running, active, inactive, stalled, stalled_uploading, stalled_downloading
303 // - category (string): torrent category for filtering by it (empty string means "uncategorized"; no "category" param presented means "any category")
304 // - tag (string): torrent tag for filtering by it (empty string means "untagged"; no "tag" param presented means "any tag")
305 // - hashes (string): filter by hashes, can contain multiple hashes separated by |
306 // - private (bool): filter torrents that are from private trackers (true) or not (false). Empty means any torrent (no filtering)
307 // - sort (string): name of column for sorting by its value
308 // - reverse (bool): enable reverse sorting
309 // - limit (int): set limit number of torrents returned (if greater than 0, otherwise - unlimited)
310 // - offset (int): set offset (if less than 0 - offset from end)
311 void TorrentsController::infoAction()
313 const QString filter {params()[u"filter"_s]};
314 const std::optional<QString> category = getOptionalString(params(), u"category"_s);
315 const std::optional<Tag> tag = getOptionalTag(params(), u"tag"_s);
316 const QString sortedColumn {params()[u"sort"_s]};
317 const bool reverse {parseBool(params()[u"reverse"_s]).value_or(false)};
318 int limit {params()[u"limit"_s].toInt()};
319 int offset {params()[u"offset"_s].toInt()};
320 const QStringList hashes {params()[u"hashes"_s].split(u'|', Qt::SkipEmptyParts)};
321 const std::optional<bool> isPrivate = parseBool(params()[u"private"_s]);
323 std::optional<TorrentIDSet> idSet;
324 if (!hashes.isEmpty())
326 idSet = TorrentIDSet();
327 for (const QString &hash : hashes)
328 idSet->insert(BitTorrent::TorrentID::fromString(hash));
331 const TorrentFilter torrentFilter {filter, idSet, category, tag, isPrivate};
332 QVariantList torrentList;
333 for (const BitTorrent::Torrent *torrent : asConst(BitTorrent::Session::instance()->torrents()))
335 if (torrentFilter.match(torrent))
336 torrentList.append(serialize(*torrent));
339 if (torrentList.isEmpty())
341 setResult(QJsonArray {});
342 return;
345 if (!sortedColumn.isEmpty())
347 if (!torrentList[0].toMap().contains(sortedColumn))
348 throw APIError(APIErrorType::BadParams, tr("'sort' parameter is invalid"));
350 const auto lessThan = [](const QVariant &left, const QVariant &right) -> bool
352 Q_ASSERT(left.userType() == right.userType());
354 switch (left.userType())
356 case QMetaType::Bool:
357 return left.value<bool>() < right.value<bool>();
358 case QMetaType::Double:
359 return left.value<double>() < right.value<double>();
360 case QMetaType::Float:
361 return left.value<float>() < right.value<float>();
362 case QMetaType::Int:
363 return left.value<int>() < right.value<int>();
364 case QMetaType::LongLong:
365 return left.value<qlonglong>() < right.value<qlonglong>();
366 case QMetaType::QString:
367 return left.value<QString>() < right.value<QString>();
368 default:
369 qWarning("Unhandled QVariant comparison, type: %d, name: %s"
370 , left.userType(), left.metaType().name());
371 break;
373 return false;
376 std::sort(torrentList.begin(), torrentList.end()
377 , [reverse, &sortedColumn, &lessThan](const QVariant &torrent1, const QVariant &torrent2)
379 const QVariant value1 {torrent1.toMap().value(sortedColumn)};
380 const QVariant value2 {torrent2.toMap().value(sortedColumn)};
381 return reverse ? lessThan(value2, value1) : lessThan(value1, value2);
385 const int size = torrentList.size();
386 // normalize offset
387 if (offset < 0)
388 offset = size + offset;
389 if ((offset >= size) || (offset < 0))
390 offset = 0;
391 // normalize limit
392 if (limit <= 0)
393 limit = -1; // unlimited
395 if ((limit > 0) || (offset > 0))
396 torrentList = torrentList.mid(offset, limit);
398 setResult(QJsonArray::fromVariantList(torrentList));
401 // Returns the properties for a torrent in JSON format.
402 // The return value is a JSON-formatted dictionary.
403 // The dictionary keys are:
404 // - "time_elapsed": Torrent elapsed time
405 // - "seeding_time": Torrent elapsed time while complete
406 // - "eta": Torrent ETA
407 // - "nb_connections": Torrent connection count
408 // - "nb_connections_limit": Torrent connection count limit
409 // - "total_downloaded": Total data uploaded for torrent
410 // - "total_downloaded_session": Total data downloaded this session
411 // - "total_uploaded": Total data uploaded for torrent
412 // - "total_uploaded_session": Total data uploaded this session
413 // - "dl_speed": Torrent download speed
414 // - "dl_speed_avg": Torrent average download speed
415 // - "up_speed": Torrent upload speed
416 // - "up_speed_avg": Torrent average upload speed
417 // - "dl_limit": Torrent download limit
418 // - "up_limit": Torrent upload limit
419 // - "total_wasted": Total data wasted for torrent
420 // - "seeds": Torrent connected seeds
421 // - "seeds_total": Torrent total number of seeds
422 // - "peers": Torrent connected peers
423 // - "peers_total": Torrent total number of peers
424 // - "share_ratio": Torrent share ratio
425 // - "popularity": Torrent popularity
426 // - "reannounce": Torrent next reannounce time
427 // - "total_size": Torrent total size
428 // - "pieces_num": Torrent pieces count
429 // - "piece_size": Torrent piece size
430 // - "pieces_have": Torrent pieces have
431 // - "created_by": Torrent creator
432 // - "last_seen": Torrent last seen complete
433 // - "addition_date": Torrent addition date
434 // - "completion_date": Torrent completion date
435 // - "creation_date": Torrent creation date
436 // - "save_path": Torrent save path
437 // - "download_path": Torrent download path
438 // - "comment": Torrent comment
439 // - "infohash_v1": Torrent v1 infohash (or empty string for v2 torrents)
440 // - "infohash_v2": Torrent v2 infohash (or empty string for v1 torrents)
441 // - "hash": Torrent TorrentID (infohashv1 for v1 torrents, truncated infohashv2 for v2/hybrid torrents)
442 // - "name": Torrent name
443 void TorrentsController::propertiesAction()
445 requireParams({u"hash"_s});
447 const auto id = BitTorrent::TorrentID::fromString(params()[u"hash"_s]);
448 const BitTorrent::Torrent *const torrent = BitTorrent::Session::instance()->getTorrent(id);
449 if (!torrent)
450 throw APIError(APIErrorType::NotFound);
452 const BitTorrent::InfoHash infoHash = torrent->infoHash();
453 const qlonglong totalDownload = torrent->totalDownload();
454 const qlonglong totalUpload = torrent->totalUpload();
455 const qlonglong dlDuration = torrent->activeTime() - torrent->finishedTime();
456 const qlonglong ulDuration = torrent->activeTime();
457 const int downloadLimit = torrent->downloadLimit();
458 const int uploadLimit = torrent->uploadLimit();
459 const qreal ratio = torrent->realRatio();
460 const qreal popularity = torrent->popularity();
461 const bool hasMetadata = torrent->hasMetadata();
462 const bool isPrivate = torrent->isPrivate();
464 const QJsonObject ret
466 {KEY_TORRENT_INFOHASHV1, infoHash.v1().toString()},
467 {KEY_TORRENT_INFOHASHV2, infoHash.v2().toString()},
468 {KEY_TORRENT_NAME, torrent->name()},
469 {KEY_TORRENT_ID, torrent->id().toString()},
470 {KEY_PROP_TIME_ELAPSED, torrent->activeTime()},
471 {KEY_PROP_SEEDING_TIME, torrent->finishedTime()},
472 {KEY_PROP_ETA, torrent->eta()},
473 {KEY_PROP_CONNECT_COUNT, torrent->connectionsCount()},
474 {KEY_PROP_CONNECT_COUNT_LIMIT, torrent->connectionsLimit()},
475 {KEY_PROP_DOWNLOADED, totalDownload},
476 {KEY_PROP_DOWNLOADED_SESSION, torrent->totalPayloadDownload()},
477 {KEY_PROP_UPLOADED, totalUpload},
478 {KEY_PROP_UPLOADED_SESSION, torrent->totalPayloadUpload()},
479 {KEY_PROP_DL_SPEED, torrent->downloadPayloadRate()},
480 {KEY_PROP_DL_SPEED_AVG, ((dlDuration > 0) ? (totalDownload / dlDuration) : -1)},
481 {KEY_PROP_UP_SPEED, torrent->uploadPayloadRate()},
482 {KEY_PROP_UP_SPEED_AVG, ((ulDuration > 0) ? (totalUpload / ulDuration) : -1)},
483 {KEY_PROP_DL_LIMIT, ((downloadLimit > 0) ? downloadLimit : -1)},
484 {KEY_PROP_UP_LIMIT, ((uploadLimit > 0) ? uploadLimit : -1)},
485 {KEY_PROP_WASTED, torrent->wastedSize()},
486 {KEY_PROP_SEEDS, torrent->seedsCount()},
487 {KEY_PROP_SEEDS_TOTAL, torrent->totalSeedsCount()},
488 {KEY_PROP_PEERS, torrent->leechsCount()},
489 {KEY_PROP_PEERS_TOTAL, torrent->totalLeechersCount()},
490 {KEY_PROP_RATIO, ((ratio > BitTorrent::Torrent::MAX_RATIO) ? -1 : ratio)},
491 {KEY_PROP_POPULARITY, ((popularity > BitTorrent::Torrent::MAX_RATIO) ? -1 : popularity)},
492 {KEY_PROP_REANNOUNCE, torrent->nextAnnounce()},
493 {KEY_PROP_TOTAL_SIZE, torrent->totalSize()},
494 {KEY_PROP_PIECES_NUM, torrent->piecesCount()},
495 {KEY_PROP_PIECE_SIZE, torrent->pieceLength()},
496 {KEY_PROP_PIECES_HAVE, torrent->piecesHave()},
497 {KEY_PROP_CREATED_BY, torrent->creator()},
498 {KEY_PROP_IS_PRIVATE, torrent->isPrivate()}, // used for maintaining backward compatibility
499 {KEY_PROP_PRIVATE, (hasMetadata ? isPrivate : QJsonValue())},
500 {KEY_PROP_ADDITION_DATE, Utils::DateTime::toSecsSinceEpoch(torrent->addedTime())},
501 {KEY_PROP_LAST_SEEN, Utils::DateTime::toSecsSinceEpoch(torrent->lastSeenComplete())},
502 {KEY_PROP_COMPLETION_DATE, Utils::DateTime::toSecsSinceEpoch(torrent->completedTime())},
503 {KEY_PROP_CREATION_DATE, Utils::DateTime::toSecsSinceEpoch(torrent->creationDate())},
504 {KEY_PROP_SAVE_PATH, torrent->savePath().toString()},
505 {KEY_PROP_DOWNLOAD_PATH, torrent->downloadPath().toString()},
506 {KEY_PROP_COMMENT, torrent->comment()},
507 {KEY_PROP_HAS_METADATA, torrent->hasMetadata()},
508 {KEY_PROP_PROGRESS, torrent->progress()}
511 setResult(ret);
514 // Returns the trackers for a torrent in JSON format.
515 // The return value is a JSON-formatted list of dictionaries.
516 // The dictionary keys are:
517 // - "url": Tracker URL
518 // - "status": Tracker status
519 // - "tier": Tracker tier
520 // - "num_peers": Number of peers this torrent is currently connected to
521 // - "num_seeds": Number of peers that have the whole file
522 // - "num_leeches": Number of peers that are still downloading
523 // - "num_downloaded": Tracker downloaded count
524 // - "msg": Tracker message (last)
525 void TorrentsController::trackersAction()
527 requireParams({u"hash"_s});
529 const auto id = BitTorrent::TorrentID::fromString(params()[u"hash"_s]);
530 const BitTorrent::Torrent *const torrent = BitTorrent::Session::instance()->getTorrent(id);
531 if (!torrent)
532 throw APIError(APIErrorType::NotFound);
534 QJsonArray trackerList = getStickyTrackers(torrent);
536 for (const BitTorrent::TrackerEntryStatus &tracker : asConst(torrent->trackers()))
538 const bool isNotWorking = (tracker.state == BitTorrent::TrackerEndpointState::NotWorking)
539 || (tracker.state == BitTorrent::TrackerEndpointState::TrackerError)
540 || (tracker.state == BitTorrent::TrackerEndpointState::Unreachable);
541 trackerList << QJsonObject
543 {KEY_TRACKER_URL, tracker.url},
544 {KEY_TRACKER_TIER, tracker.tier},
545 {KEY_TRACKER_STATUS, static_cast<int>((isNotWorking ? BitTorrent::TrackerEndpointState::NotWorking : tracker.state))},
546 {KEY_TRACKER_MSG, tracker.message},
547 {KEY_TRACKER_PEERS_COUNT, tracker.numPeers},
548 {KEY_TRACKER_SEEDS_COUNT, tracker.numSeeds},
549 {KEY_TRACKER_LEECHES_COUNT, tracker.numLeeches},
550 {KEY_TRACKER_DOWNLOADED_COUNT, tracker.numDownloaded}
554 setResult(trackerList);
557 // Returns the web seeds for a torrent in JSON format.
558 // The return value is a JSON-formatted list of dictionaries.
559 // The dictionary keys are:
560 // - "url": Web seed URL
561 void TorrentsController::webseedsAction()
563 requireParams({u"hash"_s});
565 const auto id = BitTorrent::TorrentID::fromString(params()[u"hash"_s]);
566 BitTorrent::Torrent *const torrent = BitTorrent::Session::instance()->getTorrent(id);
567 if (!torrent)
568 throw APIError(APIErrorType::NotFound);
570 QJsonArray webSeedList;
571 for (const QUrl &webseed : asConst(torrent->urlSeeds()))
573 webSeedList.append(QJsonObject
575 {KEY_WEBSEED_URL, webseed.toString()}
579 setResult(webSeedList);
582 void TorrentsController::addWebSeedsAction()
584 requireParams({u"hash"_s, u"urls"_s});
585 const QStringList paramUrls = params()[u"urls"_s].split(u'|', Qt::SkipEmptyParts);
587 const auto id = BitTorrent::TorrentID::fromString(params()[u"hash"_s]);
588 BitTorrent::Torrent *const torrent = BitTorrent::Session::instance()->getTorrent(id);
589 if (!torrent)
590 throw APIError(APIErrorType::NotFound);
592 QList<QUrl> urls;
593 urls.reserve(paramUrls.size());
594 for (const QString &urlStr : paramUrls)
596 const auto result = validateWebSeedUrl(urlStr);
597 if (!result)
598 throw APIError(APIErrorType::BadParams, result.error());
599 urls << result.value();
602 torrent->addUrlSeeds(urls);
605 void TorrentsController::editWebSeedAction()
607 requireParams({u"hash"_s, u"origUrl"_s, u"newUrl"_s});
609 const auto id = BitTorrent::TorrentID::fromString(params()[u"hash"_s]);
610 const QString origUrlStr = params()[u"origUrl"_s];
611 const QString newUrlStr = params()[u"newUrl"_s];
613 BitTorrent::Torrent *const torrent = BitTorrent::Session::instance()->getTorrent(id);
614 if (!torrent)
615 throw APIError(APIErrorType::NotFound);
617 const auto origUrlResult = validateWebSeedUrl(origUrlStr);
618 if (!origUrlResult)
619 throw APIError(APIErrorType::BadParams, origUrlResult.error());
620 const QUrl origUrl = origUrlResult.value();
622 const auto newUrlResult = validateWebSeedUrl(newUrlStr);
623 if (!newUrlResult)
624 throw APIError(APIErrorType::BadParams, newUrlResult.error());
625 const QUrl newUrl = newUrlResult.value();
627 if (newUrl != origUrl)
629 if (!torrent->urlSeeds().contains(origUrl))
630 throw APIError(APIErrorType::Conflict, tr("\"%1\" is not an existing URL").arg(origUrl.toString()));
632 torrent->removeUrlSeeds({origUrl});
633 torrent->addUrlSeeds({newUrl});
637 void TorrentsController::removeWebSeedsAction()
639 requireParams({u"hash"_s, u"urls"_s});
640 const QStringList paramUrls = params()[u"urls"_s].split(u'|', Qt::SkipEmptyParts);
642 const auto id = BitTorrent::TorrentID::fromString(params()[u"hash"_s]);
643 BitTorrent::Torrent *const torrent = BitTorrent::Session::instance()->getTorrent(id);
644 if (!torrent)
645 throw APIError(APIErrorType::NotFound);
647 QList<QUrl> urls;
648 urls.reserve(paramUrls.size());
649 for (const QString &urlStr : paramUrls)
651 const auto result = validateWebSeedUrl(urlStr);
652 if (!result)
653 throw APIError(APIErrorType::BadParams, result.error());
654 urls << result.value();
657 torrent->removeUrlSeeds(urls);
660 // Returns the files in a torrent in JSON format.
661 // The return value is a JSON-formatted list of dictionaries.
662 // The dictionary keys are:
663 // - "index": File index
664 // - "name": File name
665 // - "size": File size
666 // - "progress": File progress
667 // - "priority": File priority
668 // - "is_seed": Flag indicating if torrent is seeding/complete
669 // - "piece_range": Piece index range, the first number is the starting piece index
670 // and the second number is the ending piece index (inclusive)
671 void TorrentsController::filesAction()
673 requireParams({u"hash"_s});
675 const auto id = BitTorrent::TorrentID::fromString(params()[u"hash"_s]);
676 const BitTorrent::Torrent *const torrent = BitTorrent::Session::instance()->getTorrent(id);
677 if (!torrent)
678 throw APIError(APIErrorType::NotFound);
680 const int filesCount = torrent->filesCount();
681 QList<int> fileIndexes;
682 const auto idxIt = params().constFind(u"indexes"_s);
683 if (idxIt != params().cend())
685 const QStringList indexStrings = idxIt.value().split(u'|');
686 fileIndexes.reserve(indexStrings.size());
687 std::transform(indexStrings.cbegin(), indexStrings.cend(), std::back_inserter(fileIndexes)
688 , [&filesCount](const QString &indexString) -> int
690 bool ok = false;
691 const int index = indexString.toInt(&ok);
692 if (!ok || (index < 0))
693 throw APIError(APIErrorType::Conflict, tr("\"%1\" is not a valid file index.").arg(indexString));
694 if (index >= filesCount)
695 throw APIError(APIErrorType::Conflict, tr("Index %1 is out of bounds.").arg(indexString));
696 return index;
699 else
701 fileIndexes.reserve(filesCount);
702 for (int i = 0; i < filesCount; ++i)
703 fileIndexes.append(i);
706 QJsonArray fileList;
707 if (torrent->hasMetadata())
709 const QList<BitTorrent::DownloadPriority> priorities = torrent->filePriorities();
710 const QList<qreal> fp = torrent->filesProgress();
711 const QList<qreal> fileAvailability = torrent->availableFileFractions();
712 const BitTorrent::TorrentInfo info = torrent->info();
713 for (const int index : asConst(fileIndexes))
715 QJsonObject fileDict =
717 {KEY_FILE_INDEX, index},
718 {KEY_FILE_PROGRESS, fp[index]},
719 {KEY_FILE_PRIORITY, static_cast<int>(priorities[index])},
720 {KEY_FILE_SIZE, torrent->fileSize(index)},
721 {KEY_FILE_AVAILABILITY, fileAvailability[index]},
722 // need to provide paths using a platform-independent separator format
723 {KEY_FILE_NAME, torrent->filePath(index).data()}
726 const BitTorrent::TorrentInfo::PieceRange idx = info.filePieces(index);
727 fileDict[KEY_FILE_PIECE_RANGE] = QJsonArray {idx.first(), idx.last()};
729 if (index == 0)
730 fileDict[KEY_FILE_IS_SEED] = torrent->isFinished();
732 fileList.append(fileDict);
736 setResult(fileList);
739 // Returns an array of hashes (of each pieces respectively) for a torrent in JSON format.
740 // The return value is a JSON-formatted array of strings (hex strings).
741 void TorrentsController::pieceHashesAction()
743 requireParams({u"hash"_s});
745 const auto id = BitTorrent::TorrentID::fromString(params()[u"hash"_s]);
746 BitTorrent::Torrent *const torrent = BitTorrent::Session::instance()->getTorrent(id);
747 if (!torrent)
748 throw APIError(APIErrorType::NotFound);
750 QJsonArray pieceHashes;
751 if (torrent->hasMetadata())
753 const QList<QByteArray> hashes = torrent->info().pieceHashes();
754 for (const QByteArray &hash : hashes)
755 pieceHashes.append(QString::fromLatin1(hash.toHex()));
758 setResult(pieceHashes);
761 // Returns an array of states (of each pieces respectively) for a torrent in JSON format.
762 // The return value is a JSON-formatted array of ints.
763 // 0: piece not downloaded
764 // 1: piece requested or downloading
765 // 2: piece already downloaded
766 void TorrentsController::pieceStatesAction()
768 requireParams({u"hash"_s});
770 const auto id = BitTorrent::TorrentID::fromString(params()[u"hash"_s]);
771 BitTorrent::Torrent *const torrent = BitTorrent::Session::instance()->getTorrent(id);
772 if (!torrent)
773 throw APIError(APIErrorType::NotFound);
775 QJsonArray pieceStates;
776 const QBitArray states = torrent->pieces();
777 for (int i = 0; i < states.size(); ++i)
778 pieceStates.append(static_cast<int>(states[i]) * 2);
780 const QBitArray dlstates = torrent->downloadingPieces();
781 for (int i = 0; i < states.size(); ++i)
783 if (dlstates[i])
784 pieceStates[i] = 1;
787 setResult(pieceStates);
790 void TorrentsController::addAction()
792 const QString urls = params()[u"urls"_s];
794 const bool skipChecking = parseBool(params()[u"skip_checking"_s]).value_or(false);
795 const bool seqDownload = parseBool(params()[u"sequentialDownload"_s]).value_or(false);
796 const bool firstLastPiece = parseBool(params()[u"firstLastPiecePrio"_s]).value_or(false);
797 const std::optional<bool> addToQueueTop = parseBool(params()[u"addToTopOfQueue"_s]);
798 const std::optional<bool> addStopped = parseBool(params()[u"stopped"_s]);
799 const QString savepath = params()[u"savepath"_s].trimmed();
800 const QString downloadPath = params()[u"downloadPath"_s].trimmed();
801 const std::optional<bool> useDownloadPath = parseBool(params()[u"useDownloadPath"_s]);
802 const QString category = params()[u"category"_s];
803 const QStringList tags = params()[u"tags"_s].split(u',', Qt::SkipEmptyParts);
804 const QString torrentName = params()[u"rename"_s].trimmed();
805 const int upLimit = parseInt(params()[u"upLimit"_s]).value_or(-1);
806 const int dlLimit = parseInt(params()[u"dlLimit"_s]).value_or(-1);
807 const double ratioLimit = parseDouble(params()[u"ratioLimit"_s]).value_or(BitTorrent::Torrent::USE_GLOBAL_RATIO);
808 const int seedingTimeLimit = parseInt(params()[u"seedingTimeLimit"_s]).value_or(BitTorrent::Torrent::USE_GLOBAL_SEEDING_TIME);
809 const int inactiveSeedingTimeLimit = parseInt(params()[u"inactiveSeedingTimeLimit"_s]).value_or(BitTorrent::Torrent::USE_GLOBAL_INACTIVE_SEEDING_TIME);
810 const BitTorrent::ShareLimitAction shareLimitAction = Utils::String::toEnum(params()[u"shareLimitAction"_s], BitTorrent::ShareLimitAction::Default);
811 const std::optional<bool> autoTMM = parseBool(params()[u"autoTMM"_s]);
813 const QString stopConditionParam = params()[u"stopCondition"_s];
814 const std::optional<BitTorrent::Torrent::StopCondition> stopCondition = (!stopConditionParam.isEmpty()
815 ? Utils::String::toEnum(stopConditionParam, BitTorrent::Torrent::StopCondition::None)
816 : std::optional<BitTorrent::Torrent::StopCondition> {});
818 const QString contentLayoutParam = params()[u"contentLayout"_s];
819 const std::optional<BitTorrent::TorrentContentLayout> contentLayout = (!contentLayoutParam.isEmpty()
820 ? Utils::String::toEnum(contentLayoutParam, BitTorrent::TorrentContentLayout::Original)
821 : std::optional<BitTorrent::TorrentContentLayout> {});
823 const BitTorrent::AddTorrentParams addTorrentParams
825 // TODO: Check if destination actually exists
826 .name = torrentName,
827 .category = category,
828 .tags = {tags.cbegin(), tags.cend()},
829 .savePath = Path(savepath),
830 .useDownloadPath = useDownloadPath,
831 .downloadPath = Path(downloadPath),
832 .sequential = seqDownload,
833 .firstLastPiecePriority = firstLastPiece,
834 .addForced = false,
835 .addToQueueTop = addToQueueTop,
836 .addStopped = addStopped,
837 .stopCondition = stopCondition,
838 .filePaths = {},
839 .filePriorities = {},
840 .skipChecking = skipChecking,
841 .contentLayout = contentLayout,
842 .useAutoTMM = autoTMM,
843 .uploadLimit = upLimit,
844 .downloadLimit = dlLimit,
845 .seedingTimeLimit = seedingTimeLimit,
846 .inactiveSeedingTimeLimit = inactiveSeedingTimeLimit,
847 .ratioLimit = ratioLimit,
848 .shareLimitAction = shareLimitAction,
849 .sslParameters =
851 .certificate = QSslCertificate(params()[KEY_PROP_SSL_CERTIFICATE].toLatin1()),
852 .privateKey = Utils::SSLKey::load(params()[KEY_PROP_SSL_PRIVATEKEY].toLatin1()),
853 .dhParams = params()[KEY_PROP_SSL_DHPARAMS].toLatin1()
857 bool partialSuccess = false;
858 for (QString url : asConst(urls.split(u'\n')))
860 url = url.trimmed();
861 if (!url.isEmpty())
863 partialSuccess |= app()->addTorrentManager()->addTorrent(url, addTorrentParams);
867 const DataMap &torrents = data();
868 for (auto it = torrents.constBegin(); it != torrents.constEnd(); ++it)
870 if (const auto loadResult = BitTorrent::TorrentDescriptor::load(it.value()))
872 partialSuccess |= BitTorrent::Session::instance()->addTorrent(loadResult.value(), addTorrentParams);
874 else
876 throw APIError(APIErrorType::BadData, tr("Error: '%1' is not a valid torrent file.").arg(it.key()));
880 if (partialSuccess)
881 setResult(u"Ok."_s);
882 else
883 setResult(u"Fails."_s);
886 void TorrentsController::addTrackersAction()
888 requireParams({u"hash"_s, u"urls"_s});
890 const auto id = BitTorrent::TorrentID::fromString(params()[u"hash"_s]);
891 BitTorrent::Torrent *const torrent = BitTorrent::Session::instance()->getTorrent(id);
892 if (!torrent)
893 throw APIError(APIErrorType::NotFound);
895 const QList<BitTorrent::TrackerEntry> entries = BitTorrent::parseTrackerEntries(params()[u"urls"_s]);
896 torrent->addTrackers(entries);
899 void TorrentsController::editTrackerAction()
901 requireParams({u"hash"_s, u"origUrl"_s, u"newUrl"_s});
903 const auto id = BitTorrent::TorrentID::fromString(params()[u"hash"_s]);
904 const QString origUrl = params()[u"origUrl"_s];
905 const QString newUrl = params()[u"newUrl"_s];
907 BitTorrent::Torrent *const torrent = BitTorrent::Session::instance()->getTorrent(id);
908 if (!torrent)
909 throw APIError(APIErrorType::NotFound);
911 const QUrl origTrackerUrl {origUrl};
912 const QUrl newTrackerUrl {newUrl};
913 if (origTrackerUrl == newTrackerUrl)
914 return;
915 if (!newTrackerUrl.isValid())
916 throw APIError(APIErrorType::BadParams, u"New tracker URL is invalid"_s);
918 const QList<BitTorrent::TrackerEntryStatus> currentTrackers = torrent->trackers();
919 QList<BitTorrent::TrackerEntry> entries;
920 entries.reserve(currentTrackers.size());
922 bool match = false;
923 for (const BitTorrent::TrackerEntryStatus &tracker : currentTrackers)
925 const QUrl trackerUrl {tracker.url};
927 if (trackerUrl == newTrackerUrl)
928 throw APIError(APIErrorType::Conflict, u"New tracker URL already exists"_s);
930 BitTorrent::TrackerEntry entry
932 .url = tracker.url,
933 .tier = tracker.tier
936 if (trackerUrl == origTrackerUrl)
938 match = true;
939 entry.url = newTrackerUrl.toString();
941 entries.append(entry);
943 if (!match)
944 throw APIError(APIErrorType::Conflict, u"Tracker not found"_s);
946 torrent->replaceTrackers(entries);
948 if (!torrent->isStopped())
949 torrent->forceReannounce();
952 void TorrentsController::removeTrackersAction()
954 requireParams({u"hash"_s, u"urls"_s});
956 const QString hashParam = params()[u"hash"_s];
957 const QStringList urlsParam = params()[u"urls"_s].split(u'|', Qt::SkipEmptyParts);
959 QStringList urls;
960 urls.reserve(urlsParam.size());
961 for (const QString &urlStr : urlsParam)
962 urls << QUrl::fromPercentEncoding(urlStr.toLatin1());
964 QList<BitTorrent::Torrent *> torrents;
966 if (hashParam == u"*"_s)
968 // remove trackers from all torrents
969 torrents = BitTorrent::Session::instance()->torrents();
971 else
973 // remove trackers from specified torrent
974 const auto id = BitTorrent::TorrentID::fromString(hashParam);
975 BitTorrent::Torrent *const torrent = BitTorrent::Session::instance()->getTorrent(id);
976 if (!torrent)
977 throw APIError(APIErrorType::NotFound);
979 torrents.append(torrent);
982 for (BitTorrent::Torrent *const torrent : asConst(torrents))
983 torrent->removeTrackers(urls);
986 void TorrentsController::addPeersAction()
988 requireParams({u"hashes"_s, u"peers"_s});
990 const QStringList hashes = params()[u"hashes"_s].split(u'|');
991 const QStringList peers = params()[u"peers"_s].split(u'|');
993 QList<BitTorrent::PeerAddress> peerList;
994 peerList.reserve(peers.size());
995 for (const QString &peer : peers)
997 const BitTorrent::PeerAddress addr = BitTorrent::PeerAddress::parse(peer.trimmed());
998 if (!addr.ip.isNull())
999 peerList.append(addr);
1002 if (peerList.isEmpty())
1003 throw APIError(APIErrorType::BadParams, u"No valid peers were specified"_s);
1005 QJsonObject results;
1007 applyToTorrents(hashes, [peers, peerList, &results](BitTorrent::Torrent *const torrent)
1009 const int peersAdded = std::count_if(peerList.cbegin(), peerList.cend(), [torrent](const BitTorrent::PeerAddress &peer)
1011 return torrent->connectPeer(peer);
1014 results[torrent->id().toString()] = QJsonObject
1016 {u"added"_s, peersAdded},
1017 {u"failed"_s, (peers.size() - peersAdded)}
1021 setResult(results);
1024 void TorrentsController::stopAction()
1026 requireParams({u"hashes"_s});
1028 const QStringList hashes = params()[u"hashes"_s].split(u'|');
1029 applyToTorrents(hashes, [](BitTorrent::Torrent *const torrent) { torrent->stop(); });
1032 void TorrentsController::startAction()
1034 requireParams({u"hashes"_s});
1036 const QStringList idStrings = params()[u"hashes"_s].split(u'|');
1037 applyToTorrents(idStrings, [](BitTorrent::Torrent *const torrent) { torrent->start(); });
1040 void TorrentsController::filePrioAction()
1042 requireParams({u"hash"_s, u"id"_s, u"priority"_s});
1044 const auto id = BitTorrent::TorrentID::fromString(params()[u"hash"_s]);
1045 bool ok = false;
1046 const auto priority = static_cast<BitTorrent::DownloadPriority>(params()[u"priority"_s].toInt(&ok));
1047 if (!ok)
1048 throw APIError(APIErrorType::BadParams, tr("Priority must be an integer"));
1050 if (!BitTorrent::isValidDownloadPriority(priority))
1051 throw APIError(APIErrorType::BadParams, tr("Priority is not valid"));
1053 BitTorrent::Torrent *const torrent = BitTorrent::Session::instance()->getTorrent(id);
1054 if (!torrent)
1055 throw APIError(APIErrorType::NotFound);
1056 if (!torrent->hasMetadata())
1057 throw APIError(APIErrorType::Conflict, tr("Torrent's metadata has not yet downloaded"));
1059 const int filesCount = torrent->filesCount();
1060 QList<BitTorrent::DownloadPriority> priorities = torrent->filePriorities();
1061 bool priorityChanged = false;
1062 for (const QString &fileID : params()[u"id"_s].split(u'|'))
1064 const int id = fileID.toInt(&ok);
1065 if (!ok)
1066 throw APIError(APIErrorType::BadParams, tr("File IDs must be integers"));
1067 if ((id < 0) || (id >= filesCount))
1068 throw APIError(APIErrorType::Conflict, tr("File ID is not valid"));
1070 if (priorities[id] != priority)
1072 priorities[id] = priority;
1073 priorityChanged = true;
1077 if (priorityChanged)
1078 torrent->prioritizeFiles(priorities);
1081 void TorrentsController::uploadLimitAction()
1083 requireParams({u"hashes"_s});
1085 const QStringList idList {params()[u"hashes"_s].split(u'|')};
1086 QJsonObject map;
1087 for (const QString &id : idList)
1089 int limit = -1;
1090 const BitTorrent::Torrent *const torrent = BitTorrent::Session::instance()->getTorrent(BitTorrent::TorrentID::fromString(id));
1091 if (torrent)
1092 limit = torrent->uploadLimit();
1093 map[id] = limit;
1096 setResult(map);
1099 void TorrentsController::downloadLimitAction()
1101 requireParams({u"hashes"_s});
1103 const QStringList idList {params()[u"hashes"_s].split(u'|')};
1104 QJsonObject map;
1105 for (const QString &id : idList)
1107 int limit = -1;
1108 const BitTorrent::Torrent *const torrent = BitTorrent::Session::instance()->getTorrent(BitTorrent::TorrentID::fromString(id));
1109 if (torrent)
1110 limit = torrent->downloadLimit();
1111 map[id] = limit;
1114 setResult(map);
1117 void TorrentsController::setUploadLimitAction()
1119 requireParams({u"hashes"_s, u"limit"_s});
1121 qlonglong limit = params()[u"limit"_s].toLongLong();
1122 if (limit == 0)
1123 limit = -1;
1125 const QStringList hashes {params()[u"hashes"_s].split(u'|')};
1126 applyToTorrents(hashes, [limit](BitTorrent::Torrent *const torrent) { torrent->setUploadLimit(limit); });
1129 void TorrentsController::setDownloadLimitAction()
1131 requireParams({u"hashes"_s, u"limit"_s});
1133 qlonglong limit = params()[u"limit"_s].toLongLong();
1134 if (limit == 0)
1135 limit = -1;
1137 const QStringList hashes {params()[u"hashes"_s].split(u'|')};
1138 applyToTorrents(hashes, [limit](BitTorrent::Torrent *const torrent) { torrent->setDownloadLimit(limit); });
1141 void TorrentsController::setShareLimitsAction()
1143 requireParams({u"hashes"_s, u"ratioLimit"_s, u"seedingTimeLimit"_s, u"inactiveSeedingTimeLimit"_s});
1145 const qreal ratioLimit = params()[u"ratioLimit"_s].toDouble();
1146 const qlonglong seedingTimeLimit = params()[u"seedingTimeLimit"_s].toLongLong();
1147 const qlonglong inactiveSeedingTimeLimit = params()[u"inactiveSeedingTimeLimit"_s].toLongLong();
1148 const QStringList hashes = params()[u"hashes"_s].split(u'|');
1150 applyToTorrents(hashes, [ratioLimit, seedingTimeLimit, inactiveSeedingTimeLimit](BitTorrent::Torrent *const torrent)
1152 torrent->setRatioLimit(ratioLimit);
1153 torrent->setSeedingTimeLimit(seedingTimeLimit);
1154 torrent->setInactiveSeedingTimeLimit(inactiveSeedingTimeLimit);
1158 void TorrentsController::toggleSequentialDownloadAction()
1160 requireParams({u"hashes"_s});
1162 const QStringList hashes {params()[u"hashes"_s].split(u'|')};
1163 applyToTorrents(hashes, [](BitTorrent::Torrent *const torrent) { torrent->toggleSequentialDownload(); });
1166 void TorrentsController::toggleFirstLastPiecePrioAction()
1168 requireParams({u"hashes"_s});
1170 const QStringList hashes {params()[u"hashes"_s].split(u'|')};
1171 applyToTorrents(hashes, [](BitTorrent::Torrent *const torrent) { torrent->toggleFirstLastPiecePriority(); });
1174 void TorrentsController::setSuperSeedingAction()
1176 requireParams({u"hashes"_s, u"value"_s});
1178 const bool value {parseBool(params()[u"value"_s]).value_or(false)};
1179 const QStringList hashes {params()[u"hashes"_s].split(u'|')};
1180 applyToTorrents(hashes, [value](BitTorrent::Torrent *const torrent) { torrent->setSuperSeeding(value); });
1183 void TorrentsController::setForceStartAction()
1185 requireParams({u"hashes"_s, u"value"_s});
1187 const bool value {parseBool(params()[u"value"_s]).value_or(false)};
1188 const QStringList hashes {params()[u"hashes"_s].split(u'|')};
1189 applyToTorrents(hashes, [value](BitTorrent::Torrent *const torrent)
1191 torrent->start(value ? BitTorrent::TorrentOperatingMode::Forced : BitTorrent::TorrentOperatingMode::AutoManaged);
1195 void TorrentsController::deleteAction()
1197 requireParams({u"hashes"_s, u"deleteFiles"_s});
1199 const QStringList hashes {params()[u"hashes"_s].split(u'|')};
1200 const BitTorrent::TorrentRemoveOption deleteOption = parseBool(params()[u"deleteFiles"_s]).value_or(false)
1201 ? BitTorrent::TorrentRemoveOption::RemoveContent : BitTorrent::TorrentRemoveOption::KeepContent;
1202 applyToTorrents(hashes, [deleteOption](const BitTorrent::Torrent *torrent)
1204 BitTorrent::Session::instance()->removeTorrent(torrent->id(), deleteOption);
1208 void TorrentsController::increasePrioAction()
1210 requireParams({u"hashes"_s});
1212 if (!BitTorrent::Session::instance()->isQueueingSystemEnabled())
1213 throw APIError(APIErrorType::Conflict, tr("Torrent queueing must be enabled"));
1215 const QStringList hashes {params()[u"hashes"_s].split(u'|')};
1216 BitTorrent::Session::instance()->increaseTorrentsQueuePos(toTorrentIDs(hashes));
1219 void TorrentsController::decreasePrioAction()
1221 requireParams({u"hashes"_s});
1223 if (!BitTorrent::Session::instance()->isQueueingSystemEnabled())
1224 throw APIError(APIErrorType::Conflict, tr("Torrent queueing must be enabled"));
1226 const QStringList hashes {params()[u"hashes"_s].split(u'|')};
1227 BitTorrent::Session::instance()->decreaseTorrentsQueuePos(toTorrentIDs(hashes));
1230 void TorrentsController::topPrioAction()
1232 requireParams({u"hashes"_s});
1234 if (!BitTorrent::Session::instance()->isQueueingSystemEnabled())
1235 throw APIError(APIErrorType::Conflict, tr("Torrent queueing must be enabled"));
1237 const QStringList hashes {params()[u"hashes"_s].split(u'|')};
1238 BitTorrent::Session::instance()->topTorrentsQueuePos(toTorrentIDs(hashes));
1241 void TorrentsController::bottomPrioAction()
1243 requireParams({u"hashes"_s});
1245 if (!BitTorrent::Session::instance()->isQueueingSystemEnabled())
1246 throw APIError(APIErrorType::Conflict, tr("Torrent queueing must be enabled"));
1248 const QStringList hashes {params()[u"hashes"_s].split(u'|')};
1249 BitTorrent::Session::instance()->bottomTorrentsQueuePos(toTorrentIDs(hashes));
1252 void TorrentsController::setLocationAction()
1254 requireParams({u"hashes"_s, u"location"_s});
1256 const QStringList hashes {params()[u"hashes"_s].split(u'|')};
1257 const Path newLocation {params()[u"location"_s].trimmed()};
1259 if (newLocation.isEmpty())
1260 throw APIError(APIErrorType::BadParams, tr("Save path cannot be empty"));
1262 // try to create the location if it does not exist
1263 if (!Utils::Fs::mkpath(newLocation))
1264 throw APIError(APIErrorType::Conflict, tr("Cannot make save path"));
1266 applyToTorrents(hashes, [newLocation](BitTorrent::Torrent *const torrent)
1268 LogMsg(tr("WebUI Set location: moving \"%1\", from \"%2\" to \"%3\"")
1269 .arg(torrent->name(), torrent->savePath().toString(), newLocation.toString()));
1270 torrent->setAutoTMMEnabled(false);
1271 torrent->setSavePath(newLocation);
1275 void TorrentsController::setSavePathAction()
1277 requireParams({u"id"_s, u"path"_s});
1279 const QStringList ids {params()[u"id"_s].split(u'|')};
1280 const Path newPath {params()[u"path"_s]};
1282 if (newPath.isEmpty())
1283 throw APIError(APIErrorType::BadParams, tr("Save path cannot be empty"));
1285 // try to create the directory if it does not exist
1286 if (!Utils::Fs::mkpath(newPath))
1287 throw APIError(APIErrorType::Conflict, tr("Cannot create target directory"));
1289 // check permissions
1290 if (!Utils::Fs::isWritable(newPath))
1291 throw APIError(APIErrorType::AccessDenied, tr("Cannot write to directory"));
1293 applyToTorrents(ids, [&newPath](BitTorrent::Torrent *const torrent)
1295 if (!torrent->isAutoTMMEnabled())
1296 torrent->setSavePath(newPath);
1300 void TorrentsController::setDownloadPathAction()
1302 requireParams({u"id"_s, u"path"_s});
1304 const QStringList ids {params()[u"id"_s].split(u'|')};
1305 const Path newPath {params()[u"path"_s]};
1307 if (!newPath.isEmpty())
1309 // try to create the directory if it does not exist
1310 if (!Utils::Fs::mkpath(newPath))
1311 throw APIError(APIErrorType::Conflict, tr("Cannot create target directory"));
1313 // check permissions
1314 if (!Utils::Fs::isWritable(newPath))
1315 throw APIError(APIErrorType::AccessDenied, tr("Cannot write to directory"));
1318 applyToTorrents(ids, [&newPath](BitTorrent::Torrent *const torrent)
1320 if (!torrent->isAutoTMMEnabled())
1321 torrent->setDownloadPath(newPath);
1325 void TorrentsController::renameAction()
1327 requireParams({u"hash"_s, u"name"_s});
1329 const auto id = BitTorrent::TorrentID::fromString(params()[u"hash"_s]);
1330 QString name = params()[u"name"_s].trimmed();
1332 if (name.isEmpty())
1333 throw APIError(APIErrorType::Conflict, tr("Incorrect torrent name"));
1335 BitTorrent::Torrent *const torrent = BitTorrent::Session::instance()->getTorrent(id);
1336 if (!torrent)
1337 throw APIError(APIErrorType::NotFound);
1339 name.replace(QRegularExpression(u"\r?\n|\r"_s), u" "_s);
1340 torrent->setName(name);
1343 void TorrentsController::setAutoManagementAction()
1345 requireParams({u"hashes"_s, u"enable"_s});
1347 const QStringList hashes {params()[u"hashes"_s].split(u'|')};
1348 const bool isEnabled {parseBool(params()[u"enable"_s]).value_or(false)};
1350 applyToTorrents(hashes, [isEnabled](BitTorrent::Torrent *const torrent)
1352 torrent->setAutoTMMEnabled(isEnabled);
1356 void TorrentsController::recheckAction()
1358 requireParams({u"hashes"_s});
1360 const QStringList hashes {params()[u"hashes"_s].split(u'|')};
1361 applyToTorrents(hashes, [](BitTorrent::Torrent *const torrent) { torrent->forceRecheck(); });
1364 void TorrentsController::reannounceAction()
1366 requireParams({u"hashes"_s});
1368 const QStringList hashes {params()[u"hashes"_s].split(u'|')};
1369 applyToTorrents(hashes, [](BitTorrent::Torrent *const torrent) { torrent->forceReannounce(); });
1372 void TorrentsController::setCategoryAction()
1374 requireParams({u"hashes"_s, u"category"_s});
1376 const QStringList hashes {params()[u"hashes"_s].split(u'|')};
1377 const QString category {params()[u"category"_s]};
1379 applyToTorrents(hashes, [category](BitTorrent::Torrent *const torrent)
1381 if (!torrent->setCategory(category))
1382 throw APIError(APIErrorType::Conflict, tr("Incorrect category name"));
1386 void TorrentsController::createCategoryAction()
1388 requireParams({u"category"_s});
1390 const QString category = params()[u"category"_s];
1391 if (category.isEmpty())
1392 throw APIError(APIErrorType::BadParams, tr("Category cannot be empty"));
1394 if (!BitTorrent::Session::isValidCategoryName(category))
1395 throw APIError(APIErrorType::Conflict, tr("Incorrect category name"));
1397 const Path savePath {params()[u"savePath"_s]};
1398 const auto useDownloadPath = parseBool(params()[u"downloadPathEnabled"_s]);
1399 BitTorrent::CategoryOptions categoryOptions;
1400 categoryOptions.savePath = savePath;
1401 if (useDownloadPath.has_value())
1403 const Path downloadPath {params()[u"downloadPath"_s]};
1404 categoryOptions.downloadPath = {useDownloadPath.value(), downloadPath};
1407 if (!BitTorrent::Session::instance()->addCategory(category, categoryOptions))
1408 throw APIError(APIErrorType::Conflict, tr("Unable to create category"));
1411 void TorrentsController::editCategoryAction()
1413 requireParams({u"category"_s, u"savePath"_s});
1415 const QString category = params()[u"category"_s];
1416 if (category.isEmpty())
1417 throw APIError(APIErrorType::BadParams, tr("Category cannot be empty"));
1419 const Path savePath {params()[u"savePath"_s]};
1420 const auto useDownloadPath = parseBool(params()[u"downloadPathEnabled"_s]);
1421 BitTorrent::CategoryOptions categoryOptions;
1422 categoryOptions.savePath = savePath;
1423 if (useDownloadPath.has_value())
1425 const Path downloadPath {params()[u"downloadPath"_s]};
1426 categoryOptions.downloadPath = {useDownloadPath.value(), downloadPath};
1429 if (!BitTorrent::Session::instance()->editCategory(category, categoryOptions))
1430 throw APIError(APIErrorType::Conflict, tr("Unable to edit category"));
1433 void TorrentsController::removeCategoriesAction()
1435 requireParams({u"categories"_s});
1437 const QStringList categories {params()[u"categories"_s].split(u'\n')};
1438 for (const QString &category : categories)
1439 BitTorrent::Session::instance()->removeCategory(category);
1442 void TorrentsController::categoriesAction()
1444 const auto *session = BitTorrent::Session::instance();
1446 QJsonObject categories;
1447 const QStringList categoriesList = session->categories();
1448 for (const auto &categoryName : categoriesList)
1450 const BitTorrent::CategoryOptions categoryOptions = session->categoryOptions(categoryName);
1451 QJsonObject category = categoryOptions.toJSON();
1452 // adjust it to be compatible with existing WebAPI
1453 category[u"savePath"_s] = category.take(u"save_path"_s);
1454 category.insert(u"name"_s, categoryName);
1455 categories[categoryName] = category;
1458 setResult(categories);
1461 void TorrentsController::addTagsAction()
1463 requireParams({u"hashes"_s, u"tags"_s});
1465 const QStringList hashes {params()[u"hashes"_s].split(u'|')};
1466 const QStringList tags {params()[u"tags"_s].split(u',', Qt::SkipEmptyParts)};
1468 for (const QString &tagStr : tags)
1470 applyToTorrents(hashes, [&tagStr](BitTorrent::Torrent *const torrent)
1472 torrent->addTag(Tag(tagStr));
1477 void TorrentsController::removeTagsAction()
1479 requireParams({u"hashes"_s});
1481 const QStringList hashes {params()[u"hashes"_s].split(u'|')};
1482 const QStringList tags {params()[u"tags"_s].split(u',', Qt::SkipEmptyParts)};
1484 for (const QString &tagStr : tags)
1486 applyToTorrents(hashes, [&tagStr](BitTorrent::Torrent *const torrent)
1488 torrent->removeTag(Tag(tagStr));
1492 if (tags.isEmpty())
1494 applyToTorrents(hashes, [](BitTorrent::Torrent *const torrent)
1496 torrent->removeAllTags();
1501 void TorrentsController::createTagsAction()
1503 requireParams({u"tags"_s});
1505 const QStringList tags {params()[u"tags"_s].split(u',', Qt::SkipEmptyParts)};
1507 for (const QString &tagStr : tags)
1508 BitTorrent::Session::instance()->addTag(Tag(tagStr));
1511 void TorrentsController::deleteTagsAction()
1513 requireParams({u"tags"_s});
1515 const QStringList tags {params()[u"tags"_s].split(u',', Qt::SkipEmptyParts)};
1516 for (const QString &tagStr : tags)
1517 BitTorrent::Session::instance()->removeTag(Tag(tagStr));
1520 void TorrentsController::tagsAction()
1522 QJsonArray result;
1523 for (const Tag &tag : asConst(BitTorrent::Session::instance()->tags()))
1524 result << tag.toString();
1525 setResult(result);
1528 void TorrentsController::renameFileAction()
1530 requireParams({u"hash"_s, u"oldPath"_s, u"newPath"_s});
1532 const auto id = BitTorrent::TorrentID::fromString(params()[u"hash"_s]);
1533 BitTorrent::Torrent *const torrent = BitTorrent::Session::instance()->getTorrent(id);
1534 if (!torrent)
1535 throw APIError(APIErrorType::NotFound);
1537 const Path oldPath {params()[u"oldPath"_s]};
1538 const Path newPath {params()[u"newPath"_s]};
1542 torrent->renameFile(oldPath, newPath);
1544 catch (const RuntimeError &error)
1546 throw APIError(APIErrorType::Conflict, error.message());
1550 void TorrentsController::renameFolderAction()
1552 requireParams({u"hash"_s, u"oldPath"_s, u"newPath"_s});
1554 const auto id = BitTorrent::TorrentID::fromString(params()[u"hash"_s]);
1555 BitTorrent::Torrent *const torrent = BitTorrent::Session::instance()->getTorrent(id);
1556 if (!torrent)
1557 throw APIError(APIErrorType::NotFound);
1559 const Path oldPath {params()[u"oldPath"_s]};
1560 const Path newPath {params()[u"newPath"_s]};
1564 torrent->renameFolder(oldPath, newPath);
1566 catch (const RuntimeError &error)
1568 throw APIError(APIErrorType::Conflict, error.message());
1572 void TorrentsController::exportAction()
1574 requireParams({u"hash"_s});
1576 const auto id = BitTorrent::TorrentID::fromString(params()[u"hash"_s]);
1577 const BitTorrent::Torrent *const torrent = BitTorrent::Session::instance()->getTorrent(id);
1578 if (!torrent)
1579 throw APIError(APIErrorType::NotFound);
1581 const nonstd::expected<QByteArray, QString> result = torrent->exportToBuffer();
1582 if (!result)
1583 throw APIError(APIErrorType::Conflict, tr("Unable to export torrent file. Error: %1").arg(result.error()));
1585 setResult(result.value(), u"application/x-bittorrent"_s, (id.toString() + u".torrent"));
1588 void TorrentsController::SSLParametersAction()
1590 requireParams({u"hash"_s});
1592 const auto id = BitTorrent::TorrentID::fromString(params()[u"hash"_s]);
1593 const BitTorrent::Torrent *torrent = BitTorrent::Session::instance()->getTorrent(id);
1594 if (!torrent)
1595 throw APIError(APIErrorType::NotFound);
1597 const BitTorrent::SSLParameters sslParams = torrent->getSSLParameters();
1598 const QJsonObject ret
1600 {KEY_PROP_SSL_CERTIFICATE, QString::fromLatin1(sslParams.certificate.toPem())},
1601 {KEY_PROP_SSL_PRIVATEKEY, QString::fromLatin1(sslParams.privateKey.toPem())},
1602 {KEY_PROP_SSL_DHPARAMS, QString::fromLatin1(sslParams.dhParams)}
1604 setResult(ret);
1607 void TorrentsController::setSSLParametersAction()
1609 requireParams({u"hash"_s, KEY_PROP_SSL_CERTIFICATE, KEY_PROP_SSL_PRIVATEKEY, KEY_PROP_SSL_DHPARAMS});
1611 const auto id = BitTorrent::TorrentID::fromString(params()[u"hash"_s]);
1612 BitTorrent::Torrent *const torrent = BitTorrent::Session::instance()->getTorrent(id);
1613 if (!torrent)
1614 throw APIError(APIErrorType::NotFound);
1616 const BitTorrent::SSLParameters sslParams
1618 .certificate = QSslCertificate(params()[KEY_PROP_SSL_CERTIFICATE].toLatin1()),
1619 .privateKey = Utils::SSLKey::load(params()[KEY_PROP_SSL_PRIVATEKEY].toLatin1()),
1620 .dhParams = params()[KEY_PROP_SSL_DHPARAMS].toLatin1()
1622 if (!sslParams.isValid())
1623 throw APIError(APIErrorType::BadData);
1625 torrent->setSSLParameters(sslParams);