From 67357e99648a02134c7a38c93c0fa87f9932c962 Mon Sep 17 00:00:00 2001 From: Vladimir Golovnev Date: Sun, 9 Oct 2022 16:07:16 +0300 Subject: [PATCH] Allow to set torrent stop condition PR #17814. Closes #17792. Closes #929. (Actually it should close all issues about lack of ability to stop torrent after metadata downloaded or after files are initially checked.) Also makes explicit the temporary start of the torrent in the case when recheck of the stopped torrent is performed. --- src/base/bittorrent/addtorrentparams.h | 1 + .../bittorrent/bencoderesumedatastorage.cpp | 22 +++--- src/base/bittorrent/dbresumedatastorage.cpp | 42 +++++++--- src/base/bittorrent/dbresumedatastorage.h | 2 +- src/base/bittorrent/loadtorrentparams.h | 1 + .../bittorrent/nativetorrentextension.cpp | 21 +---- src/base/bittorrent/nativetorrentextension.h | 1 - src/base/bittorrent/session.h | 2 + src/base/bittorrent/sessionimpl.cpp | 12 +++ src/base/bittorrent/sessionimpl.h | 3 + src/base/bittorrent/torrent.h | 13 +++ src/base/bittorrent/torrentimpl.cpp | 67 +++++++++++++--- src/base/bittorrent/torrentimpl.h | 4 + src/gui/addnewtorrentdialog.cpp | 19 +++++ src/gui/addnewtorrentdialog.ui | 79 +++++++++++++------ src/gui/optionsdialog.cpp | 20 +++++ src/gui/optionsdialog.ui | 47 +++++++++++ src/webui/api/torrentscontroller.cpp | 6 ++ src/webui/webapplication.h | 2 +- 19 files changed, 286 insertions(+), 78 deletions(-) diff --git a/src/base/bittorrent/addtorrentparams.h b/src/base/bittorrent/addtorrentparams.h index 80d9839b8..17df907d6 100644 --- a/src/base/bittorrent/addtorrentparams.h +++ b/src/base/bittorrent/addtorrentparams.h @@ -55,6 +55,7 @@ namespace BitTorrent bool firstLastPiecePriority = false; bool addForced = false; std::optional addPaused; + std::optional stopCondition; PathList filePaths; // used if TorrentInfo is set QVector filePriorities; // used if TorrentInfo is set bool skipChecking = false; diff --git a/src/base/bittorrent/bencoderesumedatastorage.cpp b/src/base/bittorrent/bencoderesumedatastorage.cpp index b541fbf0e..f500c5e2d 100644 --- a/src/base/bittorrent/bencoderesumedatastorage.cpp +++ b/src/base/bittorrent/bencoderesumedatastorage.cpp @@ -245,6 +245,9 @@ BitTorrent::LoadResumeDataResult BitTorrent::BencodeResumeDataStorage::loadTorre // fromLTString(root.dict_find_string_value("qBt-contentLayout")), TorrentContentLayout::Default); // === END REPLACEMENT CODE === // + torrentParams.stopCondition = Utils::String::toEnum( + fromLTString(resumeDataRoot.dict_find_string_value("qBt-stopCondition")), Torrent::StopCondition::None); + const lt::string_view ratioLimitString = resumeDataRoot.dict_find_string_value("qBt-ratioLimit"); if (ratioLimitString.empty()) torrentParams.ratioLimit = resumeDataRoot.dict_find_int_value("qBt-ratioLimit", Torrent::USE_GLOBAL_RATIO * 1000) / 1000.0; @@ -284,20 +287,14 @@ BitTorrent::LoadResumeDataResult BitTorrent::BencodeResumeDataStorage::loadTorre p.save_path = Profile::instance()->fromPortablePath( Path(fromLTString(p.save_path))).toString().toStdString(); + torrentParams.stopped = (p.flags & lt::torrent_flags::paused) && !(p.flags & lt::torrent_flags::auto_managed); + torrentParams.operatingMode = (p.flags & lt::torrent_flags::paused) || (p.flags & lt::torrent_flags::auto_managed) + ? TorrentOperatingMode::AutoManaged : TorrentOperatingMode::Forced; + if (p.flags & lt::torrent_flags::stop_when_ready) { - // If torrent has "stop_when_ready" flag set then it is actually "stopped" - torrentParams.stopped = true; - torrentParams.operatingMode = TorrentOperatingMode::AutoManaged; - // ...but temporarily "resumed" to perform some service jobs (e.g. checking) - p.flags &= ~lt::torrent_flags::paused; - p.flags |= lt::torrent_flags::auto_managed; - } - else - { - torrentParams.stopped = (p.flags & lt::torrent_flags::paused) && !(p.flags & lt::torrent_flags::auto_managed); - torrentParams.operatingMode = (p.flags & lt::torrent_flags::paused) || (p.flags & lt::torrent_flags::auto_managed) - ? TorrentOperatingMode::AutoManaged : TorrentOperatingMode::Forced; + p.flags &= ~lt::torrent_flags::stop_when_ready; + torrentParams.stopCondition = Torrent::StopCondition::FilesChecked; } const bool hasMetadata = (p.ti && p.ti->is_valid()); @@ -393,6 +390,7 @@ void BitTorrent::BencodeResumeDataStorage::Worker::store(const TorrentID &id, co data["qBt-seedStatus"] = resumeData.hasSeedStatus; data["qBt-contentLayout"] = Utils::String::fromEnum(resumeData.contentLayout).toStdString(); data["qBt-firstLastPiecePriority"] = resumeData.firstLastPiecePriority; + data["qBt-stopCondition"] = Utils::String::fromEnum(resumeData.stopCondition).toStdString(); if (!resumeData.useAutoTMM) { diff --git a/src/base/bittorrent/dbresumedatastorage.cpp b/src/base/bittorrent/dbresumedatastorage.cpp index 3719d4356..a969d5a3d 100644 --- a/src/base/bittorrent/dbresumedatastorage.cpp +++ b/src/base/bittorrent/dbresumedatastorage.cpp @@ -61,7 +61,7 @@ namespace { const QString DB_CONNECTION_NAME = u"ResumeDataStorage"_qs; - const int DB_VERSION = 2; + const int DB_VERSION = 3; const QString DB_TABLE_META = u"meta"_qs; const QString DB_TABLE_TORRENTS = u"torrents"_qs; @@ -94,6 +94,7 @@ namespace const Column DB_COLUMN_HAS_SEED_STATUS = makeColumn("has_seed_status"); const Column DB_COLUMN_OPERATING_MODE = makeColumn("operating_mode"); const Column DB_COLUMN_STOPPED = makeColumn("stopped"); + const Column DB_COLUMN_STOP_CONDITION = makeColumn("stop_condition"); const Column DB_COLUMN_RESUMEDATA = makeColumn("libtorrent_resume_data"); const Column DB_COLUMN_METADATA = makeColumn("metadata"); const Column DB_COLUMN_VALUE = makeColumn("value"); @@ -213,6 +214,8 @@ namespace BitTorrent resumeData.operatingMode = Utils::String::toEnum( query.value(DB_COLUMN_OPERATING_MODE.name).toString(), TorrentOperatingMode::AutoManaged); resumeData.stopped = query.value(DB_COLUMN_STOPPED.name).toBool(); + resumeData.stopCondition = Utils::String::toEnum( + query.value(DB_COLUMN_STOP_CONDITION.name).toString(), Torrent::StopCondition::None); resumeData.savePath = Profile::instance()->fromPortablePath( Path(query.value(DB_COLUMN_TARGET_SAVE_PATH.name).toString())); @@ -241,6 +244,12 @@ namespace BitTorrent p.save_path = Profile::instance()->fromPortablePath(Path(fromLTString(p.save_path))) .toString().toStdString(); + if (p.flags & lt::torrent_flags::stop_when_ready) + { + p.flags &= ~lt::torrent_flags::stop_when_ready; + resumeData.stopCondition = Torrent::StopCondition::FilesChecked; + } + return resumeData; } } @@ -263,9 +272,9 @@ BitTorrent::DBResumeDataStorage::DBResumeDataStorage(const Path &dbPath, QObject } else { - const int dbVersion = currentDBVersion(); - if ((dbVersion == 1) || !db.record(DB_TABLE_TORRENTS).contains(DB_COLUMN_DOWNLOAD_PATH.name)) - updateDBFromVersion1(); + const int dbVersion = (!db.record(DB_TABLE_TORRENTS).contains(DB_COLUMN_DOWNLOAD_PATH.name) ? 1 : currentDBVersion()); + if (dbVersion != DB_VERSION) + updateDB(dbVersion); } m_asyncWorker = new Worker(dbPath, u"ResumeDataStorageWorker"_qs, m_dbLock); @@ -507,8 +516,11 @@ void BitTorrent::DBResumeDataStorage::createDB() const } } -void BitTorrent::DBResumeDataStorage::updateDBFromVersion1() const +void BitTorrent::DBResumeDataStorage::updateDB(const int fromVersion) const { + Q_ASSERT(fromVersion > 0); + Q_ASSERT(fromVersion != DB_VERSION); + auto db = QSqlDatabase::database(DB_CONNECTION_NAME); const QWriteLocker locker {&m_dbLock}; @@ -520,10 +532,21 @@ void BitTorrent::DBResumeDataStorage::updateDBFromVersion1() const try { - const auto alterTableTorrentsQuery = u"ALTER TABLE %1 ADD %2"_qs - .arg(quoted(DB_TABLE_TORRENTS), makeColumnDefinition(DB_COLUMN_DOWNLOAD_PATH, "TEXT")); - if (!query.exec(alterTableTorrentsQuery)) - throw RuntimeError(query.lastError().text()); + if (fromVersion == 1) + { + const auto alterTableTorrentsQuery = u"ALTER TABLE %1 ADD %2"_qs + .arg(quoted(DB_TABLE_TORRENTS), makeColumnDefinition(DB_COLUMN_DOWNLOAD_PATH, "TEXT")); + if (!query.exec(alterTableTorrentsQuery)) + throw RuntimeError(query.lastError().text()); + } + + if (fromVersion <= 2) + { + const auto alterTableTorrentsQuery = u"ALTER TABLE %1 ADD %2"_qs + .arg(quoted(DB_TABLE_TORRENTS), makeColumnDefinition(DB_COLUMN_STOP_CONDITION, "TEXT NOT NULL DEFAULT `None`")); + if (!query.exec(alterTableTorrentsQuery)) + throw RuntimeError(query.lastError().text()); + } const QString updateMetaVersionQuery = makeUpdateStatement(DB_TABLE_META, {DB_COLUMN_NAME, DB_COLUMN_VALUE}); if (!query.prepare(updateMetaVersionQuery)) @@ -662,6 +685,7 @@ void BitTorrent::DBResumeDataStorage::Worker::store(const TorrentID &id, const L query.bindValue(DB_COLUMN_HAS_SEED_STATUS.placeholder, resumeData.hasSeedStatus); query.bindValue(DB_COLUMN_OPERATING_MODE.placeholder, Utils::String::fromEnum(resumeData.operatingMode)); query.bindValue(DB_COLUMN_STOPPED.placeholder, resumeData.stopped); + query.bindValue(DB_COLUMN_STOP_CONDITION.placeholder, Utils::String::fromEnum(resumeData.stopCondition)); if (!resumeData.useAutoTMM) { diff --git a/src/base/bittorrent/dbresumedatastorage.h b/src/base/bittorrent/dbresumedatastorage.h index 1657e584f..1dd9392cb 100644 --- a/src/base/bittorrent/dbresumedatastorage.h +++ b/src/base/bittorrent/dbresumedatastorage.h @@ -57,7 +57,7 @@ namespace BitTorrent void doLoadAll() const override; int currentDBVersion() const; void createDB() const; - void updateDBFromVersion1() const; + void updateDB(int fromVersion) const; QThread *m_ioThread = nullptr; diff --git a/src/base/bittorrent/loadtorrentparams.h b/src/base/bittorrent/loadtorrentparams.h index 4892bece8..f49870c89 100644 --- a/src/base/bittorrent/loadtorrentparams.h +++ b/src/base/bittorrent/loadtorrentparams.h @@ -54,6 +54,7 @@ namespace BitTorrent bool firstLastPiecePriority = false; bool hasSeedStatus = false; bool stopped = false; + Torrent::StopCondition stopCondition; qreal ratioLimit = Torrent::USE_GLOBAL_RATIO; int seedingTimeLimit = Torrent::USE_GLOBAL_SEEDING_TIME; diff --git a/src/base/bittorrent/nativetorrentextension.cpp b/src/base/bittorrent/nativetorrentextension.cpp index b0ea54a21..1f8ab1a1e 100644 --- a/src/base/bittorrent/nativetorrentextension.cpp +++ b/src/base/bittorrent/nativetorrentextension.cpp @@ -30,14 +30,6 @@ #include -namespace -{ - bool isAutoManaged(const lt::torrent_status &torrentStatus) - { - return static_cast(torrentStatus.flags & lt::torrent_flags::auto_managed); - } -} - NativeTorrentExtension::NativeTorrentExtension(const lt::torrent_handle &torrentHandle, ExtensionData *data) : m_torrentHandle {torrentHandle} , m_data {data} @@ -65,19 +57,10 @@ NativeTorrentExtension::~NativeTorrentExtension() delete m_data; } -bool NativeTorrentExtension::on_pause() -{ - if (!isAutoManaged(m_torrentHandle.status({}))) - m_torrentHandle.unset_flags(lt::torrent_flags::stop_when_ready); - - // return `false` to allow standard handler - // and other extensions to be also invoked. - return false; -} - void NativeTorrentExtension::on_state(const lt::torrent_status::state_t state) { - if (m_state == lt::torrent_status::downloading_metadata) + if ((m_state == lt::torrent_status::downloading_metadata) + || (m_state == lt::torrent_status::checking_files)) { m_torrentHandle.unset_flags(lt::torrent_flags::auto_managed); m_torrentHandle.pause(); diff --git a/src/base/bittorrent/nativetorrentextension.h b/src/base/bittorrent/nativetorrentextension.h index 8667eec50..4aed9e10d 100644 --- a/src/base/bittorrent/nativetorrentextension.h +++ b/src/base/bittorrent/nativetorrentextension.h @@ -40,7 +40,6 @@ public: ~NativeTorrentExtension(); private: - bool on_pause() override; void on_state(lt::torrent_status::state_t state) override; lt::torrent_handle m_torrentHandle; diff --git a/src/base/bittorrent/session.h b/src/base/bittorrent/session.h index 2f62a7646..c0dd788fd 100644 --- a/src/base/bittorrent/session.h +++ b/src/base/bittorrent/session.h @@ -208,6 +208,8 @@ namespace BitTorrent virtual void setPeXEnabled(bool enabled) = 0; virtual bool isAddTorrentPaused() const = 0; virtual void setAddTorrentPaused(bool value) = 0; + virtual Torrent::StopCondition torrentStopCondition() const = 0; + virtual void setTorrentStopCondition(Torrent::StopCondition stopCondition) = 0; virtual TorrentContentLayout torrentContentLayout() const = 0; virtual void setTorrentContentLayout(TorrentContentLayout value) = 0; virtual bool isTrackerEnabled() const = 0; diff --git a/src/base/bittorrent/sessionimpl.cpp b/src/base/bittorrent/sessionimpl.cpp index c96b54625..ceca6ed64 100644 --- a/src/base/bittorrent/sessionimpl.cpp +++ b/src/base/bittorrent/sessionimpl.cpp @@ -438,6 +438,7 @@ SessionImpl::SessionImpl(QObject *parent) , m_globalMaxRatio(BITTORRENT_SESSION_KEY(u"GlobalMaxRatio"_qs), -1, [](qreal r) { return r < 0 ? -1. : r;}) , m_globalMaxSeedingMinutes(BITTORRENT_SESSION_KEY(u"GlobalMaxSeedingMinutes"_qs), -1, lowerLimited(-1)) , m_isAddTorrentPaused(BITTORRENT_SESSION_KEY(u"AddTorrentPaused"_qs), false) + , m_torrentStopCondition(BITTORRENT_SESSION_KEY(u"TorrentStopCondition"_qs), Torrent::StopCondition::None) , m_torrentContentLayout(BITTORRENT_SESSION_KEY(u"TorrentContentLayout"_qs), TorrentContentLayout::Original) , m_isAppendExtensionEnabled(BITTORRENT_SESSION_KEY(u"AddExtensionToIncompleteFiles"_qs), false) , m_refreshInterval(BITTORRENT_SESSION_KEY(u"RefreshInterval"_qs), 1500) @@ -950,6 +951,16 @@ void SessionImpl::setAddTorrentPaused(const bool value) m_isAddTorrentPaused = value; } +Torrent::StopCondition SessionImpl::torrentStopCondition() const +{ + return m_torrentStopCondition; +} + +void SessionImpl::setTorrentStopCondition(const Torrent::StopCondition stopCondition) +{ + m_torrentStopCondition = stopCondition; +} + bool SessionImpl::isTrackerEnabled() const { return m_isTrackerEnabled; @@ -2497,6 +2508,7 @@ LoadTorrentParams SessionImpl::initLoadTorrentParams(const AddTorrentParams &add loadTorrentParams.contentLayout = addTorrentParams.contentLayout.value_or(torrentContentLayout()); loadTorrentParams.operatingMode = (addTorrentParams.addForced ? TorrentOperatingMode::Forced : TorrentOperatingMode::AutoManaged); loadTorrentParams.stopped = addTorrentParams.addPaused.value_or(isAddTorrentPaused()); + loadTorrentParams.stopCondition = addTorrentParams.stopCondition.value_or(torrentStopCondition()); loadTorrentParams.ratioLimit = addTorrentParams.ratioLimit; loadTorrentParams.seedingTimeLimit = addTorrentParams.seedingTimeLimit; diff --git a/src/base/bittorrent/sessionimpl.h b/src/base/bittorrent/sessionimpl.h index 1dabf4e3f..0d105a395 100644 --- a/src/base/bittorrent/sessionimpl.h +++ b/src/base/bittorrent/sessionimpl.h @@ -188,6 +188,8 @@ namespace BitTorrent void setPeXEnabled(bool enabled) override; bool isAddTorrentPaused() const override; void setAddTorrentPaused(bool value) override; + Torrent::StopCondition torrentStopCondition() const override; + void setTorrentStopCondition(Torrent::StopCondition stopCondition) override; TorrentContentLayout torrentContentLayout() const override; void setTorrentContentLayout(TorrentContentLayout value) override; bool isTrackerEnabled() const override; @@ -620,6 +622,7 @@ namespace BitTorrent CachedSettingValue m_globalMaxRatio; CachedSettingValue m_globalMaxSeedingMinutes; CachedSettingValue m_isAddTorrentPaused; + CachedSettingValue m_torrentStopCondition; CachedSettingValue m_torrentContentLayout; CachedSettingValue m_isAppendExtensionEnabled; CachedSettingValue m_refreshInterval; diff --git a/src/base/bittorrent/torrent.h b/src/base/bittorrent/torrent.h index e1872e884..36bc8e072 100644 --- a/src/base/bittorrent/torrent.h +++ b/src/base/bittorrent/torrent.h @@ -108,7 +108,17 @@ namespace BitTorrent class Torrent : public AbstractFileStorage { + Q_GADGET + public: + enum class StopCondition + { + None = 0, + MetadataReceived = 1, + FilesChecked = 2 + }; + Q_ENUM(StopCondition) + static const qreal USE_GLOBAL_RATIO; static const qreal NO_RATIO_LIMIT; @@ -300,6 +310,9 @@ namespace BitTorrent virtual void clearPeers() = 0; virtual bool setMetadata(const TorrentInfo &torrentInfo) = 0; + virtual StopCondition stopCondition() const = 0; + virtual void setStopCondition(StopCondition stopCondition) = 0; + virtual QString createMagnetURI() const = 0; virtual nonstd::expected exportToBuffer() const = 0; virtual nonstd::expected exportToFile(const Path &path) const = 0; diff --git a/src/base/bittorrent/torrentimpl.cpp b/src/base/bittorrent/torrentimpl.cpp index 3a919d1f1..f6a87e90e 100644 --- a/src/base/bittorrent/torrentimpl.cpp +++ b/src/base/bittorrent/torrentimpl.cpp @@ -295,6 +295,8 @@ TorrentImpl::TorrentImpl(SessionImpl *session, lt::session *nativeSession } } + setStopCondition(params.stopCondition); + const auto *extensionData = static_cast(m_ltAddTorrentParams.userdata); m_trackerEntries.reserve(static_cast(extensionData->trackers.size())); for (const lt::announce_entry &announceEntry : extensionData->trackers) @@ -993,9 +995,7 @@ void TorrentImpl::updateState() else m_state = isForced() ? TorrentState::ForcedDownloadingMetadata : TorrentState::DownloadingMetadata; } - else if ((m_nativeStatus.state == lt::torrent_status::checking_files) - && (!isPaused() || (m_nativeStatus.flags & lt::torrent_flags::auto_managed) - || !(m_nativeStatus.flags & lt::torrent_flags::paused))) + else if ((m_nativeStatus.state == lt::torrent_status::checking_files) && !isPaused()) { // If the torrent is not just in the "checking" state, but is being actually checked m_state = m_hasSeedStatus ? TorrentState::CheckingUploading : TorrentState::CheckingDownloading; @@ -1423,8 +1423,8 @@ void TorrentImpl::forceRecheck() if (isPaused()) { // When "force recheck" is applied on paused torrent, we temporarily resume it - // (really we just allow libtorrent to resume it by enabling auto management for it). - m_nativeHandle.set_flags(lt::torrent_flags::stop_when_ready | lt::torrent_flags::auto_managed); + resume(); + setStopCondition(StopCondition::FilesChecked); } } @@ -1581,6 +1581,17 @@ void TorrentImpl::endReceivedMetadataHandling(const Path &savePath, const PathLi p.save_path = savePath.toString().toStdString(); p.ti = metadata; + if (stopCondition() == StopCondition::MetadataReceived) + { + m_stopCondition = StopCondition::None; + + m_isStopped = true; + p.flags |= lt::torrent_flags::paused; + p.flags &= ~lt::torrent_flags::auto_managed; + + m_session->handleTorrentPaused(this); + } + reload(); // If first/last piece priority was specified when adding this torrent, @@ -1639,6 +1650,7 @@ void TorrentImpl::pause() { if (!m_isStopped) { + m_stopCondition = StopCondition::None; m_isStopped = true; m_session->handleTorrentNeedSaveResumeData(this); m_session->handleTorrentPaused(this); @@ -1674,10 +1686,6 @@ void TorrentImpl::resume(const TorrentOperatingMode mode) if (m_isStopped) { - // Torrent may have been temporarily resumed to perform checking files - // so we have to ensure it will not pause after checking is done. - m_nativeHandle.unset_flags(lt::torrent_flags::stop_when_ready); - m_isStopped = false; m_session->handleTorrentNeedSaveResumeData(this); m_session->handleTorrentResumed(this); @@ -1756,13 +1764,13 @@ void TorrentImpl::handleTorrentCheckedAlert(const lt::torrent_checked_alert *p) return; } + if (stopCondition() == StopCondition::FilesChecked) + pause(); + m_statusUpdatedTriggers.enqueue([this]() { qDebug("\"%s\" have just finished checking.", qUtf8Printable(name())); - if (m_nativeStatus.need_save_resume) - m_session->handleTorrentNeedSaveResumeData(this); - if (!m_hasMissingFiles) { if ((progress() < 1.0) && (wantedSize() > 0)) @@ -1772,8 +1780,20 @@ void TorrentImpl::handleTorrentCheckedAlert(const lt::torrent_checked_alert *p) adjustStorageLocation(); manageIncompleteFiles(); + + if (!isPaused()) + { + // torrent is internally paused using NativeTorrentExtension after files checked + // so we need to resume it if there is no corresponding "stop condition" set + setAutoManaged(m_operatingMode == TorrentOperatingMode::AutoManaged); + if (m_operatingMode == TorrentOperatingMode::Forced) + m_nativeHandle.resume(); + } } + if (m_nativeStatus.need_save_resume) + m_session->handleTorrentNeedSaveResumeData(this); + m_session->handleTorrentChecked(this); }); } @@ -1909,6 +1929,7 @@ void TorrentImpl::prepareResumeData(const lt::add_torrent_params ¶ms) resumeData.firstLastPiecePriority = m_hasFirstLastPiecePriority; resumeData.hasSeedStatus = m_hasSeedStatus; resumeData.stopped = m_isStopped; + resumeData.stopCondition = m_stopCondition; resumeData.operatingMode = m_operatingMode; resumeData.ltAddTorrentParams = m_ltAddTorrentParams; resumeData.useAutoTMM = m_useAutoTMM; @@ -2166,6 +2187,28 @@ bool TorrentImpl::setMetadata(const TorrentInfo &torrentInfo) #endif } +Torrent::StopCondition TorrentImpl::stopCondition() const +{ + return m_stopCondition; +} + +void TorrentImpl::setStopCondition(const StopCondition stopCondition) +{ + if (stopCondition == m_stopCondition) + return; + + if (isPaused()) + return; + + if ((stopCondition == StopCondition::MetadataReceived) && hasMetadata()) + return; + + if ((stopCondition == StopCondition::FilesChecked) && hasMetadata() && !isChecking()) + return; + + m_stopCondition = stopCondition; +} + bool TorrentImpl::isMoveInProgress() const { return m_storageIsMoving; diff --git a/src/base/bittorrent/torrentimpl.h b/src/base/bittorrent/torrentimpl.h index 7a8e69539..d03b319e8 100644 --- a/src/base/bittorrent/torrentimpl.h +++ b/src/base/bittorrent/torrentimpl.h @@ -227,6 +227,9 @@ namespace BitTorrent void clearPeers() override; bool setMetadata(const TorrentInfo &torrentInfo) override; + StopCondition stopCondition() const override; + void setStopCondition(StopCondition stopCondition) override; + QString createMagnetURI() const override; nonstd::expected exportToBuffer() const override; nonstd::expected exportToFile(const Path &path) const override; @@ -332,6 +335,7 @@ namespace BitTorrent bool m_hasFirstLastPiecePriority = false; bool m_useAutoTMM; bool m_isStopped; + StopCondition m_stopCondition; bool m_unchecked = false; diff --git a/src/gui/addnewtorrentdialog.cpp b/src/gui/addnewtorrentdialog.cpp index 9324c7499..c6049fded 100644 --- a/src/gui/addnewtorrentdialog.cpp +++ b/src/gui/addnewtorrentdialog.cpp @@ -218,6 +218,24 @@ AddNewTorrentDialog::AddNewTorrentDialog(const BitTorrent::AddTorrentParams &inP m_ui->downloadPath->setMaxVisibleItems(20); m_ui->startTorrentCheckBox->setChecked(!m_torrentParams.addPaused.value_or(session->isAddTorrentPaused())); + m_ui->stopConditionComboBox->setToolTip( + u"

" + tr("None") + u" - " + tr("No stop condition is set.") + u"

" + + tr("Metadata received") + u" - " + tr("Torrent will stop after metadata is received.") + + u" " + tr("Torrents that have metadata initially aren't affected.") + u"

" + + tr("Files checked") + u" - " + tr("Torrent will stop after files are initially checked.") + + u" " + tr("This will also download metadata if it wasn't there initially.") + u"

"); + m_ui->stopConditionComboBox->setItemData(0, QVariant::fromValue(BitTorrent::Torrent::StopCondition::None)); + m_ui->stopConditionComboBox->setItemData(1, QVariant::fromValue(BitTorrent::Torrent::StopCondition::MetadataReceived)); + m_ui->stopConditionComboBox->setItemData(2, QVariant::fromValue(BitTorrent::Torrent::StopCondition::FilesChecked)); + m_ui->stopConditionComboBox->setCurrentIndex(m_ui->stopConditionComboBox->findData( + QVariant::fromValue(m_torrentParams.stopCondition.value_or(session->torrentStopCondition())))); + m_ui->stopConditionLabel->setEnabled(m_ui->startTorrentCheckBox->isChecked()); + m_ui->stopConditionComboBox->setEnabled(m_ui->startTorrentCheckBox->isChecked()); + connect(m_ui->startTorrentCheckBox, &QCheckBox::toggled, this, [this](const bool checked) + { + m_ui->stopConditionLabel->setEnabled(checked); + m_ui->stopConditionComboBox->setEnabled(checked); + }); m_ui->comboTTM->blockSignals(true); // the TreeView size isn't correct if the slot does its job at this point m_ui->comboTTM->setCurrentIndex(session->isAutoTMMDisabledByDefault() ? 0 : 1); @@ -872,6 +890,7 @@ void AddNewTorrentDialog::accept() m_torrentParams.filePriorities = m_contentModel->model()->getFilePriorities(); m_torrentParams.addPaused = !m_ui->startTorrentCheckBox->isChecked(); + m_torrentParams.stopCondition = m_ui->stopConditionComboBox->currentData().value(); m_torrentParams.contentLayout = static_cast(m_ui->contentLayoutComboBox->currentIndex()); m_torrentParams.sequential = m_ui->sequentialCheckBox->isChecked(); diff --git a/src/gui/addnewtorrentdialog.ui b/src/gui/addnewtorrentdialog.ui index 5e484108c..3b8926ee9 100644 --- a/src/gui/addnewtorrentdialog.ui +++ b/src/gui/addnewtorrentdialog.ui @@ -203,22 +203,58 @@ - - - - When checked, the .torrent file will not be deleted regardless of the settings at the "Download" page of the Options dialog - + + - Do not delete .torrent file + Start torrent - - + + + + - Download first and last pieces first + Stop condition: + + + + + + + 0 + + + None + + + + + Metadata received + + + + + Files checked + + + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + @@ -227,32 +263,29 @@ - + Download in sequential order - - + + - Start torrent + Download first and last pieces first - - - - Qt::Horizontal + + + + When checked, the .torrent file will not be deleted regardless of the settings at the "Download" page of the Options dialog - - - 40 - 20 - + + Do not delete .torrent file - + diff --git a/src/gui/optionsdialog.cpp b/src/gui/optionsdialog.cpp index e86c81b89..ea6befb74 100644 --- a/src/gui/optionsdialog.cpp +++ b/src/gui/optionsdialog.cpp @@ -525,6 +525,19 @@ void OptionsDialog::loadDownloadsTabOptions() m_ui->contentLayoutComboBox->setCurrentIndex(static_cast(session->torrentContentLayout())); m_ui->checkStartPaused->setChecked(session->isAddTorrentPaused()); + m_ui->stopConditionComboBox->setToolTip( + u"

" + tr("None") + u" - " + tr("No stop condition is set.") + u"

" + + tr("Metadata received") + u" - " + tr("Torrent will stop after metadata is received.") + + u" " + tr("Torrents that have metadata initially aren't affected.") + u"

" + + tr("Files checked") + u" - " + tr("Torrent will stop after files are initially checked.") + + u" " + tr("This will also download metadata if it wasn't there initially.") + u"

"); + m_ui->stopConditionComboBox->setItemData(0, QVariant::fromValue(BitTorrent::Torrent::StopCondition::None)); + m_ui->stopConditionComboBox->setItemData(1, QVariant::fromValue(BitTorrent::Torrent::StopCondition::MetadataReceived)); + m_ui->stopConditionComboBox->setItemData(2, QVariant::fromValue(BitTorrent::Torrent::StopCondition::FilesChecked)); + m_ui->stopConditionComboBox->setCurrentIndex(m_ui->stopConditionComboBox->findData(QVariant::fromValue(session->torrentStopCondition()))); + m_ui->stopConditionLabel->setEnabled(!m_ui->checkStartPaused->isChecked()); + m_ui->stopConditionComboBox->setEnabled(!m_ui->checkStartPaused->isChecked()); + const TorrentFileGuard::AutoDeleteMode autoDeleteMode = TorrentFileGuard::autoDeleteMode(); m_ui->deleteTorrentBox->setChecked(autoDeleteMode != TorrentFileGuard::Never); m_ui->deleteCancelledTorrentBox->setChecked(autoDeleteMode == TorrentFileGuard::Always); @@ -633,6 +646,12 @@ void OptionsDialog::loadDownloadsTabOptions() connect(m_ui->contentLayoutComboBox, qComboBoxCurrentIndexChanged, this, &ThisType::enableApplyButton); connect(m_ui->checkStartPaused, &QAbstractButton::toggled, this, &ThisType::enableApplyButton); + connect(m_ui->checkStartPaused, &QAbstractButton::toggled, this, [this](const bool checked) + { + m_ui->stopConditionLabel->setEnabled(!checked); + m_ui->stopConditionComboBox->setEnabled(!checked); + }); + connect(m_ui->stopConditionComboBox, qComboBoxCurrentIndexChanged, this, &ThisType::enableApplyButton); connect(m_ui->deleteTorrentBox, &QGroupBox::toggled, this, &ThisType::enableApplyButton); connect(m_ui->deleteCancelledTorrentBox, &QAbstractButton::toggled, this, &ThisType::enableApplyButton); @@ -692,6 +711,7 @@ void OptionsDialog::saveDownloadsTabOptions() const session->setTorrentContentLayout(static_cast(m_ui->contentLayoutComboBox->currentIndex())); session->setAddTorrentPaused(addTorrentsInPause()); + session->setTorrentStopCondition(m_ui->stopConditionComboBox->currentData().value()); TorrentFileGuard::setAutoDeleteMode(!m_ui->deleteTorrentBox->isChecked() ? TorrentFileGuard::Never : !m_ui->deleteCancelledTorrentBox->isChecked() ? TorrentFileGuard::IfAdded : TorrentFileGuard::Always); diff --git a/src/gui/optionsdialog.ui b/src/gui/optionsdialog.ui index 284107c85..e85f0e1cc 100644 --- a/src/gui/optionsdialog.ui +++ b/src/gui/optionsdialog.ui @@ -839,6 +839,52 @@ + + + + + + Torrent stop condition: + + + + + + + 0 + + + + None + + + + + Metadata received + + + + + Files checked + + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + @@ -3555,6 +3601,7 @@ Use ';' to split multiple entries. Can use wildcard '*'. checkUseCustomTheme customThemeFilePath checkStartPaused + stopConditionComboBox spinPort checkUPnP textWebUiUsername diff --git a/src/webui/api/torrentscontroller.cpp b/src/webui/api/torrentscontroller.cpp index 192cecac4..72483e09d 100644 --- a/src/webui/api/torrentscontroller.cpp +++ b/src/webui/api/torrentscontroller.cpp @@ -665,6 +665,11 @@ void TorrentsController::addAction() const int seedingTimeLimit = parseInt(params()[u"seedingTimeLimit"_qs]).value_or(BitTorrent::Torrent::USE_GLOBAL_SEEDING_TIME); const std::optional autoTMM = parseBool(params()[u"autoTMM"_qs]); + const QString stopConditionParam = params()[u"stopCondition"_qs]; + const std::optional stopCondition = (!stopConditionParam.isEmpty() + ? Utils::String::toEnum(stopConditionParam, BitTorrent::Torrent::StopCondition::None) + : std::optional {}); + const QString contentLayoutParam = params()[u"contentLayout"_qs]; const std::optional contentLayout = (!contentLayoutParam.isEmpty() ? Utils::String::toEnum(contentLayoutParam, BitTorrent::TorrentContentLayout::Original) @@ -693,6 +698,7 @@ void TorrentsController::addAction() addTorrentParams.sequential = seqDownload; addTorrentParams.firstLastPiecePriority = firstLastPiece; addTorrentParams.addPaused = addPaused; + addTorrentParams.stopCondition = stopCondition; addTorrentParams.contentLayout = contentLayout; addTorrentParams.savePath = Path(savepath); addTorrentParams.downloadPath = Path(downloadPath); diff --git a/src/webui/webapplication.h b/src/webui/webapplication.h index f5b0034fd..cd89f6011 100644 --- a/src/webui/webapplication.h +++ b/src/webui/webapplication.h @@ -52,7 +52,7 @@ #include "base/utils/version.h" #include "api/isessionmanager.h" -inline const Utils::Version<3, 2> API_VERSION {2, 8, 14}; +inline const Utils::Version<3, 2> API_VERSION {2, 8, 15}; class APIController; class AuthController;