Provide safe helper for converting to 'seconds since epoch'
[qBittorrent.git] / src / webui / api / torrentscontroller.cpp
blob25618da55ae41faabcaef5deccf2e6ee461edd83
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/torrent.h"
49 #include "base/bittorrent/torrentdescriptor.h"
50 #include "base/bittorrent/trackerentry.h"
51 #include "base/interfaces/iapplication.h"
52 #include "base/global.h"
53 #include "base/logger.h"
54 #include "base/net/downloadmanager.h"
55 #include "base/torrentfilter.h"
56 #include "base/utils/datetime.h"
57 #include "base/utils/fs.h"
58 #include "base/utils/string.h"
59 #include "apierror.h"
60 #include "serialize/serialize_torrent.h"
62 // Tracker keys
63 const QString KEY_TRACKER_URL = u"url"_s;
64 const QString KEY_TRACKER_STATUS = u"status"_s;
65 const QString KEY_TRACKER_TIER = u"tier"_s;
66 const QString KEY_TRACKER_MSG = u"msg"_s;
67 const QString KEY_TRACKER_PEERS_COUNT = u"num_peers"_s;
68 const QString KEY_TRACKER_SEEDS_COUNT = u"num_seeds"_s;
69 const QString KEY_TRACKER_LEECHES_COUNT = u"num_leeches"_s;
70 const QString KEY_TRACKER_DOWNLOADED_COUNT = u"num_downloaded"_s;
72 // Web seed keys
73 const QString KEY_WEBSEED_URL = u"url"_s;
75 // Torrent keys (Properties)
76 const QString KEY_PROP_TIME_ELAPSED = u"time_elapsed"_s;
77 const QString KEY_PROP_SEEDING_TIME = u"seeding_time"_s;
78 const QString KEY_PROP_ETA = u"eta"_s;
79 const QString KEY_PROP_CONNECT_COUNT = u"nb_connections"_s;
80 const QString KEY_PROP_CONNECT_COUNT_LIMIT = u"nb_connections_limit"_s;
81 const QString KEY_PROP_DOWNLOADED = u"total_downloaded"_s;
82 const QString KEY_PROP_DOWNLOADED_SESSION = u"total_downloaded_session"_s;
83 const QString KEY_PROP_UPLOADED = u"total_uploaded"_s;
84 const QString KEY_PROP_UPLOADED_SESSION = u"total_uploaded_session"_s;
85 const QString KEY_PROP_DL_SPEED = u"dl_speed"_s;
86 const QString KEY_PROP_DL_SPEED_AVG = u"dl_speed_avg"_s;
87 const QString KEY_PROP_UP_SPEED = u"up_speed"_s;
88 const QString KEY_PROP_UP_SPEED_AVG = u"up_speed_avg"_s;
89 const QString KEY_PROP_DL_LIMIT = u"dl_limit"_s;
90 const QString KEY_PROP_UP_LIMIT = u"up_limit"_s;
91 const QString KEY_PROP_WASTED = u"total_wasted"_s;
92 const QString KEY_PROP_SEEDS = u"seeds"_s;
93 const QString KEY_PROP_SEEDS_TOTAL = u"seeds_total"_s;
94 const QString KEY_PROP_PEERS = u"peers"_s;
95 const QString KEY_PROP_PEERS_TOTAL = u"peers_total"_s;
96 const QString KEY_PROP_RATIO = u"share_ratio"_s;
97 const QString KEY_PROP_REANNOUNCE = u"reannounce"_s;
98 const QString KEY_PROP_TOTAL_SIZE = u"total_size"_s;
99 const QString KEY_PROP_PIECES_NUM = u"pieces_num"_s;
100 const QString KEY_PROP_PIECE_SIZE = u"piece_size"_s;
101 const QString KEY_PROP_PIECES_HAVE = u"pieces_have"_s;
102 const QString KEY_PROP_CREATED_BY = u"created_by"_s;
103 const QString KEY_PROP_LAST_SEEN = u"last_seen"_s;
104 const QString KEY_PROP_ADDITION_DATE = u"addition_date"_s;
105 const QString KEY_PROP_COMPLETION_DATE = u"completion_date"_s;
106 const QString KEY_PROP_CREATION_DATE = u"creation_date"_s;
107 const QString KEY_PROP_SAVE_PATH = u"save_path"_s;
108 const QString KEY_PROP_DOWNLOAD_PATH = u"download_path"_s;
109 const QString KEY_PROP_COMMENT = u"comment"_s;
110 const QString KEY_PROP_ISPRIVATE = u"is_private"_s;
112 // File keys
113 const QString KEY_FILE_INDEX = u"index"_s;
114 const QString KEY_FILE_NAME = u"name"_s;
115 const QString KEY_FILE_SIZE = u"size"_s;
116 const QString KEY_FILE_PROGRESS = u"progress"_s;
117 const QString KEY_FILE_PRIORITY = u"priority"_s;
118 const QString KEY_FILE_IS_SEED = u"is_seed"_s;
119 const QString KEY_FILE_PIECE_RANGE = u"piece_range"_s;
120 const QString KEY_FILE_AVAILABILITY = u"availability"_s;
122 namespace
124 using Utils::String::parseBool;
125 using Utils::String::parseInt;
126 using Utils::String::parseDouble;
128 void applyToTorrents(const QStringList &idList, const std::function<void (BitTorrent::Torrent *torrent)> &func)
130 if ((idList.size() == 1) && (idList[0] == u"all"))
132 for (BitTorrent::Torrent *const torrent : asConst(BitTorrent::Session::instance()->torrents()))
133 func(torrent);
135 else
137 for (const QString &idString : idList)
139 const auto hash = BitTorrent::TorrentID::fromString(idString);
140 BitTorrent::Torrent *const torrent = BitTorrent::Session::instance()->getTorrent(hash);
141 if (torrent)
142 func(torrent);
147 std::optional<QString> getOptionalString(const StringMap &params, const QString &name)
149 const auto it = params.constFind(name);
150 if (it == params.cend())
151 return std::nullopt;
153 return it.value();
156 std::optional<Tag> getOptionalTag(const StringMap &params, const QString &name)
158 const auto it = params.constFind(name);
159 if (it == params.cend())
160 return std::nullopt;
162 return Tag(it.value());
165 QJsonArray getStickyTrackers(const BitTorrent::Torrent *const torrent)
167 int seedsDHT = 0, seedsPeX = 0, seedsLSD = 0, leechesDHT = 0, leechesPeX = 0, leechesLSD = 0;
168 for (const BitTorrent::PeerInfo &peer : asConst(torrent->peers()))
170 if (peer.isConnecting()) continue;
172 if (peer.isSeed())
174 if (peer.fromDHT())
175 ++seedsDHT;
176 if (peer.fromPeX())
177 ++seedsPeX;
178 if (peer.fromLSD())
179 ++seedsLSD;
181 else
183 if (peer.fromDHT())
184 ++leechesDHT;
185 if (peer.fromPeX())
186 ++leechesPeX;
187 if (peer.fromLSD())
188 ++leechesLSD;
192 const int working = static_cast<int>(BitTorrent::TrackerEntryStatus::Working);
193 const int disabled = 0;
195 const QString privateMsg {QCoreApplication::translate("TrackerListWidget", "This torrent is private")};
196 const bool isTorrentPrivate = torrent->isPrivate();
198 const QJsonObject dht
200 {KEY_TRACKER_URL, u"** [DHT] **"_s},
201 {KEY_TRACKER_TIER, -1},
202 {KEY_TRACKER_MSG, (isTorrentPrivate ? privateMsg : u""_s)},
203 {KEY_TRACKER_STATUS, ((BitTorrent::Session::instance()->isDHTEnabled() && !isTorrentPrivate) ? working : disabled)},
204 {KEY_TRACKER_PEERS_COUNT, 0},
205 {KEY_TRACKER_DOWNLOADED_COUNT, 0},
206 {KEY_TRACKER_SEEDS_COUNT, seedsDHT},
207 {KEY_TRACKER_LEECHES_COUNT, leechesDHT}
210 const QJsonObject pex
212 {KEY_TRACKER_URL, u"** [PeX] **"_s},
213 {KEY_TRACKER_TIER, -1},
214 {KEY_TRACKER_MSG, (isTorrentPrivate ? privateMsg : u""_s)},
215 {KEY_TRACKER_STATUS, ((BitTorrent::Session::instance()->isPeXEnabled() && !isTorrentPrivate) ? working : disabled)},
216 {KEY_TRACKER_PEERS_COUNT, 0},
217 {KEY_TRACKER_DOWNLOADED_COUNT, 0},
218 {KEY_TRACKER_SEEDS_COUNT, seedsPeX},
219 {KEY_TRACKER_LEECHES_COUNT, leechesPeX}
222 const QJsonObject lsd
224 {KEY_TRACKER_URL, u"** [LSD] **"_s},
225 {KEY_TRACKER_TIER, -1},
226 {KEY_TRACKER_MSG, (isTorrentPrivate ? privateMsg : u""_s)},
227 {KEY_TRACKER_STATUS, ((BitTorrent::Session::instance()->isLSDEnabled() && !isTorrentPrivate) ? working : disabled)},
228 {KEY_TRACKER_PEERS_COUNT, 0},
229 {KEY_TRACKER_DOWNLOADED_COUNT, 0},
230 {KEY_TRACKER_SEEDS_COUNT, seedsLSD},
231 {KEY_TRACKER_LEECHES_COUNT, leechesLSD}
234 return {dht, pex, lsd};
237 QVector<BitTorrent::TorrentID> toTorrentIDs(const QStringList &idStrings)
239 QVector<BitTorrent::TorrentID> idList;
240 idList.reserve(idStrings.size());
241 for (const QString &hash : idStrings)
242 idList << BitTorrent::TorrentID::fromString(hash);
243 return idList;
247 void TorrentsController::countAction()
249 setResult(QString::number(BitTorrent::Session::instance()->torrentsCount()));
252 // Returns all the torrents in JSON format.
253 // The return value is a JSON-formatted list of dictionaries.
254 // The dictionary keys are:
255 // - "hash": Torrent hash (ID)
256 // - "name": Torrent name
257 // - "size": Torrent size
258 // - "progress": Torrent progress
259 // - "dlspeed": Torrent download speed
260 // - "upspeed": Torrent upload speed
261 // - "priority": Torrent queue position (-1 if queuing is disabled)
262 // - "num_seeds": Torrent seeds connected to
263 // - "num_complete": Torrent seeds in the swarm
264 // - "num_leechs": Torrent leechers connected to
265 // - "num_incomplete": Torrent leechers in the swarm
266 // - "ratio": Torrent share ratio
267 // - "eta": Torrent ETA
268 // - "state": Torrent state
269 // - "seq_dl": Torrent sequential download state
270 // - "f_l_piece_prio": Torrent first last piece priority state
271 // - "force_start": Torrent force start state
272 // - "category": Torrent category
273 // GET params:
274 // - filter (string): all, downloading, seeding, completed, paused, resumed, active, inactive, stalled, stalled_uploading, stalled_downloading
275 // - category (string): torrent category for filtering by it (empty string means "uncategorized"; no "category" param presented means "any category")
276 // - tag (string): torrent tag for filtering by it (empty string means "untagged"; no "tag" param presented means "any tag")
277 // - hashes (string): filter by hashes, can contain multiple hashes separated by |
278 // - sort (string): name of column for sorting by its value
279 // - reverse (bool): enable reverse sorting
280 // - limit (int): set limit number of torrents returned (if greater than 0, otherwise - unlimited)
281 // - offset (int): set offset (if less than 0 - offset from end)
282 void TorrentsController::infoAction()
284 const QString filter {params()[u"filter"_s]};
285 const std::optional<QString> category = getOptionalString(params(), u"category"_s);
286 const std::optional<Tag> tag = getOptionalTag(params(), u"tag"_s);
287 const QString sortedColumn {params()[u"sort"_s]};
288 const bool reverse {parseBool(params()[u"reverse"_s]).value_or(false)};
289 int limit {params()[u"limit"_s].toInt()};
290 int offset {params()[u"offset"_s].toInt()};
291 const QStringList hashes {params()[u"hashes"_s].split(u'|', Qt::SkipEmptyParts)};
293 std::optional<TorrentIDSet> idSet;
294 if (!hashes.isEmpty())
296 idSet = TorrentIDSet();
297 for (const QString &hash : hashes)
298 idSet->insert(BitTorrent::TorrentID::fromString(hash));
301 const TorrentFilter torrentFilter {filter, idSet, category, tag};
302 QVariantList torrentList;
303 for (const BitTorrent::Torrent *torrent : asConst(BitTorrent::Session::instance()->torrents()))
305 if (torrentFilter.match(torrent))
306 torrentList.append(serialize(*torrent));
309 if (torrentList.isEmpty())
311 setResult(QJsonArray {});
312 return;
315 if (!sortedColumn.isEmpty())
317 if (!torrentList[0].toMap().contains(sortedColumn))
318 throw APIError(APIErrorType::BadParams, tr("'sort' parameter is invalid"));
320 const auto lessThan = [](const QVariant &left, const QVariant &right) -> bool
322 Q_ASSERT(left.userType() == right.userType());
324 switch (left.userType())
326 case QMetaType::Bool:
327 return left.value<bool>() < right.value<bool>();
328 case QMetaType::Double:
329 return left.value<double>() < right.value<double>();
330 case QMetaType::Float:
331 return left.value<float>() < right.value<float>();
332 case QMetaType::Int:
333 return left.value<int>() < right.value<int>();
334 case QMetaType::LongLong:
335 return left.value<qlonglong>() < right.value<qlonglong>();
336 case QMetaType::QString:
337 return left.value<QString>() < right.value<QString>();
338 default:
339 qWarning("Unhandled QVariant comparison, type: %d, name: %s"
340 , left.userType(), left.metaType().name());
341 break;
343 return false;
346 std::sort(torrentList.begin(), torrentList.end()
347 , [reverse, &sortedColumn, &lessThan](const QVariant &torrent1, const QVariant &torrent2)
349 const QVariant value1 {torrent1.toMap().value(sortedColumn)};
350 const QVariant value2 {torrent2.toMap().value(sortedColumn)};
351 return reverse ? lessThan(value2, value1) : lessThan(value1, value2);
355 const int size = torrentList.size();
356 // normalize offset
357 if (offset < 0)
358 offset = size + offset;
359 if ((offset >= size) || (offset < 0))
360 offset = 0;
361 // normalize limit
362 if (limit <= 0)
363 limit = -1; // unlimited
365 if ((limit > 0) || (offset > 0))
366 torrentList = torrentList.mid(offset, limit);
368 setResult(QJsonArray::fromVariantList(torrentList));
371 // Returns the properties for a torrent in JSON format.
372 // The return value is a JSON-formatted dictionary.
373 // The dictionary keys are:
374 // - "time_elapsed": Torrent elapsed time
375 // - "seeding_time": Torrent elapsed time while complete
376 // - "eta": Torrent ETA
377 // - "nb_connections": Torrent connection count
378 // - "nb_connections_limit": Torrent connection count limit
379 // - "total_downloaded": Total data uploaded for torrent
380 // - "total_downloaded_session": Total data downloaded this session
381 // - "total_uploaded": Total data uploaded for torrent
382 // - "total_uploaded_session": Total data uploaded this session
383 // - "dl_speed": Torrent download speed
384 // - "dl_speed_avg": Torrent average download speed
385 // - "up_speed": Torrent upload speed
386 // - "up_speed_avg": Torrent average upload speed
387 // - "dl_limit": Torrent download limit
388 // - "up_limit": Torrent upload limit
389 // - "total_wasted": Total data wasted for torrent
390 // - "seeds": Torrent connected seeds
391 // - "seeds_total": Torrent total number of seeds
392 // - "peers": Torrent connected peers
393 // - "peers_total": Torrent total number of peers
394 // - "share_ratio": Torrent share ratio
395 // - "reannounce": Torrent next reannounce time
396 // - "total_size": Torrent total size
397 // - "pieces_num": Torrent pieces count
398 // - "piece_size": Torrent piece size
399 // - "pieces_have": Torrent pieces have
400 // - "created_by": Torrent creator
401 // - "last_seen": Torrent last seen complete
402 // - "addition_date": Torrent addition date
403 // - "completion_date": Torrent completion date
404 // - "creation_date": Torrent creation date
405 // - "save_path": Torrent save path
406 // - "download_path": Torrent download path
407 // - "comment": Torrent comment
408 // - "infohash_v1": Torrent v1 infohash (or empty string for v2 torrents)
409 // - "infohash_v2": Torrent v2 infohash (or empty string for v1 torrents)
410 // - "hash": Torrent TorrentID (infohashv1 for v1 torrents, truncated infohashv2 for v2/hybrid torrents)
411 // - "name": Torrent name
412 void TorrentsController::propertiesAction()
414 requireParams({u"hash"_s});
416 const auto id = BitTorrent::TorrentID::fromString(params()[u"hash"_s]);
417 const BitTorrent::Torrent *const torrent = BitTorrent::Session::instance()->getTorrent(id);
418 if (!torrent)
419 throw APIError(APIErrorType::NotFound);
421 const BitTorrent::InfoHash infoHash = torrent->infoHash();
422 const qlonglong totalDownload = torrent->totalDownload();
423 const qlonglong totalUpload = torrent->totalUpload();
424 const qlonglong dlDuration = torrent->activeTime() - torrent->finishedTime();
425 const qlonglong ulDuration = torrent->activeTime();
426 const int downloadLimit = torrent->downloadLimit();
427 const int uploadLimit = torrent->uploadLimit();
428 const qreal ratio = torrent->realRatio();
430 const QJsonObject ret
432 {KEY_TORRENT_INFOHASHV1, infoHash.v1().toString()},
433 {KEY_TORRENT_INFOHASHV2, infoHash.v2().toString()},
434 {KEY_TORRENT_NAME, torrent->name()},
435 {KEY_TORRENT_ID, torrent->id().toString()},
436 {KEY_PROP_TIME_ELAPSED, torrent->activeTime()},
437 {KEY_PROP_SEEDING_TIME, torrent->finishedTime()},
438 {KEY_PROP_ETA, torrent->eta()},
439 {KEY_PROP_CONNECT_COUNT, torrent->connectionsCount()},
440 {KEY_PROP_CONNECT_COUNT_LIMIT, torrent->connectionsLimit()},
441 {KEY_PROP_DOWNLOADED, totalDownload},
442 {KEY_PROP_DOWNLOADED_SESSION, torrent->totalPayloadDownload()},
443 {KEY_PROP_UPLOADED, totalUpload},
444 {KEY_PROP_UPLOADED_SESSION, torrent->totalPayloadUpload()},
445 {KEY_PROP_DL_SPEED, torrent->downloadPayloadRate()},
446 {KEY_PROP_DL_SPEED_AVG, ((dlDuration > 0) ? (totalDownload / dlDuration) : -1)},
447 {KEY_PROP_UP_SPEED, torrent->uploadPayloadRate()},
448 {KEY_PROP_UP_SPEED_AVG, ((ulDuration > 0) ? (totalUpload / ulDuration) : -1)},
449 {KEY_PROP_DL_LIMIT, ((downloadLimit > 0) ? downloadLimit : -1)},
450 {KEY_PROP_UP_LIMIT, ((uploadLimit > 0) ? uploadLimit : -1)},
451 {KEY_PROP_WASTED, torrent->wastedSize()},
452 {KEY_PROP_SEEDS, torrent->seedsCount()},
453 {KEY_PROP_SEEDS_TOTAL, torrent->totalSeedsCount()},
454 {KEY_PROP_PEERS, torrent->leechsCount()},
455 {KEY_PROP_PEERS_TOTAL, torrent->totalLeechersCount()},
456 {KEY_PROP_RATIO, ((ratio > BitTorrent::Torrent::MAX_RATIO) ? -1 : ratio)},
457 {KEY_PROP_REANNOUNCE, torrent->nextAnnounce()},
458 {KEY_PROP_TOTAL_SIZE, torrent->totalSize()},
459 {KEY_PROP_PIECES_NUM, torrent->piecesCount()},
460 {KEY_PROP_PIECE_SIZE, torrent->pieceLength()},
461 {KEY_PROP_PIECES_HAVE, torrent->piecesHave()},
462 {KEY_PROP_CREATED_BY, torrent->creator()},
463 {KEY_PROP_ISPRIVATE, torrent->isPrivate()},
464 {KEY_PROP_ADDITION_DATE, Utils::DateTime::toSecsSinceEpoch(torrent->addedTime())},
465 {KEY_PROP_LAST_SEEN, Utils::DateTime::toSecsSinceEpoch(torrent->lastSeenComplete())},
466 {KEY_PROP_COMPLETION_DATE, Utils::DateTime::toSecsSinceEpoch(torrent->completedTime())},
467 {KEY_PROP_CREATION_DATE, Utils::DateTime::toSecsSinceEpoch(torrent->creationDate())},
468 {KEY_PROP_SAVE_PATH, torrent->savePath().toString()},
469 {KEY_PROP_DOWNLOAD_PATH, torrent->downloadPath().toString()},
470 {KEY_PROP_COMMENT, torrent->comment()}
473 setResult(ret);
476 // Returns the trackers for a torrent in JSON format.
477 // The return value is a JSON-formatted list of dictionaries.
478 // The dictionary keys are:
479 // - "url": Tracker URL
480 // - "status": Tracker status
481 // - "tier": Tracker tier
482 // - "num_peers": Number of peers this torrent is currently connected to
483 // - "num_seeds": Number of peers that have the whole file
484 // - "num_leeches": Number of peers that are still downloading
485 // - "num_downloaded": Tracker downloaded count
486 // - "msg": Tracker message (last)
487 void TorrentsController::trackersAction()
489 requireParams({u"hash"_s});
491 const auto id = BitTorrent::TorrentID::fromString(params()[u"hash"_s]);
492 const BitTorrent::Torrent *const torrent = BitTorrent::Session::instance()->getTorrent(id);
493 if (!torrent)
494 throw APIError(APIErrorType::NotFound);
496 QJsonArray trackerList = getStickyTrackers(torrent);
498 for (const BitTorrent::TrackerEntry &tracker : asConst(torrent->trackers()))
500 const bool isNotWorking = (tracker.status == BitTorrent::TrackerEntryStatus::NotWorking)
501 || (tracker.status == BitTorrent::TrackerEntryStatus::TrackerError)
502 || (tracker.status == BitTorrent::TrackerEntryStatus::Unreachable);
503 trackerList << QJsonObject
505 {KEY_TRACKER_URL, tracker.url},
506 {KEY_TRACKER_TIER, tracker.tier},
507 {KEY_TRACKER_STATUS, static_cast<int>((isNotWorking ? BitTorrent::TrackerEntryStatus::NotWorking : tracker.status))},
508 {KEY_TRACKER_MSG, tracker.message},
509 {KEY_TRACKER_PEERS_COUNT, tracker.numPeers},
510 {KEY_TRACKER_SEEDS_COUNT, tracker.numSeeds},
511 {KEY_TRACKER_LEECHES_COUNT, tracker.numLeeches},
512 {KEY_TRACKER_DOWNLOADED_COUNT, tracker.numDownloaded}
516 setResult(trackerList);
519 // Returns the web seeds for a torrent in JSON format.
520 // The return value is a JSON-formatted list of dictionaries.
521 // The dictionary keys are:
522 // - "url": Web seed URL
523 void TorrentsController::webseedsAction()
525 requireParams({u"hash"_s});
527 const auto id = BitTorrent::TorrentID::fromString(params()[u"hash"_s]);
528 BitTorrent::Torrent *const torrent = BitTorrent::Session::instance()->getTorrent(id);
529 if (!torrent)
530 throw APIError(APIErrorType::NotFound);
532 QJsonArray webSeedList;
533 for (const QUrl &webseed : asConst(torrent->urlSeeds()))
535 webSeedList.append(QJsonObject
537 {KEY_WEBSEED_URL, webseed.toString()}
541 setResult(webSeedList);
544 // Returns the files in a torrent in JSON format.
545 // The return value is a JSON-formatted list of dictionaries.
546 // The dictionary keys are:
547 // - "index": File index
548 // - "name": File name
549 // - "size": File size
550 // - "progress": File progress
551 // - "priority": File priority
552 // - "is_seed": Flag indicating if torrent is seeding/complete
553 // - "piece_range": Piece index range, the first number is the starting piece index
554 // and the second number is the ending piece index (inclusive)
555 void TorrentsController::filesAction()
557 requireParams({u"hash"_s});
559 const auto id = BitTorrent::TorrentID::fromString(params()[u"hash"_s]);
560 const BitTorrent::Torrent *const torrent = BitTorrent::Session::instance()->getTorrent(id);
561 if (!torrent)
562 throw APIError(APIErrorType::NotFound);
564 const int filesCount = torrent->filesCount();
565 QVector<int> fileIndexes;
566 const auto idxIt = params().constFind(u"indexes"_s);
567 if (idxIt != params().cend())
569 const QStringList indexStrings = idxIt.value().split(u'|');
570 fileIndexes.reserve(indexStrings.size());
571 std::transform(indexStrings.cbegin(), indexStrings.cend(), std::back_inserter(fileIndexes)
572 , [&filesCount](const QString &indexString) -> int
574 bool ok = false;
575 const int index = indexString.toInt(&ok);
576 if (!ok || (index < 0))
577 throw APIError(APIErrorType::Conflict, tr("\"%1\" is not a valid file index.").arg(indexString));
578 if (index >= filesCount)
579 throw APIError(APIErrorType::Conflict, tr("Index %1 is out of bounds.").arg(indexString));
580 return index;
583 else
585 fileIndexes.reserve(filesCount);
586 for (int i = 0; i < filesCount; ++i)
587 fileIndexes.append(i);
590 QJsonArray fileList;
591 if (torrent->hasMetadata())
593 const QVector<BitTorrent::DownloadPriority> priorities = torrent->filePriorities();
594 const QVector<qreal> fp = torrent->filesProgress();
595 const QVector<qreal> fileAvailability = torrent->availableFileFractions();
596 const BitTorrent::TorrentInfo info = torrent->info();
597 for (const int index : asConst(fileIndexes))
599 QJsonObject fileDict =
601 {KEY_FILE_INDEX, index},
602 {KEY_FILE_PROGRESS, fp[index]},
603 {KEY_FILE_PRIORITY, static_cast<int>(priorities[index])},
604 {KEY_FILE_SIZE, torrent->fileSize(index)},
605 {KEY_FILE_AVAILABILITY, fileAvailability[index]},
606 // need to provide paths using a platform-independent separator format
607 {KEY_FILE_NAME, torrent->filePath(index).data()}
610 const BitTorrent::TorrentInfo::PieceRange idx = info.filePieces(index);
611 fileDict[KEY_FILE_PIECE_RANGE] = QJsonArray {idx.first(), idx.last()};
613 if (index == 0)
614 fileDict[KEY_FILE_IS_SEED] = torrent->isFinished();
616 fileList.append(fileDict);
620 setResult(fileList);
623 // Returns an array of hashes (of each pieces respectively) for a torrent in JSON format.
624 // The return value is a JSON-formatted array of strings (hex strings).
625 void TorrentsController::pieceHashesAction()
627 requireParams({u"hash"_s});
629 const auto id = BitTorrent::TorrentID::fromString(params()[u"hash"_s]);
630 BitTorrent::Torrent *const torrent = BitTorrent::Session::instance()->getTorrent(id);
631 if (!torrent)
632 throw APIError(APIErrorType::NotFound);
634 QJsonArray pieceHashes;
635 if (torrent->hasMetadata())
637 const QVector<QByteArray> hashes = torrent->info().pieceHashes();
638 for (const QByteArray &hash : hashes)
639 pieceHashes.append(QString::fromLatin1(hash.toHex()));
642 setResult(pieceHashes);
645 // Returns an array of states (of each pieces respectively) for a torrent in JSON format.
646 // The return value is a JSON-formatted array of ints.
647 // 0: piece not downloaded
648 // 1: piece requested or downloading
649 // 2: piece already downloaded
650 void TorrentsController::pieceStatesAction()
652 requireParams({u"hash"_s});
654 const auto id = BitTorrent::TorrentID::fromString(params()[u"hash"_s]);
655 BitTorrent::Torrent *const torrent = BitTorrent::Session::instance()->getTorrent(id);
656 if (!torrent)
657 throw APIError(APIErrorType::NotFound);
659 QJsonArray pieceStates;
660 const QBitArray states = torrent->pieces();
661 for (int i = 0; i < states.size(); ++i)
662 pieceStates.append(static_cast<int>(states[i]) * 2);
664 const QBitArray dlstates = torrent->downloadingPieces();
665 for (int i = 0; i < states.size(); ++i)
667 if (dlstates[i])
668 pieceStates[i] = 1;
671 setResult(pieceStates);
674 void TorrentsController::addAction()
676 const QString urls = params()[u"urls"_s];
677 const QString cookie = params()[u"cookie"_s];
679 const bool skipChecking = parseBool(params()[u"skip_checking"_s]).value_or(false);
680 const bool seqDownload = parseBool(params()[u"sequentialDownload"_s]).value_or(false);
681 const bool firstLastPiece = parseBool(params()[u"firstLastPiecePrio"_s]).value_or(false);
682 const std::optional<bool> addToQueueTop = parseBool(params()[u"addToTopOfQueue"_s]);
683 const std::optional<bool> addPaused = parseBool(params()[u"paused"_s]);
684 const QString savepath = params()[u"savepath"_s].trimmed();
685 const QString downloadPath = params()[u"downloadPath"_s].trimmed();
686 const std::optional<bool> useDownloadPath = parseBool(params()[u"useDownloadPath"_s]);
687 const QString category = params()[u"category"_s];
688 const QStringList tags = params()[u"tags"_s].split(u',', Qt::SkipEmptyParts);
689 const QString torrentName = params()[u"rename"_s].trimmed();
690 const int upLimit = parseInt(params()[u"upLimit"_s]).value_or(-1);
691 const int dlLimit = parseInt(params()[u"dlLimit"_s]).value_or(-1);
692 const double ratioLimit = parseDouble(params()[u"ratioLimit"_s]).value_or(BitTorrent::Torrent::USE_GLOBAL_RATIO);
693 const int seedingTimeLimit = parseInt(params()[u"seedingTimeLimit"_s]).value_or(BitTorrent::Torrent::USE_GLOBAL_SEEDING_TIME);
694 const int inactiveSeedingTimeLimit = parseInt(params()[u"inactiveSeedingTimeLimit"_s]).value_or(BitTorrent::Torrent::USE_GLOBAL_INACTIVE_SEEDING_TIME);
695 const std::optional<bool> autoTMM = parseBool(params()[u"autoTMM"_s]);
697 const QString stopConditionParam = params()[u"stopCondition"_s];
698 const std::optional<BitTorrent::Torrent::StopCondition> stopCondition = (!stopConditionParam.isEmpty()
699 ? Utils::String::toEnum(stopConditionParam, BitTorrent::Torrent::StopCondition::None)
700 : std::optional<BitTorrent::Torrent::StopCondition> {});
702 const QString contentLayoutParam = params()[u"contentLayout"_s];
703 const std::optional<BitTorrent::TorrentContentLayout> contentLayout = (!contentLayoutParam.isEmpty()
704 ? Utils::String::toEnum(contentLayoutParam, BitTorrent::TorrentContentLayout::Original)
705 : std::optional<BitTorrent::TorrentContentLayout> {});
707 QList<QNetworkCookie> cookies;
708 if (!cookie.isEmpty())
710 const QStringList cookiesStr = cookie.split(u"; "_s);
711 for (QString cookieStr : cookiesStr)
713 cookieStr = cookieStr.trimmed();
714 int index = cookieStr.indexOf(u'=');
715 if (index > 1)
717 QByteArray name = cookieStr.left(index).toLatin1();
718 QByteArray value = cookieStr.right(cookieStr.length() - index - 1).toLatin1();
719 cookies += QNetworkCookie(name, value);
724 BitTorrent::AddTorrentParams addTorrentParams;
725 // TODO: Check if destination actually exists
726 addTorrentParams.skipChecking = skipChecking;
727 addTorrentParams.sequential = seqDownload;
728 addTorrentParams.firstLastPiecePriority = firstLastPiece;
729 addTorrentParams.addToQueueTop = addToQueueTop;
730 addTorrentParams.addPaused = addPaused;
731 addTorrentParams.stopCondition = stopCondition;
732 addTorrentParams.contentLayout = contentLayout;
733 addTorrentParams.savePath = Path(savepath);
734 addTorrentParams.downloadPath = Path(downloadPath);
735 addTorrentParams.useDownloadPath = useDownloadPath;
736 addTorrentParams.category = category;
737 addTorrentParams.tags.insert(tags.cbegin(), tags.cend());
738 addTorrentParams.name = torrentName;
739 addTorrentParams.uploadLimit = upLimit;
740 addTorrentParams.downloadLimit = dlLimit;
741 addTorrentParams.seedingTimeLimit = seedingTimeLimit;
742 addTorrentParams.inactiveSeedingTimeLimit = inactiveSeedingTimeLimit;
743 addTorrentParams.ratioLimit = ratioLimit;
744 addTorrentParams.useAutoTMM = autoTMM;
746 bool partialSuccess = false;
747 for (QString url : asConst(urls.split(u'\n')))
749 url = url.trimmed();
750 if (!url.isEmpty())
752 Net::DownloadManager::instance()->setCookiesFromUrl(cookies, QUrl::fromEncoded(url.toUtf8()));
753 partialSuccess |= app()->addTorrentManager()->addTorrent(url, addTorrentParams);
757 const DataMap torrents = data();
758 for (auto it = torrents.constBegin(); it != torrents.constEnd(); ++it)
760 if (const auto loadResult = BitTorrent::TorrentDescriptor::load(it.value()))
762 partialSuccess |= BitTorrent::Session::instance()->addTorrent(loadResult.value(), addTorrentParams);
764 else
766 throw APIError(APIErrorType::BadData, tr("Error: '%1' is not a valid torrent file.").arg(it.key()));
770 if (partialSuccess)
771 setResult(u"Ok."_s);
772 else
773 setResult(u"Fails."_s);
776 void TorrentsController::addTrackersAction()
778 requireParams({u"hash"_s, u"urls"_s});
780 const auto id = BitTorrent::TorrentID::fromString(params()[u"hash"_s]);
781 BitTorrent::Torrent *const torrent = BitTorrent::Session::instance()->getTorrent(id);
782 if (!torrent)
783 throw APIError(APIErrorType::NotFound);
785 const QVector<BitTorrent::TrackerEntry> entries = BitTorrent::parseTrackerEntries(params()[u"urls"_s]);
786 torrent->addTrackers(entries);
789 void TorrentsController::editTrackerAction()
791 requireParams({u"hash"_s, u"origUrl"_s, u"newUrl"_s});
793 const auto id = BitTorrent::TorrentID::fromString(params()[u"hash"_s]);
794 const QString origUrl = params()[u"origUrl"_s];
795 const QString newUrl = params()[u"newUrl"_s];
797 BitTorrent::Torrent *const torrent = BitTorrent::Session::instance()->getTorrent(id);
798 if (!torrent)
799 throw APIError(APIErrorType::NotFound);
801 const QUrl origTrackerUrl {origUrl};
802 const QUrl newTrackerUrl {newUrl};
803 if (origTrackerUrl == newTrackerUrl)
804 return;
805 if (!newTrackerUrl.isValid())
806 throw APIError(APIErrorType::BadParams, u"New tracker URL is invalid"_s);
808 QVector<BitTorrent::TrackerEntry> trackers = torrent->trackers();
809 bool match = false;
810 for (BitTorrent::TrackerEntry &tracker : trackers)
812 const QUrl trackerUrl {tracker.url};
813 if (trackerUrl == newTrackerUrl)
814 throw APIError(APIErrorType::Conflict, u"New tracker URL already exists"_s);
815 if (trackerUrl == origTrackerUrl)
817 match = true;
818 tracker.url = newTrackerUrl.toString();
821 if (!match)
822 throw APIError(APIErrorType::Conflict, u"Tracker not found"_s);
824 torrent->replaceTrackers(trackers);
826 if (!torrent->isPaused())
827 torrent->forceReannounce();
830 void TorrentsController::removeTrackersAction()
832 requireParams({u"hash"_s, u"urls"_s});
834 const auto id = BitTorrent::TorrentID::fromString(params()[u"hash"_s]);
835 BitTorrent::Torrent *const torrent = BitTorrent::Session::instance()->getTorrent(id);
836 if (!torrent)
837 throw APIError(APIErrorType::NotFound);
839 const QStringList urls = params()[u"urls"_s].split(u'|');
840 torrent->removeTrackers(urls);
842 if (!torrent->isPaused())
843 torrent->forceReannounce();
846 void TorrentsController::addPeersAction()
848 requireParams({u"hashes"_s, u"peers"_s});
850 const QStringList hashes = params()[u"hashes"_s].split(u'|');
851 const QStringList peers = params()[u"peers"_s].split(u'|');
853 QVector<BitTorrent::PeerAddress> peerList;
854 peerList.reserve(peers.size());
855 for (const QString &peer : peers)
857 const BitTorrent::PeerAddress addr = BitTorrent::PeerAddress::parse(peer.trimmed());
858 if (!addr.ip.isNull())
859 peerList.append(addr);
862 if (peerList.isEmpty())
863 throw APIError(APIErrorType::BadParams, u"No valid peers were specified"_s);
865 QJsonObject results;
867 applyToTorrents(hashes, [peers, peerList, &results](BitTorrent::Torrent *const torrent)
869 const int peersAdded = std::count_if(peerList.cbegin(), peerList.cend(), [torrent](const BitTorrent::PeerAddress &peer)
871 return torrent->connectPeer(peer);
874 results[torrent->id().toString()] = QJsonObject
876 {u"added"_s, peersAdded},
877 {u"failed"_s, (peers.size() - peersAdded)}
881 setResult(results);
884 void TorrentsController::pauseAction()
886 requireParams({u"hashes"_s});
888 const QStringList hashes = params()[u"hashes"_s].split(u'|');
889 applyToTorrents(hashes, [](BitTorrent::Torrent *const torrent) { torrent->pause(); });
892 void TorrentsController::resumeAction()
894 requireParams({u"hashes"_s});
896 const QStringList idStrings = params()[u"hashes"_s].split(u'|');
897 applyToTorrents(idStrings, [](BitTorrent::Torrent *const torrent) { torrent->resume(); });
900 void TorrentsController::filePrioAction()
902 requireParams({u"hash"_s, u"id"_s, u"priority"_s});
904 const auto id = BitTorrent::TorrentID::fromString(params()[u"hash"_s]);
905 bool ok = false;
906 const auto priority = static_cast<BitTorrent::DownloadPriority>(params()[u"priority"_s].toInt(&ok));
907 if (!ok)
908 throw APIError(APIErrorType::BadParams, tr("Priority must be an integer"));
910 if (!BitTorrent::isValidDownloadPriority(priority))
911 throw APIError(APIErrorType::BadParams, tr("Priority is not valid"));
913 BitTorrent::Torrent *const torrent = BitTorrent::Session::instance()->getTorrent(id);
914 if (!torrent)
915 throw APIError(APIErrorType::NotFound);
916 if (!torrent->hasMetadata())
917 throw APIError(APIErrorType::Conflict, tr("Torrent's metadata has not yet downloaded"));
919 const int filesCount = torrent->filesCount();
920 QVector<BitTorrent::DownloadPriority> priorities = torrent->filePriorities();
921 bool priorityChanged = false;
922 for (const QString &fileID : params()[u"id"_s].split(u'|'))
924 const int id = fileID.toInt(&ok);
925 if (!ok)
926 throw APIError(APIErrorType::BadParams, tr("File IDs must be integers"));
927 if ((id < 0) || (id >= filesCount))
928 throw APIError(APIErrorType::Conflict, tr("File ID is not valid"));
930 if (priorities[id] != priority)
932 priorities[id] = priority;
933 priorityChanged = true;
937 if (priorityChanged)
938 torrent->prioritizeFiles(priorities);
941 void TorrentsController::uploadLimitAction()
943 requireParams({u"hashes"_s});
945 const QStringList idList {params()[u"hashes"_s].split(u'|')};
946 QJsonObject map;
947 for (const QString &id : idList)
949 int limit = -1;
950 const BitTorrent::Torrent *const torrent = BitTorrent::Session::instance()->getTorrent(BitTorrent::TorrentID::fromString(id));
951 if (torrent)
952 limit = torrent->uploadLimit();
953 map[id] = limit;
956 setResult(map);
959 void TorrentsController::downloadLimitAction()
961 requireParams({u"hashes"_s});
963 const QStringList idList {params()[u"hashes"_s].split(u'|')};
964 QJsonObject map;
965 for (const QString &id : idList)
967 int limit = -1;
968 const BitTorrent::Torrent *const torrent = BitTorrent::Session::instance()->getTorrent(BitTorrent::TorrentID::fromString(id));
969 if (torrent)
970 limit = torrent->downloadLimit();
971 map[id] = limit;
974 setResult(map);
977 void TorrentsController::setUploadLimitAction()
979 requireParams({u"hashes"_s, u"limit"_s});
981 qlonglong limit = params()[u"limit"_s].toLongLong();
982 if (limit == 0)
983 limit = -1;
985 const QStringList hashes {params()[u"hashes"_s].split(u'|')};
986 applyToTorrents(hashes, [limit](BitTorrent::Torrent *const torrent) { torrent->setUploadLimit(limit); });
989 void TorrentsController::setDownloadLimitAction()
991 requireParams({u"hashes"_s, u"limit"_s});
993 qlonglong limit = params()[u"limit"_s].toLongLong();
994 if (limit == 0)
995 limit = -1;
997 const QStringList hashes {params()[u"hashes"_s].split(u'|')};
998 applyToTorrents(hashes, [limit](BitTorrent::Torrent *const torrent) { torrent->setDownloadLimit(limit); });
1001 void TorrentsController::setShareLimitsAction()
1003 requireParams({u"hashes"_s, u"ratioLimit"_s, u"seedingTimeLimit"_s, u"inactiveSeedingTimeLimit"_s});
1005 const qreal ratioLimit = params()[u"ratioLimit"_s].toDouble();
1006 const qlonglong seedingTimeLimit = params()[u"seedingTimeLimit"_s].toLongLong();
1007 const qlonglong inactiveSeedingTimeLimit = params()[u"inactiveSeedingTimeLimit"_s].toLongLong();
1008 const QStringList hashes = params()[u"hashes"_s].split(u'|');
1010 applyToTorrents(hashes, [ratioLimit, seedingTimeLimit, inactiveSeedingTimeLimit](BitTorrent::Torrent *const torrent)
1012 torrent->setRatioLimit(ratioLimit);
1013 torrent->setSeedingTimeLimit(seedingTimeLimit);
1014 torrent->setInactiveSeedingTimeLimit(inactiveSeedingTimeLimit);
1018 void TorrentsController::toggleSequentialDownloadAction()
1020 requireParams({u"hashes"_s});
1022 const QStringList hashes {params()[u"hashes"_s].split(u'|')};
1023 applyToTorrents(hashes, [](BitTorrent::Torrent *const torrent) { torrent->toggleSequentialDownload(); });
1026 void TorrentsController::toggleFirstLastPiecePrioAction()
1028 requireParams({u"hashes"_s});
1030 const QStringList hashes {params()[u"hashes"_s].split(u'|')};
1031 applyToTorrents(hashes, [](BitTorrent::Torrent *const torrent) { torrent->toggleFirstLastPiecePriority(); });
1034 void TorrentsController::setSuperSeedingAction()
1036 requireParams({u"hashes"_s, u"value"_s});
1038 const bool value {parseBool(params()[u"value"_s]).value_or(false)};
1039 const QStringList hashes {params()[u"hashes"_s].split(u'|')};
1040 applyToTorrents(hashes, [value](BitTorrent::Torrent *const torrent) { torrent->setSuperSeeding(value); });
1043 void TorrentsController::setForceStartAction()
1045 requireParams({u"hashes"_s, u"value"_s});
1047 const bool value {parseBool(params()[u"value"_s]).value_or(false)};
1048 const QStringList hashes {params()[u"hashes"_s].split(u'|')};
1049 applyToTorrents(hashes, [value](BitTorrent::Torrent *const torrent)
1051 torrent->resume(value ? BitTorrent::TorrentOperatingMode::Forced : BitTorrent::TorrentOperatingMode::AutoManaged);
1055 void TorrentsController::deleteAction()
1057 requireParams({u"hashes"_s, u"deleteFiles"_s});
1059 const QStringList hashes {params()[u"hashes"_s].split(u'|')};
1060 const DeleteOption deleteOption = parseBool(params()[u"deleteFiles"_s]).value_or(false)
1061 ? DeleteTorrentAndFiles : DeleteTorrent;
1062 applyToTorrents(hashes, [deleteOption](const BitTorrent::Torrent *torrent)
1064 BitTorrent::Session::instance()->deleteTorrent(torrent->id(), deleteOption);
1068 void TorrentsController::increasePrioAction()
1070 requireParams({u"hashes"_s});
1072 if (!BitTorrent::Session::instance()->isQueueingSystemEnabled())
1073 throw APIError(APIErrorType::Conflict, tr("Torrent queueing must be enabled"));
1075 const QStringList hashes {params()[u"hashes"_s].split(u'|')};
1076 BitTorrent::Session::instance()->increaseTorrentsQueuePos(toTorrentIDs(hashes));
1079 void TorrentsController::decreasePrioAction()
1081 requireParams({u"hashes"_s});
1083 if (!BitTorrent::Session::instance()->isQueueingSystemEnabled())
1084 throw APIError(APIErrorType::Conflict, tr("Torrent queueing must be enabled"));
1086 const QStringList hashes {params()[u"hashes"_s].split(u'|')};
1087 BitTorrent::Session::instance()->decreaseTorrentsQueuePos(toTorrentIDs(hashes));
1090 void TorrentsController::topPrioAction()
1092 requireParams({u"hashes"_s});
1094 if (!BitTorrent::Session::instance()->isQueueingSystemEnabled())
1095 throw APIError(APIErrorType::Conflict, tr("Torrent queueing must be enabled"));
1097 const QStringList hashes {params()[u"hashes"_s].split(u'|')};
1098 BitTorrent::Session::instance()->topTorrentsQueuePos(toTorrentIDs(hashes));
1101 void TorrentsController::bottomPrioAction()
1103 requireParams({u"hashes"_s});
1105 if (!BitTorrent::Session::instance()->isQueueingSystemEnabled())
1106 throw APIError(APIErrorType::Conflict, tr("Torrent queueing must be enabled"));
1108 const QStringList hashes {params()[u"hashes"_s].split(u'|')};
1109 BitTorrent::Session::instance()->bottomTorrentsQueuePos(toTorrentIDs(hashes));
1112 void TorrentsController::setLocationAction()
1114 requireParams({u"hashes"_s, u"location"_s});
1116 const QStringList hashes {params()[u"hashes"_s].split(u'|')};
1117 const Path newLocation {params()[u"location"_s].trimmed()};
1119 if (newLocation.isEmpty())
1120 throw APIError(APIErrorType::BadParams, tr("Save path cannot be empty"));
1122 // try to create the location if it does not exist
1123 if (!Utils::Fs::mkpath(newLocation))
1124 throw APIError(APIErrorType::Conflict, tr("Cannot make save path"));
1126 applyToTorrents(hashes, [newLocation](BitTorrent::Torrent *const torrent)
1128 LogMsg(tr("WebUI Set location: moving \"%1\", from \"%2\" to \"%3\"")
1129 .arg(torrent->name(), torrent->savePath().toString(), newLocation.toString()));
1130 torrent->setAutoTMMEnabled(false);
1131 torrent->setSavePath(newLocation);
1135 void TorrentsController::setSavePathAction()
1137 requireParams({u"id"_s, u"path"_s});
1139 const QStringList ids {params()[u"id"_s].split(u'|')};
1140 const Path newPath {params()[u"path"_s]};
1142 if (newPath.isEmpty())
1143 throw APIError(APIErrorType::BadParams, tr("Save path cannot be empty"));
1145 // try to create the directory if it does not exist
1146 if (!Utils::Fs::mkpath(newPath))
1147 throw APIError(APIErrorType::Conflict, tr("Cannot create target directory"));
1149 // check permissions
1150 if (!Utils::Fs::isWritable(newPath))
1151 throw APIError(APIErrorType::AccessDenied, tr("Cannot write to directory"));
1153 applyToTorrents(ids, [&newPath](BitTorrent::Torrent *const torrent)
1155 if (!torrent->isAutoTMMEnabled())
1156 torrent->setSavePath(newPath);
1160 void TorrentsController::setDownloadPathAction()
1162 requireParams({u"id"_s, u"path"_s});
1164 const QStringList ids {params()[u"id"_s].split(u'|')};
1165 const Path newPath {params()[u"path"_s]};
1167 if (!newPath.isEmpty())
1169 // try to create the directory if it does not exist
1170 if (!Utils::Fs::mkpath(newPath))
1171 throw APIError(APIErrorType::Conflict, tr("Cannot create target directory"));
1173 // check permissions
1174 if (!Utils::Fs::isWritable(newPath))
1175 throw APIError(APIErrorType::AccessDenied, tr("Cannot write to directory"));
1178 applyToTorrents(ids, [&newPath](BitTorrent::Torrent *const torrent)
1180 if (!torrent->isAutoTMMEnabled())
1181 torrent->setDownloadPath(newPath);
1185 void TorrentsController::renameAction()
1187 requireParams({u"hash"_s, u"name"_s});
1189 const auto id = BitTorrent::TorrentID::fromString(params()[u"hash"_s]);
1190 QString name = params()[u"name"_s].trimmed();
1192 if (name.isEmpty())
1193 throw APIError(APIErrorType::Conflict, tr("Incorrect torrent name"));
1195 BitTorrent::Torrent *const torrent = BitTorrent::Session::instance()->getTorrent(id);
1196 if (!torrent)
1197 throw APIError(APIErrorType::NotFound);
1199 name.replace(QRegularExpression(u"\r?\n|\r"_s), u" "_s);
1200 torrent->setName(name);
1203 void TorrentsController::setAutoManagementAction()
1205 requireParams({u"hashes"_s, u"enable"_s});
1207 const QStringList hashes {params()[u"hashes"_s].split(u'|')};
1208 const bool isEnabled {parseBool(params()[u"enable"_s]).value_or(false)};
1210 applyToTorrents(hashes, [isEnabled](BitTorrent::Torrent *const torrent)
1212 torrent->setAutoTMMEnabled(isEnabled);
1216 void TorrentsController::recheckAction()
1218 requireParams({u"hashes"_s});
1220 const QStringList hashes {params()[u"hashes"_s].split(u'|')};
1221 applyToTorrents(hashes, [](BitTorrent::Torrent *const torrent) { torrent->forceRecheck(); });
1224 void TorrentsController::reannounceAction()
1226 requireParams({u"hashes"_s});
1228 const QStringList hashes {params()[u"hashes"_s].split(u'|')};
1229 applyToTorrents(hashes, [](BitTorrent::Torrent *const torrent) { torrent->forceReannounce(); });
1232 void TorrentsController::setCategoryAction()
1234 requireParams({u"hashes"_s, u"category"_s});
1236 const QStringList hashes {params()[u"hashes"_s].split(u'|')};
1237 const QString category {params()[u"category"_s]};
1239 applyToTorrents(hashes, [category](BitTorrent::Torrent *const torrent)
1241 if (!torrent->setCategory(category))
1242 throw APIError(APIErrorType::Conflict, tr("Incorrect category name"));
1246 void TorrentsController::createCategoryAction()
1248 requireParams({u"category"_s});
1250 const QString category = params()[u"category"_s];
1251 if (category.isEmpty())
1252 throw APIError(APIErrorType::BadParams, tr("Category cannot be empty"));
1254 if (!BitTorrent::Session::isValidCategoryName(category))
1255 throw APIError(APIErrorType::Conflict, tr("Incorrect category name"));
1257 const Path savePath {params()[u"savePath"_s]};
1258 const auto useDownloadPath = parseBool(params()[u"downloadPathEnabled"_s]);
1259 BitTorrent::CategoryOptions categoryOptions;
1260 categoryOptions.savePath = savePath;
1261 if (useDownloadPath.has_value())
1263 const Path downloadPath {params()[u"downloadPath"_s]};
1264 categoryOptions.downloadPath = {useDownloadPath.value(), downloadPath};
1267 if (!BitTorrent::Session::instance()->addCategory(category, categoryOptions))
1268 throw APIError(APIErrorType::Conflict, tr("Unable to create category"));
1271 void TorrentsController::editCategoryAction()
1273 requireParams({u"category"_s, u"savePath"_s});
1275 const QString category = params()[u"category"_s];
1276 if (category.isEmpty())
1277 throw APIError(APIErrorType::BadParams, tr("Category cannot be empty"));
1279 const Path savePath {params()[u"savePath"_s]};
1280 const auto useDownloadPath = parseBool(params()[u"downloadPathEnabled"_s]);
1281 BitTorrent::CategoryOptions categoryOptions;
1282 categoryOptions.savePath = savePath;
1283 if (useDownloadPath.has_value())
1285 const Path downloadPath {params()[u"downloadPath"_s]};
1286 categoryOptions.downloadPath = {useDownloadPath.value(), downloadPath};
1289 if (!BitTorrent::Session::instance()->editCategory(category, categoryOptions))
1290 throw APIError(APIErrorType::Conflict, tr("Unable to edit category"));
1293 void TorrentsController::removeCategoriesAction()
1295 requireParams({u"categories"_s});
1297 const QStringList categories {params()[u"categories"_s].split(u'\n')};
1298 for (const QString &category : categories)
1299 BitTorrent::Session::instance()->removeCategory(category);
1302 void TorrentsController::categoriesAction()
1304 const auto *session = BitTorrent::Session::instance();
1306 QJsonObject categories;
1307 const QStringList categoriesList = session->categories();
1308 for (const auto &categoryName : categoriesList)
1310 const BitTorrent::CategoryOptions categoryOptions = session->categoryOptions(categoryName);
1311 QJsonObject category = categoryOptions.toJSON();
1312 // adjust it to be compatible with existing WebAPI
1313 category[u"savePath"_s] = category.take(u"save_path"_s);
1314 category.insert(u"name"_s, categoryName);
1315 categories[categoryName] = category;
1318 setResult(categories);
1321 void TorrentsController::addTagsAction()
1323 requireParams({u"hashes"_s, u"tags"_s});
1325 const QStringList hashes {params()[u"hashes"_s].split(u'|')};
1326 const QStringList tags {params()[u"tags"_s].split(u',', Qt::SkipEmptyParts)};
1328 for (const QString &tagStr : tags)
1330 applyToTorrents(hashes, [&tagStr](BitTorrent::Torrent *const torrent)
1332 torrent->addTag(Tag(tagStr));
1337 void TorrentsController::removeTagsAction()
1339 requireParams({u"hashes"_s});
1341 const QStringList hashes {params()[u"hashes"_s].split(u'|')};
1342 const QStringList tags {params()[u"tags"_s].split(u',', Qt::SkipEmptyParts)};
1344 for (const QString &tagStr : tags)
1346 applyToTorrents(hashes, [&tagStr](BitTorrent::Torrent *const torrent)
1348 torrent->removeTag(Tag(tagStr));
1352 if (tags.isEmpty())
1354 applyToTorrents(hashes, [](BitTorrent::Torrent *const torrent)
1356 torrent->removeAllTags();
1361 void TorrentsController::createTagsAction()
1363 requireParams({u"tags"_s});
1365 const QStringList tags {params()[u"tags"_s].split(u',', Qt::SkipEmptyParts)};
1367 for (const QString &tagStr : tags)
1368 BitTorrent::Session::instance()->addTag(Tag(tagStr));
1371 void TorrentsController::deleteTagsAction()
1373 requireParams({u"tags"_s});
1375 const QStringList tags {params()[u"tags"_s].split(u',', Qt::SkipEmptyParts)};
1376 for (const QString &tagStr : tags)
1377 BitTorrent::Session::instance()->removeTag(Tag(tagStr));
1380 void TorrentsController::tagsAction()
1382 QJsonArray result;
1383 for (const Tag &tag : asConst(BitTorrent::Session::instance()->tags()))
1384 result << tag.toString();
1385 setResult(result);
1388 void TorrentsController::renameFileAction()
1390 requireParams({u"hash"_s, u"oldPath"_s, u"newPath"_s});
1392 const auto id = BitTorrent::TorrentID::fromString(params()[u"hash"_s]);
1393 BitTorrent::Torrent *const torrent = BitTorrent::Session::instance()->getTorrent(id);
1394 if (!torrent)
1395 throw APIError(APIErrorType::NotFound);
1397 const Path oldPath {params()[u"oldPath"_s]};
1398 const Path newPath {params()[u"newPath"_s]};
1402 torrent->renameFile(oldPath, newPath);
1404 catch (const RuntimeError &error)
1406 throw APIError(APIErrorType::Conflict, error.message());
1410 void TorrentsController::renameFolderAction()
1412 requireParams({u"hash"_s, u"oldPath"_s, u"newPath"_s});
1414 const auto id = BitTorrent::TorrentID::fromString(params()[u"hash"_s]);
1415 BitTorrent::Torrent *const torrent = BitTorrent::Session::instance()->getTorrent(id);
1416 if (!torrent)
1417 throw APIError(APIErrorType::NotFound);
1419 const Path oldPath {params()[u"oldPath"_s]};
1420 const Path newPath {params()[u"newPath"_s]};
1424 torrent->renameFolder(oldPath, newPath);
1426 catch (const RuntimeError &error)
1428 throw APIError(APIErrorType::Conflict, error.message());
1432 void TorrentsController::exportAction()
1434 requireParams({u"hash"_s});
1436 const auto id = BitTorrent::TorrentID::fromString(params()[u"hash"_s]);
1437 const BitTorrent::Torrent *const torrent = BitTorrent::Session::instance()->getTorrent(id);
1438 if (!torrent)
1439 throw APIError(APIErrorType::NotFound);
1441 const nonstd::expected<QByteArray, QString> result = torrent->exportToBuffer();
1442 if (!result)
1443 throw APIError(APIErrorType::Conflict, tr("Unable to export torrent file. Error: %1").arg(result.error()));
1445 setResult(result.value());