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_POPULARITY
= u
"popularity"_s
;
101 const QString KEY_PROP_REANNOUNCE
= u
"reannounce"_s
;
102 const QString KEY_PROP_TOTAL_SIZE
= u
"total_size"_s
;
103 const QString KEY_PROP_PIECES_NUM
= u
"pieces_num"_s
;
104 const QString KEY_PROP_PIECE_SIZE
= u
"piece_size"_s
;
105 const QString KEY_PROP_PIECES_HAVE
= u
"pieces_have"_s
;
106 const QString KEY_PROP_CREATED_BY
= u
"created_by"_s
;
107 const QString KEY_PROP_LAST_SEEN
= u
"last_seen"_s
;
108 const QString KEY_PROP_ADDITION_DATE
= u
"addition_date"_s
;
109 const QString KEY_PROP_COMPLETION_DATE
= u
"completion_date"_s
;
110 const QString KEY_PROP_CREATION_DATE
= u
"creation_date"_s
;
111 const QString KEY_PROP_SAVE_PATH
= u
"save_path"_s
;
112 const QString KEY_PROP_DOWNLOAD_PATH
= u
"download_path"_s
;
113 const QString KEY_PROP_COMMENT
= u
"comment"_s
;
114 const QString KEY_PROP_IS_PRIVATE
= u
"is_private"_s
; // deprecated, "private" should be used instead
115 const QString KEY_PROP_PRIVATE
= u
"private"_s
;
116 const QString KEY_PROP_SSL_CERTIFICATE
= u
"ssl_certificate"_s
;
117 const QString KEY_PROP_SSL_PRIVATEKEY
= u
"ssl_private_key"_s
;
118 const QString KEY_PROP_SSL_DHPARAMS
= u
"ssl_dh_params"_s
;
119 const QString KEY_PROP_HAS_METADATA
= u
"has_metadata"_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 void applyToTorrents(const QStringList
&idList
, const std::function
<void (BitTorrent::Torrent
*torrent
)> &func
)
142 if ((idList
.size() == 1) && (idList
[0] == u
"all"))
144 for (BitTorrent::Torrent
*const torrent
: asConst(BitTorrent::Session::instance()->torrents()))
149 for (const QString
&idString
: idList
)
151 const auto hash
= BitTorrent::TorrentID::fromString(idString
);
152 BitTorrent::Torrent
*const torrent
= BitTorrent::Session::instance()->getTorrent(hash
);
159 std::optional
<QString
> getOptionalString(const StringMap
¶ms
, const QString
&name
)
161 const auto it
= params
.constFind(name
);
162 if (it
== params
.cend())
168 std::optional
<Tag
> getOptionalTag(const StringMap
¶ms
, const QString
&name
)
170 const auto it
= params
.constFind(name
);
171 if (it
== params
.cend())
174 return Tag(it
.value());
177 QJsonArray
getStickyTrackers(const BitTorrent::Torrent
*const torrent
)
179 int seedsDHT
= 0, seedsPeX
= 0, seedsLSD
= 0, leechesDHT
= 0, leechesPeX
= 0, leechesLSD
= 0;
180 for (const BitTorrent::PeerInfo
&peer
: asConst(torrent
->peers()))
182 if (peer
.isConnecting()) continue;
204 const int working
= static_cast<int>(BitTorrent::TrackerEndpointState::Working
);
205 const int disabled
= 0;
207 const QString privateMsg
{QCoreApplication::translate("TrackerListWidget", "This torrent is private")};
208 const bool isTorrentPrivate
= torrent
->isPrivate();
210 const QJsonObject dht
212 {KEY_TRACKER_URL
, u
"** [DHT] **"_s
},
213 {KEY_TRACKER_TIER
, -1},
214 {KEY_TRACKER_MSG
, (isTorrentPrivate
? privateMsg
: u
""_s
)},
215 {KEY_TRACKER_STATUS
, ((BitTorrent::Session::instance()->isDHTEnabled() && !isTorrentPrivate
) ? working
: disabled
)},
216 {KEY_TRACKER_PEERS_COUNT
, 0},
217 {KEY_TRACKER_DOWNLOADED_COUNT
, 0},
218 {KEY_TRACKER_SEEDS_COUNT
, seedsDHT
},
219 {KEY_TRACKER_LEECHES_COUNT
, leechesDHT
}
222 const QJsonObject pex
224 {KEY_TRACKER_URL
, u
"** [PeX] **"_s
},
225 {KEY_TRACKER_TIER
, -1},
226 {KEY_TRACKER_MSG
, (isTorrentPrivate
? privateMsg
: u
""_s
)},
227 {KEY_TRACKER_STATUS
, ((BitTorrent::Session::instance()->isPeXEnabled() && !isTorrentPrivate
) ? working
: disabled
)},
228 {KEY_TRACKER_PEERS_COUNT
, 0},
229 {KEY_TRACKER_DOWNLOADED_COUNT
, 0},
230 {KEY_TRACKER_SEEDS_COUNT
, seedsPeX
},
231 {KEY_TRACKER_LEECHES_COUNT
, leechesPeX
}
234 const QJsonObject lsd
236 {KEY_TRACKER_URL
, u
"** [LSD] **"_s
},
237 {KEY_TRACKER_TIER
, -1},
238 {KEY_TRACKER_MSG
, (isTorrentPrivate
? privateMsg
: u
""_s
)},
239 {KEY_TRACKER_STATUS
, ((BitTorrent::Session::instance()->isLSDEnabled() && !isTorrentPrivate
) ? working
: disabled
)},
240 {KEY_TRACKER_PEERS_COUNT
, 0},
241 {KEY_TRACKER_DOWNLOADED_COUNT
, 0},
242 {KEY_TRACKER_SEEDS_COUNT
, seedsLSD
},
243 {KEY_TRACKER_LEECHES_COUNT
, leechesLSD
}
246 return {dht
, pex
, lsd
};
249 QList
<BitTorrent::TorrentID
> toTorrentIDs(const QStringList
&idStrings
)
251 QList
<BitTorrent::TorrentID
> idList
;
252 idList
.reserve(idStrings
.size());
253 for (const QString
&hash
: idStrings
)
254 idList
<< BitTorrent::TorrentID::fromString(hash
);
258 nonstd::expected
<QUrl
, QString
> validateWebSeedUrl(const QString
&urlStr
)
260 const QString normalizedUrlStr
= QUrl::fromPercentEncoding(urlStr
.toLatin1());
262 const QUrl url
{normalizedUrlStr
, QUrl::StrictMode
};
264 return nonstd::make_unexpected(TorrentsController::tr("\"%1\" is not a valid URL").arg(normalizedUrlStr
));
266 if (!SUPPORTED_WEB_SEED_SCHEMES
.contains(url
.scheme()))
267 return nonstd::make_unexpected(TorrentsController::tr("URL scheme must be one of [%1]").arg(SUPPORTED_WEB_SEED_SCHEMES
.values().join(u
", ")));
273 void TorrentsController::countAction()
275 setResult(QString::number(BitTorrent::Session::instance()->torrentsCount()));
278 // Returns all the torrents in JSON format.
279 // The return value is a JSON-formatted list of dictionaries.
280 // The dictionary keys are:
281 // - "hash": Torrent hash (ID)
282 // - "name": Torrent name
283 // - "size": Torrent size
284 // - "progress": Torrent progress
285 // - "dlspeed": Torrent download speed
286 // - "upspeed": Torrent upload speed
287 // - "priority": Torrent queue position (-1 if queuing is disabled)
288 // - "num_seeds": Torrent seeds connected to
289 // - "num_complete": Torrent seeds in the swarm
290 // - "num_leechs": Torrent leechers connected to
291 // - "num_incomplete": Torrent leechers in the swarm
292 // - "ratio": Torrent share ratio
293 // - "eta": Torrent ETA
294 // - "state": Torrent state
295 // - "seq_dl": Torrent sequential download state
296 // - "f_l_piece_prio": Torrent first last piece priority state
297 // - "force_start": Torrent force start state
298 // - "category": Torrent category
300 // - filter (string): all, downloading, seeding, completed, stopped, running, active, inactive, stalled, stalled_uploading, stalled_downloading
301 // - category (string): torrent category for filtering by it (empty string means "uncategorized"; no "category" param presented means "any category")
302 // - tag (string): torrent tag for filtering by it (empty string means "untagged"; no "tag" param presented means "any tag")
303 // - hashes (string): filter by hashes, can contain multiple hashes separated by |
304 // - private (bool): filter torrents that are from private trackers (true) or not (false). Empty means any torrent (no filtering)
305 // - sort (string): name of column for sorting by its value
306 // - reverse (bool): enable reverse sorting
307 // - limit (int): set limit number of torrents returned (if greater than 0, otherwise - unlimited)
308 // - offset (int): set offset (if less than 0 - offset from end)
309 void TorrentsController::infoAction()
311 const QString filter
{params()[u
"filter"_s
]};
312 const std::optional
<QString
> category
= getOptionalString(params(), u
"category"_s
);
313 const std::optional
<Tag
> tag
= getOptionalTag(params(), u
"tag"_s
);
314 const QString sortedColumn
{params()[u
"sort"_s
]};
315 const bool reverse
{parseBool(params()[u
"reverse"_s
]).value_or(false)};
316 int limit
{params()[u
"limit"_s
].toInt()};
317 int offset
{params()[u
"offset"_s
].toInt()};
318 const QStringList hashes
{params()[u
"hashes"_s
].split(u
'|', Qt::SkipEmptyParts
)};
319 const std::optional
<bool> isPrivate
= parseBool(params()[u
"private"_s
]);
321 std::optional
<TorrentIDSet
> idSet
;
322 if (!hashes
.isEmpty())
324 idSet
= TorrentIDSet();
325 for (const QString
&hash
: hashes
)
326 idSet
->insert(BitTorrent::TorrentID::fromString(hash
));
329 const TorrentFilter torrentFilter
{filter
, idSet
, category
, tag
, isPrivate
};
330 QVariantList torrentList
;
331 for (const BitTorrent::Torrent
*torrent
: asConst(BitTorrent::Session::instance()->torrents()))
333 if (torrentFilter
.match(torrent
))
334 torrentList
.append(serialize(*torrent
));
337 if (torrentList
.isEmpty())
339 setResult(QJsonArray
{});
343 if (!sortedColumn
.isEmpty())
345 if (!torrentList
[0].toMap().contains(sortedColumn
))
346 throw APIError(APIErrorType::BadParams
, tr("'sort' parameter is invalid"));
348 const auto lessThan
= [](const QVariant
&left
, const QVariant
&right
) -> bool
350 Q_ASSERT(left
.userType() == right
.userType());
352 switch (left
.userType())
354 case QMetaType::Bool
:
355 return left
.value
<bool>() < right
.value
<bool>();
356 case QMetaType::Double
:
357 return left
.value
<double>() < right
.value
<double>();
358 case QMetaType::Float
:
359 return left
.value
<float>() < right
.value
<float>();
361 return left
.value
<int>() < right
.value
<int>();
362 case QMetaType::LongLong
:
363 return left
.value
<qlonglong
>() < right
.value
<qlonglong
>();
364 case QMetaType::QString
:
365 return left
.value
<QString
>() < right
.value
<QString
>();
367 qWarning("Unhandled QVariant comparison, type: %d, name: %s"
368 , left
.userType(), left
.metaType().name());
374 std::sort(torrentList
.begin(), torrentList
.end()
375 , [reverse
, &sortedColumn
, &lessThan
](const QVariant
&torrent1
, const QVariant
&torrent2
)
377 const QVariant value1
{torrent1
.toMap().value(sortedColumn
)};
378 const QVariant value2
{torrent2
.toMap().value(sortedColumn
)};
379 return reverse
? lessThan(value2
, value1
) : lessThan(value1
, value2
);
383 const int size
= torrentList
.size();
386 offset
= size
+ offset
;
387 if ((offset
>= size
) || (offset
< 0))
391 limit
= -1; // unlimited
393 if ((limit
> 0) || (offset
> 0))
394 torrentList
= torrentList
.mid(offset
, limit
);
396 setResult(QJsonArray::fromVariantList(torrentList
));
399 // Returns the properties for a torrent in JSON format.
400 // The return value is a JSON-formatted dictionary.
401 // The dictionary keys are:
402 // - "time_elapsed": Torrent elapsed time
403 // - "seeding_time": Torrent elapsed time while complete
404 // - "eta": Torrent ETA
405 // - "nb_connections": Torrent connection count
406 // - "nb_connections_limit": Torrent connection count limit
407 // - "total_downloaded": Total data uploaded for torrent
408 // - "total_downloaded_session": Total data downloaded this session
409 // - "total_uploaded": Total data uploaded for torrent
410 // - "total_uploaded_session": Total data uploaded this session
411 // - "dl_speed": Torrent download speed
412 // - "dl_speed_avg": Torrent average download speed
413 // - "up_speed": Torrent upload speed
414 // - "up_speed_avg": Torrent average upload speed
415 // - "dl_limit": Torrent download limit
416 // - "up_limit": Torrent upload limit
417 // - "total_wasted": Total data wasted for torrent
418 // - "seeds": Torrent connected seeds
419 // - "seeds_total": Torrent total number of seeds
420 // - "peers": Torrent connected peers
421 // - "peers_total": Torrent total number of peers
422 // - "share_ratio": Torrent share ratio
423 // - "popularity": Torrent popularity
424 // - "reannounce": Torrent next reannounce time
425 // - "total_size": Torrent total size
426 // - "pieces_num": Torrent pieces count
427 // - "piece_size": Torrent piece size
428 // - "pieces_have": Torrent pieces have
429 // - "created_by": Torrent creator
430 // - "last_seen": Torrent last seen complete
431 // - "addition_date": Torrent addition date
432 // - "completion_date": Torrent completion date
433 // - "creation_date": Torrent creation date
434 // - "save_path": Torrent save path
435 // - "download_path": Torrent download path
436 // - "comment": Torrent comment
437 // - "infohash_v1": Torrent v1 infohash (or empty string for v2 torrents)
438 // - "infohash_v2": Torrent v2 infohash (or empty string for v1 torrents)
439 // - "hash": Torrent TorrentID (infohashv1 for v1 torrents, truncated infohashv2 for v2/hybrid torrents)
440 // - "name": Torrent name
441 void TorrentsController::propertiesAction()
443 requireParams({u
"hash"_s
});
445 const auto id
= BitTorrent::TorrentID::fromString(params()[u
"hash"_s
]);
446 const BitTorrent::Torrent
*const torrent
= BitTorrent::Session::instance()->getTorrent(id
);
448 throw APIError(APIErrorType::NotFound
);
450 const BitTorrent::InfoHash infoHash
= torrent
->infoHash();
451 const qlonglong totalDownload
= torrent
->totalDownload();
452 const qlonglong totalUpload
= torrent
->totalUpload();
453 const qlonglong dlDuration
= torrent
->activeTime() - torrent
->finishedTime();
454 const qlonglong ulDuration
= torrent
->activeTime();
455 const int downloadLimit
= torrent
->downloadLimit();
456 const int uploadLimit
= torrent
->uploadLimit();
457 const qreal ratio
= torrent
->realRatio();
458 const qreal popularity
= torrent
->popularity();
459 const bool hasMetadata
= torrent
->hasMetadata();
460 const bool isPrivate
= torrent
->isPrivate();
462 const QJsonObject ret
464 {KEY_TORRENT_INFOHASHV1
, infoHash
.v1().toString()},
465 {KEY_TORRENT_INFOHASHV2
, infoHash
.v2().toString()},
466 {KEY_TORRENT_NAME
, torrent
->name()},
467 {KEY_TORRENT_ID
, torrent
->id().toString()},
468 {KEY_PROP_TIME_ELAPSED
, torrent
->activeTime()},
469 {KEY_PROP_SEEDING_TIME
, torrent
->finishedTime()},
470 {KEY_PROP_ETA
, torrent
->eta()},
471 {KEY_PROP_CONNECT_COUNT
, torrent
->connectionsCount()},
472 {KEY_PROP_CONNECT_COUNT_LIMIT
, torrent
->connectionsLimit()},
473 {KEY_PROP_DOWNLOADED
, totalDownload
},
474 {KEY_PROP_DOWNLOADED_SESSION
, torrent
->totalPayloadDownload()},
475 {KEY_PROP_UPLOADED
, totalUpload
},
476 {KEY_PROP_UPLOADED_SESSION
, torrent
->totalPayloadUpload()},
477 {KEY_PROP_DL_SPEED
, torrent
->downloadPayloadRate()},
478 {KEY_PROP_DL_SPEED_AVG
, ((dlDuration
> 0) ? (totalDownload
/ dlDuration
) : -1)},
479 {KEY_PROP_UP_SPEED
, torrent
->uploadPayloadRate()},
480 {KEY_PROP_UP_SPEED_AVG
, ((ulDuration
> 0) ? (totalUpload
/ ulDuration
) : -1)},
481 {KEY_PROP_DL_LIMIT
, ((downloadLimit
> 0) ? downloadLimit
: -1)},
482 {KEY_PROP_UP_LIMIT
, ((uploadLimit
> 0) ? uploadLimit
: -1)},
483 {KEY_PROP_WASTED
, torrent
->wastedSize()},
484 {KEY_PROP_SEEDS
, torrent
->seedsCount()},
485 {KEY_PROP_SEEDS_TOTAL
, torrent
->totalSeedsCount()},
486 {KEY_PROP_PEERS
, torrent
->leechsCount()},
487 {KEY_PROP_PEERS_TOTAL
, torrent
->totalLeechersCount()},
488 {KEY_PROP_RATIO
, ((ratio
> BitTorrent::Torrent::MAX_RATIO
) ? -1 : ratio
)},
489 {KEY_PROP_POPULARITY
, ((popularity
> BitTorrent::Torrent::MAX_RATIO
) ? -1 : popularity
)},
490 {KEY_PROP_REANNOUNCE
, torrent
->nextAnnounce()},
491 {KEY_PROP_TOTAL_SIZE
, torrent
->totalSize()},
492 {KEY_PROP_PIECES_NUM
, torrent
->piecesCount()},
493 {KEY_PROP_PIECE_SIZE
, torrent
->pieceLength()},
494 {KEY_PROP_PIECES_HAVE
, torrent
->piecesHave()},
495 {KEY_PROP_CREATED_BY
, torrent
->creator()},
496 {KEY_PROP_IS_PRIVATE
, torrent
->isPrivate()}, // used for maintaining backward compatibility
497 {KEY_PROP_PRIVATE
, (hasMetadata
? isPrivate
: QJsonValue())},
498 {KEY_PROP_ADDITION_DATE
, Utils::DateTime::toSecsSinceEpoch(torrent
->addedTime())},
499 {KEY_PROP_LAST_SEEN
, Utils::DateTime::toSecsSinceEpoch(torrent
->lastSeenComplete())},
500 {KEY_PROP_COMPLETION_DATE
, Utils::DateTime::toSecsSinceEpoch(torrent
->completedTime())},
501 {KEY_PROP_CREATION_DATE
, Utils::DateTime::toSecsSinceEpoch(torrent
->creationDate())},
502 {KEY_PROP_SAVE_PATH
, torrent
->savePath().toString()},
503 {KEY_PROP_DOWNLOAD_PATH
, torrent
->downloadPath().toString()},
504 {KEY_PROP_COMMENT
, torrent
->comment()},
505 {KEY_PROP_HAS_METADATA
, torrent
->hasMetadata()}
511 // Returns the trackers for a torrent in JSON format.
512 // The return value is a JSON-formatted list of dictionaries.
513 // The dictionary keys are:
514 // - "url": Tracker URL
515 // - "status": Tracker status
516 // - "tier": Tracker tier
517 // - "num_peers": Number of peers this torrent is currently connected to
518 // - "num_seeds": Number of peers that have the whole file
519 // - "num_leeches": Number of peers that are still downloading
520 // - "num_downloaded": Tracker downloaded count
521 // - "msg": Tracker message (last)
522 void TorrentsController::trackersAction()
524 requireParams({u
"hash"_s
});
526 const auto id
= BitTorrent::TorrentID::fromString(params()[u
"hash"_s
]);
527 const BitTorrent::Torrent
*const torrent
= BitTorrent::Session::instance()->getTorrent(id
);
529 throw APIError(APIErrorType::NotFound
);
531 QJsonArray trackerList
= getStickyTrackers(torrent
);
533 for (const BitTorrent::TrackerEntryStatus
&tracker
: asConst(torrent
->trackers()))
535 const bool isNotWorking
= (tracker
.state
== BitTorrent::TrackerEndpointState::NotWorking
)
536 || (tracker
.state
== BitTorrent::TrackerEndpointState::TrackerError
)
537 || (tracker
.state
== BitTorrent::TrackerEndpointState::Unreachable
);
538 trackerList
<< QJsonObject
540 {KEY_TRACKER_URL
, tracker
.url
},
541 {KEY_TRACKER_TIER
, tracker
.tier
},
542 {KEY_TRACKER_STATUS
, static_cast<int>((isNotWorking
? BitTorrent::TrackerEndpointState::NotWorking
: tracker
.state
))},
543 {KEY_TRACKER_MSG
, tracker
.message
},
544 {KEY_TRACKER_PEERS_COUNT
, tracker
.numPeers
},
545 {KEY_TRACKER_SEEDS_COUNT
, tracker
.numSeeds
},
546 {KEY_TRACKER_LEECHES_COUNT
, tracker
.numLeeches
},
547 {KEY_TRACKER_DOWNLOADED_COUNT
, tracker
.numDownloaded
}
551 setResult(trackerList
);
554 // Returns the web seeds for a torrent in JSON format.
555 // The return value is a JSON-formatted list of dictionaries.
556 // The dictionary keys are:
557 // - "url": Web seed URL
558 void TorrentsController::webseedsAction()
560 requireParams({u
"hash"_s
});
562 const auto id
= BitTorrent::TorrentID::fromString(params()[u
"hash"_s
]);
563 BitTorrent::Torrent
*const torrent
= BitTorrent::Session::instance()->getTorrent(id
);
565 throw APIError(APIErrorType::NotFound
);
567 QJsonArray webSeedList
;
568 for (const QUrl
&webseed
: asConst(torrent
->urlSeeds()))
570 webSeedList
.append(QJsonObject
572 {KEY_WEBSEED_URL
, webseed
.toString()}
576 setResult(webSeedList
);
579 void TorrentsController::addWebSeedsAction()
581 requireParams({u
"hash"_s
, u
"urls"_s
});
582 const QStringList paramUrls
= params()[u
"urls"_s
].split(u
'|', Qt::SkipEmptyParts
);
584 const auto id
= BitTorrent::TorrentID::fromString(params()[u
"hash"_s
]);
585 BitTorrent::Torrent
*const torrent
= BitTorrent::Session::instance()->getTorrent(id
);
587 throw APIError(APIErrorType::NotFound
);
590 urls
.reserve(paramUrls
.size());
591 for (const QString
&urlStr
: paramUrls
)
593 const auto result
= validateWebSeedUrl(urlStr
);
595 throw APIError(APIErrorType::BadParams
, result
.error());
596 urls
<< result
.value();
599 torrent
->addUrlSeeds(urls
);
602 void TorrentsController::editWebSeedAction()
604 requireParams({u
"hash"_s
, u
"origUrl"_s
, u
"newUrl"_s
});
606 const auto id
= BitTorrent::TorrentID::fromString(params()[u
"hash"_s
]);
607 const QString origUrlStr
= params()[u
"origUrl"_s
];
608 const QString newUrlStr
= params()[u
"newUrl"_s
];
610 BitTorrent::Torrent
*const torrent
= BitTorrent::Session::instance()->getTorrent(id
);
612 throw APIError(APIErrorType::NotFound
);
614 const auto origUrlResult
= validateWebSeedUrl(origUrlStr
);
616 throw APIError(APIErrorType::BadParams
, origUrlResult
.error());
617 const QUrl origUrl
= origUrlResult
.value();
619 const auto newUrlResult
= validateWebSeedUrl(newUrlStr
);
621 throw APIError(APIErrorType::BadParams
, newUrlResult
.error());
622 const QUrl newUrl
= newUrlResult
.value();
624 if (newUrl
!= origUrl
)
626 if (!torrent
->urlSeeds().contains(origUrl
))
627 throw APIError(APIErrorType::Conflict
, tr("\"%1\" is not an existing URL").arg(origUrl
.toString()));
629 torrent
->removeUrlSeeds({origUrl
});
630 torrent
->addUrlSeeds({newUrl
});
634 void TorrentsController::removeWebSeedsAction()
636 requireParams({u
"hash"_s
, u
"urls"_s
});
637 const QStringList paramUrls
= params()[u
"urls"_s
].split(u
'|', Qt::SkipEmptyParts
);
639 const auto id
= BitTorrent::TorrentID::fromString(params()[u
"hash"_s
]);
640 BitTorrent::Torrent
*const torrent
= BitTorrent::Session::instance()->getTorrent(id
);
642 throw APIError(APIErrorType::NotFound
);
645 urls
.reserve(paramUrls
.size());
646 for (const QString
&urlStr
: paramUrls
)
648 const auto result
= validateWebSeedUrl(urlStr
);
650 throw APIError(APIErrorType::BadParams
, result
.error());
651 urls
<< result
.value();
654 torrent
->removeUrlSeeds(urls
);
657 // Returns the files in a torrent in JSON format.
658 // The return value is a JSON-formatted list of dictionaries.
659 // The dictionary keys are:
660 // - "index": File index
661 // - "name": File name
662 // - "size": File size
663 // - "progress": File progress
664 // - "priority": File priority
665 // - "is_seed": Flag indicating if torrent is seeding/complete
666 // - "piece_range": Piece index range, the first number is the starting piece index
667 // and the second number is the ending piece index (inclusive)
668 void TorrentsController::filesAction()
670 requireParams({u
"hash"_s
});
672 const auto id
= BitTorrent::TorrentID::fromString(params()[u
"hash"_s
]);
673 const BitTorrent::Torrent
*const torrent
= BitTorrent::Session::instance()->getTorrent(id
);
675 throw APIError(APIErrorType::NotFound
);
677 const int filesCount
= torrent
->filesCount();
678 QList
<int> fileIndexes
;
679 const auto idxIt
= params().constFind(u
"indexes"_s
);
680 if (idxIt
!= params().cend())
682 const QStringList indexStrings
= idxIt
.value().split(u
'|');
683 fileIndexes
.reserve(indexStrings
.size());
684 std::transform(indexStrings
.cbegin(), indexStrings
.cend(), std::back_inserter(fileIndexes
)
685 , [&filesCount
](const QString
&indexString
) -> int
688 const int index
= indexString
.toInt(&ok
);
689 if (!ok
|| (index
< 0))
690 throw APIError(APIErrorType::Conflict
, tr("\"%1\" is not a valid file index.").arg(indexString
));
691 if (index
>= filesCount
)
692 throw APIError(APIErrorType::Conflict
, tr("Index %1 is out of bounds.").arg(indexString
));
698 fileIndexes
.reserve(filesCount
);
699 for (int i
= 0; i
< filesCount
; ++i
)
700 fileIndexes
.append(i
);
704 if (torrent
->hasMetadata())
706 const QList
<BitTorrent::DownloadPriority
> priorities
= torrent
->filePriorities();
707 const QList
<qreal
> fp
= torrent
->filesProgress();
708 const QList
<qreal
> fileAvailability
= torrent
->availableFileFractions();
709 const BitTorrent::TorrentInfo info
= torrent
->info();
710 for (const int index
: asConst(fileIndexes
))
712 QJsonObject fileDict
=
714 {KEY_FILE_INDEX
, index
},
715 {KEY_FILE_PROGRESS
, fp
[index
]},
716 {KEY_FILE_PRIORITY
, static_cast<int>(priorities
[index
])},
717 {KEY_FILE_SIZE
, torrent
->fileSize(index
)},
718 {KEY_FILE_AVAILABILITY
, fileAvailability
[index
]},
719 // need to provide paths using a platform-independent separator format
720 {KEY_FILE_NAME
, torrent
->filePath(index
).data()}
723 const BitTorrent::TorrentInfo::PieceRange idx
= info
.filePieces(index
);
724 fileDict
[KEY_FILE_PIECE_RANGE
] = QJsonArray
{idx
.first(), idx
.last()};
727 fileDict
[KEY_FILE_IS_SEED
] = torrent
->isFinished();
729 fileList
.append(fileDict
);
736 // Returns an array of hashes (of each pieces respectively) for a torrent in JSON format.
737 // The return value is a JSON-formatted array of strings (hex strings).
738 void TorrentsController::pieceHashesAction()
740 requireParams({u
"hash"_s
});
742 const auto id
= BitTorrent::TorrentID::fromString(params()[u
"hash"_s
]);
743 BitTorrent::Torrent
*const torrent
= BitTorrent::Session::instance()->getTorrent(id
);
745 throw APIError(APIErrorType::NotFound
);
747 QJsonArray pieceHashes
;
748 if (torrent
->hasMetadata())
750 const QList
<QByteArray
> hashes
= torrent
->info().pieceHashes();
751 for (const QByteArray
&hash
: hashes
)
752 pieceHashes
.append(QString::fromLatin1(hash
.toHex()));
755 setResult(pieceHashes
);
758 // Returns an array of states (of each pieces respectively) for a torrent in JSON format.
759 // The return value is a JSON-formatted array of ints.
760 // 0: piece not downloaded
761 // 1: piece requested or downloading
762 // 2: piece already downloaded
763 void TorrentsController::pieceStatesAction()
765 requireParams({u
"hash"_s
});
767 const auto id
= BitTorrent::TorrentID::fromString(params()[u
"hash"_s
]);
768 BitTorrent::Torrent
*const torrent
= BitTorrent::Session::instance()->getTorrent(id
);
770 throw APIError(APIErrorType::NotFound
);
772 QJsonArray pieceStates
;
773 const QBitArray states
= torrent
->pieces();
774 for (int i
= 0; i
< states
.size(); ++i
)
775 pieceStates
.append(static_cast<int>(states
[i
]) * 2);
777 const QBitArray dlstates
= torrent
->downloadingPieces();
778 for (int i
= 0; i
< states
.size(); ++i
)
784 setResult(pieceStates
);
787 void TorrentsController::addAction()
789 const QString urls
= params()[u
"urls"_s
];
790 const QString cookie
= params()[u
"cookie"_s
];
792 const bool skipChecking
= parseBool(params()[u
"skip_checking"_s
]).value_or(false);
793 const bool seqDownload
= parseBool(params()[u
"sequentialDownload"_s
]).value_or(false);
794 const bool firstLastPiece
= parseBool(params()[u
"firstLastPiecePrio"_s
]).value_or(false);
795 const std::optional
<bool> addToQueueTop
= parseBool(params()[u
"addToTopOfQueue"_s
]);
796 const std::optional
<bool> addStopped
= parseBool(params()[u
"stopped"_s
]);
797 const QString savepath
= params()[u
"savepath"_s
].trimmed();
798 const QString downloadPath
= params()[u
"downloadPath"_s
].trimmed();
799 const std::optional
<bool> useDownloadPath
= parseBool(params()[u
"useDownloadPath"_s
]);
800 const QString category
= params()[u
"category"_s
];
801 const QStringList tags
= params()[u
"tags"_s
].split(u
',', Qt::SkipEmptyParts
);
802 const QString torrentName
= params()[u
"rename"_s
].trimmed();
803 const int upLimit
= parseInt(params()[u
"upLimit"_s
]).value_or(-1);
804 const int dlLimit
= parseInt(params()[u
"dlLimit"_s
]).value_or(-1);
805 const double ratioLimit
= parseDouble(params()[u
"ratioLimit"_s
]).value_or(BitTorrent::Torrent::USE_GLOBAL_RATIO
);
806 const int seedingTimeLimit
= parseInt(params()[u
"seedingTimeLimit"_s
]).value_or(BitTorrent::Torrent::USE_GLOBAL_SEEDING_TIME
);
807 const int inactiveSeedingTimeLimit
= parseInt(params()[u
"inactiveSeedingTimeLimit"_s
]).value_or(BitTorrent::Torrent::USE_GLOBAL_INACTIVE_SEEDING_TIME
);
808 const BitTorrent::ShareLimitAction shareLimitAction
= Utils::String::toEnum(params()[u
"shareLimitAction"_s
], BitTorrent::ShareLimitAction::Default
);
809 const std::optional
<bool> autoTMM
= parseBool(params()[u
"autoTMM"_s
]);
811 const QString stopConditionParam
= params()[u
"stopCondition"_s
];
812 const std::optional
<BitTorrent::Torrent::StopCondition
> stopCondition
= (!stopConditionParam
.isEmpty()
813 ? Utils::String::toEnum(stopConditionParam
, BitTorrent::Torrent::StopCondition::None
)
814 : std::optional
<BitTorrent::Torrent::StopCondition
> {});
816 const QString contentLayoutParam
= params()[u
"contentLayout"_s
];
817 const std::optional
<BitTorrent::TorrentContentLayout
> contentLayout
= (!contentLayoutParam
.isEmpty()
818 ? Utils::String::toEnum(contentLayoutParam
, BitTorrent::TorrentContentLayout::Original
)
819 : std::optional
<BitTorrent::TorrentContentLayout
> {});
821 QList
<QNetworkCookie
> cookies
;
822 if (!cookie
.isEmpty())
824 const QStringList cookiesStr
= cookie
.split(u
"; "_s
);
825 for (QString cookieStr
: cookiesStr
)
827 cookieStr
= cookieStr
.trimmed();
828 int index
= cookieStr
.indexOf(u
'=');
831 QByteArray name
= cookieStr
.left(index
).toLatin1();
832 QByteArray value
= cookieStr
.right(cookieStr
.length() - index
- 1).toLatin1();
833 cookies
+= QNetworkCookie(name
, value
);
838 const BitTorrent::AddTorrentParams addTorrentParams
840 // TODO: Check if destination actually exists
842 .category
= category
,
843 .tags
= {tags
.cbegin(), tags
.cend()},
844 .savePath
= Path(savepath
),
845 .useDownloadPath
= useDownloadPath
,
846 .downloadPath
= Path(downloadPath
),
847 .sequential
= seqDownload
,
848 .firstLastPiecePriority
= firstLastPiece
,
850 .addToQueueTop
= addToQueueTop
,
851 .addStopped
= addStopped
,
852 .stopCondition
= stopCondition
,
854 .filePriorities
= {},
855 .skipChecking
= skipChecking
,
856 .contentLayout
= contentLayout
,
857 .useAutoTMM
= autoTMM
,
858 .uploadLimit
= upLimit
,
859 .downloadLimit
= dlLimit
,
860 .seedingTimeLimit
= seedingTimeLimit
,
861 .inactiveSeedingTimeLimit
= inactiveSeedingTimeLimit
,
862 .ratioLimit
= ratioLimit
,
863 .shareLimitAction
= shareLimitAction
,
866 .certificate
= QSslCertificate(params()[KEY_PROP_SSL_CERTIFICATE
].toLatin1()),
867 .privateKey
= Utils::SSLKey::load(params()[KEY_PROP_SSL_PRIVATEKEY
].toLatin1()),
868 .dhParams
= params()[KEY_PROP_SSL_DHPARAMS
].toLatin1()
872 bool partialSuccess
= false;
873 for (QString url
: asConst(urls
.split(u
'\n')))
878 Net::DownloadManager::instance()->setCookiesFromUrl(cookies
, QUrl::fromEncoded(url
.toUtf8()));
879 partialSuccess
|= app()->addTorrentManager()->addTorrent(url
, addTorrentParams
);
883 const DataMap
&torrents
= data();
884 for (auto it
= torrents
.constBegin(); it
!= torrents
.constEnd(); ++it
)
886 if (const auto loadResult
= BitTorrent::TorrentDescriptor::load(it
.value()))
888 partialSuccess
|= BitTorrent::Session::instance()->addTorrent(loadResult
.value(), addTorrentParams
);
892 throw APIError(APIErrorType::BadData
, tr("Error: '%1' is not a valid torrent file.").arg(it
.key()));
899 setResult(u
"Fails."_s
);
902 void TorrentsController::addTrackersAction()
904 requireParams({u
"hash"_s
, u
"urls"_s
});
906 const auto id
= BitTorrent::TorrentID::fromString(params()[u
"hash"_s
]);
907 BitTorrent::Torrent
*const torrent
= BitTorrent::Session::instance()->getTorrent(id
);
909 throw APIError(APIErrorType::NotFound
);
911 const QList
<BitTorrent::TrackerEntry
> entries
= BitTorrent::parseTrackerEntries(params()[u
"urls"_s
]);
912 torrent
->addTrackers(entries
);
915 void TorrentsController::editTrackerAction()
917 requireParams({u
"hash"_s
, u
"origUrl"_s
, u
"newUrl"_s
});
919 const auto id
= BitTorrent::TorrentID::fromString(params()[u
"hash"_s
]);
920 const QString origUrl
= params()[u
"origUrl"_s
];
921 const QString newUrl
= params()[u
"newUrl"_s
];
923 BitTorrent::Torrent
*const torrent
= BitTorrent::Session::instance()->getTorrent(id
);
925 throw APIError(APIErrorType::NotFound
);
927 const QUrl origTrackerUrl
{origUrl
};
928 const QUrl newTrackerUrl
{newUrl
};
929 if (origTrackerUrl
== newTrackerUrl
)
931 if (!newTrackerUrl
.isValid())
932 throw APIError(APIErrorType::BadParams
, u
"New tracker URL is invalid"_s
);
934 const QList
<BitTorrent::TrackerEntryStatus
> currentTrackers
= torrent
->trackers();
935 QList
<BitTorrent::TrackerEntry
> entries
;
936 entries
.reserve(currentTrackers
.size());
939 for (const BitTorrent::TrackerEntryStatus
&tracker
: currentTrackers
)
941 const QUrl trackerUrl
{tracker
.url
};
943 if (trackerUrl
== newTrackerUrl
)
944 throw APIError(APIErrorType::Conflict
, u
"New tracker URL already exists"_s
);
946 BitTorrent::TrackerEntry entry
952 if (trackerUrl
== origTrackerUrl
)
955 entry
.url
= newTrackerUrl
.toString();
957 entries
.append(entry
);
960 throw APIError(APIErrorType::Conflict
, u
"Tracker not found"_s
);
962 torrent
->replaceTrackers(entries
);
964 if (!torrent
->isStopped())
965 torrent
->forceReannounce();
968 void TorrentsController::removeTrackersAction()
970 requireParams({u
"hash"_s
, u
"urls"_s
});
972 const auto id
= BitTorrent::TorrentID::fromString(params()[u
"hash"_s
]);
973 BitTorrent::Torrent
*const torrent
= BitTorrent::Session::instance()->getTorrent(id
);
975 throw APIError(APIErrorType::NotFound
);
977 const QStringList urls
= params()[u
"urls"_s
].split(u
'|');
978 torrent
->removeTrackers(urls
);
981 void TorrentsController::addPeersAction()
983 requireParams({u
"hashes"_s
, u
"peers"_s
});
985 const QStringList hashes
= params()[u
"hashes"_s
].split(u
'|');
986 const QStringList peers
= params()[u
"peers"_s
].split(u
'|');
988 QList
<BitTorrent::PeerAddress
> peerList
;
989 peerList
.reserve(peers
.size());
990 for (const QString
&peer
: peers
)
992 const BitTorrent::PeerAddress addr
= BitTorrent::PeerAddress::parse(peer
.trimmed());
993 if (!addr
.ip
.isNull())
994 peerList
.append(addr
);
997 if (peerList
.isEmpty())
998 throw APIError(APIErrorType::BadParams
, u
"No valid peers were specified"_s
);
1000 QJsonObject results
;
1002 applyToTorrents(hashes
, [peers
, peerList
, &results
](BitTorrent::Torrent
*const torrent
)
1004 const int peersAdded
= std::count_if(peerList
.cbegin(), peerList
.cend(), [torrent
](const BitTorrent::PeerAddress
&peer
)
1006 return torrent
->connectPeer(peer
);
1009 results
[torrent
->id().toString()] = QJsonObject
1011 {u
"added"_s
, peersAdded
},
1012 {u
"failed"_s
, (peers
.size() - peersAdded
)}
1019 void TorrentsController::stopAction()
1021 requireParams({u
"hashes"_s
});
1023 const QStringList hashes
= params()[u
"hashes"_s
].split(u
'|');
1024 applyToTorrents(hashes
, [](BitTorrent::Torrent
*const torrent
) { torrent
->stop(); });
1027 void TorrentsController::startAction()
1029 requireParams({u
"hashes"_s
});
1031 const QStringList idStrings
= params()[u
"hashes"_s
].split(u
'|');
1032 applyToTorrents(idStrings
, [](BitTorrent::Torrent
*const torrent
) { torrent
->start(); });
1035 void TorrentsController::filePrioAction()
1037 requireParams({u
"hash"_s
, u
"id"_s
, u
"priority"_s
});
1039 const auto id
= BitTorrent::TorrentID::fromString(params()[u
"hash"_s
]);
1041 const auto priority
= static_cast<BitTorrent::DownloadPriority
>(params()[u
"priority"_s
].toInt(&ok
));
1043 throw APIError(APIErrorType::BadParams
, tr("Priority must be an integer"));
1045 if (!BitTorrent::isValidDownloadPriority(priority
))
1046 throw APIError(APIErrorType::BadParams
, tr("Priority is not valid"));
1048 BitTorrent::Torrent
*const torrent
= BitTorrent::Session::instance()->getTorrent(id
);
1050 throw APIError(APIErrorType::NotFound
);
1051 if (!torrent
->hasMetadata())
1052 throw APIError(APIErrorType::Conflict
, tr("Torrent's metadata has not yet downloaded"));
1054 const int filesCount
= torrent
->filesCount();
1055 QList
<BitTorrent::DownloadPriority
> priorities
= torrent
->filePriorities();
1056 bool priorityChanged
= false;
1057 for (const QString
&fileID
: params()[u
"id"_s
].split(u
'|'))
1059 const int id
= fileID
.toInt(&ok
);
1061 throw APIError(APIErrorType::BadParams
, tr("File IDs must be integers"));
1062 if ((id
< 0) || (id
>= filesCount
))
1063 throw APIError(APIErrorType::Conflict
, tr("File ID is not valid"));
1065 if (priorities
[id
] != priority
)
1067 priorities
[id
] = priority
;
1068 priorityChanged
= true;
1072 if (priorityChanged
)
1073 torrent
->prioritizeFiles(priorities
);
1076 void TorrentsController::uploadLimitAction()
1078 requireParams({u
"hashes"_s
});
1080 const QStringList idList
{params()[u
"hashes"_s
].split(u
'|')};
1082 for (const QString
&id
: idList
)
1085 const BitTorrent::Torrent
*const torrent
= BitTorrent::Session::instance()->getTorrent(BitTorrent::TorrentID::fromString(id
));
1087 limit
= torrent
->uploadLimit();
1094 void TorrentsController::downloadLimitAction()
1096 requireParams({u
"hashes"_s
});
1098 const QStringList idList
{params()[u
"hashes"_s
].split(u
'|')};
1100 for (const QString
&id
: idList
)
1103 const BitTorrent::Torrent
*const torrent
= BitTorrent::Session::instance()->getTorrent(BitTorrent::TorrentID::fromString(id
));
1105 limit
= torrent
->downloadLimit();
1112 void TorrentsController::setUploadLimitAction()
1114 requireParams({u
"hashes"_s
, u
"limit"_s
});
1116 qlonglong limit
= params()[u
"limit"_s
].toLongLong();
1120 const QStringList hashes
{params()[u
"hashes"_s
].split(u
'|')};
1121 applyToTorrents(hashes
, [limit
](BitTorrent::Torrent
*const torrent
) { torrent
->setUploadLimit(limit
); });
1124 void TorrentsController::setDownloadLimitAction()
1126 requireParams({u
"hashes"_s
, u
"limit"_s
});
1128 qlonglong limit
= params()[u
"limit"_s
].toLongLong();
1132 const QStringList hashes
{params()[u
"hashes"_s
].split(u
'|')};
1133 applyToTorrents(hashes
, [limit
](BitTorrent::Torrent
*const torrent
) { torrent
->setDownloadLimit(limit
); });
1136 void TorrentsController::setShareLimitsAction()
1138 requireParams({u
"hashes"_s
, u
"ratioLimit"_s
, u
"seedingTimeLimit"_s
, u
"inactiveSeedingTimeLimit"_s
});
1140 const qreal ratioLimit
= params()[u
"ratioLimit"_s
].toDouble();
1141 const qlonglong seedingTimeLimit
= params()[u
"seedingTimeLimit"_s
].toLongLong();
1142 const qlonglong inactiveSeedingTimeLimit
= params()[u
"inactiveSeedingTimeLimit"_s
].toLongLong();
1143 const QStringList hashes
= params()[u
"hashes"_s
].split(u
'|');
1145 applyToTorrents(hashes
, [ratioLimit
, seedingTimeLimit
, inactiveSeedingTimeLimit
](BitTorrent::Torrent
*const torrent
)
1147 torrent
->setRatioLimit(ratioLimit
);
1148 torrent
->setSeedingTimeLimit(seedingTimeLimit
);
1149 torrent
->setInactiveSeedingTimeLimit(inactiveSeedingTimeLimit
);
1153 void TorrentsController::toggleSequentialDownloadAction()
1155 requireParams({u
"hashes"_s
});
1157 const QStringList hashes
{params()[u
"hashes"_s
].split(u
'|')};
1158 applyToTorrents(hashes
, [](BitTorrent::Torrent
*const torrent
) { torrent
->toggleSequentialDownload(); });
1161 void TorrentsController::toggleFirstLastPiecePrioAction()
1163 requireParams({u
"hashes"_s
});
1165 const QStringList hashes
{params()[u
"hashes"_s
].split(u
'|')};
1166 applyToTorrents(hashes
, [](BitTorrent::Torrent
*const torrent
) { torrent
->toggleFirstLastPiecePriority(); });
1169 void TorrentsController::setSuperSeedingAction()
1171 requireParams({u
"hashes"_s
, u
"value"_s
});
1173 const bool value
{parseBool(params()[u
"value"_s
]).value_or(false)};
1174 const QStringList hashes
{params()[u
"hashes"_s
].split(u
'|')};
1175 applyToTorrents(hashes
, [value
](BitTorrent::Torrent
*const torrent
) { torrent
->setSuperSeeding(value
); });
1178 void TorrentsController::setForceStartAction()
1180 requireParams({u
"hashes"_s
, u
"value"_s
});
1182 const bool value
{parseBool(params()[u
"value"_s
]).value_or(false)};
1183 const QStringList hashes
{params()[u
"hashes"_s
].split(u
'|')};
1184 applyToTorrents(hashes
, [value
](BitTorrent::Torrent
*const torrent
)
1186 torrent
->start(value
? BitTorrent::TorrentOperatingMode::Forced
: BitTorrent::TorrentOperatingMode::AutoManaged
);
1190 void TorrentsController::deleteAction()
1192 requireParams({u
"hashes"_s
, u
"deleteFiles"_s
});
1194 const QStringList hashes
{params()[u
"hashes"_s
].split(u
'|')};
1195 const BitTorrent::TorrentRemoveOption deleteOption
= parseBool(params()[u
"deleteFiles"_s
]).value_or(false)
1196 ? BitTorrent::TorrentRemoveOption::RemoveContent
: BitTorrent::TorrentRemoveOption::KeepContent
;
1197 applyToTorrents(hashes
, [deleteOption
](const BitTorrent::Torrent
*torrent
)
1199 BitTorrent::Session::instance()->removeTorrent(torrent
->id(), deleteOption
);
1203 void TorrentsController::increasePrioAction()
1205 requireParams({u
"hashes"_s
});
1207 if (!BitTorrent::Session::instance()->isQueueingSystemEnabled())
1208 throw APIError(APIErrorType::Conflict
, tr("Torrent queueing must be enabled"));
1210 const QStringList hashes
{params()[u
"hashes"_s
].split(u
'|')};
1211 BitTorrent::Session::instance()->increaseTorrentsQueuePos(toTorrentIDs(hashes
));
1214 void TorrentsController::decreasePrioAction()
1216 requireParams({u
"hashes"_s
});
1218 if (!BitTorrent::Session::instance()->isQueueingSystemEnabled())
1219 throw APIError(APIErrorType::Conflict
, tr("Torrent queueing must be enabled"));
1221 const QStringList hashes
{params()[u
"hashes"_s
].split(u
'|')};
1222 BitTorrent::Session::instance()->decreaseTorrentsQueuePos(toTorrentIDs(hashes
));
1225 void TorrentsController::topPrioAction()
1227 requireParams({u
"hashes"_s
});
1229 if (!BitTorrent::Session::instance()->isQueueingSystemEnabled())
1230 throw APIError(APIErrorType::Conflict
, tr("Torrent queueing must be enabled"));
1232 const QStringList hashes
{params()[u
"hashes"_s
].split(u
'|')};
1233 BitTorrent::Session::instance()->topTorrentsQueuePos(toTorrentIDs(hashes
));
1236 void TorrentsController::bottomPrioAction()
1238 requireParams({u
"hashes"_s
});
1240 if (!BitTorrent::Session::instance()->isQueueingSystemEnabled())
1241 throw APIError(APIErrorType::Conflict
, tr("Torrent queueing must be enabled"));
1243 const QStringList hashes
{params()[u
"hashes"_s
].split(u
'|')};
1244 BitTorrent::Session::instance()->bottomTorrentsQueuePos(toTorrentIDs(hashes
));
1247 void TorrentsController::setLocationAction()
1249 requireParams({u
"hashes"_s
, u
"location"_s
});
1251 const QStringList hashes
{params()[u
"hashes"_s
].split(u
'|')};
1252 const Path newLocation
{params()[u
"location"_s
].trimmed()};
1254 if (newLocation
.isEmpty())
1255 throw APIError(APIErrorType::BadParams
, tr("Save path cannot be empty"));
1257 // try to create the location if it does not exist
1258 if (!Utils::Fs::mkpath(newLocation
))
1259 throw APIError(APIErrorType::Conflict
, tr("Cannot make save path"));
1261 applyToTorrents(hashes
, [newLocation
](BitTorrent::Torrent
*const torrent
)
1263 LogMsg(tr("WebUI Set location: moving \"%1\", from \"%2\" to \"%3\"")
1264 .arg(torrent
->name(), torrent
->savePath().toString(), newLocation
.toString()));
1265 torrent
->setAutoTMMEnabled(false);
1266 torrent
->setSavePath(newLocation
);
1270 void TorrentsController::setSavePathAction()
1272 requireParams({u
"id"_s
, u
"path"_s
});
1274 const QStringList ids
{params()[u
"id"_s
].split(u
'|')};
1275 const Path newPath
{params()[u
"path"_s
]};
1277 if (newPath
.isEmpty())
1278 throw APIError(APIErrorType::BadParams
, tr("Save path cannot be empty"));
1280 // try to create the directory if it does not exist
1281 if (!Utils::Fs::mkpath(newPath
))
1282 throw APIError(APIErrorType::Conflict
, tr("Cannot create target directory"));
1284 // check permissions
1285 if (!Utils::Fs::isWritable(newPath
))
1286 throw APIError(APIErrorType::AccessDenied
, tr("Cannot write to directory"));
1288 applyToTorrents(ids
, [&newPath
](BitTorrent::Torrent
*const torrent
)
1290 if (!torrent
->isAutoTMMEnabled())
1291 torrent
->setSavePath(newPath
);
1295 void TorrentsController::setDownloadPathAction()
1297 requireParams({u
"id"_s
, u
"path"_s
});
1299 const QStringList ids
{params()[u
"id"_s
].split(u
'|')};
1300 const Path newPath
{params()[u
"path"_s
]};
1302 if (!newPath
.isEmpty())
1304 // try to create the directory if it does not exist
1305 if (!Utils::Fs::mkpath(newPath
))
1306 throw APIError(APIErrorType::Conflict
, tr("Cannot create target directory"));
1308 // check permissions
1309 if (!Utils::Fs::isWritable(newPath
))
1310 throw APIError(APIErrorType::AccessDenied
, tr("Cannot write to directory"));
1313 applyToTorrents(ids
, [&newPath
](BitTorrent::Torrent
*const torrent
)
1315 if (!torrent
->isAutoTMMEnabled())
1316 torrent
->setDownloadPath(newPath
);
1320 void TorrentsController::renameAction()
1322 requireParams({u
"hash"_s
, u
"name"_s
});
1324 const auto id
= BitTorrent::TorrentID::fromString(params()[u
"hash"_s
]);
1325 QString name
= params()[u
"name"_s
].trimmed();
1328 throw APIError(APIErrorType::Conflict
, tr("Incorrect torrent name"));
1330 BitTorrent::Torrent
*const torrent
= BitTorrent::Session::instance()->getTorrent(id
);
1332 throw APIError(APIErrorType::NotFound
);
1334 name
.replace(QRegularExpression(u
"\r?\n|\r"_s
), u
" "_s
);
1335 torrent
->setName(name
);
1338 void TorrentsController::setAutoManagementAction()
1340 requireParams({u
"hashes"_s
, u
"enable"_s
});
1342 const QStringList hashes
{params()[u
"hashes"_s
].split(u
'|')};
1343 const bool isEnabled
{parseBool(params()[u
"enable"_s
]).value_or(false)};
1345 applyToTorrents(hashes
, [isEnabled
](BitTorrent::Torrent
*const torrent
)
1347 torrent
->setAutoTMMEnabled(isEnabled
);
1351 void TorrentsController::recheckAction()
1353 requireParams({u
"hashes"_s
});
1355 const QStringList hashes
{params()[u
"hashes"_s
].split(u
'|')};
1356 applyToTorrents(hashes
, [](BitTorrent::Torrent
*const torrent
) { torrent
->forceRecheck(); });
1359 void TorrentsController::reannounceAction()
1361 requireParams({u
"hashes"_s
});
1363 const QStringList hashes
{params()[u
"hashes"_s
].split(u
'|')};
1364 applyToTorrents(hashes
, [](BitTorrent::Torrent
*const torrent
) { torrent
->forceReannounce(); });
1367 void TorrentsController::setCategoryAction()
1369 requireParams({u
"hashes"_s
, u
"category"_s
});
1371 const QStringList hashes
{params()[u
"hashes"_s
].split(u
'|')};
1372 const QString category
{params()[u
"category"_s
]};
1374 applyToTorrents(hashes
, [category
](BitTorrent::Torrent
*const torrent
)
1376 if (!torrent
->setCategory(category
))
1377 throw APIError(APIErrorType::Conflict
, tr("Incorrect category name"));
1381 void TorrentsController::createCategoryAction()
1383 requireParams({u
"category"_s
});
1385 const QString category
= params()[u
"category"_s
];
1386 if (category
.isEmpty())
1387 throw APIError(APIErrorType::BadParams
, tr("Category cannot be empty"));
1389 if (!BitTorrent::Session::isValidCategoryName(category
))
1390 throw APIError(APIErrorType::Conflict
, tr("Incorrect category name"));
1392 const Path savePath
{params()[u
"savePath"_s
]};
1393 const auto useDownloadPath
= parseBool(params()[u
"downloadPathEnabled"_s
]);
1394 BitTorrent::CategoryOptions categoryOptions
;
1395 categoryOptions
.savePath
= savePath
;
1396 if (useDownloadPath
.has_value())
1398 const Path downloadPath
{params()[u
"downloadPath"_s
]};
1399 categoryOptions
.downloadPath
= {useDownloadPath
.value(), downloadPath
};
1402 if (!BitTorrent::Session::instance()->addCategory(category
, categoryOptions
))
1403 throw APIError(APIErrorType::Conflict
, tr("Unable to create category"));
1406 void TorrentsController::editCategoryAction()
1408 requireParams({u
"category"_s
, u
"savePath"_s
});
1410 const QString category
= params()[u
"category"_s
];
1411 if (category
.isEmpty())
1412 throw APIError(APIErrorType::BadParams
, tr("Category cannot be empty"));
1414 const Path savePath
{params()[u
"savePath"_s
]};
1415 const auto useDownloadPath
= parseBool(params()[u
"downloadPathEnabled"_s
]);
1416 BitTorrent::CategoryOptions categoryOptions
;
1417 categoryOptions
.savePath
= savePath
;
1418 if (useDownloadPath
.has_value())
1420 const Path downloadPath
{params()[u
"downloadPath"_s
]};
1421 categoryOptions
.downloadPath
= {useDownloadPath
.value(), downloadPath
};
1424 if (!BitTorrent::Session::instance()->editCategory(category
, categoryOptions
))
1425 throw APIError(APIErrorType::Conflict
, tr("Unable to edit category"));
1428 void TorrentsController::removeCategoriesAction()
1430 requireParams({u
"categories"_s
});
1432 const QStringList categories
{params()[u
"categories"_s
].split(u
'\n')};
1433 for (const QString
&category
: categories
)
1434 BitTorrent::Session::instance()->removeCategory(category
);
1437 void TorrentsController::categoriesAction()
1439 const auto *session
= BitTorrent::Session::instance();
1441 QJsonObject categories
;
1442 const QStringList categoriesList
= session
->categories();
1443 for (const auto &categoryName
: categoriesList
)
1445 const BitTorrent::CategoryOptions categoryOptions
= session
->categoryOptions(categoryName
);
1446 QJsonObject category
= categoryOptions
.toJSON();
1447 // adjust it to be compatible with existing WebAPI
1448 category
[u
"savePath"_s
] = category
.take(u
"save_path"_s
);
1449 category
.insert(u
"name"_s
, categoryName
);
1450 categories
[categoryName
] = category
;
1453 setResult(categories
);
1456 void TorrentsController::addTagsAction()
1458 requireParams({u
"hashes"_s
, u
"tags"_s
});
1460 const QStringList hashes
{params()[u
"hashes"_s
].split(u
'|')};
1461 const QStringList tags
{params()[u
"tags"_s
].split(u
',', Qt::SkipEmptyParts
)};
1463 for (const QString
&tagStr
: tags
)
1465 applyToTorrents(hashes
, [&tagStr
](BitTorrent::Torrent
*const torrent
)
1467 torrent
->addTag(Tag(tagStr
));
1472 void TorrentsController::removeTagsAction()
1474 requireParams({u
"hashes"_s
});
1476 const QStringList hashes
{params()[u
"hashes"_s
].split(u
'|')};
1477 const QStringList tags
{params()[u
"tags"_s
].split(u
',', Qt::SkipEmptyParts
)};
1479 for (const QString
&tagStr
: tags
)
1481 applyToTorrents(hashes
, [&tagStr
](BitTorrent::Torrent
*const torrent
)
1483 torrent
->removeTag(Tag(tagStr
));
1489 applyToTorrents(hashes
, [](BitTorrent::Torrent
*const torrent
)
1491 torrent
->removeAllTags();
1496 void TorrentsController::createTagsAction()
1498 requireParams({u
"tags"_s
});
1500 const QStringList tags
{params()[u
"tags"_s
].split(u
',', Qt::SkipEmptyParts
)};
1502 for (const QString
&tagStr
: tags
)
1503 BitTorrent::Session::instance()->addTag(Tag(tagStr
));
1506 void TorrentsController::deleteTagsAction()
1508 requireParams({u
"tags"_s
});
1510 const QStringList tags
{params()[u
"tags"_s
].split(u
',', Qt::SkipEmptyParts
)};
1511 for (const QString
&tagStr
: tags
)
1512 BitTorrent::Session::instance()->removeTag(Tag(tagStr
));
1515 void TorrentsController::tagsAction()
1518 for (const Tag
&tag
: asConst(BitTorrent::Session::instance()->tags()))
1519 result
<< tag
.toString();
1523 void TorrentsController::renameFileAction()
1525 requireParams({u
"hash"_s
, u
"oldPath"_s
, u
"newPath"_s
});
1527 const auto id
= BitTorrent::TorrentID::fromString(params()[u
"hash"_s
]);
1528 BitTorrent::Torrent
*const torrent
= BitTorrent::Session::instance()->getTorrent(id
);
1530 throw APIError(APIErrorType::NotFound
);
1532 const Path oldPath
{params()[u
"oldPath"_s
]};
1533 const Path newPath
{params()[u
"newPath"_s
]};
1537 torrent
->renameFile(oldPath
, newPath
);
1539 catch (const RuntimeError
&error
)
1541 throw APIError(APIErrorType::Conflict
, error
.message());
1545 void TorrentsController::renameFolderAction()
1547 requireParams({u
"hash"_s
, u
"oldPath"_s
, u
"newPath"_s
});
1549 const auto id
= BitTorrent::TorrentID::fromString(params()[u
"hash"_s
]);
1550 BitTorrent::Torrent
*const torrent
= BitTorrent::Session::instance()->getTorrent(id
);
1552 throw APIError(APIErrorType::NotFound
);
1554 const Path oldPath
{params()[u
"oldPath"_s
]};
1555 const Path newPath
{params()[u
"newPath"_s
]};
1559 torrent
->renameFolder(oldPath
, newPath
);
1561 catch (const RuntimeError
&error
)
1563 throw APIError(APIErrorType::Conflict
, error
.message());
1567 void TorrentsController::exportAction()
1569 requireParams({u
"hash"_s
});
1571 const auto id
= BitTorrent::TorrentID::fromString(params()[u
"hash"_s
]);
1572 const BitTorrent::Torrent
*const torrent
= BitTorrent::Session::instance()->getTorrent(id
);
1574 throw APIError(APIErrorType::NotFound
);
1576 const nonstd::expected
<QByteArray
, QString
> result
= torrent
->exportToBuffer();
1578 throw APIError(APIErrorType::Conflict
, tr("Unable to export torrent file. Error: %1").arg(result
.error()));
1580 setResult(result
.value(), u
"application/x-bittorrent"_s
, (id
.toString() + u
".torrent"));
1583 void TorrentsController::SSLParametersAction()
1585 requireParams({u
"hash"_s
});
1587 const auto id
= BitTorrent::TorrentID::fromString(params()[u
"hash"_s
]);
1588 const BitTorrent::Torrent
*torrent
= BitTorrent::Session::instance()->getTorrent(id
);
1590 throw APIError(APIErrorType::NotFound
);
1592 const BitTorrent::SSLParameters sslParams
= torrent
->getSSLParameters();
1593 const QJsonObject ret
1595 {KEY_PROP_SSL_CERTIFICATE
, QString::fromLatin1(sslParams
.certificate
.toPem())},
1596 {KEY_PROP_SSL_PRIVATEKEY
, QString::fromLatin1(sslParams
.privateKey
.toPem())},
1597 {KEY_PROP_SSL_DHPARAMS
, QString::fromLatin1(sslParams
.dhParams
)}
1602 void TorrentsController::setSSLParametersAction()
1604 requireParams({u
"hash"_s
, KEY_PROP_SSL_CERTIFICATE
, KEY_PROP_SSL_PRIVATEKEY
, KEY_PROP_SSL_DHPARAMS
});
1606 const auto id
= BitTorrent::TorrentID::fromString(params()[u
"hash"_s
]);
1607 BitTorrent::Torrent
*const torrent
= BitTorrent::Session::instance()->getTorrent(id
);
1609 throw APIError(APIErrorType::NotFound
);
1611 const BitTorrent::SSLParameters sslParams
1613 .certificate
= QSslCertificate(params()[KEY_PROP_SSL_CERTIFICATE
].toLatin1()),
1614 .privateKey
= Utils::SSLKey::load(params()[KEY_PROP_SSL_PRIVATEKEY
].toLatin1()),
1615 .dhParams
= params()[KEY_PROP_SSL_DHPARAMS
].toLatin1()
1617 if (!sslParams
.isValid())
1618 throw APIError(APIErrorType::BadData
);
1620 torrent
->setSSLParameters(sslParams
);