Browse Source

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.
adaptive-webui-19844
Vladimir Golovnev 2 years ago committed by GitHub
parent
commit
67357e9964
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 1
      src/base/bittorrent/addtorrentparams.h
  2. 20
      src/base/bittorrent/bencoderesumedatastorage.cpp
  3. 34
      src/base/bittorrent/dbresumedatastorage.cpp
  4. 2
      src/base/bittorrent/dbresumedatastorage.h
  5. 1
      src/base/bittorrent/loadtorrentparams.h
  6. 21
      src/base/bittorrent/nativetorrentextension.cpp
  7. 1
      src/base/bittorrent/nativetorrentextension.h
  8. 2
      src/base/bittorrent/session.h
  9. 12
      src/base/bittorrent/sessionimpl.cpp
  10. 3
      src/base/bittorrent/sessionimpl.h
  11. 13
      src/base/bittorrent/torrent.h
  12. 67
      src/base/bittorrent/torrentimpl.cpp
  13. 4
      src/base/bittorrent/torrentimpl.h
  14. 19
      src/gui/addnewtorrentdialog.cpp
  15. 73
      src/gui/addnewtorrentdialog.ui
  16. 20
      src/gui/optionsdialog.cpp
  17. 47
      src/gui/optionsdialog.ui
  18. 6
      src/webui/api/torrentscontroller.cpp
  19. 2
      src/webui/webapplication.h

1
src/base/bittorrent/addtorrentparams.h

@ -55,6 +55,7 @@ namespace BitTorrent
bool firstLastPiecePriority = false; bool firstLastPiecePriority = false;
bool addForced = false; bool addForced = false;
std::optional<bool> addPaused; std::optional<bool> addPaused;
std::optional<Torrent::StopCondition> stopCondition;
PathList filePaths; // used if TorrentInfo is set PathList filePaths; // used if TorrentInfo is set
QVector<DownloadPriority> filePriorities; // used if TorrentInfo is set QVector<DownloadPriority> filePriorities; // used if TorrentInfo is set
bool skipChecking = false; bool skipChecking = false;

20
src/base/bittorrent/bencoderesumedatastorage.cpp

@ -245,6 +245,9 @@ BitTorrent::LoadResumeDataResult BitTorrent::BencodeResumeDataStorage::loadTorre
// fromLTString(root.dict_find_string_value("qBt-contentLayout")), TorrentContentLayout::Default); // fromLTString(root.dict_find_string_value("qBt-contentLayout")), TorrentContentLayout::Default);
// === END REPLACEMENT CODE === // // === 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"); const lt::string_view ratioLimitString = resumeDataRoot.dict_find_string_value("qBt-ratioLimit");
if (ratioLimitString.empty()) if (ratioLimitString.empty())
torrentParams.ratioLimit = resumeDataRoot.dict_find_int_value("qBt-ratioLimit", Torrent::USE_GLOBAL_RATIO * 1000) / 1000.0; 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( p.save_path = Profile::instance()->fromPortablePath(
Path(fromLTString(p.save_path))).toString().toStdString(); Path(fromLTString(p.save_path))).toString().toStdString();
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.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) torrentParams.operatingMode = (p.flags & lt::torrent_flags::paused) || (p.flags & lt::torrent_flags::auto_managed)
? TorrentOperatingMode::AutoManaged : TorrentOperatingMode::Forced; ? TorrentOperatingMode::AutoManaged : TorrentOperatingMode::Forced;
if (p.flags & lt::torrent_flags::stop_when_ready)
{
p.flags &= ~lt::torrent_flags::stop_when_ready;
torrentParams.stopCondition = Torrent::StopCondition::FilesChecked;
} }
const bool hasMetadata = (p.ti && p.ti->is_valid()); 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-seedStatus"] = resumeData.hasSeedStatus;
data["qBt-contentLayout"] = Utils::String::fromEnum(resumeData.contentLayout).toStdString(); data["qBt-contentLayout"] = Utils::String::fromEnum(resumeData.contentLayout).toStdString();
data["qBt-firstLastPiecePriority"] = resumeData.firstLastPiecePriority; data["qBt-firstLastPiecePriority"] = resumeData.firstLastPiecePriority;
data["qBt-stopCondition"] = Utils::String::fromEnum(resumeData.stopCondition).toStdString();
if (!resumeData.useAutoTMM) if (!resumeData.useAutoTMM)
{ {

34
src/base/bittorrent/dbresumedatastorage.cpp

@ -61,7 +61,7 @@ namespace
{ {
const QString DB_CONNECTION_NAME = u"ResumeDataStorage"_qs; 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_META = u"meta"_qs;
const QString DB_TABLE_TORRENTS = u"torrents"_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_HAS_SEED_STATUS = makeColumn("has_seed_status");
const Column DB_COLUMN_OPERATING_MODE = makeColumn("operating_mode"); const Column DB_COLUMN_OPERATING_MODE = makeColumn("operating_mode");
const Column DB_COLUMN_STOPPED = makeColumn("stopped"); 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_RESUMEDATA = makeColumn("libtorrent_resume_data");
const Column DB_COLUMN_METADATA = makeColumn("metadata"); const Column DB_COLUMN_METADATA = makeColumn("metadata");
const Column DB_COLUMN_VALUE = makeColumn("value"); const Column DB_COLUMN_VALUE = makeColumn("value");
@ -213,6 +214,8 @@ namespace BitTorrent
resumeData.operatingMode = Utils::String::toEnum<TorrentOperatingMode>( resumeData.operatingMode = Utils::String::toEnum<TorrentOperatingMode>(
query.value(DB_COLUMN_OPERATING_MODE.name).toString(), TorrentOperatingMode::AutoManaged); query.value(DB_COLUMN_OPERATING_MODE.name).toString(), TorrentOperatingMode::AutoManaged);
resumeData.stopped = query.value(DB_COLUMN_STOPPED.name).toBool(); 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( resumeData.savePath = Profile::instance()->fromPortablePath(
Path(query.value(DB_COLUMN_TARGET_SAVE_PATH.name).toString())); 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))) p.save_path = Profile::instance()->fromPortablePath(Path(fromLTString(p.save_path)))
.toString().toStdString(); .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; return resumeData;
} }
} }
@ -263,9 +272,9 @@ BitTorrent::DBResumeDataStorage::DBResumeDataStorage(const Path &dbPath, QObject
} }
else else
{ {
const int dbVersion = currentDBVersion(); const int dbVersion = (!db.record(DB_TABLE_TORRENTS).contains(DB_COLUMN_DOWNLOAD_PATH.name) ? 1 : currentDBVersion());
if ((dbVersion == 1) || !db.record(DB_TABLE_TORRENTS).contains(DB_COLUMN_DOWNLOAD_PATH.name)) if (dbVersion != DB_VERSION)
updateDBFromVersion1(); updateDB(dbVersion);
} }
m_asyncWorker = new Worker(dbPath, u"ResumeDataStorageWorker"_qs, m_dbLock); 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); auto db = QSqlDatabase::database(DB_CONNECTION_NAME);
const QWriteLocker locker {&m_dbLock}; const QWriteLocker locker {&m_dbLock};
@ -519,11 +531,22 @@ void BitTorrent::DBResumeDataStorage::updateDBFromVersion1() const
QSqlQuery query {db}; QSqlQuery query {db};
try try
{
if (fromVersion == 1)
{ {
const auto alterTableTorrentsQuery = u"ALTER TABLE %1 ADD %2"_qs const auto alterTableTorrentsQuery = u"ALTER TABLE %1 ADD %2"_qs
.arg(quoted(DB_TABLE_TORRENTS), makeColumnDefinition(DB_COLUMN_DOWNLOAD_PATH, "TEXT")); .arg(quoted(DB_TABLE_TORRENTS), makeColumnDefinition(DB_COLUMN_DOWNLOAD_PATH, "TEXT"));
if (!query.exec(alterTableTorrentsQuery)) if (!query.exec(alterTableTorrentsQuery))
throw RuntimeError(query.lastError().text()); 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}); const QString updateMetaVersionQuery = makeUpdateStatement(DB_TABLE_META, {DB_COLUMN_NAME, DB_COLUMN_VALUE});
if (!query.prepare(updateMetaVersionQuery)) 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_HAS_SEED_STATUS.placeholder, resumeData.hasSeedStatus);
query.bindValue(DB_COLUMN_OPERATING_MODE.placeholder, Utils::String::fromEnum(resumeData.operatingMode)); query.bindValue(DB_COLUMN_OPERATING_MODE.placeholder, Utils::String::fromEnum(resumeData.operatingMode));
query.bindValue(DB_COLUMN_STOPPED.placeholder, resumeData.stopped); query.bindValue(DB_COLUMN_STOPPED.placeholder, resumeData.stopped);
query.bindValue(DB_COLUMN_STOP_CONDITION.placeholder, Utils::String::fromEnum(resumeData.stopCondition));
if (!resumeData.useAutoTMM) if (!resumeData.useAutoTMM)
{ {

2
src/base/bittorrent/dbresumedatastorage.h

@ -57,7 +57,7 @@ namespace BitTorrent
void doLoadAll() const override; void doLoadAll() const override;
int currentDBVersion() const; int currentDBVersion() const;
void createDB() const; void createDB() const;
void updateDBFromVersion1() const; void updateDB(int fromVersion) const;
QThread *m_ioThread = nullptr; QThread *m_ioThread = nullptr;

1
src/base/bittorrent/loadtorrentparams.h

@ -54,6 +54,7 @@ namespace BitTorrent
bool firstLastPiecePriority = false; bool firstLastPiecePriority = false;
bool hasSeedStatus = false; bool hasSeedStatus = false;
bool stopped = false; bool stopped = false;
Torrent::StopCondition stopCondition;
qreal ratioLimit = Torrent::USE_GLOBAL_RATIO; qreal ratioLimit = Torrent::USE_GLOBAL_RATIO;
int seedingTimeLimit = Torrent::USE_GLOBAL_SEEDING_TIME; int seedingTimeLimit = Torrent::USE_GLOBAL_SEEDING_TIME;

21
src/base/bittorrent/nativetorrentextension.cpp

@ -30,14 +30,6 @@
#include <libtorrent/torrent_status.hpp> #include <libtorrent/torrent_status.hpp>
namespace
{
bool isAutoManaged(const lt::torrent_status &torrentStatus)
{
return static_cast<bool>(torrentStatus.flags & lt::torrent_flags::auto_managed);
}
}
NativeTorrentExtension::NativeTorrentExtension(const lt::torrent_handle &torrentHandle, ExtensionData *data) NativeTorrentExtension::NativeTorrentExtension(const lt::torrent_handle &torrentHandle, ExtensionData *data)
: m_torrentHandle {torrentHandle} : m_torrentHandle {torrentHandle}
, m_data {data} , m_data {data}
@ -65,19 +57,10 @@ NativeTorrentExtension::~NativeTorrentExtension()
delete m_data; 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) 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.unset_flags(lt::torrent_flags::auto_managed);
m_torrentHandle.pause(); m_torrentHandle.pause();

1
src/base/bittorrent/nativetorrentextension.h

@ -40,7 +40,6 @@ public:
~NativeTorrentExtension(); ~NativeTorrentExtension();
private: private:
bool on_pause() override;
void on_state(lt::torrent_status::state_t state) override; void on_state(lt::torrent_status::state_t state) override;
lt::torrent_handle m_torrentHandle; lt::torrent_handle m_torrentHandle;

2
src/base/bittorrent/session.h

@ -208,6 +208,8 @@ namespace BitTorrent
virtual void setPeXEnabled(bool enabled) = 0; virtual void setPeXEnabled(bool enabled) = 0;
virtual bool isAddTorrentPaused() const = 0; virtual bool isAddTorrentPaused() const = 0;
virtual void setAddTorrentPaused(bool value) = 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 TorrentContentLayout torrentContentLayout() const = 0;
virtual void setTorrentContentLayout(TorrentContentLayout value) = 0; virtual void setTorrentContentLayout(TorrentContentLayout value) = 0;
virtual bool isTrackerEnabled() const = 0; virtual bool isTrackerEnabled() const = 0;

12
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_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_globalMaxSeedingMinutes(BITTORRENT_SESSION_KEY(u"GlobalMaxSeedingMinutes"_qs), -1, lowerLimited(-1))
, m_isAddTorrentPaused(BITTORRENT_SESSION_KEY(u"AddTorrentPaused"_qs), false) , 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_torrentContentLayout(BITTORRENT_SESSION_KEY(u"TorrentContentLayout"_qs), TorrentContentLayout::Original)
, m_isAppendExtensionEnabled(BITTORRENT_SESSION_KEY(u"AddExtensionToIncompleteFiles"_qs), false) , m_isAppendExtensionEnabled(BITTORRENT_SESSION_KEY(u"AddExtensionToIncompleteFiles"_qs), false)
, m_refreshInterval(BITTORRENT_SESSION_KEY(u"RefreshInterval"_qs), 1500) , m_refreshInterval(BITTORRENT_SESSION_KEY(u"RefreshInterval"_qs), 1500)
@ -950,6 +951,16 @@ void SessionImpl::setAddTorrentPaused(const bool value)
m_isAddTorrentPaused = 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 bool SessionImpl::isTrackerEnabled() const
{ {
return m_isTrackerEnabled; return m_isTrackerEnabled;
@ -2497,6 +2508,7 @@ LoadTorrentParams SessionImpl::initLoadTorrentParams(const AddTorrentParams &add
loadTorrentParams.contentLayout = addTorrentParams.contentLayout.value_or(torrentContentLayout()); loadTorrentParams.contentLayout = addTorrentParams.contentLayout.value_or(torrentContentLayout());
loadTorrentParams.operatingMode = (addTorrentParams.addForced ? TorrentOperatingMode::Forced : TorrentOperatingMode::AutoManaged); loadTorrentParams.operatingMode = (addTorrentParams.addForced ? TorrentOperatingMode::Forced : TorrentOperatingMode::AutoManaged);
loadTorrentParams.stopped = addTorrentParams.addPaused.value_or(isAddTorrentPaused()); loadTorrentParams.stopped = addTorrentParams.addPaused.value_or(isAddTorrentPaused());
loadTorrentParams.stopCondition = addTorrentParams.stopCondition.value_or(torrentStopCondition());
loadTorrentParams.ratioLimit = addTorrentParams.ratioLimit; loadTorrentParams.ratioLimit = addTorrentParams.ratioLimit;
loadTorrentParams.seedingTimeLimit = addTorrentParams.seedingTimeLimit; loadTorrentParams.seedingTimeLimit = addTorrentParams.seedingTimeLimit;

3
src/base/bittorrent/sessionimpl.h

@ -188,6 +188,8 @@ namespace BitTorrent
void setPeXEnabled(bool enabled) override; void setPeXEnabled(bool enabled) override;
bool isAddTorrentPaused() const override; bool isAddTorrentPaused() const override;
void setAddTorrentPaused(bool value) override; void setAddTorrentPaused(bool value) override;
Torrent::StopCondition torrentStopCondition() const override;
void setTorrentStopCondition(Torrent::StopCondition stopCondition) override;
TorrentContentLayout torrentContentLayout() const override; TorrentContentLayout torrentContentLayout() const override;
void setTorrentContentLayout(TorrentContentLayout value) override; void setTorrentContentLayout(TorrentContentLayout value) override;
bool isTrackerEnabled() const override; bool isTrackerEnabled() const override;
@ -620,6 +622,7 @@ namespace BitTorrent
CachedSettingValue<qreal> m_globalMaxRatio; CachedSettingValue<qreal> m_globalMaxRatio;
CachedSettingValue<int> m_globalMaxSeedingMinutes; CachedSettingValue<int> m_globalMaxSeedingMinutes;
CachedSettingValue<bool> m_isAddTorrentPaused; CachedSettingValue<bool> m_isAddTorrentPaused;
CachedSettingValue<Torrent::StopCondition> m_torrentStopCondition;
CachedSettingValue<TorrentContentLayout> m_torrentContentLayout; CachedSettingValue<TorrentContentLayout> m_torrentContentLayout;
CachedSettingValue<bool> m_isAppendExtensionEnabled; CachedSettingValue<bool> m_isAppendExtensionEnabled;
CachedSettingValue<int> m_refreshInterval; CachedSettingValue<int> m_refreshInterval;

13
src/base/bittorrent/torrent.h

@ -108,7 +108,17 @@ namespace BitTorrent
class Torrent : public AbstractFileStorage class Torrent : public AbstractFileStorage
{ {
Q_GADGET
public: public:
enum class StopCondition
{
None = 0,
MetadataReceived = 1,
FilesChecked = 2
};
Q_ENUM(StopCondition)
static const qreal USE_GLOBAL_RATIO; static const qreal USE_GLOBAL_RATIO;
static const qreal NO_RATIO_LIMIT; static const qreal NO_RATIO_LIMIT;
@ -300,6 +310,9 @@ namespace BitTorrent
virtual void clearPeers() = 0; virtual void clearPeers() = 0;
virtual bool setMetadata(const TorrentInfo &torrentInfo) = 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 QString createMagnetURI() const = 0;
virtual nonstd::expected<QByteArray, QString> exportToBuffer() const = 0; virtual nonstd::expected<QByteArray, QString> exportToBuffer() const = 0;
virtual nonstd::expected<void, QString> exportToFile(const Path &path) const = 0; virtual nonstd::expected<void, QString> exportToFile(const Path &path) const = 0;

67
src/base/bittorrent/torrentimpl.cpp

@ -295,6 +295,8 @@ TorrentImpl::TorrentImpl(SessionImpl *session, lt::session *nativeSession
} }
} }
setStopCondition(params.stopCondition);
const auto *extensionData = static_cast<ExtensionData *>(m_ltAddTorrentParams.userdata); const auto *extensionData = static_cast<ExtensionData *>(m_ltAddTorrentParams.userdata);
m_trackerEntries.reserve(static_cast<decltype(m_trackerEntries)::size_type>(extensionData->trackers.size())); m_trackerEntries.reserve(static_cast<decltype(m_trackerEntries)::size_type>(extensionData->trackers.size()));
for (const lt::announce_entry &announceEntry : extensionData->trackers) for (const lt::announce_entry &announceEntry : extensionData->trackers)
@ -993,9 +995,7 @@ void TorrentImpl::updateState()
else else
m_state = isForced() ? TorrentState::ForcedDownloadingMetadata : TorrentState::DownloadingMetadata; m_state = isForced() ? TorrentState::ForcedDownloadingMetadata : TorrentState::DownloadingMetadata;
} }
else if ((m_nativeStatus.state == lt::torrent_status::checking_files) else if ((m_nativeStatus.state == lt::torrent_status::checking_files) && !isPaused())
&& (!isPaused() || (m_nativeStatus.flags & lt::torrent_flags::auto_managed)
|| !(m_nativeStatus.flags & lt::torrent_flags::paused)))
{ {
// If the torrent is not just in the "checking" state, but is being actually checked // If the torrent is not just in the "checking" state, but is being actually checked
m_state = m_hasSeedStatus ? TorrentState::CheckingUploading : TorrentState::CheckingDownloading; m_state = m_hasSeedStatus ? TorrentState::CheckingUploading : TorrentState::CheckingDownloading;
@ -1423,8 +1423,8 @@ void TorrentImpl::forceRecheck()
if (isPaused()) if (isPaused())
{ {
// When "force recheck" is applied on paused torrent, we temporarily resume it // 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). resume();
m_nativeHandle.set_flags(lt::torrent_flags::stop_when_ready | lt::torrent_flags::auto_managed); setStopCondition(StopCondition::FilesChecked);
} }
} }
@ -1581,6 +1581,17 @@ void TorrentImpl::endReceivedMetadataHandling(const Path &savePath, const PathLi
p.save_path = savePath.toString().toStdString(); p.save_path = savePath.toString().toStdString();
p.ti = metadata; 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(); reload();
// If first/last piece priority was specified when adding this torrent, // If first/last piece priority was specified when adding this torrent,
@ -1639,6 +1650,7 @@ void TorrentImpl::pause()
{ {
if (!m_isStopped) if (!m_isStopped)
{ {
m_stopCondition = StopCondition::None;
m_isStopped = true; m_isStopped = true;
m_session->handleTorrentNeedSaveResumeData(this); m_session->handleTorrentNeedSaveResumeData(this);
m_session->handleTorrentPaused(this); m_session->handleTorrentPaused(this);
@ -1674,10 +1686,6 @@ void TorrentImpl::resume(const TorrentOperatingMode mode)
if (m_isStopped) 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_isStopped = false;
m_session->handleTorrentNeedSaveResumeData(this); m_session->handleTorrentNeedSaveResumeData(this);
m_session->handleTorrentResumed(this); m_session->handleTorrentResumed(this);
@ -1756,13 +1764,13 @@ void TorrentImpl::handleTorrentCheckedAlert(const lt::torrent_checked_alert *p)
return; return;
} }
if (stopCondition() == StopCondition::FilesChecked)
pause();
m_statusUpdatedTriggers.enqueue([this]() m_statusUpdatedTriggers.enqueue([this]()
{ {
qDebug("\"%s\" have just finished checking.", qUtf8Printable(name())); qDebug("\"%s\" have just finished checking.", qUtf8Printable(name()));
if (m_nativeStatus.need_save_resume)
m_session->handleTorrentNeedSaveResumeData(this);
if (!m_hasMissingFiles) if (!m_hasMissingFiles)
{ {
if ((progress() < 1.0) && (wantedSize() > 0)) if ((progress() < 1.0) && (wantedSize() > 0))
@ -1772,8 +1780,20 @@ void TorrentImpl::handleTorrentCheckedAlert(const lt::torrent_checked_alert *p)
adjustStorageLocation(); adjustStorageLocation();
manageIncompleteFiles(); 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); m_session->handleTorrentChecked(this);
}); });
} }
@ -1909,6 +1929,7 @@ void TorrentImpl::prepareResumeData(const lt::add_torrent_params &params)
resumeData.firstLastPiecePriority = m_hasFirstLastPiecePriority; resumeData.firstLastPiecePriority = m_hasFirstLastPiecePriority;
resumeData.hasSeedStatus = m_hasSeedStatus; resumeData.hasSeedStatus = m_hasSeedStatus;
resumeData.stopped = m_isStopped; resumeData.stopped = m_isStopped;
resumeData.stopCondition = m_stopCondition;
resumeData.operatingMode = m_operatingMode; resumeData.operatingMode = m_operatingMode;
resumeData.ltAddTorrentParams = m_ltAddTorrentParams; resumeData.ltAddTorrentParams = m_ltAddTorrentParams;
resumeData.useAutoTMM = m_useAutoTMM; resumeData.useAutoTMM = m_useAutoTMM;
@ -2166,6 +2187,28 @@ bool TorrentImpl::setMetadata(const TorrentInfo &torrentInfo)
#endif #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 bool TorrentImpl::isMoveInProgress() const
{ {
return m_storageIsMoving; return m_storageIsMoving;

4
src/base/bittorrent/torrentimpl.h

@ -227,6 +227,9 @@ namespace BitTorrent
void clearPeers() override; void clearPeers() override;
bool setMetadata(const TorrentInfo &torrentInfo) override; bool setMetadata(const TorrentInfo &torrentInfo) override;
StopCondition stopCondition() const override;
void setStopCondition(StopCondition stopCondition) override;
QString createMagnetURI() const override; QString createMagnetURI() const override;
nonstd::expected<QByteArray, QString> exportToBuffer() const override; nonstd::expected<QByteArray, QString> exportToBuffer() const override;
nonstd::expected<void, QString> exportToFile(const Path &path) const override; nonstd::expected<void, QString> exportToFile(const Path &path) const override;
@ -332,6 +335,7 @@ namespace BitTorrent
bool m_hasFirstLastPiecePriority = false; bool m_hasFirstLastPiecePriority = false;
bool m_useAutoTMM; bool m_useAutoTMM;
bool m_isStopped; bool m_isStopped;
StopCondition m_stopCondition;
bool m_unchecked = false; bool m_unchecked = false;

19
src/gui/addnewtorrentdialog.cpp

@ -218,6 +218,24 @@ AddNewTorrentDialog::AddNewTorrentDialog(const BitTorrent::AddTorrentParams &inP
m_ui->downloadPath->setMaxVisibleItems(20); m_ui->downloadPath->setMaxVisibleItems(20);
m_ui->startTorrentCheckBox->setChecked(!m_torrentParams.addPaused.value_or(session->isAddTorrentPaused())); m_ui->startTorrentCheckBox->setChecked(!m_torrentParams.addPaused.value_or(session->isAddTorrentPaused()));
m_ui->stopConditionComboBox->setToolTip(
u"<html><body><p><b>" + tr("None") + u"</b> - " + tr("No stop condition is set.") + u"</p><p><b>" +
tr("Metadata received") + u"</b> - " + tr("Torrent will stop after metadata is received.") +
u" <em>" + tr("Torrents that have metadata initially aren't affected.") + u"</em></p><p><b>" +
tr("Files checked") + u"</b> - " + tr("Torrent will stop after files are initially checked.") +
u" <em>" + tr("This will also download metadata if it wasn't there initially.") + u"</em></p></body></html>");
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->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); m_ui->comboTTM->setCurrentIndex(session->isAutoTMMDisabledByDefault() ? 0 : 1);
@ -872,6 +890,7 @@ void AddNewTorrentDialog::accept()
m_torrentParams.filePriorities = m_contentModel->model()->getFilePriorities(); m_torrentParams.filePriorities = m_contentModel->model()->getFilePriorities();
m_torrentParams.addPaused = !m_ui->startTorrentCheckBox->isChecked(); m_torrentParams.addPaused = !m_ui->startTorrentCheckBox->isChecked();
m_torrentParams.stopCondition = m_ui->stopConditionComboBox->currentData().value<BitTorrent::Torrent::StopCondition>();
m_torrentParams.contentLayout = static_cast<BitTorrent::TorrentContentLayout>(m_ui->contentLayoutComboBox->currentIndex()); m_torrentParams.contentLayout = static_cast<BitTorrent::TorrentContentLayout>(m_ui->contentLayoutComboBox->currentIndex());
m_torrentParams.sequential = m_ui->sequentialCheckBox->isChecked(); m_torrentParams.sequential = m_ui->sequentialCheckBox->isChecked();

73
src/gui/addnewtorrentdialog.ui

@ -203,44 +203,46 @@
</item> </item>
<item> <item>
<layout class="QGridLayout" name="gridLayout"> <layout class="QGridLayout" name="gridLayout">
<item row="2" column="0"> <item row="0" column="0">
<widget class="QCheckBox" name="doNotDeleteTorrentCheckBox"> <widget class="QCheckBox" name="startTorrentCheckBox">
<property name="toolTip">
<string>When checked, the .torrent file will not be deleted regardless of the settings at the &quot;Download&quot; page of the Options dialog</string>
</property>
<property name="text"> <property name="text">
<string>Do not delete .torrent file</string> <string>Start torrent</string>
</property> </property>
</widget> </widget>
</item> </item>
<item row="1" column="1"> <item row="0" column="1">
<widget class="QCheckBox" name="firstLastCheckBox"> <layout class="QHBoxLayout" name="stopConditionLayout">
<item>
<widget class="QLabel" name="stopConditionLabel">
<property name="text"> <property name="text">
<string>Download first and last pieces first</string> <string>Stop condition:</string>
</property> </property>
</widget> </widget>
</item> </item>
<item row="1" column="0"> <item>
<widget class="QCheckBox" name="skipCheckingCheckBox"> <widget class="QComboBox" name="stopConditionComboBox">
<property name="currentIndex">
<number>0</number>
</property>
<item>
<property name="text"> <property name="text">
<string>Skip hash check</string> <string>None</string>
</property> </property>
</widget>
</item> </item>
<item row="0" column="1"> <item>
<widget class="QCheckBox" name="sequentialCheckBox">
<property name="text"> <property name="text">
<string>Download in sequential order</string> <string>Metadata received</string>
</property> </property>
</widget>
</item> </item>
<item row="0" column="0"> <item>
<widget class="QCheckBox" name="startTorrentCheckBox">
<property name="text"> <property name="text">
<string>Start torrent</string> <string>Files checked</string>
</property> </property>
</item>
</widget> </widget>
</item> </item>
</layout>
</item>
<item row="0" column="2"> <item row="0" column="2">
<spacer name="horizontalSpacer_3"> <spacer name="horizontalSpacer_3">
<property name="orientation"> <property name="orientation">
@ -254,6 +256,37 @@
</property> </property>
</spacer> </spacer>
</item> </item>
<item row="1" column="0">
<widget class="QCheckBox" name="skipCheckingCheckBox">
<property name="text">
<string>Skip hash check</string>
</property>
</widget>
</item>
<item row="2" column="0">
<widget class="QCheckBox" name="sequentialCheckBox">
<property name="text">
<string>Download in sequential order</string>
</property>
</widget>
</item>
<item row="2" column="1">
<widget class="QCheckBox" name="firstLastCheckBox">
<property name="text">
<string>Download first and last pieces first</string>
</property>
</widget>
</item>
<item row="3" column="0">
<widget class="QCheckBox" name="doNotDeleteTorrentCheckBox">
<property name="toolTip">
<string>When checked, the .torrent file will not be deleted regardless of the settings at the &quot;Download&quot; page of the Options dialog</string>
</property>
<property name="text">
<string>Do not delete .torrent file</string>
</property>
</widget>
</item>
</layout> </layout>
</item> </item>
<item> <item>

20
src/gui/optionsdialog.cpp

@ -525,6 +525,19 @@ void OptionsDialog::loadDownloadsTabOptions()
m_ui->contentLayoutComboBox->setCurrentIndex(static_cast<int>(session->torrentContentLayout())); m_ui->contentLayoutComboBox->setCurrentIndex(static_cast<int>(session->torrentContentLayout()));
m_ui->checkStartPaused->setChecked(session->isAddTorrentPaused()); m_ui->checkStartPaused->setChecked(session->isAddTorrentPaused());
m_ui->stopConditionComboBox->setToolTip(
u"<html><body><p><b>" + tr("None") + u"</b> - " + tr("No stop condition is set.") + u"</p><p><b>" +
tr("Metadata received") + u"</b> - " + tr("Torrent will stop after metadata is received.") +
u" <em>" + tr("Torrents that have metadata initially aren't affected.") + u"</em></p><p><b>" +
tr("Files checked") + u"</b> - " + tr("Torrent will stop after files are initially checked.") +
u" <em>" + tr("This will also download metadata if it wasn't there initially.") + u"</em></p></body></html>");
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(); const TorrentFileGuard::AutoDeleteMode autoDeleteMode = TorrentFileGuard::autoDeleteMode();
m_ui->deleteTorrentBox->setChecked(autoDeleteMode != TorrentFileGuard::Never); m_ui->deleteTorrentBox->setChecked(autoDeleteMode != TorrentFileGuard::Never);
m_ui->deleteCancelledTorrentBox->setChecked(autoDeleteMode == TorrentFileGuard::Always); 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->contentLayoutComboBox, qComboBoxCurrentIndexChanged, this, &ThisType::enableApplyButton);
connect(m_ui->checkStartPaused, &QAbstractButton::toggled, 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->deleteTorrentBox, &QGroupBox::toggled, this, &ThisType::enableApplyButton);
connect(m_ui->deleteCancelledTorrentBox, &QAbstractButton::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<BitTorrent::TorrentContentLayout>(m_ui->contentLayoutComboBox->currentIndex())); session->setTorrentContentLayout(static_cast<BitTorrent::TorrentContentLayout>(m_ui->contentLayoutComboBox->currentIndex()));
session->setAddTorrentPaused(addTorrentsInPause()); session->setAddTorrentPaused(addTorrentsInPause());
session->setTorrentStopCondition(m_ui->stopConditionComboBox->currentData().value<BitTorrent::Torrent::StopCondition>());
TorrentFileGuard::setAutoDeleteMode(!m_ui->deleteTorrentBox->isChecked() ? TorrentFileGuard::Never TorrentFileGuard::setAutoDeleteMode(!m_ui->deleteTorrentBox->isChecked() ? TorrentFileGuard::Never
: !m_ui->deleteCancelledTorrentBox->isChecked() ? TorrentFileGuard::IfAdded : !m_ui->deleteCancelledTorrentBox->isChecked() ? TorrentFileGuard::IfAdded
: TorrentFileGuard::Always); : TorrentFileGuard::Always);

47
src/gui/optionsdialog.ui

@ -839,6 +839,52 @@
</property> </property>
</widget> </widget>
</item> </item>
<item>
<layout class="QHBoxLayout" name="stopConditionLayout">
<item>
<widget class="QLabel" name="stopConditionLabel">
<property name="text">
<string>Torrent stop condition:</string>
</property>
</widget>
</item>
<item>
<widget class="QComboBox" name="stopConditionComboBox">
<property name="currentIndex">
<number>0</number>
</property>
<item>
<property name="text">
<string>None</string>
</property>
</item>
<item>
<property name="text">
<string>Metadata received</string>
</property>
</item>
<item>
<property name="text">
<string>Files checked</string>
</property>
</item>
</widget>
</item>
<item>
<spacer name="stopConditionSpacer">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
</layout>
</item>
<item> <item>
<widget class="QGroupBox" name="deleteTorrentBox"> <widget class="QGroupBox" name="deleteTorrentBox">
<property name="toolTip"> <property name="toolTip">
@ -3555,6 +3601,7 @@ Use ';' to split multiple entries. Can use wildcard '*'.</string>
<tabstop>checkUseCustomTheme</tabstop> <tabstop>checkUseCustomTheme</tabstop>
<tabstop>customThemeFilePath</tabstop> <tabstop>customThemeFilePath</tabstop>
<tabstop>checkStartPaused</tabstop> <tabstop>checkStartPaused</tabstop>
<tabstop>stopConditionComboBox</tabstop>
<tabstop>spinPort</tabstop> <tabstop>spinPort</tabstop>
<tabstop>checkUPnP</tabstop> <tabstop>checkUPnP</tabstop>
<tabstop>textWebUiUsername</tabstop> <tabstop>textWebUiUsername</tabstop>

6
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 int seedingTimeLimit = parseInt(params()[u"seedingTimeLimit"_qs]).value_or(BitTorrent::Torrent::USE_GLOBAL_SEEDING_TIME);
const std::optional<bool> autoTMM = parseBool(params()[u"autoTMM"_qs]); const std::optional<bool> autoTMM = parseBool(params()[u"autoTMM"_qs]);
const QString stopConditionParam = params()[u"stopCondition"_qs];
const std::optional<BitTorrent::Torrent::StopCondition> stopCondition = (!stopConditionParam.isEmpty()
? Utils::String::toEnum(stopConditionParam, BitTorrent::Torrent::StopCondition::None)
: std::optional<BitTorrent::Torrent::StopCondition> {});
const QString contentLayoutParam = params()[u"contentLayout"_qs]; const QString contentLayoutParam = params()[u"contentLayout"_qs];
const std::optional<BitTorrent::TorrentContentLayout> contentLayout = (!contentLayoutParam.isEmpty() const std::optional<BitTorrent::TorrentContentLayout> contentLayout = (!contentLayoutParam.isEmpty()
? Utils::String::toEnum(contentLayoutParam, BitTorrent::TorrentContentLayout::Original) ? Utils::String::toEnum(contentLayoutParam, BitTorrent::TorrentContentLayout::Original)
@ -693,6 +698,7 @@ void TorrentsController::addAction()
addTorrentParams.sequential = seqDownload; addTorrentParams.sequential = seqDownload;
addTorrentParams.firstLastPiecePriority = firstLastPiece; addTorrentParams.firstLastPiecePriority = firstLastPiece;
addTorrentParams.addPaused = addPaused; addTorrentParams.addPaused = addPaused;
addTorrentParams.stopCondition = stopCondition;
addTorrentParams.contentLayout = contentLayout; addTorrentParams.contentLayout = contentLayout;
addTorrentParams.savePath = Path(savepath); addTorrentParams.savePath = Path(savepath);
addTorrentParams.downloadPath = Path(downloadPath); addTorrentParams.downloadPath = Path(downloadPath);

2
src/webui/webapplication.h

@ -52,7 +52,7 @@
#include "base/utils/version.h" #include "base/utils/version.h"
#include "api/isessionmanager.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 APIController;
class AuthController; class AuthController;

Loading…
Cancel
Save