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