qBittorrent
torrentscontroller.cpp
Go to the documentation of this file.
1 /*
2  * Bittorrent Client using Qt and libtorrent.
3  * Copyright (C) 2018 Vladimir Golovnev <glassez@yandex.ru>
4  *
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.
9  *
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.
14  *
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.
18  *
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.
27  */
28 
29 #include "torrentscontroller.h"
30 
31 #include <functional>
32 
33 #include <QBitArray>
34 #include <QDir>
35 #include <QJsonArray>
36 #include <QJsonObject>
37 #include <QList>
38 #include <QNetworkCookie>
39 #include <QRegularExpression>
40 #include <QUrl>
41 
51 #include "base/global.h"
52 #include "base/logger.h"
54 #include "base/torrentfilter.h"
55 #include "base/utils/fs.h"
56 #include "base/utils/string.h"
57 #include "apierror.h"
59 
60 // Tracker keys
61 const char KEY_TRACKER_URL[] = "url";
62 const char KEY_TRACKER_STATUS[] = "status";
63 const char KEY_TRACKER_TIER[] = "tier";
64 const char KEY_TRACKER_MSG[] = "msg";
65 const char KEY_TRACKER_PEERS_COUNT[] = "num_peers";
66 const char KEY_TRACKER_SEEDS_COUNT[] = "num_seeds";
67 const char KEY_TRACKER_LEECHES_COUNT[] = "num_leeches";
68 const char KEY_TRACKER_DOWNLOADED_COUNT[] = "num_downloaded";
69 
70 // Web seed keys
71 const char KEY_WEBSEED_URL[] = "url";
72 
73 // Torrent keys (Properties)
74 const char KEY_PROP_TIME_ELAPSED[] = "time_elapsed";
75 const char KEY_PROP_SEEDING_TIME[] = "seeding_time";
76 const char KEY_PROP_ETA[] = "eta";
77 const char KEY_PROP_CONNECT_COUNT[] = "nb_connections";
78 const char KEY_PROP_CONNECT_COUNT_LIMIT[] = "nb_connections_limit";
79 const char KEY_PROP_DOWNLOADED[] = "total_downloaded";
80 const char KEY_PROP_DOWNLOADED_SESSION[] = "total_downloaded_session";
81 const char KEY_PROP_UPLOADED[] = "total_uploaded";
82 const char KEY_PROP_UPLOADED_SESSION[] = "total_uploaded_session";
83 const char KEY_PROP_DL_SPEED[] = "dl_speed";
84 const char KEY_PROP_DL_SPEED_AVG[] = "dl_speed_avg";
85 const char KEY_PROP_UP_SPEED[] = "up_speed";
86 const char KEY_PROP_UP_SPEED_AVG[] = "up_speed_avg";
87 const char KEY_PROP_DL_LIMIT[] = "dl_limit";
88 const char KEY_PROP_UP_LIMIT[] = "up_limit";
89 const char KEY_PROP_WASTED[] = "total_wasted";
90 const char KEY_PROP_SEEDS[] = "seeds";
91 const char KEY_PROP_SEEDS_TOTAL[] = "seeds_total";
92 const char KEY_PROP_PEERS[] = "peers";
93 const char KEY_PROP_PEERS_TOTAL[] = "peers_total";
94 const char KEY_PROP_RATIO[] = "share_ratio";
95 const char KEY_PROP_REANNOUNCE[] = "reannounce";
96 const char KEY_PROP_TOTAL_SIZE[] = "total_size";
97 const char KEY_PROP_PIECES_NUM[] = "pieces_num";
98 const char KEY_PROP_PIECE_SIZE[] = "piece_size";
99 const char KEY_PROP_PIECES_HAVE[] = "pieces_have";
100 const char KEY_PROP_CREATED_BY[] = "created_by";
101 const char KEY_PROP_LAST_SEEN[] = "last_seen";
102 const char KEY_PROP_ADDITION_DATE[] = "addition_date";
103 const char KEY_PROP_COMPLETION_DATE[] = "completion_date";
104 const char KEY_PROP_CREATION_DATE[] = "creation_date";
105 const char KEY_PROP_SAVE_PATH[] = "save_path";
106 const char KEY_PROP_DOWNLOAD_PATH[] = "download_path";
107 const char KEY_PROP_COMMENT[] = "comment";
108 
109 // File keys
110 const char KEY_FILE_INDEX[] = "index";
111 const char KEY_FILE_NAME[] = "name";
112 const char KEY_FILE_SIZE[] = "size";
113 const char KEY_FILE_PROGRESS[] = "progress";
114 const char KEY_FILE_PRIORITY[] = "priority";
115 const char KEY_FILE_IS_SEED[] = "is_seed";
116 const char KEY_FILE_PIECE_RANGE[] = "piece_range";
117 const char KEY_FILE_AVAILABILITY[] = "availability";
118 
119 namespace
120 {
124 
125  void applyToTorrents(const QStringList &idList, const std::function<void (BitTorrent::Torrent *torrent)> &func)
126  {
127  if ((idList.size() == 1) && (idList[0] == QLatin1String("all")))
128  {
129  for (BitTorrent::Torrent *const torrent : asConst(BitTorrent::Session::instance()->torrents()))
130  func(torrent);
131  }
132  else
133  {
134  for (const QString &idString : idList)
135  {
136  const auto hash = BitTorrent::TorrentID::fromString(idString);
138  if (torrent)
139  func(torrent);
140  }
141  }
142  }
143 
144  QJsonArray getStickyTrackers(const BitTorrent::Torrent *const torrent)
145  {
146  int seedsDHT = 0, seedsPeX = 0, seedsLSD = 0, leechesDHT = 0, leechesPeX = 0, leechesLSD = 0;
147  for (const BitTorrent::PeerInfo &peer : asConst(torrent->peers()))
148  {
149  if (peer.isConnecting()) continue;
150 
151  if (peer.isSeed())
152  {
153  if (peer.fromDHT())
154  ++seedsDHT;
155  if (peer.fromPeX())
156  ++seedsPeX;
157  if (peer.fromLSD())
158  ++seedsLSD;
159  }
160  else
161  {
162  if (peer.fromDHT())
163  ++leechesDHT;
164  if (peer.fromPeX())
165  ++leechesPeX;
166  if (peer.fromLSD())
167  ++leechesLSD;
168  }
169  }
170 
171  const int working = static_cast<int>(BitTorrent::TrackerEntry::Working);
172  const int disabled = 0;
173 
174  const QString privateMsg {QCoreApplication::translate("TrackerListWidget", "This torrent is private")};
175  const bool isTorrentPrivate = torrent->isPrivate();
176 
177  const QJsonObject dht
178  {
179  {KEY_TRACKER_URL, "** [DHT] **"},
180  {KEY_TRACKER_TIER, -1},
181  {KEY_TRACKER_MSG, (isTorrentPrivate ? privateMsg : "")},
182  {KEY_TRACKER_STATUS, ((BitTorrent::Session::instance()->isDHTEnabled() && !isTorrentPrivate) ? working : disabled)},
185  {KEY_TRACKER_SEEDS_COUNT, seedsDHT},
186  {KEY_TRACKER_LEECHES_COUNT, leechesDHT}
187  };
188 
189  const QJsonObject pex
190  {
191  {KEY_TRACKER_URL, "** [PeX] **"},
192  {KEY_TRACKER_TIER, -1},
193  {KEY_TRACKER_MSG, (isTorrentPrivate ? privateMsg : "")},
194  {KEY_TRACKER_STATUS, ((BitTorrent::Session::instance()->isPeXEnabled() && !isTorrentPrivate) ? working : disabled)},
197  {KEY_TRACKER_SEEDS_COUNT, seedsPeX},
198  {KEY_TRACKER_LEECHES_COUNT, leechesPeX}
199  };
200 
201  const QJsonObject lsd
202  {
203  {KEY_TRACKER_URL, "** [LSD] **"},
204  {KEY_TRACKER_TIER, -1},
205  {KEY_TRACKER_MSG, (isTorrentPrivate ? privateMsg : "")},
206  {KEY_TRACKER_STATUS, ((BitTorrent::Session::instance()->isLSDEnabled() && !isTorrentPrivate) ? working : disabled)},
209  {KEY_TRACKER_SEEDS_COUNT, seedsLSD},
210  {KEY_TRACKER_LEECHES_COUNT, leechesLSD}
211  };
212 
213  return {dht, pex, lsd};
214  }
215 
216  QVector<BitTorrent::TorrentID> toTorrentIDs(const QStringList &idStrings)
217  {
218  QVector<BitTorrent::TorrentID> idList;
219  idList.reserve(idStrings.size());
220  for (const QString &hash : idStrings)
221  idList << BitTorrent::TorrentID::fromString(hash);
222  return idList;
223  }
224 }
225 
226 // Returns all the torrents in JSON format.
227 // The return value is a JSON-formatted list of dictionaries.
228 // The dictionary keys are:
229 // - "hash": Torrent hash (ID)
230 // - "name": Torrent name
231 // - "size": Torrent size
232 // - "progress": Torrent progress
233 // - "dlspeed": Torrent download speed
234 // - "upspeed": Torrent upload speed
235 // - "priority": Torrent queue position (-1 if queuing is disabled)
236 // - "num_seeds": Torrent seeds connected to
237 // - "num_complete": Torrent seeds in the swarm
238 // - "num_leechs": Torrent leechers connected to
239 // - "num_incomplete": Torrent leechers in the swarm
240 // - "ratio": Torrent share ratio
241 // - "eta": Torrent ETA
242 // - "state": Torrent state
243 // - "seq_dl": Torrent sequential download state
244 // - "f_l_piece_prio": Torrent first last piece priority state
245 // - "force_start": Torrent force start state
246 // - "category": Torrent category
247 // GET params:
248 // - filter (string): all, downloading, seeding, completed, paused, resumed, active, inactive, stalled, stalled_uploading, stalled_downloading
249 // - category (string): torrent category for filtering by it (empty string means "uncategorized"; no "category" param presented means "any category")
250 // - tag (string): torrent tag for filtering by it (empty string means "untagged"; no "tag" param presented means "any tag")
251 // - hashes (string): filter by hashes, can contain multiple hashes separated by |
252 // - sort (string): name of column for sorting by its value
253 // - reverse (bool): enable reverse sorting
254 // - limit (int): set limit number of torrents returned (if greater than 0, otherwise - unlimited)
255 // - offset (int): set offset (if less than 0 - offset from end)
257 {
258  const QString filter {params()["filter"]};
259  const QString category {params()["category"]};
260  const QString tag {params()["tag"]};
261  const QString sortedColumn {params()["sort"]};
262  const bool reverse {parseBool(params()["reverse"]).value_or(false)};
263  int limit {params()["limit"].toInt()};
264  int offset {params()["offset"].toInt()};
265  const QStringList hashes {params()["hashes"].split('|', Qt::SkipEmptyParts)};
266 
267  TorrentIDSet idSet;
268  for (const QString &hash : hashes)
269  idSet.insert(BitTorrent::TorrentID::fromString(hash));
270 
271  const TorrentFilter torrentFilter(filter, (hashes.isEmpty() ? TorrentFilter::AnyID : idSet), category, tag);
272  QVariantList torrentList;
273  for (const BitTorrent::Torrent *torrent : asConst(BitTorrent::Session::instance()->torrents()))
274  {
275  if (torrentFilter.match(torrent))
276  torrentList.append(serialize(*torrent));
277  }
278 
279  if (torrentList.isEmpty())
280  {
281  setResult(QJsonArray {});
282  return;
283  }
284 
285  if (!sortedColumn.isEmpty())
286  {
287  if (!torrentList[0].toMap().contains(sortedColumn))
288  throw APIError(APIErrorType::BadParams, tr("'sort' parameter is invalid"));
289 
290  const auto lessThan = [](const QVariant &left, const QVariant &right) -> bool
291  {
292  Q_ASSERT(left.type() == right.type());
293 
294  switch (static_cast<QMetaType::Type>(left.type()))
295  {
296  case QMetaType::Bool:
297  return left.value<bool>() < right.value<bool>();
298  case QMetaType::Double:
299  return left.value<double>() < right.value<double>();
300  case QMetaType::Float:
301  return left.value<float>() < right.value<float>();
302  case QMetaType::Int:
303  return left.value<int>() < right.value<int>();
304  case QMetaType::LongLong:
305  return left.value<qlonglong>() < right.value<qlonglong>();
306  case QMetaType::QString:
307  return left.value<QString>() < right.value<QString>();
308  default:
309  qWarning("Unhandled QVariant comparison, type: %d, name: %s", left.type()
310  , QMetaType::typeName(left.type()));
311  break;
312  }
313  return false;
314  };
315 
316  std::sort(torrentList.begin(), torrentList.end()
317  , [reverse, &sortedColumn, &lessThan](const QVariant &torrent1, const QVariant &torrent2)
318  {
319  const QVariant value1 {torrent1.toMap().value(sortedColumn)};
320  const QVariant value2 {torrent2.toMap().value(sortedColumn)};
321  return reverse ? lessThan(value2, value1) : lessThan(value1, value2);
322  });
323  }
324 
325  const int size = torrentList.size();
326  // normalize offset
327  if (offset < 0)
328  offset = size + offset;
329  if ((offset >= size) || (offset < 0))
330  offset = 0;
331  // normalize limit
332  if (limit <= 0)
333  limit = -1; // unlimited
334 
335  if ((limit > 0) || (offset > 0))
336  torrentList = torrentList.mid(offset, limit);
337 
338  setResult(QJsonArray::fromVariantList(torrentList));
339 }
340 
341 // Returns the properties for a torrent in JSON format.
342 // The return value is a JSON-formatted dictionary.
343 // The dictionary keys are:
344 // - "time_elapsed": Torrent elapsed time
345 // - "seeding_time": Torrent elapsed time while complete
346 // - "eta": Torrent ETA
347 // - "nb_connections": Torrent connection count
348 // - "nb_connections_limit": Torrent connection count limit
349 // - "total_downloaded": Total data uploaded for torrent
350 // - "total_downloaded_session": Total data downloaded this session
351 // - "total_uploaded": Total data uploaded for torrent
352 // - "total_uploaded_session": Total data uploaded this session
353 // - "dl_speed": Torrent download speed
354 // - "dl_speed_avg": Torrent average download speed
355 // - "up_speed": Torrent upload speed
356 // - "up_speed_avg": Torrent average upload speed
357 // - "dl_limit": Torrent download limit
358 // - "up_limit": Torrent upload limit
359 // - "total_wasted": Total data wasted for torrent
360 // - "seeds": Torrent connected seeds
361 // - "seeds_total": Torrent total number of seeds
362 // - "peers": Torrent connected peers
363 // - "peers_total": Torrent total number of peers
364 // - "share_ratio": Torrent share ratio
365 // - "reannounce": Torrent next reannounce time
366 // - "total_size": Torrent total size
367 // - "pieces_num": Torrent pieces count
368 // - "piece_size": Torrent piece size
369 // - "pieces_have": Torrent pieces have
370 // - "created_by": Torrent creator
371 // - "last_seen": Torrent last seen complete
372 // - "addition_date": Torrent addition date
373 // - "completion_date": Torrent completion date
374 // - "creation_date": Torrent creation date
375 // - "save_path": Torrent save path
376 // - "download_path": Torrent download path
377 // - "comment": Torrent comment
379 {
380  requireParams({"hash"});
381 
382  const auto id = BitTorrent::TorrentID::fromString(params()["hash"]);
384  if (!torrent)
386 
387  QJsonObject dataDict;
388 
389  dataDict[KEY_TORRENT_INFOHASHV1] = torrent->infoHash().v1().toString();
390  dataDict[KEY_TORRENT_INFOHASHV2] = torrent->infoHash().v2().toString();
391  dataDict[KEY_PROP_TIME_ELAPSED] = torrent->activeTime();
392  dataDict[KEY_PROP_SEEDING_TIME] = torrent->finishedTime();
393  dataDict[KEY_PROP_ETA] = static_cast<double>(torrent->eta());
394  dataDict[KEY_PROP_CONNECT_COUNT] = torrent->connectionsCount();
395  dataDict[KEY_PROP_CONNECT_COUNT_LIMIT] = torrent->connectionsLimit();
396  dataDict[KEY_PROP_DOWNLOADED] = torrent->totalDownload();
397  dataDict[KEY_PROP_DOWNLOADED_SESSION] = torrent->totalPayloadDownload();
398  dataDict[KEY_PROP_UPLOADED] = torrent->totalUpload();
399  dataDict[KEY_PROP_UPLOADED_SESSION] = torrent->totalPayloadUpload();
400  dataDict[KEY_PROP_DL_SPEED] = torrent->downloadPayloadRate();
401  const qlonglong dlDuration = torrent->activeTime() - torrent->finishedTime();
402  dataDict[KEY_PROP_DL_SPEED_AVG] = torrent->totalDownload() / ((dlDuration == 0) ? -1 : dlDuration);
403  dataDict[KEY_PROP_UP_SPEED] = torrent->uploadPayloadRate();
404  const qlonglong ulDuration = torrent->activeTime();
405  dataDict[KEY_PROP_UP_SPEED_AVG] = torrent->totalUpload() / ((ulDuration == 0) ? -1 : ulDuration);
406  dataDict[KEY_PROP_DL_LIMIT] = torrent->downloadLimit() <= 0 ? -1 : torrent->downloadLimit();
407  dataDict[KEY_PROP_UP_LIMIT] = torrent->uploadLimit() <= 0 ? -1 : torrent->uploadLimit();
408  dataDict[KEY_PROP_WASTED] = torrent->wastedSize();
409  dataDict[KEY_PROP_SEEDS] = torrent->seedsCount();
410  dataDict[KEY_PROP_SEEDS_TOTAL] = torrent->totalSeedsCount();
411  dataDict[KEY_PROP_PEERS] = torrent->leechsCount();
412  dataDict[KEY_PROP_PEERS_TOTAL] = torrent->totalLeechersCount();
413  const qreal ratio = torrent->realRatio();
414  dataDict[KEY_PROP_RATIO] = ratio > BitTorrent::Torrent::MAX_RATIO ? -1 : ratio;
415  dataDict[KEY_PROP_REANNOUNCE] = torrent->nextAnnounce();
416  dataDict[KEY_PROP_TOTAL_SIZE] = torrent->totalSize();
417  dataDict[KEY_PROP_PIECES_NUM] = torrent->piecesCount();
418  dataDict[KEY_PROP_PIECE_SIZE] = torrent->pieceLength();
419  dataDict[KEY_PROP_PIECES_HAVE] = torrent->piecesHave();
420  dataDict[KEY_PROP_CREATED_BY] = torrent->creator();
421  dataDict[KEY_PROP_ADDITION_DATE] = static_cast<double>(torrent->addedTime().toSecsSinceEpoch());
422  if (torrent->hasMetadata())
423  {
424  dataDict[KEY_PROP_LAST_SEEN] = torrent->lastSeenComplete().isValid() ? torrent->lastSeenComplete().toSecsSinceEpoch() : -1;
425  dataDict[KEY_PROP_COMPLETION_DATE] = torrent->completedTime().isValid() ? torrent->completedTime().toSecsSinceEpoch() : -1;
426  dataDict[KEY_PROP_CREATION_DATE] = static_cast<double>(torrent->creationDate().toSecsSinceEpoch());
427  }
428  else
429  {
430  dataDict[KEY_PROP_LAST_SEEN] = -1;
431  dataDict[KEY_PROP_COMPLETION_DATE] = -1;
432  dataDict[KEY_PROP_CREATION_DATE] = -1;
433  }
434  dataDict[KEY_PROP_SAVE_PATH] = Utils::Fs::toNativePath(torrent->savePath());
436  dataDict[KEY_PROP_COMMENT] = torrent->comment();
437 
438  setResult(dataDict);
439 }
440 
441 // Returns the trackers for a torrent in JSON format.
442 // The return value is a JSON-formatted list of dictionaries.
443 // The dictionary keys are:
444 // - "url": Tracker URL
445 // - "status": Tracker status
446 // - "tier": Tracker tier
447 // - "num_peers": Number of peers this torrent is currently connected to
448 // - "num_seeds": Number of peers that have the whole file
449 // - "num_leeches": Number of peers that are still downloading
450 // - "num_downloaded": Tracker downloaded count
451 // - "msg": Tracker message (last)
453 {
454  requireParams({"hash"});
455 
456  const auto id = BitTorrent::TorrentID::fromString(params()["hash"]);
457  const BitTorrent::Torrent *const torrent = BitTorrent::Session::instance()->findTorrent(id);
458  if (!torrent)
460 
461  QJsonArray trackerList = getStickyTrackers(torrent);
462 
463  for (const BitTorrent::TrackerEntry &tracker : asConst(torrent->trackers()))
464  {
465  trackerList << QJsonObject
466  {
467  {KEY_TRACKER_URL, tracker.url},
468  {KEY_TRACKER_TIER, tracker.tier},
469  {KEY_TRACKER_STATUS, static_cast<int>(tracker.status)},
470  {KEY_TRACKER_MSG, tracker.message},
471  {KEY_TRACKER_PEERS_COUNT, tracker.numPeers},
472  {KEY_TRACKER_SEEDS_COUNT, tracker.numSeeds},
473  {KEY_TRACKER_LEECHES_COUNT, tracker.numLeeches},
474  {KEY_TRACKER_DOWNLOADED_COUNT, tracker.numDownloaded}
475  };
476  }
477 
478  setResult(trackerList);
479 }
480 
481 // Returns the web seeds for a torrent in JSON format.
482 // The return value is a JSON-formatted list of dictionaries.
483 // The dictionary keys are:
484 // - "url": Web seed URL
486 {
487  requireParams({"hash"});
488 
489  const auto id = BitTorrent::TorrentID::fromString(params()["hash"]);
491  if (!torrent)
493 
494  QJsonArray webSeedList;
495  for (const QUrl &webseed : asConst(torrent->urlSeeds()))
496  {
497  webSeedList.append(QJsonObject
498  {
499  {KEY_WEBSEED_URL, webseed.toString()}
500  });
501  }
502 
503  setResult(webSeedList);
504 }
505 
506 // Returns the files in a torrent in JSON format.
507 // The return value is a JSON-formatted list of dictionaries.
508 // The dictionary keys are:
509 // - "index": File index
510 // - "name": File name
511 // - "size": File size
512 // - "progress": File progress
513 // - "priority": File priority
514 // - "is_seed": Flag indicating if torrent is seeding/complete
515 // - "piece_range": Piece index range, the first number is the starting piece index
516 // and the second number is the ending piece index (inclusive)
518 {
519  requireParams({"hash"});
520 
521  const auto id = BitTorrent::TorrentID::fromString(params()["hash"]);
522  const BitTorrent::Torrent *const torrent = BitTorrent::Session::instance()->findTorrent(id);
523  if (!torrent)
525 
526  const int filesCount = torrent->filesCount();
527  QVector<int> fileIndexes;
528  const auto idxIt = params().constFind(QLatin1String("indexes"));
529  if (idxIt != params().cend())
530  {
531  const QStringList indexStrings = idxIt.value().split('|');
532  fileIndexes.reserve(indexStrings.size());
533  std::transform(indexStrings.cbegin(), indexStrings.cend(), std::back_inserter(fileIndexes)
534  , [&filesCount](const QString &indexString) -> int
535  {
536  bool ok = false;
537  const int index = indexString.toInt(&ok);
538  if (!ok || (index < 0))
539  throw APIError(APIErrorType::Conflict, tr("\"%1\" is not a valid file index.").arg(indexString));
540  if (index >= filesCount)
541  throw APIError(APIErrorType::Conflict, tr("Index %1 is out of bounds.").arg(indexString));
542  return index;
543  });
544  }
545  else
546  {
547  fileIndexes.reserve(filesCount);
548  for (int i = 0; i < filesCount; ++i)
549  fileIndexes.append(i);
550  }
551 
552  QJsonArray fileList;
553  if (torrent->hasMetadata())
554  {
555  const QVector<BitTorrent::DownloadPriority> priorities = torrent->filePriorities();
556  const QVector<qreal> fp = torrent->filesProgress();
557  const QVector<qreal> fileAvailability = torrent->availableFileFractions();
558  const BitTorrent::TorrentInfo info = torrent->info();
559  for (const int index : asConst(fileIndexes))
560  {
561  QJsonObject fileDict =
562  {
563  {KEY_FILE_INDEX, index},
564  {KEY_FILE_PROGRESS, fp[index]},
565  {KEY_FILE_PRIORITY, static_cast<int>(priorities[index])},
566  {KEY_FILE_SIZE, torrent->fileSize(index)},
567  {KEY_FILE_AVAILABILITY, fileAvailability[index]},
569  };
570 
571  const BitTorrent::TorrentInfo::PieceRange idx = info.filePieces(index);
572  fileDict[KEY_FILE_PIECE_RANGE] = QJsonArray {idx.first(), idx.last()};
573 
574  if (index == 0)
575  fileDict[KEY_FILE_IS_SEED] = torrent->isSeed();
576 
577  fileList.append(fileDict);
578  }
579  }
580 
581  setResult(fileList);
582 }
583 
584 // Returns an array of hashes (of each pieces respectively) for a torrent in JSON format.
585 // The return value is a JSON-formatted array of strings (hex strings).
587 {
588  requireParams({"hash"});
589 
590  const auto id = BitTorrent::TorrentID::fromString(params()["hash"]);
592  if (!torrent)
594 
595  QJsonArray pieceHashes;
596  if (torrent->hasMetadata())
597  {
598  const QVector<QByteArray> hashes = torrent->info().pieceHashes();
599  for (const QByteArray &hash : hashes)
600  pieceHashes.append(QString(hash.toHex()));
601  }
602 
603  setResult(pieceHashes);
604 }
605 
606 // Returns an array of states (of each pieces respectively) for a torrent in JSON format.
607 // The return value is a JSON-formatted array of ints.
608 // 0: piece not downloaded
609 // 1: piece requested or downloading
610 // 2: piece already downloaded
612 {
613  requireParams({"hash"});
614 
615  const auto id = BitTorrent::TorrentID::fromString(params()["hash"]);
617  if (!torrent)
619 
620  QJsonArray pieceStates;
621  const QBitArray states = torrent->pieces();
622  for (int i = 0; i < states.size(); ++i)
623  pieceStates.append(static_cast<int>(states[i]) * 2);
624 
625  const QBitArray dlstates = torrent->downloadingPieces();
626  for (int i = 0; i < states.size(); ++i)
627  {
628  if (dlstates[i])
629  pieceStates[i] = 1;
630  }
631 
632  setResult(pieceStates);
633 }
634 
636 {
637  const QString urls = params()["urls"];
638  const QString cookie = params()["cookie"];
639 
640  const bool skipChecking = parseBool(params()["skip_checking"]).value_or(false);
641  const bool seqDownload = parseBool(params()["sequentialDownload"]).value_or(false);
642  const bool firstLastPiece = parseBool(params()["firstLastPiecePrio"]).value_or(false);
643  const std::optional<bool> addPaused = parseBool(params()["paused"]);
644  const QString savepath = params()["savepath"].trimmed();
645  const QString downloadPath = params()["downloadPath"].trimmed();
646  const std::optional<bool> useDownloadPath = parseBool(params()["useDownloadPath"]);
647  const QString category = params()["category"];
648  const QStringList tags = params()["tags"].split(',', Qt::SkipEmptyParts);
649  const QString torrentName = params()["rename"].trimmed();
650  const int upLimit = parseInt(params()["upLimit"]).value_or(-1);
651  const int dlLimit = parseInt(params()["dlLimit"]).value_or(-1);
652  const double ratioLimit = parseDouble(params()["ratioLimit"]).value_or(BitTorrent::Torrent::USE_GLOBAL_RATIO);
653  const int seedingTimeLimit = parseInt(params()["seedingTimeLimit"]).value_or(BitTorrent::Torrent::USE_GLOBAL_SEEDING_TIME);
654  const std::optional<bool> autoTMM = parseBool(params()["autoTMM"]);
655 
656  const QString contentLayoutParam = params()["contentLayout"];
657  const std::optional<BitTorrent::TorrentContentLayout> contentLayout = (!contentLayoutParam.isEmpty()
658  ? Utils::String::toEnum(contentLayoutParam, BitTorrent::TorrentContentLayout::Original)
659  : std::optional<BitTorrent::TorrentContentLayout> {});
660 
661  QList<QNetworkCookie> cookies;
662  if (!cookie.isEmpty())
663  {
664  const QStringList cookiesStr = cookie.split("; ");
665  for (QString cookieStr : cookiesStr)
666  {
667  cookieStr = cookieStr.trimmed();
668  int index = cookieStr.indexOf('=');
669  if (index > 1)
670  {
671  QByteArray name = cookieStr.left(index).toLatin1();
672  QByteArray value = cookieStr.right(cookieStr.length() - index - 1).toLatin1();
673  cookies += QNetworkCookie(name, value);
674  }
675  }
676  }
677 
678  BitTorrent::AddTorrentParams addTorrentParams;
679  // TODO: Check if destination actually exists
680  addTorrentParams.skipChecking = skipChecking;
681  addTorrentParams.sequential = seqDownload;
682  addTorrentParams.firstLastPiecePriority = firstLastPiece;
683  addTorrentParams.addPaused = addPaused;
684  addTorrentParams.contentLayout = contentLayout;
685  addTorrentParams.savePath = savepath;
686  addTorrentParams.downloadPath = downloadPath;
687  addTorrentParams.useDownloadPath = useDownloadPath;
688  addTorrentParams.category = category;
689  addTorrentParams.tags.insert(tags.cbegin(), tags.cend());
690  addTorrentParams.name = torrentName;
691  addTorrentParams.uploadLimit = upLimit;
692  addTorrentParams.downloadLimit = dlLimit;
693  addTorrentParams.seedingTimeLimit = seedingTimeLimit;
694  addTorrentParams.ratioLimit = ratioLimit;
695  addTorrentParams.useAutoTMM = autoTMM;
696 
697  bool partialSuccess = false;
698  for (QString url : asConst(urls.split('\n')))
699  {
700  url = url.trimmed();
701  if (!url.isEmpty())
702  {
703  Net::DownloadManager::instance()->setCookiesFromUrl(cookies, QUrl::fromEncoded(url.toUtf8()));
704  partialSuccess |= BitTorrent::Session::instance()->addTorrent(url, addTorrentParams);
705  }
706  }
707 
708  for (auto it = data().constBegin(); it != data().constEnd(); ++it)
709  {
710  const nonstd::expected<BitTorrent::TorrentInfo, QString> result = BitTorrent::TorrentInfo::load(it.value());
711  if (!result)
712  {
714  , tr("Error: '%1' is not a valid torrent file.").arg(it.key()));
715  }
716 
717  partialSuccess |= BitTorrent::Session::instance()->addTorrent(result.value(), addTorrentParams);
718  }
719 
720  if (partialSuccess)
721  setResult(QLatin1String("Ok."));
722  else
723  setResult(QLatin1String("Fails."));
724 }
725 
727 {
728  requireParams({"hash", "urls"});
729 
730  const auto id = BitTorrent::TorrentID::fromString(params()["hash"]);
732  if (!torrent)
734 
735  QVector<BitTorrent::TrackerEntry> trackers;
736  for (const QString &urlStr : asConst(params()["urls"].split('\n')))
737  {
738  const QUrl url {urlStr.trimmed()};
739  if (url.isValid())
740  trackers.append({url.toString()});
741  }
742  torrent->addTrackers(trackers);
743 }
744 
746 {
747  requireParams({"hash", "origUrl", "newUrl"});
748 
749  const auto id = BitTorrent::TorrentID::fromString(params()["hash"]);
750  const QString origUrl = params()["origUrl"];
751  const QString newUrl = params()["newUrl"];
752 
754  if (!torrent)
756 
757  const QUrl origTrackerUrl(origUrl);
758  const QUrl newTrackerUrl(newUrl);
759  if (origTrackerUrl == newTrackerUrl)
760  return;
761  if (!newTrackerUrl.isValid())
762  throw APIError(APIErrorType::BadParams, "New tracker URL is invalid");
763 
764  QVector<BitTorrent::TrackerEntry> trackers = torrent->trackers();
765  bool match = false;
766  for (BitTorrent::TrackerEntry &tracker : trackers)
767  {
768  const QUrl trackerUrl(tracker.url);
769  if (trackerUrl == newTrackerUrl)
770  throw APIError(APIErrorType::Conflict, "New tracker URL already exists");
771  if (trackerUrl == origTrackerUrl)
772  {
773  match = true;
774  tracker.url = newTrackerUrl.toString();
775  }
776  }
777  if (!match)
778  throw APIError(APIErrorType::Conflict, "Tracker not found");
779 
780  torrent->replaceTrackers(trackers);
781 
782  if (!torrent->isPaused())
783  torrent->forceReannounce();
784 }
785 
787 {
788  requireParams({"hash", "urls"});
789 
790  const auto id = BitTorrent::TorrentID::fromString(params()["hash"]);
792  if (!torrent)
794 
795  const QStringList urls = params()["urls"].split('|');
796 
797  const QVector<BitTorrent::TrackerEntry> trackers = torrent->trackers();
798  QVector<BitTorrent::TrackerEntry> remainingTrackers;
799  remainingTrackers.reserve(trackers.size());
800  for (const BitTorrent::TrackerEntry &entry : trackers)
801  {
802  if (!urls.contains(entry.url))
803  remainingTrackers.push_back(entry);
804  }
805 
806  if (remainingTrackers.size() == trackers.size())
807  throw APIError(APIErrorType::Conflict, "No trackers were removed");
808 
809  torrent->replaceTrackers(remainingTrackers);
810 
811  if (!torrent->isPaused())
812  torrent->forceReannounce();
813 }
814 
816 {
817  requireParams({"hashes", "peers"});
818 
819  const QStringList hashes = params()["hashes"].split('|');
820  const QStringList peers = params()["peers"].split('|');
821 
822  QVector<BitTorrent::PeerAddress> peerList;
823  peerList.reserve(peers.size());
824  for (const QString &peer : peers)
825  {
826  const BitTorrent::PeerAddress addr = BitTorrent::PeerAddress::parse(peer.trimmed());
827  if (!addr.ip.isNull())
828  peerList.append(addr);
829  }
830 
831  if (peerList.isEmpty())
832  throw APIError(APIErrorType::BadParams, "No valid peers were specified");
833 
834  QJsonObject results;
835 
836  applyToTorrents(hashes, [peers, peerList, &results](BitTorrent::Torrent *const torrent)
837  {
838  const int peersAdded = std::count_if(peerList.cbegin(), peerList.cend(), [torrent](const BitTorrent::PeerAddress &peer)
839  {
840  return torrent->connectPeer(peer);
841  });
842 
843  results[torrent->id().toString()] = QJsonObject
844  {
845  {"added", peersAdded},
846  {"failed", (peers.size() - peersAdded)}
847  };
848  });
849 
850  setResult(results);
851 }
852 
854 {
855  requireParams({"hashes"});
856 
857  const QStringList hashes = params()["hashes"].split('|');
858  applyToTorrents(hashes, [](BitTorrent::Torrent *const torrent) { torrent->pause(); });
859 }
860 
862 {
863  requireParams({"hashes"});
864 
865  const QStringList idStrings = params()["hashes"].split('|');
866  applyToTorrents(idStrings, [](BitTorrent::Torrent *const torrent) { torrent->resume(); });
867 }
868 
870 {
871  requireParams({"hash", "id", "priority"});
872 
873  const auto id = BitTorrent::TorrentID::fromString(params()["hash"]);
874  bool ok = false;
875  const auto priority = static_cast<BitTorrent::DownloadPriority>(params()["priority"].toInt(&ok));
876  if (!ok)
877  throw APIError(APIErrorType::BadParams, tr("Priority must be an integer"));
878 
880  throw APIError(APIErrorType::BadParams, tr("Priority is not valid"));
881 
883  if (!torrent)
885  if (!torrent->hasMetadata())
886  throw APIError(APIErrorType::Conflict, tr("Torrent's metadata has not yet downloaded"));
887 
888  const int filesCount = torrent->filesCount();
889  QVector<BitTorrent::DownloadPriority> priorities = torrent->filePriorities();
890  bool priorityChanged = false;
891  for (const QString &fileID : params()["id"].split('|'))
892  {
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"));
898 
899  if (priorities[id] != priority)
900  {
901  priorities[id] = priority;
902  priorityChanged = true;
903  }
904  }
905 
906  if (priorityChanged)
907  torrent->prioritizeFiles(priorities);
908 }
909 
911 {
912  requireParams({"hashes"});
913 
914  const QStringList idList {params()["hashes"].split('|')};
915  QJsonObject map;
916  for (const QString &id : idList)
917  {
918  int limit = -1;
920  if (torrent)
921  limit = torrent->uploadLimit();
922  map[id] = limit;
923  }
924 
925  setResult(map);
926 }
927 
929 {
930  requireParams({"hashes"});
931 
932  const QStringList idList {params()["hashes"].split('|')};
933  QJsonObject map;
934  for (const QString &id : idList)
935  {
936  int limit = -1;
938  if (torrent)
939  limit = torrent->downloadLimit();
940  map[id] = limit;
941  }
942 
943  setResult(map);
944 }
945 
947 {
948  requireParams({"hashes", "limit"});
949 
950  qlonglong limit = params()["limit"].toLongLong();
951  if (limit == 0)
952  limit = -1;
953 
954  const QStringList hashes {params()["hashes"].split('|')};
955  applyToTorrents(hashes, [limit](BitTorrent::Torrent *const torrent) { torrent->setUploadLimit(limit); });
956 }
957 
959 {
960  requireParams({"hashes", "limit"});
961 
962  qlonglong limit = params()["limit"].toLongLong();
963  if (limit == 0)
964  limit = -1;
965 
966  const QStringList hashes {params()["hashes"].split('|')};
967  applyToTorrents(hashes, [limit](BitTorrent::Torrent *const torrent) { torrent->setDownloadLimit(limit); });
968 }
969 
971 {
972  requireParams({"hashes", "ratioLimit", "seedingTimeLimit"});
973 
974  const qreal ratioLimit = params()["ratioLimit"].toDouble();
975  const qlonglong seedingTimeLimit = params()["seedingTimeLimit"].toLongLong();
976  const QStringList hashes = params()["hashes"].split('|');
977 
978  applyToTorrents(hashes, [ratioLimit, seedingTimeLimit](BitTorrent::Torrent *const torrent)
979  {
980  torrent->setRatioLimit(ratioLimit);
981  torrent->setSeedingTimeLimit(seedingTimeLimit);
982  });
983 }
984 
986 {
987  requireParams({"hashes"});
988 
989  const QStringList hashes {params()["hashes"].split('|')};
990  applyToTorrents(hashes, [](BitTorrent::Torrent *const torrent) { torrent->toggleSequentialDownload(); });
991 }
992 
994 {
995  requireParams({"hashes"});
996 
997  const QStringList hashes {params()["hashes"].split('|')};
998  applyToTorrents(hashes, [](BitTorrent::Torrent *const torrent) { torrent->toggleFirstLastPiecePriority(); });
999 }
1000 
1002 {
1003  requireParams({"hashes", "value"});
1004 
1005  const bool value {parseBool(params()["value"]).value_or(false)};
1006  const QStringList hashes {params()["hashes"].split('|')};
1007  applyToTorrents(hashes, [value](BitTorrent::Torrent *const torrent) { torrent->setSuperSeeding(value); });
1008 }
1009 
1011 {
1012  requireParams({"hashes", "value"});
1013 
1014  const bool value {parseBool(params()["value"]).value_or(false)};
1015  const QStringList hashes {params()["hashes"].split('|')};
1016  applyToTorrents(hashes, [value](BitTorrent::Torrent *const torrent)
1017  {
1018  torrent->resume(value ? BitTorrent::TorrentOperatingMode::Forced : BitTorrent::TorrentOperatingMode::AutoManaged);
1019  });
1020 }
1021 
1023 {
1024  requireParams({"hashes", "deleteFiles"});
1025 
1026  const QStringList hashes {params()["hashes"].split('|')};
1027  const DeleteOption deleteOption = parseBool(params()["deleteFiles"]).value_or(false)
1029  applyToTorrents(hashes, [deleteOption](const BitTorrent::Torrent *torrent)
1030  {
1031  BitTorrent::Session::instance()->deleteTorrent(torrent->id(), deleteOption);
1032  });
1033 }
1034 
1036 {
1037  requireParams({"hashes"});
1038 
1039  if (!BitTorrent::Session::instance()->isQueueingSystemEnabled())
1040  throw APIError(APIErrorType::Conflict, tr("Torrent queueing must be enabled"));
1041 
1042  const QStringList hashes {params()["hashes"].split('|')};
1044 }
1045 
1047 {
1048  requireParams({"hashes"});
1049 
1050  if (!BitTorrent::Session::instance()->isQueueingSystemEnabled())
1051  throw APIError(APIErrorType::Conflict, tr("Torrent queueing must be enabled"));
1052 
1053  const QStringList hashes {params()["hashes"].split('|')};
1055 }
1056 
1058 {
1059  requireParams({"hashes"});
1060 
1061  if (!BitTorrent::Session::instance()->isQueueingSystemEnabled())
1062  throw APIError(APIErrorType::Conflict, tr("Torrent queueing must be enabled"));
1063 
1064  const QStringList hashes {params()["hashes"].split('|')};
1066 }
1067 
1069 {
1070  requireParams({"hashes"});
1071 
1072  if (!BitTorrent::Session::instance()->isQueueingSystemEnabled())
1073  throw APIError(APIErrorType::Conflict, tr("Torrent queueing must be enabled"));
1074 
1075  const QStringList hashes {params()["hashes"].split('|')};
1077 }
1078 
1080 {
1081  requireParams({"hashes", "location"});
1082 
1083  const QStringList hashes {params()["hashes"].split('|')};
1084  const QString newLocation {params()["location"].trimmed()};
1085 
1086  if (newLocation.isEmpty())
1087  throw APIError(APIErrorType::BadParams, tr("Save path cannot be empty"));
1088 
1089  // try to create the location if it does not exist
1090  if (!QDir(newLocation).mkpath("."))
1091  throw APIError(APIErrorType::Conflict, tr("Cannot make save path"));
1092 
1093  // check permissions
1094  if (!QFileInfo(newLocation).isWritable())
1095  throw APIError(APIErrorType::AccessDenied, tr("Cannot write to directory"));
1096 
1097  applyToTorrents(hashes, [newLocation](BitTorrent::Torrent *const torrent)
1098  {
1099  LogMsg(tr("WebUI Set location: moving \"%1\", from \"%2\" to \"%3\"")
1100  .arg(torrent->name(), Utils::Fs::toNativePath(torrent->savePath()), Utils::Fs::toNativePath(newLocation)));
1101  torrent->setAutoTMMEnabled(false);
1102  torrent->setSavePath(Utils::Fs::expandPathAbs(newLocation));
1103  });
1104 }
1105 
1107 {
1108  requireParams({"id", "path"});
1109 
1110  const QStringList ids {params()["id"].split('|')};
1111  const QString newPath {params()["path"]};
1112 
1113  if (newPath.isEmpty())
1114  throw APIError(APIErrorType::BadParams, tr("Save path cannot be empty"));
1115 
1116  // try to create the directory if it does not exist
1117  if (!QDir(newPath).mkpath("."))
1118  throw APIError(APIErrorType::Conflict, tr("Cannot create target directory"));
1119 
1120  // check permissions
1121  if (!QFileInfo(newPath).isWritable())
1122  throw APIError(APIErrorType::AccessDenied, tr("Cannot write to directory"));
1123 
1124  applyToTorrents(ids, [&newPath](BitTorrent::Torrent *const torrent)
1125  {
1126  if (!torrent->isAutoTMMEnabled())
1127  torrent->setSavePath(newPath);
1128  });
1129 }
1130 
1132 {
1133  requireParams({"id", "path"});
1134 
1135  const QStringList ids {params()["id"].split('|')};
1136  const QString newPath {params()["path"]};
1137 
1138  if (!newPath.isEmpty())
1139  {
1140  // try to create the directory if it does not exist
1141  if (!QDir(newPath).mkpath("."))
1142  throw APIError(APIErrorType::Conflict, tr("Cannot create target directory"));
1143 
1144  // check permissions
1145  if (!QFileInfo(newPath).isWritable())
1146  throw APIError(APIErrorType::AccessDenied, tr("Cannot write to directory"));
1147  }
1148 
1149  applyToTorrents(ids, [&newPath](BitTorrent::Torrent *const torrent)
1150  {
1151  if (!torrent->isAutoTMMEnabled())
1152  torrent->setDownloadPath(newPath);
1153  });
1154 }
1155 
1157 {
1158  requireParams({"hash", "name"});
1159 
1160  const auto id = BitTorrent::TorrentID::fromString(params()["hash"]);
1161  QString name = params()["name"].trimmed();
1162 
1163  if (name.isEmpty())
1164  throw APIError(APIErrorType::Conflict, tr("Incorrect torrent name"));
1165 
1167  if (!torrent)
1169 
1170  name.replace(QRegularExpression("\r?\n|\r"), " ");
1171  torrent->setName(name);
1172 }
1173 
1175 {
1176  requireParams({"hashes", "enable"});
1177 
1178  const QStringList hashes {params()["hashes"].split('|')};
1179  const bool isEnabled {parseBool(params()["enable"]).value_or(false)};
1180 
1181  applyToTorrents(hashes, [isEnabled](BitTorrent::Torrent *const torrent)
1182  {
1183  torrent->setAutoTMMEnabled(isEnabled);
1184  });
1185 }
1186 
1188 {
1189  requireParams({"hashes"});
1190 
1191  const QStringList hashes {params()["hashes"].split('|')};
1192  applyToTorrents(hashes, [](BitTorrent::Torrent *const torrent) { torrent->forceRecheck(); });
1193 }
1194 
1196 {
1197  requireParams({"hashes"});
1198 
1199  const QStringList hashes {params()["hashes"].split('|')};
1200  applyToTorrents(hashes, [](BitTorrent::Torrent *const torrent) { torrent->forceReannounce(); });
1201 }
1202 
1204 {
1205  requireParams({"hashes", "category"});
1206 
1207  const QStringList hashes {params()["hashes"].split('|')};
1208  const QString category {params()["category"]};
1209 
1210  applyToTorrents(hashes, [category](BitTorrent::Torrent *const torrent)
1211  {
1212  if (!torrent->setCategory(category))
1213  throw APIError(APIErrorType::Conflict, tr("Incorrect category name"));
1214  });
1215 }
1216 
1218 {
1219  requireParams({"category"});
1220 
1221  const QString category = params()["category"];
1222  if (category.isEmpty())
1223  throw APIError(APIErrorType::BadParams, tr("Category cannot be empty"));
1224 
1226  throw APIError(APIErrorType::Conflict, tr("Incorrect category name"));
1227 
1228  const QString savePath = params()["savePath"];
1229  const auto useDownloadPath = parseBool(params()["downloadPathEnabled"]);
1230  BitTorrent::CategoryOptions categoryOptions;
1231  categoryOptions.savePath = savePath;
1232  if (useDownloadPath.has_value())
1233  {
1234  const QString downloadPath = params()["downloadPath"];
1235  categoryOptions.downloadPath = {useDownloadPath.value(), downloadPath};
1236  }
1237 
1238  if (!BitTorrent::Session::instance()->addCategory(category, categoryOptions))
1239  throw APIError(APIErrorType::Conflict, tr("Unable to create category"));
1240 }
1241 
1243 {
1244  requireParams({"category", "savePath"});
1245 
1246  const QString category = params()["category"];
1247  if (category.isEmpty())
1248  throw APIError(APIErrorType::BadParams, tr("Category cannot be empty"));
1249 
1250  const QString savePath = params()["savePath"];
1251  const auto useDownloadPath = parseBool(params()["downloadPathEnabled"]);
1252  BitTorrent::CategoryOptions categoryOptions;
1253  categoryOptions.savePath = savePath;
1254  if (useDownloadPath.has_value())
1255  {
1256  const QString downloadPath = params()["downloadPath"];
1257  categoryOptions.downloadPath = {useDownloadPath.value(), downloadPath};
1258  }
1259 
1260  if (!BitTorrent::Session::instance()->editCategory(category, categoryOptions))
1261  throw APIError(APIErrorType::Conflict, tr("Unable to edit category"));
1262 }
1263 
1265 {
1266  requireParams({"categories"});
1267 
1268  const QStringList categories {params()["categories"].split('\n')};
1269  for (const QString &category : categories)
1271 }
1272 
1274 {
1275  const auto session = BitTorrent::Session::instance();
1276 
1277  QJsonObject categories;
1278  const QStringList categoriesList = session->categories();
1279  for (const auto &categoryName : categoriesList)
1280  {
1281  const BitTorrent::CategoryOptions categoryOptions = session->categoryOptions(categoryName);
1282  QJsonObject category = categoryOptions.toJSON();
1283  category.insert(QLatin1String("name"), categoryName);
1284  categories[categoryName] = category;
1285  }
1286 
1287  setResult(categories);
1288 }
1289 
1291 {
1292  requireParams({"hashes", "tags"});
1293 
1294  const QStringList hashes {params()["hashes"].split('|')};
1295  const QStringList tags {params()["tags"].split(',', Qt::SkipEmptyParts)};
1296 
1297  for (const QString &tag : tags)
1298  {
1299  const QString tagTrimmed {tag.trimmed()};
1300  applyToTorrents(hashes, [&tagTrimmed](BitTorrent::Torrent *const torrent)
1301  {
1302  torrent->addTag(tagTrimmed);
1303  });
1304  }
1305 }
1306 
1308 {
1309  requireParams({"hashes"});
1310 
1311  const QStringList hashes {params()["hashes"].split('|')};
1312  const QStringList tags {params()["tags"].split(',', Qt::SkipEmptyParts)};
1313 
1314  for (const QString &tag : tags)
1315  {
1316  const QString tagTrimmed {tag.trimmed()};
1317  applyToTorrents(hashes, [&tagTrimmed](BitTorrent::Torrent *const torrent)
1318  {
1319  torrent->removeTag(tagTrimmed);
1320  });
1321  }
1322 
1323  if (tags.isEmpty())
1324  {
1325  applyToTorrents(hashes, [](BitTorrent::Torrent *const torrent)
1326  {
1327  torrent->removeAllTags();
1328  });
1329  }
1330 }
1331 
1333 {
1334  requireParams({"tags"});
1335 
1336  const QStringList tags {params()["tags"].split(',', Qt::SkipEmptyParts)};
1337 
1338  for (const QString &tag : tags)
1339  BitTorrent::Session::instance()->addTag(tag.trimmed());
1340 }
1341 
1343 {
1344  requireParams({"tags"});
1345 
1346  const QStringList tags {params()["tags"].split(',', Qt::SkipEmptyParts)};
1347  for (const QString &tag : tags)
1348  BitTorrent::Session::instance()->removeTag(tag.trimmed());
1349 }
1350 
1352 {
1353  QJsonArray result;
1354  for (const QString &tag : asConst(BitTorrent::Session::instance()->tags()))
1355  result << tag;
1356  setResult(result);
1357 }
1358 
1360 {
1361  requireParams({"hash", "oldPath", "newPath"});
1362 
1363  const auto id = BitTorrent::TorrentID::fromString(params()["hash"]);
1365  if (!torrent)
1367 
1368  const QString oldPath = params()["oldPath"];
1369  const QString newPath = params()["newPath"];
1370 
1371  try
1372  {
1373  torrent->renameFile(oldPath, newPath);
1374  }
1375  catch (const RuntimeError &error)
1376  {
1377  throw APIError(APIErrorType::Conflict, error.message());
1378  }
1379 }
1380 
1382 {
1383  requireParams({"hash", "oldPath", "newPath"});
1384 
1385  const auto id = BitTorrent::TorrentID::fromString(params()["hash"]);
1387  if (!torrent)
1389 
1390  const QString oldPath = params()["oldPath"];
1391  const QString newPath = params()["newPath"];
1392 
1393  try
1394  {
1395  torrent->renameFolder(oldPath, newPath);
1396  }
1397  catch (const RuntimeError &error)
1398  {
1399  throw APIError(APIErrorType::Conflict, error.message());
1400  }
1401 }
void requireParams(const QVector< QString > &requiredParams) const
const StringMap & params() const
const DataMap & data() const
void setResult(const QString &result)
void renameFolder(const QString &oldPath, const QString &newPath)
virtual void renameFile(int index, const QString &name)=0
virtual int filesCount() const =0
virtual qlonglong fileSize(int index) const =0
virtual QString filePath(int index) const =0
SHA1Hash v1() const
Definition: infohash.cpp:44
SHA256Hash v2() const
Definition: infohash.cpp:53
bool removeTag(const QString &tag)
Definition: session.cpp:850
static Session * instance()
Definition: session.cpp:997
bool isDHTEnabled() const
Definition: session.cpp:498
void decreaseTorrentsQueuePos(const QVector< TorrentID > &ids)
Definition: session.cpp:1889
static bool isValidCategoryName(const QString &name)
Definition: session.cpp:630
bool removeCategory(const QString &name)
Definition: session.cpp:760
void bottomTorrentsQueuePos(const QVector< TorrentID > &ids)
Definition: session.cpp:1942
bool isPeXEnabled() const
Definition: session.cpp:529
bool addTag(const QString &tag)
Definition: session.cpp:839
bool deleteTorrent(const TorrentID &id, DeleteOption deleteOption=DeleteTorrent)
Definition: session.cpp:1792
bool isLSDEnabled() const
Definition: session.cpp:513
bool addTorrent(const QString &source, const AddTorrentParams &params=AddTorrentParams())
Definition: session.cpp:2007
void topTorrentsQueuePos(const QVector< TorrentID > &ids)
Definition: session.cpp:1917
void increaseTorrentsQueuePos(const QVector< TorrentID > &ids)
Definition: session.cpp:1862
Torrent * findTorrent(const TorrentID &id) const
Definition: session.cpp:1742
virtual qlonglong totalSize() const =0
virtual QVector< PeerInfo > peers() const =0
virtual int uploadPayloadRate() const =0
virtual void forceRecheck()=0
virtual int seedsCount() const =0
virtual int downloadLimit() const =0
virtual void setSuperSeeding(bool enable)=0
TorrentID id() const
Definition: torrent.cpp:54
virtual qlonglong nextAnnounce() const =0
virtual bool removeTag(const QString &tag)=0
virtual QVector< DownloadPriority > filePriorities() const =0
virtual void setAutoTMMEnabled(bool enabled)=0
virtual void forceReannounce(int index=-1)=0
virtual void setDownloadPath(const QString &downloadPath)=0
static const int USE_GLOBAL_SEEDING_TIME
Definition: torrent.h:107
virtual QVector< qreal > filesProgress() const =0
virtual qlonglong totalUpload() const =0
virtual int piecesHave() const =0
virtual QDateTime creationDate() const =0
void toggleFirstLastPiecePriority()
Definition: torrent.cpp:74
virtual QString comment() const =0
virtual qlonglong pieceLength() const =0
virtual void setUploadLimit(int limit)=0
virtual qlonglong activeTime() const =0
virtual qlonglong totalPayloadDownload() const =0
virtual void setSeedingTimeLimit(int limit)=0
virtual InfoHash infoHash() const =0
virtual void prioritizeFiles(const QVector< DownloadPriority > &priorities)=0
virtual qlonglong finishedTime() const =0
virtual int piecesCount() const =0
virtual int uploadLimit() const =0
void toggleSequentialDownload()
Definition: torrent.cpp:69
virtual int connectionsLimit() const =0
virtual int totalSeedsCount() const =0
virtual qlonglong totalDownload() const =0
virtual void resume(TorrentOperatingMode mode=TorrentOperatingMode::AutoManaged)=0
virtual void setSavePath(const QString &savePath)=0
virtual QString creator() const =0
virtual QString savePath() const =0
virtual qreal realRatio() const =0
virtual void removeAllTags()=0
virtual QString downloadPath() const =0
virtual QBitArray downloadingPieces() const =0
virtual bool isPaused() const =0
virtual QVector< TrackerEntry > trackers() const =0
virtual bool setCategory(const QString &category)=0
virtual QDateTime addedTime() const =0
virtual bool isAutoTMMEnabled() const =0
virtual void setName(const QString &name)=0
virtual qlonglong totalPayloadUpload() const =0
virtual void addTrackers(const QVector< TrackerEntry > &trackers)=0
virtual void replaceTrackers(const QVector< TrackerEntry > &trackers)=0
virtual QBitArray pieces() const =0
virtual QVector< qreal > availableFileFractions() const =0
fraction of file pieces that are available at least from one peer
virtual int connectionsCount() const =0
virtual QDateTime completedTime() const =0
virtual bool isPrivate() const =0
virtual QVector< QUrl > urlSeeds() const =0
virtual TorrentInfo info() const =0
static const qreal USE_GLOBAL_RATIO
Definition: torrent.h:104
virtual qlonglong wastedSize() const =0
virtual bool addTag(const QString &tag)=0
virtual QDateTime lastSeenComplete() const =0
virtual void setDownloadLimit(int limit)=0
virtual int leechsCount() const =0
virtual bool isSeed() const =0
virtual qlonglong eta() const =0
static const qreal MAX_RATIO
Definition: torrent.h:110
virtual void pause()=0
virtual int downloadPayloadRate() const =0
virtual int totalLeechersCount() const =0
virtual void setRatioLimit(qreal limit)=0
virtual bool hasMetadata() const =0
virtual QString name() const =0
static TorrentID fromString(const QString &hashString)
Definition: infohash.cpp:76
static nonstd::expected< TorrentInfo, QString > load(const QByteArray &data) noexcept
Definition: torrentinfo.cpp:89
PieceRange filePieces(const QString &file) const
QVector< QByteArray > pieceHashes() const
QString toString() const
Definition: digest32.h:85
QString message() const noexcept
Definition: exceptions.cpp:36
constexpr IndexType last() const
Definition: indexrange.h:156
constexpr IndexType first() const
Definition: indexrange.h:151
bool setCookiesFromUrl(const QList< QNetworkCookie > &cookieList, const QUrl &url)
static DownloadManager * instance()
static const TorrentIDSet AnyID
Definition: torrentfilter.h:65
bool match(const BitTorrent::Torrent *torrent) const
constexpr std::add_const_t< T > & asConst(T &t) noexcept
Definition: global.h:42
void LogMsg(const QString &message, const Log::MsgType &type)
Definition: logger.cpp:125
bool isValidDownloadPriority(const DownloadPriority priority)
QString toUniformPath(const QString &path)
Definition: fs.cpp:69
QString expandPathAbs(const QString &path)
Definition: fs.cpp:309
QString toNativePath(const QString &path)
Definition: fs.cpp:64
std::optional< bool > parseBool(const QString &string)
Definition: string.cpp:72
std::optional< int > parseInt(const QString &string)
Definition: string.cpp:82
T toEnum(const QString &serializedValue, const T &defaultValue)
Definition: string.h:77
std::optional< double > parseDouble(const QString &string)
Definition: string.cpp:92
T value(const QString &key, const T &defaultValue={})
Definition: preferences.cpp:64
void applyToTorrents(const QStringList &idList, const std::function< void(BitTorrent::Torrent *torrent)> &func)
QVector< BitTorrent::TorrentID > toTorrentIDs(const QStringList &idStrings)
QJsonArray getStickyTrackers(const BitTorrent::Torrent *const torrent)
QVariantMap serialize(const BitTorrent::Torrent &torrent)
const char KEY_TORRENT_INFOHASHV1[]
const char KEY_TORRENT_INFOHASHV2[]
DeleteOption
Definition: session.h:80
@ DeleteTorrent
Definition: session.h:81
@ DeleteTorrentAndFiles
Definition: session.h:82
std::optional< bool > useDownloadPath
std::optional< bool > useAutoTMM
std::optional< bool > addPaused
std::optional< BitTorrent::TorrentContentLayout > contentLayout
QJsonObject toJSON() const
std::optional< DownloadPathOption > downloadPath
static PeerAddress parse(QStringView address)
Definition: peeraddress.cpp:35
Definition: trackerentry.h:38
@ Working
Definition: trackerentry.h:42
QSet< BitTorrent::TorrentID > TorrentIDSet
Definition: torrentfilter.h:41
const char KEY_PROP_DOWNLOADED_SESSION[]
const char KEY_PROP_DL_LIMIT[]
const char KEY_PROP_PEERS_TOTAL[]
const char KEY_PROP_SAVE_PATH[]
const char KEY_FILE_NAME[]
const char KEY_PROP_DL_SPEED_AVG[]
const char KEY_FILE_SIZE[]
const char KEY_PROP_COMPLETION_DATE[]
const char KEY_PROP_UP_SPEED_AVG[]
const char KEY_FILE_IS_SEED[]
const char KEY_PROP_TOTAL_SIZE[]
const char KEY_PROP_PEERS[]
const char KEY_PROP_DL_SPEED[]
const char KEY_TRACKER_DOWNLOADED_COUNT[]
const char KEY_FILE_INDEX[]
const char KEY_PROP_UP_SPEED[]
const char KEY_PROP_PIECE_SIZE[]
const char KEY_TRACKER_LEECHES_COUNT[]
const char KEY_FILE_PIECE_RANGE[]
const char KEY_PROP_CONNECT_COUNT[]
const char KEY_PROP_UPLOADED_SESSION[]
const char KEY_PROP_UP_LIMIT[]
const char KEY_PROP_SEEDS_TOTAL[]
const char KEY_TRACKER_SEEDS_COUNT[]
const char KEY_PROP_PIECES_HAVE[]
const char KEY_FILE_PRIORITY[]
const char KEY_PROP_TIME_ELAPSED[]
const char KEY_PROP_COMMENT[]
const char KEY_TRACKER_PEERS_COUNT[]
const char KEY_TRACKER_MSG[]
const char KEY_PROP_UPLOADED[]
const char KEY_PROP_CREATION_DATE[]
const char KEY_PROP_ETA[]
const char KEY_FILE_AVAILABILITY[]
const char KEY_PROP_CREATED_BY[]
const char KEY_PROP_CONNECT_COUNT_LIMIT[]
const char KEY_FILE_PROGRESS[]
const char KEY_PROP_ADDITION_DATE[]
const char KEY_TRACKER_URL[]
const char KEY_TRACKER_TIER[]
const char KEY_PROP_DOWNLOAD_PATH[]
const char KEY_TRACKER_STATUS[]
const char KEY_PROP_DOWNLOADED[]
const char KEY_PROP_PIECES_NUM[]
const char KEY_PROP_WASTED[]
const char KEY_PROP_LAST_SEEN[]
const char KEY_PROP_SEEDING_TIME[]
const char KEY_WEBSEED_URL[]
const char KEY_PROP_SEEDS[]
const char KEY_PROP_REANNOUNCE[]
const char KEY_PROP_RATIO[]