WebUI: Provide 'Merge trackers to existing torrent' option
[qBittorrent.git] / src / webui / api / torrentscontroller.cpp
blob290cb21687fa5186ce735fd61fd7e3c24232efe9
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_POPULARITY = u"popularity"_s;
101 const QString KEY_PROP_REANNOUNCE = u"reannounce"_s;
102 const QString KEY_PROP_TOTAL_SIZE = u"total_size"_s;
103 const QString KEY_PROP_PIECES_NUM = u"pieces_num"_s;
104 const QString KEY_PROP_PIECE_SIZE = u"piece_size"_s;
105 const QString KEY_PROP_PIECES_HAVE = u"pieces_have"_s;
106 const QString KEY_PROP_CREATED_BY = u"created_by"_s;
107 const QString KEY_PROP_LAST_SEEN = u"last_seen"_s;
108 const QString KEY_PROP_ADDITION_DATE = u"addition_date"_s;
109 const QString KEY_PROP_COMPLETION_DATE = u"completion_date"_s;
110 const QString KEY_PROP_CREATION_DATE = u"creation_date"_s;
111 const QString KEY_PROP_SAVE_PATH = u"save_path"_s;
112 const QString KEY_PROP_DOWNLOAD_PATH = u"download_path"_s;
113 const QString KEY_PROP_COMMENT = u"comment"_s;
114 const QString KEY_PROP_IS_PRIVATE = u"is_private"_s; // deprecated, "private" should be used instead
115 const QString KEY_PROP_PRIVATE = u"private"_s;
116 const QString KEY_PROP_SSL_CERTIFICATE = u"ssl_certificate"_s;
117 const QString KEY_PROP_SSL_PRIVATEKEY = u"ssl_private_key"_s;
118 const QString KEY_PROP_SSL_DHPARAMS = u"ssl_dh_params"_s;
119 const QString KEY_PROP_HAS_METADATA = u"has_metadata"_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 void applyToTorrents(const QStringList &idList, const std::function<void (BitTorrent::Torrent *torrent)> &func)
142 if ((idList.size() == 1) && (idList[0] == u"all"))
144 for (BitTorrent::Torrent *const torrent : asConst(BitTorrent::Session::instance()->torrents()))
145 func(torrent);
147 else
149 for (const QString &idString : idList)
151 const auto hash = BitTorrent::TorrentID::fromString(idString);
152 BitTorrent::Torrent *const torrent = BitTorrent::Session::instance()->getTorrent(hash);
153 if (torrent)
154 func(torrent);
159 std::optional<QString> getOptionalString(const StringMap &params, const QString &name)
161 const auto it = params.constFind(name);
162 if (it == params.cend())
163 return std::nullopt;
165 return it.value();
168 std::optional<Tag> getOptionalTag(const StringMap &params, const QString &name)
170 const auto it = params.constFind(name);
171 if (it == params.cend())
172 return std::nullopt;
174 return Tag(it.value());
177 QJsonArray getStickyTrackers(const BitTorrent::Torrent *const torrent)
179 int seedsDHT = 0, seedsPeX = 0, seedsLSD = 0, leechesDHT = 0, leechesPeX = 0, leechesLSD = 0;
180 for (const BitTorrent::PeerInfo &peer : asConst(torrent->peers()))
182 if (peer.isConnecting()) continue;
184 if (peer.isSeed())
186 if (peer.fromDHT())
187 ++seedsDHT;
188 if (peer.fromPeX())
189 ++seedsPeX;
190 if (peer.fromLSD())
191 ++seedsLSD;
193 else
195 if (peer.fromDHT())
196 ++leechesDHT;
197 if (peer.fromPeX())
198 ++leechesPeX;
199 if (peer.fromLSD())
200 ++leechesLSD;
204 const int working = static_cast<int>(BitTorrent::TrackerEndpointState::Working);
205 const int disabled = 0;
207 const QString privateMsg {QCoreApplication::translate("TrackerListWidget", "This torrent is private")};
208 const bool isTorrentPrivate = torrent->isPrivate();
210 const QJsonObject dht
212 {KEY_TRACKER_URL, u"** [DHT] **"_s},
213 {KEY_TRACKER_TIER, -1},
214 {KEY_TRACKER_MSG, (isTorrentPrivate ? privateMsg : u""_s)},
215 {KEY_TRACKER_STATUS, ((BitTorrent::Session::instance()->isDHTEnabled() && !isTorrentPrivate) ? working : disabled)},
216 {KEY_TRACKER_PEERS_COUNT, 0},
217 {KEY_TRACKER_DOWNLOADED_COUNT, 0},
218 {KEY_TRACKER_SEEDS_COUNT, seedsDHT},
219 {KEY_TRACKER_LEECHES_COUNT, leechesDHT}
222 const QJsonObject pex
224 {KEY_TRACKER_URL, u"** [PeX] **"_s},
225 {KEY_TRACKER_TIER, -1},
226 {KEY_TRACKER_MSG, (isTorrentPrivate ? privateMsg : u""_s)},
227 {KEY_TRACKER_STATUS, ((BitTorrent::Session::instance()->isPeXEnabled() && !isTorrentPrivate) ? working : disabled)},
228 {KEY_TRACKER_PEERS_COUNT, 0},
229 {KEY_TRACKER_DOWNLOADED_COUNT, 0},
230 {KEY_TRACKER_SEEDS_COUNT, seedsPeX},
231 {KEY_TRACKER_LEECHES_COUNT, leechesPeX}
234 const QJsonObject lsd
236 {KEY_TRACKER_URL, u"** [LSD] **"_s},
237 {KEY_TRACKER_TIER, -1},
238 {KEY_TRACKER_MSG, (isTorrentPrivate ? privateMsg : u""_s)},
239 {KEY_TRACKER_STATUS, ((BitTorrent::Session::instance()->isLSDEnabled() && !isTorrentPrivate) ? working : disabled)},
240 {KEY_TRACKER_PEERS_COUNT, 0},
241 {KEY_TRACKER_DOWNLOADED_COUNT, 0},
242 {KEY_TRACKER_SEEDS_COUNT, seedsLSD},
243 {KEY_TRACKER_LEECHES_COUNT, leechesLSD}
246 return {dht, pex, lsd};
249 QList<BitTorrent::TorrentID> toTorrentIDs(const QStringList &idStrings)
251 QList<BitTorrent::TorrentID> idList;
252 idList.reserve(idStrings.size());
253 for (const QString &hash : idStrings)
254 idList << BitTorrent::TorrentID::fromString(hash);
255 return idList;
258 nonstd::expected<QUrl, QString> validateWebSeedUrl(const QString &urlStr)
260 const QString normalizedUrlStr = QUrl::fromPercentEncoding(urlStr.toLatin1());
262 const QUrl url {normalizedUrlStr, QUrl::StrictMode};
263 if (!url.isValid())
264 return nonstd::make_unexpected(TorrentsController::tr("\"%1\" is not a valid URL").arg(normalizedUrlStr));
266 if (!SUPPORTED_WEB_SEED_SCHEMES.contains(url.scheme()))
267 return nonstd::make_unexpected(TorrentsController::tr("URL scheme must be one of [%1]").arg(SUPPORTED_WEB_SEED_SCHEMES.values().join(u", ")));
269 return url;
273 void TorrentsController::countAction()
275 setResult(QString::number(BitTorrent::Session::instance()->torrentsCount()));
278 // Returns all the torrents in JSON format.
279 // The return value is a JSON-formatted list of dictionaries.
280 // The dictionary keys are:
281 // - "hash": Torrent hash (ID)
282 // - "name": Torrent name
283 // - "size": Torrent size
284 // - "progress": Torrent progress
285 // - "dlspeed": Torrent download speed
286 // - "upspeed": Torrent upload speed
287 // - "priority": Torrent queue position (-1 if queuing is disabled)
288 // - "num_seeds": Torrent seeds connected to
289 // - "num_complete": Torrent seeds in the swarm
290 // - "num_leechs": Torrent leechers connected to
291 // - "num_incomplete": Torrent leechers in the swarm
292 // - "ratio": Torrent share ratio
293 // - "eta": Torrent ETA
294 // - "state": Torrent state
295 // - "seq_dl": Torrent sequential download state
296 // - "f_l_piece_prio": Torrent first last piece priority state
297 // - "force_start": Torrent force start state
298 // - "category": Torrent category
299 // GET params:
300 // - filter (string): all, downloading, seeding, completed, stopped, running, active, inactive, stalled, stalled_uploading, stalled_downloading
301 // - category (string): torrent category for filtering by it (empty string means "uncategorized"; no "category" param presented means "any category")
302 // - tag (string): torrent tag for filtering by it (empty string means "untagged"; no "tag" param presented means "any tag")
303 // - hashes (string): filter by hashes, can contain multiple hashes separated by |
304 // - private (bool): filter torrents that are from private trackers (true) or not (false). Empty means any torrent (no filtering)
305 // - sort (string): name of column for sorting by its value
306 // - reverse (bool): enable reverse sorting
307 // - limit (int): set limit number of torrents returned (if greater than 0, otherwise - unlimited)
308 // - offset (int): set offset (if less than 0 - offset from end)
309 void TorrentsController::infoAction()
311 const QString filter {params()[u"filter"_s]};
312 const std::optional<QString> category = getOptionalString(params(), u"category"_s);
313 const std::optional<Tag> tag = getOptionalTag(params(), u"tag"_s);
314 const QString sortedColumn {params()[u"sort"_s]};
315 const bool reverse {parseBool(params()[u"reverse"_s]).value_or(false)};
316 int limit {params()[u"limit"_s].toInt()};
317 int offset {params()[u"offset"_s].toInt()};
318 const QStringList hashes {params()[u"hashes"_s].split(u'|', Qt::SkipEmptyParts)};
319 const std::optional<bool> isPrivate = parseBool(params()[u"private"_s]);
321 std::optional<TorrentIDSet> idSet;
322 if (!hashes.isEmpty())
324 idSet = TorrentIDSet();
325 for (const QString &hash : hashes)
326 idSet->insert(BitTorrent::TorrentID::fromString(hash));
329 const TorrentFilter torrentFilter {filter, idSet, category, tag, isPrivate};
330 QVariantList torrentList;
331 for (const BitTorrent::Torrent *torrent : asConst(BitTorrent::Session::instance()->torrents()))
333 if (torrentFilter.match(torrent))
334 torrentList.append(serialize(*torrent));
337 if (torrentList.isEmpty())
339 setResult(QJsonArray {});
340 return;
343 if (!sortedColumn.isEmpty())
345 if (!torrentList[0].toMap().contains(sortedColumn))
346 throw APIError(APIErrorType::BadParams, tr("'sort' parameter is invalid"));
348 const auto lessThan = [](const QVariant &left, const QVariant &right) -> bool
350 Q_ASSERT(left.userType() == right.userType());
352 switch (left.userType())
354 case QMetaType::Bool:
355 return left.value<bool>() < right.value<bool>();
356 case QMetaType::Double:
357 return left.value<double>() < right.value<double>();
358 case QMetaType::Float:
359 return left.value<float>() < right.value<float>();
360 case QMetaType::Int:
361 return left.value<int>() < right.value<int>();
362 case QMetaType::LongLong:
363 return left.value<qlonglong>() < right.value<qlonglong>();
364 case QMetaType::QString:
365 return left.value<QString>() < right.value<QString>();
366 default:
367 qWarning("Unhandled QVariant comparison, type: %d, name: %s"
368 , left.userType(), left.metaType().name());
369 break;
371 return false;
374 std::sort(torrentList.begin(), torrentList.end()
375 , [reverse, &sortedColumn, &lessThan](const QVariant &torrent1, const QVariant &torrent2)
377 const QVariant value1 {torrent1.toMap().value(sortedColumn)};
378 const QVariant value2 {torrent2.toMap().value(sortedColumn)};
379 return reverse ? lessThan(value2, value1) : lessThan(value1, value2);
383 const int size = torrentList.size();
384 // normalize offset
385 if (offset < 0)
386 offset = size + offset;
387 if ((offset >= size) || (offset < 0))
388 offset = 0;
389 // normalize limit
390 if (limit <= 0)
391 limit = -1; // unlimited
393 if ((limit > 0) || (offset > 0))
394 torrentList = torrentList.mid(offset, limit);
396 setResult(QJsonArray::fromVariantList(torrentList));
399 // Returns the properties for a torrent in JSON format.
400 // The return value is a JSON-formatted dictionary.
401 // The dictionary keys are:
402 // - "time_elapsed": Torrent elapsed time
403 // - "seeding_time": Torrent elapsed time while complete
404 // - "eta": Torrent ETA
405 // - "nb_connections": Torrent connection count
406 // - "nb_connections_limit": Torrent connection count limit
407 // - "total_downloaded": Total data uploaded for torrent
408 // - "total_downloaded_session": Total data downloaded this session
409 // - "total_uploaded": Total data uploaded for torrent
410 // - "total_uploaded_session": Total data uploaded this session
411 // - "dl_speed": Torrent download speed
412 // - "dl_speed_avg": Torrent average download speed
413 // - "up_speed": Torrent upload speed
414 // - "up_speed_avg": Torrent average upload speed
415 // - "dl_limit": Torrent download limit
416 // - "up_limit": Torrent upload limit
417 // - "total_wasted": Total data wasted for torrent
418 // - "seeds": Torrent connected seeds
419 // - "seeds_total": Torrent total number of seeds
420 // - "peers": Torrent connected peers
421 // - "peers_total": Torrent total number of peers
422 // - "share_ratio": Torrent share ratio
423 // - "popularity": Torrent popularity
424 // - "reannounce": Torrent next reannounce time
425 // - "total_size": Torrent total size
426 // - "pieces_num": Torrent pieces count
427 // - "piece_size": Torrent piece size
428 // - "pieces_have": Torrent pieces have
429 // - "created_by": Torrent creator
430 // - "last_seen": Torrent last seen complete
431 // - "addition_date": Torrent addition date
432 // - "completion_date": Torrent completion date
433 // - "creation_date": Torrent creation date
434 // - "save_path": Torrent save path
435 // - "download_path": Torrent download path
436 // - "comment": Torrent comment
437 // - "infohash_v1": Torrent v1 infohash (or empty string for v2 torrents)
438 // - "infohash_v2": Torrent v2 infohash (or empty string for v1 torrents)
439 // - "hash": Torrent TorrentID (infohashv1 for v1 torrents, truncated infohashv2 for v2/hybrid torrents)
440 // - "name": Torrent name
441 void TorrentsController::propertiesAction()
443 requireParams({u"hash"_s});
445 const auto id = BitTorrent::TorrentID::fromString(params()[u"hash"_s]);
446 const BitTorrent::Torrent *const torrent = BitTorrent::Session::instance()->getTorrent(id);
447 if (!torrent)
448 throw APIError(APIErrorType::NotFound);
450 const BitTorrent::InfoHash infoHash = torrent->infoHash();
451 const qlonglong totalDownload = torrent->totalDownload();
452 const qlonglong totalUpload = torrent->totalUpload();
453 const qlonglong dlDuration = torrent->activeTime() - torrent->finishedTime();
454 const qlonglong ulDuration = torrent->activeTime();
455 const int downloadLimit = torrent->downloadLimit();
456 const int uploadLimit = torrent->uploadLimit();
457 const qreal ratio = torrent->realRatio();
458 const qreal popularity = torrent->popularity();
459 const bool hasMetadata = torrent->hasMetadata();
460 const bool isPrivate = torrent->isPrivate();
462 const QJsonObject ret
464 {KEY_TORRENT_INFOHASHV1, infoHash.v1().toString()},
465 {KEY_TORRENT_INFOHASHV2, infoHash.v2().toString()},
466 {KEY_TORRENT_NAME, torrent->name()},
467 {KEY_TORRENT_ID, torrent->id().toString()},
468 {KEY_PROP_TIME_ELAPSED, torrent->activeTime()},
469 {KEY_PROP_SEEDING_TIME, torrent->finishedTime()},
470 {KEY_PROP_ETA, torrent->eta()},
471 {KEY_PROP_CONNECT_COUNT, torrent->connectionsCount()},
472 {KEY_PROP_CONNECT_COUNT_LIMIT, torrent->connectionsLimit()},
473 {KEY_PROP_DOWNLOADED, totalDownload},
474 {KEY_PROP_DOWNLOADED_SESSION, torrent->totalPayloadDownload()},
475 {KEY_PROP_UPLOADED, totalUpload},
476 {KEY_PROP_UPLOADED_SESSION, torrent->totalPayloadUpload()},
477 {KEY_PROP_DL_SPEED, torrent->downloadPayloadRate()},
478 {KEY_PROP_DL_SPEED_AVG, ((dlDuration > 0) ? (totalDownload / dlDuration) : -1)},
479 {KEY_PROP_UP_SPEED, torrent->uploadPayloadRate()},
480 {KEY_PROP_UP_SPEED_AVG, ((ulDuration > 0) ? (totalUpload / ulDuration) : -1)},
481 {KEY_PROP_DL_LIMIT, ((downloadLimit > 0) ? downloadLimit : -1)},
482 {KEY_PROP_UP_LIMIT, ((uploadLimit > 0) ? uploadLimit : -1)},
483 {KEY_PROP_WASTED, torrent->wastedSize()},
484 {KEY_PROP_SEEDS, torrent->seedsCount()},
485 {KEY_PROP_SEEDS_TOTAL, torrent->totalSeedsCount()},
486 {KEY_PROP_PEERS, torrent->leechsCount()},
487 {KEY_PROP_PEERS_TOTAL, torrent->totalLeechersCount()},
488 {KEY_PROP_RATIO, ((ratio > BitTorrent::Torrent::MAX_RATIO) ? -1 : ratio)},
489 {KEY_PROP_POPULARITY, ((popularity > BitTorrent::Torrent::MAX_RATIO) ? -1 : popularity)},
490 {KEY_PROP_REANNOUNCE, torrent->nextAnnounce()},
491 {KEY_PROP_TOTAL_SIZE, torrent->totalSize()},
492 {KEY_PROP_PIECES_NUM, torrent->piecesCount()},
493 {KEY_PROP_PIECE_SIZE, torrent->pieceLength()},
494 {KEY_PROP_PIECES_HAVE, torrent->piecesHave()},
495 {KEY_PROP_CREATED_BY, torrent->creator()},
496 {KEY_PROP_IS_PRIVATE, torrent->isPrivate()}, // used for maintaining backward compatibility
497 {KEY_PROP_PRIVATE, (hasMetadata ? isPrivate : QJsonValue())},
498 {KEY_PROP_ADDITION_DATE, Utils::DateTime::toSecsSinceEpoch(torrent->addedTime())},
499 {KEY_PROP_LAST_SEEN, Utils::DateTime::toSecsSinceEpoch(torrent->lastSeenComplete())},
500 {KEY_PROP_COMPLETION_DATE, Utils::DateTime::toSecsSinceEpoch(torrent->completedTime())},
501 {KEY_PROP_CREATION_DATE, Utils::DateTime::toSecsSinceEpoch(torrent->creationDate())},
502 {KEY_PROP_SAVE_PATH, torrent->savePath().toString()},
503 {KEY_PROP_DOWNLOAD_PATH, torrent->downloadPath().toString()},
504 {KEY_PROP_COMMENT, torrent->comment()},
505 {KEY_PROP_HAS_METADATA, torrent->hasMetadata()}
508 setResult(ret);
511 // Returns the trackers for a torrent in JSON format.
512 // The return value is a JSON-formatted list of dictionaries.
513 // The dictionary keys are:
514 // - "url": Tracker URL
515 // - "status": Tracker status
516 // - "tier": Tracker tier
517 // - "num_peers": Number of peers this torrent is currently connected to
518 // - "num_seeds": Number of peers that have the whole file
519 // - "num_leeches": Number of peers that are still downloading
520 // - "num_downloaded": Tracker downloaded count
521 // - "msg": Tracker message (last)
522 void TorrentsController::trackersAction()
524 requireParams({u"hash"_s});
526 const auto id = BitTorrent::TorrentID::fromString(params()[u"hash"_s]);
527 const BitTorrent::Torrent *const torrent = BitTorrent::Session::instance()->getTorrent(id);
528 if (!torrent)
529 throw APIError(APIErrorType::NotFound);
531 QJsonArray trackerList = getStickyTrackers(torrent);
533 for (const BitTorrent::TrackerEntryStatus &tracker : asConst(torrent->trackers()))
535 const bool isNotWorking = (tracker.state == BitTorrent::TrackerEndpointState::NotWorking)
536 || (tracker.state == BitTorrent::TrackerEndpointState::TrackerError)
537 || (tracker.state == BitTorrent::TrackerEndpointState::Unreachable);
538 trackerList << QJsonObject
540 {KEY_TRACKER_URL, tracker.url},
541 {KEY_TRACKER_TIER, tracker.tier},
542 {KEY_TRACKER_STATUS, static_cast<int>((isNotWorking ? BitTorrent::TrackerEndpointState::NotWorking : tracker.state))},
543 {KEY_TRACKER_MSG, tracker.message},
544 {KEY_TRACKER_PEERS_COUNT, tracker.numPeers},
545 {KEY_TRACKER_SEEDS_COUNT, tracker.numSeeds},
546 {KEY_TRACKER_LEECHES_COUNT, tracker.numLeeches},
547 {KEY_TRACKER_DOWNLOADED_COUNT, tracker.numDownloaded}
551 setResult(trackerList);
554 // Returns the web seeds for a torrent in JSON format.
555 // The return value is a JSON-formatted list of dictionaries.
556 // The dictionary keys are:
557 // - "url": Web seed URL
558 void TorrentsController::webseedsAction()
560 requireParams({u"hash"_s});
562 const auto id = BitTorrent::TorrentID::fromString(params()[u"hash"_s]);
563 BitTorrent::Torrent *const torrent = BitTorrent::Session::instance()->getTorrent(id);
564 if (!torrent)
565 throw APIError(APIErrorType::NotFound);
567 QJsonArray webSeedList;
568 for (const QUrl &webseed : asConst(torrent->urlSeeds()))
570 webSeedList.append(QJsonObject
572 {KEY_WEBSEED_URL, webseed.toString()}
576 setResult(webSeedList);
579 void TorrentsController::addWebSeedsAction()
581 requireParams({u"hash"_s, u"urls"_s});
582 const QStringList paramUrls = params()[u"urls"_s].split(u'|', Qt::SkipEmptyParts);
584 const auto id = BitTorrent::TorrentID::fromString(params()[u"hash"_s]);
585 BitTorrent::Torrent *const torrent = BitTorrent::Session::instance()->getTorrent(id);
586 if (!torrent)
587 throw APIError(APIErrorType::NotFound);
589 QList<QUrl> urls;
590 urls.reserve(paramUrls.size());
591 for (const QString &urlStr : paramUrls)
593 const auto result = validateWebSeedUrl(urlStr);
594 if (!result)
595 throw APIError(APIErrorType::BadParams, result.error());
596 urls << result.value();
599 torrent->addUrlSeeds(urls);
602 void TorrentsController::editWebSeedAction()
604 requireParams({u"hash"_s, u"origUrl"_s, u"newUrl"_s});
606 const auto id = BitTorrent::TorrentID::fromString(params()[u"hash"_s]);
607 const QString origUrlStr = params()[u"origUrl"_s];
608 const QString newUrlStr = params()[u"newUrl"_s];
610 BitTorrent::Torrent *const torrent = BitTorrent::Session::instance()->getTorrent(id);
611 if (!torrent)
612 throw APIError(APIErrorType::NotFound);
614 const auto origUrlResult = validateWebSeedUrl(origUrlStr);
615 if (!origUrlResult)
616 throw APIError(APIErrorType::BadParams, origUrlResult.error());
617 const QUrl origUrl = origUrlResult.value();
619 const auto newUrlResult = validateWebSeedUrl(newUrlStr);
620 if (!newUrlResult)
621 throw APIError(APIErrorType::BadParams, newUrlResult.error());
622 const QUrl newUrl = newUrlResult.value();
624 if (newUrl != origUrl)
626 if (!torrent->urlSeeds().contains(origUrl))
627 throw APIError(APIErrorType::Conflict, tr("\"%1\" is not an existing URL").arg(origUrl.toString()));
629 torrent->removeUrlSeeds({origUrl});
630 torrent->addUrlSeeds({newUrl});
634 void TorrentsController::removeWebSeedsAction()
636 requireParams({u"hash"_s, u"urls"_s});
637 const QStringList paramUrls = params()[u"urls"_s].split(u'|', Qt::SkipEmptyParts);
639 const auto id = BitTorrent::TorrentID::fromString(params()[u"hash"_s]);
640 BitTorrent::Torrent *const torrent = BitTorrent::Session::instance()->getTorrent(id);
641 if (!torrent)
642 throw APIError(APIErrorType::NotFound);
644 QList<QUrl> urls;
645 urls.reserve(paramUrls.size());
646 for (const QString &urlStr : paramUrls)
648 const auto result = validateWebSeedUrl(urlStr);
649 if (!result)
650 throw APIError(APIErrorType::BadParams, result.error());
651 urls << result.value();
654 torrent->removeUrlSeeds(urls);
657 // Returns the files in a torrent in JSON format.
658 // The return value is a JSON-formatted list of dictionaries.
659 // The dictionary keys are:
660 // - "index": File index
661 // - "name": File name
662 // - "size": File size
663 // - "progress": File progress
664 // - "priority": File priority
665 // - "is_seed": Flag indicating if torrent is seeding/complete
666 // - "piece_range": Piece index range, the first number is the starting piece index
667 // and the second number is the ending piece index (inclusive)
668 void TorrentsController::filesAction()
670 requireParams({u"hash"_s});
672 const auto id = BitTorrent::TorrentID::fromString(params()[u"hash"_s]);
673 const BitTorrent::Torrent *const torrent = BitTorrent::Session::instance()->getTorrent(id);
674 if (!torrent)
675 throw APIError(APIErrorType::NotFound);
677 const int filesCount = torrent->filesCount();
678 QList<int> fileIndexes;
679 const auto idxIt = params().constFind(u"indexes"_s);
680 if (idxIt != params().cend())
682 const QStringList indexStrings = idxIt.value().split(u'|');
683 fileIndexes.reserve(indexStrings.size());
684 std::transform(indexStrings.cbegin(), indexStrings.cend(), std::back_inserter(fileIndexes)
685 , [&filesCount](const QString &indexString) -> int
687 bool ok = false;
688 const int index = indexString.toInt(&ok);
689 if (!ok || (index < 0))
690 throw APIError(APIErrorType::Conflict, tr("\"%1\" is not a valid file index.").arg(indexString));
691 if (index >= filesCount)
692 throw APIError(APIErrorType::Conflict, tr("Index %1 is out of bounds.").arg(indexString));
693 return index;
696 else
698 fileIndexes.reserve(filesCount);
699 for (int i = 0; i < filesCount; ++i)
700 fileIndexes.append(i);
703 QJsonArray fileList;
704 if (torrent->hasMetadata())
706 const QList<BitTorrent::DownloadPriority> priorities = torrent->filePriorities();
707 const QList<qreal> fp = torrent->filesProgress();
708 const QList<qreal> fileAvailability = torrent->availableFileFractions();
709 const BitTorrent::TorrentInfo info = torrent->info();
710 for (const int index : asConst(fileIndexes))
712 QJsonObject fileDict =
714 {KEY_FILE_INDEX, index},
715 {KEY_FILE_PROGRESS, fp[index]},
716 {KEY_FILE_PRIORITY, static_cast<int>(priorities[index])},
717 {KEY_FILE_SIZE, torrent->fileSize(index)},
718 {KEY_FILE_AVAILABILITY, fileAvailability[index]},
719 // need to provide paths using a platform-independent separator format
720 {KEY_FILE_NAME, torrent->filePath(index).data()}
723 const BitTorrent::TorrentInfo::PieceRange idx = info.filePieces(index);
724 fileDict[KEY_FILE_PIECE_RANGE] = QJsonArray {idx.first(), idx.last()};
726 if (index == 0)
727 fileDict[KEY_FILE_IS_SEED] = torrent->isFinished();
729 fileList.append(fileDict);
733 setResult(fileList);
736 // Returns an array of hashes (of each pieces respectively) for a torrent in JSON format.
737 // The return value is a JSON-formatted array of strings (hex strings).
738 void TorrentsController::pieceHashesAction()
740 requireParams({u"hash"_s});
742 const auto id = BitTorrent::TorrentID::fromString(params()[u"hash"_s]);
743 BitTorrent::Torrent *const torrent = BitTorrent::Session::instance()->getTorrent(id);
744 if (!torrent)
745 throw APIError(APIErrorType::NotFound);
747 QJsonArray pieceHashes;
748 if (torrent->hasMetadata())
750 const QList<QByteArray> hashes = torrent->info().pieceHashes();
751 for (const QByteArray &hash : hashes)
752 pieceHashes.append(QString::fromLatin1(hash.toHex()));
755 setResult(pieceHashes);
758 // Returns an array of states (of each pieces respectively) for a torrent in JSON format.
759 // The return value is a JSON-formatted array of ints.
760 // 0: piece not downloaded
761 // 1: piece requested or downloading
762 // 2: piece already downloaded
763 void TorrentsController::pieceStatesAction()
765 requireParams({u"hash"_s});
767 const auto id = BitTorrent::TorrentID::fromString(params()[u"hash"_s]);
768 BitTorrent::Torrent *const torrent = BitTorrent::Session::instance()->getTorrent(id);
769 if (!torrent)
770 throw APIError(APIErrorType::NotFound);
772 QJsonArray pieceStates;
773 const QBitArray states = torrent->pieces();
774 for (int i = 0; i < states.size(); ++i)
775 pieceStates.append(static_cast<int>(states[i]) * 2);
777 const QBitArray dlstates = torrent->downloadingPieces();
778 for (int i = 0; i < states.size(); ++i)
780 if (dlstates[i])
781 pieceStates[i] = 1;
784 setResult(pieceStates);
787 void TorrentsController::addAction()
789 const QString urls = params()[u"urls"_s];
790 const QString cookie = params()[u"cookie"_s];
792 const bool skipChecking = parseBool(params()[u"skip_checking"_s]).value_or(false);
793 const bool seqDownload = parseBool(params()[u"sequentialDownload"_s]).value_or(false);
794 const bool firstLastPiece = parseBool(params()[u"firstLastPiecePrio"_s]).value_or(false);
795 const std::optional<bool> addToQueueTop = parseBool(params()[u"addToTopOfQueue"_s]);
796 const std::optional<bool> addStopped = parseBool(params()[u"stopped"_s]);
797 const QString savepath = params()[u"savepath"_s].trimmed();
798 const QString downloadPath = params()[u"downloadPath"_s].trimmed();
799 const std::optional<bool> useDownloadPath = parseBool(params()[u"useDownloadPath"_s]);
800 const QString category = params()[u"category"_s];
801 const QStringList tags = params()[u"tags"_s].split(u',', Qt::SkipEmptyParts);
802 const QString torrentName = params()[u"rename"_s].trimmed();
803 const int upLimit = parseInt(params()[u"upLimit"_s]).value_or(-1);
804 const int dlLimit = parseInt(params()[u"dlLimit"_s]).value_or(-1);
805 const double ratioLimit = parseDouble(params()[u"ratioLimit"_s]).value_or(BitTorrent::Torrent::USE_GLOBAL_RATIO);
806 const int seedingTimeLimit = parseInt(params()[u"seedingTimeLimit"_s]).value_or(BitTorrent::Torrent::USE_GLOBAL_SEEDING_TIME);
807 const int inactiveSeedingTimeLimit = parseInt(params()[u"inactiveSeedingTimeLimit"_s]).value_or(BitTorrent::Torrent::USE_GLOBAL_INACTIVE_SEEDING_TIME);
808 const BitTorrent::ShareLimitAction shareLimitAction = Utils::String::toEnum(params()[u"shareLimitAction"_s], BitTorrent::ShareLimitAction::Default);
809 const std::optional<bool> autoTMM = parseBool(params()[u"autoTMM"_s]);
811 const QString stopConditionParam = params()[u"stopCondition"_s];
812 const std::optional<BitTorrent::Torrent::StopCondition> stopCondition = (!stopConditionParam.isEmpty()
813 ? Utils::String::toEnum(stopConditionParam, BitTorrent::Torrent::StopCondition::None)
814 : std::optional<BitTorrent::Torrent::StopCondition> {});
816 const QString contentLayoutParam = params()[u"contentLayout"_s];
817 const std::optional<BitTorrent::TorrentContentLayout> contentLayout = (!contentLayoutParam.isEmpty()
818 ? Utils::String::toEnum(contentLayoutParam, BitTorrent::TorrentContentLayout::Original)
819 : std::optional<BitTorrent::TorrentContentLayout> {});
821 QList<QNetworkCookie> cookies;
822 if (!cookie.isEmpty())
824 const QStringList cookiesStr = cookie.split(u"; "_s);
825 for (QString cookieStr : cookiesStr)
827 cookieStr = cookieStr.trimmed();
828 int index = cookieStr.indexOf(u'=');
829 if (index > 1)
831 QByteArray name = cookieStr.left(index).toLatin1();
832 QByteArray value = cookieStr.right(cookieStr.length() - index - 1).toLatin1();
833 cookies += QNetworkCookie(name, value);
838 const BitTorrent::AddTorrentParams addTorrentParams
840 // TODO: Check if destination actually exists
841 .name = torrentName,
842 .category = category,
843 .tags = {tags.cbegin(), tags.cend()},
844 .savePath = Path(savepath),
845 .useDownloadPath = useDownloadPath,
846 .downloadPath = Path(downloadPath),
847 .sequential = seqDownload,
848 .firstLastPiecePriority = firstLastPiece,
849 .addForced = false,
850 .addToQueueTop = addToQueueTop,
851 .addStopped = addStopped,
852 .stopCondition = stopCondition,
853 .filePaths = {},
854 .filePriorities = {},
855 .skipChecking = skipChecking,
856 .contentLayout = contentLayout,
857 .useAutoTMM = autoTMM,
858 .uploadLimit = upLimit,
859 .downloadLimit = dlLimit,
860 .seedingTimeLimit = seedingTimeLimit,
861 .inactiveSeedingTimeLimit = inactiveSeedingTimeLimit,
862 .ratioLimit = ratioLimit,
863 .shareLimitAction = shareLimitAction,
864 .sslParameters =
866 .certificate = QSslCertificate(params()[KEY_PROP_SSL_CERTIFICATE].toLatin1()),
867 .privateKey = Utils::SSLKey::load(params()[KEY_PROP_SSL_PRIVATEKEY].toLatin1()),
868 .dhParams = params()[KEY_PROP_SSL_DHPARAMS].toLatin1()
872 bool partialSuccess = false;
873 for (QString url : asConst(urls.split(u'\n')))
875 url = url.trimmed();
876 if (!url.isEmpty())
878 Net::DownloadManager::instance()->setCookiesFromUrl(cookies, QUrl::fromEncoded(url.toUtf8()));
879 partialSuccess |= app()->addTorrentManager()->addTorrent(url, addTorrentParams);
883 const DataMap &torrents = data();
884 for (auto it = torrents.constBegin(); it != torrents.constEnd(); ++it)
886 if (const auto loadResult = BitTorrent::TorrentDescriptor::load(it.value()))
888 partialSuccess |= BitTorrent::Session::instance()->addTorrent(loadResult.value(), addTorrentParams);
890 else
892 throw APIError(APIErrorType::BadData, tr("Error: '%1' is not a valid torrent file.").arg(it.key()));
896 if (partialSuccess)
897 setResult(u"Ok."_s);
898 else
899 setResult(u"Fails."_s);
902 void TorrentsController::addTrackersAction()
904 requireParams({u"hash"_s, u"urls"_s});
906 const auto id = BitTorrent::TorrentID::fromString(params()[u"hash"_s]);
907 BitTorrent::Torrent *const torrent = BitTorrent::Session::instance()->getTorrent(id);
908 if (!torrent)
909 throw APIError(APIErrorType::NotFound);
911 const QList<BitTorrent::TrackerEntry> entries = BitTorrent::parseTrackerEntries(params()[u"urls"_s]);
912 torrent->addTrackers(entries);
915 void TorrentsController::editTrackerAction()
917 requireParams({u"hash"_s, u"origUrl"_s, u"newUrl"_s});
919 const auto id = BitTorrent::TorrentID::fromString(params()[u"hash"_s]);
920 const QString origUrl = params()[u"origUrl"_s];
921 const QString newUrl = params()[u"newUrl"_s];
923 BitTorrent::Torrent *const torrent = BitTorrent::Session::instance()->getTorrent(id);
924 if (!torrent)
925 throw APIError(APIErrorType::NotFound);
927 const QUrl origTrackerUrl {origUrl};
928 const QUrl newTrackerUrl {newUrl};
929 if (origTrackerUrl == newTrackerUrl)
930 return;
931 if (!newTrackerUrl.isValid())
932 throw APIError(APIErrorType::BadParams, u"New tracker URL is invalid"_s);
934 const QList<BitTorrent::TrackerEntryStatus> currentTrackers = torrent->trackers();
935 QList<BitTorrent::TrackerEntry> entries;
936 entries.reserve(currentTrackers.size());
938 bool match = false;
939 for (const BitTorrent::TrackerEntryStatus &tracker : currentTrackers)
941 const QUrl trackerUrl {tracker.url};
943 if (trackerUrl == newTrackerUrl)
944 throw APIError(APIErrorType::Conflict, u"New tracker URL already exists"_s);
946 BitTorrent::TrackerEntry entry
948 .url = tracker.url,
949 .tier = tracker.tier
952 if (trackerUrl == origTrackerUrl)
954 match = true;
955 entry.url = newTrackerUrl.toString();
957 entries.append(entry);
959 if (!match)
960 throw APIError(APIErrorType::Conflict, u"Tracker not found"_s);
962 torrent->replaceTrackers(entries);
964 if (!torrent->isStopped())
965 torrent->forceReannounce();
968 void TorrentsController::removeTrackersAction()
970 requireParams({u"hash"_s, u"urls"_s});
972 const auto id = BitTorrent::TorrentID::fromString(params()[u"hash"_s]);
973 BitTorrent::Torrent *const torrent = BitTorrent::Session::instance()->getTorrent(id);
974 if (!torrent)
975 throw APIError(APIErrorType::NotFound);
977 const QStringList urls = params()[u"urls"_s].split(u'|');
978 torrent->removeTrackers(urls);
981 void TorrentsController::addPeersAction()
983 requireParams({u"hashes"_s, u"peers"_s});
985 const QStringList hashes = params()[u"hashes"_s].split(u'|');
986 const QStringList peers = params()[u"peers"_s].split(u'|');
988 QList<BitTorrent::PeerAddress> peerList;
989 peerList.reserve(peers.size());
990 for (const QString &peer : peers)
992 const BitTorrent::PeerAddress addr = BitTorrent::PeerAddress::parse(peer.trimmed());
993 if (!addr.ip.isNull())
994 peerList.append(addr);
997 if (peerList.isEmpty())
998 throw APIError(APIErrorType::BadParams, u"No valid peers were specified"_s);
1000 QJsonObject results;
1002 applyToTorrents(hashes, [peers, peerList, &results](BitTorrent::Torrent *const torrent)
1004 const int peersAdded = std::count_if(peerList.cbegin(), peerList.cend(), [torrent](const BitTorrent::PeerAddress &peer)
1006 return torrent->connectPeer(peer);
1009 results[torrent->id().toString()] = QJsonObject
1011 {u"added"_s, peersAdded},
1012 {u"failed"_s, (peers.size() - peersAdded)}
1016 setResult(results);
1019 void TorrentsController::stopAction()
1021 requireParams({u"hashes"_s});
1023 const QStringList hashes = params()[u"hashes"_s].split(u'|');
1024 applyToTorrents(hashes, [](BitTorrent::Torrent *const torrent) { torrent->stop(); });
1027 void TorrentsController::startAction()
1029 requireParams({u"hashes"_s});
1031 const QStringList idStrings = params()[u"hashes"_s].split(u'|');
1032 applyToTorrents(idStrings, [](BitTorrent::Torrent *const torrent) { torrent->start(); });
1035 void TorrentsController::filePrioAction()
1037 requireParams({u"hash"_s, u"id"_s, u"priority"_s});
1039 const auto id = BitTorrent::TorrentID::fromString(params()[u"hash"_s]);
1040 bool ok = false;
1041 const auto priority = static_cast<BitTorrent::DownloadPriority>(params()[u"priority"_s].toInt(&ok));
1042 if (!ok)
1043 throw APIError(APIErrorType::BadParams, tr("Priority must be an integer"));
1045 if (!BitTorrent::isValidDownloadPriority(priority))
1046 throw APIError(APIErrorType::BadParams, tr("Priority is not valid"));
1048 BitTorrent::Torrent *const torrent = BitTorrent::Session::instance()->getTorrent(id);
1049 if (!torrent)
1050 throw APIError(APIErrorType::NotFound);
1051 if (!torrent->hasMetadata())
1052 throw APIError(APIErrorType::Conflict, tr("Torrent's metadata has not yet downloaded"));
1054 const int filesCount = torrent->filesCount();
1055 QList<BitTorrent::DownloadPriority> priorities = torrent->filePriorities();
1056 bool priorityChanged = false;
1057 for (const QString &fileID : params()[u"id"_s].split(u'|'))
1059 const int id = fileID.toInt(&ok);
1060 if (!ok)
1061 throw APIError(APIErrorType::BadParams, tr("File IDs must be integers"));
1062 if ((id < 0) || (id >= filesCount))
1063 throw APIError(APIErrorType::Conflict, tr("File ID is not valid"));
1065 if (priorities[id] != priority)
1067 priorities[id] = priority;
1068 priorityChanged = true;
1072 if (priorityChanged)
1073 torrent->prioritizeFiles(priorities);
1076 void TorrentsController::uploadLimitAction()
1078 requireParams({u"hashes"_s});
1080 const QStringList idList {params()[u"hashes"_s].split(u'|')};
1081 QJsonObject map;
1082 for (const QString &id : idList)
1084 int limit = -1;
1085 const BitTorrent::Torrent *const torrent = BitTorrent::Session::instance()->getTorrent(BitTorrent::TorrentID::fromString(id));
1086 if (torrent)
1087 limit = torrent->uploadLimit();
1088 map[id] = limit;
1091 setResult(map);
1094 void TorrentsController::downloadLimitAction()
1096 requireParams({u"hashes"_s});
1098 const QStringList idList {params()[u"hashes"_s].split(u'|')};
1099 QJsonObject map;
1100 for (const QString &id : idList)
1102 int limit = -1;
1103 const BitTorrent::Torrent *const torrent = BitTorrent::Session::instance()->getTorrent(BitTorrent::TorrentID::fromString(id));
1104 if (torrent)
1105 limit = torrent->downloadLimit();
1106 map[id] = limit;
1109 setResult(map);
1112 void TorrentsController::setUploadLimitAction()
1114 requireParams({u"hashes"_s, u"limit"_s});
1116 qlonglong limit = params()[u"limit"_s].toLongLong();
1117 if (limit == 0)
1118 limit = -1;
1120 const QStringList hashes {params()[u"hashes"_s].split(u'|')};
1121 applyToTorrents(hashes, [limit](BitTorrent::Torrent *const torrent) { torrent->setUploadLimit(limit); });
1124 void TorrentsController::setDownloadLimitAction()
1126 requireParams({u"hashes"_s, u"limit"_s});
1128 qlonglong limit = params()[u"limit"_s].toLongLong();
1129 if (limit == 0)
1130 limit = -1;
1132 const QStringList hashes {params()[u"hashes"_s].split(u'|')};
1133 applyToTorrents(hashes, [limit](BitTorrent::Torrent *const torrent) { torrent->setDownloadLimit(limit); });
1136 void TorrentsController::setShareLimitsAction()
1138 requireParams({u"hashes"_s, u"ratioLimit"_s, u"seedingTimeLimit"_s, u"inactiveSeedingTimeLimit"_s});
1140 const qreal ratioLimit = params()[u"ratioLimit"_s].toDouble();
1141 const qlonglong seedingTimeLimit = params()[u"seedingTimeLimit"_s].toLongLong();
1142 const qlonglong inactiveSeedingTimeLimit = params()[u"inactiveSeedingTimeLimit"_s].toLongLong();
1143 const QStringList hashes = params()[u"hashes"_s].split(u'|');
1145 applyToTorrents(hashes, [ratioLimit, seedingTimeLimit, inactiveSeedingTimeLimit](BitTorrent::Torrent *const torrent)
1147 torrent->setRatioLimit(ratioLimit);
1148 torrent->setSeedingTimeLimit(seedingTimeLimit);
1149 torrent->setInactiveSeedingTimeLimit(inactiveSeedingTimeLimit);
1153 void TorrentsController::toggleSequentialDownloadAction()
1155 requireParams({u"hashes"_s});
1157 const QStringList hashes {params()[u"hashes"_s].split(u'|')};
1158 applyToTorrents(hashes, [](BitTorrent::Torrent *const torrent) { torrent->toggleSequentialDownload(); });
1161 void TorrentsController::toggleFirstLastPiecePrioAction()
1163 requireParams({u"hashes"_s});
1165 const QStringList hashes {params()[u"hashes"_s].split(u'|')};
1166 applyToTorrents(hashes, [](BitTorrent::Torrent *const torrent) { torrent->toggleFirstLastPiecePriority(); });
1169 void TorrentsController::setSuperSeedingAction()
1171 requireParams({u"hashes"_s, u"value"_s});
1173 const bool value {parseBool(params()[u"value"_s]).value_or(false)};
1174 const QStringList hashes {params()[u"hashes"_s].split(u'|')};
1175 applyToTorrents(hashes, [value](BitTorrent::Torrent *const torrent) { torrent->setSuperSeeding(value); });
1178 void TorrentsController::setForceStartAction()
1180 requireParams({u"hashes"_s, u"value"_s});
1182 const bool value {parseBool(params()[u"value"_s]).value_or(false)};
1183 const QStringList hashes {params()[u"hashes"_s].split(u'|')};
1184 applyToTorrents(hashes, [value](BitTorrent::Torrent *const torrent)
1186 torrent->start(value ? BitTorrent::TorrentOperatingMode::Forced : BitTorrent::TorrentOperatingMode::AutoManaged);
1190 void TorrentsController::deleteAction()
1192 requireParams({u"hashes"_s, u"deleteFiles"_s});
1194 const QStringList hashes {params()[u"hashes"_s].split(u'|')};
1195 const BitTorrent::TorrentRemoveOption deleteOption = parseBool(params()[u"deleteFiles"_s]).value_or(false)
1196 ? BitTorrent::TorrentRemoveOption::RemoveContent : BitTorrent::TorrentRemoveOption::KeepContent;
1197 applyToTorrents(hashes, [deleteOption](const BitTorrent::Torrent *torrent)
1199 BitTorrent::Session::instance()->removeTorrent(torrent->id(), deleteOption);
1203 void TorrentsController::increasePrioAction()
1205 requireParams({u"hashes"_s});
1207 if (!BitTorrent::Session::instance()->isQueueingSystemEnabled())
1208 throw APIError(APIErrorType::Conflict, tr("Torrent queueing must be enabled"));
1210 const QStringList hashes {params()[u"hashes"_s].split(u'|')};
1211 BitTorrent::Session::instance()->increaseTorrentsQueuePos(toTorrentIDs(hashes));
1214 void TorrentsController::decreasePrioAction()
1216 requireParams({u"hashes"_s});
1218 if (!BitTorrent::Session::instance()->isQueueingSystemEnabled())
1219 throw APIError(APIErrorType::Conflict, tr("Torrent queueing must be enabled"));
1221 const QStringList hashes {params()[u"hashes"_s].split(u'|')};
1222 BitTorrent::Session::instance()->decreaseTorrentsQueuePos(toTorrentIDs(hashes));
1225 void TorrentsController::topPrioAction()
1227 requireParams({u"hashes"_s});
1229 if (!BitTorrent::Session::instance()->isQueueingSystemEnabled())
1230 throw APIError(APIErrorType::Conflict, tr("Torrent queueing must be enabled"));
1232 const QStringList hashes {params()[u"hashes"_s].split(u'|')};
1233 BitTorrent::Session::instance()->topTorrentsQueuePos(toTorrentIDs(hashes));
1236 void TorrentsController::bottomPrioAction()
1238 requireParams({u"hashes"_s});
1240 if (!BitTorrent::Session::instance()->isQueueingSystemEnabled())
1241 throw APIError(APIErrorType::Conflict, tr("Torrent queueing must be enabled"));
1243 const QStringList hashes {params()[u"hashes"_s].split(u'|')};
1244 BitTorrent::Session::instance()->bottomTorrentsQueuePos(toTorrentIDs(hashes));
1247 void TorrentsController::setLocationAction()
1249 requireParams({u"hashes"_s, u"location"_s});
1251 const QStringList hashes {params()[u"hashes"_s].split(u'|')};
1252 const Path newLocation {params()[u"location"_s].trimmed()};
1254 if (newLocation.isEmpty())
1255 throw APIError(APIErrorType::BadParams, tr("Save path cannot be empty"));
1257 // try to create the location if it does not exist
1258 if (!Utils::Fs::mkpath(newLocation))
1259 throw APIError(APIErrorType::Conflict, tr("Cannot make save path"));
1261 applyToTorrents(hashes, [newLocation](BitTorrent::Torrent *const torrent)
1263 LogMsg(tr("WebUI Set location: moving \"%1\", from \"%2\" to \"%3\"")
1264 .arg(torrent->name(), torrent->savePath().toString(), newLocation.toString()));
1265 torrent->setAutoTMMEnabled(false);
1266 torrent->setSavePath(newLocation);
1270 void TorrentsController::setSavePathAction()
1272 requireParams({u"id"_s, u"path"_s});
1274 const QStringList ids {params()[u"id"_s].split(u'|')};
1275 const Path newPath {params()[u"path"_s]};
1277 if (newPath.isEmpty())
1278 throw APIError(APIErrorType::BadParams, tr("Save path cannot be empty"));
1280 // try to create the directory if it does not exist
1281 if (!Utils::Fs::mkpath(newPath))
1282 throw APIError(APIErrorType::Conflict, tr("Cannot create target directory"));
1284 // check permissions
1285 if (!Utils::Fs::isWritable(newPath))
1286 throw APIError(APIErrorType::AccessDenied, tr("Cannot write to directory"));
1288 applyToTorrents(ids, [&newPath](BitTorrent::Torrent *const torrent)
1290 if (!torrent->isAutoTMMEnabled())
1291 torrent->setSavePath(newPath);
1295 void TorrentsController::setDownloadPathAction()
1297 requireParams({u"id"_s, u"path"_s});
1299 const QStringList ids {params()[u"id"_s].split(u'|')};
1300 const Path newPath {params()[u"path"_s]};
1302 if (!newPath.isEmpty())
1304 // try to create the directory if it does not exist
1305 if (!Utils::Fs::mkpath(newPath))
1306 throw APIError(APIErrorType::Conflict, tr("Cannot create target directory"));
1308 // check permissions
1309 if (!Utils::Fs::isWritable(newPath))
1310 throw APIError(APIErrorType::AccessDenied, tr("Cannot write to directory"));
1313 applyToTorrents(ids, [&newPath](BitTorrent::Torrent *const torrent)
1315 if (!torrent->isAutoTMMEnabled())
1316 torrent->setDownloadPath(newPath);
1320 void TorrentsController::renameAction()
1322 requireParams({u"hash"_s, u"name"_s});
1324 const auto id = BitTorrent::TorrentID::fromString(params()[u"hash"_s]);
1325 QString name = params()[u"name"_s].trimmed();
1327 if (name.isEmpty())
1328 throw APIError(APIErrorType::Conflict, tr("Incorrect torrent name"));
1330 BitTorrent::Torrent *const torrent = BitTorrent::Session::instance()->getTorrent(id);
1331 if (!torrent)
1332 throw APIError(APIErrorType::NotFound);
1334 name.replace(QRegularExpression(u"\r?\n|\r"_s), u" "_s);
1335 torrent->setName(name);
1338 void TorrentsController::setAutoManagementAction()
1340 requireParams({u"hashes"_s, u"enable"_s});
1342 const QStringList hashes {params()[u"hashes"_s].split(u'|')};
1343 const bool isEnabled {parseBool(params()[u"enable"_s]).value_or(false)};
1345 applyToTorrents(hashes, [isEnabled](BitTorrent::Torrent *const torrent)
1347 torrent->setAutoTMMEnabled(isEnabled);
1351 void TorrentsController::recheckAction()
1353 requireParams({u"hashes"_s});
1355 const QStringList hashes {params()[u"hashes"_s].split(u'|')};
1356 applyToTorrents(hashes, [](BitTorrent::Torrent *const torrent) { torrent->forceRecheck(); });
1359 void TorrentsController::reannounceAction()
1361 requireParams({u"hashes"_s});
1363 const QStringList hashes {params()[u"hashes"_s].split(u'|')};
1364 applyToTorrents(hashes, [](BitTorrent::Torrent *const torrent) { torrent->forceReannounce(); });
1367 void TorrentsController::setCategoryAction()
1369 requireParams({u"hashes"_s, u"category"_s});
1371 const QStringList hashes {params()[u"hashes"_s].split(u'|')};
1372 const QString category {params()[u"category"_s]};
1374 applyToTorrents(hashes, [category](BitTorrent::Torrent *const torrent)
1376 if (!torrent->setCategory(category))
1377 throw APIError(APIErrorType::Conflict, tr("Incorrect category name"));
1381 void TorrentsController::createCategoryAction()
1383 requireParams({u"category"_s});
1385 const QString category = params()[u"category"_s];
1386 if (category.isEmpty())
1387 throw APIError(APIErrorType::BadParams, tr("Category cannot be empty"));
1389 if (!BitTorrent::Session::isValidCategoryName(category))
1390 throw APIError(APIErrorType::Conflict, tr("Incorrect category name"));
1392 const Path savePath {params()[u"savePath"_s]};
1393 const auto useDownloadPath = parseBool(params()[u"downloadPathEnabled"_s]);
1394 BitTorrent::CategoryOptions categoryOptions;
1395 categoryOptions.savePath = savePath;
1396 if (useDownloadPath.has_value())
1398 const Path downloadPath {params()[u"downloadPath"_s]};
1399 categoryOptions.downloadPath = {useDownloadPath.value(), downloadPath};
1402 if (!BitTorrent::Session::instance()->addCategory(category, categoryOptions))
1403 throw APIError(APIErrorType::Conflict, tr("Unable to create category"));
1406 void TorrentsController::editCategoryAction()
1408 requireParams({u"category"_s, u"savePath"_s});
1410 const QString category = params()[u"category"_s];
1411 if (category.isEmpty())
1412 throw APIError(APIErrorType::BadParams, tr("Category cannot be empty"));
1414 const Path savePath {params()[u"savePath"_s]};
1415 const auto useDownloadPath = parseBool(params()[u"downloadPathEnabled"_s]);
1416 BitTorrent::CategoryOptions categoryOptions;
1417 categoryOptions.savePath = savePath;
1418 if (useDownloadPath.has_value())
1420 const Path downloadPath {params()[u"downloadPath"_s]};
1421 categoryOptions.downloadPath = {useDownloadPath.value(), downloadPath};
1424 if (!BitTorrent::Session::instance()->editCategory(category, categoryOptions))
1425 throw APIError(APIErrorType::Conflict, tr("Unable to edit category"));
1428 void TorrentsController::removeCategoriesAction()
1430 requireParams({u"categories"_s});
1432 const QStringList categories {params()[u"categories"_s].split(u'\n')};
1433 for (const QString &category : categories)
1434 BitTorrent::Session::instance()->removeCategory(category);
1437 void TorrentsController::categoriesAction()
1439 const auto *session = BitTorrent::Session::instance();
1441 QJsonObject categories;
1442 const QStringList categoriesList = session->categories();
1443 for (const auto &categoryName : categoriesList)
1445 const BitTorrent::CategoryOptions categoryOptions = session->categoryOptions(categoryName);
1446 QJsonObject category = categoryOptions.toJSON();
1447 // adjust it to be compatible with existing WebAPI
1448 category[u"savePath"_s] = category.take(u"save_path"_s);
1449 category.insert(u"name"_s, categoryName);
1450 categories[categoryName] = category;
1453 setResult(categories);
1456 void TorrentsController::addTagsAction()
1458 requireParams({u"hashes"_s, u"tags"_s});
1460 const QStringList hashes {params()[u"hashes"_s].split(u'|')};
1461 const QStringList tags {params()[u"tags"_s].split(u',', Qt::SkipEmptyParts)};
1463 for (const QString &tagStr : tags)
1465 applyToTorrents(hashes, [&tagStr](BitTorrent::Torrent *const torrent)
1467 torrent->addTag(Tag(tagStr));
1472 void TorrentsController::removeTagsAction()
1474 requireParams({u"hashes"_s});
1476 const QStringList hashes {params()[u"hashes"_s].split(u'|')};
1477 const QStringList tags {params()[u"tags"_s].split(u',', Qt::SkipEmptyParts)};
1479 for (const QString &tagStr : tags)
1481 applyToTorrents(hashes, [&tagStr](BitTorrent::Torrent *const torrent)
1483 torrent->removeTag(Tag(tagStr));
1487 if (tags.isEmpty())
1489 applyToTorrents(hashes, [](BitTorrent::Torrent *const torrent)
1491 torrent->removeAllTags();
1496 void TorrentsController::createTagsAction()
1498 requireParams({u"tags"_s});
1500 const QStringList tags {params()[u"tags"_s].split(u',', Qt::SkipEmptyParts)};
1502 for (const QString &tagStr : tags)
1503 BitTorrent::Session::instance()->addTag(Tag(tagStr));
1506 void TorrentsController::deleteTagsAction()
1508 requireParams({u"tags"_s});
1510 const QStringList tags {params()[u"tags"_s].split(u',', Qt::SkipEmptyParts)};
1511 for (const QString &tagStr : tags)
1512 BitTorrent::Session::instance()->removeTag(Tag(tagStr));
1515 void TorrentsController::tagsAction()
1517 QJsonArray result;
1518 for (const Tag &tag : asConst(BitTorrent::Session::instance()->tags()))
1519 result << tag.toString();
1520 setResult(result);
1523 void TorrentsController::renameFileAction()
1525 requireParams({u"hash"_s, u"oldPath"_s, u"newPath"_s});
1527 const auto id = BitTorrent::TorrentID::fromString(params()[u"hash"_s]);
1528 BitTorrent::Torrent *const torrent = BitTorrent::Session::instance()->getTorrent(id);
1529 if (!torrent)
1530 throw APIError(APIErrorType::NotFound);
1532 const Path oldPath {params()[u"oldPath"_s]};
1533 const Path newPath {params()[u"newPath"_s]};
1537 torrent->renameFile(oldPath, newPath);
1539 catch (const RuntimeError &error)
1541 throw APIError(APIErrorType::Conflict, error.message());
1545 void TorrentsController::renameFolderAction()
1547 requireParams({u"hash"_s, u"oldPath"_s, u"newPath"_s});
1549 const auto id = BitTorrent::TorrentID::fromString(params()[u"hash"_s]);
1550 BitTorrent::Torrent *const torrent = BitTorrent::Session::instance()->getTorrent(id);
1551 if (!torrent)
1552 throw APIError(APIErrorType::NotFound);
1554 const Path oldPath {params()[u"oldPath"_s]};
1555 const Path newPath {params()[u"newPath"_s]};
1559 torrent->renameFolder(oldPath, newPath);
1561 catch (const RuntimeError &error)
1563 throw APIError(APIErrorType::Conflict, error.message());
1567 void TorrentsController::exportAction()
1569 requireParams({u"hash"_s});
1571 const auto id = BitTorrent::TorrentID::fromString(params()[u"hash"_s]);
1572 const BitTorrent::Torrent *const torrent = BitTorrent::Session::instance()->getTorrent(id);
1573 if (!torrent)
1574 throw APIError(APIErrorType::NotFound);
1576 const nonstd::expected<QByteArray, QString> result = torrent->exportToBuffer();
1577 if (!result)
1578 throw APIError(APIErrorType::Conflict, tr("Unable to export torrent file. Error: %1").arg(result.error()));
1580 setResult(result.value(), u"application/x-bittorrent"_s, (id.toString() + u".torrent"));
1583 void TorrentsController::SSLParametersAction()
1585 requireParams({u"hash"_s});
1587 const auto id = BitTorrent::TorrentID::fromString(params()[u"hash"_s]);
1588 const BitTorrent::Torrent *torrent = BitTorrent::Session::instance()->getTorrent(id);
1589 if (!torrent)
1590 throw APIError(APIErrorType::NotFound);
1592 const BitTorrent::SSLParameters sslParams = torrent->getSSLParameters();
1593 const QJsonObject ret
1595 {KEY_PROP_SSL_CERTIFICATE, QString::fromLatin1(sslParams.certificate.toPem())},
1596 {KEY_PROP_SSL_PRIVATEKEY, QString::fromLatin1(sslParams.privateKey.toPem())},
1597 {KEY_PROP_SSL_DHPARAMS, QString::fromLatin1(sslParams.dhParams)}
1599 setResult(ret);
1602 void TorrentsController::setSSLParametersAction()
1604 requireParams({u"hash"_s, KEY_PROP_SSL_CERTIFICATE, KEY_PROP_SSL_PRIVATEKEY, KEY_PROP_SSL_DHPARAMS});
1606 const auto id = BitTorrent::TorrentID::fromString(params()[u"hash"_s]);
1607 BitTorrent::Torrent *const torrent = BitTorrent::Session::instance()->getTorrent(id);
1608 if (!torrent)
1609 throw APIError(APIErrorType::NotFound);
1611 const BitTorrent::SSLParameters sslParams
1613 .certificate = QSslCertificate(params()[KEY_PROP_SSL_CERTIFICATE].toLatin1()),
1614 .privateKey = Utils::SSLKey::load(params()[KEY_PROP_SSL_PRIVATEKEY].toLatin1()),
1615 .dhParams = params()[KEY_PROP_SSL_DHPARAMS].toLatin1()
1617 if (!sslParams.isValid())
1618 throw APIError(APIErrorType::BadData);
1620 torrent->setSSLParameters(sslParams);