2 * Bittorrent Client using Qt and libtorrent.
3 * Copyright (C) 2018-2024 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"
32 #include <QJsonObject>
33 #include <QMetaObject>
35 #include "base/algorithm.h"
36 #include "base/bittorrent/cachestatus.h"
37 #include "base/bittorrent/infohash.h"
38 #include "base/bittorrent/peeraddress.h"
39 #include "base/bittorrent/peerinfo.h"
40 #include "base/bittorrent/session.h"
41 #include "base/bittorrent/sessionstatus.h"
42 #include "base/bittorrent/torrent.h"
43 #include "base/bittorrent/torrentinfo.h"
44 #include "base/bittorrent/trackerentrystatus.h"
45 #include "base/global.h"
46 #include "base/net/geoipmanager.h"
47 #include "base/preferences.h"
48 #include "base/utils/string.h"
50 #include "serialize/serialize_torrent.h"
54 // Sync main data keys
55 const QString KEY_SYNC_MAINDATA_QUEUEING
= u
"queueing"_s
;
56 const QString KEY_SYNC_MAINDATA_REFRESH_INTERVAL
= u
"refresh_interval"_s
;
57 const QString KEY_SYNC_MAINDATA_USE_ALT_SPEED_LIMITS
= u
"use_alt_speed_limits"_s
;
58 const QString KEY_SYNC_MAINDATA_USE_SUBCATEGORIES
= u
"use_subcategories"_s
;
60 // Sync torrent peers keys
61 const QString KEY_SYNC_TORRENT_PEERS_SHOW_FLAGS
= u
"show_flags"_s
;
64 const QString KEY_PEER_CLIENT
= u
"client"_s
;
65 const QString KEY_PEER_ID_CLIENT
= u
"peer_id_client"_s
;
66 const QString KEY_PEER_CONNECTION_TYPE
= u
"connection"_s
;
67 const QString KEY_PEER_COUNTRY
= u
"country"_s
;
68 const QString KEY_PEER_COUNTRY_CODE
= u
"country_code"_s
;
69 const QString KEY_PEER_DOWN_SPEED
= u
"dl_speed"_s
;
70 const QString KEY_PEER_FILES
= u
"files"_s
;
71 const QString KEY_PEER_FLAGS
= u
"flags"_s
;
72 const QString KEY_PEER_FLAGS_DESCRIPTION
= u
"flags_desc"_s
;
73 const QString KEY_PEER_IP
= u
"ip"_s
;
74 const QString KEY_PEER_PORT
= u
"port"_s
;
75 const QString KEY_PEER_PROGRESS
= u
"progress"_s
;
76 const QString KEY_PEER_RELEVANCE
= u
"relevance"_s
;
77 const QString KEY_PEER_TOT_DOWN
= u
"downloaded"_s
;
78 const QString KEY_PEER_TOT_UP
= u
"uploaded"_s
;
79 const QString KEY_PEER_UP_SPEED
= u
"up_speed"_s
;
82 const QString KEY_TRANSFER_CONNECTION_STATUS
= u
"connection_status"_s
;
83 const QString KEY_TRANSFER_DHT_NODES
= u
"dht_nodes"_s
;
84 const QString KEY_TRANSFER_DLDATA
= u
"dl_info_data"_s
;
85 const QString KEY_TRANSFER_DLRATELIMIT
= u
"dl_rate_limit"_s
;
86 const QString KEY_TRANSFER_DLSPEED
= u
"dl_info_speed"_s
;
87 const QString KEY_TRANSFER_FREESPACEONDISK
= u
"free_space_on_disk"_s
;
88 const QString KEY_TRANSFER_LAST_EXTERNAL_ADDRESS_V4
= u
"last_external_address_v4"_s
;
89 const QString KEY_TRANSFER_LAST_EXTERNAL_ADDRESS_V6
= u
"last_external_address_v6"_s
;
90 const QString KEY_TRANSFER_UPDATA
= u
"up_info_data"_s
;
91 const QString KEY_TRANSFER_UPRATELIMIT
= u
"up_rate_limit"_s
;
92 const QString KEY_TRANSFER_UPSPEED
= u
"up_info_speed"_s
;
95 const QString KEY_TRANSFER_ALLTIME_DL
= u
"alltime_dl"_s
;
96 const QString KEY_TRANSFER_ALLTIME_UL
= u
"alltime_ul"_s
;
97 const QString KEY_TRANSFER_AVERAGE_TIME_QUEUE
= u
"average_time_queue"_s
;
98 const QString KEY_TRANSFER_GLOBAL_RATIO
= u
"global_ratio"_s
;
99 const QString KEY_TRANSFER_QUEUED_IO_JOBS
= u
"queued_io_jobs"_s
;
100 const QString KEY_TRANSFER_READ_CACHE_HITS
= u
"read_cache_hits"_s
;
101 const QString KEY_TRANSFER_READ_CACHE_OVERLOAD
= u
"read_cache_overload"_s
;
102 const QString KEY_TRANSFER_TOTAL_BUFFERS_SIZE
= u
"total_buffers_size"_s
;
103 const QString KEY_TRANSFER_TOTAL_PEER_CONNECTIONS
= u
"total_peer_connections"_s
;
104 const QString KEY_TRANSFER_TOTAL_QUEUED_SIZE
= u
"total_queued_size"_s
;
105 const QString KEY_TRANSFER_TOTAL_WASTE_SESSION
= u
"total_wasted_session"_s
;
106 const QString KEY_TRANSFER_WRITE_CACHE_OVERLOAD
= u
"write_cache_overload"_s
;
108 const QString KEY_SUFFIX_REMOVED
= u
"_removed"_s
;
110 const QString KEY_CATEGORIES
= u
"categories"_s
;
111 const QString KEY_CATEGORIES_REMOVED
= KEY_CATEGORIES
+ KEY_SUFFIX_REMOVED
;
112 const QString KEY_TAGS
= u
"tags"_s
;
113 const QString KEY_TAGS_REMOVED
= KEY_TAGS
+ KEY_SUFFIX_REMOVED
;
114 const QString KEY_TORRENTS
= u
"torrents"_s
;
115 const QString KEY_TORRENTS_REMOVED
= KEY_TORRENTS
+ KEY_SUFFIX_REMOVED
;
116 const QString KEY_TRACKERS
= u
"trackers"_s
;
117 const QString KEY_TRACKERS_REMOVED
= KEY_TRACKERS
+ KEY_SUFFIX_REMOVED
;
118 const QString KEY_SERVER_STATE
= u
"server_state"_s
;
119 const QString KEY_FULL_UPDATE
= u
"full_update"_s
;
120 const QString KEY_RESPONSE_ID
= u
"rid"_s
;
122 QVariantMap
processMap(const QVariantMap
&prevData
, const QVariantMap
&data
);
123 std::pair
<QVariantMap
, QVariantList
> processHash(QVariantHash prevData
, const QVariantHash
&data
);
124 std::pair
<QVariantList
, QVariantList
> processList(QVariantList prevData
, const QVariantList
&data
);
125 QJsonObject
generateSyncData(int acceptedResponseId
, const QVariantMap
&data
, QVariantMap
&lastAcceptedData
, QVariantMap
&lastData
);
127 QVariantMap
getTransferInfo()
130 const auto *session
= BitTorrent::Session::instance();
132 const BitTorrent::SessionStatus
&sessionStatus
= session
->status();
133 const BitTorrent::CacheStatus
&cacheStatus
= session
->cacheStatus();
134 map
[KEY_TRANSFER_DLSPEED
] = sessionStatus
.payloadDownloadRate
;
135 map
[KEY_TRANSFER_DLDATA
] = sessionStatus
.totalPayloadDownload
;
136 map
[KEY_TRANSFER_UPSPEED
] = sessionStatus
.payloadUploadRate
;
137 map
[KEY_TRANSFER_UPDATA
] = sessionStatus
.totalPayloadUpload
;
138 map
[KEY_TRANSFER_DLRATELIMIT
] = session
->downloadSpeedLimit();
139 map
[KEY_TRANSFER_UPRATELIMIT
] = session
->uploadSpeedLimit();
141 const qint64 atd
= sessionStatus
.allTimeDownload
;
142 const qint64 atu
= sessionStatus
.allTimeUpload
;
143 map
[KEY_TRANSFER_ALLTIME_DL
] = atd
;
144 map
[KEY_TRANSFER_ALLTIME_UL
] = atu
;
145 map
[KEY_TRANSFER_TOTAL_WASTE_SESSION
] = sessionStatus
.totalWasted
;
146 map
[KEY_TRANSFER_GLOBAL_RATIO
] = ((atd
> 0) && (atu
> 0)) ? Utils::String::fromDouble(static_cast<qreal
>(atu
) / atd
, 2) : u
"-"_s
;
147 map
[KEY_TRANSFER_TOTAL_PEER_CONNECTIONS
] = sessionStatus
.peersCount
;
149 const qreal readRatio
= cacheStatus
.readRatio
; // TODO: remove when LIBTORRENT_VERSION_NUM >= 20000
150 map
[KEY_TRANSFER_READ_CACHE_HITS
] = (readRatio
> 0) ? Utils::String::fromDouble(100 * readRatio
, 2) : u
"0"_s
;
151 map
[KEY_TRANSFER_TOTAL_BUFFERS_SIZE
] = cacheStatus
.totalUsedBuffers
* 16 * 1024;
153 map
[KEY_TRANSFER_WRITE_CACHE_OVERLOAD
] = ((sessionStatus
.diskWriteQueue
> 0) && (sessionStatus
.peersCount
> 0))
154 ? Utils::String::fromDouble((100. * sessionStatus
.diskWriteQueue
/ sessionStatus
.peersCount
), 2)
156 map
[KEY_TRANSFER_READ_CACHE_OVERLOAD
] = ((sessionStatus
.diskReadQueue
> 0) && (sessionStatus
.peersCount
> 0))
157 ? Utils::String::fromDouble((100. * sessionStatus
.diskReadQueue
/ sessionStatus
.peersCount
), 2)
160 map
[KEY_TRANSFER_QUEUED_IO_JOBS
] = cacheStatus
.jobQueueLength
;
161 map
[KEY_TRANSFER_AVERAGE_TIME_QUEUE
] = cacheStatus
.averageJobTime
;
162 map
[KEY_TRANSFER_TOTAL_QUEUED_SIZE
] = cacheStatus
.queuedBytes
;
164 map
[KEY_TRANSFER_LAST_EXTERNAL_ADDRESS_V4
] = session
->lastExternalIPv4Address();
165 map
[KEY_TRANSFER_LAST_EXTERNAL_ADDRESS_V6
] = session
->lastExternalIPv6Address();
166 map
[KEY_TRANSFER_DHT_NODES
] = sessionStatus
.dhtNodes
;
167 map
[KEY_TRANSFER_CONNECTION_STATUS
] = session
->isListening()
168 ? (sessionStatus
.hasIncomingConnections
? u
"connected"_s
: u
"firewalled"_s
)
174 // Compare two structures (prevData, data) and calculate difference (syncData).
175 // Structures encoded as map.
176 QVariantMap
processMap(const QVariantMap
&prevData
, const QVariantMap
&data
)
178 // initialize output variable
179 QVariantMap syncData
;
181 for (auto i
= data
.cbegin(); i
!= data
.cend(); ++i
)
183 const QString
&key
= i
.key();
184 const QVariant
&value
= i
.value();
186 switch (value
.userType())
188 case QMetaType::QVariantMap
:
190 const QVariantMap map
= processMap(prevData
[key
].toMap(), value
.toMap());
195 case QMetaType::QVariantHash
:
197 const auto [map
, removedItems
] = processHash(prevData
[key
].toHash(), value
.toHash());
200 if (!removedItems
.isEmpty())
201 syncData
[key
+ KEY_SUFFIX_REMOVED
] = removedItems
;
204 case QMetaType::QVariantList
:
206 const auto [list
, removedItems
] = processList(prevData
[key
].toList(), value
.toList());
208 syncData
[key
] = list
;
209 if (!removedItems
.isEmpty())
210 syncData
[key
+ KEY_SUFFIX_REMOVED
] = removedItems
;
213 case QMetaType::QString
:
214 case QMetaType::LongLong
:
215 case QMetaType::Float
:
217 case QMetaType::Bool
:
218 case QMetaType::Double
:
219 case QMetaType::ULongLong
:
220 case QMetaType::UInt
:
221 case QMetaType::QDateTime
:
222 case QMetaType::Nullptr
:
223 case QMetaType::UnknownType
:
224 if (prevData
[key
] != value
)
225 syncData
[key
] = value
;
228 Q_ASSERT_X(false, "processMap"
229 , u
"Unexpected type: %1"_s
.arg(QString::fromLatin1(value
.metaType().name()))
230 .toUtf8().constData());
237 // Compare two lists of structures (prevData, data) and calculate difference (syncData, removedItems).
238 // Structures encoded as map.
239 // Lists are encoded as hash table (indexed by structure key value) to improve ease of searching for removed items.
240 std::pair
<QVariantMap
, QVariantList
> processHash(QVariantHash prevData
, const QVariantHash
&data
)
242 // initialize output variables
243 std::pair
<QVariantMap
, QVariantList
> result
;
244 auto &[syncData
, removedItems
] = result
;
246 if (prevData
.isEmpty())
248 // If list was empty before, then difference is a whole new list.
249 for (auto i
= data
.cbegin(); i
!= data
.cend(); ++i
)
250 syncData
[i
.key()] = i
.value();
254 for (auto i
= data
.cbegin(); i
!= data
.cend(); ++i
)
256 switch (i
.value().userType())
258 case QMetaType::QVariantMap
:
259 if (!prevData
.contains(i
.key()))
261 // new list item found - append it to syncData
262 syncData
[i
.key()] = i
.value();
266 const QVariantMap map
= processMap(prevData
[i
.key()].toMap(), i
.value().toMap());
267 // existing list item found - remove it from prevData
268 prevData
.remove(i
.key());
271 // changed list item found - append its changes to syncData
272 syncData
[i
.key()] = map
;
276 case QMetaType::QStringList
:
277 if (!prevData
.contains(i
.key()))
279 // new list item found - append it to syncData
280 syncData
[i
.key()] = i
.value();
284 const auto [list
, removedList
] = processList(prevData
[i
.key()].toList(), i
.value().toList());
285 // existing list item found - remove it from prevData
286 prevData
.remove(i
.key());
287 if (!list
.isEmpty() || !removedList
.isEmpty())
289 // changed list item found - append entire list to syncData
290 syncData
[i
.key()] = i
.value();
300 if (!prevData
.isEmpty())
302 // prevData contains only items that are missing now -
303 // put them in removedItems
304 for (auto i
= prevData
.cbegin(); i
!= prevData
.cend(); ++i
)
305 removedItems
<< i
.key();
312 // Compare two lists of simple value (prevData, data) and calculate difference (syncData, removedItems).
313 std::pair
<QVariantList
, QVariantList
> processList(QVariantList prevData
, const QVariantList
&data
)
315 // initialize output variables
316 std::pair
<QVariantList
, QVariantList
> result
;
317 auto &[syncData
, removedItems
] = result
;
319 if (prevData
.isEmpty())
321 // If list was empty before, then difference is a whole new list.
326 for (const QVariant
&item
: data
)
328 if (!prevData
.contains(item
))
330 // new list item found - append it to syncData
331 syncData
.append(item
);
335 // unchanged list item found - remove it from prevData
336 prevData
.removeOne(item
);
340 if (!prevData
.isEmpty())
342 // prevData contains only items that are missing now -
343 // put them in removedItems
344 removedItems
= prevData
;
351 QJsonObject
generateSyncData(int acceptedResponseId
, const QVariantMap
&data
, QVariantMap
&lastAcceptedData
, QVariantMap
&lastData
)
353 QVariantMap syncData
;
354 bool fullUpdate
= true;
355 const int lastResponseId
= (acceptedResponseId
> 0) ? lastData
[KEY_RESPONSE_ID
].toInt() : 0;
356 if (lastResponseId
> 0)
358 if (lastResponseId
== acceptedResponseId
)
359 lastAcceptedData
= lastData
;
361 if (const int lastAcceptedResponseId
= lastAcceptedData
[KEY_RESPONSE_ID
].toInt()
362 ; lastAcceptedResponseId
== acceptedResponseId
)
370 lastAcceptedData
.clear();
372 syncData
[KEY_FULL_UPDATE
] = true;
376 syncData
= processMap(lastAcceptedData
, data
);
379 const int responseId
= (lastResponseId
% 1000000) + 1; // cycle between 1 and 1000000
381 lastData
[KEY_RESPONSE_ID
] = responseId
;
382 syncData
[KEY_RESPONSE_ID
] = responseId
;
384 return QJsonObject::fromVariantMap(syncData
);
388 SyncController::SyncController(IApplication
*app
, QObject
*parent
)
389 : APIController(app
, parent
)
393 void SyncController::updateFreeDiskSpace(const qint64 freeDiskSpace
)
395 m_freeDiskSpace
= freeDiskSpace
;
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 // - "last_external_address_v4": last external address v4
454 // - "last_external_address_v6": last external address v6
455 // - "up_info_data: bytes uploaded
456 // - "up_info_speed: upload speed
457 // - "up_rate_limit: upload speed limit
458 // - "queueing": queue system usage flag
459 // - "refresh_interval": torrents table refresh interval
460 // - "free_space_on_disk": Free space on the default save path
462 // - rid (int): last response id
463 void SyncController::maindataAction()
465 if (m_maindataAcceptedID
< 0)
467 makeMaindataSnapshot();
469 const auto *btSession
= BitTorrent::Session::instance();
470 connect(btSession
, &BitTorrent::Session::categoryAdded
, this, &SyncController::onCategoryAdded
);
471 connect(btSession
, &BitTorrent::Session::categoryRemoved
, this, &SyncController::onCategoryRemoved
);
472 connect(btSession
, &BitTorrent::Session::categoryOptionsChanged
, this, &SyncController::onCategoryOptionsChanged
);
473 connect(btSession
, &BitTorrent::Session::subcategoriesSupportChanged
, this, &SyncController::onSubcategoriesSupportChanged
);
474 connect(btSession
, &BitTorrent::Session::tagAdded
, this, &SyncController::onTagAdded
);
475 connect(btSession
, &BitTorrent::Session::tagRemoved
, this, &SyncController::onTagRemoved
);
476 connect(btSession
, &BitTorrent::Session::torrentAdded
, this, &SyncController::onTorrentAdded
);
477 connect(btSession
, &BitTorrent::Session::torrentAboutToBeRemoved
, this, &SyncController::onTorrentAboutToBeRemoved
);
478 connect(btSession
, &BitTorrent::Session::torrentCategoryChanged
, this, &SyncController::onTorrentCategoryChanged
);
479 connect(btSession
, &BitTorrent::Session::torrentMetadataReceived
, this, &SyncController::onTorrentMetadataReceived
);
480 connect(btSession
, &BitTorrent::Session::torrentStopped
, this, &SyncController::onTorrentStopped
);
481 connect(btSession
, &BitTorrent::Session::torrentStarted
, this, &SyncController::onTorrentStarted
);
482 connect(btSession
, &BitTorrent::Session::torrentSavePathChanged
, this, &SyncController::onTorrentSavePathChanged
);
483 connect(btSession
, &BitTorrent::Session::torrentSavingModeChanged
, this, &SyncController::onTorrentSavingModeChanged
);
484 connect(btSession
, &BitTorrent::Session::torrentTagAdded
, this, &SyncController::onTorrentTagAdded
);
485 connect(btSession
, &BitTorrent::Session::torrentTagRemoved
, this, &SyncController::onTorrentTagRemoved
);
486 connect(btSession
, &BitTorrent::Session::torrentsUpdated
, this, &SyncController::onTorrentsUpdated
);
487 connect(btSession
, &BitTorrent::Session::trackersAdded
, this, &SyncController::onTorrentTrackersChanged
);
488 connect(btSession
, &BitTorrent::Session::trackersRemoved
, this, &SyncController::onTorrentTrackersChanged
);
489 connect(btSession
, &BitTorrent::Session::trackersChanged
, this, &SyncController::onTorrentTrackersChanged
);
492 const int acceptedID
= params()[u
"rid"_s
].toInt();
493 bool fullUpdate
= true;
494 if ((acceptedID
> 0) && (m_maindataLastSentID
> 0))
496 if (m_maindataLastSentID
== acceptedID
)
498 m_maindataAcceptedID
= acceptedID
;
499 m_maindataSyncBuf
= {};
502 if (m_maindataAcceptedID
== acceptedID
)
504 // We are still able to send changes for the current state of the data having by client.
509 const int id
= (m_maindataLastSentID
% 1000000) + 1; // cycle between 1 and 1000000
510 setResult(generateMaindataSyncData(id
, fullUpdate
));
511 m_maindataLastSentID
= id
;
514 void SyncController::makeMaindataSnapshot()
516 m_knownTrackers
.clear();
517 m_maindataAcceptedID
= 0;
518 m_maindataSnapshot
= {};
520 const auto *session
= BitTorrent::Session::instance();
522 for (const BitTorrent::Torrent
*torrent
: asConst(session
->torrents()))
524 const BitTorrent::TorrentID torrentID
= torrent
->id();
526 QVariantMap serializedTorrent
= serialize(*torrent
);
527 serializedTorrent
.remove(KEY_TORRENT_ID
);
529 for (const BitTorrent::TrackerEntryStatus
&status
: asConst(torrent
->trackers()))
530 m_knownTrackers
[status
.url
].insert(torrentID
);
532 m_maindataSnapshot
.torrents
[torrentID
.toString()] = serializedTorrent
;
535 const QStringList categoriesList
= session
->categories();
536 for (const auto &categoryName
: categoriesList
)
538 const BitTorrent::CategoryOptions categoryOptions
= session
->categoryOptions(categoryName
);
539 QJsonObject category
= categoryOptions
.toJSON();
540 // adjust it to be compatible with existing WebAPI
541 category
[u
"savePath"_s
] = category
.take(u
"save_path"_s
);
542 category
.insert(u
"name"_s
, categoryName
);
543 m_maindataSnapshot
.categories
[categoryName
] = category
.toVariantMap();
546 for (const Tag
&tag
: asConst(session
->tags()))
547 m_maindataSnapshot
.tags
.append(tag
.toString());
549 for (auto trackersIter
= m_knownTrackers
.cbegin(); trackersIter
!= m_knownTrackers
.cend(); ++trackersIter
)
551 QStringList torrentIDs
;
552 for (const BitTorrent::TorrentID
&torrentID
: asConst(trackersIter
.value()))
553 torrentIDs
.append(torrentID
.toString());
555 m_maindataSnapshot
.trackers
[trackersIter
.key()] = torrentIDs
;
558 m_maindataSnapshot
.serverState
= getTransferInfo();
559 m_maindataSnapshot
.serverState
[KEY_TRANSFER_FREESPACEONDISK
] = m_freeDiskSpace
;
560 m_maindataSnapshot
.serverState
[KEY_SYNC_MAINDATA_QUEUEING
] = session
->isQueueingSystemEnabled();
561 m_maindataSnapshot
.serverState
[KEY_SYNC_MAINDATA_USE_ALT_SPEED_LIMITS
] = session
->isAltGlobalSpeedLimitEnabled();
562 m_maindataSnapshot
.serverState
[KEY_SYNC_MAINDATA_REFRESH_INTERVAL
] = session
->refreshInterval();
563 m_maindataSnapshot
.serverState
[KEY_SYNC_MAINDATA_USE_SUBCATEGORIES
] = session
->isSubcategoriesEnabled();
566 QJsonObject
SyncController::generateMaindataSyncData(const int id
, const bool fullUpdate
)
568 // if need to update existing sync data
569 for (const QString
&category
: asConst(m_updatedCategories
))
570 m_maindataSyncBuf
.removedCategories
.removeOne(category
);
571 for (const QString
&category
: asConst(m_removedCategories
))
572 m_maindataSyncBuf
.categories
.remove(category
);
574 for (const QString
&tag
: asConst(m_addedTags
))
575 m_maindataSyncBuf
.removedTags
.removeOne(tag
);
576 for (const QString
&tag
: asConst(m_removedTags
))
577 m_maindataSyncBuf
.tags
.removeOne(tag
);
579 for (const BitTorrent::TorrentID
&torrentID
: asConst(m_updatedTorrents
))
580 m_maindataSyncBuf
.removedTorrents
.removeOne(torrentID
.toString());
581 for (const BitTorrent::TorrentID
&torrentID
: asConst(m_removedTorrents
))
582 m_maindataSyncBuf
.torrents
.remove(torrentID
.toString());
584 for (const QString
&tracker
: asConst(m_updatedTrackers
))
585 m_maindataSyncBuf
.removedTrackers
.removeOne(tracker
);
586 for (const QString
&tracker
: asConst(m_removedTrackers
))
587 m_maindataSyncBuf
.trackers
.remove(tracker
);
589 const auto *session
= BitTorrent::Session::instance();
591 for (const QString
&categoryName
: asConst(m_updatedCategories
))
593 const BitTorrent::CategoryOptions categoryOptions
= session
->categoryOptions(categoryName
);
594 auto category
= categoryOptions
.toJSON().toVariantMap();
595 // adjust it to be compatible with existing WebAPI
596 category
[u
"savePath"_s
] = category
.take(u
"save_path"_s
);
597 category
.insert(u
"name"_s
, categoryName
);
599 auto &categorySnapshot
= m_maindataSnapshot
.categories
[categoryName
];
600 if (const QVariantMap syncData
= processMap(categorySnapshot
, category
); !syncData
.isEmpty())
602 m_maindataSyncBuf
.categories
[categoryName
] = syncData
;
603 categorySnapshot
= category
;
606 m_updatedCategories
.clear();
608 for (const QString
&category
: asConst(m_removedCategories
))
610 m_maindataSyncBuf
.removedCategories
.append(category
);
611 m_maindataSnapshot
.categories
.remove(category
);
613 m_removedCategories
.clear();
615 for (const QString
&tag
: asConst(m_addedTags
))
617 m_maindataSyncBuf
.tags
.append(tag
);
618 m_maindataSnapshot
.tags
.append(tag
);
622 for (const QString
&tag
: asConst(m_removedTags
))
624 m_maindataSyncBuf
.removedTags
.append(tag
);
625 m_maindataSnapshot
.tags
.removeOne(tag
);
627 m_removedTags
.clear();
629 for (const BitTorrent::TorrentID
&torrentID
: asConst(m_updatedTorrents
))
631 const BitTorrent::Torrent
*torrent
= session
->getTorrent(torrentID
);
634 QVariantMap serializedTorrent
= serialize(*torrent
);
635 serializedTorrent
.remove(KEY_TORRENT_ID
);
637 auto &torrentSnapshot
= m_maindataSnapshot
.torrents
[torrentID
.toString()];
639 if (const QVariantMap syncData
= processMap(torrentSnapshot
, serializedTorrent
); !syncData
.isEmpty())
641 m_maindataSyncBuf
.torrents
[torrentID
.toString()] = syncData
;
642 torrentSnapshot
= serializedTorrent
;
645 m_updatedTorrents
.clear();
647 for (const BitTorrent::TorrentID
&torrentID
: asConst(m_removedTorrents
))
649 m_maindataSyncBuf
.removedTorrents
.append(torrentID
.toString());
650 m_maindataSnapshot
.torrents
.remove(torrentID
.toString());
652 m_removedTorrents
.clear();
654 for (const QString
&tracker
: asConst(m_updatedTrackers
))
656 const QSet
<BitTorrent::TorrentID
> torrentIDs
= m_knownTrackers
[tracker
];
657 QStringList serializedTorrentIDs
;
658 serializedTorrentIDs
.reserve(torrentIDs
.size());
659 for (const BitTorrent::TorrentID
&torrentID
: torrentIDs
)
660 serializedTorrentIDs
.append(torrentID
.toString());
662 m_maindataSyncBuf
.trackers
[tracker
] = serializedTorrentIDs
;
663 m_maindataSnapshot
.trackers
[tracker
] = serializedTorrentIDs
;
665 m_updatedTrackers
.clear();
667 for (const QString
&tracker
: asConst(m_removedTrackers
))
669 m_maindataSyncBuf
.removedTrackers
.append(tracker
);
670 m_maindataSnapshot
.trackers
.remove(tracker
);
672 m_removedTrackers
.clear();
674 QVariantMap serverState
= getTransferInfo();
675 serverState
[KEY_TRANSFER_FREESPACEONDISK
] = m_freeDiskSpace
;
676 serverState
[KEY_SYNC_MAINDATA_QUEUEING
] = session
->isQueueingSystemEnabled();
677 serverState
[KEY_SYNC_MAINDATA_USE_ALT_SPEED_LIMITS
] = session
->isAltGlobalSpeedLimitEnabled();
678 serverState
[KEY_SYNC_MAINDATA_REFRESH_INTERVAL
] = session
->refreshInterval();
679 serverState
[KEY_SYNC_MAINDATA_USE_SUBCATEGORIES
] = session
->isSubcategoriesEnabled();
680 if (const QVariantMap syncData
= processMap(m_maindataSnapshot
.serverState
, serverState
); !syncData
.isEmpty())
682 m_maindataSyncBuf
.serverState
= syncData
;
683 m_maindataSnapshot
.serverState
= serverState
;
686 QJsonObject syncData
;
687 syncData
[KEY_RESPONSE_ID
] = id
;
690 m_maindataSyncBuf
= m_maindataSnapshot
;
691 syncData
[KEY_FULL_UPDATE
] = true;
694 if (!m_maindataSyncBuf
.categories
.isEmpty())
696 QJsonObject categories
;
697 for (auto it
= m_maindataSyncBuf
.categories
.cbegin(); it
!= m_maindataSyncBuf
.categories
.cend(); ++it
)
698 categories
[it
.key()] = QJsonObject::fromVariantMap(it
.value());
699 syncData
[KEY_CATEGORIES
] = categories
;
701 if (!m_maindataSyncBuf
.removedCategories
.isEmpty())
702 syncData
[KEY_CATEGORIES_REMOVED
] = QJsonArray::fromStringList(m_maindataSyncBuf
.removedCategories
);
704 if (!m_maindataSyncBuf
.tags
.isEmpty())
705 syncData
[KEY_TAGS
] = QJsonArray::fromVariantList(m_maindataSyncBuf
.tags
);
706 if (!m_maindataSyncBuf
.removedTags
.isEmpty())
707 syncData
[KEY_TAGS_REMOVED
] = QJsonArray::fromStringList(m_maindataSyncBuf
.removedTags
);
709 if (!m_maindataSyncBuf
.torrents
.isEmpty())
711 QJsonObject torrents
;
712 for (auto it
= m_maindataSyncBuf
.torrents
.cbegin(); it
!= m_maindataSyncBuf
.torrents
.cend(); ++it
)
713 torrents
[it
.key()] = QJsonObject::fromVariantMap(it
.value());
714 syncData
[KEY_TORRENTS
] = torrents
;
716 if (!m_maindataSyncBuf
.removedTorrents
.isEmpty())
717 syncData
[KEY_TORRENTS_REMOVED
] = QJsonArray::fromStringList(m_maindataSyncBuf
.removedTorrents
);
719 if (!m_maindataSyncBuf
.trackers
.isEmpty())
721 QJsonObject trackers
;
722 for (auto it
= m_maindataSyncBuf
.trackers
.cbegin(); it
!= m_maindataSyncBuf
.trackers
.cend(); ++it
)
723 trackers
[it
.key()] = QJsonArray::fromStringList(it
.value());
724 syncData
[KEY_TRACKERS
] = trackers
;
726 if (!m_maindataSyncBuf
.removedTrackers
.isEmpty())
727 syncData
[KEY_TRACKERS_REMOVED
] = QJsonArray::fromStringList(m_maindataSyncBuf
.removedTrackers
);
729 if (!m_maindataSyncBuf
.serverState
.isEmpty())
730 syncData
[KEY_SERVER_STATE
] = QJsonObject::fromVariantMap(m_maindataSyncBuf
.serverState
);
736 // - hash (string): torrent hash (ID)
737 // - rid (int): last response id
738 void SyncController::torrentPeersAction()
740 const auto id
= BitTorrent::TorrentID::fromString(params()[u
"hash"_s
]);
741 const BitTorrent::Torrent
*torrent
= BitTorrent::Session::instance()->getTorrent(id
);
743 throw APIError(APIErrorType::NotFound
);
748 const QList
<BitTorrent::PeerInfo
> peersList
= torrent
->peers();
750 bool resolvePeerCountries
= Preferences::instance()->resolvePeerCountries();
752 data
[KEY_SYNC_TORRENT_PEERS_SHOW_FLAGS
] = resolvePeerCountries
;
754 for (const BitTorrent::PeerInfo
&pi
: peersList
)
756 if (pi
.address().ip
.isNull()) continue;
760 {KEY_PEER_IP
, pi
.address().ip
.toString()},
761 {KEY_PEER_PORT
, pi
.address().port
},
762 {KEY_PEER_CLIENT
, pi
.client()},
763 {KEY_PEER_ID_CLIENT
, pi
.peerIdClient()},
764 {KEY_PEER_PROGRESS
, pi
.progress()},
765 {KEY_PEER_DOWN_SPEED
, pi
.payloadDownSpeed()},
766 {KEY_PEER_UP_SPEED
, pi
.payloadUpSpeed()},
767 {KEY_PEER_TOT_DOWN
, pi
.totalDownload()},
768 {KEY_PEER_TOT_UP
, pi
.totalUpload()},
769 {KEY_PEER_CONNECTION_TYPE
, pi
.connectionType()},
770 {KEY_PEER_FLAGS
, pi
.flags()},
771 {KEY_PEER_FLAGS_DESCRIPTION
, pi
.flagsDescription()},
772 {KEY_PEER_RELEVANCE
, pi
.relevance()}
775 if (torrent
->hasMetadata())
777 const PathList filePaths
= torrent
->info().filesForPiece(pi
.downloadingPieceIndex());
778 QStringList filesForPiece
;
779 filesForPiece
.reserve(filePaths
.size());
780 for (const Path
&filePath
: filePaths
)
781 filesForPiece
.append(filePath
.toString());
782 peer
.insert(KEY_PEER_FILES
, filesForPiece
.join(u
'\n'));
785 if (resolvePeerCountries
)
787 peer
[KEY_PEER_COUNTRY_CODE
] = pi
.country().toLower();
788 peer
[KEY_PEER_COUNTRY
] = Net::GeoIPManager::CountryName(pi
.country());
791 peers
[pi
.address().toString()] = peer
;
793 data
[u
"peers"_s
] = peers
;
795 const int acceptedResponseId
= params()[u
"rid"_s
].toInt();
796 setResult(generateSyncData(acceptedResponseId
, data
, m_lastAcceptedPeersResponse
, m_lastPeersResponse
));
799 void SyncController::onCategoryAdded(const QString
&categoryName
)
801 m_removedCategories
.remove(categoryName
);
802 m_updatedCategories
.insert(categoryName
);
805 void SyncController::onCategoryRemoved(const QString
&categoryName
)
807 m_updatedCategories
.remove(categoryName
);
808 m_removedCategories
.insert(categoryName
);
811 void SyncController::onCategoryOptionsChanged(const QString
&categoryName
)
813 Q_ASSERT(!m_removedCategories
.contains(categoryName
));
815 m_updatedCategories
.insert(categoryName
);
818 void SyncController::onSubcategoriesSupportChanged()
820 const QStringList categoriesList
= BitTorrent::Session::instance()->categories();
821 for (const auto &categoryName
: categoriesList
)
823 if (!m_maindataSnapshot
.categories
.contains(categoryName
))
825 m_removedCategories
.remove(categoryName
);
826 m_updatedCategories
.insert(categoryName
);
831 void SyncController::onTagAdded(const Tag
&tag
)
833 m_removedTags
.remove(tag
.toString());
834 m_addedTags
.insert(tag
.toString());
837 void SyncController::onTagRemoved(const Tag
&tag
)
839 m_addedTags
.remove(tag
.toString());
840 m_removedTags
.insert(tag
.toString());
843 void SyncController::onTorrentAdded(BitTorrent::Torrent
*torrent
)
845 const BitTorrent::TorrentID torrentID
= torrent
->id();
847 m_removedTorrents
.remove(torrentID
);
848 m_updatedTorrents
.insert(torrentID
);
850 for (const BitTorrent::TrackerEntryStatus
&status
: asConst(torrent
->trackers()))
852 m_knownTrackers
[status
.url
].insert(torrentID
);
853 m_updatedTrackers
.insert(status
.url
);
854 m_removedTrackers
.remove(status
.url
);
858 void SyncController::onTorrentAboutToBeRemoved(BitTorrent::Torrent
*torrent
)
860 const BitTorrent::TorrentID torrentID
= torrent
->id();
862 m_updatedTorrents
.remove(torrentID
);
863 m_removedTorrents
.insert(torrentID
);
865 for (const BitTorrent::TrackerEntryStatus
&status
: asConst(torrent
->trackers()))
867 auto iter
= m_knownTrackers
.find(status
.url
);
868 Q_ASSERT(iter
!= m_knownTrackers
.end());
869 if (iter
== m_knownTrackers
.end()) [[unlikely
]]
872 QSet
<BitTorrent::TorrentID
> &torrentIDs
= iter
.value();
873 torrentIDs
.remove(torrentID
);
874 if (torrentIDs
.isEmpty())
876 m_knownTrackers
.erase(iter
);
877 m_updatedTrackers
.remove(status
.url
);
878 m_removedTrackers
.insert(status
.url
);
882 m_updatedTrackers
.insert(status
.url
);
887 void SyncController::onTorrentCategoryChanged(BitTorrent::Torrent
*torrent
888 , [[maybe_unused
]] const QString
&oldCategory
)
890 m_updatedTorrents
.insert(torrent
->id());
893 void SyncController::onTorrentMetadataReceived(BitTorrent::Torrent
*torrent
)
895 m_updatedTorrents
.insert(torrent
->id());
898 void SyncController::onTorrentStopped(BitTorrent::Torrent
*torrent
)
900 m_updatedTorrents
.insert(torrent
->id());
903 void SyncController::onTorrentStarted(BitTorrent::Torrent
*torrent
)
905 m_updatedTorrents
.insert(torrent
->id());
908 void SyncController::onTorrentSavePathChanged(BitTorrent::Torrent
*torrent
)
910 m_updatedTorrents
.insert(torrent
->id());
913 void SyncController::onTorrentSavingModeChanged(BitTorrent::Torrent
*torrent
)
915 m_updatedTorrents
.insert(torrent
->id());
918 void SyncController::onTorrentTagAdded(BitTorrent::Torrent
*torrent
, [[maybe_unused
]] const Tag
&tag
)
920 m_updatedTorrents
.insert(torrent
->id());
923 void SyncController::onTorrentTagRemoved(BitTorrent::Torrent
*torrent
, [[maybe_unused
]] const Tag
&tag
)
925 m_updatedTorrents
.insert(torrent
->id());
928 void SyncController::onTorrentsUpdated(const QList
<BitTorrent::Torrent
*> &torrents
)
930 for (const BitTorrent::Torrent
*torrent
: torrents
)
931 m_updatedTorrents
.insert(torrent
->id());
934 void SyncController::onTorrentTrackersChanged(BitTorrent::Torrent
*torrent
)
936 using namespace BitTorrent
;
938 const QList
<TrackerEntryStatus
> trackers
= torrent
->trackers();
940 QSet
<QString
> currentTrackers
;
941 currentTrackers
.reserve(trackers
.size());
942 for (const TrackerEntryStatus
&status
: trackers
)
943 currentTrackers
.insert(status
.url
);
945 const TorrentID torrentID
= torrent
->id();
946 Algorithm::removeIf(m_knownTrackers
947 , [this, torrentID
, currentTrackers
](const QString
&knownTracker
, QSet
<TorrentID
> &torrentIDs
)
949 if (auto idIter
= torrentIDs
.find(torrentID
)
950 ; (idIter
!= torrentIDs
.end()) && !currentTrackers
.contains(knownTracker
))
952 torrentIDs
.erase(idIter
);
953 if (torrentIDs
.isEmpty())
955 m_updatedTrackers
.remove(knownTracker
);
956 m_removedTrackers
.insert(knownTracker
);
960 m_updatedTrackers
.insert(knownTracker
);
964 if (currentTrackers
.contains(knownTracker
) && !torrentIDs
.contains(torrentID
))
966 torrentIDs
.insert(torrentID
);
967 m_updatedTrackers
.insert(knownTracker
);
974 for (const QString
¤tTracker
: asConst(currentTrackers
))
976 if (!m_knownTrackers
.contains(currentTracker
))
978 m_knownTrackers
.insert(currentTracker
, {torrentID
});
979 m_updatedTrackers
.insert(currentTracker
);
980 m_removedTrackers
.remove(currentTracker
);