diff --git a/src/base/bittorrent/session.h b/src/base/bittorrent/session.h index 07346ca08..777eaae15 100644 --- a/src/base/bittorrent/session.h +++ b/src/base/bittorrent/session.h @@ -433,6 +433,7 @@ namespace BitTorrent void allTorrentsFinished(); void categoryAdded(const QString &categoryName); void categoryRemoved(const QString &categoryName); + void categoryOptionsChanged(const QString &categoryName); void downloadFromUrlFailed(const QString &url, const QString &reason); void downloadFromUrlFinished(const QString &url); void fullDiskError(Torrent *torrent, const QString &msg); diff --git a/src/base/bittorrent/sessionimpl.cpp b/src/base/bittorrent/sessionimpl.cpp index 1a068f761..50caa36c4 100644 --- a/src/base/bittorrent/sessionimpl.cpp +++ b/src/base/bittorrent/sessionimpl.cpp @@ -865,6 +865,7 @@ bool SessionImpl::editCategory(const QString &name, const CategoryOptions &optio } } + emit categoryOptionsChanged(name); return true; } diff --git a/src/webui/api/synccontroller.cpp b/src/webui/api/synccontroller.cpp index 1473d4cd6..d11ee8a93 100644 --- a/src/webui/api/synccontroller.cpp +++ b/src/webui/api/synccontroller.cpp @@ -1,6 +1,6 @@ /* * Bittorrent Client using Qt and libtorrent. - * Copyright (C) 2018 Vladimir Golovnev + * Copyright (C) 2018-2023 Vladimir Golovnev * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License @@ -30,10 +30,12 @@ #include +#include #include #include #include +#include "base/algorithm.h" #include "base/bittorrent/cachestatus.h" #include "base/bittorrent/infohash.h" #include "base/bittorrent/peeraddress.h" @@ -106,14 +108,24 @@ namespace const QString KEY_TRANSFER_TOTAL_WASTE_SESSION = u"total_wasted_session"_qs; const QString KEY_TRANSFER_WRITE_CACHE_OVERLOAD = u"write_cache_overload"_qs; + const QString KEY_SUFFIX_REMOVED = u"_removed"_qs; + + const QString KEY_CATEGORIES = u"categories"_qs; + const QString KEY_CATEGORIES_REMOVED = KEY_CATEGORIES + KEY_SUFFIX_REMOVED; + const QString KEY_TAGS = u"tags"_qs; + const QString KEY_TAGS_REMOVED = KEY_TAGS + KEY_SUFFIX_REMOVED; + const QString KEY_TORRENTS = u"torrents"_qs; + const QString KEY_TORRENTS_REMOVED = KEY_TORRENTS + KEY_SUFFIX_REMOVED; + const QString KEY_TRACKERS = u"trackers"_qs; + const QString KEY_TRACKERS_REMOVED = KEY_TRACKERS + KEY_SUFFIX_REMOVED; + const QString KEY_SERVER_STATE = u"server_state"_qs; const QString KEY_FULL_UPDATE = u"full_update"_qs; const QString KEY_RESPONSE_ID = u"rid"_qs; - const QString KEY_SUFFIX_REMOVED = u"_removed"_qs; void processMap(const QVariantMap &prevData, const QVariantMap &data, QVariantMap &syncData); void processHash(QVariantHash prevData, const QVariantHash &data, QVariantMap &syncData, QVariantList &removedItems); void processList(QVariantList prevData, const QVariantList &data, QVariantList &syncData, QVariantList &removedItems); - QVariantMap generateSyncData(int acceptedResponseId, const QVariantMap &data, QVariantMap &lastAcceptedData, QVariantMap &lastData); + QJsonObject generateSyncData(int acceptedResponseId, const QVariantMap &data, QVariantMap &lastAcceptedData, QVariantMap &lastData); QVariantMap getTransferInfo() { @@ -332,23 +344,19 @@ namespace } } - QVariantMap generateSyncData(int acceptedResponseId, const QVariantMap &data, QVariantMap &lastAcceptedData, QVariantMap &lastData) + QJsonObject generateSyncData(int acceptedResponseId, const QVariantMap &data, QVariantMap &lastAcceptedData, QVariantMap &lastData) { QVariantMap syncData; bool fullUpdate = true; - int lastResponseId = 0; - if (acceptedResponseId > 0) + const int lastResponseId = (acceptedResponseId > 0) ? lastData[KEY_RESPONSE_ID].toInt() : 0; + if (lastResponseId > 0) { - lastResponseId = lastData[KEY_RESPONSE_ID].toInt(); - if (lastResponseId == acceptedResponseId) lastAcceptedData = lastData; - int lastAcceptedResponseId = lastAcceptedData[KEY_RESPONSE_ID].toInt(); - - if (lastAcceptedResponseId == acceptedResponseId) + if (const int lastAcceptedResponseId = lastAcceptedData[KEY_RESPONSE_ID].toInt() + ; lastAcceptedResponseId == acceptedResponseId) { - processMap(lastAcceptedData, data, syncData); fullUpdate = false; } } @@ -359,13 +367,17 @@ namespace syncData = data; syncData[KEY_FULL_UPDATE] = true; } + else + { + processMap(lastAcceptedData, data, syncData); + } - lastResponseId = (lastResponseId % 1000000) + 1; // cycle between 1 and 1000000 + const int responseId = (lastResponseId % 1000000) + 1; // cycle between 1 and 1000000 lastData = data; - lastData[KEY_RESPONSE_ID] = lastResponseId; - syncData[KEY_RESPONSE_ID] = lastResponseId; + lastData[KEY_RESPONSE_ID] = responseId; + syncData[KEY_RESPONSE_ID] = responseId; - return syncData; + return QJsonObject::fromVariantMap(syncData); } } @@ -441,49 +453,74 @@ SyncController::SyncController(IApplication *app, QObject *parent) // - rid (int): last response id void SyncController::maindataAction() { - const auto *session = BitTorrent::Session::instance(); - - QVariantMap data; - - QVariantHash torrents; - QHash trackers; - for (const BitTorrent::Torrent *torrent : asConst(session->torrents())) + if (m_maindataAcceptedID < 0) { - const BitTorrent::TorrentID torrentID = torrent->id(); + makeMaindataSnapshot(); + + const auto *btSession = BitTorrent::Session::instance(); + connect(btSession, &BitTorrent::Session::categoryAdded, this, &SyncController::onCategoryAdded); + connect(btSession, &BitTorrent::Session::categoryRemoved, this, &SyncController::onCategoryRemoved); + connect(btSession, &BitTorrent::Session::categoryOptionsChanged, this, &SyncController::onCategoryOptionsChanged); + connect(btSession, &BitTorrent::Session::subcategoriesSupportChanged, this, &SyncController::onSubcategoriesSupportChanged); + connect(btSession, &BitTorrent::Session::tagAdded, this, &SyncController::onTagAdded); + connect(btSession, &BitTorrent::Session::tagRemoved, this, &SyncController::onTagRemoved); + connect(btSession, &BitTorrent::Session::torrentAdded, this, &SyncController::onTorrentAdded); + connect(btSession, &BitTorrent::Session::torrentAboutToBeRemoved, this, &SyncController::onTorrentAboutToBeRemoved); + connect(btSession, &BitTorrent::Session::torrentCategoryChanged, this, &SyncController::onTorrentCategoryChanged); + connect(btSession, &BitTorrent::Session::torrentMetadataReceived, this, &SyncController::onTorrentMetadataReceived); + connect(btSession, &BitTorrent::Session::torrentPaused, this, &SyncController::onTorrentPaused); + connect(btSession, &BitTorrent::Session::torrentResumed, this, &SyncController::onTorrentResumed); + connect(btSession, &BitTorrent::Session::torrentSavePathChanged, this, &SyncController::onTorrentSavePathChanged); + connect(btSession, &BitTorrent::Session::torrentSavingModeChanged, this, &SyncController::onTorrentSavingModeChanged); + connect(btSession, &BitTorrent::Session::torrentTagAdded, this, &SyncController::onTorrentTagAdded); + connect(btSession, &BitTorrent::Session::torrentTagRemoved, this, &SyncController::onTorrentTagRemoved); + connect(btSession, &BitTorrent::Session::torrentsUpdated, this, &SyncController::onTorrentsUpdated); + connect(btSession, &BitTorrent::Session::trackersChanged, this, &SyncController::onTorrentTrackersChanged); + } - QVariantMap map = serialize(*torrent); - map.remove(KEY_TORRENT_ID); + const int acceptedID = params()[u"rid"_qs].toInt(); + bool fullUpdate = true; + if ((acceptedID > 0) && (m_maindataLastSentID > 0)) + { + if (m_maindataLastSentID == acceptedID) + { + m_maindataAcceptedID = acceptedID; + m_maindataSyncBuf = {}; + } - // Calculated last activity time can differ from actual value by up to 10 seconds (this is a libtorrent issue). - // So we don't need unnecessary updates of last activity time in response. - const auto iterTorrents = m_lastMaindataResponse.find(u"torrents"_qs); - if (iterTorrents != m_lastMaindataResponse.end()) + if (m_maindataAcceptedID == acceptedID) { - const QVariantHash lastResponseTorrents = iterTorrents->toHash(); - const auto iterID = lastResponseTorrents.find(torrentID.toString()); + // We are still able to send changes for the current state of the data having by client. + fullUpdate = false; + } + } - if (iterID != lastResponseTorrents.end()) - { - const QVariantMap torrentData = iterID->toMap(); - const auto iterLastActivity = torrentData.find(KEY_TORRENT_LAST_ACTIVITY_TIME); + const int id = (m_maindataLastSentID % 1000000) + 1; // cycle between 1 and 1000000 + setResult(generateMaindataSyncData(id, fullUpdate)); + m_maindataLastSentID = id; +} - if (iterLastActivity != torrentData.end()) - { - const int lastValue = iterLastActivity->toInt(); - if (qAbs(lastValue - map[KEY_TORRENT_LAST_ACTIVITY_TIME].toInt()) < 15) - map[KEY_TORRENT_LAST_ACTIVITY_TIME] = lastValue; - } - } - } +void SyncController::makeMaindataSnapshot() +{ + m_knownTrackers.clear(); + m_maindataAcceptedID = 0; + m_maindataSnapshot = {}; + + const auto *session = BitTorrent::Session::instance(); + + for (const BitTorrent::Torrent *torrent : asConst(session->torrents())) + { + const BitTorrent::TorrentID torrentID = torrent->id(); + + QVariantMap serializedTorrent = serialize(*torrent); + serializedTorrent.remove(KEY_TORRENT_ID); for (const BitTorrent::TrackerEntry &tracker : asConst(torrent->trackers())) - trackers[tracker.url] << torrentID.toString(); + m_knownTrackers[tracker.url].insert(torrentID); - torrents[torrentID.toString()] = map; + m_maindataSnapshot.torrents[torrentID.toString()] = serializedTorrent; } - data[u"torrents"_qs] = torrents; - QVariantHash categories; const QStringList categoriesList = session->categories(); for (const auto &categoryName : categoriesList) { @@ -492,31 +529,184 @@ void SyncController::maindataAction() // adjust it to be compatible with existing WebAPI category[u"savePath"_qs] = category.take(u"save_path"_qs); category.insert(u"name"_qs, categoryName); - categories[categoryName] = category.toVariantMap(); + m_maindataSnapshot.categories[categoryName] = category.toVariantMap(); } - data[u"categories"_qs] = categories; - QVariantList tags; for (const QString &tag : asConst(session->tags())) - tags << tag; - data[u"tags"_qs] = tags; + m_maindataSnapshot.tags.append(tag); + + for (auto trackersIter = m_knownTrackers.cbegin(); trackersIter != m_knownTrackers.cend(); ++trackersIter) + { + QStringList torrentIDs; + for (const BitTorrent::TorrentID &torrentID : asConst(trackersIter.value())) + torrentIDs.append(torrentID.toString()); + + m_maindataSnapshot.trackers[trackersIter.key()] = torrentIDs; + } + + m_maindataSnapshot.serverState = getTransferInfo(); + m_maindataSnapshot.serverState[KEY_TRANSFER_FREESPACEONDISK] = getFreeDiskSpace(); + m_maindataSnapshot.serverState[KEY_SYNC_MAINDATA_QUEUEING] = session->isQueueingSystemEnabled(); + m_maindataSnapshot.serverState[KEY_SYNC_MAINDATA_USE_ALT_SPEED_LIMITS] = session->isAltGlobalSpeedLimitEnabled(); + m_maindataSnapshot.serverState[KEY_SYNC_MAINDATA_REFRESH_INTERVAL] = session->refreshInterval(); +} + +QJsonObject SyncController::generateMaindataSyncData(const int id, const bool fullUpdate) +{ + // if need to update existing sync data + for (const QString &category : asConst(m_updatedCategories)) + m_maindataSyncBuf.removedCategories.removeOne(category); + for (const QString &category : asConst(m_removedCategories)) + m_maindataSyncBuf.categories.remove(category); + + for (const QString &tag : asConst(m_addedTags)) + m_maindataSyncBuf.removedTags.removeOne(tag); + for (const QString &tag : asConst(m_removedTags)) + m_maindataSyncBuf.tags.removeOne(tag); + + for (const BitTorrent::TorrentID &torrentID : asConst(m_updatedTorrents)) + m_maindataSyncBuf.removedTorrents.removeOne(torrentID.toString()); + for (const BitTorrent::TorrentID &torrentID : asConst(m_removedTorrents)) + m_maindataSyncBuf.torrents.remove(torrentID.toString()); + + for (const QString &tracker : asConst(m_updatedTrackers)) + m_maindataSyncBuf.removedTrackers.removeOne(tracker); + for (const QString &tracker : asConst(m_removedTrackers)) + m_maindataSyncBuf.trackers.remove(tracker); + + const auto *session = BitTorrent::Session::instance(); - QVariantHash trackersHash; - for (auto i = trackers.constBegin(); i != trackers.constEnd(); ++i) + for (const QString &categoryName : asConst(m_updatedCategories)) { - trackersHash[i.key()] = i.value(); + const BitTorrent::CategoryOptions categoryOptions = session->categoryOptions(categoryName); + auto category = categoryOptions.toJSON().toVariantMap(); + // adjust it to be compatible with existing WebAPI + category[u"savePath"_qs] = category.take(u"save_path"_qs); + category.insert(u"name"_qs, categoryName); + + auto &categorySnapshot = m_maindataSnapshot.categories[categoryName]; + processMap(categorySnapshot, category, m_maindataSyncBuf.categories[categoryName]); + categorySnapshot = category; + } + m_updatedCategories.clear(); + + for (const QString &category : asConst(m_removedCategories)) + { + m_maindataSyncBuf.removedCategories.append(category); + m_maindataSnapshot.categories.remove(category); + } + m_removedCategories.clear(); + + for (const QString &tag : asConst(m_addedTags)) + { + m_maindataSyncBuf.tags.append(tag); + m_maindataSnapshot.tags.append(tag); + } + m_addedTags.clear(); + + for (const QString &tag : asConst(m_removedTags)) + { + m_maindataSyncBuf.removedTags.append(tag); + m_maindataSnapshot.tags.removeOne(tag); + } + m_removedTags.clear(); + + for (const BitTorrent::TorrentID &torrentID : asConst(m_updatedTorrents)) + { + const BitTorrent::Torrent *torrent = session->getTorrent(torrentID); + Q_ASSERT(torrent); + + QVariantMap serializedTorrent = serialize(*torrent); + serializedTorrent.remove(KEY_TORRENT_ID); + + auto &torrentSnapshot = m_maindataSnapshot.torrents[torrentID.toString()]; + processMap(torrentSnapshot, serializedTorrent, m_maindataSyncBuf.torrents[torrentID.toString()]); + torrentSnapshot = serializedTorrent; + } + m_updatedTorrents.clear(); + + for (const BitTorrent::TorrentID &torrentID : asConst(m_removedTorrents)) + { + m_maindataSyncBuf.removedTorrents.append(torrentID.toString()); + m_maindataSnapshot.torrents.remove(torrentID.toString()); } - data[u"trackers"_qs] = trackersHash; + m_removedTorrents.clear(); + + for (const QString &tracker : asConst(m_updatedTrackers)) + { + const QSet torrentIDs = m_knownTrackers[tracker]; + QStringList serializedTorrentIDs; + serializedTorrentIDs.reserve(torrentIDs.size()); + for (const BitTorrent::TorrentID &torrentID : torrentIDs) + serializedTorrentIDs.append(torrentID.toString()); + + m_maindataSyncBuf.trackers[tracker] = serializedTorrentIDs; + m_maindataSnapshot.trackers[tracker] = serializedTorrentIDs; + } + m_updatedTrackers.clear(); + + for (const QString &tracker : asConst(m_removedTrackers)) + { + m_maindataSyncBuf.removedTrackers.append(tracker); + m_maindataSnapshot.trackers.remove(tracker); + } + m_removedTrackers.clear(); QVariantMap serverState = getTransferInfo(); serverState[KEY_TRANSFER_FREESPACEONDISK] = getFreeDiskSpace(); serverState[KEY_SYNC_MAINDATA_QUEUEING] = session->isQueueingSystemEnabled(); serverState[KEY_SYNC_MAINDATA_USE_ALT_SPEED_LIMITS] = session->isAltGlobalSpeedLimitEnabled(); serverState[KEY_SYNC_MAINDATA_REFRESH_INTERVAL] = session->refreshInterval(); - data[u"server_state"_qs] = serverState; + processMap(m_maindataSnapshot.serverState, serverState, m_maindataSyncBuf.serverState); + m_maindataSnapshot.serverState = serverState; - const int acceptedResponseId = params()[u"rid"_qs].toInt(); - setResult(QJsonObject::fromVariantMap(generateSyncData(acceptedResponseId, data, m_lastAcceptedMaindataResponse, m_lastMaindataResponse))); + QJsonObject syncData; + syncData[KEY_RESPONSE_ID] = id; + if (fullUpdate) + { + m_maindataSyncBuf = m_maindataSnapshot; + syncData[KEY_FULL_UPDATE] = true; + } + + if (!m_maindataSyncBuf.categories.isEmpty()) + { + QJsonObject categories; + for (auto it = m_maindataSyncBuf.categories.cbegin(); it != m_maindataSyncBuf.categories.cend(); ++it) + categories[it.key()] = QJsonObject::fromVariantMap(it.value()); + syncData[KEY_CATEGORIES] = categories; + } + if (!m_maindataSyncBuf.removedCategories.isEmpty()) + syncData[KEY_CATEGORIES_REMOVED] = QJsonArray::fromStringList(m_maindataSyncBuf.removedCategories); + + if (!m_maindataSyncBuf.tags.isEmpty()) + syncData[KEY_TAGS] = QJsonArray::fromVariantList(m_maindataSyncBuf.tags); + if (!m_maindataSyncBuf.removedTags.isEmpty()) + syncData[KEY_TAGS_REMOVED] = QJsonArray::fromStringList(m_maindataSyncBuf.removedTags); + + if (!m_maindataSyncBuf.torrents.isEmpty()) + { + QJsonObject torrents; + for (auto it = m_maindataSyncBuf.torrents.cbegin(); it != m_maindataSyncBuf.torrents.cend(); ++it) + torrents[it.key()] = QJsonObject::fromVariantMap(it.value()); + syncData[KEY_TORRENTS] = torrents; + } + if (!m_maindataSyncBuf.removedTorrents.isEmpty()) + syncData[KEY_TORRENTS_REMOVED] = QJsonArray::fromStringList(m_maindataSyncBuf.removedTorrents); + + if (!m_maindataSyncBuf.trackers.isEmpty()) + { + QJsonObject trackers; + for (auto it = m_maindataSyncBuf.trackers.cbegin(); it != m_maindataSyncBuf.trackers.cend(); ++it) + trackers[it.key()] = QJsonArray::fromStringList(it.value()); + syncData[KEY_TRACKERS] = trackers; + } + if (!m_maindataSyncBuf.removedTrackers.isEmpty()) + syncData[KEY_TRACKERS_REMOVED] = QJsonArray::fromStringList(m_maindataSyncBuf.removedTrackers); + + if (!m_maindataSyncBuf.serverState.isEmpty()) + syncData[KEY_SERVER_STATE] = QJsonObject::fromVariantMap(m_maindataSyncBuf.serverState); + + return syncData; } // GET param: @@ -580,7 +770,7 @@ void SyncController::torrentPeersAction() data[u"peers"_qs] = peers; const int acceptedResponseId = params()[u"rid"_qs].toInt(); - setResult(QJsonObject::fromVariantMap(generateSyncData(acceptedResponseId, data, m_lastAcceptedPeersResponse, m_lastPeersResponse))); + setResult(generateSyncData(acceptedResponseId, data, m_lastAcceptedPeersResponse, m_lastPeersResponse)); } qint64 SyncController::getFreeDiskSpace() @@ -610,3 +800,189 @@ void SyncController::invokeChecker() freeDiskSpaceChecker->check(); }); } + +void SyncController::onCategoryAdded(const QString &categoryName) +{ + m_removedCategories.remove(categoryName); + m_updatedCategories.insert(categoryName); +} + +void SyncController::onCategoryRemoved(const QString &categoryName) +{ + m_updatedCategories.remove(categoryName); + m_removedCategories.insert(categoryName); +} + +void SyncController::onCategoryOptionsChanged(const QString &categoryName) +{ + Q_ASSERT(!m_removedCategories.contains(categoryName)); + + m_updatedCategories.insert(categoryName); +} + +void SyncController::onSubcategoriesSupportChanged() +{ + const QStringList categoriesList = BitTorrent::Session::instance()->categories(); + for (const auto &categoryName : categoriesList) + { + if (!m_maindataSnapshot.categories.contains(categoryName)) + { + m_removedCategories.remove(categoryName); + m_updatedCategories.insert(categoryName); + } + } +} + +void SyncController::onTagAdded(const QString &tag) +{ + m_removedTags.remove(tag); + m_addedTags.insert(tag); +} + +void SyncController::onTagRemoved(const QString &tag) +{ + m_addedTags.remove(tag); + m_removedTags.insert(tag); +} + +void SyncController::onTorrentAdded(BitTorrent::Torrent *torrent) +{ + const BitTorrent::TorrentID torrentID = torrent->id(); + + m_removedTorrents.remove(torrentID); + m_updatedTorrents.insert(torrentID); + + for (const BitTorrent::TrackerEntry &trackerEntry : asConst(torrent->trackers())) + { + m_knownTrackers[trackerEntry.url].insert(torrentID); + m_updatedTrackers.insert(trackerEntry.url); + m_removedTrackers.remove(trackerEntry.url); + } +} + +void SyncController::onTorrentAboutToBeRemoved(BitTorrent::Torrent *torrent) +{ + const BitTorrent::TorrentID torrentID = torrent->id(); + + m_updatedTorrents.remove(torrentID); + m_removedTorrents.insert(torrentID); + + for (const BitTorrent::TrackerEntry &trackerEntry : asConst(torrent->trackers())) + { + auto iter = m_knownTrackers.find(trackerEntry.url); + Q_ASSERT(iter != m_knownTrackers.end()); + if (Q_UNLIKELY(iter == m_knownTrackers.end())) + continue; + + QSet &torrentIDs = iter.value(); + torrentIDs.remove(torrentID); + if (torrentIDs.isEmpty()) + { + m_knownTrackers.erase(iter); + m_updatedTrackers.remove(trackerEntry.url); + m_removedTrackers.insert(trackerEntry.url); + } + else + { + m_updatedTrackers.insert(trackerEntry.url); + } + } +} + +void SyncController::onTorrentCategoryChanged(BitTorrent::Torrent *torrent + , [[maybe_unused]] const QString &oldCategory) +{ + m_updatedTorrents.insert(torrent->id()); +} + +void SyncController::onTorrentMetadataReceived(BitTorrent::Torrent *torrent) +{ + m_updatedTorrents.insert(torrent->id()); +} + +void SyncController::onTorrentPaused(BitTorrent::Torrent *torrent) +{ + m_updatedTorrents.insert(torrent->id()); +} + +void SyncController::onTorrentResumed(BitTorrent::Torrent *torrent) +{ + m_updatedTorrents.insert(torrent->id()); +} + +void SyncController::onTorrentSavePathChanged(BitTorrent::Torrent *torrent) +{ + m_updatedTorrents.insert(torrent->id()); +} + +void SyncController::onTorrentSavingModeChanged(BitTorrent::Torrent *torrent) +{ + m_updatedTorrents.insert(torrent->id()); +} + +void SyncController::onTorrentTagAdded(BitTorrent::Torrent *torrent, [[maybe_unused]] const QString &tag) +{ + m_updatedTorrents.insert(torrent->id()); +} + +void SyncController::onTorrentTagRemoved(BitTorrent::Torrent *torrent, [[maybe_unused]] const QString &tag) +{ + m_updatedTorrents.insert(torrent->id()); +} + +void SyncController::onTorrentsUpdated(const QVector &torrents) +{ + for (const BitTorrent::Torrent *torrent : torrents) + m_updatedTorrents.insert(torrent->id()); +} + +void SyncController::onTorrentTrackersChanged(BitTorrent::Torrent *torrent) +{ + using namespace BitTorrent; + + const QVector currentTrackerEntries = torrent->trackers(); + QSet currentTrackers; + currentTrackers.reserve(currentTrackerEntries.size()); + for (const TrackerEntry ¤tTrackerEntry : currentTrackerEntries) + currentTrackers.insert(currentTrackerEntry.url); + + const TorrentID torrentID = torrent->id(); + Algorithm::removeIf(m_knownTrackers + , [this, torrentID, currentTrackers] + (const QString &knownTracker, QSet &torrentIDs) + { + if (auto idIter = torrentIDs.find(torrentID) + ; (idIter != torrentIDs.end()) && !currentTrackers.contains(knownTracker)) + { + torrentIDs.erase(idIter); + if (torrentIDs.isEmpty()) + { + m_updatedTrackers.remove(knownTracker); + m_removedTrackers.insert(knownTracker); + return true; + } + + m_updatedTrackers.insert(knownTracker); + return false; + } + + if (currentTrackers.contains(knownTracker) && !torrentIDs.contains(torrentID)) + { + torrentIDs.insert(torrentID); + m_updatedTrackers.insert(knownTracker); + return false; + } + + return false; + }); + + for (const QString ¤tTracker : asConst(currentTrackers)) + { + if (!m_knownTrackers.contains(currentTracker)) + { + m_knownTrackers.insert(currentTracker, {torrentID}); + m_updatedTrackers.insert(currentTracker); + m_removedTrackers.remove(currentTracker); + } + } +} diff --git a/src/webui/api/synccontroller.h b/src/webui/api/synccontroller.h index 9d9a4014f..a77487f38 100644 --- a/src/webui/api/synccontroller.h +++ b/src/webui/api/synccontroller.h @@ -1,6 +1,6 @@ /* * Bittorrent Client using Qt and libtorrent. - * Copyright (C) 2018 Vladimir Golovnev + * Copyright (C) 2018-2023 Vladimir Golovnev * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License @@ -30,11 +30,18 @@ #include #include +#include +#include "base/bittorrent/infohash.h" #include "apicontroller.h" class QThread; +namespace BitTorrent +{ + class Torrent; +} + class FreeDiskSpaceChecker; class SyncController : public APIController @@ -55,12 +62,62 @@ private: qint64 getFreeDiskSpace(); void invokeChecker(); + void makeMaindataSnapshot(); + QJsonObject generateMaindataSyncData(int id, bool fullUpdate); + + void onCategoryAdded(const QString &categoryName); + void onCategoryRemoved(const QString &categoryName); + void onCategoryOptionsChanged(const QString &categoryName); + void onSubcategoriesSupportChanged(); + void onTagAdded(const QString &tag); + void onTagRemoved(const QString &tag); + void onTorrentAdded(BitTorrent::Torrent *torrent); + void onTorrentAboutToBeRemoved(BitTorrent::Torrent *torrent); + void onTorrentCategoryChanged(BitTorrent::Torrent *torrent, const QString &oldCategory); + void onTorrentMetadataReceived(BitTorrent::Torrent *torrent); + void onTorrentPaused(BitTorrent::Torrent *torrent); + void onTorrentResumed(BitTorrent::Torrent *torrent); + void onTorrentSavePathChanged(BitTorrent::Torrent *torrent); + void onTorrentSavingModeChanged(BitTorrent::Torrent *torrent); + void onTorrentTagAdded(BitTorrent::Torrent *torrent, const QString &tag); + void onTorrentTagRemoved(BitTorrent::Torrent *torrent, const QString &tag); + void onTorrentsUpdated(const QVector &torrents); + void onTorrentTrackersChanged(BitTorrent::Torrent *torrent); + qint64 m_freeDiskSpace = 0; QElapsedTimer m_freeDiskSpaceElapsedTimer; bool m_isFreeDiskSpaceCheckerRunning = false; - QVariantMap m_lastMaindataResponse; - QVariantMap m_lastAcceptedMaindataResponse; QVariantMap m_lastPeersResponse; QVariantMap m_lastAcceptedPeersResponse; + + QHash> m_knownTrackers; + + QSet m_updatedCategories; + QSet m_removedCategories; + QSet m_addedTags; + QSet m_removedTags; + QSet m_updatedTrackers; + QSet m_removedTrackers; + QSet m_updatedTorrents; + QSet m_removedTorrents; + + struct MaindataSyncBuf + { + QHash categories; + QVariantList tags; + QHash torrents; + QHash trackers; + QVariantMap serverState; + + QStringList removedCategories; + QStringList removedTags; + QStringList removedTorrents; + QStringList removedTrackers; + }; + + MaindataSyncBuf m_maindataSnapshot; + MaindataSyncBuf m_maindataSyncBuf; + int m_maindataLastSentID = 0; + int m_maindataAcceptedID = -1; };