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"
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"
53 #include "freediskspacechecker.h"
54 #include "serialize/serialize_torrent.h"
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
;
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
;
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
;
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()
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)
160 map
[KEY_TRANSFER_READ_CACHE_OVERLOAD
] = ((sessionStatus
.diskReadQueue
> 0) && (sessionStatus
.peersCount
> 0))
161 ? Utils::String::fromDouble((100. * sessionStatus
.diskReadQueue
/ sessionStatus
.peersCount
), 2)
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
)
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
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
:
194 processMap(prevData
[key
].toMap(), value
.toMap(), map
);
199 case QMetaType::QVariantHash
:
202 processHash(prevData
[key
].toHash(), value
.toHash(), map
, removedItems
);
205 if (!removedItems
.isEmpty())
206 syncData
[key
+ KEY_SUFFIX_REMOVED
] = removedItems
;
209 case QMetaType::QVariantList
:
212 processList(prevData
[key
].toList(), value
.toList(), list
, removedItems
);
214 syncData
[key
] = list
;
215 if (!removedItems
.isEmpty())
216 syncData
[key
+ KEY_SUFFIX_REMOVED
] = removedItems
;
219 case QMetaType::QString
:
220 case QMetaType::LongLong
:
221 case QMetaType::Float
:
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
;
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
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();
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();
271 processMap(prevData
[i
.key()].toMap(), i
.value().toMap(), map
);
272 // existing list item found - remove it from prevData
273 prevData
.remove(i
.key());
276 // changed list item found - append its changes to syncData
277 syncData
[i
.key()] = map
;
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();
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();
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
322 removedItems
.clear();
324 if (prevData
.isEmpty())
326 // If list was empty before, then difference is a whole new list.
331 for (const QVariant
&item
: data
)
333 if (!prevData
.contains(item
))
335 // new list item found - append it to syncData
336 syncData
.append(item
);
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
)
373 lastAcceptedData
.clear();
375 syncData
[KEY_FULL_UPDATE
] = true;
379 processMap(lastAcceptedData
, data
, syncData
);
382 const int responseId
= (lastResponseId
% 1000000) + 1; // cycle between 1 and 1000000
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
)
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
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.
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
);
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
);
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
;
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
);
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
);
731 throw APIError(APIErrorType::NotFound
);
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;
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
))
792 return m_freeDiskSpace
;
795 void SyncController::invokeChecker()
797 if (m_isFreeDiskSpaceCheckerRunning
)
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
]]
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
);
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
¤tTrackerEntry
: 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
);
975 m_updatedTrackers
.insert(knownTracker
);
979 if (currentTrackers
.contains(knownTracker
) && !torrentIDs
.contains(torrentID
))
981 torrentIDs
.insert(torrentID
);
982 m_updatedTrackers
.insert(knownTracker
);
989 for (const QString
¤tTracker
: asConst(currentTrackers
))
991 if (!m_knownTrackers
.contains(currentTracker
))
993 m_knownTrackers
.insert(currentTracker
, {torrentID
});
994 m_updatedTrackers
.insert(currentTracker
);
995 m_removedTrackers
.remove(currentTracker
);