diff --git a/src/base/bittorrent/session.h b/src/base/bittorrent/session.h index cae03ba8342d..574d355866ec 100644 --- a/src/base/bittorrent/session.h +++ b/src/base/bittorrent/session.h @@ -379,6 +379,14 @@ namespace BitTorrent virtual void setUploadRateForSlowTorrents(int rateInKibiBytes) = 0; virtual int slowTorrentsInactivityTimer() const = 0; virtual void setSlowTorrentsInactivityTimer(int timeInSeconds) = 0; + virtual bool isSlowTorrentDetectionEnabled() const = 0; + virtual void setSlowTorrentDetectionEnabled(bool enabled) = 0; + virtual int slowTorrentDetectionDuration() const = 0; + virtual void setSlowTorrentDetectionDuration(int minutes) = 0; + virtual int slowTorrentMinimumProgress() const = 0; + virtual void setSlowTorrentMinimumProgress(int megabytes) = 0; + virtual QString slowTorrentExcludedTag() const = 0; + virtual void setSlowTorrentExcludedTag(const QString &tag) = 0; virtual int outgoingPortsMin() const = 0; virtual void setOutgoingPortsMin(int min) = 0; virtual int outgoingPortsMax() const = 0; diff --git a/src/base/bittorrent/sessionimpl.cpp b/src/base/bittorrent/sessionimpl.cpp index 708258e93885..aa273c15f3ba 100644 --- a/src/base/bittorrent/sessionimpl.cpp +++ b/src/base/bittorrent/sessionimpl.cpp @@ -481,6 +481,10 @@ SessionImpl::SessionImpl(QObject *parent) , m_downloadRateForSlowTorrents(BITTORRENT_SESSION_KEY(u"SlowTorrentsDownloadRate"_s), 2) , m_uploadRateForSlowTorrents(BITTORRENT_SESSION_KEY(u"SlowTorrentsUploadRate"_s), 2) , m_slowTorrentsInactivityTimer(BITTORRENT_SESSION_KEY(u"SlowTorrentsInactivityTimer"_s), 60) + , m_isSlowTorrentDetectionEnabled(BITTORRENT_SESSION_KEY(u"SlowTorrentDetectionEnabled"_s), false) + , m_slowTorrentDetectionDuration(BITTORRENT_SESSION_KEY(u"SlowTorrentDetectionDuration"_s), 10, lowerLimited(1)) + , m_slowTorrentMinimumProgress(BITTORRENT_SESSION_KEY(u"SlowTorrentMinimumProgress"_s), 10, lowerLimited(1)) + , m_slowTorrentExcludedTag(BITTORRENT_SESSION_KEY(u"SlowTorrentExcludedTag"_s)) , m_outgoingPortsMin(BITTORRENT_SESSION_KEY(u"OutgoingPortsMin"_s), 0) , m_outgoingPortsMax(BITTORRENT_SESSION_KEY(u"OutgoingPortsMax"_s), 0) , m_UPnPLeaseDuration(BITTORRENT_SESSION_KEY(u"UPnPLeaseDuration"_s), 0) @@ -583,6 +587,7 @@ SessionImpl::SessionImpl(QObject *parent) , m_startPaused {BITTORRENT_SESSION_KEY(u"StartPaused"_s)} , m_seedingLimitTimer {new QTimer(this)} , m_resumeDataTimer {new QTimer(this)} + , m_slowTorrentDetectionTimer {new QTimer(this)} , m_ioThread {new QThread} , m_asyncWorker {new QThreadPool(this)} , m_recentErroredTorrentsTimer {new QTimer(this)} @@ -619,6 +624,9 @@ SessionImpl::SessionImpl(QObject *parent) processTorrentShareLimits(torrent); }); + m_slowTorrentDetectionTimer->setInterval(10s); + connect(m_slowTorrentDetectionTimer, &QTimer::timeout, this, &SessionImpl::processSlowTorrentDetection); + initializeNativeSession(); configureComponents(); @@ -4730,10 +4738,23 @@ void SessionImpl::setQueueingSystemEnabled(const bool enabled) configureDeferred(); if (enabled) + { m_torrentsQueueChanged = true; + + // Start slow torrent detection timer if enabled + if (isSlowTorrentDetectionEnabled() && !m_slowTorrentDetectionTimer->isActive()) + m_slowTorrentDetectionTimer->start(); + } else + { removeTorrentsQueue(); + // Stop slow torrent detection timer when queueing is disabled + if (m_slowTorrentDetectionTimer->isActive()) + m_slowTorrentDetectionTimer->stop(); + m_downloadProgressRecords.clear(); + } + for (TorrentImpl *torrent : asConst(m_torrents)) torrent->handleQueueingModeChanged(); } @@ -4840,6 +4861,71 @@ void SessionImpl::setSlowTorrentsInactivityTimer(const int timeInSeconds) configureDeferred(); } +bool SessionImpl::isSlowTorrentDetectionEnabled() const +{ + return m_isSlowTorrentDetectionEnabled; +} + +void SessionImpl::setSlowTorrentDetectionEnabled(const bool enabled) +{ + if (enabled != m_isSlowTorrentDetectionEnabled) + { + m_isSlowTorrentDetectionEnabled = enabled; + + if (enabled && isQueueingSystemEnabled()) + { + if (!m_slowTorrentDetectionTimer->isActive()) + m_slowTorrentDetectionTimer->start(); + } + else + { + if (m_slowTorrentDetectionTimer->isActive()) + m_slowTorrentDetectionTimer->stop(); + m_downloadProgressRecords.clear(); + } + } +} + +int SessionImpl::slowTorrentDetectionDuration() const +{ + return m_slowTorrentDetectionDuration; +} + +void SessionImpl::setSlowTorrentDetectionDuration(const int minutes) +{ + if (minutes == m_slowTorrentDetectionDuration) + return; + + m_slowTorrentDetectionDuration = minutes; + m_downloadProgressRecords.clear(); +} + +int SessionImpl::slowTorrentMinimumProgress() const +{ + return m_slowTorrentMinimumProgress; +} + +void SessionImpl::setSlowTorrentMinimumProgress(const int megabytes) +{ + if (megabytes == m_slowTorrentMinimumProgress) + return; + + m_slowTorrentMinimumProgress = megabytes; +} + +QString SessionImpl::slowTorrentExcludedTag() const +{ + return m_slowTorrentExcludedTag; +} + +void SessionImpl::setSlowTorrentExcludedTag(const QString &tag) +{ + if (tag == m_slowTorrentExcludedTag) + return; + + m_slowTorrentExcludedTag = tag; +} + int SessionImpl::outgoingPortsMin() const { return m_outgoingPortsMin; @@ -6615,3 +6701,112 @@ void SessionImpl::handleRemovedTorrent(const TorrentID &torrentID, const QString m_removingTorrents.erase(removingTorrentDataIter); } + +void SessionImpl::processSlowTorrentDetection() +{ + // Only run if both queueing and slow torrent detection are enabled + if (!isQueueingSystemEnabled() || !isSlowTorrentDetectionEnabled()) + { + m_downloadProgressRecords.clear(); + return; + } + + // Check if there are any queued torrents + bool hasQueuedTorrents = false; + for (TorrentImpl *torrent : asConst(m_torrents)) + { + if (torrent->state() == TorrentState::QueuedDownloading) + { + hasQueuedTorrents = true; + break; + } + } + + // If no queued torrents, clear records and return + if (!hasQueuedTorrents) + { + m_downloadProgressRecords.clear(); + return; + } + + // Calculate the number of samples needed for the monitoring window + // Using ceiling division: (duration_minutes * 60 + 9) / 10 + // This ensures we have enough samples for the full monitoring window + // Example: 10 minutes = 600 seconds, 600/10 = 60 samples needed + const int monitoringSamplesCount = (m_slowTorrentDetectionDuration * 60 + 9) / 10; + const qint64 minimumProgressBytes = static_cast(m_slowTorrentMinimumProgress) * 1024 * 1024; + const QString excludedTag = m_slowTorrentExcludedTag; + + // Collect current download states for active downloading torrents + QHash currentDownloadStates; + for (TorrentImpl *torrent : asConst(m_torrents)) + { + const TorrentState state = torrent->state(); + + // Only monitor downloading, metaDL, and stalledDL states + if (state != TorrentState::Downloading + && state != TorrentState::DownloadingMetadata + && state != TorrentState::StalledDownloading) + { + continue; + } + + // Skip if torrent has the excluded tag + if (!excludedTag.isEmpty()) + { + const TagSet torrentTags = torrent->tags(); + if (torrentTags.contains(Tag(excludedTag))) + continue; + } + + currentDownloadStates.insert(torrent->id(), torrent->totalDownload()); + } + + // Remove records for torrents that are no longer being monitored + QMutableHashIterator> iter(m_downloadProgressRecords); + while (iter.hasNext()) + { + iter.next(); + if (!currentDownloadStates.contains(iter.key())) + iter.remove(); + } + + // Update progress records for each torrent + for (auto it = currentDownloadStates.cbegin(); it != currentDownloadStates.cend(); ++it) + { + const TorrentID &torrentID = it.key(); + const qint64 downloadedBytes = it.value(); + + QList &records = m_downloadProgressRecords[torrentID]; + records.append(downloadedBytes); + + // Keep only the last N samples + while (records.size() > monitoringSamplesCount) + records.removeFirst(); + } + + // Identify stagnant torrents that need to be moved to queue bottom + QList stagnantTorrentIDs; + for (auto it = m_downloadProgressRecords.cbegin(); it != m_downloadProgressRecords.cend(); ++it) + { + const QList &records = it.value(); + + // Only check torrents that have been monitored for the full window + if (records.size() == monitoringSamplesCount) + { + const qint64 progressDifference = records.last() - records.first(); + if (progressDifference < minimumProgressBytes) + stagnantTorrentIDs.append(it.key()); + } + } + + // Move stagnant torrents to bottom of queue + if (!stagnantTorrentIDs.isEmpty()) + { + bottomTorrentsQueuePos(stagnantTorrentIDs); + + // Clear records for torrents that were moved to bottom + for (const TorrentID &torrentID : asConst(stagnantTorrentIDs)) + m_downloadProgressRecords.remove(torrentID); + } +} diff --git a/src/base/bittorrent/sessionimpl.h b/src/base/bittorrent/sessionimpl.h index bd2c836cab65..2c45ad24ad32 100644 --- a/src/base/bittorrent/sessionimpl.h +++ b/src/base/bittorrent/sessionimpl.h @@ -353,6 +353,14 @@ namespace BitTorrent void setUploadRateForSlowTorrents(int rateInKibiBytes) override; int slowTorrentsInactivityTimer() const override; void setSlowTorrentsInactivityTimer(int timeInSeconds) override; + bool isSlowTorrentDetectionEnabled() const override; + void setSlowTorrentDetectionEnabled(bool enabled) override; + int slowTorrentDetectionDuration() const override; + void setSlowTorrentDetectionDuration(int minutes) override; + int slowTorrentMinimumProgress() const override; + void setSlowTorrentMinimumProgress(int megabytes) override; + QString slowTorrentExcludedTag() const override; + void setSlowTorrentExcludedTag(const QString &tag) override; int outgoingPortsMin() const override; void setOutgoingPortsMin(int min) override; int outgoingPortsMax() const override; @@ -521,6 +529,7 @@ namespace BitTorrent void handleIPFilterParsed(int ruleCount); void handleIPFilterError(); void torrentContentRemovingFinished(const QString &torrentName, const QString &errorMessage); + void processSlowTorrentDetection(); private: struct ResumeSessionContext; @@ -687,6 +696,10 @@ namespace BitTorrent CachedSettingValue m_downloadRateForSlowTorrents; CachedSettingValue m_uploadRateForSlowTorrents; CachedSettingValue m_slowTorrentsInactivityTimer; + CachedSettingValue m_isSlowTorrentDetectionEnabled; + CachedSettingValue m_slowTorrentDetectionDuration; + CachedSettingValue m_slowTorrentMinimumProgress; + CachedSettingValue m_slowTorrentExcludedTag; CachedSettingValue m_outgoingPortsMin; CachedSettingValue m_outgoingPortsMax; CachedSettingValue m_UPnPLeaseDuration; @@ -815,6 +828,8 @@ namespace BitTorrent bool m_refreshEnqueued = false; QTimer *m_seedingLimitTimer = nullptr; QTimer *m_resumeDataTimer = nullptr; + QTimer *m_slowTorrentDetectionTimer = nullptr; + QHash> m_downloadProgressRecords; // IP filtering QPointer m_filterParser; QPointer m_bwScheduler; diff --git a/src/gui/optionsdialog.cpp b/src/gui/optionsdialog.cpp index 526582a7992b..a84d2ec29aa6 100644 --- a/src/gui/optionsdialog.cpp +++ b/src/gui/optionsdialog.cpp @@ -1117,6 +1117,21 @@ void OptionsDialog::loadBittorrentTabOptions() m_ui->spinUploadRateForSlowTorrents->setValue(session->uploadRateForSlowTorrents()); m_ui->spinSlowTorrentsInactivityTimer->setValue(session->slowTorrentsInactivityTimer()); + m_ui->checkEnableSlowTorrentDetection->setChecked(session->isSlowTorrentDetectionEnabled()); + m_ui->spinSlowTorrentDetectionDuration->setValue(session->slowTorrentDetectionDuration()); + m_ui->spinSlowTorrentMinimumProgress->setValue(session->slowTorrentMinimumProgress()); + + // Populate tag combo box + m_ui->comboSlowTorrentExcludedTag->clear(); + m_ui->comboSlowTorrentExcludedTag->addItem(tr("None"), QString()); + const TagSet tags = session->tags(); + for (const Tag &tag : tags) + m_ui->comboSlowTorrentExcludedTag->addItem(tag.toString(), tag.toString()); + + const QString excludedTag = session->slowTorrentExcludedTag(); + const int tagIndex = m_ui->comboSlowTorrentExcludedTag->findData(excludedTag); + m_ui->comboSlowTorrentExcludedTag->setCurrentIndex(tagIndex >= 0 ? tagIndex : 0); + if (session->globalMaxRatio() >= 0.) { // Enable @@ -1191,6 +1206,10 @@ void OptionsDialog::loadBittorrentTabOptions() connect(m_ui->spinDownloadRateForSlowTorrents, qSpinBoxValueChanged, this, &ThisType::enableApplyButton); connect(m_ui->spinUploadRateForSlowTorrents, qSpinBoxValueChanged, this, &ThisType::enableApplyButton); connect(m_ui->spinSlowTorrentsInactivityTimer, qSpinBoxValueChanged, this, &ThisType::enableApplyButton); + connect(m_ui->checkEnableSlowTorrentDetection, &QGroupBox::toggled, this, &ThisType::enableApplyButton); + connect(m_ui->spinSlowTorrentDetectionDuration, qSpinBoxValueChanged, this, &ThisType::enableApplyButton); + connect(m_ui->spinSlowTorrentMinimumProgress, qSpinBoxValueChanged, this, &ThisType::enableApplyButton); + connect(m_ui->comboSlowTorrentExcludedTag, qComboBoxCurrentIndexChanged, this, &ThisType::enableApplyButton); connect(m_ui->checkMaxRatio, &QAbstractButton::toggled, m_ui->spinMaxRatio, &QWidget::setEnabled); connect(m_ui->checkMaxRatio, &QAbstractButton::toggled, this, &ThisType::toggleComboRatioLimitAct); @@ -1233,6 +1252,10 @@ void OptionsDialog::saveBittorrentTabOptions() const session->setDownloadRateForSlowTorrents(m_ui->spinDownloadRateForSlowTorrents->value()); session->setUploadRateForSlowTorrents(m_ui->spinUploadRateForSlowTorrents->value()); session->setSlowTorrentsInactivityTimer(m_ui->spinSlowTorrentsInactivityTimer->value()); + session->setSlowTorrentDetectionEnabled(m_ui->checkEnableSlowTorrentDetection->isChecked()); + session->setSlowTorrentDetectionDuration(m_ui->spinSlowTorrentDetectionDuration->value()); + session->setSlowTorrentMinimumProgress(m_ui->spinSlowTorrentMinimumProgress->value()); + session->setSlowTorrentExcludedTag(m_ui->comboSlowTorrentExcludedTag->currentData().toString()); session->setGlobalMaxRatio(getMaxRatio()); session->setGlobalMaxSeedingMinutes(getMaxSeedingMinutes()); diff --git a/src/gui/optionsdialog.ui b/src/gui/optionsdialog.ui index fe427909f498..123522c0f7de 100644 --- a/src/gui/optionsdialog.ui +++ b/src/gui/optionsdialog.ui @@ -3057,6 +3057,104 @@ Disable encryption: Only connect to peers without protocol encryption + + + + Move slow downloading torrents to queue end + + + true + + + false + + + + + + Time: + + + + + + + min + + + 1 + + + 999999 + + + 10 + + + + + + + Qt::Orientation::Horizontal + + + + 40 + 20 + + + + + + + + Minimum size: + + + + + + + MiB + + + 1 + + + 999999 + + + 10 + + + + + + + Excluded tag: + + + + + + + false + + + + + + + Torrents that fail to reach the minimum size within the time window will be moved to the end of the queue. + + + true + + + + + + diff --git a/src/webui/api/appcontroller.cpp b/src/webui/api/appcontroller.cpp index 11d6bee7e12d..9119265af5d7 100644 --- a/src/webui/api/appcontroller.cpp +++ b/src/webui/api/appcontroller.cpp @@ -304,6 +304,10 @@ void AppController::preferencesAction() data[u"slow_torrent_dl_rate_threshold"_s] = session->downloadRateForSlowTorrents(); data[u"slow_torrent_ul_rate_threshold"_s] = session->uploadRateForSlowTorrents(); data[u"slow_torrent_inactive_timer"_s] = session->slowTorrentsInactivityTimer(); + data[u"slow_torrent_detection_enabled"_s] = session->isSlowTorrentDetectionEnabled(); + data[u"slow_torrent_detection_duration"_s] = session->slowTorrentDetectionDuration(); + data[u"slow_torrent_minimum_progress"_s] = session->slowTorrentMinimumProgress(); + data[u"slow_torrent_excluded_tag"_s] = session->slowTorrentExcludedTag(); // Share Ratio Limiting data[u"max_ratio_enabled"_s] = (session->globalMaxRatio() >= 0.); data[u"max_ratio"_s] = session->globalMaxRatio(); @@ -837,6 +841,14 @@ void AppController::setPreferencesAction() session->setUploadRateForSlowTorrents(it.value().toInt()); if (hasKey(u"slow_torrent_inactive_timer"_s)) session->setSlowTorrentsInactivityTimer(it.value().toInt()); + if (hasKey(u"slow_torrent_detection_enabled"_s)) + session->setSlowTorrentDetectionEnabled(it.value().toBool()); + if (hasKey(u"slow_torrent_detection_duration"_s)) + session->setSlowTorrentDetectionDuration(it.value().toInt()); + if (hasKey(u"slow_torrent_minimum_progress"_s)) + session->setSlowTorrentMinimumProgress(it.value().toInt()); + if (hasKey(u"slow_torrent_excluded_tag"_s)) + session->setSlowTorrentExcludedTag(it.value().toString()); // Share Ratio Limiting if (hasKey(u"max_ratio_enabled"_s) && !it.value().toBool()) session->setGlobalMaxRatio(-1); diff --git a/src/webui/www/private/views/preferences.html b/src/webui/www/private/views/preferences.html index 76257e9bbe40..6b864255117f 100644 --- a/src/webui/www/private/views/preferences.html +++ b/src/webui/www/private/views/preferences.html @@ -784,6 +784,45 @@ +
+ + + + + + + + + + + + + + + + + + + + + + +
+ + +   QBT_TR(minutes)QBT_TR[CONTEXT=OptionsDialog] +
+ + +   QBT_TR(MiB)QBT_TR[CONTEXT=OptionsDialog] +
+ + + +
+
QBT_TR(Torrents that fail to reach the minimum size within the time window will be moved to the end of the queue.)QBT_TR[CONTEXT=OptionsDialog]
+
+
@@ -1815,6 +1854,7 @@ updateSchedulingEnabled: updateSchedulingEnabled, updateQueueingSystem: updateQueueingSystem, updateSlowTorrentsSettings: updateSlowTorrentsSettings, + updateSlowTorrentDetectionSettings: updateSlowTorrentDetectionSettings, updateMaxRatioTimeEnabled: updateMaxRatioTimeEnabled, updateAddTrackersEnabled: updateAddTrackersEnabled, updateAddTrackersFromURLEnabled: updateAddTrackersFromURLEnabled, @@ -2084,7 +2124,9 @@ document.getElementById("maxActiveUpValue").disabled = !isQueueingEnabled; document.getElementById("maxActiveToValue").disabled = !isQueueingEnabled; document.getElementById("dontCountSlowTorrentsCheckbox").disabled = !isQueueingEnabled; + document.getElementById("slowTorrentDetectionCheckbox").disabled = !isQueueingEnabled; updateSlowTorrentsSettings(); + updateSlowTorrentDetectionSettings(); }; const updateSlowTorrentsSettings = () => { @@ -2094,6 +2136,13 @@ document.getElementById("torrentInactiveTimerValue").disabled = !isDontCountSlowTorrentsEnabled; }; + const updateSlowTorrentDetectionSettings = () => { + const isSlowTorrentDetectionEnabled = (!document.getElementById("slowTorrentDetectionCheckbox").disabled) && document.getElementById("slowTorrentDetectionCheckbox").checked; + document.getElementById("slowTorrentDetectionDuration").disabled = !isSlowTorrentDetectionEnabled; + document.getElementById("slowTorrentMinimumProgress").disabled = !isSlowTorrentDetectionEnabled; + document.getElementById("slowTorrentExcludedTag").disabled = !isSlowTorrentDetectionEnabled; + }; + const updateMaxRatioTimeEnabled = () => { const isMaxRatioEnabled = document.getElementById("maxRatioCheckbox").checked; document.getElementById("maxRatioValue").disabled = !isMaxRatioEnabled; @@ -2518,6 +2567,25 @@ document.getElementById("dlRateThresholdValue").value = Number(pref.slow_torrent_dl_rate_threshold); document.getElementById("ulRateThresholdValue").value = Number(pref.slow_torrent_ul_rate_threshold); document.getElementById("torrentInactiveTimerValue").value = Number(pref.slow_torrent_inactive_timer); + document.getElementById("slowTorrentDetectionCheckbox").checked = pref.slow_torrent_detection_enabled; + document.getElementById("slowTorrentDetectionDuration").value = Number(pref.slow_torrent_detection_duration); + document.getElementById("slowTorrentMinimumProgress").value = Number(pref.slow_torrent_minimum_progress); + + // Populate tag dropdown + fetch("api/v2/torrents/tags") + .then(res => res.json()) + .then(tags => { + const tagSelect = document.getElementById("slowTorrentExcludedTag"); + tagSelect.innerHTML = ''; + for (const tag of tags) { + const option = document.createElement("option"); + option.value = tag; + option.textContent = tag; + tagSelect.appendChild(option); + } + tagSelect.value = pref.slow_torrent_excluded_tag || ""; + }); + updateQueueingSystem(); // Share Limiting @@ -2970,6 +3038,24 @@ return; } settings["slow_torrent_inactive_timer"] = torrentInactiveTimer; + + settings["slow_torrent_detection_enabled"] = document.getElementById("slowTorrentDetectionCheckbox").checked; + + const slowTorrentDetectionDuration = Number(document.getElementById("slowTorrentDetectionDuration").value); + if (Number.isNaN(slowTorrentDetectionDuration) || (slowTorrentDetectionDuration < 1)) { + alert("QBT_TR(Monitoring window duration must be greater than 0.)QBT_TR[CONTEXT=HttpServer]"); + return; + } + settings["slow_torrent_detection_duration"] = slowTorrentDetectionDuration; + + const slowTorrentMinimumProgress = Number(document.getElementById("slowTorrentMinimumProgress").value); + if (Number.isNaN(slowTorrentMinimumProgress) || (slowTorrentMinimumProgress < 1)) { + alert("QBT_TR(Minimum file progress must be greater than 0.)QBT_TR[CONTEXT=HttpServer]"); + return; + } + settings["slow_torrent_minimum_progress"] = slowTorrentMinimumProgress; + + settings["slow_torrent_excluded_tag"] = document.getElementById("slowTorrentExcludedTag").value; } // Share Ratio Limiting