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