Revamp tracker entries handling
[qBittorrent.git] / src / webui / api / torrentscontroller.cpp
blob875933d0ca2ad65092d068c68f8b862e7a785b0d
1 /*
2 * Bittorrent Client using Qt and libtorrent.
3 * Copyright (C) 2018 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/bittorrent/categoryoptions.h"
42 #include "base/bittorrent/downloadpriority.h"
43 #include "base/bittorrent/infohash.h"
44 #include "base/bittorrent/peeraddress.h"
45 #include "base/bittorrent/peerinfo.h"
46 #include "base/bittorrent/session.h"
47 #include "base/bittorrent/torrent.h"
48 #include "base/bittorrent/torrentinfo.h"
49 #include "base/bittorrent/trackerentry.h"
50 #include "base/global.h"
51 #include "base/logger.h"
52 #include "base/net/downloadmanager.h"
53 #include "base/torrentfilter.h"
54 #include "base/utils/fs.h"
55 #include "base/utils/string.h"
56 #include "apierror.h"
57 #include "serialize/serialize_torrent.h"
59 // Tracker keys
60 const QString KEY_TRACKER_URL = u"url"_qs;
61 const QString KEY_TRACKER_STATUS = u"status"_qs;
62 const QString KEY_TRACKER_TIER = u"tier"_qs;
63 const QString KEY_TRACKER_MSG = u"msg"_qs;
64 const QString KEY_TRACKER_PEERS_COUNT = u"num_peers"_qs;
65 const QString KEY_TRACKER_SEEDS_COUNT = u"num_seeds"_qs;
66 const QString KEY_TRACKER_LEECHES_COUNT = u"num_leeches"_qs;
67 const QString KEY_TRACKER_DOWNLOADED_COUNT = u"num_downloaded"_qs;
69 // Web seed keys
70 const QString KEY_WEBSEED_URL = u"url"_qs;
72 // Torrent keys (Properties)
73 const QString KEY_PROP_TIME_ELAPSED = u"time_elapsed"_qs;
74 const QString KEY_PROP_SEEDING_TIME = u"seeding_time"_qs;
75 const QString KEY_PROP_ETA = u"eta"_qs;
76 const QString KEY_PROP_CONNECT_COUNT = u"nb_connections"_qs;
77 const QString KEY_PROP_CONNECT_COUNT_LIMIT = u"nb_connections_limit"_qs;
78 const QString KEY_PROP_DOWNLOADED = u"total_downloaded"_qs;
79 const QString KEY_PROP_DOWNLOADED_SESSION = u"total_downloaded_session"_qs;
80 const QString KEY_PROP_UPLOADED = u"total_uploaded"_qs;
81 const QString KEY_PROP_UPLOADED_SESSION = u"total_uploaded_session"_qs;
82 const QString KEY_PROP_DL_SPEED = u"dl_speed"_qs;
83 const QString KEY_PROP_DL_SPEED_AVG = u"dl_speed_avg"_qs;
84 const QString KEY_PROP_UP_SPEED = u"up_speed"_qs;
85 const QString KEY_PROP_UP_SPEED_AVG = u"up_speed_avg"_qs;
86 const QString KEY_PROP_DL_LIMIT = u"dl_limit"_qs;
87 const QString KEY_PROP_UP_LIMIT = u"up_limit"_qs;
88 const QString KEY_PROP_WASTED = u"total_wasted"_qs;
89 const QString KEY_PROP_SEEDS = u"seeds"_qs;
90 const QString KEY_PROP_SEEDS_TOTAL = u"seeds_total"_qs;
91 const QString KEY_PROP_PEERS = u"peers"_qs;
92 const QString KEY_PROP_PEERS_TOTAL = u"peers_total"_qs;
93 const QString KEY_PROP_RATIO = u"share_ratio"_qs;
94 const QString KEY_PROP_REANNOUNCE = u"reannounce"_qs;
95 const QString KEY_PROP_TOTAL_SIZE = u"total_size"_qs;
96 const QString KEY_PROP_PIECES_NUM = u"pieces_num"_qs;
97 const QString KEY_PROP_PIECE_SIZE = u"piece_size"_qs;
98 const QString KEY_PROP_PIECES_HAVE = u"pieces_have"_qs;
99 const QString KEY_PROP_CREATED_BY = u"created_by"_qs;
100 const QString KEY_PROP_LAST_SEEN = u"last_seen"_qs;
101 const QString KEY_PROP_ADDITION_DATE = u"addition_date"_qs;
102 const QString KEY_PROP_COMPLETION_DATE = u"completion_date"_qs;
103 const QString KEY_PROP_CREATION_DATE = u"creation_date"_qs;
104 const QString KEY_PROP_SAVE_PATH = u"save_path"_qs;
105 const QString KEY_PROP_DOWNLOAD_PATH = u"download_path"_qs;
106 const QString KEY_PROP_COMMENT = u"comment"_qs;
108 // File keys
109 const QString KEY_FILE_INDEX = u"index"_qs;
110 const QString KEY_FILE_NAME = u"name"_qs;
111 const QString KEY_FILE_SIZE = u"size"_qs;
112 const QString KEY_FILE_PROGRESS = u"progress"_qs;
113 const QString KEY_FILE_PRIORITY = u"priority"_qs;
114 const QString KEY_FILE_IS_SEED = u"is_seed"_qs;
115 const QString KEY_FILE_PIECE_RANGE = u"piece_range"_qs;
116 const QString KEY_FILE_AVAILABILITY = u"availability"_qs;
118 namespace
120 using Utils::String::parseBool;
121 using Utils::String::parseInt;
122 using Utils::String::parseDouble;
124 void applyToTorrents(const QStringList &idList, const std::function<void (BitTorrent::Torrent *torrent)> &func)
126 if ((idList.size() == 1) && (idList[0] == u"all"))
128 for (BitTorrent::Torrent *const torrent : asConst(BitTorrent::Session::instance()->torrents()))
129 func(torrent);
131 else
133 for (const QString &idString : idList)
135 const auto hash = BitTorrent::TorrentID::fromString(idString);
136 BitTorrent::Torrent *const torrent = BitTorrent::Session::instance()->findTorrent(hash);
137 if (torrent)
138 func(torrent);
143 std::optional<QString> getOptionalString(const StringMap &params, const QString &name)
145 const auto it = params.constFind(name);
146 if (it == params.cend())
147 return std::nullopt;
149 return it.value();
152 QJsonArray getStickyTrackers(const BitTorrent::Torrent *const torrent)
154 int seedsDHT = 0, seedsPeX = 0, seedsLSD = 0, leechesDHT = 0, leechesPeX = 0, leechesLSD = 0;
155 for (const BitTorrent::PeerInfo &peer : asConst(torrent->peers()))
157 if (peer.isConnecting()) continue;
159 if (peer.isSeed())
161 if (peer.fromDHT())
162 ++seedsDHT;
163 if (peer.fromPeX())
164 ++seedsPeX;
165 if (peer.fromLSD())
166 ++seedsLSD;
168 else
170 if (peer.fromDHT())
171 ++leechesDHT;
172 if (peer.fromPeX())
173 ++leechesPeX;
174 if (peer.fromLSD())
175 ++leechesLSD;
179 const int working = static_cast<int>(BitTorrent::TrackerEntry::Working);
180 const int disabled = 0;
182 const QString privateMsg {QCoreApplication::translate("TrackerListWidget", "This torrent is private")};
183 const bool isTorrentPrivate = torrent->isPrivate();
185 const QJsonObject dht
187 {KEY_TRACKER_URL, u"** [DHT] **"_qs},
188 {KEY_TRACKER_TIER, -1},
189 {KEY_TRACKER_MSG, (isTorrentPrivate ? privateMsg : u""_qs)},
190 {KEY_TRACKER_STATUS, ((BitTorrent::Session::instance()->isDHTEnabled() && !isTorrentPrivate) ? working : disabled)},
191 {KEY_TRACKER_PEERS_COUNT, 0},
192 {KEY_TRACKER_DOWNLOADED_COUNT, 0},
193 {KEY_TRACKER_SEEDS_COUNT, seedsDHT},
194 {KEY_TRACKER_LEECHES_COUNT, leechesDHT}
197 const QJsonObject pex
199 {KEY_TRACKER_URL, u"** [PeX] **"_qs},
200 {KEY_TRACKER_TIER, -1},
201 {KEY_TRACKER_MSG, (isTorrentPrivate ? privateMsg : u""_qs)},
202 {KEY_TRACKER_STATUS, ((BitTorrent::Session::instance()->isPeXEnabled() && !isTorrentPrivate) ? working : disabled)},
203 {KEY_TRACKER_PEERS_COUNT, 0},
204 {KEY_TRACKER_DOWNLOADED_COUNT, 0},
205 {KEY_TRACKER_SEEDS_COUNT, seedsPeX},
206 {KEY_TRACKER_LEECHES_COUNT, leechesPeX}
209 const QJsonObject lsd
211 {KEY_TRACKER_URL, u"** [LSD] **"_qs},
212 {KEY_TRACKER_TIER, -1},
213 {KEY_TRACKER_MSG, (isTorrentPrivate ? privateMsg : u""_qs)},
214 {KEY_TRACKER_STATUS, ((BitTorrent::Session::instance()->isLSDEnabled() && !isTorrentPrivate) ? working : disabled)},
215 {KEY_TRACKER_PEERS_COUNT, 0},
216 {KEY_TRACKER_DOWNLOADED_COUNT, 0},
217 {KEY_TRACKER_SEEDS_COUNT, seedsLSD},
218 {KEY_TRACKER_LEECHES_COUNT, leechesLSD}
221 return {dht, pex, lsd};
224 QVector<BitTorrent::TorrentID> toTorrentIDs(const QStringList &idStrings)
226 QVector<BitTorrent::TorrentID> idList;
227 idList.reserve(idStrings.size());
228 for (const QString &hash : idStrings)
229 idList << BitTorrent::TorrentID::fromString(hash);
230 return idList;
234 // Returns all the torrents in JSON format.
235 // The return value is a JSON-formatted list of dictionaries.
236 // The dictionary keys are:
237 // - "hash": Torrent hash (ID)
238 // - "name": Torrent name
239 // - "size": Torrent size
240 // - "progress": Torrent progress
241 // - "dlspeed": Torrent download speed
242 // - "upspeed": Torrent upload speed
243 // - "priority": Torrent queue position (-1 if queuing is disabled)
244 // - "num_seeds": Torrent seeds connected to
245 // - "num_complete": Torrent seeds in the swarm
246 // - "num_leechs": Torrent leechers connected to
247 // - "num_incomplete": Torrent leechers in the swarm
248 // - "ratio": Torrent share ratio
249 // - "eta": Torrent ETA
250 // - "state": Torrent state
251 // - "seq_dl": Torrent sequential download state
252 // - "f_l_piece_prio": Torrent first last piece priority state
253 // - "force_start": Torrent force start state
254 // - "category": Torrent category
255 // GET params:
256 // - filter (string): all, downloading, seeding, completed, paused, resumed, active, inactive, stalled, stalled_uploading, stalled_downloading
257 // - category (string): torrent category for filtering by it (empty string means "uncategorized"; no "category" param presented means "any category")
258 // - tag (string): torrent tag for filtering by it (empty string means "untagged"; no "tag" param presented means "any tag")
259 // - hashes (string): filter by hashes, can contain multiple hashes separated by |
260 // - sort (string): name of column for sorting by its value
261 // - reverse (bool): enable reverse sorting
262 // - limit (int): set limit number of torrents returned (if greater than 0, otherwise - unlimited)
263 // - offset (int): set offset (if less than 0 - offset from end)
264 void TorrentsController::infoAction()
266 const QString filter {params()[u"filter"_qs]};
267 const std::optional<QString> category = getOptionalString(params(), u"category"_qs);
268 const std::optional<QString> tag = getOptionalString(params(), u"tag"_qs);
269 const QString sortedColumn {params()[u"sort"_qs]};
270 const bool reverse {parseBool(params()[u"reverse"_qs]).value_or(false)};
271 int limit {params()[u"limit"_qs].toInt()};
272 int offset {params()[u"offset"_qs].toInt()};
273 const QStringList hashes {params()[u"hashes"_qs].split(u'|', Qt::SkipEmptyParts)};
275 std::optional<TorrentIDSet> idSet;
276 if (!hashes.isEmpty())
278 idSet = TorrentIDSet();
279 for (const QString &hash : hashes)
280 idSet->insert(BitTorrent::TorrentID::fromString(hash));
283 const TorrentFilter torrentFilter {filter, idSet, category, tag};
284 QVariantList torrentList;
285 for (const BitTorrent::Torrent *torrent : asConst(BitTorrent::Session::instance()->torrents()))
287 if (torrentFilter.match(torrent))
288 torrentList.append(serialize(*torrent));
291 if (torrentList.isEmpty())
293 setResult(QJsonArray {});
294 return;
297 if (!sortedColumn.isEmpty())
299 if (!torrentList[0].toMap().contains(sortedColumn))
300 throw APIError(APIErrorType::BadParams, tr("'sort' parameter is invalid"));
302 const auto lessThan = [](const QVariant &left, const QVariant &right) -> bool
304 Q_ASSERT(left.type() == right.type());
306 switch (static_cast<QMetaType::Type>(left.type()))
308 case QMetaType::Bool:
309 return left.value<bool>() < right.value<bool>();
310 case QMetaType::Double:
311 return left.value<double>() < right.value<double>();
312 case QMetaType::Float:
313 return left.value<float>() < right.value<float>();
314 case QMetaType::Int:
315 return left.value<int>() < right.value<int>();
316 case QMetaType::LongLong:
317 return left.value<qlonglong>() < right.value<qlonglong>();
318 case QMetaType::QString:
319 return left.value<QString>() < right.value<QString>();
320 default:
321 qWarning("Unhandled QVariant comparison, type: %d, name: %s", left.type()
322 , QMetaType::typeName(left.type()));
323 break;
325 return false;
328 std::sort(torrentList.begin(), torrentList.end()
329 , [reverse, &sortedColumn, &lessThan](const QVariant &torrent1, const QVariant &torrent2)
331 const QVariant value1 {torrent1.toMap().value(sortedColumn)};
332 const QVariant value2 {torrent2.toMap().value(sortedColumn)};
333 return reverse ? lessThan(value2, value1) : lessThan(value1, value2);
337 const int size = torrentList.size();
338 // normalize offset
339 if (offset < 0)
340 offset = size + offset;
341 if ((offset >= size) || (offset < 0))
342 offset = 0;
343 // normalize limit
344 if (limit <= 0)
345 limit = -1; // unlimited
347 if ((limit > 0) || (offset > 0))
348 torrentList = torrentList.mid(offset, limit);
350 setResult(QJsonArray::fromVariantList(torrentList));
353 // Returns the properties for a torrent in JSON format.
354 // The return value is a JSON-formatted dictionary.
355 // The dictionary keys are:
356 // - "time_elapsed": Torrent elapsed time
357 // - "seeding_time": Torrent elapsed time while complete
358 // - "eta": Torrent ETA
359 // - "nb_connections": Torrent connection count
360 // - "nb_connections_limit": Torrent connection count limit
361 // - "total_downloaded": Total data uploaded for torrent
362 // - "total_downloaded_session": Total data downloaded this session
363 // - "total_uploaded": Total data uploaded for torrent
364 // - "total_uploaded_session": Total data uploaded this session
365 // - "dl_speed": Torrent download speed
366 // - "dl_speed_avg": Torrent average download speed
367 // - "up_speed": Torrent upload speed
368 // - "up_speed_avg": Torrent average upload speed
369 // - "dl_limit": Torrent download limit
370 // - "up_limit": Torrent upload limit
371 // - "total_wasted": Total data wasted for torrent
372 // - "seeds": Torrent connected seeds
373 // - "seeds_total": Torrent total number of seeds
374 // - "peers": Torrent connected peers
375 // - "peers_total": Torrent total number of peers
376 // - "share_ratio": Torrent share ratio
377 // - "reannounce": Torrent next reannounce time
378 // - "total_size": Torrent total size
379 // - "pieces_num": Torrent pieces count
380 // - "piece_size": Torrent piece size
381 // - "pieces_have": Torrent pieces have
382 // - "created_by": Torrent creator
383 // - "last_seen": Torrent last seen complete
384 // - "addition_date": Torrent addition date
385 // - "completion_date": Torrent completion date
386 // - "creation_date": Torrent creation date
387 // - "save_path": Torrent save path
388 // - "download_path": Torrent download path
389 // - "comment": Torrent comment
390 void TorrentsController::propertiesAction()
392 requireParams({u"hash"_qs});
394 const auto id = BitTorrent::TorrentID::fromString(params()[u"hash"_qs]);
395 BitTorrent::Torrent *const torrent = BitTorrent::Session::instance()->findTorrent(id);
396 if (!torrent)
397 throw APIError(APIErrorType::NotFound);
399 QJsonObject dataDict;
401 dataDict[KEY_TORRENT_INFOHASHV1] = torrent->infoHash().v1().toString();
402 dataDict[KEY_TORRENT_INFOHASHV2] = torrent->infoHash().v2().toString();
403 dataDict[KEY_PROP_TIME_ELAPSED] = torrent->activeTime();
404 dataDict[KEY_PROP_SEEDING_TIME] = torrent->finishedTime();
405 dataDict[KEY_PROP_ETA] = static_cast<double>(torrent->eta());
406 dataDict[KEY_PROP_CONNECT_COUNT] = torrent->connectionsCount();
407 dataDict[KEY_PROP_CONNECT_COUNT_LIMIT] = torrent->connectionsLimit();
408 dataDict[KEY_PROP_DOWNLOADED] = torrent->totalDownload();
409 dataDict[KEY_PROP_DOWNLOADED_SESSION] = torrent->totalPayloadDownload();
410 dataDict[KEY_PROP_UPLOADED] = torrent->totalUpload();
411 dataDict[KEY_PROP_UPLOADED_SESSION] = torrent->totalPayloadUpload();
412 dataDict[KEY_PROP_DL_SPEED] = torrent->downloadPayloadRate();
413 const qlonglong dlDuration = torrent->activeTime() - torrent->finishedTime();
414 dataDict[KEY_PROP_DL_SPEED_AVG] = torrent->totalDownload() / ((dlDuration == 0) ? -1 : dlDuration);
415 dataDict[KEY_PROP_UP_SPEED] = torrent->uploadPayloadRate();
416 const qlonglong ulDuration = torrent->activeTime();
417 dataDict[KEY_PROP_UP_SPEED_AVG] = torrent->totalUpload() / ((ulDuration == 0) ? -1 : ulDuration);
418 dataDict[KEY_PROP_DL_LIMIT] = torrent->downloadLimit() <= 0 ? -1 : torrent->downloadLimit();
419 dataDict[KEY_PROP_UP_LIMIT] = torrent->uploadLimit() <= 0 ? -1 : torrent->uploadLimit();
420 dataDict[KEY_PROP_WASTED] = torrent->wastedSize();
421 dataDict[KEY_PROP_SEEDS] = torrent->seedsCount();
422 dataDict[KEY_PROP_SEEDS_TOTAL] = torrent->totalSeedsCount();
423 dataDict[KEY_PROP_PEERS] = torrent->leechsCount();
424 dataDict[KEY_PROP_PEERS_TOTAL] = torrent->totalLeechersCount();
425 const qreal ratio = torrent->realRatio();
426 dataDict[KEY_PROP_RATIO] = ratio > BitTorrent::Torrent::MAX_RATIO ? -1 : ratio;
427 dataDict[KEY_PROP_REANNOUNCE] = torrent->nextAnnounce();
428 dataDict[KEY_PROP_TOTAL_SIZE] = torrent->totalSize();
429 dataDict[KEY_PROP_PIECES_NUM] = torrent->piecesCount();
430 dataDict[KEY_PROP_PIECE_SIZE] = torrent->pieceLength();
431 dataDict[KEY_PROP_PIECES_HAVE] = torrent->piecesHave();
432 dataDict[KEY_PROP_CREATED_BY] = torrent->creator();
433 dataDict[KEY_PROP_ADDITION_DATE] = static_cast<double>(torrent->addedTime().toSecsSinceEpoch());
434 if (torrent->hasMetadata())
436 dataDict[KEY_PROP_LAST_SEEN] = torrent->lastSeenComplete().isValid() ? torrent->lastSeenComplete().toSecsSinceEpoch() : -1;
437 dataDict[KEY_PROP_COMPLETION_DATE] = torrent->completedTime().isValid() ? torrent->completedTime().toSecsSinceEpoch() : -1;
438 dataDict[KEY_PROP_CREATION_DATE] = static_cast<double>(torrent->creationDate().toSecsSinceEpoch());
440 else
442 dataDict[KEY_PROP_LAST_SEEN] = -1;
443 dataDict[KEY_PROP_COMPLETION_DATE] = -1;
444 dataDict[KEY_PROP_CREATION_DATE] = -1;
446 dataDict[KEY_PROP_SAVE_PATH] = torrent->savePath().toString();
447 dataDict[KEY_PROP_DOWNLOAD_PATH] = torrent->downloadPath().toString();
448 dataDict[KEY_PROP_COMMENT] = torrent->comment();
450 setResult(dataDict);
453 // Returns the trackers for a torrent in JSON format.
454 // The return value is a JSON-formatted list of dictionaries.
455 // The dictionary keys are:
456 // - "url": Tracker URL
457 // - "status": Tracker status
458 // - "tier": Tracker tier
459 // - "num_peers": Number of peers this torrent is currently connected to
460 // - "num_seeds": Number of peers that have the whole file
461 // - "num_leeches": Number of peers that are still downloading
462 // - "num_downloaded": Tracker downloaded count
463 // - "msg": Tracker message (last)
464 void TorrentsController::trackersAction()
466 requireParams({u"hash"_qs});
468 const auto id = BitTorrent::TorrentID::fromString(params()[u"hash"_qs]);
469 const BitTorrent::Torrent *const torrent = BitTorrent::Session::instance()->findTorrent(id);
470 if (!torrent)
471 throw APIError(APIErrorType::NotFound);
473 QJsonArray trackerList = getStickyTrackers(torrent);
475 for (const BitTorrent::TrackerEntry &tracker : asConst(torrent->trackers()))
477 trackerList << QJsonObject
479 {KEY_TRACKER_URL, tracker.url},
480 {KEY_TRACKER_TIER, tracker.tier},
481 {KEY_TRACKER_STATUS, static_cast<int>(tracker.status)},
482 {KEY_TRACKER_MSG, tracker.message},
483 {KEY_TRACKER_PEERS_COUNT, tracker.numPeers},
484 {KEY_TRACKER_SEEDS_COUNT, tracker.numSeeds},
485 {KEY_TRACKER_LEECHES_COUNT, tracker.numLeeches},
486 {KEY_TRACKER_DOWNLOADED_COUNT, tracker.numDownloaded}
490 setResult(trackerList);
493 // Returns the web seeds for a torrent in JSON format.
494 // The return value is a JSON-formatted list of dictionaries.
495 // The dictionary keys are:
496 // - "url": Web seed URL
497 void TorrentsController::webseedsAction()
499 requireParams({u"hash"_qs});
501 const auto id = BitTorrent::TorrentID::fromString(params()[u"hash"_qs]);
502 BitTorrent::Torrent *const torrent = BitTorrent::Session::instance()->findTorrent(id);
503 if (!torrent)
504 throw APIError(APIErrorType::NotFound);
506 QJsonArray webSeedList;
507 for (const QUrl &webseed : asConst(torrent->urlSeeds()))
509 webSeedList.append(QJsonObject
511 {KEY_WEBSEED_URL, webseed.toString()}
515 setResult(webSeedList);
518 // Returns the files in a torrent in JSON format.
519 // The return value is a JSON-formatted list of dictionaries.
520 // The dictionary keys are:
521 // - "index": File index
522 // - "name": File name
523 // - "size": File size
524 // - "progress": File progress
525 // - "priority": File priority
526 // - "is_seed": Flag indicating if torrent is seeding/complete
527 // - "piece_range": Piece index range, the first number is the starting piece index
528 // and the second number is the ending piece index (inclusive)
529 void TorrentsController::filesAction()
531 requireParams({u"hash"_qs});
533 const auto id = BitTorrent::TorrentID::fromString(params()[u"hash"_qs]);
534 const BitTorrent::Torrent *const torrent = BitTorrent::Session::instance()->findTorrent(id);
535 if (!torrent)
536 throw APIError(APIErrorType::NotFound);
538 const int filesCount = torrent->filesCount();
539 QVector<int> fileIndexes;
540 const auto idxIt = params().constFind(u"indexes"_qs);
541 if (idxIt != params().cend())
543 const QStringList indexStrings = idxIt.value().split(u'|');
544 fileIndexes.reserve(indexStrings.size());
545 std::transform(indexStrings.cbegin(), indexStrings.cend(), std::back_inserter(fileIndexes)
546 , [&filesCount](const QString &indexString) -> int
548 bool ok = false;
549 const int index = indexString.toInt(&ok);
550 if (!ok || (index < 0))
551 throw APIError(APIErrorType::Conflict, tr("\"%1\" is not a valid file index.").arg(indexString));
552 if (index >= filesCount)
553 throw APIError(APIErrorType::Conflict, tr("Index %1 is out of bounds.").arg(indexString));
554 return index;
557 else
559 fileIndexes.reserve(filesCount);
560 for (int i = 0; i < filesCount; ++i)
561 fileIndexes.append(i);
564 QJsonArray fileList;
565 if (torrent->hasMetadata())
567 const QVector<BitTorrent::DownloadPriority> priorities = torrent->filePriorities();
568 const QVector<qreal> fp = torrent->filesProgress();
569 const QVector<qreal> fileAvailability = torrent->availableFileFractions();
570 const BitTorrent::TorrentInfo info = torrent->info();
571 for (const int index : asConst(fileIndexes))
573 QJsonObject fileDict =
575 {KEY_FILE_INDEX, index},
576 {KEY_FILE_PROGRESS, fp[index]},
577 {KEY_FILE_PRIORITY, static_cast<int>(priorities[index])},
578 {KEY_FILE_SIZE, torrent->fileSize(index)},
579 {KEY_FILE_AVAILABILITY, fileAvailability[index]},
580 {KEY_FILE_NAME, torrent->filePath(index).toString()}
583 const BitTorrent::TorrentInfo::PieceRange idx = info.filePieces(index);
584 fileDict[KEY_FILE_PIECE_RANGE] = QJsonArray {idx.first(), idx.last()};
586 if (index == 0)
587 fileDict[KEY_FILE_IS_SEED] = torrent->isSeed();
589 fileList.append(fileDict);
593 setResult(fileList);
596 // Returns an array of hashes (of each pieces respectively) for a torrent in JSON format.
597 // The return value is a JSON-formatted array of strings (hex strings).
598 void TorrentsController::pieceHashesAction()
600 requireParams({u"hash"_qs});
602 const auto id = BitTorrent::TorrentID::fromString(params()[u"hash"_qs]);
603 BitTorrent::Torrent *const torrent = BitTorrent::Session::instance()->findTorrent(id);
604 if (!torrent)
605 throw APIError(APIErrorType::NotFound);
607 QJsonArray pieceHashes;
608 if (torrent->hasMetadata())
610 const QVector<QByteArray> hashes = torrent->info().pieceHashes();
611 for (const QByteArray &hash : hashes)
612 pieceHashes.append(QString::fromLatin1(hash.toHex()));
615 setResult(pieceHashes);
618 // Returns an array of states (of each pieces respectively) for a torrent in JSON format.
619 // The return value is a JSON-formatted array of ints.
620 // 0: piece not downloaded
621 // 1: piece requested or downloading
622 // 2: piece already downloaded
623 void TorrentsController::pieceStatesAction()
625 requireParams({u"hash"_qs});
627 const auto id = BitTorrent::TorrentID::fromString(params()[u"hash"_qs]);
628 BitTorrent::Torrent *const torrent = BitTorrent::Session::instance()->findTorrent(id);
629 if (!torrent)
630 throw APIError(APIErrorType::NotFound);
632 QJsonArray pieceStates;
633 const QBitArray states = torrent->pieces();
634 for (int i = 0; i < states.size(); ++i)
635 pieceStates.append(static_cast<int>(states[i]) * 2);
637 const QBitArray dlstates = torrent->downloadingPieces();
638 for (int i = 0; i < states.size(); ++i)
640 if (dlstates[i])
641 pieceStates[i] = 1;
644 setResult(pieceStates);
647 void TorrentsController::addAction()
649 const QString urls = params()[u"urls"_qs];
650 const QString cookie = params()[u"cookie"_qs];
652 const bool skipChecking = parseBool(params()[u"skip_checking"_qs]).value_or(false);
653 const bool seqDownload = parseBool(params()[u"sequentialDownload"_qs]).value_or(false);
654 const bool firstLastPiece = parseBool(params()[u"firstLastPiecePrio"_qs]).value_or(false);
655 const std::optional<bool> addPaused = parseBool(params()[u"paused"_qs]);
656 const QString savepath = params()[u"savepath"_qs].trimmed();
657 const QString downloadPath = params()[u"downloadPath"_qs].trimmed();
658 const std::optional<bool> useDownloadPath = parseBool(params()[u"useDownloadPath"_qs]);
659 const QString category = params()[u"category"_qs];
660 const QStringList tags = params()[u"tags"_qs].split(u',', Qt::SkipEmptyParts);
661 const QString torrentName = params()[u"rename"_qs].trimmed();
662 const int upLimit = parseInt(params()[u"upLimit"_qs]).value_or(-1);
663 const int dlLimit = parseInt(params()[u"dlLimit"_qs]).value_or(-1);
664 const double ratioLimit = parseDouble(params()[u"ratioLimit"_qs]).value_or(BitTorrent::Torrent::USE_GLOBAL_RATIO);
665 const int seedingTimeLimit = parseInt(params()[u"seedingTimeLimit"_qs]).value_or(BitTorrent::Torrent::USE_GLOBAL_SEEDING_TIME);
666 const std::optional<bool> autoTMM = parseBool(params()[u"autoTMM"_qs]);
668 const QString contentLayoutParam = params()[u"contentLayout"_qs];
669 const std::optional<BitTorrent::TorrentContentLayout> contentLayout = (!contentLayoutParam.isEmpty()
670 ? Utils::String::toEnum(contentLayoutParam, BitTorrent::TorrentContentLayout::Original)
671 : std::optional<BitTorrent::TorrentContentLayout> {});
673 QList<QNetworkCookie> cookies;
674 if (!cookie.isEmpty())
676 const QStringList cookiesStr = cookie.split(u"; "_qs);
677 for (QString cookieStr : cookiesStr)
679 cookieStr = cookieStr.trimmed();
680 int index = cookieStr.indexOf(u'=');
681 if (index > 1)
683 QByteArray name = cookieStr.left(index).toLatin1();
684 QByteArray value = cookieStr.right(cookieStr.length() - index - 1).toLatin1();
685 cookies += QNetworkCookie(name, value);
690 BitTorrent::AddTorrentParams addTorrentParams;
691 // TODO: Check if destination actually exists
692 addTorrentParams.skipChecking = skipChecking;
693 addTorrentParams.sequential = seqDownload;
694 addTorrentParams.firstLastPiecePriority = firstLastPiece;
695 addTorrentParams.addPaused = addPaused;
696 addTorrentParams.contentLayout = contentLayout;
697 addTorrentParams.savePath = Path(savepath);
698 addTorrentParams.downloadPath = Path(downloadPath);
699 addTorrentParams.useDownloadPath = useDownloadPath;
700 addTorrentParams.category = category;
701 addTorrentParams.tags.insert(tags.cbegin(), tags.cend());
702 addTorrentParams.name = torrentName;
703 addTorrentParams.uploadLimit = upLimit;
704 addTorrentParams.downloadLimit = dlLimit;
705 addTorrentParams.seedingTimeLimit = seedingTimeLimit;
706 addTorrentParams.ratioLimit = ratioLimit;
707 addTorrentParams.useAutoTMM = autoTMM;
709 bool partialSuccess = false;
710 for (QString url : asConst(urls.split(u'\n')))
712 url = url.trimmed();
713 if (!url.isEmpty())
715 Net::DownloadManager::instance()->setCookiesFromUrl(cookies, QUrl::fromEncoded(url.toUtf8()));
716 partialSuccess |= BitTorrent::Session::instance()->addTorrent(url, addTorrentParams);
720 const DataMap torrents = data();
721 for (auto it = torrents.constBegin(); it != torrents.constEnd(); ++it)
723 const nonstd::expected<BitTorrent::TorrentInfo, QString> result = BitTorrent::TorrentInfo::load(it.value());
724 if (!result)
726 throw APIError(APIErrorType::BadData
727 , tr("Error: '%1' is not a valid torrent file.").arg(it.key()));
730 partialSuccess |= BitTorrent::Session::instance()->addTorrent(result.value(), addTorrentParams);
733 if (partialSuccess)
734 setResult(u"Ok."_qs);
735 else
736 setResult(u"Fails."_qs);
739 void TorrentsController::addTrackersAction()
741 requireParams({u"hash"_qs, u"urls"_qs});
743 const auto id = BitTorrent::TorrentID::fromString(params()[u"hash"_qs]);
744 BitTorrent::Torrent *const torrent = BitTorrent::Session::instance()->findTorrent(id);
745 if (!torrent)
746 throw APIError(APIErrorType::NotFound);
748 QVector<BitTorrent::TrackerEntry> trackers;
749 for (const QString &urlStr : asConst(params()[u"urls"_qs].split(u'\n')))
751 const QUrl url {urlStr.trimmed()};
752 if (url.isValid())
753 trackers.append({url.toString()});
755 torrent->addTrackers(trackers);
758 void TorrentsController::editTrackerAction()
760 requireParams({u"hash"_qs, u"origUrl"_qs, u"newUrl"_qs});
762 const auto id = BitTorrent::TorrentID::fromString(params()[u"hash"_qs]);
763 const QString origUrl = params()[u"origUrl"_qs];
764 const QString newUrl = params()[u"newUrl"_qs];
766 BitTorrent::Torrent *const torrent = BitTorrent::Session::instance()->findTorrent(id);
767 if (!torrent)
768 throw APIError(APIErrorType::NotFound);
770 const QUrl origTrackerUrl {origUrl};
771 const QUrl newTrackerUrl {newUrl};
772 if (origTrackerUrl == newTrackerUrl)
773 return;
774 if (!newTrackerUrl.isValid())
775 throw APIError(APIErrorType::BadParams, u"New tracker URL is invalid"_qs);
777 QVector<BitTorrent::TrackerEntry> trackers = torrent->trackers();
778 bool match = false;
779 for (BitTorrent::TrackerEntry &tracker : trackers)
781 const QUrl trackerUrl {tracker.url};
782 if (trackerUrl == newTrackerUrl)
783 throw APIError(APIErrorType::Conflict, u"New tracker URL already exists"_qs);
784 if (trackerUrl == origTrackerUrl)
786 match = true;
787 tracker.url = newTrackerUrl.toString();
790 if (!match)
791 throw APIError(APIErrorType::Conflict, u"Tracker not found"_qs);
793 torrent->replaceTrackers(trackers);
795 if (!torrent->isPaused())
796 torrent->forceReannounce();
799 void TorrentsController::removeTrackersAction()
801 requireParams({u"hash"_qs, u"urls"_qs});
803 const auto id = BitTorrent::TorrentID::fromString(params()[u"hash"_qs]);
804 BitTorrent::Torrent *const torrent = BitTorrent::Session::instance()->findTorrent(id);
805 if (!torrent)
806 throw APIError(APIErrorType::NotFound);
808 const QStringList urls = params()[u"urls"_qs].split(u'|');
809 torrent->removeTrackers(urls);
811 if (!torrent->isPaused())
812 torrent->forceReannounce();
815 void TorrentsController::addPeersAction()
817 requireParams({u"hashes"_qs, u"peers"_qs});
819 const QStringList hashes = params()[u"hashes"_qs].split(u'|');
820 const QStringList peers = params()[u"peers"_qs].split(u'|');
822 QVector<BitTorrent::PeerAddress> peerList;
823 peerList.reserve(peers.size());
824 for (const QString &peer : peers)
826 const BitTorrent::PeerAddress addr = BitTorrent::PeerAddress::parse(peer.trimmed());
827 if (!addr.ip.isNull())
828 peerList.append(addr);
831 if (peerList.isEmpty())
832 throw APIError(APIErrorType::BadParams, u"No valid peers were specified"_qs);
834 QJsonObject results;
836 applyToTorrents(hashes, [peers, peerList, &results](BitTorrent::Torrent *const torrent)
838 const int peersAdded = std::count_if(peerList.cbegin(), peerList.cend(), [torrent](const BitTorrent::PeerAddress &peer)
840 return torrent->connectPeer(peer);
843 results[torrent->id().toString()] = QJsonObject
845 {u"added"_qs, peersAdded},
846 {u"failed"_qs, (peers.size() - peersAdded)}
850 setResult(results);
853 void TorrentsController::pauseAction()
855 requireParams({u"hashes"_qs});
857 const QStringList hashes = params()[u"hashes"_qs].split(u'|');
858 applyToTorrents(hashes, [](BitTorrent::Torrent *const torrent) { torrent->pause(); });
861 void TorrentsController::resumeAction()
863 requireParams({u"hashes"_qs});
865 const QStringList idStrings = params()[u"hashes"_qs].split(u'|');
866 applyToTorrents(idStrings, [](BitTorrent::Torrent *const torrent) { torrent->resume(); });
869 void TorrentsController::filePrioAction()
871 requireParams({u"hash"_qs, u"id"_qs, u"priority"_qs});
873 const auto id = BitTorrent::TorrentID::fromString(params()[u"hash"_qs]);
874 bool ok = false;
875 const auto priority = static_cast<BitTorrent::DownloadPriority>(params()[u"priority"_qs].toInt(&ok));
876 if (!ok)
877 throw APIError(APIErrorType::BadParams, tr("Priority must be an integer"));
879 if (!BitTorrent::isValidDownloadPriority(priority))
880 throw APIError(APIErrorType::BadParams, tr("Priority is not valid"));
882 BitTorrent::Torrent *const torrent = BitTorrent::Session::instance()->findTorrent(id);
883 if (!torrent)
884 throw APIError(APIErrorType::NotFound);
885 if (!torrent->hasMetadata())
886 throw APIError(APIErrorType::Conflict, tr("Torrent's metadata has not yet downloaded"));
888 const int filesCount = torrent->filesCount();
889 QVector<BitTorrent::DownloadPriority> priorities = torrent->filePriorities();
890 bool priorityChanged = false;
891 for (const QString &fileID : params()[u"id"_qs].split(u'|'))
893 const int id = fileID.toInt(&ok);
894 if (!ok)
895 throw APIError(APIErrorType::BadParams, tr("File IDs must be integers"));
896 if ((id < 0) || (id >= filesCount))
897 throw APIError(APIErrorType::Conflict, tr("File ID is not valid"));
899 if (priorities[id] != priority)
901 priorities[id] = priority;
902 priorityChanged = true;
906 if (priorityChanged)
907 torrent->prioritizeFiles(priorities);
910 void TorrentsController::uploadLimitAction()
912 requireParams({u"hashes"_qs});
914 const QStringList idList {params()[u"hashes"_qs].split(u'|')};
915 QJsonObject map;
916 for (const QString &id : idList)
918 int limit = -1;
919 const BitTorrent::Torrent *const torrent = BitTorrent::Session::instance()->findTorrent(BitTorrent::TorrentID::fromString(id));
920 if (torrent)
921 limit = torrent->uploadLimit();
922 map[id] = limit;
925 setResult(map);
928 void TorrentsController::downloadLimitAction()
930 requireParams({u"hashes"_qs});
932 const QStringList idList {params()[u"hashes"_qs].split(u'|')};
933 QJsonObject map;
934 for (const QString &id : idList)
936 int limit = -1;
937 const BitTorrent::Torrent *const torrent = BitTorrent::Session::instance()->findTorrent(BitTorrent::TorrentID::fromString(id));
938 if (torrent)
939 limit = torrent->downloadLimit();
940 map[id] = limit;
943 setResult(map);
946 void TorrentsController::setUploadLimitAction()
948 requireParams({u"hashes"_qs, u"limit"_qs});
950 qlonglong limit = params()[u"limit"_qs].toLongLong();
951 if (limit == 0)
952 limit = -1;
954 const QStringList hashes {params()[u"hashes"_qs].split(u'|')};
955 applyToTorrents(hashes, [limit](BitTorrent::Torrent *const torrent) { torrent->setUploadLimit(limit); });
958 void TorrentsController::setDownloadLimitAction()
960 requireParams({u"hashes"_qs, u"limit"_qs});
962 qlonglong limit = params()[u"limit"_qs].toLongLong();
963 if (limit == 0)
964 limit = -1;
966 const QStringList hashes {params()[u"hashes"_qs].split(u'|')};
967 applyToTorrents(hashes, [limit](BitTorrent::Torrent *const torrent) { torrent->setDownloadLimit(limit); });
970 void TorrentsController::setShareLimitsAction()
972 requireParams({u"hashes"_qs, u"ratioLimit"_qs, u"seedingTimeLimit"_qs});
974 const qreal ratioLimit = params()[u"ratioLimit"_qs].toDouble();
975 const qlonglong seedingTimeLimit = params()[u"seedingTimeLimit"_qs].toLongLong();
976 const QStringList hashes = params()[u"hashes"_qs].split(u'|');
978 applyToTorrents(hashes, [ratioLimit, seedingTimeLimit](BitTorrent::Torrent *const torrent)
980 torrent->setRatioLimit(ratioLimit);
981 torrent->setSeedingTimeLimit(seedingTimeLimit);
985 void TorrentsController::toggleSequentialDownloadAction()
987 requireParams({u"hashes"_qs});
989 const QStringList hashes {params()[u"hashes"_qs].split(u'|')};
990 applyToTorrents(hashes, [](BitTorrent::Torrent *const torrent) { torrent->toggleSequentialDownload(); });
993 void TorrentsController::toggleFirstLastPiecePrioAction()
995 requireParams({u"hashes"_qs});
997 const QStringList hashes {params()[u"hashes"_qs].split(u'|')};
998 applyToTorrents(hashes, [](BitTorrent::Torrent *const torrent) { torrent->toggleFirstLastPiecePriority(); });
1001 void TorrentsController::setSuperSeedingAction()
1003 requireParams({u"hashes"_qs, u"value"_qs});
1005 const bool value {parseBool(params()[u"value"_qs]).value_or(false)};
1006 const QStringList hashes {params()[u"hashes"_qs].split(u'|')};
1007 applyToTorrents(hashes, [value](BitTorrent::Torrent *const torrent) { torrent->setSuperSeeding(value); });
1010 void TorrentsController::setForceStartAction()
1012 requireParams({u"hashes"_qs, u"value"_qs});
1014 const bool value {parseBool(params()[u"value"_qs]).value_or(false)};
1015 const QStringList hashes {params()[u"hashes"_qs].split(u'|')};
1016 applyToTorrents(hashes, [value](BitTorrent::Torrent *const torrent)
1018 torrent->resume(value ? BitTorrent::TorrentOperatingMode::Forced : BitTorrent::TorrentOperatingMode::AutoManaged);
1022 void TorrentsController::deleteAction()
1024 requireParams({u"hashes"_qs, u"deleteFiles"_qs});
1026 const QStringList hashes {params()[u"hashes"_qs].split(u'|')};
1027 const DeleteOption deleteOption = parseBool(params()[u"deleteFiles"_qs]).value_or(false)
1028 ? DeleteTorrentAndFiles : DeleteTorrent;
1029 applyToTorrents(hashes, [deleteOption](const BitTorrent::Torrent *torrent)
1031 BitTorrent::Session::instance()->deleteTorrent(torrent->id(), deleteOption);
1035 void TorrentsController::increasePrioAction()
1037 requireParams({u"hashes"_qs});
1039 if (!BitTorrent::Session::instance()->isQueueingSystemEnabled())
1040 throw APIError(APIErrorType::Conflict, tr("Torrent queueing must be enabled"));
1042 const QStringList hashes {params()[u"hashes"_qs].split(u'|')};
1043 BitTorrent::Session::instance()->increaseTorrentsQueuePos(toTorrentIDs(hashes));
1046 void TorrentsController::decreasePrioAction()
1048 requireParams({u"hashes"_qs});
1050 if (!BitTorrent::Session::instance()->isQueueingSystemEnabled())
1051 throw APIError(APIErrorType::Conflict, tr("Torrent queueing must be enabled"));
1053 const QStringList hashes {params()[u"hashes"_qs].split(u'|')};
1054 BitTorrent::Session::instance()->decreaseTorrentsQueuePos(toTorrentIDs(hashes));
1057 void TorrentsController::topPrioAction()
1059 requireParams({u"hashes"_qs});
1061 if (!BitTorrent::Session::instance()->isQueueingSystemEnabled())
1062 throw APIError(APIErrorType::Conflict, tr("Torrent queueing must be enabled"));
1064 const QStringList hashes {params()[u"hashes"_qs].split(u'|')};
1065 BitTorrent::Session::instance()->topTorrentsQueuePos(toTorrentIDs(hashes));
1068 void TorrentsController::bottomPrioAction()
1070 requireParams({u"hashes"_qs});
1072 if (!BitTorrent::Session::instance()->isQueueingSystemEnabled())
1073 throw APIError(APIErrorType::Conflict, tr("Torrent queueing must be enabled"));
1075 const QStringList hashes {params()[u"hashes"_qs].split(u'|')};
1076 BitTorrent::Session::instance()->bottomTorrentsQueuePos(toTorrentIDs(hashes));
1079 void TorrentsController::setLocationAction()
1081 requireParams({u"hashes"_qs, u"location"_qs});
1083 const QStringList hashes {params()[u"hashes"_qs].split(u'|')};
1084 const Path newLocation {params()[u"location"_qs].trimmed()};
1086 if (newLocation.isEmpty())
1087 throw APIError(APIErrorType::BadParams, tr("Save path cannot be empty"));
1089 // try to create the location if it does not exist
1090 if (!Utils::Fs::mkpath(newLocation))
1091 throw APIError(APIErrorType::Conflict, tr("Cannot make save path"));
1093 // check permissions
1094 if (!Utils::Fs::isWritable(newLocation))
1095 throw APIError(APIErrorType::AccessDenied, tr("Cannot write to directory"));
1097 applyToTorrents(hashes, [newLocation](BitTorrent::Torrent *const torrent)
1099 LogMsg(tr("WebUI Set location: moving \"%1\", from \"%2\" to \"%3\"")
1100 .arg(torrent->name(), torrent->savePath().toString(), newLocation.toString()));
1101 torrent->setAutoTMMEnabled(false);
1102 torrent->setSavePath(newLocation);
1106 void TorrentsController::setSavePathAction()
1108 requireParams({u"id"_qs, u"path"_qs});
1110 const QStringList ids {params()[u"id"_qs].split(u'|')};
1111 const Path newPath {params()[u"path"_qs]};
1113 if (newPath.isEmpty())
1114 throw APIError(APIErrorType::BadParams, tr("Save path cannot be empty"));
1116 // try to create the directory if it does not exist
1117 if (!Utils::Fs::mkpath(newPath))
1118 throw APIError(APIErrorType::Conflict, tr("Cannot create target directory"));
1120 // check permissions
1121 if (!Utils::Fs::isWritable(newPath))
1122 throw APIError(APIErrorType::AccessDenied, tr("Cannot write to directory"));
1124 applyToTorrents(ids, [&newPath](BitTorrent::Torrent *const torrent)
1126 if (!torrent->isAutoTMMEnabled())
1127 torrent->setSavePath(newPath);
1131 void TorrentsController::setDownloadPathAction()
1133 requireParams({u"id"_qs, u"path"_qs});
1135 const QStringList ids {params()[u"id"_qs].split(u'|')};
1136 const Path newPath {params()[u"path"_qs]};
1138 if (!newPath.isEmpty())
1140 // try to create the directory if it does not exist
1141 if (!Utils::Fs::mkpath(newPath))
1142 throw APIError(APIErrorType::Conflict, tr("Cannot create target directory"));
1144 // check permissions
1145 if (!Utils::Fs::isWritable(newPath))
1146 throw APIError(APIErrorType::AccessDenied, tr("Cannot write to directory"));
1149 applyToTorrents(ids, [&newPath](BitTorrent::Torrent *const torrent)
1151 if (!torrent->isAutoTMMEnabled())
1152 torrent->setDownloadPath(newPath);
1156 void TorrentsController::renameAction()
1158 requireParams({u"hash"_qs, u"name"_qs});
1160 const auto id = BitTorrent::TorrentID::fromString(params()[u"hash"_qs]);
1161 QString name = params()[u"name"_qs].trimmed();
1163 if (name.isEmpty())
1164 throw APIError(APIErrorType::Conflict, tr("Incorrect torrent name"));
1166 BitTorrent::Torrent *const torrent = BitTorrent::Session::instance()->findTorrent(id);
1167 if (!torrent)
1168 throw APIError(APIErrorType::NotFound);
1170 name.replace(QRegularExpression(u"\r?\n|\r"_qs), u" "_qs);
1171 torrent->setName(name);
1174 void TorrentsController::setAutoManagementAction()
1176 requireParams({u"hashes"_qs, u"enable"_qs});
1178 const QStringList hashes {params()[u"hashes"_qs].split(u'|')};
1179 const bool isEnabled {parseBool(params()[u"enable"_qs]).value_or(false)};
1181 applyToTorrents(hashes, [isEnabled](BitTorrent::Torrent *const torrent)
1183 torrent->setAutoTMMEnabled(isEnabled);
1187 void TorrentsController::recheckAction()
1189 requireParams({u"hashes"_qs});
1191 const QStringList hashes {params()[u"hashes"_qs].split(u'|')};
1192 applyToTorrents(hashes, [](BitTorrent::Torrent *const torrent) { torrent->forceRecheck(); });
1195 void TorrentsController::reannounceAction()
1197 requireParams({u"hashes"_qs});
1199 const QStringList hashes {params()[u"hashes"_qs].split(u'|')};
1200 applyToTorrents(hashes, [](BitTorrent::Torrent *const torrent) { torrent->forceReannounce(); });
1203 void TorrentsController::setCategoryAction()
1205 requireParams({u"hashes"_qs, u"category"_qs});
1207 const QStringList hashes {params()[u"hashes"_qs].split(u'|')};
1208 const QString category {params()[u"category"_qs]};
1210 applyToTorrents(hashes, [category](BitTorrent::Torrent *const torrent)
1212 if (!torrent->setCategory(category))
1213 throw APIError(APIErrorType::Conflict, tr("Incorrect category name"));
1217 void TorrentsController::createCategoryAction()
1219 requireParams({u"category"_qs});
1221 const QString category = params()[u"category"_qs];
1222 if (category.isEmpty())
1223 throw APIError(APIErrorType::BadParams, tr("Category cannot be empty"));
1225 if (!BitTorrent::Session::isValidCategoryName(category))
1226 throw APIError(APIErrorType::Conflict, tr("Incorrect category name"));
1228 const Path savePath {params()[u"savePath"_qs]};
1229 const auto useDownloadPath = parseBool(params()[u"downloadPathEnabled"_qs]);
1230 BitTorrent::CategoryOptions categoryOptions;
1231 categoryOptions.savePath = savePath;
1232 if (useDownloadPath.has_value())
1234 const Path downloadPath {params()[u"downloadPath"_qs]};
1235 categoryOptions.downloadPath = {useDownloadPath.value(), downloadPath};
1238 if (!BitTorrent::Session::instance()->addCategory(category, categoryOptions))
1239 throw APIError(APIErrorType::Conflict, tr("Unable to create category"));
1242 void TorrentsController::editCategoryAction()
1244 requireParams({u"category"_qs, u"savePath"_qs});
1246 const QString category = params()[u"category"_qs];
1247 if (category.isEmpty())
1248 throw APIError(APIErrorType::BadParams, tr("Category cannot be empty"));
1250 const Path savePath {params()[u"savePath"_qs]};
1251 const auto useDownloadPath = parseBool(params()[u"downloadPathEnabled"_qs]);
1252 BitTorrent::CategoryOptions categoryOptions;
1253 categoryOptions.savePath = savePath;
1254 if (useDownloadPath.has_value())
1256 const Path downloadPath {params()[u"downloadPath"_qs]};
1257 categoryOptions.downloadPath = {useDownloadPath.value(), downloadPath};
1260 if (!BitTorrent::Session::instance()->editCategory(category, categoryOptions))
1261 throw APIError(APIErrorType::Conflict, tr("Unable to edit category"));
1264 void TorrentsController::removeCategoriesAction()
1266 requireParams({u"categories"_qs});
1268 const QStringList categories {params()[u"categories"_qs].split(u'\n')};
1269 for (const QString &category : categories)
1270 BitTorrent::Session::instance()->removeCategory(category);
1273 void TorrentsController::categoriesAction()
1275 const auto session = BitTorrent::Session::instance();
1277 QJsonObject categories;
1278 const QStringList categoriesList = session->categories();
1279 for (const auto &categoryName : categoriesList)
1281 const BitTorrent::CategoryOptions categoryOptions = session->categoryOptions(categoryName);
1282 QJsonObject category = categoryOptions.toJSON();
1283 // adjust it to be compatible with exisitng WebAPI
1284 category[u"savePath"_qs] = category.take(u"save_path"_qs);
1285 category.insert(u"name"_qs, categoryName);
1286 categories[categoryName] = category;
1289 setResult(categories);
1292 void TorrentsController::addTagsAction()
1294 requireParams({u"hashes"_qs, u"tags"_qs});
1296 const QStringList hashes {params()[u"hashes"_qs].split(u'|')};
1297 const QStringList tags {params()[u"tags"_qs].split(u',', Qt::SkipEmptyParts)};
1299 for (const QString &tag : tags)
1301 const QString tagTrimmed {tag.trimmed()};
1302 applyToTorrents(hashes, [&tagTrimmed](BitTorrent::Torrent *const torrent)
1304 torrent->addTag(tagTrimmed);
1309 void TorrentsController::removeTagsAction()
1311 requireParams({u"hashes"_qs});
1313 const QStringList hashes {params()[u"hashes"_qs].split(u'|')};
1314 const QStringList tags {params()[u"tags"_qs].split(u',', Qt::SkipEmptyParts)};
1316 for (const QString &tag : tags)
1318 const QString tagTrimmed {tag.trimmed()};
1319 applyToTorrents(hashes, [&tagTrimmed](BitTorrent::Torrent *const torrent)
1321 torrent->removeTag(tagTrimmed);
1325 if (tags.isEmpty())
1327 applyToTorrents(hashes, [](BitTorrent::Torrent *const torrent)
1329 torrent->removeAllTags();
1334 void TorrentsController::createTagsAction()
1336 requireParams({u"tags"_qs});
1338 const QStringList tags {params()[u"tags"_qs].split(u',', Qt::SkipEmptyParts)};
1340 for (const QString &tag : tags)
1341 BitTorrent::Session::instance()->addTag(tag.trimmed());
1344 void TorrentsController::deleteTagsAction()
1346 requireParams({u"tags"_qs});
1348 const QStringList tags {params()[u"tags"_qs].split(u',', Qt::SkipEmptyParts)};
1349 for (const QString &tag : tags)
1350 BitTorrent::Session::instance()->removeTag(tag.trimmed());
1353 void TorrentsController::tagsAction()
1355 QJsonArray result;
1356 for (const QString &tag : asConst(BitTorrent::Session::instance()->tags()))
1357 result << tag;
1358 setResult(result);
1361 void TorrentsController::renameFileAction()
1363 requireParams({u"hash"_qs, u"oldPath"_qs, u"newPath"_qs});
1365 const auto id = BitTorrent::TorrentID::fromString(params()[u"hash"_qs]);
1366 BitTorrent::Torrent *const torrent = BitTorrent::Session::instance()->findTorrent(id);
1367 if (!torrent)
1368 throw APIError(APIErrorType::NotFound);
1370 const Path oldPath {params()[u"oldPath"_qs]};
1371 const Path newPath {params()[u"newPath"_qs]};
1375 torrent->renameFile(oldPath, newPath);
1377 catch (const RuntimeError &error)
1379 throw APIError(APIErrorType::Conflict, error.message());
1383 void TorrentsController::renameFolderAction()
1385 requireParams({u"hash"_qs, u"oldPath"_qs, u"newPath"_qs});
1387 const auto id = BitTorrent::TorrentID::fromString(params()[u"hash"_qs]);
1388 BitTorrent::Torrent *const torrent = BitTorrent::Session::instance()->findTorrent(id);
1389 if (!torrent)
1390 throw APIError(APIErrorType::NotFound);
1392 const Path oldPath {params()[u"oldPath"_qs]};
1393 const Path newPath {params()[u"newPath"_qs]};
1397 torrent->renameFolder(oldPath, newPath);
1399 catch (const RuntimeError &error)
1401 throw APIError(APIErrorType::Conflict, error.message());
1405 void TorrentsController::exportAction()
1407 requireParams({u"hash"_qs});
1409 const auto id = BitTorrent::TorrentID::fromString(params()[u"hash"_qs]);
1410 const BitTorrent::Torrent *const torrent = BitTorrent::Session::instance()->findTorrent(id);
1411 if (!torrent)
1412 throw APIError(APIErrorType::NotFound);
1414 const nonstd::expected<QByteArray, QString> result = torrent->exportToBuffer();
1415 if (!result)
1416 throw APIError(APIErrorType::Conflict, tr("Unable to export torrent file. Error: %1").arg(result.error()));
1418 setResult(result.value());