Revise Tracker related classes
[qBittorrent.git] / src / webui / api / torrentscontroller.cpp
blob76e0a07d79ad015f266b2175d309e11b29be5dfe
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 <functional>
33 #include <QBitArray>
34 #include <QJsonArray>
35 #include <QJsonObject>
36 #include <QList>
37 #include <QNetworkCookie>
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/net/downloadmanager.h"
57 #include "base/torrentfilter.h"
58 #include "base/utils/datetime.h"
59 #include "base/utils/fs.h"
60 #include "base/utils/sslkey.h"
61 #include "base/utils/string.h"
62 #include "apierror.h"
63 #include "serialize/serialize_torrent.h"
65 // Tracker keys
66 const QString KEY_TRACKER_URL = u"url"_s;
67 const QString KEY_TRACKER_STATUS = u"status"_s;
68 const QString KEY_TRACKER_TIER = u"tier"_s;
69 const QString KEY_TRACKER_MSG = u"msg"_s;
70 const QString KEY_TRACKER_PEERS_COUNT = u"num_peers"_s;
71 const QString KEY_TRACKER_SEEDS_COUNT = u"num_seeds"_s;
72 const QString KEY_TRACKER_LEECHES_COUNT = u"num_leeches"_s;
73 const QString KEY_TRACKER_DOWNLOADED_COUNT = u"num_downloaded"_s;
75 // Web seed keys
76 const QString KEY_WEBSEED_URL = u"url"_s;
78 // Torrent keys (Properties)
79 const QString KEY_PROP_TIME_ELAPSED = u"time_elapsed"_s;
80 const QString KEY_PROP_SEEDING_TIME = u"seeding_time"_s;
81 const QString KEY_PROP_ETA = u"eta"_s;
82 const QString KEY_PROP_CONNECT_COUNT = u"nb_connections"_s;
83 const QString KEY_PROP_CONNECT_COUNT_LIMIT = u"nb_connections_limit"_s;
84 const QString KEY_PROP_DOWNLOADED = u"total_downloaded"_s;
85 const QString KEY_PROP_DOWNLOADED_SESSION = u"total_downloaded_session"_s;
86 const QString KEY_PROP_UPLOADED = u"total_uploaded"_s;
87 const QString KEY_PROP_UPLOADED_SESSION = u"total_uploaded_session"_s;
88 const QString KEY_PROP_DL_SPEED = u"dl_speed"_s;
89 const QString KEY_PROP_DL_SPEED_AVG = u"dl_speed_avg"_s;
90 const QString KEY_PROP_UP_SPEED = u"up_speed"_s;
91 const QString KEY_PROP_UP_SPEED_AVG = u"up_speed_avg"_s;
92 const QString KEY_PROP_DL_LIMIT = u"dl_limit"_s;
93 const QString KEY_PROP_UP_LIMIT = u"up_limit"_s;
94 const QString KEY_PROP_WASTED = u"total_wasted"_s;
95 const QString KEY_PROP_SEEDS = u"seeds"_s;
96 const QString KEY_PROP_SEEDS_TOTAL = u"seeds_total"_s;
97 const QString KEY_PROP_PEERS = u"peers"_s;
98 const QString KEY_PROP_PEERS_TOTAL = u"peers_total"_s;
99 const QString KEY_PROP_RATIO = u"share_ratio"_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_ISPRIVATE = u"is_private"_s;
114 const QString KEY_PROP_SSL_CERTIFICATE = u"ssl_certificate"_s;
115 const QString KEY_PROP_SSL_PRIVATEKEY = u"ssl_private_key"_s;
116 const QString KEY_PROP_SSL_DHPARAMS = u"ssl_dh_params"_s;
118 // File keys
119 const QString KEY_FILE_INDEX = u"index"_s;
120 const QString KEY_FILE_NAME = u"name"_s;
121 const QString KEY_FILE_SIZE = u"size"_s;
122 const QString KEY_FILE_PROGRESS = u"progress"_s;
123 const QString KEY_FILE_PRIORITY = u"priority"_s;
124 const QString KEY_FILE_IS_SEED = u"is_seed"_s;
125 const QString KEY_FILE_PIECE_RANGE = u"piece_range"_s;
126 const QString KEY_FILE_AVAILABILITY = u"availability"_s;
128 namespace
130 using Utils::String::parseBool;
131 using Utils::String::parseInt;
132 using Utils::String::parseDouble;
134 void applyToTorrents(const QStringList &idList, const std::function<void (BitTorrent::Torrent *torrent)> &func)
136 if ((idList.size() == 1) && (idList[0] == u"all"))
138 for (BitTorrent::Torrent *const torrent : asConst(BitTorrent::Session::instance()->torrents()))
139 func(torrent);
141 else
143 for (const QString &idString : idList)
145 const auto hash = BitTorrent::TorrentID::fromString(idString);
146 BitTorrent::Torrent *const torrent = BitTorrent::Session::instance()->getTorrent(hash);
147 if (torrent)
148 func(torrent);
153 std::optional<QString> getOptionalString(const StringMap &params, const QString &name)
155 const auto it = params.constFind(name);
156 if (it == params.cend())
157 return std::nullopt;
159 return it.value();
162 std::optional<Tag> getOptionalTag(const StringMap &params, const QString &name)
164 const auto it = params.constFind(name);
165 if (it == params.cend())
166 return std::nullopt;
168 return Tag(it.value());
171 QJsonArray getStickyTrackers(const BitTorrent::Torrent *const torrent)
173 int seedsDHT = 0, seedsPeX = 0, seedsLSD = 0, leechesDHT = 0, leechesPeX = 0, leechesLSD = 0;
174 for (const BitTorrent::PeerInfo &peer : asConst(torrent->peers()))
176 if (peer.isConnecting()) continue;
178 if (peer.isSeed())
180 if (peer.fromDHT())
181 ++seedsDHT;
182 if (peer.fromPeX())
183 ++seedsPeX;
184 if (peer.fromLSD())
185 ++seedsLSD;
187 else
189 if (peer.fromDHT())
190 ++leechesDHT;
191 if (peer.fromPeX())
192 ++leechesPeX;
193 if (peer.fromLSD())
194 ++leechesLSD;
198 const int working = static_cast<int>(BitTorrent::TrackerEndpointState::Working);
199 const int disabled = 0;
201 const QString privateMsg {QCoreApplication::translate("TrackerListWidget", "This torrent is private")};
202 const bool isTorrentPrivate = torrent->isPrivate();
204 const QJsonObject dht
206 {KEY_TRACKER_URL, u"** [DHT] **"_s},
207 {KEY_TRACKER_TIER, -1},
208 {KEY_TRACKER_MSG, (isTorrentPrivate ? privateMsg : u""_s)},
209 {KEY_TRACKER_STATUS, ((BitTorrent::Session::instance()->isDHTEnabled() && !isTorrentPrivate) ? working : disabled)},
210 {KEY_TRACKER_PEERS_COUNT, 0},
211 {KEY_TRACKER_DOWNLOADED_COUNT, 0},
212 {KEY_TRACKER_SEEDS_COUNT, seedsDHT},
213 {KEY_TRACKER_LEECHES_COUNT, leechesDHT}
216 const QJsonObject pex
218 {KEY_TRACKER_URL, u"** [PeX] **"_s},
219 {KEY_TRACKER_TIER, -1},
220 {KEY_TRACKER_MSG, (isTorrentPrivate ? privateMsg : u""_s)},
221 {KEY_TRACKER_STATUS, ((BitTorrent::Session::instance()->isPeXEnabled() && !isTorrentPrivate) ? working : disabled)},
222 {KEY_TRACKER_PEERS_COUNT, 0},
223 {KEY_TRACKER_DOWNLOADED_COUNT, 0},
224 {KEY_TRACKER_SEEDS_COUNT, seedsPeX},
225 {KEY_TRACKER_LEECHES_COUNT, leechesPeX}
228 const QJsonObject lsd
230 {KEY_TRACKER_URL, u"** [LSD] **"_s},
231 {KEY_TRACKER_TIER, -1},
232 {KEY_TRACKER_MSG, (isTorrentPrivate ? privateMsg : u""_s)},
233 {KEY_TRACKER_STATUS, ((BitTorrent::Session::instance()->isLSDEnabled() && !isTorrentPrivate) ? working : disabled)},
234 {KEY_TRACKER_PEERS_COUNT, 0},
235 {KEY_TRACKER_DOWNLOADED_COUNT, 0},
236 {KEY_TRACKER_SEEDS_COUNT, seedsLSD},
237 {KEY_TRACKER_LEECHES_COUNT, leechesLSD}
240 return {dht, pex, lsd};
243 QVector<BitTorrent::TorrentID> toTorrentIDs(const QStringList &idStrings)
245 QVector<BitTorrent::TorrentID> idList;
246 idList.reserve(idStrings.size());
247 for (const QString &hash : idStrings)
248 idList << BitTorrent::TorrentID::fromString(hash);
249 return idList;
253 void TorrentsController::countAction()
255 setResult(QString::number(BitTorrent::Session::instance()->torrentsCount()));
258 // Returns all the torrents in JSON format.
259 // The return value is a JSON-formatted list of dictionaries.
260 // The dictionary keys are:
261 // - "hash": Torrent hash (ID)
262 // - "name": Torrent name
263 // - "size": Torrent size
264 // - "progress": Torrent progress
265 // - "dlspeed": Torrent download speed
266 // - "upspeed": Torrent upload speed
267 // - "priority": Torrent queue position (-1 if queuing is disabled)
268 // - "num_seeds": Torrent seeds connected to
269 // - "num_complete": Torrent seeds in the swarm
270 // - "num_leechs": Torrent leechers connected to
271 // - "num_incomplete": Torrent leechers in the swarm
272 // - "ratio": Torrent share ratio
273 // - "eta": Torrent ETA
274 // - "state": Torrent state
275 // - "seq_dl": Torrent sequential download state
276 // - "f_l_piece_prio": Torrent first last piece priority state
277 // - "force_start": Torrent force start state
278 // - "category": Torrent category
279 // GET params:
280 // - filter (string): all, downloading, seeding, completed, stopped, running, active, inactive, stalled, stalled_uploading, stalled_downloading
281 // - category (string): torrent category for filtering by it (empty string means "uncategorized"; no "category" param presented means "any category")
282 // - tag (string): torrent tag for filtering by it (empty string means "untagged"; no "tag" param presented means "any tag")
283 // - hashes (string): filter by hashes, can contain multiple hashes separated by |
284 // - sort (string): name of column for sorting by its value
285 // - reverse (bool): enable reverse sorting
286 // - limit (int): set limit number of torrents returned (if greater than 0, otherwise - unlimited)
287 // - offset (int): set offset (if less than 0 - offset from end)
288 void TorrentsController::infoAction()
290 const QString filter {params()[u"filter"_s]};
291 const std::optional<QString> category = getOptionalString(params(), u"category"_s);
292 const std::optional<Tag> tag = getOptionalTag(params(), u"tag"_s);
293 const QString sortedColumn {params()[u"sort"_s]};
294 const bool reverse {parseBool(params()[u"reverse"_s]).value_or(false)};
295 int limit {params()[u"limit"_s].toInt()};
296 int offset {params()[u"offset"_s].toInt()};
297 const QStringList hashes {params()[u"hashes"_s].split(u'|', Qt::SkipEmptyParts)};
299 std::optional<TorrentIDSet> idSet;
300 if (!hashes.isEmpty())
302 idSet = TorrentIDSet();
303 for (const QString &hash : hashes)
304 idSet->insert(BitTorrent::TorrentID::fromString(hash));
307 const TorrentFilter torrentFilter {filter, idSet, category, tag};
308 QVariantList torrentList;
309 for (const BitTorrent::Torrent *torrent : asConst(BitTorrent::Session::instance()->torrents()))
311 if (torrentFilter.match(torrent))
312 torrentList.append(serialize(*torrent));
315 if (torrentList.isEmpty())
317 setResult(QJsonArray {});
318 return;
321 if (!sortedColumn.isEmpty())
323 if (!torrentList[0].toMap().contains(sortedColumn))
324 throw APIError(APIErrorType::BadParams, tr("'sort' parameter is invalid"));
326 const auto lessThan = [](const QVariant &left, const QVariant &right) -> bool
328 Q_ASSERT(left.userType() == right.userType());
330 switch (left.userType())
332 case QMetaType::Bool:
333 return left.value<bool>() < right.value<bool>();
334 case QMetaType::Double:
335 return left.value<double>() < right.value<double>();
336 case QMetaType::Float:
337 return left.value<float>() < right.value<float>();
338 case QMetaType::Int:
339 return left.value<int>() < right.value<int>();
340 case QMetaType::LongLong:
341 return left.value<qlonglong>() < right.value<qlonglong>();
342 case QMetaType::QString:
343 return left.value<QString>() < right.value<QString>();
344 default:
345 qWarning("Unhandled QVariant comparison, type: %d, name: %s"
346 , left.userType(), left.metaType().name());
347 break;
349 return false;
352 std::sort(torrentList.begin(), torrentList.end()
353 , [reverse, &sortedColumn, &lessThan](const QVariant &torrent1, const QVariant &torrent2)
355 const QVariant value1 {torrent1.toMap().value(sortedColumn)};
356 const QVariant value2 {torrent2.toMap().value(sortedColumn)};
357 return reverse ? lessThan(value2, value1) : lessThan(value1, value2);
361 const int size = torrentList.size();
362 // normalize offset
363 if (offset < 0)
364 offset = size + offset;
365 if ((offset >= size) || (offset < 0))
366 offset = 0;
367 // normalize limit
368 if (limit <= 0)
369 limit = -1; // unlimited
371 if ((limit > 0) || (offset > 0))
372 torrentList = torrentList.mid(offset, limit);
374 setResult(QJsonArray::fromVariantList(torrentList));
377 // Returns the properties for a torrent in JSON format.
378 // The return value is a JSON-formatted dictionary.
379 // The dictionary keys are:
380 // - "time_elapsed": Torrent elapsed time
381 // - "seeding_time": Torrent elapsed time while complete
382 // - "eta": Torrent ETA
383 // - "nb_connections": Torrent connection count
384 // - "nb_connections_limit": Torrent connection count limit
385 // - "total_downloaded": Total data uploaded for torrent
386 // - "total_downloaded_session": Total data downloaded this session
387 // - "total_uploaded": Total data uploaded for torrent
388 // - "total_uploaded_session": Total data uploaded this session
389 // - "dl_speed": Torrent download speed
390 // - "dl_speed_avg": Torrent average download speed
391 // - "up_speed": Torrent upload speed
392 // - "up_speed_avg": Torrent average upload speed
393 // - "dl_limit": Torrent download limit
394 // - "up_limit": Torrent upload limit
395 // - "total_wasted": Total data wasted for torrent
396 // - "seeds": Torrent connected seeds
397 // - "seeds_total": Torrent total number of seeds
398 // - "peers": Torrent connected peers
399 // - "peers_total": Torrent total number of peers
400 // - "share_ratio": Torrent share ratio
401 // - "reannounce": Torrent next reannounce time
402 // - "total_size": Torrent total size
403 // - "pieces_num": Torrent pieces count
404 // - "piece_size": Torrent piece size
405 // - "pieces_have": Torrent pieces have
406 // - "created_by": Torrent creator
407 // - "last_seen": Torrent last seen complete
408 // - "addition_date": Torrent addition date
409 // - "completion_date": Torrent completion date
410 // - "creation_date": Torrent creation date
411 // - "save_path": Torrent save path
412 // - "download_path": Torrent download path
413 // - "comment": Torrent comment
414 // - "infohash_v1": Torrent v1 infohash (or empty string for v2 torrents)
415 // - "infohash_v2": Torrent v2 infohash (or empty string for v1 torrents)
416 // - "hash": Torrent TorrentID (infohashv1 for v1 torrents, truncated infohashv2 for v2/hybrid torrents)
417 // - "name": Torrent name
418 void TorrentsController::propertiesAction()
420 requireParams({u"hash"_s});
422 const auto id = BitTorrent::TorrentID::fromString(params()[u"hash"_s]);
423 const BitTorrent::Torrent *const torrent = BitTorrent::Session::instance()->getTorrent(id);
424 if (!torrent)
425 throw APIError(APIErrorType::NotFound);
427 const BitTorrent::InfoHash infoHash = torrent->infoHash();
428 const qlonglong totalDownload = torrent->totalDownload();
429 const qlonglong totalUpload = torrent->totalUpload();
430 const qlonglong dlDuration = torrent->activeTime() - torrent->finishedTime();
431 const qlonglong ulDuration = torrent->activeTime();
432 const int downloadLimit = torrent->downloadLimit();
433 const int uploadLimit = torrent->uploadLimit();
434 const qreal ratio = torrent->realRatio();
436 const QJsonObject ret
438 {KEY_TORRENT_INFOHASHV1, infoHash.v1().toString()},
439 {KEY_TORRENT_INFOHASHV2, infoHash.v2().toString()},
440 {KEY_TORRENT_NAME, torrent->name()},
441 {KEY_TORRENT_ID, torrent->id().toString()},
442 {KEY_PROP_TIME_ELAPSED, torrent->activeTime()},
443 {KEY_PROP_SEEDING_TIME, torrent->finishedTime()},
444 {KEY_PROP_ETA, torrent->eta()},
445 {KEY_PROP_CONNECT_COUNT, torrent->connectionsCount()},
446 {KEY_PROP_CONNECT_COUNT_LIMIT, torrent->connectionsLimit()},
447 {KEY_PROP_DOWNLOADED, totalDownload},
448 {KEY_PROP_DOWNLOADED_SESSION, torrent->totalPayloadDownload()},
449 {KEY_PROP_UPLOADED, totalUpload},
450 {KEY_PROP_UPLOADED_SESSION, torrent->totalPayloadUpload()},
451 {KEY_PROP_DL_SPEED, torrent->downloadPayloadRate()},
452 {KEY_PROP_DL_SPEED_AVG, ((dlDuration > 0) ? (totalDownload / dlDuration) : -1)},
453 {KEY_PROP_UP_SPEED, torrent->uploadPayloadRate()},
454 {KEY_PROP_UP_SPEED_AVG, ((ulDuration > 0) ? (totalUpload / ulDuration) : -1)},
455 {KEY_PROP_DL_LIMIT, ((downloadLimit > 0) ? downloadLimit : -1)},
456 {KEY_PROP_UP_LIMIT, ((uploadLimit > 0) ? uploadLimit : -1)},
457 {KEY_PROP_WASTED, torrent->wastedSize()},
458 {KEY_PROP_SEEDS, torrent->seedsCount()},
459 {KEY_PROP_SEEDS_TOTAL, torrent->totalSeedsCount()},
460 {KEY_PROP_PEERS, torrent->leechsCount()},
461 {KEY_PROP_PEERS_TOTAL, torrent->totalLeechersCount()},
462 {KEY_PROP_RATIO, ((ratio > BitTorrent::Torrent::MAX_RATIO) ? -1 : ratio)},
463 {KEY_PROP_REANNOUNCE, torrent->nextAnnounce()},
464 {KEY_PROP_TOTAL_SIZE, torrent->totalSize()},
465 {KEY_PROP_PIECES_NUM, torrent->piecesCount()},
466 {KEY_PROP_PIECE_SIZE, torrent->pieceLength()},
467 {KEY_PROP_PIECES_HAVE, torrent->piecesHave()},
468 {KEY_PROP_CREATED_BY, torrent->creator()},
469 {KEY_PROP_ISPRIVATE, torrent->isPrivate()},
470 {KEY_PROP_ADDITION_DATE, Utils::DateTime::toSecsSinceEpoch(torrent->addedTime())},
471 {KEY_PROP_LAST_SEEN, Utils::DateTime::toSecsSinceEpoch(torrent->lastSeenComplete())},
472 {KEY_PROP_COMPLETION_DATE, Utils::DateTime::toSecsSinceEpoch(torrent->completedTime())},
473 {KEY_PROP_CREATION_DATE, Utils::DateTime::toSecsSinceEpoch(torrent->creationDate())},
474 {KEY_PROP_SAVE_PATH, torrent->savePath().toString()},
475 {KEY_PROP_DOWNLOAD_PATH, torrent->downloadPath().toString()},
476 {KEY_PROP_COMMENT, torrent->comment()}
479 setResult(ret);
482 // Returns the trackers for a torrent in JSON format.
483 // The return value is a JSON-formatted list of dictionaries.
484 // The dictionary keys are:
485 // - "url": Tracker URL
486 // - "status": Tracker status
487 // - "tier": Tracker tier
488 // - "num_peers": Number of peers this torrent is currently connected to
489 // - "num_seeds": Number of peers that have the whole file
490 // - "num_leeches": Number of peers that are still downloading
491 // - "num_downloaded": Tracker downloaded count
492 // - "msg": Tracker message (last)
493 void TorrentsController::trackersAction()
495 requireParams({u"hash"_s});
497 const auto id = BitTorrent::TorrentID::fromString(params()[u"hash"_s]);
498 const BitTorrent::Torrent *const torrent = BitTorrent::Session::instance()->getTorrent(id);
499 if (!torrent)
500 throw APIError(APIErrorType::NotFound);
502 QJsonArray trackerList = getStickyTrackers(torrent);
504 for (const BitTorrent::TrackerEntryStatus &tracker : asConst(torrent->trackers()))
506 const bool isNotWorking = (tracker.state == BitTorrent::TrackerEndpointState::NotWorking)
507 || (tracker.state == BitTorrent::TrackerEndpointState::TrackerError)
508 || (tracker.state == BitTorrent::TrackerEndpointState::Unreachable);
509 trackerList << QJsonObject
511 {KEY_TRACKER_URL, tracker.url},
512 {KEY_TRACKER_TIER, tracker.tier},
513 {KEY_TRACKER_STATUS, static_cast<int>((isNotWorking ? BitTorrent::TrackerEndpointState::NotWorking : tracker.state))},
514 {KEY_TRACKER_MSG, tracker.message},
515 {KEY_TRACKER_PEERS_COUNT, tracker.numPeers},
516 {KEY_TRACKER_SEEDS_COUNT, tracker.numSeeds},
517 {KEY_TRACKER_LEECHES_COUNT, tracker.numLeeches},
518 {KEY_TRACKER_DOWNLOADED_COUNT, tracker.numDownloaded}
522 setResult(trackerList);
525 // Returns the web seeds for a torrent in JSON format.
526 // The return value is a JSON-formatted list of dictionaries.
527 // The dictionary keys are:
528 // - "url": Web seed URL
529 void TorrentsController::webseedsAction()
531 requireParams({u"hash"_s});
533 const auto id = BitTorrent::TorrentID::fromString(params()[u"hash"_s]);
534 BitTorrent::Torrent *const torrent = BitTorrent::Session::instance()->getTorrent(id);
535 if (!torrent)
536 throw APIError(APIErrorType::NotFound);
538 QJsonArray webSeedList;
539 for (const QUrl &webseed : asConst(torrent->urlSeeds()))
541 webSeedList.append(QJsonObject
543 {KEY_WEBSEED_URL, webseed.toString()}
547 setResult(webSeedList);
550 // Returns the files in a torrent in JSON format.
551 // The return value is a JSON-formatted list of dictionaries.
552 // The dictionary keys are:
553 // - "index": File index
554 // - "name": File name
555 // - "size": File size
556 // - "progress": File progress
557 // - "priority": File priority
558 // - "is_seed": Flag indicating if torrent is seeding/complete
559 // - "piece_range": Piece index range, the first number is the starting piece index
560 // and the second number is the ending piece index (inclusive)
561 void TorrentsController::filesAction()
563 requireParams({u"hash"_s});
565 const auto id = BitTorrent::TorrentID::fromString(params()[u"hash"_s]);
566 const BitTorrent::Torrent *const torrent = BitTorrent::Session::instance()->getTorrent(id);
567 if (!torrent)
568 throw APIError(APIErrorType::NotFound);
570 const int filesCount = torrent->filesCount();
571 QVector<int> fileIndexes;
572 const auto idxIt = params().constFind(u"indexes"_s);
573 if (idxIt != params().cend())
575 const QStringList indexStrings = idxIt.value().split(u'|');
576 fileIndexes.reserve(indexStrings.size());
577 std::transform(indexStrings.cbegin(), indexStrings.cend(), std::back_inserter(fileIndexes)
578 , [&filesCount](const QString &indexString) -> int
580 bool ok = false;
581 const int index = indexString.toInt(&ok);
582 if (!ok || (index < 0))
583 throw APIError(APIErrorType::Conflict, tr("\"%1\" is not a valid file index.").arg(indexString));
584 if (index >= filesCount)
585 throw APIError(APIErrorType::Conflict, tr("Index %1 is out of bounds.").arg(indexString));
586 return index;
589 else
591 fileIndexes.reserve(filesCount);
592 for (int i = 0; i < filesCount; ++i)
593 fileIndexes.append(i);
596 QJsonArray fileList;
597 if (torrent->hasMetadata())
599 const QVector<BitTorrent::DownloadPriority> priorities = torrent->filePriorities();
600 const QVector<qreal> fp = torrent->filesProgress();
601 const QVector<qreal> fileAvailability = torrent->availableFileFractions();
602 const BitTorrent::TorrentInfo info = torrent->info();
603 for (const int index : asConst(fileIndexes))
605 QJsonObject fileDict =
607 {KEY_FILE_INDEX, index},
608 {KEY_FILE_PROGRESS, fp[index]},
609 {KEY_FILE_PRIORITY, static_cast<int>(priorities[index])},
610 {KEY_FILE_SIZE, torrent->fileSize(index)},
611 {KEY_FILE_AVAILABILITY, fileAvailability[index]},
612 // need to provide paths using a platform-independent separator format
613 {KEY_FILE_NAME, torrent->filePath(index).data()}
616 const BitTorrent::TorrentInfo::PieceRange idx = info.filePieces(index);
617 fileDict[KEY_FILE_PIECE_RANGE] = QJsonArray {idx.first(), idx.last()};
619 if (index == 0)
620 fileDict[KEY_FILE_IS_SEED] = torrent->isFinished();
622 fileList.append(fileDict);
626 setResult(fileList);
629 // Returns an array of hashes (of each pieces respectively) for a torrent in JSON format.
630 // The return value is a JSON-formatted array of strings (hex strings).
631 void TorrentsController::pieceHashesAction()
633 requireParams({u"hash"_s});
635 const auto id = BitTorrent::TorrentID::fromString(params()[u"hash"_s]);
636 BitTorrent::Torrent *const torrent = BitTorrent::Session::instance()->getTorrent(id);
637 if (!torrent)
638 throw APIError(APIErrorType::NotFound);
640 QJsonArray pieceHashes;
641 if (torrent->hasMetadata())
643 const QVector<QByteArray> hashes = torrent->info().pieceHashes();
644 for (const QByteArray &hash : hashes)
645 pieceHashes.append(QString::fromLatin1(hash.toHex()));
648 setResult(pieceHashes);
651 // Returns an array of states (of each pieces respectively) for a torrent in JSON format.
652 // The return value is a JSON-formatted array of ints.
653 // 0: piece not downloaded
654 // 1: piece requested or downloading
655 // 2: piece already downloaded
656 void TorrentsController::pieceStatesAction()
658 requireParams({u"hash"_s});
660 const auto id = BitTorrent::TorrentID::fromString(params()[u"hash"_s]);
661 BitTorrent::Torrent *const torrent = BitTorrent::Session::instance()->getTorrent(id);
662 if (!torrent)
663 throw APIError(APIErrorType::NotFound);
665 QJsonArray pieceStates;
666 const QBitArray states = torrent->pieces();
667 for (int i = 0; i < states.size(); ++i)
668 pieceStates.append(static_cast<int>(states[i]) * 2);
670 const QBitArray dlstates = torrent->downloadingPieces();
671 for (int i = 0; i < states.size(); ++i)
673 if (dlstates[i])
674 pieceStates[i] = 1;
677 setResult(pieceStates);
680 void TorrentsController::addAction()
682 const QString urls = params()[u"urls"_s];
683 const QString cookie = params()[u"cookie"_s];
685 const bool skipChecking = parseBool(params()[u"skip_checking"_s]).value_or(false);
686 const bool seqDownload = parseBool(params()[u"sequentialDownload"_s]).value_or(false);
687 const bool firstLastPiece = parseBool(params()[u"firstLastPiecePrio"_s]).value_or(false);
688 const std::optional<bool> addToQueueTop = parseBool(params()[u"addToTopOfQueue"_s]);
689 const std::optional<bool> addStopped = parseBool(params()[u"stopped"_s]);
690 const QString savepath = params()[u"savepath"_s].trimmed();
691 const QString downloadPath = params()[u"downloadPath"_s].trimmed();
692 const std::optional<bool> useDownloadPath = parseBool(params()[u"useDownloadPath"_s]);
693 const QString category = params()[u"category"_s];
694 const QStringList tags = params()[u"tags"_s].split(u',', Qt::SkipEmptyParts);
695 const QString torrentName = params()[u"rename"_s].trimmed();
696 const int upLimit = parseInt(params()[u"upLimit"_s]).value_or(-1);
697 const int dlLimit = parseInt(params()[u"dlLimit"_s]).value_or(-1);
698 const double ratioLimit = parseDouble(params()[u"ratioLimit"_s]).value_or(BitTorrent::Torrent::USE_GLOBAL_RATIO);
699 const int seedingTimeLimit = parseInt(params()[u"seedingTimeLimit"_s]).value_or(BitTorrent::Torrent::USE_GLOBAL_SEEDING_TIME);
700 const int inactiveSeedingTimeLimit = parseInt(params()[u"inactiveSeedingTimeLimit"_s]).value_or(BitTorrent::Torrent::USE_GLOBAL_INACTIVE_SEEDING_TIME);
701 const BitTorrent::ShareLimitAction shareLimitAction = Utils::String::toEnum(params()[u"shareLimitAction"_s], BitTorrent::ShareLimitAction::Default);
702 const std::optional<bool> autoTMM = parseBool(params()[u"autoTMM"_s]);
704 const QString stopConditionParam = params()[u"stopCondition"_s];
705 const std::optional<BitTorrent::Torrent::StopCondition> stopCondition = (!stopConditionParam.isEmpty()
706 ? Utils::String::toEnum(stopConditionParam, BitTorrent::Torrent::StopCondition::None)
707 : std::optional<BitTorrent::Torrent::StopCondition> {});
709 const QString contentLayoutParam = params()[u"contentLayout"_s];
710 const std::optional<BitTorrent::TorrentContentLayout> contentLayout = (!contentLayoutParam.isEmpty()
711 ? Utils::String::toEnum(contentLayoutParam, BitTorrent::TorrentContentLayout::Original)
712 : std::optional<BitTorrent::TorrentContentLayout> {});
714 QList<QNetworkCookie> cookies;
715 if (!cookie.isEmpty())
717 const QStringList cookiesStr = cookie.split(u"; "_s);
718 for (QString cookieStr : cookiesStr)
720 cookieStr = cookieStr.trimmed();
721 int index = cookieStr.indexOf(u'=');
722 if (index > 1)
724 QByteArray name = cookieStr.left(index).toLatin1();
725 QByteArray value = cookieStr.right(cookieStr.length() - index - 1).toLatin1();
726 cookies += QNetworkCookie(name, value);
731 const BitTorrent::AddTorrentParams addTorrentParams
733 // TODO: Check if destination actually exists
734 .name = torrentName,
735 .category = category,
736 .tags = {tags.cbegin(), tags.cend()},
737 .savePath = Path(savepath),
738 .useDownloadPath = useDownloadPath,
739 .downloadPath = Path(downloadPath),
740 .sequential = seqDownload,
741 .firstLastPiecePriority = firstLastPiece,
742 .addForced = false,
743 .addToQueueTop = addToQueueTop,
744 .addStopped = addStopped,
745 .stopCondition = stopCondition,
746 .filePaths = {},
747 .filePriorities = {},
748 .skipChecking = skipChecking,
749 .contentLayout = contentLayout,
750 .useAutoTMM = autoTMM,
751 .uploadLimit = upLimit,
752 .downloadLimit = dlLimit,
753 .seedingTimeLimit = seedingTimeLimit,
754 .inactiveSeedingTimeLimit = inactiveSeedingTimeLimit,
755 .ratioLimit = ratioLimit,
756 .shareLimitAction = shareLimitAction,
757 .sslParameters =
759 .certificate = QSslCertificate(params()[KEY_PROP_SSL_CERTIFICATE].toLatin1()),
760 .privateKey = Utils::SSLKey::load(params()[KEY_PROP_SSL_PRIVATEKEY].toLatin1()),
761 .dhParams = params()[KEY_PROP_SSL_DHPARAMS].toLatin1()
765 bool partialSuccess = false;
766 for (QString url : asConst(urls.split(u'\n')))
768 url = url.trimmed();
769 if (!url.isEmpty())
771 Net::DownloadManager::instance()->setCookiesFromUrl(cookies, QUrl::fromEncoded(url.toUtf8()));
772 partialSuccess |= app()->addTorrentManager()->addTorrent(url, addTorrentParams);
776 const DataMap &torrents = data();
777 for (auto it = torrents.constBegin(); it != torrents.constEnd(); ++it)
779 if (const auto loadResult = BitTorrent::TorrentDescriptor::load(it.value()))
781 partialSuccess |= BitTorrent::Session::instance()->addTorrent(loadResult.value(), addTorrentParams);
783 else
785 throw APIError(APIErrorType::BadData, tr("Error: '%1' is not a valid torrent file.").arg(it.key()));
789 if (partialSuccess)
790 setResult(u"Ok."_s);
791 else
792 setResult(u"Fails."_s);
795 void TorrentsController::addTrackersAction()
797 requireParams({u"hash"_s, u"urls"_s});
799 const auto id = BitTorrent::TorrentID::fromString(params()[u"hash"_s]);
800 BitTorrent::Torrent *const torrent = BitTorrent::Session::instance()->getTorrent(id);
801 if (!torrent)
802 throw APIError(APIErrorType::NotFound);
804 const QList<BitTorrent::TrackerEntry> entries = BitTorrent::parseTrackerEntries(params()[u"urls"_s]);
805 torrent->addTrackers(entries);
808 void TorrentsController::editTrackerAction()
810 requireParams({u"hash"_s, u"origUrl"_s, u"newUrl"_s});
812 const auto id = BitTorrent::TorrentID::fromString(params()[u"hash"_s]);
813 const QString origUrl = params()[u"origUrl"_s];
814 const QString newUrl = params()[u"newUrl"_s];
816 BitTorrent::Torrent *const torrent = BitTorrent::Session::instance()->getTorrent(id);
817 if (!torrent)
818 throw APIError(APIErrorType::NotFound);
820 const QUrl origTrackerUrl {origUrl};
821 const QUrl newTrackerUrl {newUrl};
822 if (origTrackerUrl == newTrackerUrl)
823 return;
824 if (!newTrackerUrl.isValid())
825 throw APIError(APIErrorType::BadParams, u"New tracker URL is invalid"_s);
827 const QList<BitTorrent::TrackerEntryStatus> currentTrackers = torrent->trackers();
828 QList<BitTorrent::TrackerEntry> entries;
829 entries.reserve(currentTrackers.size());
831 bool match = false;
832 for (const BitTorrent::TrackerEntryStatus &tracker : currentTrackers)
834 const QUrl trackerUrl {tracker.url};
836 if (trackerUrl == newTrackerUrl)
837 throw APIError(APIErrorType::Conflict, u"New tracker URL already exists"_s);
839 BitTorrent::TrackerEntry entry
841 .url = tracker.url,
842 .tier = tracker.tier
845 if (trackerUrl == origTrackerUrl)
847 match = true;
848 entry.url = newTrackerUrl.toString();
850 entries.append(entry);
852 if (!match)
853 throw APIError(APIErrorType::Conflict, u"Tracker not found"_s);
855 torrent->replaceTrackers(entries);
857 if (!torrent->isStopped())
858 torrent->forceReannounce();
861 void TorrentsController::removeTrackersAction()
863 requireParams({u"hash"_s, u"urls"_s});
865 const auto id = BitTorrent::TorrentID::fromString(params()[u"hash"_s]);
866 BitTorrent::Torrent *const torrent = BitTorrent::Session::instance()->getTorrent(id);
867 if (!torrent)
868 throw APIError(APIErrorType::NotFound);
870 const QStringList urls = params()[u"urls"_s].split(u'|');
871 torrent->removeTrackers(urls);
873 if (!torrent->isStopped())
874 torrent->forceReannounce();
877 void TorrentsController::addPeersAction()
879 requireParams({u"hashes"_s, u"peers"_s});
881 const QStringList hashes = params()[u"hashes"_s].split(u'|');
882 const QStringList peers = params()[u"peers"_s].split(u'|');
884 QVector<BitTorrent::PeerAddress> peerList;
885 peerList.reserve(peers.size());
886 for (const QString &peer : peers)
888 const BitTorrent::PeerAddress addr = BitTorrent::PeerAddress::parse(peer.trimmed());
889 if (!addr.ip.isNull())
890 peerList.append(addr);
893 if (peerList.isEmpty())
894 throw APIError(APIErrorType::BadParams, u"No valid peers were specified"_s);
896 QJsonObject results;
898 applyToTorrents(hashes, [peers, peerList, &results](BitTorrent::Torrent *const torrent)
900 const int peersAdded = std::count_if(peerList.cbegin(), peerList.cend(), [torrent](const BitTorrent::PeerAddress &peer)
902 return torrent->connectPeer(peer);
905 results[torrent->id().toString()] = QJsonObject
907 {u"added"_s, peersAdded},
908 {u"failed"_s, (peers.size() - peersAdded)}
912 setResult(results);
915 void TorrentsController::stopAction()
917 requireParams({u"hashes"_s});
919 const QStringList hashes = params()[u"hashes"_s].split(u'|');
920 applyToTorrents(hashes, [](BitTorrent::Torrent *const torrent) { torrent->stop(); });
923 void TorrentsController::startAction()
925 requireParams({u"hashes"_s});
927 const QStringList idStrings = params()[u"hashes"_s].split(u'|');
928 applyToTorrents(idStrings, [](BitTorrent::Torrent *const torrent) { torrent->start(); });
931 void TorrentsController::filePrioAction()
933 requireParams({u"hash"_s, u"id"_s, u"priority"_s});
935 const auto id = BitTorrent::TorrentID::fromString(params()[u"hash"_s]);
936 bool ok = false;
937 const auto priority = static_cast<BitTorrent::DownloadPriority>(params()[u"priority"_s].toInt(&ok));
938 if (!ok)
939 throw APIError(APIErrorType::BadParams, tr("Priority must be an integer"));
941 if (!BitTorrent::isValidDownloadPriority(priority))
942 throw APIError(APIErrorType::BadParams, tr("Priority is not valid"));
944 BitTorrent::Torrent *const torrent = BitTorrent::Session::instance()->getTorrent(id);
945 if (!torrent)
946 throw APIError(APIErrorType::NotFound);
947 if (!torrent->hasMetadata())
948 throw APIError(APIErrorType::Conflict, tr("Torrent's metadata has not yet downloaded"));
950 const int filesCount = torrent->filesCount();
951 QVector<BitTorrent::DownloadPriority> priorities = torrent->filePriorities();
952 bool priorityChanged = false;
953 for (const QString &fileID : params()[u"id"_s].split(u'|'))
955 const int id = fileID.toInt(&ok);
956 if (!ok)
957 throw APIError(APIErrorType::BadParams, tr("File IDs must be integers"));
958 if ((id < 0) || (id >= filesCount))
959 throw APIError(APIErrorType::Conflict, tr("File ID is not valid"));
961 if (priorities[id] != priority)
963 priorities[id] = priority;
964 priorityChanged = true;
968 if (priorityChanged)
969 torrent->prioritizeFiles(priorities);
972 void TorrentsController::uploadLimitAction()
974 requireParams({u"hashes"_s});
976 const QStringList idList {params()[u"hashes"_s].split(u'|')};
977 QJsonObject map;
978 for (const QString &id : idList)
980 int limit = -1;
981 const BitTorrent::Torrent *const torrent = BitTorrent::Session::instance()->getTorrent(BitTorrent::TorrentID::fromString(id));
982 if (torrent)
983 limit = torrent->uploadLimit();
984 map[id] = limit;
987 setResult(map);
990 void TorrentsController::downloadLimitAction()
992 requireParams({u"hashes"_s});
994 const QStringList idList {params()[u"hashes"_s].split(u'|')};
995 QJsonObject map;
996 for (const QString &id : idList)
998 int limit = -1;
999 const BitTorrent::Torrent *const torrent = BitTorrent::Session::instance()->getTorrent(BitTorrent::TorrentID::fromString(id));
1000 if (torrent)
1001 limit = torrent->downloadLimit();
1002 map[id] = limit;
1005 setResult(map);
1008 void TorrentsController::setUploadLimitAction()
1010 requireParams({u"hashes"_s, u"limit"_s});
1012 qlonglong limit = params()[u"limit"_s].toLongLong();
1013 if (limit == 0)
1014 limit = -1;
1016 const QStringList hashes {params()[u"hashes"_s].split(u'|')};
1017 applyToTorrents(hashes, [limit](BitTorrent::Torrent *const torrent) { torrent->setUploadLimit(limit); });
1020 void TorrentsController::setDownloadLimitAction()
1022 requireParams({u"hashes"_s, u"limit"_s});
1024 qlonglong limit = params()[u"limit"_s].toLongLong();
1025 if (limit == 0)
1026 limit = -1;
1028 const QStringList hashes {params()[u"hashes"_s].split(u'|')};
1029 applyToTorrents(hashes, [limit](BitTorrent::Torrent *const torrent) { torrent->setDownloadLimit(limit); });
1032 void TorrentsController::setShareLimitsAction()
1034 requireParams({u"hashes"_s, u"ratioLimit"_s, u"seedingTimeLimit"_s, u"inactiveSeedingTimeLimit"_s});
1036 const qreal ratioLimit = params()[u"ratioLimit"_s].toDouble();
1037 const qlonglong seedingTimeLimit = params()[u"seedingTimeLimit"_s].toLongLong();
1038 const qlonglong inactiveSeedingTimeLimit = params()[u"inactiveSeedingTimeLimit"_s].toLongLong();
1039 const QStringList hashes = params()[u"hashes"_s].split(u'|');
1041 applyToTorrents(hashes, [ratioLimit, seedingTimeLimit, inactiveSeedingTimeLimit](BitTorrent::Torrent *const torrent)
1043 torrent->setRatioLimit(ratioLimit);
1044 torrent->setSeedingTimeLimit(seedingTimeLimit);
1045 torrent->setInactiveSeedingTimeLimit(inactiveSeedingTimeLimit);
1049 void TorrentsController::toggleSequentialDownloadAction()
1051 requireParams({u"hashes"_s});
1053 const QStringList hashes {params()[u"hashes"_s].split(u'|')};
1054 applyToTorrents(hashes, [](BitTorrent::Torrent *const torrent) { torrent->toggleSequentialDownload(); });
1057 void TorrentsController::toggleFirstLastPiecePrioAction()
1059 requireParams({u"hashes"_s});
1061 const QStringList hashes {params()[u"hashes"_s].split(u'|')};
1062 applyToTorrents(hashes, [](BitTorrent::Torrent *const torrent) { torrent->toggleFirstLastPiecePriority(); });
1065 void TorrentsController::setSuperSeedingAction()
1067 requireParams({u"hashes"_s, u"value"_s});
1069 const bool value {parseBool(params()[u"value"_s]).value_or(false)};
1070 const QStringList hashes {params()[u"hashes"_s].split(u'|')};
1071 applyToTorrents(hashes, [value](BitTorrent::Torrent *const torrent) { torrent->setSuperSeeding(value); });
1074 void TorrentsController::setForceStartAction()
1076 requireParams({u"hashes"_s, u"value"_s});
1078 const bool value {parseBool(params()[u"value"_s]).value_or(false)};
1079 const QStringList hashes {params()[u"hashes"_s].split(u'|')};
1080 applyToTorrents(hashes, [value](BitTorrent::Torrent *const torrent)
1082 torrent->start(value ? BitTorrent::TorrentOperatingMode::Forced : BitTorrent::TorrentOperatingMode::AutoManaged);
1086 void TorrentsController::deleteAction()
1088 requireParams({u"hashes"_s, u"deleteFiles"_s});
1090 const QStringList hashes {params()[u"hashes"_s].split(u'|')};
1091 const DeleteOption deleteOption = parseBool(params()[u"deleteFiles"_s]).value_or(false)
1092 ? DeleteTorrentAndFiles : DeleteTorrent;
1093 applyToTorrents(hashes, [deleteOption](const BitTorrent::Torrent *torrent)
1095 BitTorrent::Session::instance()->deleteTorrent(torrent->id(), deleteOption);
1099 void TorrentsController::increasePrioAction()
1101 requireParams({u"hashes"_s});
1103 if (!BitTorrent::Session::instance()->isQueueingSystemEnabled())
1104 throw APIError(APIErrorType::Conflict, tr("Torrent queueing must be enabled"));
1106 const QStringList hashes {params()[u"hashes"_s].split(u'|')};
1107 BitTorrent::Session::instance()->increaseTorrentsQueuePos(toTorrentIDs(hashes));
1110 void TorrentsController::decreasePrioAction()
1112 requireParams({u"hashes"_s});
1114 if (!BitTorrent::Session::instance()->isQueueingSystemEnabled())
1115 throw APIError(APIErrorType::Conflict, tr("Torrent queueing must be enabled"));
1117 const QStringList hashes {params()[u"hashes"_s].split(u'|')};
1118 BitTorrent::Session::instance()->decreaseTorrentsQueuePos(toTorrentIDs(hashes));
1121 void TorrentsController::topPrioAction()
1123 requireParams({u"hashes"_s});
1125 if (!BitTorrent::Session::instance()->isQueueingSystemEnabled())
1126 throw APIError(APIErrorType::Conflict, tr("Torrent queueing must be enabled"));
1128 const QStringList hashes {params()[u"hashes"_s].split(u'|')};
1129 BitTorrent::Session::instance()->topTorrentsQueuePos(toTorrentIDs(hashes));
1132 void TorrentsController::bottomPrioAction()
1134 requireParams({u"hashes"_s});
1136 if (!BitTorrent::Session::instance()->isQueueingSystemEnabled())
1137 throw APIError(APIErrorType::Conflict, tr("Torrent queueing must be enabled"));
1139 const QStringList hashes {params()[u"hashes"_s].split(u'|')};
1140 BitTorrent::Session::instance()->bottomTorrentsQueuePos(toTorrentIDs(hashes));
1143 void TorrentsController::setLocationAction()
1145 requireParams({u"hashes"_s, u"location"_s});
1147 const QStringList hashes {params()[u"hashes"_s].split(u'|')};
1148 const Path newLocation {params()[u"location"_s].trimmed()};
1150 if (newLocation.isEmpty())
1151 throw APIError(APIErrorType::BadParams, tr("Save path cannot be empty"));
1153 // try to create the location if it does not exist
1154 if (!Utils::Fs::mkpath(newLocation))
1155 throw APIError(APIErrorType::Conflict, tr("Cannot make save path"));
1157 applyToTorrents(hashes, [newLocation](BitTorrent::Torrent *const torrent)
1159 LogMsg(tr("WebUI Set location: moving \"%1\", from \"%2\" to \"%3\"")
1160 .arg(torrent->name(), torrent->savePath().toString(), newLocation.toString()));
1161 torrent->setAutoTMMEnabled(false);
1162 torrent->setSavePath(newLocation);
1166 void TorrentsController::setSavePathAction()
1168 requireParams({u"id"_s, u"path"_s});
1170 const QStringList ids {params()[u"id"_s].split(u'|')};
1171 const Path newPath {params()[u"path"_s]};
1173 if (newPath.isEmpty())
1174 throw APIError(APIErrorType::BadParams, tr("Save path cannot be empty"));
1176 // try to create the directory if it does not exist
1177 if (!Utils::Fs::mkpath(newPath))
1178 throw APIError(APIErrorType::Conflict, tr("Cannot create target directory"));
1180 // check permissions
1181 if (!Utils::Fs::isWritable(newPath))
1182 throw APIError(APIErrorType::AccessDenied, tr("Cannot write to directory"));
1184 applyToTorrents(ids, [&newPath](BitTorrent::Torrent *const torrent)
1186 if (!torrent->isAutoTMMEnabled())
1187 torrent->setSavePath(newPath);
1191 void TorrentsController::setDownloadPathAction()
1193 requireParams({u"id"_s, u"path"_s});
1195 const QStringList ids {params()[u"id"_s].split(u'|')};
1196 const Path newPath {params()[u"path"_s]};
1198 if (!newPath.isEmpty())
1200 // try to create the directory if it does not exist
1201 if (!Utils::Fs::mkpath(newPath))
1202 throw APIError(APIErrorType::Conflict, tr("Cannot create target directory"));
1204 // check permissions
1205 if (!Utils::Fs::isWritable(newPath))
1206 throw APIError(APIErrorType::AccessDenied, tr("Cannot write to directory"));
1209 applyToTorrents(ids, [&newPath](BitTorrent::Torrent *const torrent)
1211 if (!torrent->isAutoTMMEnabled())
1212 torrent->setDownloadPath(newPath);
1216 void TorrentsController::renameAction()
1218 requireParams({u"hash"_s, u"name"_s});
1220 const auto id = BitTorrent::TorrentID::fromString(params()[u"hash"_s]);
1221 QString name = params()[u"name"_s].trimmed();
1223 if (name.isEmpty())
1224 throw APIError(APIErrorType::Conflict, tr("Incorrect torrent name"));
1226 BitTorrent::Torrent *const torrent = BitTorrent::Session::instance()->getTorrent(id);
1227 if (!torrent)
1228 throw APIError(APIErrorType::NotFound);
1230 name.replace(QRegularExpression(u"\r?\n|\r"_s), u" "_s);
1231 torrent->setName(name);
1234 void TorrentsController::setAutoManagementAction()
1236 requireParams({u"hashes"_s, u"enable"_s});
1238 const QStringList hashes {params()[u"hashes"_s].split(u'|')};
1239 const bool isEnabled {parseBool(params()[u"enable"_s]).value_or(false)};
1241 applyToTorrents(hashes, [isEnabled](BitTorrent::Torrent *const torrent)
1243 torrent->setAutoTMMEnabled(isEnabled);
1247 void TorrentsController::recheckAction()
1249 requireParams({u"hashes"_s});
1251 const QStringList hashes {params()[u"hashes"_s].split(u'|')};
1252 applyToTorrents(hashes, [](BitTorrent::Torrent *const torrent) { torrent->forceRecheck(); });
1255 void TorrentsController::reannounceAction()
1257 requireParams({u"hashes"_s});
1259 const QStringList hashes {params()[u"hashes"_s].split(u'|')};
1260 applyToTorrents(hashes, [](BitTorrent::Torrent *const torrent) { torrent->forceReannounce(); });
1263 void TorrentsController::setCategoryAction()
1265 requireParams({u"hashes"_s, u"category"_s});
1267 const QStringList hashes {params()[u"hashes"_s].split(u'|')};
1268 const QString category {params()[u"category"_s]};
1270 applyToTorrents(hashes, [category](BitTorrent::Torrent *const torrent)
1272 if (!torrent->setCategory(category))
1273 throw APIError(APIErrorType::Conflict, tr("Incorrect category name"));
1277 void TorrentsController::createCategoryAction()
1279 requireParams({u"category"_s});
1281 const QString category = params()[u"category"_s];
1282 if (category.isEmpty())
1283 throw APIError(APIErrorType::BadParams, tr("Category cannot be empty"));
1285 if (!BitTorrent::Session::isValidCategoryName(category))
1286 throw APIError(APIErrorType::Conflict, tr("Incorrect category name"));
1288 const Path savePath {params()[u"savePath"_s]};
1289 const auto useDownloadPath = parseBool(params()[u"downloadPathEnabled"_s]);
1290 BitTorrent::CategoryOptions categoryOptions;
1291 categoryOptions.savePath = savePath;
1292 if (useDownloadPath.has_value())
1294 const Path downloadPath {params()[u"downloadPath"_s]};
1295 categoryOptions.downloadPath = {useDownloadPath.value(), downloadPath};
1298 if (!BitTorrent::Session::instance()->addCategory(category, categoryOptions))
1299 throw APIError(APIErrorType::Conflict, tr("Unable to create category"));
1302 void TorrentsController::editCategoryAction()
1304 requireParams({u"category"_s, u"savePath"_s});
1306 const QString category = params()[u"category"_s];
1307 if (category.isEmpty())
1308 throw APIError(APIErrorType::BadParams, tr("Category cannot be empty"));
1310 const Path savePath {params()[u"savePath"_s]};
1311 const auto useDownloadPath = parseBool(params()[u"downloadPathEnabled"_s]);
1312 BitTorrent::CategoryOptions categoryOptions;
1313 categoryOptions.savePath = savePath;
1314 if (useDownloadPath.has_value())
1316 const Path downloadPath {params()[u"downloadPath"_s]};
1317 categoryOptions.downloadPath = {useDownloadPath.value(), downloadPath};
1320 if (!BitTorrent::Session::instance()->editCategory(category, categoryOptions))
1321 throw APIError(APIErrorType::Conflict, tr("Unable to edit category"));
1324 void TorrentsController::removeCategoriesAction()
1326 requireParams({u"categories"_s});
1328 const QStringList categories {params()[u"categories"_s].split(u'\n')};
1329 for (const QString &category : categories)
1330 BitTorrent::Session::instance()->removeCategory(category);
1333 void TorrentsController::categoriesAction()
1335 const auto *session = BitTorrent::Session::instance();
1337 QJsonObject categories;
1338 const QStringList categoriesList = session->categories();
1339 for (const auto &categoryName : categoriesList)
1341 const BitTorrent::CategoryOptions categoryOptions = session->categoryOptions(categoryName);
1342 QJsonObject category = categoryOptions.toJSON();
1343 // adjust it to be compatible with existing WebAPI
1344 category[u"savePath"_s] = category.take(u"save_path"_s);
1345 category.insert(u"name"_s, categoryName);
1346 categories[categoryName] = category;
1349 setResult(categories);
1352 void TorrentsController::addTagsAction()
1354 requireParams({u"hashes"_s, u"tags"_s});
1356 const QStringList hashes {params()[u"hashes"_s].split(u'|')};
1357 const QStringList tags {params()[u"tags"_s].split(u',', Qt::SkipEmptyParts)};
1359 for (const QString &tagStr : tags)
1361 applyToTorrents(hashes, [&tagStr](BitTorrent::Torrent *const torrent)
1363 torrent->addTag(Tag(tagStr));
1368 void TorrentsController::removeTagsAction()
1370 requireParams({u"hashes"_s});
1372 const QStringList hashes {params()[u"hashes"_s].split(u'|')};
1373 const QStringList tags {params()[u"tags"_s].split(u',', Qt::SkipEmptyParts)};
1375 for (const QString &tagStr : tags)
1377 applyToTorrents(hashes, [&tagStr](BitTorrent::Torrent *const torrent)
1379 torrent->removeTag(Tag(tagStr));
1383 if (tags.isEmpty())
1385 applyToTorrents(hashes, [](BitTorrent::Torrent *const torrent)
1387 torrent->removeAllTags();
1392 void TorrentsController::createTagsAction()
1394 requireParams({u"tags"_s});
1396 const QStringList tags {params()[u"tags"_s].split(u',', Qt::SkipEmptyParts)};
1398 for (const QString &tagStr : tags)
1399 BitTorrent::Session::instance()->addTag(Tag(tagStr));
1402 void TorrentsController::deleteTagsAction()
1404 requireParams({u"tags"_s});
1406 const QStringList tags {params()[u"tags"_s].split(u',', Qt::SkipEmptyParts)};
1407 for (const QString &tagStr : tags)
1408 BitTorrent::Session::instance()->removeTag(Tag(tagStr));
1411 void TorrentsController::tagsAction()
1413 QJsonArray result;
1414 for (const Tag &tag : asConst(BitTorrent::Session::instance()->tags()))
1415 result << tag.toString();
1416 setResult(result);
1419 void TorrentsController::renameFileAction()
1421 requireParams({u"hash"_s, u"oldPath"_s, u"newPath"_s});
1423 const auto id = BitTorrent::TorrentID::fromString(params()[u"hash"_s]);
1424 BitTorrent::Torrent *const torrent = BitTorrent::Session::instance()->getTorrent(id);
1425 if (!torrent)
1426 throw APIError(APIErrorType::NotFound);
1428 const Path oldPath {params()[u"oldPath"_s]};
1429 const Path newPath {params()[u"newPath"_s]};
1433 torrent->renameFile(oldPath, newPath);
1435 catch (const RuntimeError &error)
1437 throw APIError(APIErrorType::Conflict, error.message());
1441 void TorrentsController::renameFolderAction()
1443 requireParams({u"hash"_s, u"oldPath"_s, u"newPath"_s});
1445 const auto id = BitTorrent::TorrentID::fromString(params()[u"hash"_s]);
1446 BitTorrent::Torrent *const torrent = BitTorrent::Session::instance()->getTorrent(id);
1447 if (!torrent)
1448 throw APIError(APIErrorType::NotFound);
1450 const Path oldPath {params()[u"oldPath"_s]};
1451 const Path newPath {params()[u"newPath"_s]};
1455 torrent->renameFolder(oldPath, newPath);
1457 catch (const RuntimeError &error)
1459 throw APIError(APIErrorType::Conflict, error.message());
1463 void TorrentsController::exportAction()
1465 requireParams({u"hash"_s});
1467 const auto id = BitTorrent::TorrentID::fromString(params()[u"hash"_s]);
1468 const BitTorrent::Torrent *const torrent = BitTorrent::Session::instance()->getTorrent(id);
1469 if (!torrent)
1470 throw APIError(APIErrorType::NotFound);
1472 const nonstd::expected<QByteArray, QString> result = torrent->exportToBuffer();
1473 if (!result)
1474 throw APIError(APIErrorType::Conflict, tr("Unable to export torrent file. Error: %1").arg(result.error()));
1476 setResult(result.value(), u"application/x-bittorrent"_s, (id.toString() + u".torrent"));
1479 void TorrentsController::SSLParametersAction()
1481 requireParams({u"hash"_s});
1483 const auto id = BitTorrent::TorrentID::fromString(params()[u"hash"_s]);
1484 const BitTorrent::Torrent *torrent = BitTorrent::Session::instance()->getTorrent(id);
1485 if (!torrent)
1486 throw APIError(APIErrorType::NotFound);
1488 const BitTorrent::SSLParameters sslParams = torrent->getSSLParameters();
1489 const QJsonObject ret
1491 {KEY_PROP_SSL_CERTIFICATE, QString::fromLatin1(sslParams.certificate.toPem())},
1492 {KEY_PROP_SSL_PRIVATEKEY, QString::fromLatin1(sslParams.privateKey.toPem())},
1493 {KEY_PROP_SSL_DHPARAMS, QString::fromLatin1(sslParams.dhParams)}
1495 setResult(ret);
1498 void TorrentsController::setSSLParametersAction()
1500 requireParams({u"hash"_s, KEY_PROP_SSL_CERTIFICATE, KEY_PROP_SSL_PRIVATEKEY, KEY_PROP_SSL_DHPARAMS});
1502 const auto id = BitTorrent::TorrentID::fromString(params()[u"hash"_s]);
1503 BitTorrent::Torrent *const torrent = BitTorrent::Session::instance()->getTorrent(id);
1504 if (!torrent)
1505 throw APIError(APIErrorType::NotFound);
1507 const BitTorrent::SSLParameters sslParams
1509 .certificate = QSslCertificate(params()[KEY_PROP_SSL_CERTIFICATE].toLatin1()),
1510 .privateKey = Utils::SSLKey::load(params()[KEY_PROP_SSL_PRIVATEKEY].toLatin1()),
1511 .dhParams = params()[KEY_PROP_SSL_DHPARAMS].toLatin1()
1513 if (!sslParams.isValid())
1514 throw APIError(APIErrorType::BadData);
1516 torrent->setSSLParameters(sslParams);