Revamp tracker list widget
[qBittorrent.git] / src / webui / api / synccontroller.cpp
blob0d25e03fa3b6bb671e0d3d3ae0b8a0ae56ac4e53
1 /*
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 "synccontroller.h"
31 #include <algorithm>
33 #include <QJsonArray>
34 #include <QJsonObject>
35 #include <QMetaObject>
36 #include <QThreadPool>
38 #include "base/algorithm.h"
39 #include "base/bittorrent/cachestatus.h"
40 #include "base/bittorrent/infohash.h"
41 #include "base/bittorrent/peeraddress.h"
42 #include "base/bittorrent/peerinfo.h"
43 #include "base/bittorrent/session.h"
44 #include "base/bittorrent/sessionstatus.h"
45 #include "base/bittorrent/torrent.h"
46 #include "base/bittorrent/torrentinfo.h"
47 #include "base/bittorrent/trackerentry.h"
48 #include "base/global.h"
49 #include "base/net/geoipmanager.h"
50 #include "base/preferences.h"
51 #include "base/utils/string.h"
52 #include "apierror.h"
53 #include "freediskspacechecker.h"
54 #include "serialize/serialize_torrent.h"
56 namespace
58 const int FREEDISKSPACE_CHECK_TIMEOUT = 30000;
60 // Sync main data keys
61 const QString KEY_SYNC_MAINDATA_QUEUEING = u"queueing"_s;
62 const QString KEY_SYNC_MAINDATA_REFRESH_INTERVAL = u"refresh_interval"_s;
63 const QString KEY_SYNC_MAINDATA_USE_ALT_SPEED_LIMITS = u"use_alt_speed_limits"_s;
64 const QString KEY_SYNC_MAINDATA_USE_SUBCATEGORIES = u"use_subcategories"_s;
66 // Sync torrent peers keys
67 const QString KEY_SYNC_TORRENT_PEERS_SHOW_FLAGS = u"show_flags"_s;
69 // Peer keys
70 const QString KEY_PEER_CLIENT = u"client"_s;
71 const QString KEY_PEER_ID_CLIENT = u"peer_id_client"_s;
72 const QString KEY_PEER_CONNECTION_TYPE = u"connection"_s;
73 const QString KEY_PEER_COUNTRY = u"country"_s;
74 const QString KEY_PEER_COUNTRY_CODE = u"country_code"_s;
75 const QString KEY_PEER_DOWN_SPEED = u"dl_speed"_s;
76 const QString KEY_PEER_FILES = u"files"_s;
77 const QString KEY_PEER_FLAGS = u"flags"_s;
78 const QString KEY_PEER_FLAGS_DESCRIPTION = u"flags_desc"_s;
79 const QString KEY_PEER_IP = u"ip"_s;
80 const QString KEY_PEER_PORT = u"port"_s;
81 const QString KEY_PEER_PROGRESS = u"progress"_s;
82 const QString KEY_PEER_RELEVANCE = u"relevance"_s;
83 const QString KEY_PEER_TOT_DOWN = u"downloaded"_s;
84 const QString KEY_PEER_TOT_UP = u"uploaded"_s;
85 const QString KEY_PEER_UP_SPEED = u"up_speed"_s;
87 // TransferInfo keys
88 const QString KEY_TRANSFER_CONNECTION_STATUS = u"connection_status"_s;
89 const QString KEY_TRANSFER_DHT_NODES = u"dht_nodes"_s;
90 const QString KEY_TRANSFER_DLDATA = u"dl_info_data"_s;
91 const QString KEY_TRANSFER_DLRATELIMIT = u"dl_rate_limit"_s;
92 const QString KEY_TRANSFER_DLSPEED = u"dl_info_speed"_s;
93 const QString KEY_TRANSFER_FREESPACEONDISK = u"free_space_on_disk"_s;
94 const QString KEY_TRANSFER_UPDATA = u"up_info_data"_s;
95 const QString KEY_TRANSFER_UPRATELIMIT = u"up_rate_limit"_s;
96 const QString KEY_TRANSFER_UPSPEED = u"up_info_speed"_s;
98 // Statistics keys
99 const QString KEY_TRANSFER_ALLTIME_DL = u"alltime_dl"_s;
100 const QString KEY_TRANSFER_ALLTIME_UL = u"alltime_ul"_s;
101 const QString KEY_TRANSFER_AVERAGE_TIME_QUEUE = u"average_time_queue"_s;
102 const QString KEY_TRANSFER_GLOBAL_RATIO = u"global_ratio"_s;
103 const QString KEY_TRANSFER_QUEUED_IO_JOBS = u"queued_io_jobs"_s;
104 const QString KEY_TRANSFER_READ_CACHE_HITS = u"read_cache_hits"_s;
105 const QString KEY_TRANSFER_READ_CACHE_OVERLOAD = u"read_cache_overload"_s;
106 const QString KEY_TRANSFER_TOTAL_BUFFERS_SIZE = u"total_buffers_size"_s;
107 const QString KEY_TRANSFER_TOTAL_PEER_CONNECTIONS = u"total_peer_connections"_s;
108 const QString KEY_TRANSFER_TOTAL_QUEUED_SIZE = u"total_queued_size"_s;
109 const QString KEY_TRANSFER_TOTAL_WASTE_SESSION = u"total_wasted_session"_s;
110 const QString KEY_TRANSFER_WRITE_CACHE_OVERLOAD = u"write_cache_overload"_s;
112 const QString KEY_SUFFIX_REMOVED = u"_removed"_s;
114 const QString KEY_CATEGORIES = u"categories"_s;
115 const QString KEY_CATEGORIES_REMOVED = KEY_CATEGORIES + KEY_SUFFIX_REMOVED;
116 const QString KEY_TAGS = u"tags"_s;
117 const QString KEY_TAGS_REMOVED = KEY_TAGS + KEY_SUFFIX_REMOVED;
118 const QString KEY_TORRENTS = u"torrents"_s;
119 const QString KEY_TORRENTS_REMOVED = KEY_TORRENTS + KEY_SUFFIX_REMOVED;
120 const QString KEY_TRACKERS = u"trackers"_s;
121 const QString KEY_TRACKERS_REMOVED = KEY_TRACKERS + KEY_SUFFIX_REMOVED;
122 const QString KEY_SERVER_STATE = u"server_state"_s;
123 const QString KEY_FULL_UPDATE = u"full_update"_s;
124 const QString KEY_RESPONSE_ID = u"rid"_s;
126 void processMap(const QVariantMap &prevData, const QVariantMap &data, QVariantMap &syncData);
127 void processHash(QVariantHash prevData, const QVariantHash &data, QVariantMap &syncData, QVariantList &removedItems);
128 void processList(QVariantList prevData, const QVariantList &data, QVariantList &syncData, QVariantList &removedItems);
129 QJsonObject generateSyncData(int acceptedResponseId, const QVariantMap &data, QVariantMap &lastAcceptedData, QVariantMap &lastData);
131 QVariantMap getTransferInfo()
133 QVariantMap map;
134 const auto *session = BitTorrent::Session::instance();
136 const BitTorrent::SessionStatus &sessionStatus = session->status();
137 const BitTorrent::CacheStatus &cacheStatus = session->cacheStatus();
138 map[KEY_TRANSFER_DLSPEED] = sessionStatus.payloadDownloadRate;
139 map[KEY_TRANSFER_DLDATA] = sessionStatus.totalPayloadDownload;
140 map[KEY_TRANSFER_UPSPEED] = sessionStatus.payloadUploadRate;
141 map[KEY_TRANSFER_UPDATA] = sessionStatus.totalPayloadUpload;
142 map[KEY_TRANSFER_DLRATELIMIT] = session->downloadSpeedLimit();
143 map[KEY_TRANSFER_UPRATELIMIT] = session->uploadSpeedLimit();
145 const qint64 atd = sessionStatus.allTimeDownload;
146 const qint64 atu = sessionStatus.allTimeUpload;
147 map[KEY_TRANSFER_ALLTIME_DL] = atd;
148 map[KEY_TRANSFER_ALLTIME_UL] = atu;
149 map[KEY_TRANSFER_TOTAL_WASTE_SESSION] = sessionStatus.totalWasted;
150 map[KEY_TRANSFER_GLOBAL_RATIO] = ((atd > 0) && (atu > 0)) ? Utils::String::fromDouble(static_cast<qreal>(atu) / atd, 2) : u"-"_s;
151 map[KEY_TRANSFER_TOTAL_PEER_CONNECTIONS] = sessionStatus.peersCount;
153 const qreal readRatio = cacheStatus.readRatio; // TODO: remove when LIBTORRENT_VERSION_NUM >= 20000
154 map[KEY_TRANSFER_READ_CACHE_HITS] = (readRatio > 0) ? Utils::String::fromDouble(100 * readRatio, 2) : u"0"_s;
155 map[KEY_TRANSFER_TOTAL_BUFFERS_SIZE] = cacheStatus.totalUsedBuffers * 16 * 1024;
157 map[KEY_TRANSFER_WRITE_CACHE_OVERLOAD] = ((sessionStatus.diskWriteQueue > 0) && (sessionStatus.peersCount > 0))
158 ? Utils::String::fromDouble((100. * sessionStatus.diskWriteQueue / sessionStatus.peersCount), 2)
159 : u"0"_s;
160 map[KEY_TRANSFER_READ_CACHE_OVERLOAD] = ((sessionStatus.diskReadQueue > 0) && (sessionStatus.peersCount > 0))
161 ? Utils::String::fromDouble((100. * sessionStatus.diskReadQueue / sessionStatus.peersCount), 2)
162 : u"0"_s;
164 map[KEY_TRANSFER_QUEUED_IO_JOBS] = cacheStatus.jobQueueLength;
165 map[KEY_TRANSFER_AVERAGE_TIME_QUEUE] = cacheStatus.averageJobTime;
166 map[KEY_TRANSFER_TOTAL_QUEUED_SIZE] = cacheStatus.queuedBytes;
168 map[KEY_TRANSFER_DHT_NODES] = sessionStatus.dhtNodes;
169 map[KEY_TRANSFER_CONNECTION_STATUS] = session->isListening()
170 ? (sessionStatus.hasIncomingConnections ? u"connected"_s : u"firewalled"_s)
171 : u"disconnected"_s;
173 return map;
176 // Compare two structures (prevData, data) and calculate difference (syncData).
177 // Structures encoded as map.
178 void processMap(const QVariantMap &prevData, const QVariantMap &data, QVariantMap &syncData)
180 // initialize output variable
181 syncData.clear();
183 for (auto i = data.cbegin(); i != data.cend(); ++i)
185 const QString &key = i.key();
186 const QVariant &value = i.value();
187 QVariantList removedItems;
189 switch (value.userType())
191 case QMetaType::QVariantMap:
193 QVariantMap map;
194 processMap(prevData[key].toMap(), value.toMap(), map);
195 if (!map.isEmpty())
196 syncData[key] = map;
198 break;
199 case QMetaType::QVariantHash:
201 QVariantMap map;
202 processHash(prevData[key].toHash(), value.toHash(), map, removedItems);
203 if (!map.isEmpty())
204 syncData[key] = map;
205 if (!removedItems.isEmpty())
206 syncData[key + KEY_SUFFIX_REMOVED] = removedItems;
208 break;
209 case QMetaType::QVariantList:
211 QVariantList list;
212 processList(prevData[key].toList(), value.toList(), list, removedItems);
213 if (!list.isEmpty())
214 syncData[key] = list;
215 if (!removedItems.isEmpty())
216 syncData[key + KEY_SUFFIX_REMOVED] = removedItems;
218 break;
219 case QMetaType::QString:
220 case QMetaType::LongLong:
221 case QMetaType::Float:
222 case QMetaType::Int:
223 case QMetaType::Bool:
224 case QMetaType::Double:
225 case QMetaType::ULongLong:
226 case QMetaType::UInt:
227 case QMetaType::QDateTime:
228 case QMetaType::Nullptr:
229 if (prevData[key] != value)
230 syncData[key] = value;
231 break;
232 default:
233 Q_ASSERT_X(false, "processMap"
234 , u"Unexpected type: %1"_s
235 .arg(QString::fromLatin1(value.metaType().name()))
236 .toUtf8().constData());
241 // Compare two lists of structures (prevData, data) and calculate difference (syncData, removedItems).
242 // Structures encoded as map.
243 // Lists are encoded as hash table (indexed by structure key value) to improve ease of searching for removed items.
244 void processHash(QVariantHash prevData, const QVariantHash &data, QVariantMap &syncData, QVariantList &removedItems)
246 // initialize output variables
247 syncData.clear();
248 removedItems.clear();
250 if (prevData.isEmpty())
252 // If list was empty before, then difference is a whole new list.
253 for (auto i = data.cbegin(); i != data.cend(); ++i)
254 syncData[i.key()] = i.value();
256 else
258 for (auto i = data.cbegin(); i != data.cend(); ++i)
260 switch (i.value().userType())
262 case QMetaType::QVariantMap:
263 if (!prevData.contains(i.key()))
265 // new list item found - append it to syncData
266 syncData[i.key()] = i.value();
268 else
270 QVariantMap map;
271 processMap(prevData[i.key()].toMap(), i.value().toMap(), map);
272 // existing list item found - remove it from prevData
273 prevData.remove(i.key());
274 if (!map.isEmpty())
276 // changed list item found - append its changes to syncData
277 syncData[i.key()] = map;
280 break;
281 case QMetaType::QStringList:
282 if (!prevData.contains(i.key()))
284 // new list item found - append it to syncData
285 syncData[i.key()] = i.value();
287 else
289 QVariantList list;
290 QVariantList removedList;
291 processList(prevData[i.key()].toList(), i.value().toList(), list, removedList);
292 // existing list item found - remove it from prevData
293 prevData.remove(i.key());
294 if (!list.isEmpty() || !removedList.isEmpty())
296 // changed list item found - append entire list to syncData
297 syncData[i.key()] = i.value();
300 break;
301 default:
302 Q_ASSERT(false);
303 break;
307 if (!prevData.isEmpty())
309 // prevData contains only items that are missing now -
310 // put them in removedItems
311 for (auto i = prevData.cbegin(); i != prevData.cend(); ++i)
312 removedItems << i.key();
317 // Compare two lists of simple value (prevData, data) and calculate difference (syncData, removedItems).
318 void processList(QVariantList prevData, const QVariantList &data, QVariantList &syncData, QVariantList &removedItems)
320 // initialize output variables
321 syncData.clear();
322 removedItems.clear();
324 if (prevData.isEmpty())
326 // If list was empty before, then difference is a whole new list.
327 syncData = data;
329 else
331 for (const QVariant &item : data)
333 if (!prevData.contains(item))
335 // new list item found - append it to syncData
336 syncData.append(item);
338 else
340 // unchanged list item found - remove it from prevData
341 prevData.removeOne(item);
345 if (!prevData.isEmpty())
347 // prevData contains only items that are missing now -
348 // put them in removedItems
349 removedItems = prevData;
354 QJsonObject generateSyncData(int acceptedResponseId, const QVariantMap &data, QVariantMap &lastAcceptedData, QVariantMap &lastData)
356 QVariantMap syncData;
357 bool fullUpdate = true;
358 const int lastResponseId = (acceptedResponseId > 0) ? lastData[KEY_RESPONSE_ID].toInt() : 0;
359 if (lastResponseId > 0)
361 if (lastResponseId == acceptedResponseId)
362 lastAcceptedData = lastData;
364 if (const int lastAcceptedResponseId = lastAcceptedData[KEY_RESPONSE_ID].toInt()
365 ; lastAcceptedResponseId == acceptedResponseId)
367 fullUpdate = false;
371 if (fullUpdate)
373 lastAcceptedData.clear();
374 syncData = data;
375 syncData[KEY_FULL_UPDATE] = true;
377 else
379 processMap(lastAcceptedData, data, syncData);
382 const int responseId = (lastResponseId % 1000000) + 1; // cycle between 1 and 1000000
383 lastData = data;
384 lastData[KEY_RESPONSE_ID] = responseId;
385 syncData[KEY_RESPONSE_ID] = responseId;
387 return QJsonObject::fromVariantMap(syncData);
391 SyncController::SyncController(IApplication *app, QObject *parent)
392 : APIController(app, parent)
394 invokeChecker();
395 m_freeDiskSpaceElapsedTimer.start();
398 // The function returns the changed data from the server to synchronize with the web client.
399 // Return value is map in JSON format.
400 // Map contain the key:
401 // - "Rid": ID response
402 // Map can contain the keys:
403 // - "full_update": full data update flag
404 // - "torrents": dictionary contains information about torrents.
405 // - "torrents_removed": a list of hashes of removed torrents
406 // - "categories": map of categories info
407 // - "categories_removed": list of removed categories
408 // - "trackers": dictionary contains information about trackers
409 // - "trackers_removed": a list of removed trackers
410 // - "server_state": map contains information about the state of the server
411 // The keys of the 'torrents' dictionary are hashes of torrents.
412 // Each value of the 'torrents' dictionary contains map. The map can contain following keys:
413 // - "name": Torrent name
414 // - "size": Torrent size
415 // - "progress": Torrent progress
416 // - "dlspeed": Torrent download speed
417 // - "upspeed": Torrent upload speed
418 // - "priority": Torrent queue position (-1 if queuing is disabled)
419 // - "num_seeds": Torrent seeds connected to
420 // - "num_complete": Torrent seeds in the swarm
421 // - "num_leechs": Torrent leechers connected to
422 // - "num_incomplete": Torrent leechers in the swarm
423 // - "ratio": Torrent share ratio
424 // - "eta": Torrent ETA
425 // - "state": Torrent state
426 // - "seq_dl": Torrent sequential download state
427 // - "f_l_piece_prio": Torrent first last piece priority state
428 // - "completion_on": Torrent copletion time
429 // - "tracker": Torrent tracker
430 // - "dl_limit": Torrent download limit
431 // - "up_limit": Torrent upload limit
432 // - "downloaded": Amount of data downloaded
433 // - "uploaded": Amount of data uploaded
434 // - "downloaded_session": Amount of data downloaded since program open
435 // - "uploaded_session": Amount of data uploaded since program open
436 // - "amount_left": Amount of data left to download
437 // - "save_path": Torrent save path
438 // - "download_path": Torrent download path
439 // - "completed": Amount of data completed
440 // - "max_ratio": Upload max share ratio
441 // - "max_seeding_time": Upload max seeding time
442 // - "ratio_limit": Upload share ratio limit
443 // - "seeding_time_limit": Upload seeding time limit
444 // - "seen_complete": Indicates the time when the torrent was last seen complete/whole
445 // - "last_activity": Last time when a chunk was downloaded/uploaded
446 // - "total_size": Size including unwanted data
447 // Server state map may contain the following keys:
448 // - "connection_status": connection status
449 // - "dht_nodes": DHT nodes count
450 // - "dl_info_data": bytes downloaded
451 // - "dl_info_speed": download speed
452 // - "dl_rate_limit: download rate limit
453 // - "up_info_data: bytes uploaded
454 // - "up_info_speed: upload speed
455 // - "up_rate_limit: upload speed limit
456 // - "queueing": queue system usage flag
457 // - "refresh_interval": torrents table refresh interval
458 // - "free_space_on_disk": Free space on the default save path
459 // GET param:
460 // - rid (int): last response id
461 void SyncController::maindataAction()
463 if (m_maindataAcceptedID < 0)
465 makeMaindataSnapshot();
467 const auto *btSession = BitTorrent::Session::instance();
468 connect(btSession, &BitTorrent::Session::categoryAdded, this, &SyncController::onCategoryAdded);
469 connect(btSession, &BitTorrent::Session::categoryRemoved, this, &SyncController::onCategoryRemoved);
470 connect(btSession, &BitTorrent::Session::categoryOptionsChanged, this, &SyncController::onCategoryOptionsChanged);
471 connect(btSession, &BitTorrent::Session::subcategoriesSupportChanged, this, &SyncController::onSubcategoriesSupportChanged);
472 connect(btSession, &BitTorrent::Session::tagAdded, this, &SyncController::onTagAdded);
473 connect(btSession, &BitTorrent::Session::tagRemoved, this, &SyncController::onTagRemoved);
474 connect(btSession, &BitTorrent::Session::torrentAdded, this, &SyncController::onTorrentAdded);
475 connect(btSession, &BitTorrent::Session::torrentAboutToBeRemoved, this, &SyncController::onTorrentAboutToBeRemoved);
476 connect(btSession, &BitTorrent::Session::torrentCategoryChanged, this, &SyncController::onTorrentCategoryChanged);
477 connect(btSession, &BitTorrent::Session::torrentMetadataReceived, this, &SyncController::onTorrentMetadataReceived);
478 connect(btSession, &BitTorrent::Session::torrentPaused, this, &SyncController::onTorrentPaused);
479 connect(btSession, &BitTorrent::Session::torrentResumed, this, &SyncController::onTorrentResumed);
480 connect(btSession, &BitTorrent::Session::torrentSavePathChanged, this, &SyncController::onTorrentSavePathChanged);
481 connect(btSession, &BitTorrent::Session::torrentSavingModeChanged, this, &SyncController::onTorrentSavingModeChanged);
482 connect(btSession, &BitTorrent::Session::torrentTagAdded, this, &SyncController::onTorrentTagAdded);
483 connect(btSession, &BitTorrent::Session::torrentTagRemoved, this, &SyncController::onTorrentTagRemoved);
484 connect(btSession, &BitTorrent::Session::torrentsUpdated, this, &SyncController::onTorrentsUpdated);
485 connect(btSession, &BitTorrent::Session::trackersAdded, this, &SyncController::onTorrentTrackersChanged);
486 connect(btSession, &BitTorrent::Session::trackersRemoved, this, &SyncController::onTorrentTrackersChanged);
487 connect(btSession, &BitTorrent::Session::trackersChanged, this, &SyncController::onTorrentTrackersChanged);
490 const int acceptedID = params()[u"rid"_s].toInt();
491 bool fullUpdate = true;
492 if ((acceptedID > 0) && (m_maindataLastSentID > 0))
494 if (m_maindataLastSentID == acceptedID)
496 m_maindataAcceptedID = acceptedID;
497 m_maindataSyncBuf = {};
500 if (m_maindataAcceptedID == acceptedID)
502 // We are still able to send changes for the current state of the data having by client.
503 fullUpdate = false;
507 const int id = (m_maindataLastSentID % 1000000) + 1; // cycle between 1 and 1000000
508 setResult(generateMaindataSyncData(id, fullUpdate));
509 m_maindataLastSentID = id;
512 void SyncController::makeMaindataSnapshot()
514 m_knownTrackers.clear();
515 m_maindataAcceptedID = 0;
516 m_maindataSnapshot = {};
518 const auto *session = BitTorrent::Session::instance();
520 for (const BitTorrent::Torrent *torrent : asConst(session->torrents()))
522 const BitTorrent::TorrentID torrentID = torrent->id();
524 QVariantMap serializedTorrent = serialize(*torrent);
525 serializedTorrent.remove(KEY_TORRENT_ID);
527 for (const BitTorrent::TrackerEntry &tracker : asConst(torrent->trackers()))
528 m_knownTrackers[tracker.url].insert(torrentID);
530 m_maindataSnapshot.torrents[torrentID.toString()] = serializedTorrent;
533 const QStringList categoriesList = session->categories();
534 for (const auto &categoryName : categoriesList)
536 const BitTorrent::CategoryOptions categoryOptions = session->categoryOptions(categoryName);
537 QJsonObject category = categoryOptions.toJSON();
538 // adjust it to be compatible with existing WebAPI
539 category[u"savePath"_s] = category.take(u"save_path"_s);
540 category.insert(u"name"_s, categoryName);
541 m_maindataSnapshot.categories[categoryName] = category.toVariantMap();
544 for (const QString &tag : asConst(session->tags()))
545 m_maindataSnapshot.tags.append(tag);
547 for (auto trackersIter = m_knownTrackers.cbegin(); trackersIter != m_knownTrackers.cend(); ++trackersIter)
549 QStringList torrentIDs;
550 for (const BitTorrent::TorrentID &torrentID : asConst(trackersIter.value()))
551 torrentIDs.append(torrentID.toString());
553 m_maindataSnapshot.trackers[trackersIter.key()] = torrentIDs;
556 m_maindataSnapshot.serverState = getTransferInfo();
557 m_maindataSnapshot.serverState[KEY_TRANSFER_FREESPACEONDISK] = getFreeDiskSpace();
558 m_maindataSnapshot.serverState[KEY_SYNC_MAINDATA_QUEUEING] = session->isQueueingSystemEnabled();
559 m_maindataSnapshot.serverState[KEY_SYNC_MAINDATA_USE_ALT_SPEED_LIMITS] = session->isAltGlobalSpeedLimitEnabled();
560 m_maindataSnapshot.serverState[KEY_SYNC_MAINDATA_REFRESH_INTERVAL] = session->refreshInterval();
561 m_maindataSnapshot.serverState[KEY_SYNC_MAINDATA_USE_SUBCATEGORIES] = session->isSubcategoriesEnabled();
564 QJsonObject SyncController::generateMaindataSyncData(const int id, const bool fullUpdate)
566 // if need to update existing sync data
567 for (const QString &category : asConst(m_updatedCategories))
568 m_maindataSyncBuf.removedCategories.removeOne(category);
569 for (const QString &category : asConst(m_removedCategories))
570 m_maindataSyncBuf.categories.remove(category);
572 for (const QString &tag : asConst(m_addedTags))
573 m_maindataSyncBuf.removedTags.removeOne(tag);
574 for (const QString &tag : asConst(m_removedTags))
575 m_maindataSyncBuf.tags.removeOne(tag);
577 for (const BitTorrent::TorrentID &torrentID : asConst(m_updatedTorrents))
578 m_maindataSyncBuf.removedTorrents.removeOne(torrentID.toString());
579 for (const BitTorrent::TorrentID &torrentID : asConst(m_removedTorrents))
580 m_maindataSyncBuf.torrents.remove(torrentID.toString());
582 for (const QString &tracker : asConst(m_updatedTrackers))
583 m_maindataSyncBuf.removedTrackers.removeOne(tracker);
584 for (const QString &tracker : asConst(m_removedTrackers))
585 m_maindataSyncBuf.trackers.remove(tracker);
587 const auto *session = BitTorrent::Session::instance();
589 for (const QString &categoryName : asConst(m_updatedCategories))
591 const BitTorrent::CategoryOptions categoryOptions = session->categoryOptions(categoryName);
592 auto category = categoryOptions.toJSON().toVariantMap();
593 // adjust it to be compatible with existing WebAPI
594 category[u"savePath"_s] = category.take(u"save_path"_s);
595 category.insert(u"name"_s, categoryName);
597 auto &categorySnapshot = m_maindataSnapshot.categories[categoryName];
598 processMap(categorySnapshot, category, m_maindataSyncBuf.categories[categoryName]);
599 categorySnapshot = category;
601 m_updatedCategories.clear();
603 for (const QString &category : asConst(m_removedCategories))
605 m_maindataSyncBuf.removedCategories.append(category);
606 m_maindataSnapshot.categories.remove(category);
608 m_removedCategories.clear();
610 for (const QString &tag : asConst(m_addedTags))
612 m_maindataSyncBuf.tags.append(tag);
613 m_maindataSnapshot.tags.append(tag);
615 m_addedTags.clear();
617 for (const QString &tag : asConst(m_removedTags))
619 m_maindataSyncBuf.removedTags.append(tag);
620 m_maindataSnapshot.tags.removeOne(tag);
622 m_removedTags.clear();
624 for (const BitTorrent::TorrentID &torrentID : asConst(m_updatedTorrents))
626 const BitTorrent::Torrent *torrent = session->getTorrent(torrentID);
627 Q_ASSERT(torrent);
629 QVariantMap serializedTorrent = serialize(*torrent);
630 serializedTorrent.remove(KEY_TORRENT_ID);
632 auto &torrentSnapshot = m_maindataSnapshot.torrents[torrentID.toString()];
633 processMap(torrentSnapshot, serializedTorrent, m_maindataSyncBuf.torrents[torrentID.toString()]);
634 torrentSnapshot = serializedTorrent;
636 m_updatedTorrents.clear();
638 for (const BitTorrent::TorrentID &torrentID : asConst(m_removedTorrents))
640 m_maindataSyncBuf.removedTorrents.append(torrentID.toString());
641 m_maindataSnapshot.torrents.remove(torrentID.toString());
643 m_removedTorrents.clear();
645 for (const QString &tracker : asConst(m_updatedTrackers))
647 const QSet<BitTorrent::TorrentID> torrentIDs = m_knownTrackers[tracker];
648 QStringList serializedTorrentIDs;
649 serializedTorrentIDs.reserve(torrentIDs.size());
650 for (const BitTorrent::TorrentID &torrentID : torrentIDs)
651 serializedTorrentIDs.append(torrentID.toString());
653 m_maindataSyncBuf.trackers[tracker] = serializedTorrentIDs;
654 m_maindataSnapshot.trackers[tracker] = serializedTorrentIDs;
656 m_updatedTrackers.clear();
658 for (const QString &tracker : asConst(m_removedTrackers))
660 m_maindataSyncBuf.removedTrackers.append(tracker);
661 m_maindataSnapshot.trackers.remove(tracker);
663 m_removedTrackers.clear();
665 QVariantMap serverState = getTransferInfo();
666 serverState[KEY_TRANSFER_FREESPACEONDISK] = getFreeDiskSpace();
667 serverState[KEY_SYNC_MAINDATA_QUEUEING] = session->isQueueingSystemEnabled();
668 serverState[KEY_SYNC_MAINDATA_USE_ALT_SPEED_LIMITS] = session->isAltGlobalSpeedLimitEnabled();
669 serverState[KEY_SYNC_MAINDATA_REFRESH_INTERVAL] = session->refreshInterval();
670 serverState[KEY_SYNC_MAINDATA_USE_SUBCATEGORIES] = session->isSubcategoriesEnabled();
671 processMap(m_maindataSnapshot.serverState, serverState, m_maindataSyncBuf.serverState);
672 m_maindataSnapshot.serverState = serverState;
674 QJsonObject syncData;
675 syncData[KEY_RESPONSE_ID] = id;
676 if (fullUpdate)
678 m_maindataSyncBuf = m_maindataSnapshot;
679 syncData[KEY_FULL_UPDATE] = true;
682 if (!m_maindataSyncBuf.categories.isEmpty())
684 QJsonObject categories;
685 for (auto it = m_maindataSyncBuf.categories.cbegin(); it != m_maindataSyncBuf.categories.cend(); ++it)
686 categories[it.key()] = QJsonObject::fromVariantMap(it.value());
687 syncData[KEY_CATEGORIES] = categories;
689 if (!m_maindataSyncBuf.removedCategories.isEmpty())
690 syncData[KEY_CATEGORIES_REMOVED] = QJsonArray::fromStringList(m_maindataSyncBuf.removedCategories);
692 if (!m_maindataSyncBuf.tags.isEmpty())
693 syncData[KEY_TAGS] = QJsonArray::fromVariantList(m_maindataSyncBuf.tags);
694 if (!m_maindataSyncBuf.removedTags.isEmpty())
695 syncData[KEY_TAGS_REMOVED] = QJsonArray::fromStringList(m_maindataSyncBuf.removedTags);
697 if (!m_maindataSyncBuf.torrents.isEmpty())
699 QJsonObject torrents;
700 for (auto it = m_maindataSyncBuf.torrents.cbegin(); it != m_maindataSyncBuf.torrents.cend(); ++it)
701 torrents[it.key()] = QJsonObject::fromVariantMap(it.value());
702 syncData[KEY_TORRENTS] = torrents;
704 if (!m_maindataSyncBuf.removedTorrents.isEmpty())
705 syncData[KEY_TORRENTS_REMOVED] = QJsonArray::fromStringList(m_maindataSyncBuf.removedTorrents);
707 if (!m_maindataSyncBuf.trackers.isEmpty())
709 QJsonObject trackers;
710 for (auto it = m_maindataSyncBuf.trackers.cbegin(); it != m_maindataSyncBuf.trackers.cend(); ++it)
711 trackers[it.key()] = QJsonArray::fromStringList(it.value());
712 syncData[KEY_TRACKERS] = trackers;
714 if (!m_maindataSyncBuf.removedTrackers.isEmpty())
715 syncData[KEY_TRACKERS_REMOVED] = QJsonArray::fromStringList(m_maindataSyncBuf.removedTrackers);
717 if (!m_maindataSyncBuf.serverState.isEmpty())
718 syncData[KEY_SERVER_STATE] = QJsonObject::fromVariantMap(m_maindataSyncBuf.serverState);
720 return syncData;
723 // GET param:
724 // - hash (string): torrent hash (ID)
725 // - rid (int): last response id
726 void SyncController::torrentPeersAction()
728 const auto id = BitTorrent::TorrentID::fromString(params()[u"hash"_s]);
729 const BitTorrent::Torrent *torrent = BitTorrent::Session::instance()->getTorrent(id);
730 if (!torrent)
731 throw APIError(APIErrorType::NotFound);
733 QVariantMap data;
734 QVariantHash peers;
736 const QVector<BitTorrent::PeerInfo> peersList = torrent->peers();
738 bool resolvePeerCountries = Preferences::instance()->resolvePeerCountries();
740 data[KEY_SYNC_TORRENT_PEERS_SHOW_FLAGS] = resolvePeerCountries;
742 for (const BitTorrent::PeerInfo &pi : peersList)
744 if (pi.address().ip.isNull()) continue;
746 QVariantMap peer =
748 {KEY_PEER_IP, pi.address().ip.toString()},
749 {KEY_PEER_PORT, pi.address().port},
750 {KEY_PEER_CLIENT, pi.client()},
751 {KEY_PEER_ID_CLIENT, pi.peerIdClient()},
752 {KEY_PEER_PROGRESS, pi.progress()},
753 {KEY_PEER_DOWN_SPEED, pi.payloadDownSpeed()},
754 {KEY_PEER_UP_SPEED, pi.payloadUpSpeed()},
755 {KEY_PEER_TOT_DOWN, pi.totalDownload()},
756 {KEY_PEER_TOT_UP, pi.totalUpload()},
757 {KEY_PEER_CONNECTION_TYPE, pi.connectionType()},
758 {KEY_PEER_FLAGS, pi.flags()},
759 {KEY_PEER_FLAGS_DESCRIPTION, pi.flagsDescription()},
760 {KEY_PEER_RELEVANCE, pi.relevance()}
763 if (torrent->hasMetadata())
765 const PathList filePaths = torrent->info().filesForPiece(pi.downloadingPieceIndex());
766 QStringList filesForPiece;
767 filesForPiece.reserve(filePaths.size());
768 for (const Path &filePath : filePaths)
769 filesForPiece.append(filePath.toString());
770 peer.insert(KEY_PEER_FILES, filesForPiece.join(u'\n'));
773 if (resolvePeerCountries)
775 peer[KEY_PEER_COUNTRY_CODE] = pi.country().toLower();
776 peer[KEY_PEER_COUNTRY] = Net::GeoIPManager::CountryName(pi.country());
779 peers[pi.address().toString()] = peer;
781 data[u"peers"_s] = peers;
783 const int acceptedResponseId = params()[u"rid"_s].toInt();
784 setResult(generateSyncData(acceptedResponseId, data, m_lastAcceptedPeersResponse, m_lastPeersResponse));
787 qint64 SyncController::getFreeDiskSpace()
789 if (m_freeDiskSpaceElapsedTimer.hasExpired(FREEDISKSPACE_CHECK_TIMEOUT))
790 invokeChecker();
792 return m_freeDiskSpace;
795 void SyncController::invokeChecker()
797 if (m_isFreeDiskSpaceCheckerRunning)
798 return;
800 auto *freeDiskSpaceChecker = new FreeDiskSpaceChecker;
801 connect(freeDiskSpaceChecker, &FreeDiskSpaceChecker::checked, this, [this](const qint64 freeSpaceSize)
803 m_freeDiskSpace = freeSpaceSize;
804 m_isFreeDiskSpaceCheckerRunning = false;
805 m_freeDiskSpaceElapsedTimer.restart();
807 connect(freeDiskSpaceChecker, &FreeDiskSpaceChecker::checked, freeDiskSpaceChecker, &QObject::deleteLater);
808 m_isFreeDiskSpaceCheckerRunning = true;
809 QThreadPool::globalInstance()->start([freeDiskSpaceChecker]
811 freeDiskSpaceChecker->check();
815 void SyncController::onCategoryAdded(const QString &categoryName)
817 m_removedCategories.remove(categoryName);
818 m_updatedCategories.insert(categoryName);
821 void SyncController::onCategoryRemoved(const QString &categoryName)
823 m_updatedCategories.remove(categoryName);
824 m_removedCategories.insert(categoryName);
827 void SyncController::onCategoryOptionsChanged(const QString &categoryName)
829 Q_ASSERT(!m_removedCategories.contains(categoryName));
831 m_updatedCategories.insert(categoryName);
834 void SyncController::onSubcategoriesSupportChanged()
836 const QStringList categoriesList = BitTorrent::Session::instance()->categories();
837 for (const auto &categoryName : categoriesList)
839 if (!m_maindataSnapshot.categories.contains(categoryName))
841 m_removedCategories.remove(categoryName);
842 m_updatedCategories.insert(categoryName);
847 void SyncController::onTagAdded(const QString &tag)
849 m_removedTags.remove(tag);
850 m_addedTags.insert(tag);
853 void SyncController::onTagRemoved(const QString &tag)
855 m_addedTags.remove(tag);
856 m_removedTags.insert(tag);
859 void SyncController::onTorrentAdded(BitTorrent::Torrent *torrent)
861 const BitTorrent::TorrentID torrentID = torrent->id();
863 m_removedTorrents.remove(torrentID);
864 m_updatedTorrents.insert(torrentID);
866 for (const BitTorrent::TrackerEntry &trackerEntry : asConst(torrent->trackers()))
868 m_knownTrackers[trackerEntry.url].insert(torrentID);
869 m_updatedTrackers.insert(trackerEntry.url);
870 m_removedTrackers.remove(trackerEntry.url);
874 void SyncController::onTorrentAboutToBeRemoved(BitTorrent::Torrent *torrent)
876 const BitTorrent::TorrentID torrentID = torrent->id();
878 m_updatedTorrents.remove(torrentID);
879 m_removedTorrents.insert(torrentID);
881 for (const BitTorrent::TrackerEntry &trackerEntry : asConst(torrent->trackers()))
883 auto iter = m_knownTrackers.find(trackerEntry.url);
884 Q_ASSERT(iter != m_knownTrackers.end());
885 if (iter == m_knownTrackers.end()) [[unlikely]]
886 continue;
888 QSet<BitTorrent::TorrentID> &torrentIDs = iter.value();
889 torrentIDs.remove(torrentID);
890 if (torrentIDs.isEmpty())
892 m_knownTrackers.erase(iter);
893 m_updatedTrackers.remove(trackerEntry.url);
894 m_removedTrackers.insert(trackerEntry.url);
896 else
898 m_updatedTrackers.insert(trackerEntry.url);
903 void SyncController::onTorrentCategoryChanged(BitTorrent::Torrent *torrent
904 , [[maybe_unused]] const QString &oldCategory)
906 m_updatedTorrents.insert(torrent->id());
909 void SyncController::onTorrentMetadataReceived(BitTorrent::Torrent *torrent)
911 m_updatedTorrents.insert(torrent->id());
914 void SyncController::onTorrentPaused(BitTorrent::Torrent *torrent)
916 m_updatedTorrents.insert(torrent->id());
919 void SyncController::onTorrentResumed(BitTorrent::Torrent *torrent)
921 m_updatedTorrents.insert(torrent->id());
924 void SyncController::onTorrentSavePathChanged(BitTorrent::Torrent *torrent)
926 m_updatedTorrents.insert(torrent->id());
929 void SyncController::onTorrentSavingModeChanged(BitTorrent::Torrent *torrent)
931 m_updatedTorrents.insert(torrent->id());
934 void SyncController::onTorrentTagAdded(BitTorrent::Torrent *torrent, [[maybe_unused]] const QString &tag)
936 m_updatedTorrents.insert(torrent->id());
939 void SyncController::onTorrentTagRemoved(BitTorrent::Torrent *torrent, [[maybe_unused]] const QString &tag)
941 m_updatedTorrents.insert(torrent->id());
944 void SyncController::onTorrentsUpdated(const QVector<BitTorrent::Torrent *> &torrents)
946 for (const BitTorrent::Torrent *torrent : torrents)
947 m_updatedTorrents.insert(torrent->id());
950 void SyncController::onTorrentTrackersChanged(BitTorrent::Torrent *torrent)
952 using namespace BitTorrent;
954 const QVector<TrackerEntry> currentTrackerEntries = torrent->trackers();
955 QSet<QString> currentTrackers;
956 currentTrackers.reserve(currentTrackerEntries.size());
957 for (const TrackerEntry &currentTrackerEntry : currentTrackerEntries)
958 currentTrackers.insert(currentTrackerEntry.url);
960 const TorrentID torrentID = torrent->id();
961 Algorithm::removeIf(m_knownTrackers
962 , [this, torrentID, currentTrackers](const QString &knownTracker, QSet<TorrentID> &torrentIDs)
964 if (auto idIter = torrentIDs.find(torrentID)
965 ; (idIter != torrentIDs.end()) && !currentTrackers.contains(knownTracker))
967 torrentIDs.erase(idIter);
968 if (torrentIDs.isEmpty())
970 m_updatedTrackers.remove(knownTracker);
971 m_removedTrackers.insert(knownTracker);
972 return true;
975 m_updatedTrackers.insert(knownTracker);
976 return false;
979 if (currentTrackers.contains(knownTracker) && !torrentIDs.contains(torrentID))
981 torrentIDs.insert(torrentID);
982 m_updatedTrackers.insert(knownTracker);
983 return false;
986 return false;
989 for (const QString &currentTracker : asConst(currentTrackers))
991 if (!m_knownTrackers.contains(currentTracker))
993 m_knownTrackers.insert(currentTracker, {torrentID});
994 m_updatedTrackers.insert(currentTracker);
995 m_removedTrackers.remove(currentTracker);