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"
36 #include <QJsonObject>
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/torrentfilter.h"
57 #include "base/utils/datetime.h"
58 #include "base/utils/fs.h"
59 #include "base/utils/sslkey.h"
60 #include "base/utils/string.h"
62 #include "serialize/serialize_torrent.h"
65 const QString KEY_TRACKER_URL
= u
"url"_s
;
66 const QString KEY_TRACKER_STATUS
= u
"status"_s
;
67 const QString KEY_TRACKER_TIER
= u
"tier"_s
;
68 const QString KEY_TRACKER_MSG
= u
"msg"_s
;
69 const QString KEY_TRACKER_PEERS_COUNT
= u
"num_peers"_s
;
70 const QString KEY_TRACKER_SEEDS_COUNT
= u
"num_seeds"_s
;
71 const QString KEY_TRACKER_LEECHES_COUNT
= u
"num_leeches"_s
;
72 const QString KEY_TRACKER_DOWNLOADED_COUNT
= u
"num_downloaded"_s
;
75 const QString KEY_WEBSEED_URL
= u
"url"_s
;
77 // Torrent keys (Properties)
78 const QString KEY_PROP_TIME_ELAPSED
= u
"time_elapsed"_s
;
79 const QString KEY_PROP_SEEDING_TIME
= u
"seeding_time"_s
;
80 const QString KEY_PROP_ETA
= u
"eta"_s
;
81 const QString KEY_PROP_CONNECT_COUNT
= u
"nb_connections"_s
;
82 const QString KEY_PROP_CONNECT_COUNT_LIMIT
= u
"nb_connections_limit"_s
;
83 const QString KEY_PROP_DOWNLOADED
= u
"total_downloaded"_s
;
84 const QString KEY_PROP_DOWNLOADED_SESSION
= u
"total_downloaded_session"_s
;
85 const QString KEY_PROP_UPLOADED
= u
"total_uploaded"_s
;
86 const QString KEY_PROP_UPLOADED_SESSION
= u
"total_uploaded_session"_s
;
87 const QString KEY_PROP_DL_SPEED
= u
"dl_speed"_s
;
88 const QString KEY_PROP_DL_SPEED_AVG
= u
"dl_speed_avg"_s
;
89 const QString KEY_PROP_UP_SPEED
= u
"up_speed"_s
;
90 const QString KEY_PROP_UP_SPEED_AVG
= u
"up_speed_avg"_s
;
91 const QString KEY_PROP_DL_LIMIT
= u
"dl_limit"_s
;
92 const QString KEY_PROP_UP_LIMIT
= u
"up_limit"_s
;
93 const QString KEY_PROP_WASTED
= u
"total_wasted"_s
;
94 const QString KEY_PROP_SEEDS
= u
"seeds"_s
;
95 const QString KEY_PROP_SEEDS_TOTAL
= u
"seeds_total"_s
;
96 const QString KEY_PROP_PEERS
= u
"peers"_s
;
97 const QString KEY_PROP_PEERS_TOTAL
= u
"peers_total"_s
;
98 const QString KEY_PROP_RATIO
= u
"share_ratio"_s
;
99 const QString KEY_PROP_POPULARITY
= u
"popularity"_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_IS_PRIVATE
= u
"is_private"_s
; // deprecated, "private" should be used instead
114 const QString KEY_PROP_PRIVATE
= u
"private"_s
;
115 const QString KEY_PROP_SSL_CERTIFICATE
= u
"ssl_certificate"_s
;
116 const QString KEY_PROP_SSL_PRIVATEKEY
= u
"ssl_private_key"_s
;
117 const QString KEY_PROP_SSL_DHPARAMS
= u
"ssl_dh_params"_s
;
118 const QString KEY_PROP_HAS_METADATA
= u
"has_metadata"_s
;
119 const QString KEY_PROP_PROGRESS
= u
"progress"_s
;
123 const QString KEY_FILE_INDEX
= u
"index"_s
;
124 const QString KEY_FILE_NAME
= u
"name"_s
;
125 const QString KEY_FILE_SIZE
= u
"size"_s
;
126 const QString KEY_FILE_PROGRESS
= u
"progress"_s
;
127 const QString KEY_FILE_PRIORITY
= u
"priority"_s
;
128 const QString KEY_FILE_IS_SEED
= u
"is_seed"_s
;
129 const QString KEY_FILE_PIECE_RANGE
= u
"piece_range"_s
;
130 const QString KEY_FILE_AVAILABILITY
= u
"availability"_s
;
134 using Utils::String::parseBool
;
135 using Utils::String::parseInt
;
136 using Utils::String::parseDouble
;
138 const QSet
<QString
> SUPPORTED_WEB_SEED_SCHEMES
{u
"http"_s
, u
"https"_s
, u
"ftp"_s
};
140 template <typename Func
>
141 void applyToTorrents(const QStringList
&idList
, Func func
)
142 requires
std::invocable
<Func
, BitTorrent::Torrent
*>
144 if ((idList
.size() == 1) && (idList
[0] == u
"all"))
146 for (BitTorrent::Torrent
*const torrent
: asConst(BitTorrent::Session::instance()->torrents()))
151 for (const QString
&idString
: idList
)
153 const auto hash
= BitTorrent::TorrentID::fromString(idString
);
154 BitTorrent::Torrent
*const torrent
= BitTorrent::Session::instance()->getTorrent(hash
);
161 std::optional
<QString
> getOptionalString(const StringMap
¶ms
, const QString
&name
)
163 const auto it
= params
.constFind(name
);
164 if (it
== params
.cend())
170 std::optional
<Tag
> getOptionalTag(const StringMap
¶ms
, const QString
&name
)
172 const auto it
= params
.constFind(name
);
173 if (it
== params
.cend())
176 return Tag(it
.value());
179 QJsonArray
getStickyTrackers(const BitTorrent::Torrent
*const torrent
)
181 int seedsDHT
= 0, seedsPeX
= 0, seedsLSD
= 0, leechesDHT
= 0, leechesPeX
= 0, leechesLSD
= 0;
182 for (const BitTorrent::PeerInfo
&peer
: asConst(torrent
->peers()))
184 if (peer
.isConnecting()) continue;
206 const int working
= static_cast<int>(BitTorrent::TrackerEndpointState::Working
);
207 const int disabled
= 0;
209 const QString privateMsg
{QCoreApplication::translate("TrackerListWidget", "This torrent is private")};
210 const bool isTorrentPrivate
= torrent
->isPrivate();
212 const QJsonObject dht
214 {KEY_TRACKER_URL
, u
"** [DHT] **"_s
},
215 {KEY_TRACKER_TIER
, -1},
216 {KEY_TRACKER_MSG
, (isTorrentPrivate
? privateMsg
: u
""_s
)},
217 {KEY_TRACKER_STATUS
, ((BitTorrent::Session::instance()->isDHTEnabled() && !isTorrentPrivate
) ? working
: disabled
)},
218 {KEY_TRACKER_PEERS_COUNT
, 0},
219 {KEY_TRACKER_DOWNLOADED_COUNT
, 0},
220 {KEY_TRACKER_SEEDS_COUNT
, seedsDHT
},
221 {KEY_TRACKER_LEECHES_COUNT
, leechesDHT
}
224 const QJsonObject pex
226 {KEY_TRACKER_URL
, u
"** [PeX] **"_s
},
227 {KEY_TRACKER_TIER
, -1},
228 {KEY_TRACKER_MSG
, (isTorrentPrivate
? privateMsg
: u
""_s
)},
229 {KEY_TRACKER_STATUS
, ((BitTorrent::Session::instance()->isPeXEnabled() && !isTorrentPrivate
) ? working
: disabled
)},
230 {KEY_TRACKER_PEERS_COUNT
, 0},
231 {KEY_TRACKER_DOWNLOADED_COUNT
, 0},
232 {KEY_TRACKER_SEEDS_COUNT
, seedsPeX
},
233 {KEY_TRACKER_LEECHES_COUNT
, leechesPeX
}
236 const QJsonObject lsd
238 {KEY_TRACKER_URL
, u
"** [LSD] **"_s
},
239 {KEY_TRACKER_TIER
, -1},
240 {KEY_TRACKER_MSG
, (isTorrentPrivate
? privateMsg
: u
""_s
)},
241 {KEY_TRACKER_STATUS
, ((BitTorrent::Session::instance()->isLSDEnabled() && !isTorrentPrivate
) ? working
: disabled
)},
242 {KEY_TRACKER_PEERS_COUNT
, 0},
243 {KEY_TRACKER_DOWNLOADED_COUNT
, 0},
244 {KEY_TRACKER_SEEDS_COUNT
, seedsLSD
},
245 {KEY_TRACKER_LEECHES_COUNT
, leechesLSD
}
248 return {dht
, pex
, lsd
};
251 QList
<BitTorrent::TorrentID
> toTorrentIDs(const QStringList
&idStrings
)
253 QList
<BitTorrent::TorrentID
> idList
;
254 idList
.reserve(idStrings
.size());
255 for (const QString
&hash
: idStrings
)
256 idList
<< BitTorrent::TorrentID::fromString(hash
);
260 nonstd::expected
<QUrl
, QString
> validateWebSeedUrl(const QString
&urlStr
)
262 const QString normalizedUrlStr
= QUrl::fromPercentEncoding(urlStr
.toLatin1());
264 const QUrl url
{normalizedUrlStr
, QUrl::StrictMode
};
266 return nonstd::make_unexpected(TorrentsController::tr("\"%1\" is not a valid URL").arg(normalizedUrlStr
));
268 if (!SUPPORTED_WEB_SEED_SCHEMES
.contains(url
.scheme()))
269 return nonstd::make_unexpected(TorrentsController::tr("URL scheme must be one of [%1]").arg(SUPPORTED_WEB_SEED_SCHEMES
.values().join(u
", ")));
275 void TorrentsController::countAction()
277 setResult(QString::number(BitTorrent::Session::instance()->torrentsCount()));
280 // Returns all the torrents in JSON format.
281 // The return value is a JSON-formatted list of dictionaries.
282 // The dictionary keys are:
283 // - "hash": Torrent hash (ID)
284 // - "name": Torrent name
285 // - "size": Torrent size
286 // - "progress": Torrent progress
287 // - "dlspeed": Torrent download speed
288 // - "upspeed": Torrent upload speed
289 // - "priority": Torrent queue position (-1 if queuing is disabled)
290 // - "num_seeds": Torrent seeds connected to
291 // - "num_complete": Torrent seeds in the swarm
292 // - "num_leechs": Torrent leechers connected to
293 // - "num_incomplete": Torrent leechers in the swarm
294 // - "ratio": Torrent share ratio
295 // - "eta": Torrent ETA
296 // - "state": Torrent state
297 // - "seq_dl": Torrent sequential download state
298 // - "f_l_piece_prio": Torrent first last piece priority state
299 // - "force_start": Torrent force start state
300 // - "category": Torrent category
302 // - filter (string): all, downloading, seeding, completed, stopped, running, active, inactive, stalled, stalled_uploading, stalled_downloading
303 // - category (string): torrent category for filtering by it (empty string means "uncategorized"; no "category" param presented means "any category")
304 // - tag (string): torrent tag for filtering by it (empty string means "untagged"; no "tag" param presented means "any tag")
305 // - hashes (string): filter by hashes, can contain multiple hashes separated by |
306 // - private (bool): filter torrents that are from private trackers (true) or not (false). Empty means any torrent (no filtering)
307 // - sort (string): name of column for sorting by its value
308 // - reverse (bool): enable reverse sorting
309 // - limit (int): set limit number of torrents returned (if greater than 0, otherwise - unlimited)
310 // - offset (int): set offset (if less than 0 - offset from end)
311 void TorrentsController::infoAction()
313 const QString filter
{params()[u
"filter"_s
]};
314 const std::optional
<QString
> category
= getOptionalString(params(), u
"category"_s
);
315 const std::optional
<Tag
> tag
= getOptionalTag(params(), u
"tag"_s
);
316 const QString sortedColumn
{params()[u
"sort"_s
]};
317 const bool reverse
{parseBool(params()[u
"reverse"_s
]).value_or(false)};
318 int limit
{params()[u
"limit"_s
].toInt()};
319 int offset
{params()[u
"offset"_s
].toInt()};
320 const QStringList hashes
{params()[u
"hashes"_s
].split(u
'|', Qt::SkipEmptyParts
)};
321 const std::optional
<bool> isPrivate
= parseBool(params()[u
"private"_s
]);
323 std::optional
<TorrentIDSet
> idSet
;
324 if (!hashes
.isEmpty())
326 idSet
= TorrentIDSet();
327 for (const QString
&hash
: hashes
)
328 idSet
->insert(BitTorrent::TorrentID::fromString(hash
));
331 const TorrentFilter torrentFilter
{filter
, idSet
, category
, tag
, isPrivate
};
332 QVariantList torrentList
;
333 for (const BitTorrent::Torrent
*torrent
: asConst(BitTorrent::Session::instance()->torrents()))
335 if (torrentFilter
.match(torrent
))
336 torrentList
.append(serialize(*torrent
));
339 if (torrentList
.isEmpty())
341 setResult(QJsonArray
{});
345 if (!sortedColumn
.isEmpty())
347 if (!torrentList
[0].toMap().contains(sortedColumn
))
348 throw APIError(APIErrorType::BadParams
, tr("'sort' parameter is invalid"));
350 const auto lessThan
= [](const QVariant
&left
, const QVariant
&right
) -> bool
352 Q_ASSERT(left
.userType() == right
.userType());
354 switch (left
.userType())
356 case QMetaType::Bool
:
357 return left
.value
<bool>() < right
.value
<bool>();
358 case QMetaType::Double
:
359 return left
.value
<double>() < right
.value
<double>();
360 case QMetaType::Float
:
361 return left
.value
<float>() < right
.value
<float>();
363 return left
.value
<int>() < right
.value
<int>();
364 case QMetaType::LongLong
:
365 return left
.value
<qlonglong
>() < right
.value
<qlonglong
>();
366 case QMetaType::QString
:
367 return left
.value
<QString
>() < right
.value
<QString
>();
369 qWarning("Unhandled QVariant comparison, type: %d, name: %s"
370 , left
.userType(), left
.metaType().name());
376 std::sort(torrentList
.begin(), torrentList
.end()
377 , [reverse
, &sortedColumn
, &lessThan
](const QVariant
&torrent1
, const QVariant
&torrent2
)
379 const QVariant value1
{torrent1
.toMap().value(sortedColumn
)};
380 const QVariant value2
{torrent2
.toMap().value(sortedColumn
)};
381 return reverse
? lessThan(value2
, value1
) : lessThan(value1
, value2
);
385 const int size
= torrentList
.size();
388 offset
= size
+ offset
;
389 if ((offset
>= size
) || (offset
< 0))
393 limit
= -1; // unlimited
395 if ((limit
> 0) || (offset
> 0))
396 torrentList
= torrentList
.mid(offset
, limit
);
398 setResult(QJsonArray::fromVariantList(torrentList
));
401 // Returns the properties for a torrent in JSON format.
402 // The return value is a JSON-formatted dictionary.
403 // The dictionary keys are:
404 // - "time_elapsed": Torrent elapsed time
405 // - "seeding_time": Torrent elapsed time while complete
406 // - "eta": Torrent ETA
407 // - "nb_connections": Torrent connection count
408 // - "nb_connections_limit": Torrent connection count limit
409 // - "total_downloaded": Total data uploaded for torrent
410 // - "total_downloaded_session": Total data downloaded this session
411 // - "total_uploaded": Total data uploaded for torrent
412 // - "total_uploaded_session": Total data uploaded this session
413 // - "dl_speed": Torrent download speed
414 // - "dl_speed_avg": Torrent average download speed
415 // - "up_speed": Torrent upload speed
416 // - "up_speed_avg": Torrent average upload speed
417 // - "dl_limit": Torrent download limit
418 // - "up_limit": Torrent upload limit
419 // - "total_wasted": Total data wasted for torrent
420 // - "seeds": Torrent connected seeds
421 // - "seeds_total": Torrent total number of seeds
422 // - "peers": Torrent connected peers
423 // - "peers_total": Torrent total number of peers
424 // - "share_ratio": Torrent share ratio
425 // - "popularity": Torrent popularity
426 // - "reannounce": Torrent next reannounce time
427 // - "total_size": Torrent total size
428 // - "pieces_num": Torrent pieces count
429 // - "piece_size": Torrent piece size
430 // - "pieces_have": Torrent pieces have
431 // - "created_by": Torrent creator
432 // - "last_seen": Torrent last seen complete
433 // - "addition_date": Torrent addition date
434 // - "completion_date": Torrent completion date
435 // - "creation_date": Torrent creation date
436 // - "save_path": Torrent save path
437 // - "download_path": Torrent download path
438 // - "comment": Torrent comment
439 // - "infohash_v1": Torrent v1 infohash (or empty string for v2 torrents)
440 // - "infohash_v2": Torrent v2 infohash (or empty string for v1 torrents)
441 // - "hash": Torrent TorrentID (infohashv1 for v1 torrents, truncated infohashv2 for v2/hybrid torrents)
442 // - "name": Torrent name
443 void TorrentsController::propertiesAction()
445 requireParams({u
"hash"_s
});
447 const auto id
= BitTorrent::TorrentID::fromString(params()[u
"hash"_s
]);
448 const BitTorrent::Torrent
*const torrent
= BitTorrent::Session::instance()->getTorrent(id
);
450 throw APIError(APIErrorType::NotFound
);
452 const BitTorrent::InfoHash infoHash
= torrent
->infoHash();
453 const qlonglong totalDownload
= torrent
->totalDownload();
454 const qlonglong totalUpload
= torrent
->totalUpload();
455 const qlonglong dlDuration
= torrent
->activeTime() - torrent
->finishedTime();
456 const qlonglong ulDuration
= torrent
->activeTime();
457 const int downloadLimit
= torrent
->downloadLimit();
458 const int uploadLimit
= torrent
->uploadLimit();
459 const qreal ratio
= torrent
->realRatio();
460 const qreal popularity
= torrent
->popularity();
461 const bool hasMetadata
= torrent
->hasMetadata();
462 const bool isPrivate
= torrent
->isPrivate();
464 const QJsonObject ret
466 {KEY_TORRENT_INFOHASHV1
, infoHash
.v1().toString()},
467 {KEY_TORRENT_INFOHASHV2
, infoHash
.v2().toString()},
468 {KEY_TORRENT_NAME
, torrent
->name()},
469 {KEY_TORRENT_ID
, torrent
->id().toString()},
470 {KEY_PROP_TIME_ELAPSED
, torrent
->activeTime()},
471 {KEY_PROP_SEEDING_TIME
, torrent
->finishedTime()},
472 {KEY_PROP_ETA
, torrent
->eta()},
473 {KEY_PROP_CONNECT_COUNT
, torrent
->connectionsCount()},
474 {KEY_PROP_CONNECT_COUNT_LIMIT
, torrent
->connectionsLimit()},
475 {KEY_PROP_DOWNLOADED
, totalDownload
},
476 {KEY_PROP_DOWNLOADED_SESSION
, torrent
->totalPayloadDownload()},
477 {KEY_PROP_UPLOADED
, totalUpload
},
478 {KEY_PROP_UPLOADED_SESSION
, torrent
->totalPayloadUpload()},
479 {KEY_PROP_DL_SPEED
, torrent
->downloadPayloadRate()},
480 {KEY_PROP_DL_SPEED_AVG
, ((dlDuration
> 0) ? (totalDownload
/ dlDuration
) : -1)},
481 {KEY_PROP_UP_SPEED
, torrent
->uploadPayloadRate()},
482 {KEY_PROP_UP_SPEED_AVG
, ((ulDuration
> 0) ? (totalUpload
/ ulDuration
) : -1)},
483 {KEY_PROP_DL_LIMIT
, ((downloadLimit
> 0) ? downloadLimit
: -1)},
484 {KEY_PROP_UP_LIMIT
, ((uploadLimit
> 0) ? uploadLimit
: -1)},
485 {KEY_PROP_WASTED
, torrent
->wastedSize()},
486 {KEY_PROP_SEEDS
, torrent
->seedsCount()},
487 {KEY_PROP_SEEDS_TOTAL
, torrent
->totalSeedsCount()},
488 {KEY_PROP_PEERS
, torrent
->leechsCount()},
489 {KEY_PROP_PEERS_TOTAL
, torrent
->totalLeechersCount()},
490 {KEY_PROP_RATIO
, ((ratio
> BitTorrent::Torrent::MAX_RATIO
) ? -1 : ratio
)},
491 {KEY_PROP_POPULARITY
, ((popularity
> BitTorrent::Torrent::MAX_RATIO
) ? -1 : popularity
)},
492 {KEY_PROP_REANNOUNCE
, torrent
->nextAnnounce()},
493 {KEY_PROP_TOTAL_SIZE
, torrent
->totalSize()},
494 {KEY_PROP_PIECES_NUM
, torrent
->piecesCount()},
495 {KEY_PROP_PIECE_SIZE
, torrent
->pieceLength()},
496 {KEY_PROP_PIECES_HAVE
, torrent
->piecesHave()},
497 {KEY_PROP_CREATED_BY
, torrent
->creator()},
498 {KEY_PROP_IS_PRIVATE
, torrent
->isPrivate()}, // used for maintaining backward compatibility
499 {KEY_PROP_PRIVATE
, (hasMetadata
? isPrivate
: QJsonValue())},
500 {KEY_PROP_ADDITION_DATE
, Utils::DateTime::toSecsSinceEpoch(torrent
->addedTime())},
501 {KEY_PROP_LAST_SEEN
, Utils::DateTime::toSecsSinceEpoch(torrent
->lastSeenComplete())},
502 {KEY_PROP_COMPLETION_DATE
, Utils::DateTime::toSecsSinceEpoch(torrent
->completedTime())},
503 {KEY_PROP_CREATION_DATE
, Utils::DateTime::toSecsSinceEpoch(torrent
->creationDate())},
504 {KEY_PROP_SAVE_PATH
, torrent
->savePath().toString()},
505 {KEY_PROP_DOWNLOAD_PATH
, torrent
->downloadPath().toString()},
506 {KEY_PROP_COMMENT
, torrent
->comment()},
507 {KEY_PROP_HAS_METADATA
, torrent
->hasMetadata()},
508 {KEY_PROP_PROGRESS
, torrent
->progress()}
514 // Returns the trackers for a torrent in JSON format.
515 // The return value is a JSON-formatted list of dictionaries.
516 // The dictionary keys are:
517 // - "url": Tracker URL
518 // - "status": Tracker status
519 // - "tier": Tracker tier
520 // - "num_peers": Number of peers this torrent is currently connected to
521 // - "num_seeds": Number of peers that have the whole file
522 // - "num_leeches": Number of peers that are still downloading
523 // - "num_downloaded": Tracker downloaded count
524 // - "msg": Tracker message (last)
525 void TorrentsController::trackersAction()
527 requireParams({u
"hash"_s
});
529 const auto id
= BitTorrent::TorrentID::fromString(params()[u
"hash"_s
]);
530 const BitTorrent::Torrent
*const torrent
= BitTorrent::Session::instance()->getTorrent(id
);
532 throw APIError(APIErrorType::NotFound
);
534 QJsonArray trackerList
= getStickyTrackers(torrent
);
536 for (const BitTorrent::TrackerEntryStatus
&tracker
: asConst(torrent
->trackers()))
538 const bool isNotWorking
= (tracker
.state
== BitTorrent::TrackerEndpointState::NotWorking
)
539 || (tracker
.state
== BitTorrent::TrackerEndpointState::TrackerError
)
540 || (tracker
.state
== BitTorrent::TrackerEndpointState::Unreachable
);
541 trackerList
<< QJsonObject
543 {KEY_TRACKER_URL
, tracker
.url
},
544 {KEY_TRACKER_TIER
, tracker
.tier
},
545 {KEY_TRACKER_STATUS
, static_cast<int>((isNotWorking
? BitTorrent::TrackerEndpointState::NotWorking
: tracker
.state
))},
546 {KEY_TRACKER_MSG
, tracker
.message
},
547 {KEY_TRACKER_PEERS_COUNT
, tracker
.numPeers
},
548 {KEY_TRACKER_SEEDS_COUNT
, tracker
.numSeeds
},
549 {KEY_TRACKER_LEECHES_COUNT
, tracker
.numLeeches
},
550 {KEY_TRACKER_DOWNLOADED_COUNT
, tracker
.numDownloaded
}
554 setResult(trackerList
);
557 // Returns the web seeds for a torrent in JSON format.
558 // The return value is a JSON-formatted list of dictionaries.
559 // The dictionary keys are:
560 // - "url": Web seed URL
561 void TorrentsController::webseedsAction()
563 requireParams({u
"hash"_s
});
565 const auto id
= BitTorrent::TorrentID::fromString(params()[u
"hash"_s
]);
566 BitTorrent::Torrent
*const torrent
= BitTorrent::Session::instance()->getTorrent(id
);
568 throw APIError(APIErrorType::NotFound
);
570 QJsonArray webSeedList
;
571 for (const QUrl
&webseed
: asConst(torrent
->urlSeeds()))
573 webSeedList
.append(QJsonObject
575 {KEY_WEBSEED_URL
, webseed
.toString()}
579 setResult(webSeedList
);
582 void TorrentsController::addWebSeedsAction()
584 requireParams({u
"hash"_s
, u
"urls"_s
});
585 const QStringList paramUrls
= params()[u
"urls"_s
].split(u
'|', Qt::SkipEmptyParts
);
587 const auto id
= BitTorrent::TorrentID::fromString(params()[u
"hash"_s
]);
588 BitTorrent::Torrent
*const torrent
= BitTorrent::Session::instance()->getTorrent(id
);
590 throw APIError(APIErrorType::NotFound
);
593 urls
.reserve(paramUrls
.size());
594 for (const QString
&urlStr
: paramUrls
)
596 const auto result
= validateWebSeedUrl(urlStr
);
598 throw APIError(APIErrorType::BadParams
, result
.error());
599 urls
<< result
.value();
602 torrent
->addUrlSeeds(urls
);
605 void TorrentsController::editWebSeedAction()
607 requireParams({u
"hash"_s
, u
"origUrl"_s
, u
"newUrl"_s
});
609 const auto id
= BitTorrent::TorrentID::fromString(params()[u
"hash"_s
]);
610 const QString origUrlStr
= params()[u
"origUrl"_s
];
611 const QString newUrlStr
= params()[u
"newUrl"_s
];
613 BitTorrent::Torrent
*const torrent
= BitTorrent::Session::instance()->getTorrent(id
);
615 throw APIError(APIErrorType::NotFound
);
617 const auto origUrlResult
= validateWebSeedUrl(origUrlStr
);
619 throw APIError(APIErrorType::BadParams
, origUrlResult
.error());
620 const QUrl origUrl
= origUrlResult
.value();
622 const auto newUrlResult
= validateWebSeedUrl(newUrlStr
);
624 throw APIError(APIErrorType::BadParams
, newUrlResult
.error());
625 const QUrl newUrl
= newUrlResult
.value();
627 if (newUrl
!= origUrl
)
629 if (!torrent
->urlSeeds().contains(origUrl
))
630 throw APIError(APIErrorType::Conflict
, tr("\"%1\" is not an existing URL").arg(origUrl
.toString()));
632 torrent
->removeUrlSeeds({origUrl
});
633 torrent
->addUrlSeeds({newUrl
});
637 void TorrentsController::removeWebSeedsAction()
639 requireParams({u
"hash"_s
, u
"urls"_s
});
640 const QStringList paramUrls
= params()[u
"urls"_s
].split(u
'|', Qt::SkipEmptyParts
);
642 const auto id
= BitTorrent::TorrentID::fromString(params()[u
"hash"_s
]);
643 BitTorrent::Torrent
*const torrent
= BitTorrent::Session::instance()->getTorrent(id
);
645 throw APIError(APIErrorType::NotFound
);
648 urls
.reserve(paramUrls
.size());
649 for (const QString
&urlStr
: paramUrls
)
651 const auto result
= validateWebSeedUrl(urlStr
);
653 throw APIError(APIErrorType::BadParams
, result
.error());
654 urls
<< result
.value();
657 torrent
->removeUrlSeeds(urls
);
660 // Returns the files in a torrent in JSON format.
661 // The return value is a JSON-formatted list of dictionaries.
662 // The dictionary keys are:
663 // - "index": File index
664 // - "name": File name
665 // - "size": File size
666 // - "progress": File progress
667 // - "priority": File priority
668 // - "is_seed": Flag indicating if torrent is seeding/complete
669 // - "piece_range": Piece index range, the first number is the starting piece index
670 // and the second number is the ending piece index (inclusive)
671 void TorrentsController::filesAction()
673 requireParams({u
"hash"_s
});
675 const auto id
= BitTorrent::TorrentID::fromString(params()[u
"hash"_s
]);
676 const BitTorrent::Torrent
*const torrent
= BitTorrent::Session::instance()->getTorrent(id
);
678 throw APIError(APIErrorType::NotFound
);
680 const int filesCount
= torrent
->filesCount();
681 QList
<int> fileIndexes
;
682 const auto idxIt
= params().constFind(u
"indexes"_s
);
683 if (idxIt
!= params().cend())
685 const QStringList indexStrings
= idxIt
.value().split(u
'|');
686 fileIndexes
.reserve(indexStrings
.size());
687 std::transform(indexStrings
.cbegin(), indexStrings
.cend(), std::back_inserter(fileIndexes
)
688 , [&filesCount
](const QString
&indexString
) -> int
691 const int index
= indexString
.toInt(&ok
);
692 if (!ok
|| (index
< 0))
693 throw APIError(APIErrorType::Conflict
, tr("\"%1\" is not a valid file index.").arg(indexString
));
694 if (index
>= filesCount
)
695 throw APIError(APIErrorType::Conflict
, tr("Index %1 is out of bounds.").arg(indexString
));
701 fileIndexes
.reserve(filesCount
);
702 for (int i
= 0; i
< filesCount
; ++i
)
703 fileIndexes
.append(i
);
707 if (torrent
->hasMetadata())
709 const QList
<BitTorrent::DownloadPriority
> priorities
= torrent
->filePriorities();
710 const QList
<qreal
> fp
= torrent
->filesProgress();
711 const QList
<qreal
> fileAvailability
= torrent
->availableFileFractions();
712 const BitTorrent::TorrentInfo info
= torrent
->info();
713 for (const int index
: asConst(fileIndexes
))
715 QJsonObject fileDict
=
717 {KEY_FILE_INDEX
, index
},
718 {KEY_FILE_PROGRESS
, fp
[index
]},
719 {KEY_FILE_PRIORITY
, static_cast<int>(priorities
[index
])},
720 {KEY_FILE_SIZE
, torrent
->fileSize(index
)},
721 {KEY_FILE_AVAILABILITY
, fileAvailability
[index
]},
722 // need to provide paths using a platform-independent separator format
723 {KEY_FILE_NAME
, torrent
->filePath(index
).data()}
726 const BitTorrent::TorrentInfo::PieceRange idx
= info
.filePieces(index
);
727 fileDict
[KEY_FILE_PIECE_RANGE
] = QJsonArray
{idx
.first(), idx
.last()};
730 fileDict
[KEY_FILE_IS_SEED
] = torrent
->isFinished();
732 fileList
.append(fileDict
);
739 // Returns an array of hashes (of each pieces respectively) for a torrent in JSON format.
740 // The return value is a JSON-formatted array of strings (hex strings).
741 void TorrentsController::pieceHashesAction()
743 requireParams({u
"hash"_s
});
745 const auto id
= BitTorrent::TorrentID::fromString(params()[u
"hash"_s
]);
746 BitTorrent::Torrent
*const torrent
= BitTorrent::Session::instance()->getTorrent(id
);
748 throw APIError(APIErrorType::NotFound
);
750 QJsonArray pieceHashes
;
751 if (torrent
->hasMetadata())
753 const QList
<QByteArray
> hashes
= torrent
->info().pieceHashes();
754 for (const QByteArray
&hash
: hashes
)
755 pieceHashes
.append(QString::fromLatin1(hash
.toHex()));
758 setResult(pieceHashes
);
761 // Returns an array of states (of each pieces respectively) for a torrent in JSON format.
762 // The return value is a JSON-formatted array of ints.
763 // 0: piece not downloaded
764 // 1: piece requested or downloading
765 // 2: piece already downloaded
766 void TorrentsController::pieceStatesAction()
768 requireParams({u
"hash"_s
});
770 const auto id
= BitTorrent::TorrentID::fromString(params()[u
"hash"_s
]);
771 BitTorrent::Torrent
*const torrent
= BitTorrent::Session::instance()->getTorrent(id
);
773 throw APIError(APIErrorType::NotFound
);
775 QJsonArray pieceStates
;
776 const QBitArray states
= torrent
->pieces();
777 for (int i
= 0; i
< states
.size(); ++i
)
778 pieceStates
.append(static_cast<int>(states
[i
]) * 2);
780 const QBitArray dlstates
= torrent
->downloadingPieces();
781 for (int i
= 0; i
< states
.size(); ++i
)
787 setResult(pieceStates
);
790 void TorrentsController::addAction()
792 const QString urls
= params()[u
"urls"_s
];
794 const bool skipChecking
= parseBool(params()[u
"skip_checking"_s
]).value_or(false);
795 const bool seqDownload
= parseBool(params()[u
"sequentialDownload"_s
]).value_or(false);
796 const bool firstLastPiece
= parseBool(params()[u
"firstLastPiecePrio"_s
]).value_or(false);
797 const std::optional
<bool> addToQueueTop
= parseBool(params()[u
"addToTopOfQueue"_s
]);
798 const std::optional
<bool> addStopped
= parseBool(params()[u
"stopped"_s
]);
799 const QString savepath
= params()[u
"savepath"_s
].trimmed();
800 const QString downloadPath
= params()[u
"downloadPath"_s
].trimmed();
801 const std::optional
<bool> useDownloadPath
= parseBool(params()[u
"useDownloadPath"_s
]);
802 const QString category
= params()[u
"category"_s
];
803 const QStringList tags
= params()[u
"tags"_s
].split(u
',', Qt::SkipEmptyParts
);
804 const QString torrentName
= params()[u
"rename"_s
].trimmed();
805 const int upLimit
= parseInt(params()[u
"upLimit"_s
]).value_or(-1);
806 const int dlLimit
= parseInt(params()[u
"dlLimit"_s
]).value_or(-1);
807 const double ratioLimit
= parseDouble(params()[u
"ratioLimit"_s
]).value_or(BitTorrent::Torrent::USE_GLOBAL_RATIO
);
808 const int seedingTimeLimit
= parseInt(params()[u
"seedingTimeLimit"_s
]).value_or(BitTorrent::Torrent::USE_GLOBAL_SEEDING_TIME
);
809 const int inactiveSeedingTimeLimit
= parseInt(params()[u
"inactiveSeedingTimeLimit"_s
]).value_or(BitTorrent::Torrent::USE_GLOBAL_INACTIVE_SEEDING_TIME
);
810 const BitTorrent::ShareLimitAction shareLimitAction
= Utils::String::toEnum(params()[u
"shareLimitAction"_s
], BitTorrent::ShareLimitAction::Default
);
811 const std::optional
<bool> autoTMM
= parseBool(params()[u
"autoTMM"_s
]);
813 const QString stopConditionParam
= params()[u
"stopCondition"_s
];
814 const std::optional
<BitTorrent::Torrent::StopCondition
> stopCondition
= (!stopConditionParam
.isEmpty()
815 ? Utils::String::toEnum(stopConditionParam
, BitTorrent::Torrent::StopCondition::None
)
816 : std::optional
<BitTorrent::Torrent::StopCondition
> {});
818 const QString contentLayoutParam
= params()[u
"contentLayout"_s
];
819 const std::optional
<BitTorrent::TorrentContentLayout
> contentLayout
= (!contentLayoutParam
.isEmpty()
820 ? Utils::String::toEnum(contentLayoutParam
, BitTorrent::TorrentContentLayout::Original
)
821 : std::optional
<BitTorrent::TorrentContentLayout
> {});
823 const BitTorrent::AddTorrentParams addTorrentParams
825 // TODO: Check if destination actually exists
827 .category
= category
,
828 .tags
= {tags
.cbegin(), tags
.cend()},
829 .savePath
= Path(savepath
),
830 .useDownloadPath
= useDownloadPath
,
831 .downloadPath
= Path(downloadPath
),
832 .sequential
= seqDownload
,
833 .firstLastPiecePriority
= firstLastPiece
,
835 .addToQueueTop
= addToQueueTop
,
836 .addStopped
= addStopped
,
837 .stopCondition
= stopCondition
,
839 .filePriorities
= {},
840 .skipChecking
= skipChecking
,
841 .contentLayout
= contentLayout
,
842 .useAutoTMM
= autoTMM
,
843 .uploadLimit
= upLimit
,
844 .downloadLimit
= dlLimit
,
845 .seedingTimeLimit
= seedingTimeLimit
,
846 .inactiveSeedingTimeLimit
= inactiveSeedingTimeLimit
,
847 .ratioLimit
= ratioLimit
,
848 .shareLimitAction
= shareLimitAction
,
851 .certificate
= QSslCertificate(params()[KEY_PROP_SSL_CERTIFICATE
].toLatin1()),
852 .privateKey
= Utils::SSLKey::load(params()[KEY_PROP_SSL_PRIVATEKEY
].toLatin1()),
853 .dhParams
= params()[KEY_PROP_SSL_DHPARAMS
].toLatin1()
857 bool partialSuccess
= false;
858 for (QString url
: asConst(urls
.split(u
'\n')))
863 partialSuccess
|= app()->addTorrentManager()->addTorrent(url
, addTorrentParams
);
867 const DataMap
&torrents
= data();
868 for (auto it
= torrents
.constBegin(); it
!= torrents
.constEnd(); ++it
)
870 if (const auto loadResult
= BitTorrent::TorrentDescriptor::load(it
.value()))
872 partialSuccess
|= BitTorrent::Session::instance()->addTorrent(loadResult
.value(), addTorrentParams
);
876 throw APIError(APIErrorType::BadData
, tr("Error: '%1' is not a valid torrent file.").arg(it
.key()));
883 setResult(u
"Fails."_s
);
886 void TorrentsController::addTrackersAction()
888 requireParams({u
"hash"_s
, u
"urls"_s
});
890 const auto id
= BitTorrent::TorrentID::fromString(params()[u
"hash"_s
]);
891 BitTorrent::Torrent
*const torrent
= BitTorrent::Session::instance()->getTorrent(id
);
893 throw APIError(APIErrorType::NotFound
);
895 const QList
<BitTorrent::TrackerEntry
> entries
= BitTorrent::parseTrackerEntries(params()[u
"urls"_s
]);
896 torrent
->addTrackers(entries
);
899 void TorrentsController::editTrackerAction()
901 requireParams({u
"hash"_s
, u
"origUrl"_s
, u
"newUrl"_s
});
903 const auto id
= BitTorrent::TorrentID::fromString(params()[u
"hash"_s
]);
904 const QString origUrl
= params()[u
"origUrl"_s
];
905 const QString newUrl
= params()[u
"newUrl"_s
];
907 BitTorrent::Torrent
*const torrent
= BitTorrent::Session::instance()->getTorrent(id
);
909 throw APIError(APIErrorType::NotFound
);
911 const QUrl origTrackerUrl
{origUrl
};
912 const QUrl newTrackerUrl
{newUrl
};
913 if (origTrackerUrl
== newTrackerUrl
)
915 if (!newTrackerUrl
.isValid())
916 throw APIError(APIErrorType::BadParams
, u
"New tracker URL is invalid"_s
);
918 const QList
<BitTorrent::TrackerEntryStatus
> currentTrackers
= torrent
->trackers();
919 QList
<BitTorrent::TrackerEntry
> entries
;
920 entries
.reserve(currentTrackers
.size());
923 for (const BitTorrent::TrackerEntryStatus
&tracker
: currentTrackers
)
925 const QUrl trackerUrl
{tracker
.url
};
927 if (trackerUrl
== newTrackerUrl
)
928 throw APIError(APIErrorType::Conflict
, u
"New tracker URL already exists"_s
);
930 BitTorrent::TrackerEntry entry
936 if (trackerUrl
== origTrackerUrl
)
939 entry
.url
= newTrackerUrl
.toString();
941 entries
.append(entry
);
944 throw APIError(APIErrorType::Conflict
, u
"Tracker not found"_s
);
946 torrent
->replaceTrackers(entries
);
948 if (!torrent
->isStopped())
949 torrent
->forceReannounce();
952 void TorrentsController::removeTrackersAction()
954 requireParams({u
"hash"_s
, u
"urls"_s
});
956 const QString hashParam
= params()[u
"hash"_s
];
957 const QStringList urlsParam
= params()[u
"urls"_s
].split(u
'|', Qt::SkipEmptyParts
);
960 urls
.reserve(urlsParam
.size());
961 for (const QString
&urlStr
: urlsParam
)
962 urls
<< QUrl::fromPercentEncoding(urlStr
.toLatin1());
964 QList
<BitTorrent::Torrent
*> torrents
;
966 if (hashParam
== u
"*"_s
)
968 // remove trackers from all torrents
969 torrents
= BitTorrent::Session::instance()->torrents();
973 // remove trackers from specified torrent
974 const auto id
= BitTorrent::TorrentID::fromString(hashParam
);
975 BitTorrent::Torrent
*const torrent
= BitTorrent::Session::instance()->getTorrent(id
);
977 throw APIError(APIErrorType::NotFound
);
979 torrents
.append(torrent
);
982 for (BitTorrent::Torrent
*const torrent
: asConst(torrents
))
983 torrent
->removeTrackers(urls
);
986 void TorrentsController::addPeersAction()
988 requireParams({u
"hashes"_s
, u
"peers"_s
});
990 const QStringList hashes
= params()[u
"hashes"_s
].split(u
'|');
991 const QStringList peers
= params()[u
"peers"_s
].split(u
'|');
993 QList
<BitTorrent::PeerAddress
> peerList
;
994 peerList
.reserve(peers
.size());
995 for (const QString
&peer
: peers
)
997 const BitTorrent::PeerAddress addr
= BitTorrent::PeerAddress::parse(peer
.trimmed());
998 if (!addr
.ip
.isNull())
999 peerList
.append(addr
);
1002 if (peerList
.isEmpty())
1003 throw APIError(APIErrorType::BadParams
, u
"No valid peers were specified"_s
);
1005 QJsonObject results
;
1007 applyToTorrents(hashes
, [peers
, peerList
, &results
](BitTorrent::Torrent
*const torrent
)
1009 const int peersAdded
= std::count_if(peerList
.cbegin(), peerList
.cend(), [torrent
](const BitTorrent::PeerAddress
&peer
)
1011 return torrent
->connectPeer(peer
);
1014 results
[torrent
->id().toString()] = QJsonObject
1016 {u
"added"_s
, peersAdded
},
1017 {u
"failed"_s
, (peers
.size() - peersAdded
)}
1024 void TorrentsController::stopAction()
1026 requireParams({u
"hashes"_s
});
1028 const QStringList hashes
= params()[u
"hashes"_s
].split(u
'|');
1029 applyToTorrents(hashes
, [](BitTorrent::Torrent
*const torrent
) { torrent
->stop(); });
1032 void TorrentsController::startAction()
1034 requireParams({u
"hashes"_s
});
1036 const QStringList idStrings
= params()[u
"hashes"_s
].split(u
'|');
1037 applyToTorrents(idStrings
, [](BitTorrent::Torrent
*const torrent
) { torrent
->start(); });
1040 void TorrentsController::filePrioAction()
1042 requireParams({u
"hash"_s
, u
"id"_s
, u
"priority"_s
});
1044 const auto id
= BitTorrent::TorrentID::fromString(params()[u
"hash"_s
]);
1046 const auto priority
= static_cast<BitTorrent::DownloadPriority
>(params()[u
"priority"_s
].toInt(&ok
));
1048 throw APIError(APIErrorType::BadParams
, tr("Priority must be an integer"));
1050 if (!BitTorrent::isValidDownloadPriority(priority
))
1051 throw APIError(APIErrorType::BadParams
, tr("Priority is not valid"));
1053 BitTorrent::Torrent
*const torrent
= BitTorrent::Session::instance()->getTorrent(id
);
1055 throw APIError(APIErrorType::NotFound
);
1056 if (!torrent
->hasMetadata())
1057 throw APIError(APIErrorType::Conflict
, tr("Torrent's metadata has not yet downloaded"));
1059 const int filesCount
= torrent
->filesCount();
1060 QList
<BitTorrent::DownloadPriority
> priorities
= torrent
->filePriorities();
1061 bool priorityChanged
= false;
1062 for (const QString
&fileID
: params()[u
"id"_s
].split(u
'|'))
1064 const int id
= fileID
.toInt(&ok
);
1066 throw APIError(APIErrorType::BadParams
, tr("File IDs must be integers"));
1067 if ((id
< 0) || (id
>= filesCount
))
1068 throw APIError(APIErrorType::Conflict
, tr("File ID is not valid"));
1070 if (priorities
[id
] != priority
)
1072 priorities
[id
] = priority
;
1073 priorityChanged
= true;
1077 if (priorityChanged
)
1078 torrent
->prioritizeFiles(priorities
);
1081 void TorrentsController::uploadLimitAction()
1083 requireParams({u
"hashes"_s
});
1085 const QStringList idList
{params()[u
"hashes"_s
].split(u
'|')};
1087 for (const QString
&id
: idList
)
1090 const BitTorrent::Torrent
*const torrent
= BitTorrent::Session::instance()->getTorrent(BitTorrent::TorrentID::fromString(id
));
1092 limit
= torrent
->uploadLimit();
1099 void TorrentsController::downloadLimitAction()
1101 requireParams({u
"hashes"_s
});
1103 const QStringList idList
{params()[u
"hashes"_s
].split(u
'|')};
1105 for (const QString
&id
: idList
)
1108 const BitTorrent::Torrent
*const torrent
= BitTorrent::Session::instance()->getTorrent(BitTorrent::TorrentID::fromString(id
));
1110 limit
= torrent
->downloadLimit();
1117 void TorrentsController::setUploadLimitAction()
1119 requireParams({u
"hashes"_s
, u
"limit"_s
});
1121 qlonglong limit
= params()[u
"limit"_s
].toLongLong();
1125 const QStringList hashes
{params()[u
"hashes"_s
].split(u
'|')};
1126 applyToTorrents(hashes
, [limit
](BitTorrent::Torrent
*const torrent
) { torrent
->setUploadLimit(limit
); });
1129 void TorrentsController::setDownloadLimitAction()
1131 requireParams({u
"hashes"_s
, u
"limit"_s
});
1133 qlonglong limit
= params()[u
"limit"_s
].toLongLong();
1137 const QStringList hashes
{params()[u
"hashes"_s
].split(u
'|')};
1138 applyToTorrents(hashes
, [limit
](BitTorrent::Torrent
*const torrent
) { torrent
->setDownloadLimit(limit
); });
1141 void TorrentsController::setShareLimitsAction()
1143 requireParams({u
"hashes"_s
, u
"ratioLimit"_s
, u
"seedingTimeLimit"_s
, u
"inactiveSeedingTimeLimit"_s
});
1145 const qreal ratioLimit
= params()[u
"ratioLimit"_s
].toDouble();
1146 const qlonglong seedingTimeLimit
= params()[u
"seedingTimeLimit"_s
].toLongLong();
1147 const qlonglong inactiveSeedingTimeLimit
= params()[u
"inactiveSeedingTimeLimit"_s
].toLongLong();
1148 const QStringList hashes
= params()[u
"hashes"_s
].split(u
'|');
1150 applyToTorrents(hashes
, [ratioLimit
, seedingTimeLimit
, inactiveSeedingTimeLimit
](BitTorrent::Torrent
*const torrent
)
1152 torrent
->setRatioLimit(ratioLimit
);
1153 torrent
->setSeedingTimeLimit(seedingTimeLimit
);
1154 torrent
->setInactiveSeedingTimeLimit(inactiveSeedingTimeLimit
);
1158 void TorrentsController::toggleSequentialDownloadAction()
1160 requireParams({u
"hashes"_s
});
1162 const QStringList hashes
{params()[u
"hashes"_s
].split(u
'|')};
1163 applyToTorrents(hashes
, [](BitTorrent::Torrent
*const torrent
) { torrent
->toggleSequentialDownload(); });
1166 void TorrentsController::toggleFirstLastPiecePrioAction()
1168 requireParams({u
"hashes"_s
});
1170 const QStringList hashes
{params()[u
"hashes"_s
].split(u
'|')};
1171 applyToTorrents(hashes
, [](BitTorrent::Torrent
*const torrent
) { torrent
->toggleFirstLastPiecePriority(); });
1174 void TorrentsController::setSuperSeedingAction()
1176 requireParams({u
"hashes"_s
, u
"value"_s
});
1178 const bool value
{parseBool(params()[u
"value"_s
]).value_or(false)};
1179 const QStringList hashes
{params()[u
"hashes"_s
].split(u
'|')};
1180 applyToTorrents(hashes
, [value
](BitTorrent::Torrent
*const torrent
) { torrent
->setSuperSeeding(value
); });
1183 void TorrentsController::setForceStartAction()
1185 requireParams({u
"hashes"_s
, u
"value"_s
});
1187 const bool value
{parseBool(params()[u
"value"_s
]).value_or(false)};
1188 const QStringList hashes
{params()[u
"hashes"_s
].split(u
'|')};
1189 applyToTorrents(hashes
, [value
](BitTorrent::Torrent
*const torrent
)
1191 torrent
->start(value
? BitTorrent::TorrentOperatingMode::Forced
: BitTorrent::TorrentOperatingMode::AutoManaged
);
1195 void TorrentsController::deleteAction()
1197 requireParams({u
"hashes"_s
, u
"deleteFiles"_s
});
1199 const QStringList hashes
{params()[u
"hashes"_s
].split(u
'|')};
1200 const BitTorrent::TorrentRemoveOption deleteOption
= parseBool(params()[u
"deleteFiles"_s
]).value_or(false)
1201 ? BitTorrent::TorrentRemoveOption::RemoveContent
: BitTorrent::TorrentRemoveOption::KeepContent
;
1202 applyToTorrents(hashes
, [deleteOption
](const BitTorrent::Torrent
*torrent
)
1204 BitTorrent::Session::instance()->removeTorrent(torrent
->id(), deleteOption
);
1208 void TorrentsController::increasePrioAction()
1210 requireParams({u
"hashes"_s
});
1212 if (!BitTorrent::Session::instance()->isQueueingSystemEnabled())
1213 throw APIError(APIErrorType::Conflict
, tr("Torrent queueing must be enabled"));
1215 const QStringList hashes
{params()[u
"hashes"_s
].split(u
'|')};
1216 BitTorrent::Session::instance()->increaseTorrentsQueuePos(toTorrentIDs(hashes
));
1219 void TorrentsController::decreasePrioAction()
1221 requireParams({u
"hashes"_s
});
1223 if (!BitTorrent::Session::instance()->isQueueingSystemEnabled())
1224 throw APIError(APIErrorType::Conflict
, tr("Torrent queueing must be enabled"));
1226 const QStringList hashes
{params()[u
"hashes"_s
].split(u
'|')};
1227 BitTorrent::Session::instance()->decreaseTorrentsQueuePos(toTorrentIDs(hashes
));
1230 void TorrentsController::topPrioAction()
1232 requireParams({u
"hashes"_s
});
1234 if (!BitTorrent::Session::instance()->isQueueingSystemEnabled())
1235 throw APIError(APIErrorType::Conflict
, tr("Torrent queueing must be enabled"));
1237 const QStringList hashes
{params()[u
"hashes"_s
].split(u
'|')};
1238 BitTorrent::Session::instance()->topTorrentsQueuePos(toTorrentIDs(hashes
));
1241 void TorrentsController::bottomPrioAction()
1243 requireParams({u
"hashes"_s
});
1245 if (!BitTorrent::Session::instance()->isQueueingSystemEnabled())
1246 throw APIError(APIErrorType::Conflict
, tr("Torrent queueing must be enabled"));
1248 const QStringList hashes
{params()[u
"hashes"_s
].split(u
'|')};
1249 BitTorrent::Session::instance()->bottomTorrentsQueuePos(toTorrentIDs(hashes
));
1252 void TorrentsController::setLocationAction()
1254 requireParams({u
"hashes"_s
, u
"location"_s
});
1256 const QStringList hashes
{params()[u
"hashes"_s
].split(u
'|')};
1257 const Path newLocation
{params()[u
"location"_s
].trimmed()};
1259 if (newLocation
.isEmpty())
1260 throw APIError(APIErrorType::BadParams
, tr("Save path cannot be empty"));
1262 // try to create the location if it does not exist
1263 if (!Utils::Fs::mkpath(newLocation
))
1264 throw APIError(APIErrorType::Conflict
, tr("Cannot make save path"));
1266 applyToTorrents(hashes
, [newLocation
](BitTorrent::Torrent
*const torrent
)
1268 LogMsg(tr("WebUI Set location: moving \"%1\", from \"%2\" to \"%3\"")
1269 .arg(torrent
->name(), torrent
->savePath().toString(), newLocation
.toString()));
1270 torrent
->setAutoTMMEnabled(false);
1271 torrent
->setSavePath(newLocation
);
1275 void TorrentsController::setSavePathAction()
1277 requireParams({u
"id"_s
, u
"path"_s
});
1279 const QStringList ids
{params()[u
"id"_s
].split(u
'|')};
1280 const Path newPath
{params()[u
"path"_s
]};
1282 if (newPath
.isEmpty())
1283 throw APIError(APIErrorType::BadParams
, tr("Save path cannot be empty"));
1285 // try to create the directory if it does not exist
1286 if (!Utils::Fs::mkpath(newPath
))
1287 throw APIError(APIErrorType::Conflict
, tr("Cannot create target directory"));
1289 // check permissions
1290 if (!Utils::Fs::isWritable(newPath
))
1291 throw APIError(APIErrorType::AccessDenied
, tr("Cannot write to directory"));
1293 applyToTorrents(ids
, [&newPath
](BitTorrent::Torrent
*const torrent
)
1295 if (!torrent
->isAutoTMMEnabled())
1296 torrent
->setSavePath(newPath
);
1300 void TorrentsController::setDownloadPathAction()
1302 requireParams({u
"id"_s
, u
"path"_s
});
1304 const QStringList ids
{params()[u
"id"_s
].split(u
'|')};
1305 const Path newPath
{params()[u
"path"_s
]};
1307 if (!newPath
.isEmpty())
1309 // try to create the directory if it does not exist
1310 if (!Utils::Fs::mkpath(newPath
))
1311 throw APIError(APIErrorType::Conflict
, tr("Cannot create target directory"));
1313 // check permissions
1314 if (!Utils::Fs::isWritable(newPath
))
1315 throw APIError(APIErrorType::AccessDenied
, tr("Cannot write to directory"));
1318 applyToTorrents(ids
, [&newPath
](BitTorrent::Torrent
*const torrent
)
1320 if (!torrent
->isAutoTMMEnabled())
1321 torrent
->setDownloadPath(newPath
);
1325 void TorrentsController::renameAction()
1327 requireParams({u
"hash"_s
, u
"name"_s
});
1329 const auto id
= BitTorrent::TorrentID::fromString(params()[u
"hash"_s
]);
1330 QString name
= params()[u
"name"_s
].trimmed();
1333 throw APIError(APIErrorType::Conflict
, tr("Incorrect torrent name"));
1335 BitTorrent::Torrent
*const torrent
= BitTorrent::Session::instance()->getTorrent(id
);
1337 throw APIError(APIErrorType::NotFound
);
1339 name
.replace(QRegularExpression(u
"\r?\n|\r"_s
), u
" "_s
);
1340 torrent
->setName(name
);
1343 void TorrentsController::setAutoManagementAction()
1345 requireParams({u
"hashes"_s
, u
"enable"_s
});
1347 const QStringList hashes
{params()[u
"hashes"_s
].split(u
'|')};
1348 const bool isEnabled
{parseBool(params()[u
"enable"_s
]).value_or(false)};
1350 applyToTorrents(hashes
, [isEnabled
](BitTorrent::Torrent
*const torrent
)
1352 torrent
->setAutoTMMEnabled(isEnabled
);
1356 void TorrentsController::recheckAction()
1358 requireParams({u
"hashes"_s
});
1360 const QStringList hashes
{params()[u
"hashes"_s
].split(u
'|')};
1361 applyToTorrents(hashes
, [](BitTorrent::Torrent
*const torrent
) { torrent
->forceRecheck(); });
1364 void TorrentsController::reannounceAction()
1366 requireParams({u
"hashes"_s
});
1368 const QStringList hashes
{params()[u
"hashes"_s
].split(u
'|')};
1369 applyToTorrents(hashes
, [](BitTorrent::Torrent
*const torrent
) { torrent
->forceReannounce(); });
1372 void TorrentsController::setCategoryAction()
1374 requireParams({u
"hashes"_s
, u
"category"_s
});
1376 const QStringList hashes
{params()[u
"hashes"_s
].split(u
'|')};
1377 const QString category
{params()[u
"category"_s
]};
1379 applyToTorrents(hashes
, [category
](BitTorrent::Torrent
*const torrent
)
1381 if (!torrent
->setCategory(category
))
1382 throw APIError(APIErrorType::Conflict
, tr("Incorrect category name"));
1386 void TorrentsController::createCategoryAction()
1388 requireParams({u
"category"_s
});
1390 const QString category
= params()[u
"category"_s
];
1391 if (category
.isEmpty())
1392 throw APIError(APIErrorType::BadParams
, tr("Category cannot be empty"));
1394 if (!BitTorrent::Session::isValidCategoryName(category
))
1395 throw APIError(APIErrorType::Conflict
, tr("Incorrect category name"));
1397 const Path savePath
{params()[u
"savePath"_s
]};
1398 const auto useDownloadPath
= parseBool(params()[u
"downloadPathEnabled"_s
]);
1399 BitTorrent::CategoryOptions categoryOptions
;
1400 categoryOptions
.savePath
= savePath
;
1401 if (useDownloadPath
.has_value())
1403 const Path downloadPath
{params()[u
"downloadPath"_s
]};
1404 categoryOptions
.downloadPath
= {useDownloadPath
.value(), downloadPath
};
1407 if (!BitTorrent::Session::instance()->addCategory(category
, categoryOptions
))
1408 throw APIError(APIErrorType::Conflict
, tr("Unable to create category"));
1411 void TorrentsController::editCategoryAction()
1413 requireParams({u
"category"_s
, u
"savePath"_s
});
1415 const QString category
= params()[u
"category"_s
];
1416 if (category
.isEmpty())
1417 throw APIError(APIErrorType::BadParams
, tr("Category cannot be empty"));
1419 const Path savePath
{params()[u
"savePath"_s
]};
1420 const auto useDownloadPath
= parseBool(params()[u
"downloadPathEnabled"_s
]);
1421 BitTorrent::CategoryOptions categoryOptions
;
1422 categoryOptions
.savePath
= savePath
;
1423 if (useDownloadPath
.has_value())
1425 const Path downloadPath
{params()[u
"downloadPath"_s
]};
1426 categoryOptions
.downloadPath
= {useDownloadPath
.value(), downloadPath
};
1429 if (!BitTorrent::Session::instance()->editCategory(category
, categoryOptions
))
1430 throw APIError(APIErrorType::Conflict
, tr("Unable to edit category"));
1433 void TorrentsController::removeCategoriesAction()
1435 requireParams({u
"categories"_s
});
1437 const QStringList categories
{params()[u
"categories"_s
].split(u
'\n')};
1438 for (const QString
&category
: categories
)
1439 BitTorrent::Session::instance()->removeCategory(category
);
1442 void TorrentsController::categoriesAction()
1444 const auto *session
= BitTorrent::Session::instance();
1446 QJsonObject categories
;
1447 const QStringList categoriesList
= session
->categories();
1448 for (const auto &categoryName
: categoriesList
)
1450 const BitTorrent::CategoryOptions categoryOptions
= session
->categoryOptions(categoryName
);
1451 QJsonObject category
= categoryOptions
.toJSON();
1452 // adjust it to be compatible with existing WebAPI
1453 category
[u
"savePath"_s
] = category
.take(u
"save_path"_s
);
1454 category
.insert(u
"name"_s
, categoryName
);
1455 categories
[categoryName
] = category
;
1458 setResult(categories
);
1461 void TorrentsController::addTagsAction()
1463 requireParams({u
"hashes"_s
, u
"tags"_s
});
1465 const QStringList hashes
{params()[u
"hashes"_s
].split(u
'|')};
1466 const QStringList tags
{params()[u
"tags"_s
].split(u
',', Qt::SkipEmptyParts
)};
1468 for (const QString
&tagStr
: tags
)
1470 applyToTorrents(hashes
, [&tagStr
](BitTorrent::Torrent
*const torrent
)
1472 torrent
->addTag(Tag(tagStr
));
1477 void TorrentsController::removeTagsAction()
1479 requireParams({u
"hashes"_s
});
1481 const QStringList hashes
{params()[u
"hashes"_s
].split(u
'|')};
1482 const QStringList tags
{params()[u
"tags"_s
].split(u
',', Qt::SkipEmptyParts
)};
1484 for (const QString
&tagStr
: tags
)
1486 applyToTorrents(hashes
, [&tagStr
](BitTorrent::Torrent
*const torrent
)
1488 torrent
->removeTag(Tag(tagStr
));
1494 applyToTorrents(hashes
, [](BitTorrent::Torrent
*const torrent
)
1496 torrent
->removeAllTags();
1501 void TorrentsController::createTagsAction()
1503 requireParams({u
"tags"_s
});
1505 const QStringList tags
{params()[u
"tags"_s
].split(u
',', Qt::SkipEmptyParts
)};
1507 for (const QString
&tagStr
: tags
)
1508 BitTorrent::Session::instance()->addTag(Tag(tagStr
));
1511 void TorrentsController::deleteTagsAction()
1513 requireParams({u
"tags"_s
});
1515 const QStringList tags
{params()[u
"tags"_s
].split(u
',', Qt::SkipEmptyParts
)};
1516 for (const QString
&tagStr
: tags
)
1517 BitTorrent::Session::instance()->removeTag(Tag(tagStr
));
1520 void TorrentsController::tagsAction()
1523 for (const Tag
&tag
: asConst(BitTorrent::Session::instance()->tags()))
1524 result
<< tag
.toString();
1528 void TorrentsController::renameFileAction()
1530 requireParams({u
"hash"_s
, u
"oldPath"_s
, u
"newPath"_s
});
1532 const auto id
= BitTorrent::TorrentID::fromString(params()[u
"hash"_s
]);
1533 BitTorrent::Torrent
*const torrent
= BitTorrent::Session::instance()->getTorrent(id
);
1535 throw APIError(APIErrorType::NotFound
);
1537 const Path oldPath
{params()[u
"oldPath"_s
]};
1538 const Path newPath
{params()[u
"newPath"_s
]};
1542 torrent
->renameFile(oldPath
, newPath
);
1544 catch (const RuntimeError
&error
)
1546 throw APIError(APIErrorType::Conflict
, error
.message());
1550 void TorrentsController::renameFolderAction()
1552 requireParams({u
"hash"_s
, u
"oldPath"_s
, u
"newPath"_s
});
1554 const auto id
= BitTorrent::TorrentID::fromString(params()[u
"hash"_s
]);
1555 BitTorrent::Torrent
*const torrent
= BitTorrent::Session::instance()->getTorrent(id
);
1557 throw APIError(APIErrorType::NotFound
);
1559 const Path oldPath
{params()[u
"oldPath"_s
]};
1560 const Path newPath
{params()[u
"newPath"_s
]};
1564 torrent
->renameFolder(oldPath
, newPath
);
1566 catch (const RuntimeError
&error
)
1568 throw APIError(APIErrorType::Conflict
, error
.message());
1572 void TorrentsController::exportAction()
1574 requireParams({u
"hash"_s
});
1576 const auto id
= BitTorrent::TorrentID::fromString(params()[u
"hash"_s
]);
1577 const BitTorrent::Torrent
*const torrent
= BitTorrent::Session::instance()->getTorrent(id
);
1579 throw APIError(APIErrorType::NotFound
);
1581 const nonstd::expected
<QByteArray
, QString
> result
= torrent
->exportToBuffer();
1583 throw APIError(APIErrorType::Conflict
, tr("Unable to export torrent file. Error: %1").arg(result
.error()));
1585 setResult(result
.value(), u
"application/x-bittorrent"_s
, (id
.toString() + u
".torrent"));
1588 void TorrentsController::SSLParametersAction()
1590 requireParams({u
"hash"_s
});
1592 const auto id
= BitTorrent::TorrentID::fromString(params()[u
"hash"_s
]);
1593 const BitTorrent::Torrent
*torrent
= BitTorrent::Session::instance()->getTorrent(id
);
1595 throw APIError(APIErrorType::NotFound
);
1597 const BitTorrent::SSLParameters sslParams
= torrent
->getSSLParameters();
1598 const QJsonObject ret
1600 {KEY_PROP_SSL_CERTIFICATE
, QString::fromLatin1(sslParams
.certificate
.toPem())},
1601 {KEY_PROP_SSL_PRIVATEKEY
, QString::fromLatin1(sslParams
.privateKey
.toPem())},
1602 {KEY_PROP_SSL_DHPARAMS
, QString::fromLatin1(sslParams
.dhParams
)}
1607 void TorrentsController::setSSLParametersAction()
1609 requireParams({u
"hash"_s
, KEY_PROP_SSL_CERTIFICATE
, KEY_PROP_SSL_PRIVATEKEY
, KEY_PROP_SSL_DHPARAMS
});
1611 const auto id
= BitTorrent::TorrentID::fromString(params()[u
"hash"_s
]);
1612 BitTorrent::Torrent
*const torrent
= BitTorrent::Session::instance()->getTorrent(id
);
1614 throw APIError(APIErrorType::NotFound
);
1616 const BitTorrent::SSLParameters sslParams
1618 .certificate
= QSslCertificate(params()[KEY_PROP_SSL_CERTIFICATE
].toLatin1()),
1619 .privateKey
= Utils::SSLKey::load(params()[KEY_PROP_SSL_PRIVATEKEY
].toLatin1()),
1620 .dhParams
= params()[KEY_PROP_SSL_DHPARAMS
].toLatin1()
1622 if (!sslParams
.isValid())
1623 throw APIError(APIErrorType::BadData
);
1625 torrent
->setSSLParameters(sslParams
);