From b0e41abf5a4a2b6abfe074e6d796edce38ec2acb Mon Sep 17 00:00:00 2001 From: "Vladimir Golovnev (Glassez)" Date: Wed, 1 Dec 2021 17:35:21 +0300 Subject: [PATCH 1/2] Allow to set placeholder for FileSystemPathEdit --- src/gui/fspathedit.cpp | 12 ++++++++++++ src/gui/fspathedit.h | 3 +++ src/gui/fspathedit_p.cpp | 20 ++++++++++++++++++++ src/gui/fspathedit_p.h | 6 ++++++ 4 files changed, 41 insertions(+) diff --git a/src/gui/fspathedit.cpp b/src/gui/fspathedit.cpp index be8e15c16..9d36cb84c 100644 --- a/src/gui/fspathedit.cpp +++ b/src/gui/fspathedit.cpp @@ -247,6 +247,18 @@ void FileSystemPathEdit::setFileNameFilter(const QString &val) #endif } +QString FileSystemPathEdit::placeholder() const +{ + Q_D(const FileSystemPathEdit); + return d->m_editor->placeholder(); +} + +void FileSystemPathEdit::setPlaceholder(const QString &val) +{ + Q_D(FileSystemPathEdit); + d->m_editor->setPlaceholder(val); +} + bool FileSystemPathEdit::briefBrowseButtonCaption() const { Q_D(const FileSystemPathEdit); diff --git a/src/gui/fspathedit.h b/src/gui/fspathedit.h index d9f9d5e63..f6ba65cb7 100644 --- a/src/gui/fspathedit.h +++ b/src/gui/fspathedit.h @@ -71,6 +71,9 @@ public: QString fileNameFilter() const; void setFileNameFilter(const QString &val); + QString placeholder() const; + void setPlaceholder(const QString &val); + /// The browse button caption is "..." if true, and "Browse" otherwise bool briefBrowseButtonCaption() const; void setBriefBrowseButtonCaption(bool brief); diff --git a/src/gui/fspathedit_p.cpp b/src/gui/fspathedit_p.cpp index f2c8c294a..5565d1cc2 100644 --- a/src/gui/fspathedit_p.cpp +++ b/src/gui/fspathedit_p.cpp @@ -240,6 +240,16 @@ void Private::FileLineEdit::setValidator(QValidator *validator) QLineEdit::setValidator(validator); } +QString Private::FileLineEdit::placeholder() const +{ + return placeholderText(); +} + +void Private::FileLineEdit::setPlaceholder(const QString &val) +{ + setPlaceholderText(val); +} + QWidget *Private::FileLineEdit::widget() { return this; @@ -346,6 +356,16 @@ void Private::FileComboEdit::setValidator(QValidator *validator) lineEdit()->setValidator(validator); } +QString Private::FileComboEdit::placeholder() const +{ + return lineEdit()->placeholderText(); +} + +void Private::FileComboEdit::setPlaceholder(const QString &val) +{ + lineEdit()->setPlaceholderText(val); +} + void Private::FileComboEdit::setFilenameFilters(const QStringList &filters) { static_cast(lineEdit())->setFilenameFilters(filters); diff --git a/src/gui/fspathedit_p.h b/src/gui/fspathedit_p.h index 8f443b4d2..15b1305dd 100644 --- a/src/gui/fspathedit_p.h +++ b/src/gui/fspathedit_p.h @@ -105,6 +105,8 @@ namespace Private virtual void setFilenameFilters(const QStringList &filters) = 0; virtual void setBrowseAction(QAction *action) = 0; virtual void setValidator(QValidator *validator) = 0; + virtual QString placeholder() const = 0; + virtual void setPlaceholder(const QString &val) = 0; virtual QWidget *widget() = 0; }; @@ -121,6 +123,8 @@ namespace Private void setFilenameFilters(const QStringList &filters) override; void setBrowseAction(QAction *action) override; void setValidator(QValidator *validator) override; + QString placeholder() const override; + void setPlaceholder(const QString &val) override; QWidget *widget() override; protected: @@ -149,6 +153,8 @@ namespace Private void setFilenameFilters(const QStringList &filters) override; void setBrowseAction(QAction *action) override; void setValidator(QValidator *validator) override; + QString placeholder() const override; + void setPlaceholder(const QString &val) override; QWidget *widget() override; protected: From 1c0f8b4289b8d298041fe195cdc24f793dce4df5 Mon Sep 17 00:00:00 2001 From: "Vladimir Golovnev (Glassez)" Date: Thu, 20 May 2021 10:36:44 +0300 Subject: [PATCH 2/2] Redesign "Incomplete folder" feature Change "Incomplete/temp folder" term with "download folder". Allow to set "download folder" per torrent (in manual mode) and per category (in automatic mode). --- src/base/CMakeLists.txt | 2 + src/base/base.pri | 2 + src/base/bittorrent/addtorrentparams.h | 3 +- .../bittorrent/bencoderesumedatastorage.cpp | 18 +- src/base/bittorrent/categoryoptions.cpp | 78 ++++ src/base/bittorrent/categoryoptions.h | 56 +++ src/base/bittorrent/dbresumedatastorage.cpp | 158 ++++++-- src/base/bittorrent/dbresumedatastorage.h | 2 + src/base/bittorrent/filesearcher.cpp | 14 +- src/base/bittorrent/filesearcher.h | 2 +- src/base/bittorrent/loadtorrentparams.h | 2 + src/base/bittorrent/session.cpp | 368 ++++++++++++------ src/base/bittorrent/session.h | 40 +- src/base/bittorrent/torrent.h | 17 +- src/base/bittorrent/torrentimpl.cpp | 199 +++++----- src/base/bittorrent/torrentimpl.h | 25 +- src/base/profile.cpp | 2 - src/base/torrentfileswatcher.cpp | 7 + src/base/torrentfilter.cpp | 4 +- src/base/utils/fs.cpp | 11 +- src/base/utils/fs.h | 10 + src/gui/addnewtorrentdialog.cpp | 243 ++++++++---- src/gui/addnewtorrentdialog.h | 11 +- src/gui/addnewtorrentdialog.ui | 43 +- src/gui/categoryfiltermodel.cpp | 31 +- src/gui/categoryfilterwidget.cpp | 2 +- src/gui/optionsdialog.cpp | 26 +- src/gui/optionsdialog.ui | 46 +-- src/gui/properties/propertieswidget.cpp | 4 +- src/gui/rss/automatedrssdownloader.cpp | 2 +- src/gui/torrentcategorydialog.cpp | 86 +++- src/gui/torrentcategorydialog.h | 16 +- src/gui/torrentcategorydialog.ui | 112 +++++- src/gui/torrentoptionsdialog.cpp | 89 ++++- src/gui/torrentoptionsdialog.h | 3 + src/gui/torrentoptionsdialog.ui | 61 +-- src/gui/transferlistwidget.cpp | 20 +- src/gui/watchedfolderoptionsdialog.cpp | 64 ++- src/gui/watchedfolderoptionsdialog.h | 4 +- src/gui/watchedfolderoptionsdialog.ui | 26 +- src/webui/api/appcontroller.cpp | 14 +- src/webui/api/freediskspacechecker.cpp | 2 +- src/webui/api/serialize/serialize_torrent.cpp | 1 + src/webui/api/serialize/serialize_torrent.h | 1 + src/webui/api/synccontroller.cpp | 16 +- src/webui/api/torrentscontroller.cpp | 109 +++++- src/webui/api/torrentscontroller.h | 2 + src/webui/webapplication.h | 2 +- 48 files changed, 1457 insertions(+), 599 deletions(-) create mode 100644 src/base/bittorrent/categoryoptions.cpp create mode 100644 src/base/bittorrent/categoryoptions.h diff --git a/src/base/CMakeLists.txt b/src/base/CMakeLists.txt index 064e233dc..f4e79d266 100644 --- a/src/base/CMakeLists.txt +++ b/src/base/CMakeLists.txt @@ -8,6 +8,7 @@ add_library(qbt_base STATIC bittorrent/bandwidthscheduler.h bittorrent/bencoderesumedatastorage.h bittorrent/cachestatus.h + bittorrent/categoryoptions.h bittorrent/common.h bittorrent/customstorage.h bittorrent/dbresumedatastorage.h @@ -100,6 +101,7 @@ add_library(qbt_base STATIC bittorrent/abstractfilestorage.cpp bittorrent/bandwidthscheduler.cpp bittorrent/bencoderesumedatastorage.cpp + bittorrent/categoryoptions.cpp bittorrent/customstorage.cpp bittorrent/dbresumedatastorage.cpp bittorrent/downloadpriority.cpp diff --git a/src/base/base.pri b/src/base/base.pri index 8607ca085..63196cfe1 100644 --- a/src/base/base.pri +++ b/src/base/base.pri @@ -7,6 +7,7 @@ HEADERS += \ $$PWD/bittorrent/bandwidthscheduler.h \ $$PWD/bittorrent/bencoderesumedatastorage.h \ $$PWD/bittorrent/cachestatus.h \ + $$PWD/bittorrent/categoryoptions.h \ $$PWD/bittorrent/common.h \ $$PWD/bittorrent/customstorage.h \ $$PWD/bittorrent/downloadpriority.h \ @@ -100,6 +101,7 @@ SOURCES += \ $$PWD/bittorrent/abstractfilestorage.cpp \ $$PWD/bittorrent/bandwidthscheduler.cpp \ $$PWD/bittorrent/bencoderesumedatastorage.cpp \ + $$PWD/bittorrent/categoryoptions.cpp \ $$PWD/bittorrent/customstorage.cpp \ $$PWD/bittorrent/dbresumedatastorage.cpp \ $$PWD/bittorrent/downloadpriority.cpp \ diff --git a/src/base/bittorrent/addtorrentparams.h b/src/base/bittorrent/addtorrentparams.h index 3306667ab..78bb6f96b 100644 --- a/src/base/bittorrent/addtorrentparams.h +++ b/src/base/bittorrent/addtorrentparams.h @@ -48,7 +48,8 @@ namespace BitTorrent QString category; TagSet tags; QString savePath; - bool disableTempPath = false; // e.g. for imported torrents + std::optional useDownloadPath; + QString downloadPath; bool sequential = false; bool firstLastPiecePriority = false; bool addForced = false; diff --git a/src/base/bittorrent/bencoderesumedatastorage.cpp b/src/base/bittorrent/bencoderesumedatastorage.cpp index 668556427..ad593a01e 100644 --- a/src/base/bittorrent/bencoderesumedatastorage.cpp +++ b/src/base/bittorrent/bencoderesumedatastorage.cpp @@ -173,12 +173,19 @@ std::optional BitTorrent::BencodeResumeDataStorag torrentParams.restored = true; torrentParams.category = fromLTString(root.dict_find_string_value("qBt-category")); torrentParams.name = fromLTString(root.dict_find_string_value("qBt-name")); - torrentParams.savePath = Profile::instance()->fromPortablePath( - Utils::Fs::toUniformPath(fromLTString(root.dict_find_string_value("qBt-savePath")))); torrentParams.hasSeedStatus = root.dict_find_int_value("qBt-seedStatus"); torrentParams.firstLastPiecePriority = root.dict_find_int_value("qBt-firstLastPiecePriority"); torrentParams.seedingTimeLimit = root.dict_find_int_value("qBt-seedingTimeLimit", Torrent::USE_GLOBAL_SEEDING_TIME); + torrentParams.savePath = Profile::instance()->fromPortablePath( + Utils::Fs::toUniformPath(fromLTString(root.dict_find_string_value("qBt-savePath")))); + torrentParams.useAutoTMM = torrentParams.savePath.isEmpty(); + if (!torrentParams.useAutoTMM) + { + torrentParams.downloadPath = Profile::instance()->fromPortablePath( + Utils::Fs::toUniformPath(fromLTString(root.dict_find_string_value("qBt-downloadPath")))); + } + // TODO: The following code is deprecated. Replace with the commented one after several releases in 4.4.x. // === BEGIN DEPRECATED CODE === // const lt::bdecode_node contentLayoutNode = root.dict_find("qBt-contentLayout"); @@ -352,7 +359,6 @@ void BitTorrent::BencodeResumeDataStorage::Worker::store(const TorrentID &id, co } } - data["qBt-savePath"] = Profile::instance()->toPortablePath(resumeData.savePath).toStdString(); data["qBt-ratioLimit"] = static_cast(resumeData.ratioLimit * 1000); data["qBt-seedingTimeLimit"] = resumeData.seedingTimeLimit; data["qBt-category"] = resumeData.category.toStdString(); @@ -362,6 +368,12 @@ void BitTorrent::BencodeResumeDataStorage::Worker::store(const TorrentID &id, co data["qBt-contentLayout"] = Utils::String::fromEnum(resumeData.contentLayout).toStdString(); data["qBt-firstLastPiecePriority"] = resumeData.firstLastPiecePriority; + if (!resumeData.useAutoTMM) + { + data["qBt-savePath"] = Profile::instance()->toPortablePath(resumeData.savePath).toStdString(); + data["qBt-downloadPath"] = Profile::instance()->toPortablePath(resumeData.downloadPath).toStdString(); + } + const QString resumeFilepath = m_resumeDataDir.absoluteFilePath(QString::fromLatin1("%1.fastresume").arg(id.toString())); const nonstd::expected result = Utils::IO::saveToFile(resumeFilepath, data); if (!result) diff --git a/src/base/bittorrent/categoryoptions.cpp b/src/base/bittorrent/categoryoptions.cpp new file mode 100644 index 000000000..6807e59c0 --- /dev/null +++ b/src/base/bittorrent/categoryoptions.cpp @@ -0,0 +1,78 @@ +/* + * Bittorrent Client using Qt and libtorrent. + * Copyright (C) 2021 Vladimir Golovnev + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * In addition, as a special exception, the copyright holders give permission to + * link this program with the OpenSSL project's "OpenSSL" library (or with + * modified versions of it that use the same license as the "OpenSSL" library), + * and distribute the linked executables. You must obey the GNU General Public + * License in all respects for all of the code used other than "OpenSSL". If you + * modify file(s), you may extend this exception to your version of the file(s), + * but you are not obligated to do so. If you do not wish to do so, delete this + * exception statement from your version. + */ + +#include "categoryoptions.h" + +#include +#include + +const QString OPTION_SAVEPATH {QStringLiteral("save_path")}; +const QString OPTION_DOWNLOADPATH {QStringLiteral("download_path")}; + +BitTorrent::CategoryOptions BitTorrent::CategoryOptions::fromJSON(const QJsonObject &jsonObj) +{ + CategoryOptions options; + options.savePath = jsonObj.value(OPTION_SAVEPATH).toString(); + + const QJsonValue downloadPathValue = jsonObj.value(OPTION_DOWNLOADPATH); + if (downloadPathValue.isBool()) + options.downloadPath = {downloadPathValue.toBool()}; + else if (downloadPathValue.isString()) + options.downloadPath = {true, downloadPathValue.toString()}; + + return options; +} + +QJsonObject BitTorrent::CategoryOptions::toJSON() const +{ + QJsonValue downloadPathValue = QJsonValue::Undefined; + if (downloadPath) + { + if (downloadPath->enabled) + downloadPathValue = downloadPath->path; + else + downloadPathValue = false; + } + + return { + {OPTION_SAVEPATH, savePath}, + {OPTION_DOWNLOADPATH, downloadPathValue} + }; +} + +bool BitTorrent::operator==(const BitTorrent::CategoryOptions::DownloadPathOption &left, const BitTorrent::CategoryOptions::DownloadPathOption &right) +{ + return ((left.enabled == right.enabled) + && (left.path == right.path)); +} + +bool BitTorrent::operator==(const BitTorrent::CategoryOptions &left, const BitTorrent::CategoryOptions &right) +{ + return ((left.savePath == right.savePath) + && (left.downloadPath == right.downloadPath)); +} diff --git a/src/base/bittorrent/categoryoptions.h b/src/base/bittorrent/categoryoptions.h new file mode 100644 index 000000000..61de80fa8 --- /dev/null +++ b/src/base/bittorrent/categoryoptions.h @@ -0,0 +1,56 @@ +/* + * Bittorrent Client using Qt and libtorrent. + * Copyright (C) 2021 Vladimir Golovnev + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * In addition, as a special exception, the copyright holders give permission to + * link this program with the OpenSSL project's "OpenSSL" library (or with + * modified versions of it that use the same license as the "OpenSSL" library), + * and distribute the linked executables. You must obey the GNU General Public + * License in all respects for all of the code used other than "OpenSSL". If you + * modify file(s), you may extend this exception to your version of the file(s), + * but you are not obligated to do so. If you do not wish to do so, delete this + * exception statement from your version. + */ + +#pragma once + +#include + +#include + +class QJsonObject; + +namespace BitTorrent +{ + struct CategoryOptions + { + struct DownloadPathOption + { + bool enabled; + QString path; + }; + + QString savePath; + std::optional downloadPath; + + static CategoryOptions fromJSON(const QJsonObject &jsonObj); + QJsonObject toJSON() const; + }; + + bool operator==(const CategoryOptions::DownloadPathOption &left, const CategoryOptions::DownloadPathOption &right); + bool operator==(const CategoryOptions &left, const CategoryOptions &right); +} diff --git a/src/base/bittorrent/dbresumedatastorage.cpp b/src/base/bittorrent/dbresumedatastorage.cpp index 78016dfb9..8b5e09b0c 100644 --- a/src/base/bittorrent/dbresumedatastorage.cpp +++ b/src/base/bittorrent/dbresumedatastorage.cpp @@ -28,6 +28,8 @@ #include "dbresumedatastorage.h" +#include + #include #include #include @@ -57,11 +59,13 @@ namespace { const char DB_CONNECTION_NAME[] = "ResumeDataStorage"; - const int DB_VERSION = 1; + const int DB_VERSION = 2; const char DB_TABLE_META[] = "meta"; const char DB_TABLE_TORRENTS[] = "torrents"; + const char META_VERSION[] = "version"; + struct Column { QString name; @@ -80,6 +84,7 @@ namespace const Column DB_COLUMN_CATEGORY = makeColumn("category"); const Column DB_COLUMN_TAGS = makeColumn("tags"); const Column DB_COLUMN_TARGET_SAVE_PATH = makeColumn("target_save_path"); + const Column DB_COLUMN_DOWNLOAD_PATH = makeColumn("download_path"); const Column DB_COLUMN_CONTENT_LAYOUT = makeColumn("content_layout"); const Column DB_COLUMN_RATIO_LIMIT = makeColumn("ratio_limit"); const Column DB_COLUMN_SEEDING_TIME_LIMIT = makeColumn("seeding_time_limit"); @@ -109,42 +114,50 @@ namespace return QString::fromLatin1("CREATE TABLE %1 (%2)").arg(quoted(tableName), items.join(QLatin1Char(','))); } - QString makeInsertStatement(const QString &tableName, const QVector &columns) + std::pair joinColumns(const QVector &columns) { - QStringList names; - names.reserve(columns.size()); - QStringList values; - values.reserve(columns.size()); + int namesSize = columns.size(); + int valuesSize = columns.size(); for (const Column &column : columns) { - names.append(quoted(column.name)); - values.append(column.placeholder); + namesSize += column.name.size() + 2; + valuesSize += column.placeholder.size(); } - const QString jointNames = names.join(QLatin1Char(',')); - const QString jointValues = values.join(QLatin1Char(',')); + QString names; + names.reserve(namesSize); + QString values; + values.reserve(valuesSize); + for (const Column &column : columns) + { + names.append(quoted(column.name) + QLatin1Char(',')); + values.append(column.placeholder + QLatin1Char(',')); + } + names.chop(1); + values.chop(1); + return std::make_pair(names, values); + } + + QString makeInsertStatement(const QString &tableName, const QVector &columns) + { + const auto [names, values] = joinColumns(columns); return QString::fromLatin1("INSERT INTO %1 (%2) VALUES (%3)") - .arg(quoted(tableName), jointNames, jointValues); + .arg(quoted(tableName), names, values); + } + + QString makeUpdateStatement(const QString &tableName, const QVector &columns) + { + const auto [names, values] = joinColumns(columns); + return QString::fromLatin1("UPDATE %1 SET (%2) = (%3)") + .arg(quoted(tableName), names, values); } QString makeOnConflictUpdateStatement(const Column &constraint, const QVector &columns) { - QStringList names; - names.reserve(columns.size()); - QStringList values; - values.reserve(columns.size()); - for (const Column &column : columns) - { - names.append(quoted(column.name)); - values.append(column.placeholder); - } - - const QString jointNames = names.join(QLatin1Char(',')); - const QString jointValues = values.join(QLatin1Char(',')); - + const auto [names, values] = joinColumns(columns); return QString::fromLatin1(" ON CONFLICT (%1) DO UPDATE SET (%2) = (%3)") - .arg(quoted(constraint.name), jointNames, jointValues); + .arg(quoted(constraint.name), names, values); } QString makeColumnDefinition(const Column &column, const char *definition) @@ -187,7 +200,15 @@ BitTorrent::DBResumeDataStorage::DBResumeDataStorage(const QString &dbPath, QObj throw RuntimeError(db.lastError().text()); if (needCreateDB) + { createDB(); + } + else + { + const int dbVersion = currentDBVersion(); + if (dbVersion == 1) + updateDBFromVersion1(); + } m_asyncWorker = new Worker(dbPath, QLatin1String("ResumeDataStorageWorker")); m_asyncWorker->moveToThread(m_ioThread); @@ -276,8 +297,6 @@ std::optional BitTorrent::DBResumeDataStorage::lo const QStringList tagList = tagsData.split(QLatin1Char(',')); resumeData.tags.insert(tagList.cbegin(), tagList.cend()); } - resumeData.savePath = Profile::instance()->fromPortablePath( - Utils::Fs::toUniformPath(query.value(DB_COLUMN_TARGET_SAVE_PATH.name).toString())); resumeData.hasSeedStatus = query.value(DB_COLUMN_HAS_SEED_STATUS.name).toBool(); resumeData.firstLastPiecePriority = query.value(DB_COLUMN_HAS_OUTER_PIECES_PRIORITY.name).toBool(); resumeData.ratioLimit = query.value(DB_COLUMN_RATIO_LIMIT.name).toInt() / 1000.0; @@ -288,6 +307,15 @@ std::optional BitTorrent::DBResumeDataStorage::lo query.value(DB_COLUMN_OPERATING_MODE.name).toString(), TorrentOperatingMode::AutoManaged); resumeData.stopped = query.value(DB_COLUMN_STOPPED.name).toBool(); + resumeData.savePath = Profile::instance()->fromPortablePath( + Utils::Fs::toUniformPath(query.value(DB_COLUMN_TARGET_SAVE_PATH.name).toString())); + resumeData.useAutoTMM = resumeData.savePath.isEmpty(); + if (!resumeData.useAutoTMM) + { + resumeData.downloadPath = Profile::instance()->fromPortablePath( + Utils::Fs::toUniformPath(query.value(DB_COLUMN_DOWNLOAD_PATH.name).toString())); + } + const QByteArray bencodedResumeData = query.value(DB_COLUMN_RESUMEDATA.name).toByteArray(); const QByteArray bencodedMetadata = query.value(DB_COLUMN_METADATA.name).toByteArray(); const QByteArray allData = ((bencodedMetadata.isEmpty() || bencodedResumeData.isEmpty()) @@ -297,6 +325,9 @@ std::optional BitTorrent::DBResumeDataStorage::lo lt::error_code ec; const lt::bdecode_node root = lt::bdecode(allData, ec); + resumeData.downloadPath = Profile::instance()->fromPortablePath( + Utils::Fs::toUniformPath(fromLTString(root.dict_find_string_value("qBt-downloadPath")))); + lt::add_torrent_params &p = resumeData.ltAddTorrentParams; p = lt::read_resume_data(root, ec); @@ -329,6 +360,33 @@ void BitTorrent::DBResumeDataStorage::storeQueue(const QVector &queue }); } +int BitTorrent::DBResumeDataStorage::currentDBVersion() const +{ + const auto selectDBVersionStatement = QString::fromLatin1("SELECT %1 FROM %2 WHERE %3 = %4;") + .arg(quoted(DB_COLUMN_VALUE.name), quoted(DB_TABLE_META), quoted(DB_COLUMN_NAME.name), DB_COLUMN_NAME.placeholder); + + auto db = QSqlDatabase::database(DB_CONNECTION_NAME); + QSqlQuery query {db}; + + if (!query.prepare(selectDBVersionStatement)) + throw RuntimeError(query.lastError().text()); + + query.bindValue(DB_COLUMN_NAME.placeholder, QString::fromLatin1(META_VERSION)); + + if (!query.exec()) + throw RuntimeError(query.lastError().text()); + + if (!query.next()) + throw RuntimeError(tr("Database is corrupted.")); + + bool ok; + const int dbVersion = query.value(0).toInt(&ok); + if (!ok) + throw RuntimeError(tr("Database is corrupted.")); + + return dbVersion; +} + void BitTorrent::DBResumeDataStorage::createDB() const { auto db = QSqlDatabase::database(DB_CONNECTION_NAME); @@ -353,7 +411,7 @@ void BitTorrent::DBResumeDataStorage::createDB() const if (!query.prepare(insertMetaVersionQuery)) throw RuntimeError(query.lastError().text()); - query.bindValue(DB_COLUMN_NAME.placeholder, QString::fromLatin1("version")); + query.bindValue(DB_COLUMN_NAME.placeholder, QString::fromLatin1(META_VERSION)); query.bindValue(DB_COLUMN_VALUE.placeholder, DB_VERSION); if (!query.exec()) @@ -391,6 +449,42 @@ void BitTorrent::DBResumeDataStorage::createDB() const } } +void BitTorrent::DBResumeDataStorage::updateDBFromVersion1() const +{ + auto db = QSqlDatabase::database(DB_CONNECTION_NAME); + + if (!db.transaction()) + throw RuntimeError(db.lastError().text()); + + QSqlQuery query {db}; + + try + { + const auto alterTableTorrentsQuery = QString::fromLatin1("ALTER TABLE %1 ADD %2") + .arg(quoted(DB_TABLE_TORRENTS), makeColumnDefinition(DB_COLUMN_DOWNLOAD_PATH, "TEXT")); + 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)) + throw RuntimeError(query.lastError().text()); + + query.bindValue(DB_COLUMN_NAME.placeholder, QString::fromLatin1(META_VERSION)); + query.bindValue(DB_COLUMN_VALUE.placeholder, DB_VERSION); + + if (!query.exec()) + throw RuntimeError(query.lastError().text()); + + if (!db.commit()) + throw RuntimeError(db.lastError().text()); + } + catch (const RuntimeError &) + { + db.rollback(); + throw; + } +} + BitTorrent::DBResumeDataStorage::Worker::Worker(const QString &dbPath, const QString &dbConnectionName) : m_path {dbPath} , m_connectionName {dbConnectionName} @@ -499,7 +593,6 @@ void BitTorrent::DBResumeDataStorage::Worker::store(const TorrentID &id, const L query.bindValue(DB_COLUMN_CATEGORY.placeholder, resumeData.category); query.bindValue(DB_COLUMN_TAGS.placeholder, (resumeData.tags.isEmpty() ? QVariant(QVariant::String) : resumeData.tags.join(QLatin1String(",")))); - query.bindValue(DB_COLUMN_TARGET_SAVE_PATH.placeholder, Profile::instance()->toPortablePath(resumeData.savePath)); query.bindValue(DB_COLUMN_CONTENT_LAYOUT.placeholder, Utils::String::fromEnum(resumeData.contentLayout)); query.bindValue(DB_COLUMN_RATIO_LIMIT.placeholder, static_cast(resumeData.ratioLimit * 1000)); query.bindValue(DB_COLUMN_SEEDING_TIME_LIMIT.placeholder, resumeData.seedingTimeLimit); @@ -507,6 +600,13 @@ 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); + + if (!resumeData.useAutoTMM) + { + query.bindValue(DB_COLUMN_TARGET_SAVE_PATH.placeholder, Profile::instance()->toPortablePath(resumeData.savePath)); + query.bindValue(DB_COLUMN_DOWNLOAD_PATH.placeholder, Profile::instance()->toPortablePath(resumeData.downloadPath)); + } + query.bindValue(DB_COLUMN_RESUMEDATA.placeholder, bencodedResumeData); if (!bencodedMetadata.isEmpty()) query.bindValue(DB_COLUMN_METADATA.placeholder, bencodedMetadata); diff --git a/src/base/bittorrent/dbresumedatastorage.h b/src/base/bittorrent/dbresumedatastorage.h index f166ae6e4..df784aec3 100644 --- a/src/base/bittorrent/dbresumedatastorage.h +++ b/src/base/bittorrent/dbresumedatastorage.h @@ -50,7 +50,9 @@ namespace BitTorrent void storeQueue(const QVector &queue) const override; private: + int currentDBVersion() const; void createDB() const; + void updateDBFromVersion1() const; QThread *m_ioThread = nullptr; diff --git a/src/base/bittorrent/filesearcher.cpp b/src/base/bittorrent/filesearcher.cpp index a8a3b6594..63209c0f1 100644 --- a/src/base/bittorrent/filesearcher.cpp +++ b/src/base/bittorrent/filesearcher.cpp @@ -34,7 +34,7 @@ #include "base/bittorrent/infohash.h" void FileSearcher::search(const BitTorrent::TorrentID &id, const QStringList &originalFileNames - , const QString &completeSavePath, const QString &incompleteSavePath) + , const QString &savePath, const QString &downloadPath) { const auto findInDir = [](const QString &dirPath, QStringList &fileNames) -> bool { @@ -56,14 +56,14 @@ void FileSearcher::search(const BitTorrent::TorrentID &id, const QStringList &or return found; }; - QString savePath = completeSavePath; + QString usedPath = savePath; QStringList adjustedFileNames = originalFileNames; - const bool found = findInDir(savePath, adjustedFileNames); - if (!found && !incompleteSavePath.isEmpty()) + const bool found = findInDir(usedPath, adjustedFileNames); + if (!found && !downloadPath.isEmpty()) { - savePath = incompleteSavePath; - findInDir(savePath, adjustedFileNames); + usedPath = downloadPath; + findInDir(usedPath, adjustedFileNames); } - emit searchFinished(id, savePath, adjustedFileNames); + emit searchFinished(id, usedPath, adjustedFileNames); } diff --git a/src/base/bittorrent/filesearcher.h b/src/base/bittorrent/filesearcher.h index fd2827f0a..f167d8fa1 100644 --- a/src/base/bittorrent/filesearcher.h +++ b/src/base/bittorrent/filesearcher.h @@ -45,7 +45,7 @@ public: public slots: void search(const BitTorrent::TorrentID &id, const QStringList &originalFileNames - , const QString &completeSavePath, const QString &incompleteSavePath); + , const QString &savePath, const QString &downloadPath); signals: void searchFinished(const BitTorrent::TorrentID &id, const QString &savePath, const QStringList &fileNames); diff --git a/src/base/bittorrent/loadtorrentparams.h b/src/base/bittorrent/loadtorrentparams.h index 7bac0fd48..dcb090be9 100644 --- a/src/base/bittorrent/loadtorrentparams.h +++ b/src/base/bittorrent/loadtorrentparams.h @@ -46,8 +46,10 @@ namespace BitTorrent QString category; TagSet tags; QString savePath; + QString downloadPath; TorrentContentLayout contentLayout = TorrentContentLayout::Original; TorrentOperatingMode operatingMode = TorrentOperatingMode::AutoManaged; + bool useAutoTMM = false; bool firstLastPiecePriority = false; bool hasSeedStatus = false; bool stopped = false; diff --git a/src/base/bittorrent/session.cpp b/src/base/bittorrent/session.cpp index 4d5d0638f..dc9708b20 100644 --- a/src/base/bittorrent/session.cpp +++ b/src/base/bittorrent/session.cpp @@ -58,6 +58,10 @@ #include #include #include +#include +#include +#include +#include #include #if (QT_VERSION < QT_VERSION_CHECK(6, 0, 0)) #include @@ -80,6 +84,7 @@ #include "base/unicodestrings.h" #include "base/utils/bytearray.h" #include "base/utils/fs.h" +#include "base/utils/io.h" #include "base/utils/misc.h" #include "base/utils/net.h" #include "base/utils/random.h" @@ -103,6 +108,8 @@ using namespace BitTorrent; +const QString CATEGORIES_FILE_NAME {QStringLiteral("categories.json")}; + namespace { const char PEER_ID[] = "qB"; @@ -156,42 +163,9 @@ namespace } } - QStringMap map_cast(const QVariantMap &map) + QMap expandCategories(const QMap &categories) { - QStringMap result; - for (auto i = map.cbegin(); i != map.cend(); ++i) - result[i.key()] = i.value().toString(); - return result; - } - - QVariantMap map_cast(const QStringMap &map) - { - QVariantMap result; - for (auto i = map.cbegin(); i != map.cend(); ++i) - result[i.key()] = i.value(); - return result; - } - - QString normalizePath(const QString &path) - { - QString tmp = Utils::Fs::toUniformPath(path.trimmed()); - if (!tmp.isEmpty() && !tmp.endsWith('/')) - return tmp + '/'; - return tmp; - } - - QString normalizeSavePath(QString path, const QString &defaultPath = specialFolderLocation(SpecialFolder::Downloads)) - { - path = path.trimmed(); - if (path.isEmpty()) - path = Utils::Fs::toUniformPath(defaultPath.trimmed()); - - return normalizePath(path); - } - - QStringMap expandCategories(const QStringMap &categories) - { - QStringMap expanded = categories; + QMap expanded = categories; for (auto i = categories.cbegin(); i != categories.cend(); ++i) { @@ -199,7 +173,7 @@ namespace for (const QString &subcat : asConst(Session::expandCategory(category))) { if (!expanded.contains(subcat)) - expanded[subcat] = ""; + expanded[subcat] = {}; } } @@ -418,13 +392,12 @@ Session::Session(QObject *parent) , clampValue(ChokingAlgorithm::FixedSlots, ChokingAlgorithm::RateBased)) , m_seedChokingAlgorithm(BITTORRENT_SESSION_KEY("SeedChokingAlgorithm"), SeedChokingAlgorithm::FastestUpload , clampValue(SeedChokingAlgorithm::RoundRobin, SeedChokingAlgorithm::AntiLeech)) - , m_storedCategories(BITTORRENT_SESSION_KEY("Categories")) , m_storedTags(BITTORRENT_SESSION_KEY("Tags")) , m_maxRatioAction(BITTORRENT_SESSION_KEY("MaxRatioAction"), Pause) - , m_defaultSavePath(BITTORRENT_SESSION_KEY("DefaultSavePath"), specialFolderLocation(SpecialFolder::Downloads), normalizePath) - , m_tempPath(BITTORRENT_SESSION_KEY("TempPath"), defaultSavePath() + "temp/", normalizePath) + , m_savePath(BITTORRENT_SESSION_KEY("DefaultSavePath"), specialFolderLocation(SpecialFolder::Downloads), Utils::Fs::toUniformPath) + , m_downloadPath(BITTORRENT_SESSION_KEY("TempPath"), specialFolderLocation(SpecialFolder::Downloads) + QLatin1String("/temp"), Utils::Fs::toUniformPath) , m_isSubcategoriesEnabled(BITTORRENT_SESSION_KEY("SubcategoriesEnabled"), false) - , m_isTempPathEnabled(BITTORRENT_SESSION_KEY("TempPathEnabled"), false) + , m_isDownloadPathEnabled(BITTORRENT_SESSION_KEY("TempPathEnabled"), false) , m_isAutoTMMDisabledByDefault(BITTORRENT_SESSION_KEY("DisableAutoTMMByDefault"), true) , m_isDisableAutoTMMWhenCategoryChanged(BITTORRENT_SESSION_KEY("DisableAutoTMMTriggers/CategoryChanged"), false) , m_isDisableAutoTMMWhenDefaultSavePathChanged(BITTORRENT_SESSION_KEY("DisableAutoTMMTriggers/DefaultSavePathChanged"), true) @@ -472,12 +445,11 @@ Session::Session(QObject *parent) if (isBandwidthSchedulerEnabled()) enableBandwidthScheduler(); - m_categories = map_cast(m_storedCategories); + loadCategories(); if (isSubcategoriesEnabled()) { // if subcategories support changed manually m_categories = expandCategories(m_categories); - m_storedCategories = map_cast(m_categories); } const QStringList storedTags = m_storedTags.get(); @@ -566,18 +538,18 @@ void Session::setPeXEnabled(const bool enabled) LogMsg(tr("Restart is required to toggle PeX support"), Log::WARNING); } -bool Session::isTempPathEnabled() const +bool Session::isDownloadPathEnabled() const { - return m_isTempPathEnabled; + return m_isDownloadPathEnabled; } -void Session::setTempPathEnabled(const bool enabled) +void Session::setDownloadPathEnabled(const bool enabled) { - if (enabled != isTempPathEnabled()) + if (enabled != isDownloadPathEnabled()) { - m_isTempPathEnabled = enabled; + m_isDownloadPathEnabled = enabled; for (TorrentImpl *const torrent : asConst(m_torrents)) - torrent->handleTempPathChanged(); + torrent->handleDownloadPathChanged(); } } @@ -645,22 +617,14 @@ void Session::setFinishedTorrentExportDirectory(QString path) m_finishedTorrentExportDirectory = path; } -QString Session::defaultSavePath() const +QString Session::savePath() const { - return Utils::Fs::toUniformPath(m_defaultSavePath); + return m_savePath; } -QString Session::tempPath() const +QString Session::downloadPath() const { - return Utils::Fs::toUniformPath(m_tempPath); -} - -QString Session::torrentTempPath(const TorrentInfo &torrentInfo) const -{ - if ((torrentInfo.filesCount() > 1) && !torrentInfo.hasRootFolder()) - return tempPath() + torrentInfo.name() + '/'; - - return tempPath(); + return m_downloadPath; } bool Session::isValidCategoryName(const QString &name) @@ -692,29 +656,53 @@ QStringList Session::expandCategory(const QString &category) return result; } -QStringMap Session::categories() const +QStringList Session::categories() const { - return m_categories; + return m_categories.keys(); +} + +CategoryOptions Session::categoryOptions(const QString &categoryName) const +{ + return m_categories.value(categoryName); } QString Session::categorySavePath(const QString &categoryName) const { - const QString basePath = m_defaultSavePath; - if (categoryName.isEmpty()) return basePath; + const QString basePath = savePath(); + if (categoryName.isEmpty()) + return basePath; - QString path = m_categories.value(categoryName); + QString path = m_categories.value(categoryName).savePath; if (path.isEmpty()) // use implicit save path path = Utils::Fs::toValidFileSystemName(categoryName, true); - if (!QDir::isAbsolutePath(path)) - path.prepend(basePath); - - return normalizeSavePath(path); + return (QDir::isAbsolutePath(path) ? path : Utils::Fs::resolvePath(path, basePath)); } -bool Session::addCategory(const QString &name, const QString &savePath) +QString Session::categoryDownloadPath(const QString &categoryName) const { - if (name.isEmpty()) return false; + const CategoryOptions categoryOptions = m_categories.value(categoryName); + const CategoryOptions::DownloadPathOption downloadPathOption = + categoryOptions.downloadPath.value_or(CategoryOptions::DownloadPathOption {isDownloadPathEnabled(), downloadPath()}); + if (!downloadPathOption.enabled) + return {}; + + const QString basePath = downloadPath(); + if (categoryName.isEmpty()) + return basePath; + + const QString path = (!downloadPathOption.path.isEmpty() + ? downloadPathOption.path + : Utils::Fs::toValidFileSystemName(categoryName, true)); // use implicit download path + + return (QDir::isAbsolutePath(path) ? path : Utils::Fs::resolvePath(path, basePath)); +} + +bool Session::addCategory(const QString &name, const CategoryOptions &options) +{ + if (name.isEmpty()) + return false; + if (!isValidCategoryName(name) || m_categories.contains(name)) return false; @@ -724,37 +712,46 @@ bool Session::addCategory(const QString &name, const QString &savePath) { if ((parent != name) && !m_categories.contains(parent)) { - m_categories[parent] = ""; + m_categories[parent] = {}; emit categoryAdded(parent); } } } - m_categories[name] = savePath; - m_storedCategories = map_cast(m_categories); + m_categories[name] = options; + storeCategories(); emit categoryAdded(name); return true; } -bool Session::editCategory(const QString &name, const QString &savePath) +bool Session::editCategory(const QString &name, const CategoryOptions &options) { - if (!m_categories.contains(name)) return false; - if (categorySavePath(name) == savePath) return false; + const auto it = m_categories.find(name); + if (it == m_categories.end()) + return false; - m_categories[name] = savePath; - m_storedCategories = map_cast(m_categories); + CategoryOptions ¤tOptions = it.value(); + if (options == currentOptions) + return false; + + currentOptions = options; + storeCategories(); if (isDisableAutoTMMWhenCategorySavePathChanged()) { for (TorrentImpl *const torrent : asConst(m_torrents)) + { if (torrent->category() == name) torrent->setAutoTMMEnabled(false); + } } else { for (TorrentImpl *const torrent : asConst(m_torrents)) + { if (torrent->category() == name) - torrent->handleCategorySavePathChanged(); + torrent->handleCategoryOptionsChanged(); + } } return true; @@ -763,8 +760,10 @@ bool Session::editCategory(const QString &name, const QString &savePath) bool Session::removeCategory(const QString &name) { for (TorrentImpl *const torrent : asConst(m_torrents)) + { if (torrent->belongsToCategory(name)) torrent->setCategory(""); + } // remove stored category and its subcategories if exist bool result = false; @@ -772,7 +771,7 @@ bool Session::removeCategory(const QString &name) { // remove subcategories const QString test = name + '/'; - Algorithm::removeIf(m_categories, [this, &test, &result](const QString &category, const QString &) + Algorithm::removeIf(m_categories, [this, &test, &result](const QString &category, const CategoryOptions &) { if (category.startsWith(test)) { @@ -789,7 +788,7 @@ bool Session::removeCategory(const QString &name) if (result) { // update stored categories - m_storedCategories = map_cast(m_categories); + storeCategories(); emit categoryRemoved(name); } @@ -810,12 +809,12 @@ void Session::setSubcategoriesEnabled(const bool value) // expand categories to include all parent categories m_categories = expandCategories(m_categories); // update stored categories - m_storedCategories = map_cast(m_categories); + storeCategories(); } else { // reload categories - m_categories = map_cast(m_storedCategories); + loadCategories(); } m_isSubcategoriesEnabled = value; @@ -1823,14 +1822,7 @@ bool Session::deleteTorrent(const TorrentID &id, const DeleteOption deleteOption } else { - QString rootPath = torrent->rootPath(true); - if (!rootPath.isEmpty() && torrent->useTempPath()) - { - // torrent without root folder still has it in its temporary save path - rootPath = torrent->actualStorageLocation(); - } - - m_removingTorrents[torrent->id()] = {torrent->name(), rootPath, deleteOption}; + m_removingTorrents[torrent->id()] = {torrent->name(), torrent->rootPath(), deleteOption}; if (m_moveStorageQueue.size() > 1) { @@ -2059,6 +2051,7 @@ LoadTorrentParams Session::initLoadTorrentParams(const AddTorrentParams &addTorr LoadTorrentParams loadTorrentParams; loadTorrentParams.name = addTorrentParams.name; + loadTorrentParams.useAutoTMM = addTorrentParams.useAutoTMM.value_or(!isAutoTMMDisabledByDefault()); loadTorrentParams.firstLastPiecePriority = addTorrentParams.firstLastPiecePriority; loadTorrentParams.hasSeedStatus = addTorrentParams.skipChecking; // do not react on 'torrent_finished_alert' when skipping loadTorrentParams.contentLayout = addTorrentParams.contentLayout.value_or(torrentContentLayout()); @@ -2067,21 +2060,26 @@ LoadTorrentParams Session::initLoadTorrentParams(const AddTorrentParams &addTorr loadTorrentParams.ratioLimit = addTorrentParams.ratioLimit; loadTorrentParams.seedingTimeLimit = addTorrentParams.seedingTimeLimit; - const bool useAutoTMM = addTorrentParams.useAutoTMM.value_or(!isAutoTMMDisabledByDefault()); - if (useAutoTMM) - loadTorrentParams.savePath = ""; - else if (addTorrentParams.savePath.isEmpty()) - loadTorrentParams.savePath = defaultSavePath(); - else if (QDir(addTorrentParams.savePath).isRelative()) - loadTorrentParams.savePath = QDir(defaultSavePath()).absoluteFilePath(addTorrentParams.savePath); - else - loadTorrentParams.savePath = normalizePath(addTorrentParams.savePath); + if (!loadTorrentParams.useAutoTMM) + { + loadTorrentParams.savePath = (QDir::isAbsolutePath(addTorrentParams.savePath) + ? addTorrentParams.savePath + : Utils::Fs::resolvePath(addTorrentParams.savePath, savePath())); + + const bool useDownloadPath = addTorrentParams.useDownloadPath.value_or(isDownloadPathEnabled()); + if (useDownloadPath) + { + loadTorrentParams.downloadPath = (QDir::isAbsolutePath(addTorrentParams.downloadPath) + ? addTorrentParams.downloadPath + : Utils::Fs::resolvePath(addTorrentParams.downloadPath, downloadPath())); + } + } const QString category = addTorrentParams.category; if (!category.isEmpty() && !m_categories.contains(category) && !addCategory(category)) loadTorrentParams.category = ""; else - loadTorrentParams.category = addTorrentParams.category; + loadTorrentParams.category = category; for (const QString &tag : addTorrentParams.tags) { @@ -2143,8 +2141,9 @@ bool Session::addTorrent_impl(const std::variant &source bool isFindingIncompleteFiles = false; - // If empty then Automatic mode, otherwise Manual mode - const QString actualSavePath = loadTorrentParams.savePath.isEmpty() ? categorySavePath(loadTorrentParams.category) : loadTorrentParams.savePath; + const bool useAutoTMM = loadTorrentParams.useAutoTMM; + const QString actualSavePath = useAutoTMM ? categorySavePath(loadTorrentParams.category) : loadTorrentParams.savePath; + if (hasMetadata) { const TorrentInfo &torrentInfo = std::get(source); @@ -2170,7 +2169,9 @@ bool Session::addTorrent_impl(const std::variant &source if (!loadTorrentParams.hasSeedStatus) { - findIncompleteFiles(torrentInfo, actualSavePath, filePaths); + const QString actualDownloadPath = useAutoTMM + ? categoryDownloadPath(loadTorrentParams.category) : loadTorrentParams.downloadPath; + findIncompleteFiles(torrentInfo, actualSavePath, actualDownloadPath, filePaths); isFindingIncompleteFiles = true; } @@ -2264,17 +2265,16 @@ bool Session::loadTorrent(LoadTorrentParams params) return true; } -void Session::findIncompleteFiles(const TorrentInfo &torrentInfo, const QString &savePath, const QStringList &filePaths) const +void Session::findIncompleteFiles(const TorrentInfo &torrentInfo, const QString &savePath + , const QString &downloadPath, const QStringList &filePaths) const { Q_ASSERT(filePaths.isEmpty() || (filePaths.size() == torrentInfo.filesCount())); const auto searchId = TorrentID::fromInfoHash(torrentInfo.infoHash()); const QStringList originalFileNames = (filePaths.isEmpty() ? torrentInfo.filePaths() : filePaths); - const QString completeSavePath = savePath; - const QString incompleteSavePath = (isTempPathEnabled() ? torrentTempPath(torrentInfo) : QString {}); QMetaObject::invokeMethod(m_fileSearcher, [=]() { - m_fileSearcher->search(searchId, originalFileNames, completeSavePath, incompleteSavePath); + m_fileSearcher->search(searchId, originalFileNames, savePath, downloadPath); }); } @@ -2432,30 +2432,37 @@ void Session::removeTorrentsQueue() const m_resumeDataStorage->storeQueue({}); } -void Session::setDefaultSavePath(QString path) +void Session::setSavePath(const QString &path) { - path = normalizeSavePath(path); - if (path == m_defaultSavePath) return; + const QString baseSavePath = specialFolderLocation(SpecialFolder::Downloads); + const QString resolvedPath = (QDir::isAbsolutePath(path) ? path : Utils::Fs::resolvePath(path, baseSavePath)); + if (resolvedPath == m_savePath) return; - m_defaultSavePath = path; + m_savePath = resolvedPath; if (isDisableAutoTMMWhenDefaultSavePathChanged()) + { for (TorrentImpl *const torrent : asConst(m_torrents)) torrent->setAutoTMMEnabled(false); + } else + { for (TorrentImpl *const torrent : asConst(m_torrents)) - torrent->handleCategorySavePathChanged(); + torrent->handleCategoryOptionsChanged(); + } } -void Session::setTempPath(QString path) +void Session::setDownloadPath(const QString &path) { - path = normalizeSavePath(path, defaultSavePath() + "temp/"); - if (path == m_tempPath) return; + const QString baseDownloadPath = specialFolderLocation(SpecialFolder::Downloads) + QLatin1String("/temp"); + const QString resolvedPath = (QDir::isAbsolutePath(path) ? path : Utils::Fs::resolvePath(path, baseDownloadPath)); + if (resolvedPath != m_downloadPath) + { + m_downloadPath = resolvedPath; - m_tempPath = path; - - for (TorrentImpl *const torrent : asConst(m_torrents)) - torrent->handleTempPathChanged(); + for (TorrentImpl *const torrent : asConst(m_torrents)) + torrent->handleDownloadPathChanged(); + } } #if (QT_VERSION < QT_VERSION_CHECK(6, 0, 0)) @@ -3969,7 +3976,7 @@ void Session::handleTorrentFinished(TorrentImpl *const torrent) if (torrentRelpath.endsWith(".torrent", Qt::CaseInsensitive)) { qDebug("Found possible recursive torrent download."); - const QString torrentFullpath = torrent->savePath(true) + '/' + torrentRelpath; + const QString torrentFullpath = torrent->actualStorageLocation() + '/' + torrentRelpath; qDebug("Full subtorrent path is %s", qUtf8Printable(torrentFullpath)); if (TorrentInfo::loadFromFile(torrentFullpath)) { @@ -4022,7 +4029,7 @@ bool Session::addMoveTorrentStorageJob(TorrentImpl *torrent, const QString &newP Q_ASSERT(torrent); const lt::torrent_handle torrentHandle = torrent->nativeHandle(); - const QString currentLocation = torrent->actualStorageLocation(); + const QString currentLocation = Utils::Fs::toNativePath(torrent->actualStorageLocation()); if (m_moveStorageQueue.size() > 1) { @@ -4116,6 +4123,89 @@ void Session::handleMoveTorrentStorageJobFinished() } } +void Session::storeCategories() const +{ + QJsonObject jsonObj; + for (auto it = m_categories.cbegin(); it != m_categories.cend(); ++it) + { + const QString &categoryName = it.key(); + const CategoryOptions &categoryOptions = it.value(); + jsonObj[categoryName] = categoryOptions.toJSON(); + } + + const QString path = QDir(specialFolderLocation(SpecialFolder::Config)).absoluteFilePath(CATEGORIES_FILE_NAME); + const QByteArray data = QJsonDocument(jsonObj).toJson(); + const nonstd::expected result = Utils::IO::saveToFile(path, data); + if (!result) + { + LogMsg(tr("Couldn't store Categories configuration to %1. Error: %2") + .arg(path, result.error()), Log::WARNING); + } +} + +void Session::upgradeCategories() +{ + const auto legacyCategories = SettingValue("BitTorrent/Session/Categories").get(); + for (auto it = legacyCategories.cbegin(); it != legacyCategories.cend(); ++it) + { + const QString categoryName = it.key(); + CategoryOptions categoryOptions; + categoryOptions.savePath = it.value().toString(); + m_categories[categoryName] = categoryOptions; + } + + storeCategories(); +} + +void Session::loadCategories() +{ + m_categories.clear(); + + QFile confFile {QDir(specialFolderLocation(SpecialFolder::Config)).absoluteFilePath(CATEGORIES_FILE_NAME)}; + if (!confFile.exists()) + { + // TODO: Remove the following upgrade code in v4.5 + // == BEGIN UPGRADE CODE == + upgradeCategories(); + m_needUpgradeDownloadPath = true; + // == END UPGRADE CODE == + +// return; + } + + if (!confFile.open(QFile::ReadOnly)) + { + LogMsg(tr("Couldn't load Categories from %1. Error: %2") + .arg(confFile.fileName(), confFile.errorString()), Log::CRITICAL); + return; + } + + QJsonParseError jsonError; + const QJsonDocument jsonDoc = QJsonDocument::fromJson(confFile.readAll(), &jsonError); + if (jsonError.error != QJsonParseError::NoError) + { + LogMsg(tr("Couldn't parse Categories configuration from %1. Error: %2") + .arg(confFile.fileName(), jsonError.errorString()), Log::WARNING); + return; + } + + if (!jsonDoc.isObject()) + { + LogMsg(tr("Couldn't load Categories configuration from %1. Invalid data format.") + .arg(confFile.fileName()), Log::WARNING); + return; + } + + + const QJsonObject jsonObj = jsonDoc.object(); + for (auto it = jsonObj.constBegin(); it != jsonObj.constEnd(); ++it) + { + const QString &categoryName = it.key(); + const auto categoryOptions = CategoryOptions::fromJSON(it.value().toObject()); + m_categories[categoryName] = categoryOptions; + } +} + void Session::handleTorrentTrackerWarning(TorrentImpl *const torrent, const QString &trackerUrl) { emit trackerWarning(torrent, trackerUrl); @@ -4258,20 +4348,40 @@ void Session::startUpTorrents() QVector queue; for (const TorrentID &torrentID : torrents) { - const std::optional resumeData = startupStorage->load(torrentID); - if (resumeData) + const std::optional loadResumeDataResult = startupStorage->load(torrentID); + if (loadResumeDataResult) { + LoadTorrentParams resumeData = *loadResumeDataResult; + bool needStore = false; + if (m_resumeDataStorage != startupStorage) { - m_resumeDataStorage->store(torrentID, *resumeData); - if (isQueueingSystemEnabled() && !resumeData->hasSeedStatus) + needStore = true; + if (isQueueingSystemEnabled() && !resumeData.hasSeedStatus) queue.append(torrentID); } + // TODO: Remove the following upgrade code in v4.5 + // == BEGIN UPGRADE CODE == + if (m_needUpgradeDownloadPath && isDownloadPathEnabled()) + { + if (!resumeData.useAutoTMM) + { + resumeData.downloadPath = downloadPath(); + needStore = true; + } + } + // == END UPGRADE CODE == + + if (needStore) + m_resumeDataStorage->store(torrentID, resumeData); + qDebug() << "Starting up torrent" << torrentID.toString() << "..."; - if (!loadTorrent(*resumeData)) + if (!loadTorrent(resumeData)) + { LogMsg(tr("Unable to resume torrent '%1'.", "e.g: Unable to resume torrent 'hash'.") - .arg(torrentID.toString()), Log::CRITICAL); + .arg(torrentID.toString()), Log::CRITICAL); + } // process add torrent messages before message queue overflow if ((resumedTorrentsCount % 100) == 0) readAlerts(); diff --git a/src/base/bittorrent/session.h b/src/base/bittorrent/session.h index 583cd36ba..9c6eae3d1 100644 --- a/src/base/bittorrent/session.h +++ b/src/base/bittorrent/session.h @@ -47,6 +47,7 @@ #include "base/types.h" #include "addtorrentparams.h" #include "cachestatus.h" +#include "categoryoptions.h" #include "sessionstatus.h" #include "torrentinfo.h" #include "trackerentry.h" @@ -211,22 +212,23 @@ namespace BitTorrent static void freeInstance(); static Session *instance(); - QString defaultSavePath() const; - void setDefaultSavePath(QString path); - QString tempPath() const; - void setTempPath(QString path); - bool isTempPathEnabled() const; - void setTempPathEnabled(bool enabled); - QString torrentTempPath(const TorrentInfo &torrentInfo) const; + QString savePath() const; + void setSavePath(const QString &path); + QString downloadPath() const; + void setDownloadPath(const QString &path); + bool isDownloadPathEnabled() const; + void setDownloadPathEnabled(bool enabled); static bool isValidCategoryName(const QString &name); // returns category itself and all top level categories static QStringList expandCategory(const QString &category); - QStringMap categories() const; + QStringList categories() const; + CategoryOptions categoryOptions(const QString &categoryName) const; QString categorySavePath(const QString &categoryName) const; - bool addCategory(const QString &name, const QString &savePath = ""); - bool editCategory(const QString &name, const QString &savePath); + QString categoryDownloadPath(const QString &categoryName) const; + bool addCategory(const QString &name, const CategoryOptions &options = {}); + bool editCategory(const QString &name, const CategoryOptions &options); bool removeCategory(const QString &name); bool isSubcategoriesEnabled() const; void setSubcategoriesEnabled(bool value); @@ -499,7 +501,8 @@ namespace BitTorrent bool addMoveTorrentStorageJob(TorrentImpl *torrent, const QString &newPath, MoveStorageMode mode); - void findIncompleteFiles(const TorrentInfo &torrentInfo, const QString &savePath, const QStringList &filePaths = {}) const; + void findIncompleteFiles(const TorrentInfo &torrentInfo, const QString &savePath + , const QString &downloadPath, const QStringList &filePaths = {}) const; signals: void allTorrentsFinished(); @@ -642,6 +645,10 @@ namespace BitTorrent void moveTorrentStorage(const MoveStorageJob &job) const; void handleMoveTorrentStorageJobFinished(); + void loadCategories(); + void storeCategories() const; + void upgradeCategories(); + // BitTorrent lt::session *m_nativeSession = nullptr; @@ -729,13 +736,12 @@ namespace BitTorrent CachedSettingValue m_isProxyPeerConnectionsEnabled; CachedSettingValue m_chokingAlgorithm; CachedSettingValue m_seedChokingAlgorithm; - CachedSettingValue m_storedCategories; CachedSettingValue m_storedTags; CachedSettingValue m_maxRatioAction; - CachedSettingValue m_defaultSavePath; - CachedSettingValue m_tempPath; + CachedSettingValue m_savePath; + CachedSettingValue m_downloadPath; CachedSettingValue m_isSubcategoriesEnabled; - CachedSettingValue m_isTempPathEnabled; + CachedSettingValue m_isDownloadPathEnabled; CachedSettingValue m_isAutoTMMDisabledByDefault; CachedSettingValue m_isDisableAutoTMMWhenCategoryChanged; CachedSettingValue m_isDisableAutoTMMWhenDefaultSavePathChanged; @@ -780,7 +786,7 @@ namespace BitTorrent QHash m_downloadedTorrents; QHash m_removingTorrents; QSet m_needSaveResumeDataTorrents; - QStringMap m_categories; + QMap m_categories; QSet m_tags; // I/O errored torrents @@ -800,6 +806,8 @@ namespace BitTorrent QString m_lastExternalIP; + bool m_needUpgradeDownloadPath = false; + static Session *m_instance; }; } diff --git a/src/base/bittorrent/torrent.h b/src/base/bittorrent/torrent.h index b4e6b9d9a..cd3c9345a 100644 --- a/src/base/bittorrent/torrent.h +++ b/src/base/bittorrent/torrent.h @@ -126,11 +126,10 @@ namespace BitTorrent virtual QString currentTracker() const = 0; // 1. savePath() - the path where all the files and subfolders of torrent are stored. + // 1.1 downloadPath() - the path where all the files and subfolders of torrent are stored until torrent has finished downloading. // 2. rootPath() - absolute path of torrent file tree (first common subfolder of torrent files); empty string if torrent has no root folder. // 3. contentPath() - absolute path of torrent content (root path for multifile torrents, absolute file path for singlefile torrents). // - // These methods have 'actual' parameter (defaults to false) which allow to get actual or final path variant. - // // Examples. // Suppose we have three torrent with following structures and save path `/home/user/torrents`: // @@ -168,14 +167,15 @@ namespace BitTorrent // | B | /home/user/torrents/torrentB | /home/user/torrents/torrentB/subdir1/file1 | // | C | | /home/user/torrents/file1 | - virtual QString savePath(bool actual = false) const = 0; - virtual QString rootPath(bool actual = false) const = 0; - virtual QString contentPath(bool actual = false) const = 0; - - virtual bool useTempPath() const = 0; - virtual bool isAutoTMMEnabled() const = 0; virtual void setAutoTMMEnabled(bool enabled) = 0; + virtual QString savePath() const = 0; + virtual void setSavePath(const QString &savePath) = 0; + virtual QString downloadPath() const = 0; + virtual void setDownloadPath(const QString &downloadPath) = 0; + virtual QString actualStorageLocation() const = 0; + virtual QString rootPath() const = 0; + virtual QString contentPath() const = 0; virtual QString category() const = 0; virtual bool belongsToCategory(const QString &category) const = 0; virtual bool setCategory(const QString &category) = 0; @@ -273,7 +273,6 @@ namespace BitTorrent virtual void setFirstLastPiecePriority(bool enabled) = 0; virtual void pause() = 0; virtual void resume(TorrentOperatingMode mode = TorrentOperatingMode::AutoManaged) = 0; - virtual void move(QString path) = 0; virtual void forceReannounce(int index = -1) = 0; virtual void forceDHTAnnounce() = 0; virtual void forceRecheck() = 0; diff --git a/src/base/bittorrent/torrentimpl.cpp b/src/base/bittorrent/torrentimpl.cpp index 6819a2e99..c1c0e495a 100644 --- a/src/base/bittorrent/torrentimpl.cpp +++ b/src/base/bittorrent/torrentimpl.cpp @@ -252,7 +252,8 @@ TorrentImpl::TorrentImpl(Session *session, lt::session *nativeSession , m_infoHash(m_nativeHandle.info_hash()) #endif , m_name(params.name) - , m_savePath(Utils::Fs::toNativePath(params.savePath)) + , m_savePath(params.savePath) + , m_downloadPath(params.downloadPath) , m_category(params.category) , m_tags(params.tags) , m_ratioLimit(params.ratioLimit) @@ -261,13 +262,10 @@ TorrentImpl::TorrentImpl(Session *session, lt::session *nativeSession , m_contentLayout(params.contentLayout) , m_hasSeedStatus(params.hasSeedStatus) , m_hasFirstLastPiecePriority(params.firstLastPiecePriority) - , m_useAutoTMM(params.savePath.isEmpty()) + , m_useAutoTMM(params.useAutoTMM) , m_isStopped(params.stopped) , m_ltAddTorrentParams(params.ltAddTorrentParams) { - if (m_useAutoTMM) - m_savePath = Utils::Fs::toNativePath(m_session->categorySavePath(m_category)); - if (m_ltAddTorrentParams.ti) { // Initialize it only if torrent is added with metadata. @@ -293,7 +291,7 @@ TorrentImpl::TorrentImpl(Session *session, lt::session *nativeSession if (hasMetadata()) applyFirstLastPiecePriority(m_hasFirstLastPiecePriority); - // TODO: Remove the following upgrade code in v.4.4 + // TODO: Remove the following upgrade code in v4.4 // == BEGIN UPGRADE CODE == const QString spath = actualStorageLocation(); for (int i = 0; i < filesCount(); ++i) @@ -396,15 +394,52 @@ QString TorrentImpl::currentTracker() const return QString::fromStdString(m_nativeStatus.current_tracker); } -QString TorrentImpl::savePath(bool actual) const +QString TorrentImpl::savePath() const { - if (actual) - return Utils::Fs::toUniformPath(actualStorageLocation()); - else - return Utils::Fs::toUniformPath(m_savePath); + return isAutoTMMEnabled() ? m_session->categorySavePath(category()) : m_savePath; } -QString TorrentImpl::rootPath(bool actual) const +void TorrentImpl::setSavePath(const QString &path) +{ + Q_ASSERT(!isAutoTMMEnabled()); + + const QString resolvedPath = (QDir::isAbsolutePath(path) ? path : Utils::Fs::resolvePath(path, m_session->savePath())); + if (resolvedPath == savePath()) + return; + + m_savePath = resolvedPath; + + m_session->handleTorrentNeedSaveResumeData(this); + + const bool isFinished = isSeed() || m_hasSeedStatus; + if (isFinished) + moveStorage(savePath(), MoveStorageMode::KeepExistingFiles); +} + +QString TorrentImpl::downloadPath() const +{ + return isAutoTMMEnabled() ? m_session->categoryDownloadPath(category()) : m_downloadPath; +} + +void TorrentImpl::setDownloadPath(const QString &path) +{ + Q_ASSERT(!isAutoTMMEnabled()); + + const QString resolvedPath = ((path.isEmpty() || QDir::isAbsolutePath(path)) + ? path : Utils::Fs::resolvePath(path, m_session->downloadPath())); + if (resolvedPath == m_downloadPath) + return; + + m_downloadPath = resolvedPath; + + m_session->handleTorrentNeedSaveResumeData(this); + + const bool isFinished = isSeed() || m_hasSeedStatus; + if (!isFinished) + moveStorage((m_downloadPath.isEmpty() ? savePath() : m_downloadPath), MoveStorageMode::KeepExistingFiles); +} + +QString TorrentImpl::rootPath() const { if (!hasMetadata()) return {}; @@ -413,19 +448,19 @@ QString TorrentImpl::rootPath(bool actual) const if (relativeRootPath.isEmpty()) return {}; - return QDir(savePath(actual)).absoluteFilePath(relativeRootPath); + return QDir(actualStorageLocation()).absoluteFilePath(relativeRootPath); } -QString TorrentImpl::contentPath(const bool actual) const +QString TorrentImpl::contentPath() const { if (!hasMetadata()) return {}; if (filesCount() == 1) - return QDir(savePath(actual)).absoluteFilePath(filePath(0)); + return QDir(actualStorageLocation()).absoluteFilePath(filePath(0)); - const QString rootPath = this->rootPath(actual); - return (rootPath.isEmpty() ? savePath(actual) : rootPath); + const QString rootPath = this->rootPath(); + return (rootPath.isEmpty() ? actualStorageLocation() : rootPath); } bool TorrentImpl::isAutoTMMEnabled() const @@ -435,19 +470,25 @@ bool TorrentImpl::isAutoTMMEnabled() const void TorrentImpl::setAutoTMMEnabled(bool enabled) { - if (m_useAutoTMM == enabled) return; + if (m_useAutoTMM == enabled) + return; m_useAutoTMM = enabled; + if (!m_useAutoTMM) + { + m_savePath = m_session->categorySavePath(category()); + m_downloadPath = m_session->categoryDownloadPath(category()); + } + m_session->handleTorrentNeedSaveResumeData(this); m_session->handleTorrentSavingModeChanged(this); - if (m_useAutoTMM) - move_impl(m_session->categorySavePath(m_category), MoveStorageMode::Overwrite); + adjustStorageLocation(); } QString TorrentImpl::actualStorageLocation() const { - return QString::fromStdString(m_nativeStatus.save_path); + return Utils::Fs::toUniformPath(QString::fromStdString(m_nativeStatus.save_path)); } void TorrentImpl::setAutoManaged(const bool enable) @@ -774,7 +815,7 @@ QStringList TorrentImpl::absoluteFilePaths() const { if (!hasMetadata()) return {}; - const QDir saveDir {savePath(true)}; + const QDir saveDir {actualStorageLocation()}; QStringList res; res.reserve(filesCount()); for (int i = 0; i < filesCount(); ++i) @@ -1351,7 +1392,7 @@ bool TorrentImpl::setCategory(const QString &category) if (m_useAutoTMM) { if (!m_session->isDisableAutoTMMWhenCategoryChanged()) - move_impl(m_session->categorySavePath(m_category), MoveStorageMode::Overwrite); + adjustStorageLocation(); else setAutoTMMEnabled(false); } @@ -1360,41 +1401,6 @@ bool TorrentImpl::setCategory(const QString &category) return true; } -void TorrentImpl::move(QString path) -{ - if (m_useAutoTMM) - { - m_useAutoTMM = false; - m_session->handleTorrentNeedSaveResumeData(this); - m_session->handleTorrentSavingModeChanged(this); - } - - path = Utils::Fs::toUniformPath(path.trimmed()); - if (path.isEmpty()) - path = m_session->defaultSavePath(); - if (!path.endsWith('/')) - path += '/'; - - move_impl(path, MoveStorageMode::KeepExistingFiles); -} - -void TorrentImpl::move_impl(QString path, const MoveStorageMode mode) -{ - if (path == savePath()) return; - - path = Utils::Fs::toNativePath(path); - if (!useTempPath()) - { - moveStorage(path, mode); - } - else - { - m_savePath = path; - m_session->handleTorrentNeedSaveResumeData(this); - m_session->handleTorrentSavePathChanged(this); - } -} - void TorrentImpl::forceReannounce(const int index) { m_nativeHandle.force_reannounce(0, index); @@ -1611,7 +1617,7 @@ void TorrentImpl::resume(const TorrentOperatingMode mode) void TorrentImpl::moveStorage(const QString &newPath, const MoveStorageMode mode) { - if (m_session->addMoveTorrentStorageJob(this, newPath, mode)) + if (m_session->addMoveTorrentStorageJob(this, Utils::Fs::toNativePath(newPath), mode)) { m_storageIsMoving = true; updateStatus(); @@ -1629,18 +1635,18 @@ void TorrentImpl::handleStateUpdate(const lt::torrent_status &nativeStatus) updateStatus(nativeStatus); } +void TorrentImpl::handleDownloadPathChanged() +{ + adjustStorageLocation(); +} + void TorrentImpl::handleMoveStorageJobFinished(const bool hasOutstandingJob) { m_session->handleTorrentNeedSaveResumeData(this); m_storageIsMoving = hasOutstandingJob; updateStatus(); - const QString newPath = QString::fromStdString(m_nativeStatus.save_path); - if (!useTempPath() && (newPath != m_savePath)) - { - m_savePath = newPath; - m_session->handleTorrentSavePathChanged(this); - } + m_session->handleTorrentSavePathChanged(this); if (!m_storageIsMoving) { @@ -1717,7 +1723,7 @@ void TorrentImpl::handleTorrentCheckedAlert(const lt::torrent_checked_alert *p) else if (progress() == 1.0) m_hasSeedStatus = true; - adjustActualSavePath(); + adjustStorageLocation(); manageIncompleteFiles(); } @@ -1735,7 +1741,7 @@ void TorrentImpl::handleTorrentFinishedAlert(const lt::torrent_finished_alert *p updateStatus(); m_hasSeedStatus = true; - adjustActualSavePath(); + adjustStorageLocation(); manageIncompleteFiles(); m_session->handleTorrentNeedSaveResumeData(this); @@ -1778,7 +1784,7 @@ void TorrentImpl::handleSaveResumeDataAlert(const lt::save_resume_data_alert *p) QStringList filePaths = metadata.filePaths(); applyContentLayout(filePaths, m_contentLayout); - m_session->findIncompleteFiles(metadata, m_savePath, filePaths); + m_session->findIncompleteFiles(metadata, savePath(), downloadPath(), filePaths); } else { @@ -1812,7 +1818,6 @@ void TorrentImpl::prepareResumeData(const lt::add_torrent_params ¶ms) LoadTorrentParams resumeData; resumeData.name = m_name; resumeData.category = m_category; - resumeData.savePath = m_useAutoTMM ? "" : m_savePath; resumeData.tags = m_tags; resumeData.contentLayout = m_contentLayout; resumeData.ratioLimit = m_ratioLimit; @@ -1822,6 +1827,12 @@ void TorrentImpl::prepareResumeData(const lt::add_torrent_params ¶ms) resumeData.stopped = m_isStopped; resumeData.operatingMode = m_operatingMode; resumeData.ltAddTorrentParams = m_ltAddTorrentParams; + resumeData.useAutoTMM = m_useAutoTMM; + if (!resumeData.useAutoTMM) + { + resumeData.savePath = m_savePath; + resumeData.downloadPath = m_downloadPath; + } m_session->handleTorrentResumeDataReady(this, resumeData); } @@ -1956,15 +1967,10 @@ void TorrentImpl::handlePerformanceAlert(const lt::performance_alert *p) const , Log::INFO); } -void TorrentImpl::handleTempPathChanged() -{ - adjustActualSavePath(); -} - -void TorrentImpl::handleCategorySavePathChanged() +void TorrentImpl::handleCategoryOptionsChanged() { if (m_useAutoTMM) - move_impl(m_session->categorySavePath(m_category), MoveStorageMode::Overwrite); + adjustStorageLocation(); } void TorrentImpl::handleAppendExtensionToggled() @@ -2069,39 +2075,13 @@ void TorrentImpl::manageIncompleteFiles() } } -void TorrentImpl::adjustActualSavePath() +void TorrentImpl::adjustStorageLocation() { - if (!isMoveInProgress()) - adjustActualSavePath_impl(); - else - m_moveFinishedTriggers.append([this]() { adjustActualSavePath_impl(); }); -} + const QString downloadPath = this->downloadPath(); + const bool isFinished = isSeed() || m_hasSeedStatus; + const QDir targetDir {((isFinished || downloadPath.isEmpty()) ? savePath() : downloadPath)}; -void TorrentImpl::adjustActualSavePath_impl() -{ - const bool needUseTempDir = useTempPath(); - const QDir tempDir {m_session->torrentTempPath(m_torrentInfo)}; - const QDir currentDir {actualStorageLocation()}; - const QDir targetDir {needUseTempDir ? tempDir : QDir {savePath()}}; - - if (targetDir == currentDir) return; - - if (!needUseTempDir) - { - if ((currentDir == tempDir) && (currentDir != QDir {m_session->tempPath()})) - { - // torrent without root folder still has it in its temporary save path - // so its temp path isn't equal to temp path root - const QString currentDirPath = currentDir.absolutePath(); - m_moveFinishedTriggers.append([currentDirPath] - { - qDebug() << "Removing torrent temp folder:" << currentDirPath; - Utils::Fs::smartRemoveEmptyFolderTree(currentDirPath); - }); - } - } - - moveStorage(Utils::Fs::toNativePath(targetDir.absolutePath()), MoveStorageMode::Overwrite); + moveStorage(targetDir.absolutePath(), MoveStorageMode::Overwrite); } lt::torrent_handle TorrentImpl::nativeHandle() const @@ -2114,11 +2094,6 @@ bool TorrentImpl::isMoveInProgress() const return m_storageIsMoving; } -bool TorrentImpl::useTempPath() const -{ - return m_session->isTempPathEnabled() && !(isSeed() || m_hasSeedStatus); -} - void TorrentImpl::updateStatus() { updateStatus(m_nativeHandle.status()); diff --git a/src/base/bittorrent/torrentimpl.h b/src/base/bittorrent/torrentimpl.h index 4bcaf9818..d419514db 100644 --- a/src/base/bittorrent/torrentimpl.h +++ b/src/base/bittorrent/torrentimpl.h @@ -99,14 +99,15 @@ namespace BitTorrent qlonglong wastedSize() const override; QString currentTracker() const override; - QString savePath(bool actual = false) const override; - QString rootPath(bool actual = false) const override; - QString contentPath(bool actual = false) const override; - - bool useTempPath() const override; - bool isAutoTMMEnabled() const override; void setAutoTMMEnabled(bool enabled) override; + QString savePath() const override; + void setSavePath(const QString &path) override; + QString downloadPath() const override; + void setDownloadPath(const QString &path) override; + QString actualStorageLocation() const override; + QString rootPath() const override; + QString contentPath() const override; QString category() const override; bool belongsToCategory(const QString &category) const override; bool setCategory(const QString &category) override; @@ -201,7 +202,6 @@ namespace BitTorrent void setFirstLastPiecePriority(bool enabled) override; void pause() override; void resume(TorrentOperatingMode mode = TorrentOperatingMode::AutoManaged) override; - void move(QString path) override; void forceReannounce(int index = -1) override; void forceDHTAnnounce() override; void forceRecheck() override; @@ -232,15 +232,13 @@ namespace BitTorrent void handleAlert(const lt::alert *a); void handleStateUpdate(const lt::torrent_status &nativeStatus); - void handleTempPathChanged(); - void handleCategorySavePathChanged(); + void handleDownloadPathChanged(); + void handleCategoryOptionsChanged(); void handleAppendExtensionToggled(); void saveResumeData(); void handleMoveStorageJobFinished(bool hasOutstandingJob); void fileSearchFinished(const QString &savePath, const QStringList &fileNames); - QString actualStorageLocation() const; - private: using EventTrigger = std::function; @@ -272,9 +270,7 @@ namespace BitTorrent void setAutoManaged(bool enable); - void adjustActualSavePath(); - void adjustActualSavePath_impl(); - void move_impl(QString path, MoveStorageMode mode); + void adjustStorageLocation(); void moveStorage(const QString &newPath, MoveStorageMode mode); void manageIncompleteFiles(); void applyFirstLastPiecePriority(bool enabled, const QVector &updatedFilePrio = {}); @@ -308,6 +304,7 @@ namespace BitTorrent // Persistent data QString m_name; QString m_savePath; + QString m_downloadPath; QString m_category; TagSet m_tags; qreal m_ratioLimit; diff --git a/src/base/profile.cpp b/src/base/profile.cpp index 217a9e7cc..289df87b9 100644 --- a/src/base/profile.cpp +++ b/src/base/profile.cpp @@ -88,8 +88,6 @@ QString Profile::location(const SpecialFolder folder) const break; } - if (!result.endsWith(QLatin1Char('/'))) - result += QLatin1Char('/'); return result; } diff --git a/src/base/torrentfileswatcher.cpp b/src/base/torrentfileswatcher.cpp index f879f0803..83d230e87 100644 --- a/src/base/torrentfileswatcher.cpp +++ b/src/base/torrentfileswatcher.cpp @@ -74,6 +74,8 @@ const QString OPTION_RECURSIVE {QStringLiteral("recursive")}; const QString PARAM_CATEGORY {QStringLiteral("category")}; const QString PARAM_TAGS {QStringLiteral("tags")}; const QString PARAM_SAVEPATH {QStringLiteral("save_path")}; +const QString PARAM_USEDOWNLOADPATH {QStringLiteral("use_download_path")}; +const QString PARAM_DOWNLOADPATH {QStringLiteral("download_path")}; const QString PARAM_OPERATINGMODE {QStringLiteral("operating_mode")}; const QString PARAM_STOPPED {QStringLiteral("stopped")}; const QString PARAM_SKIPCHECKING {QStringLiteral("skip_checking")}; @@ -136,6 +138,8 @@ namespace params.category = jsonObj.value(PARAM_CATEGORY).toString(); params.tags = parseTagSet(jsonObj.value(PARAM_TAGS).toArray()); params.savePath = jsonObj.value(PARAM_SAVEPATH).toString(); + params.useDownloadPath = getOptionalBool(jsonObj, PARAM_USEDOWNLOADPATH); + params.downloadPath = jsonObj.value(PARAM_DOWNLOADPATH).toString(); params.addForced = (getEnum(jsonObj, PARAM_OPERATINGMODE) == BitTorrent::TorrentOperatingMode::Forced); params.addPaused = getOptionalBool(jsonObj, PARAM_STOPPED); params.skipChecking = jsonObj.value(PARAM_SKIPCHECKING).toBool(); @@ -155,6 +159,7 @@ namespace {PARAM_CATEGORY, params.category}, {PARAM_TAGS, serializeTagSet(params.tags)}, {PARAM_SAVEPATH, params.savePath}, + {PARAM_DOWNLOADPATH, params.downloadPath}, {PARAM_OPERATINGMODE, Utils::String::fromEnum(params.addForced ? BitTorrent::TorrentOperatingMode::Forced : BitTorrent::TorrentOperatingMode::AutoManaged)}, {PARAM_SKIPCHECKING, params.skipChecking}, @@ -170,6 +175,8 @@ namespace jsonObj[PARAM_CONTENTLAYOUT] = Utils::String::fromEnum(*params.contentLayout); if (params.useAutoTMM) jsonObj[PARAM_AUTOTMM] = *params.useAutoTMM; + if (params.useDownloadPath) + jsonObj[PARAM_USEDOWNLOADPATH] = *params.useDownloadPath; return jsonObj; } diff --git a/src/base/torrentfilter.cpp b/src/base/torrentfilter.cpp index 928f38807..c7de80825 100644 --- a/src/base/torrentfilter.cpp +++ b/src/base/torrentfilter.cpp @@ -127,7 +127,7 @@ bool TorrentFilter::setCategory(const QString &category) if ((m_category != category) || (m_category.isNull() && !category.isNull()) || (!m_category.isNull() && category.isNull())) - { + { m_category = category; return true; } @@ -141,7 +141,7 @@ bool TorrentFilter::setTag(const QString &tag) if ((m_tag != tag) || (m_tag.isNull() && !tag.isNull()) || (!m_tag.isNull() && tag.isNull())) - { + { m_tag = tag; return true; } diff --git a/src/base/utils/fs.cpp b/src/base/utils/fs.cpp index 5ff572e09..c2dc19aa4 100644 --- a/src/base/utils/fs.cpp +++ b/src/base/utils/fs.cpp @@ -72,9 +72,14 @@ QString Utils::Fs::toUniformPath(const QString &path) return QDir::fromNativeSeparators(path); } -/** - * Returns the file extension part of a file name. - */ +QString Utils::Fs::resolvePath(const QString &relativePath, const QString &basePath) +{ + Q_ASSERT(QDir::isRelativePath(relativePath)); + Q_ASSERT(QDir::isAbsolutePath(basePath)); + + return (relativePath.isEmpty() ? basePath : QDir(basePath).absoluteFilePath(relativePath)); +} + QString Utils::Fs::fileExtension(const QString &filename) { const QString name = filename.endsWith(QB_EXT) diff --git a/src/base/utils/fs.h b/src/base/utils/fs.h index 9a6d28dab..676fa5b0a 100644 --- a/src/base/utils/fs.h +++ b/src/base/utils/fs.h @@ -42,6 +42,7 @@ namespace Utils::Fs * with the OS being run. */ QString toNativePath(const QString &path); + /** * Converts a path to a string suitable for processing. * This function makes sure the directory separator used is independent @@ -50,7 +51,16 @@ namespace Utils::Fs */ QString toUniformPath(const QString &path); + /** + * If `path is relative then resolves it against `basePath`, otherwise returns the `path` itself + */ + QString resolvePath(const QString &relativePath, const QString &basePath); + + /** + * Returns the file extension part of a file name. + */ QString fileExtension(const QString &filename); + QString fileName(const QString &filePath); QString folderName(const QString &filePath); qint64 computePathSize(const QString &path); diff --git a/src/gui/addnewtorrentdialog.cpp b/src/gui/addnewtorrentdialog.cpp index e97fa0180..7ffbeb91e 100644 --- a/src/gui/addnewtorrentdialog.cpp +++ b/src/gui/addnewtorrentdialog.cpp @@ -66,6 +66,7 @@ namespace const QString KEY_ENABLED = QStringLiteral(SETTINGS_KEY("Enabled")); const QString KEY_TOPLEVEL = QStringLiteral(SETTINGS_KEY("TopLevel")); const QString KEY_SAVEPATHHISTORY = QStringLiteral(SETTINGS_KEY("SavePathHistory")); + const QString KEY_DOWNLOADPATHHISTORY = QStringLiteral(SETTINGS_KEY("DownloadPathHistory")); const QString KEY_SAVEPATHHISTORYLENGTH = QStringLiteral(SETTINGS_KEY("SavePathHistoryLength")); // just a shortcut @@ -118,6 +119,45 @@ namespace const BitTorrent::TorrentInfo &m_torrentInfo; QStringList &m_filePaths; }; + + // savePath is a folder, not an absolute file path + int indexOfPath(const FileSystemPathComboEdit *fsPathEdit, const QString &savePath) + { + const QDir saveDir {savePath}; + for (int i = 0; i < fsPathEdit->count(); ++i) + { + if (QDir(fsPathEdit->item(i)) == saveDir) + return i; + } + return -1; + } + + void setPath(FileSystemPathComboEdit *fsPathEdit, const QString &newPath) + { + int existingIndex = indexOfPath(fsPathEdit, newPath); + if (existingIndex < 0) + { + // New path, prepend to combo box + fsPathEdit->insertItem(0, newPath); + existingIndex = 0; + } + + fsPathEdit->setCurrentIndex(existingIndex); + } + + void updatePathHistory(const QString &settingsKey, const QString &path, const int maxLength) + { + // Add last used save path to the front of history + + auto pathList = settings()->loadValue(settingsKey); + const int selectedSavePathIndex = pathList.indexOf(path); + if (selectedSavePathIndex > 0) + pathList.removeAt(selectedSavePathIndex); + if (selectedSavePathIndex != 0) + pathList.prepend(path); + + settings()->storeValue(settingsKey, QStringList(pathList.mid(0, maxLength))); + } } const int AddNewTorrentDialog::minPathHistoryLength; @@ -153,13 +193,19 @@ AddNewTorrentDialog::AddNewTorrentDialog(const BitTorrent::AddTorrentParams &inP const auto *session = BitTorrent::Session::instance(); + m_ui->downloadPath->setMode(FileSystemPathEdit::Mode::DirectorySave); + m_ui->downloadPath->setDialogCaption(tr("Choose save path")); + m_ui->downloadPath->setMaxVisibleItems(20); + m_ui->startTorrentCheckBox->setChecked(!m_torrentParams.addPaused.value_or(session->isAddTorrentPaused())); m_ui->comboTTM->blockSignals(true); // the TreeView size isn't correct if the slot does it job at this point - m_ui->comboTTM->setCurrentIndex(!session->isAutoTMMDisabledByDefault()); + m_ui->comboTTM->setCurrentIndex(session->isAutoTMMDisabledByDefault() ? 0 : 1); m_ui->comboTTM->blockSignals(false); - populateSavePathComboBox(); + connect(m_ui->savePath, &FileSystemPathEdit::selectedPathChanged, this, &AddNewTorrentDialog::onSavePathChanged); + connect(m_ui->downloadPath, &FileSystemPathEdit::selectedPathChanged, this, &AddNewTorrentDialog::onDownloadPathChanged); + connect(m_ui->groupBoxDownloadPath, &QGroupBox::toggled, this, &AddNewTorrentDialog::onUseDownloadPathChanged); m_ui->checkBoxRememberLastSavePath->setChecked(m_storeRememberLastSavePath); @@ -174,7 +220,7 @@ AddNewTorrentDialog::AddNewTorrentDialog(const BitTorrent::AddTorrentParams &inP m_ui->doNotDeleteTorrentCheckBox->setVisible(TorrentFileGuard::autoDeleteMode() != TorrentFileGuard::Never); // Load categories - QStringList categories = session->categories().keys(); + QStringList categories = session->categories(); std::sort(categories.begin(), categories.end(), Utils::Compare::NaturalLessThan()); const QString defaultCategory = m_storeDefaultCategory; @@ -342,6 +388,7 @@ bool AddNewTorrentDialog::loadTorrentImpl() m_ui->labelInfohash2Data->setText(m_torrentInfo.infoHash().v2().isValid() ? m_torrentInfo.infoHash().v2().toString() : tr("N/A")); setupTreeview(); TMMChanged(m_ui->comboTTM->currentIndex()); + return true; } @@ -407,36 +454,6 @@ void AddNewTorrentDialog::showEvent(QShowEvent *event) raise(); } -void AddNewTorrentDialog::saveSavePathHistory() const -{ - // Get current history - auto history = settings()->loadValue(KEY_SAVEPATHHISTORY); - QVector historyDirs; - for (const QString &path : asConst(history)) - historyDirs << QDir {path}; - - const QDir selectedSavePath {m_ui->savePath->selectedPath()}; - const int selectedSavePathIndex = historyDirs.indexOf(selectedSavePath); - if (selectedSavePathIndex > 0) - history.removeAt(selectedSavePathIndex); - if (selectedSavePathIndex != 0) - // Add last used save path to the front of history - history.push_front(selectedSavePath.absolutePath()); - - // Save history - settings()->storeValue(KEY_SAVEPATHHISTORY, QStringList {history.mid(0, savePathHistoryLength())}); -} - -// savePath is a folder, not an absolute file path -int AddNewTorrentDialog::indexOfSavePath(const QString &savePath) -{ - QDir saveDir(savePath); - for (int i = 0; i < m_ui->savePath->count(); ++i) - if (QDir(m_ui->savePath->item(i)) == saveDir) - return i; - return -1; -} - void AddNewTorrentDialog::updateDiskSpaceLabel() { // Determine torrent size @@ -470,18 +487,42 @@ void AddNewTorrentDialog::onSavePathChanged(const QString &newPath) { Q_UNUSED(newPath); // Remember index - m_oldIndex = m_ui->savePath->currentIndex(); + m_savePathIndex = m_ui->savePath->currentIndex(); updateDiskSpaceLabel(); } +void AddNewTorrentDialog::onDownloadPathChanged(const QString &newPath) +{ + Q_UNUSED(newPath); + // Remember index + const int currentPathIndex = m_ui->downloadPath->currentIndex(); + if (currentPathIndex >= 0) + m_downloadPathIndex = m_ui->downloadPath->currentIndex(); +} + +void AddNewTorrentDialog::onUseDownloadPathChanged(const bool checked) +{ + m_useDownloadPath = checked; + m_ui->downloadPath->setCurrentIndex(checked ? m_downloadPathIndex : -1); +} + void AddNewTorrentDialog::categoryChanged(int index) { Q_UNUSED(index); if (m_ui->comboTTM->currentIndex() == 1) { - QString savePath = BitTorrent::Session::instance()->categorySavePath(m_ui->categoryComboBox->currentText()); + const auto *btSession = BitTorrent::Session::instance(); + const QString categoryName = m_ui->categoryComboBox->currentText(); + + const QString savePath = btSession->categorySavePath(categoryName); m_ui->savePath->setSelectedPath(Utils::Fs::toNativePath(savePath)); + + const QString downloadPath = btSession->categoryDownloadPath(categoryName); + m_ui->downloadPath->setSelectedPath(Utils::Fs::toNativePath(downloadPath)); + + m_ui->groupBoxDownloadPath->setChecked(!m_ui->downloadPath->selectedPath().isEmpty()); + updateDiskSpaceLabel(); } } @@ -511,19 +552,6 @@ void AddNewTorrentDialog::contentLayoutChanged(const int index) } } -void AddNewTorrentDialog::setSavePath(const QString &newPath) -{ - int existingIndex = indexOfSavePath(newPath); - if (existingIndex < 0) - { - // New path, prepend to combo box - m_ui->savePath->insertItem(0, newPath); - existingIndex = 0; - } - m_ui->savePath->setCurrentIndex(existingIndex); - onSavePathChanged(newPath); -} - void AddNewTorrentDialog::saveTorrentFile() { Q_ASSERT(hasMetadata()); @@ -553,22 +581,78 @@ bool AddNewTorrentDialog::hasMetadata() const return m_torrentInfo.isValid(); } -void AddNewTorrentDialog::populateSavePathComboBox() +void AddNewTorrentDialog::populateSavePaths() { + const auto *btSession = BitTorrent::Session::instance(); + + m_ui->savePath->blockSignals(true); m_ui->savePath->clear(); + const auto savePathHistory = settings()->loadValue(KEY_SAVEPATHHISTORY); + if (savePathHistory.size() > 0) + { + for (const QString &path : savePathHistory) + m_ui->savePath->addItem(path); + } + else + { + m_ui->savePath->addItem(btSession->savePath()); + } - // Load save path history - const auto savePathHistory {settings()->loadValue(KEY_SAVEPATHHISTORY)}; - for (const QString &savePath : savePathHistory) - m_ui->savePath->addItem(savePath); + if (m_savePathIndex >= 0) + { + m_ui->savePath->setCurrentIndex(std::min(m_savePathIndex, (m_ui->savePath->count() - 1))); + } + else + { + if (!m_torrentParams.savePath.isEmpty()) + setPath(m_ui->savePath, m_torrentParams.savePath); + else if (!m_storeRememberLastSavePath) + setPath(m_ui->savePath, btSession->savePath()); + else + m_ui->savePath->setCurrentIndex(0); - const QString defSavePath {BitTorrent::Session::instance()->defaultSavePath()}; + m_savePathIndex = m_ui->savePath->currentIndex(); + } - if (!m_torrentParams.savePath.isEmpty()) - setSavePath(m_torrentParams.savePath); - else if (!m_storeRememberLastSavePath) - setSavePath(defSavePath); - // else last used save path will be selected since it is the first in the list + m_ui->savePath->blockSignals(false); + + m_ui->downloadPath->blockSignals(true); + m_ui->downloadPath->clear(); + const auto downloadPathHistory = settings()->loadValue(KEY_DOWNLOADPATHHISTORY); + if (downloadPathHistory.size() > 0) + { + for (const QString &path : downloadPathHistory) + m_ui->downloadPath->addItem(path); + } + else + { + m_ui->downloadPath->addItem(btSession->downloadPath()); + } + + if (m_downloadPathIndex >= 0) + { + m_ui->downloadPath->setCurrentIndex(m_useDownloadPath ? std::min(m_downloadPathIndex, (m_ui->downloadPath->count() - 1)) : -1); + m_ui->groupBoxDownloadPath->setChecked(m_useDownloadPath); + } + else + { + const bool useDownloadPath = m_torrentParams.useDownloadPath.value_or(btSession->isDownloadPathEnabled()); + m_ui->groupBoxDownloadPath->setChecked(useDownloadPath); + + if (!m_torrentParams.downloadPath.isEmpty()) + setPath(m_ui->downloadPath, m_torrentParams.downloadPath); + else if (!m_storeRememberLastSavePath) + setPath(m_ui->downloadPath, btSession->downloadPath()); + else + m_ui->downloadPath->setCurrentIndex(0); + + m_downloadPathIndex = m_ui->downloadPath->currentIndex(); + if (!useDownloadPath) + m_ui->downloadPath->setCurrentIndex(-1); + } + + m_ui->downloadPath->blockSignals(false); + m_ui->groupBoxDownloadPath->blockSignals(false); } void AddNewTorrentDialog::displayContentTreeMenu(const QPoint &) @@ -692,16 +776,21 @@ void AddNewTorrentDialog::accept() m_torrentParams.sequential = m_ui->sequentialCheckBox->isChecked(); m_torrentParams.firstLastPiecePriority = m_ui->firstLastCheckBox->isChecked(); - QString savePath = m_ui->savePath->selectedPath(); - if (m_ui->comboTTM->currentIndex() != 1) - { // 0 is Manual mode and 1 is Automatic mode. Handle all non 1 values as manual mode. - m_torrentParams.useAutoTMM = false; - m_torrentParams.savePath = savePath; - saveSavePathHistory(); - } - else + const bool useAutoTMM = (m_ui->comboTTM->currentIndex() == 1); // 1 is Automatic mode. Handle all non 1 values as manual mode. + m_torrentParams.useAutoTMM = useAutoTMM; + if (!useAutoTMM) { - m_torrentParams.useAutoTMM = true; + const QString savePath = m_ui->savePath->selectedPath(); + m_torrentParams.savePath = savePath; + updatePathHistory(KEY_SAVEPATHHISTORY, savePath, savePathHistoryLength()); + + m_torrentParams.useDownloadPath = m_ui->groupBoxDownloadPath->isChecked(); + if (m_torrentParams.useDownloadPath) + { + const QString downloadPath = m_ui->downloadPath->selectedPath(); + m_torrentParams.downloadPath = downloadPath; + updatePathHistory(KEY_DOWNLOADPATHHISTORY, downloadPath, savePathHistoryLength()); + } } setEnabled(!m_ui->checkBoxNeverShow->isChecked()); @@ -853,18 +942,28 @@ void AddNewTorrentDialog::TMMChanged(int index) { if (index != 1) { // 0 is Manual mode and 1 is Automatic mode. Handle all non 1 values as manual mode. - populateSavePathComboBox(); + populateSavePaths(); m_ui->groupBoxSavePath->setEnabled(true); - m_ui->savePath->blockSignals(false); - m_ui->savePath->setCurrentIndex(m_oldIndex < m_ui->savePath->count() ? m_oldIndex : m_ui->savePath->count() - 1); } else { + const auto *session = BitTorrent::Session::instance(); + m_ui->groupBoxSavePath->setEnabled(false); + m_ui->savePath->blockSignals(true); m_ui->savePath->clear(); - QString savePath = BitTorrent::Session::instance()->categorySavePath(m_ui->categoryComboBox->currentText()); + const QString savePath = session->categorySavePath(m_ui->categoryComboBox->currentText()); m_ui->savePath->addItem(savePath); + + m_ui->downloadPath->blockSignals(true); + m_ui->downloadPath->clear(); + const QString downloadPath = session->categoryDownloadPath(m_ui->categoryComboBox->currentText()); + m_ui->downloadPath->addItem(downloadPath); + + m_ui->groupBoxDownloadPath->blockSignals(true); + m_ui->groupBoxDownloadPath->setChecked(!downloadPath.isEmpty()); + updateDiskSpaceLabel(); } } diff --git a/src/gui/addnewtorrentdialog.h b/src/gui/addnewtorrentdialog.h index 4df53b491..3fc6cf4ae 100644 --- a/src/gui/addnewtorrentdialog.h +++ b/src/gui/addnewtorrentdialog.h @@ -81,6 +81,8 @@ private slots: void displayContentTreeMenu(const QPoint &); void updateDiskSpaceLabel(); void onSavePathChanged(const QString &newPath); + void onDownloadPathChanged(const QString &newPath); + void onUseDownloadPathChanged(bool checked); void updateMetadata(const BitTorrent::TorrentInfo &metadata); void handleDownloadFinished(const Net::DownloadResult &downloadResult); void TMMChanged(int index); @@ -97,14 +99,11 @@ private: bool loadTorrentFile(const QString &torrentPath); bool loadTorrentImpl(); bool loadMagnet(const BitTorrent::MagnetUri &magnetUri); - void populateSavePathComboBox(); - void saveSavePathHistory() const; - int indexOfSavePath(const QString &savePath); + void populateSavePaths(); void loadState(); void saveState(); void setMetadataProgressIndicator(bool visibleIndicator, const QString &labelText = {}); void setupTreeview(); - void setSavePath(const QString &newPath); void saveTorrentFile(); bool hasMetadata() const; @@ -115,7 +114,9 @@ private: PropListDelegate *m_contentDelegate = nullptr; BitTorrent::MagnetUri m_magnetURI; BitTorrent::TorrentInfo m_torrentInfo; - int m_oldIndex = 0; + int m_savePathIndex = -1; + int m_downloadPathIndex = -1; + bool m_useDownloadPath = false; std::unique_ptr m_torrentGuard; BitTorrent::AddTorrentParams m_torrentParams; diff --git a/src/gui/addnewtorrentdialog.ui b/src/gui/addnewtorrentdialog.ui index 992109abc..9a444a3a5 100644 --- a/src/gui/addnewtorrentdialog.ui +++ b/src/gui/addnewtorrentdialog.ui @@ -83,13 +83,48 @@ - - - - Remember last used save path + + + + Use another path for incomplete torrent + + true + + + false + + + + + + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + Remember last used save path + + + + + diff --git a/src/gui/categoryfiltermodel.cpp b/src/gui/categoryfiltermodel.cpp index 8e288a449..af09ae8db 100644 --- a/src/gui/categoryfiltermodel.cpp +++ b/src/gui/categoryfiltermodel.cpp @@ -404,40 +404,33 @@ void CategoryFilterModel::populate() // Uncategorized torrents using Torrent = BitTorrent::Torrent; - m_rootItem->addChild( - UID_UNCATEGORIZED - , new CategoryModelItem( - nullptr, tr("Uncategorized") - , std::count_if(torrents.begin(), torrents.end() - , [](Torrent *torrent) { return torrent->category().isEmpty(); }))); + const int torrentsCount = std::count_if(torrents.begin(), torrents.end() + , [](Torrent *torrent) { return torrent->category().isEmpty(); }); + m_rootItem->addChild(UID_UNCATEGORIZED, new CategoryModelItem(nullptr, tr("Uncategorized"), torrentsCount)); - using Torrent = BitTorrent::Torrent; - const QStringMap categories = session->categories(); - for (auto i = categories.cbegin(); i != categories.cend(); ++i) + using BitTorrent::Torrent; + for (const QString &categoryName : asConst(session->categories())) { - const QString &category = i.key(); if (m_isSubcategoriesEnabled) { CategoryModelItem *parent = m_rootItem; - for (const QString &subcat : asConst(session->expandCategory(category))) + for (const QString &subcat : asConst(session->expandCategory(categoryName))) { const QString subcatName = shortName(subcat); if (!parent->hasChild(subcatName)) { - new CategoryModelItem( - parent, subcatName - , std::count_if(torrents.cbegin(), torrents.cend() - , [subcat](Torrent *torrent) { return torrent->category() == subcat; })); + const int torrentsCount = std::count_if(torrents.cbegin(), torrents.cend() + , [subcat](Torrent *torrent) { return torrent->category() == subcat; }); + new CategoryModelItem(parent, subcatName, torrentsCount); } parent = parent->child(subcatName); } } else { - new CategoryModelItem( - m_rootItem, category - , std::count_if(torrents.begin(), torrents.end() - , [category](Torrent *torrent) { return torrent->belongsToCategory(category); })); + const int torrentsCount = std::count_if(torrents.begin(), torrents.end() + , [categoryName](Torrent *torrent) { return torrent->belongsToCategory(categoryName); }); + new CategoryModelItem(m_rootItem, categoryName, torrentsCount); } } } diff --git a/src/gui/categoryfilterwidget.cpp b/src/gui/categoryfilterwidget.cpp index 1a2a5efc4..ed2a8e34d 100644 --- a/src/gui/categoryfilterwidget.cpp +++ b/src/gui/categoryfilterwidget.cpp @@ -215,7 +215,7 @@ void CategoryFilterWidget::removeCategory() void CategoryFilterWidget::removeUnusedCategories() { auto session = BitTorrent::Session::instance(); - for (const QString &category : asConst(session->categories().keys())) + for (const QString &category : asConst(session->categories())) { if (model()->data(static_cast(model())->index(category), Qt::UserRole) == 0) session->removeCategory(category); diff --git a/src/gui/optionsdialog.cpp b/src/gui/optionsdialog.cpp index b5c8bb2cc..69e49a3d1 100644 --- a/src/gui/optionsdialog.cpp +++ b/src/gui/optionsdialog.cpp @@ -354,7 +354,7 @@ OptionsDialog::OptionsDialog(QWidget *parent) connect(m_ui->comboTorrentCategoryChanged, qComboBoxCurrentIndexChanged, this, &ThisType::enableApplyButton); connect(m_ui->comboCategoryDefaultPathChanged, qComboBoxCurrentIndexChanged, this, &ThisType::enableApplyButton); connect(m_ui->comboCategoryChanged, qComboBoxCurrentIndexChanged, this, &ThisType::enableApplyButton); - connect(m_ui->textTempPath, &FileSystemPathEdit::selectedPathChanged, this, &ThisType::enableApplyButton); + connect(m_ui->textDownloadPath, &FileSystemPathEdit::selectedPathChanged, this, &ThisType::enableApplyButton); connect(m_ui->checkAppendqB, &QAbstractButton::toggled, this, &ThisType::enableApplyButton); connect(m_ui->checkPreallocateAll, &QAbstractButton::toggled, this, &ThisType::enableApplyButton); connect(m_ui->checkRecursiveDownload, &QAbstractButton::toggled, this, &ThisType::enableApplyButton); @@ -372,8 +372,8 @@ OptionsDialog::OptionsDialog(QWidget *parent) connect(m_ui->textExportDirFin, &FileSystemPathEdit::selectedPathChanged, this, &ThisType::enableApplyButton); connect(m_ui->actionTorrentDlOnDblClBox, qComboBoxCurrentIndexChanged, this, &ThisType::enableApplyButton); connect(m_ui->actionTorrentFnOnDblClBox, qComboBoxCurrentIndexChanged, this, &ThisType::enableApplyButton); - connect(m_ui->checkTempFolder, &QAbstractButton::toggled, this, &ThisType::enableApplyButton); - connect(m_ui->checkTempFolder, &QAbstractButton::toggled, m_ui->textTempPath, &QWidget::setEnabled); + connect(m_ui->checkUseDownloadPath, &QAbstractButton::toggled, this, &ThisType::enableApplyButton); + connect(m_ui->checkUseDownloadPath, &QAbstractButton::toggled, m_ui->textDownloadPath, &QWidget::setEnabled); connect(m_ui->addWatchedFolderButton, &QAbstractButton::clicked, this, &ThisType::enableApplyButton); connect(m_ui->removeWatchedFolderButton, &QAbstractButton::clicked, this, &ThisType::enableApplyButton); connect(m_ui->groupMailNotification, &QGroupBox::toggled, this, &ThisType::enableApplyButton); @@ -559,8 +559,8 @@ OptionsDialog::OptionsDialog(QWidget *parent) m_ui->textSavePath->setDialogCaption(tr("Choose a save directory")); m_ui->textSavePath->setMode(FileSystemPathEdit::Mode::DirectorySave); - m_ui->textTempPath->setDialogCaption(tr("Choose a save directory")); - m_ui->textTempPath->setMode(FileSystemPathEdit::Mode::DirectorySave); + m_ui->textDownloadPath->setDialogCaption(tr("Choose a save directory")); + m_ui->textDownloadPath->setMode(FileSystemPathEdit::Mode::DirectorySave); // disable mouse wheel event on widgets to avoid mis-selection auto *wheelEventEater = new WheelEventEater(this); @@ -731,14 +731,14 @@ void OptionsDialog::saveOptions() auto session = BitTorrent::Session::instance(); // Downloads preferences - session->setDefaultSavePath(Utils::Fs::expandPathAbs(m_ui->textSavePath->selectedPath())); + session->setSavePath(Utils::Fs::expandPathAbs(m_ui->textSavePath->selectedPath())); session->setSubcategoriesEnabled(m_ui->checkUseSubcategories->isChecked()); session->setAutoTMMDisabledByDefault(m_ui->comboSavingMode->currentIndex() == 0); session->setDisableAutoTMMWhenCategoryChanged(m_ui->comboTorrentCategoryChanged->currentIndex() == 1); session->setDisableAutoTMMWhenCategorySavePathChanged(m_ui->comboCategoryChanged->currentIndex() == 1); session->setDisableAutoTMMWhenDefaultSavePathChanged(m_ui->comboCategoryDefaultPathChanged->currentIndex() == 1); - session->setTempPathEnabled(m_ui->checkTempFolder->isChecked()); - session->setTempPath(Utils::Fs::expandPathAbs(m_ui->textTempPath->selectedPath())); + session->setDownloadPathEnabled(m_ui->checkUseDownloadPath->isChecked()); + session->setDownloadPath(Utils::Fs::expandPathAbs(m_ui->textDownloadPath->selectedPath())); session->setAppendExtensionEnabled(m_ui->checkAppendqB->isChecked()); session->setPreallocationEnabled(preAllocateAllFiles()); pref->disableRecursiveDownload(!m_ui->checkRecursiveDownload->isChecked()); @@ -997,16 +997,16 @@ void OptionsDialog::loadOptions() m_ui->deleteTorrentBox->setChecked(autoDeleteMode != TorrentFileGuard::Never); m_ui->deleteCancelledTorrentBox->setChecked(autoDeleteMode == TorrentFileGuard::Always); - m_ui->textSavePath->setSelectedPath(session->defaultSavePath()); + m_ui->textSavePath->setSelectedPath(session->savePath()); m_ui->checkUseSubcategories->setChecked(session->isSubcategoriesEnabled()); m_ui->comboSavingMode->setCurrentIndex(!session->isAutoTMMDisabledByDefault()); m_ui->comboTorrentCategoryChanged->setCurrentIndex(session->isDisableAutoTMMWhenCategoryChanged()); m_ui->comboCategoryChanged->setCurrentIndex(session->isDisableAutoTMMWhenCategorySavePathChanged()); m_ui->comboCategoryDefaultPathChanged->setCurrentIndex(session->isDisableAutoTMMWhenDefaultSavePathChanged()); - m_ui->checkTempFolder->setChecked(session->isTempPathEnabled()); - m_ui->textTempPath->setEnabled(m_ui->checkTempFolder->isChecked()); - m_ui->textTempPath->setEnabled(m_ui->checkTempFolder->isChecked()); - m_ui->textTempPath->setSelectedPath(Utils::Fs::toNativePath(session->tempPath())); + m_ui->checkUseDownloadPath->setChecked(session->isDownloadPathEnabled()); + m_ui->textDownloadPath->setEnabled(m_ui->checkUseDownloadPath->isChecked()); + m_ui->textDownloadPath->setEnabled(m_ui->checkUseDownloadPath->isChecked()); + m_ui->textDownloadPath->setSelectedPath(Utils::Fs::toNativePath(session->downloadPath())); m_ui->checkAppendqB->setChecked(session->isAppendExtensionEnabled()); m_ui->checkPreallocateAll->setChecked(session->isPreallocationEnabled()); m_ui->checkRecursiveDownload->setChecked(!pref->recursiveDownloadDisabled()); diff --git a/src/gui/optionsdialog.ui b/src/gui/optionsdialog.ui index c7dd4b510..a53b10df6 100644 --- a/src/gui/optionsdialog.ui +++ b/src/gui/optionsdialog.ui @@ -1105,16 +1105,6 @@ Manual: Various torrent properties (e.g. save path) must be assigned manually - - - - - - - Copy .torrent files for finished downloads to: - - - @@ -1122,6 +1112,19 @@ Manual: Various torrent properties (e.g. save path) must be assigned manually + + + + + + + Use another path for incomplete torrents: + + + + + + @@ -1129,21 +1132,18 @@ Manual: Various torrent properties (e.g. save path) must be assigned manually - - - - Keep incomplete torrents in: - - + + - - - - - + + + + Copy .torrent files for finished downloads to: + + @@ -3490,8 +3490,8 @@ Use ';' to split multiple entries. Can use wildcard '*'. checkAdditionDialog checkAdditionDialogFront checkPreallocateAll - checkTempFolder - textTempPath + checkUseDownloadPath + textDownloadPath checkAppendqB scanFoldersView addWatchedFolderButton diff --git a/src/gui/properties/propertieswidget.cpp b/src/gui/properties/propertieswidget.cpp index 573ad90b3..829a58311 100644 --- a/src/gui/properties/propertieswidget.cpp +++ b/src/gui/properties/propertieswidget.cpp @@ -581,11 +581,12 @@ void PropertiesWidget::loadUrlSeeds() QString PropertiesWidget::getFullPath(const QModelIndex &index) const { + const QDir saveDir {m_torrent->actualStorageLocation()}; + if (m_propListModel->itemType(index) == TorrentContentModelItem::FileType) { const int fileIdx = m_propListModel->getFileIndex(index); const QString filename {m_torrent->filePath(fileIdx)}; - const QDir saveDir {m_torrent->savePath(true)}; const QString fullPath {Utils::Fs::expandPath(saveDir.absoluteFilePath(filename))}; return fullPath; } @@ -596,7 +597,6 @@ QString PropertiesWidget::getFullPath(const QModelIndex &index) const for (QModelIndex modelIdx = m_propListModel->parent(nameIndex); modelIdx.isValid(); modelIdx = modelIdx.parent()) folderPath.prepend(modelIdx.data().toString() + '/'); - const QDir saveDir {m_torrent->savePath(true)}; const QString fullPath {Utils::Fs::expandPath(saveDir.absoluteFilePath(folderPath))}; return fullPath; } diff --git a/src/gui/rss/automatedrssdownloader.cpp b/src/gui/rss/automatedrssdownloader.cpp index c8e1f2125..03c8ea67f 100644 --- a/src/gui/rss/automatedrssdownloader.cpp +++ b/src/gui/rss/automatedrssdownloader.cpp @@ -331,7 +331,7 @@ void AutomatedRssDownloader::clearRuleDefinitionBox() void AutomatedRssDownloader::initCategoryCombobox() { // Load torrent categories - QStringList categories = BitTorrent::Session::instance()->categories().keys(); + QStringList categories = BitTorrent::Session::instance()->categories(); std::sort(categories.begin(), categories.end(), Utils::Compare::NaturalLessThan()); m_ui->comboCategory->addItem(""); m_ui->comboCategory->addItems(categories); diff --git a/src/gui/torrentcategorydialog.cpp b/src/gui/torrentcategorydialog.cpp index 076776d05..728610a3c 100644 --- a/src/gui/torrentcategorydialog.cpp +++ b/src/gui/torrentcategorydialog.cpp @@ -1,6 +1,6 @@ /* * Bittorrent Client using Qt and libtorrent. - * Copyright (C) 2017 Vladimir Golovnev + * Copyright (C) 2017, 2021 Vladimir Golovnev * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License @@ -32,6 +32,7 @@ #include #include "base/bittorrent/session.h" +#include "base/utils/fs.h" #include "ui_torrentcategorydialog.h" TorrentCategoryDialog::TorrentCategoryDialog(QWidget *parent) @@ -39,15 +40,20 @@ TorrentCategoryDialog::TorrentCategoryDialog(QWidget *parent) , m_ui {new Ui::TorrentCategoryDialog} { m_ui->setupUi(this); + m_ui->comboSavePath->setMode(FileSystemPathEdit::Mode::DirectorySave); m_ui->comboSavePath->setDialogCaption(tr("Choose save path")); + m_ui->comboDownloadPath->setMode(FileSystemPathEdit::Mode::DirectorySave); + m_ui->comboDownloadPath->setDialogCaption(tr("Choose download path")); + m_ui->comboDownloadPath->setEnabled(false); + m_ui->labelDownloadPath->setEnabled(false); + // disable save button m_ui->buttonBox->button(QDialogButtonBox::Ok)->setEnabled(false); - connect(m_ui->textCategoryName, &QLineEdit::textChanged, this, [this](const QString &text) - { - m_ui->buttonBox->button(QDialogButtonBox::Ok)->setEnabled(!text.isEmpty()); - }); + + connect(m_ui->textCategoryName, &QLineEdit::textChanged, this, &TorrentCategoryDialog::categoryNameChanged); + connect(m_ui->comboUseDownloadPath, &QComboBox::currentIndexChanged, this, &TorrentCategoryDialog::useDownloadPathChanged); } TorrentCategoryDialog::~TorrentCategoryDialog() @@ -57,20 +63,21 @@ TorrentCategoryDialog::~TorrentCategoryDialog() QString TorrentCategoryDialog::createCategory(QWidget *parent, const QString &parentCategoryName) { + using BitTorrent::CategoryOptions; using BitTorrent::Session; - QString newCategoryName {parentCategoryName}; + QString newCategoryName = parentCategoryName; if (!newCategoryName.isEmpty()) newCategoryName += QLatin1Char('/'); newCategoryName += tr("New Category"); - TorrentCategoryDialog dialog(parent); + TorrentCategoryDialog dialog {parent}; dialog.setCategoryName(newCategoryName); while (dialog.exec() == TorrentCategoryDialog::Accepted) { newCategoryName = dialog.categoryName(); - if (!BitTorrent::Session::isValidCategoryName(newCategoryName)) + if (!Session::isValidCategoryName(newCategoryName)) { QMessageBox::critical( parent, tr("Invalid category name") @@ -78,7 +85,7 @@ QString TorrentCategoryDialog::createCategory(QWidget *parent, const QString &pa "Category name cannot start/end with '/'.\n" "Category name cannot contain '//' sequence.")); } - else if (BitTorrent::Session::instance()->categories().contains(newCategoryName)) + else if (Session::instance()->categories().contains(newCategoryName)) { QMessageBox::critical( parent, tr("Category creation error") @@ -87,7 +94,7 @@ QString TorrentCategoryDialog::createCategory(QWidget *parent, const QString &pa } else { - Session::instance()->addCategory(newCategoryName, dialog.savePath()); + Session::instance()->addCategory(newCategoryName, dialog.categoryOptions()); return newCategoryName; } } @@ -105,10 +112,10 @@ void TorrentCategoryDialog::editCategory(QWidget *parent, const QString &categor dialog->setAttribute(Qt::WA_DeleteOnClose); dialog->setCategoryNameEditable(false); dialog->setCategoryName(categoryName); - dialog->setSavePath(Session::instance()->categories()[categoryName]); + dialog->setCategoryOptions(Session::instance()->categoryOptions(categoryName)); connect(dialog, &TorrentCategoryDialog::accepted, parent, [dialog, categoryName]() { - Session::instance()->editCategory(categoryName, dialog->savePath()); + Session::instance()->editCategory(categoryName, dialog->categoryOptions()); }); dialog->open(); } @@ -128,12 +135,59 @@ void TorrentCategoryDialog::setCategoryName(const QString &categoryName) m_ui->textCategoryName->setText(categoryName); } -QString TorrentCategoryDialog::savePath() const +BitTorrent::CategoryOptions TorrentCategoryDialog::categoryOptions() const { - return m_ui->comboSavePath->selectedPath(); + BitTorrent::CategoryOptions categoryOptions; + categoryOptions.savePath = m_ui->comboSavePath->selectedPath(); + if (m_ui->comboUseDownloadPath->currentIndex() == 1) + categoryOptions.downloadPath = {true, m_ui->comboDownloadPath->selectedPath()}; + else if (m_ui->comboUseDownloadPath->currentIndex() == 2) + categoryOptions.downloadPath = {false}; + + return categoryOptions; } -void TorrentCategoryDialog::setSavePath(const QString &savePath) +void TorrentCategoryDialog::setCategoryOptions(const BitTorrent::CategoryOptions &categoryOptions) { - m_ui->comboSavePath->setSelectedPath(savePath); + m_ui->comboSavePath->setSelectedPath(categoryOptions.savePath); + if (categoryOptions.downloadPath) + { + m_ui->comboUseDownloadPath->setCurrentIndex(categoryOptions.downloadPath->enabled ? 1 : 2); + m_ui->comboDownloadPath->setSelectedPath(categoryOptions.downloadPath->enabled ? categoryOptions.downloadPath->path : QString()); + } + else + { + m_ui->comboUseDownloadPath->setCurrentIndex(0); + m_ui->comboDownloadPath->setSelectedPath({}); + } +} + +void TorrentCategoryDialog::categoryNameChanged(const QString &categoryName) +{ + const QString categoryPath = Utils::Fs::toValidFileSystemName(categoryName, true); + const auto *btSession = BitTorrent::Session::instance(); + m_ui->comboSavePath->setPlaceholder(Utils::Fs::resolvePath(categoryPath, btSession->savePath())); + + const int index = m_ui->comboUseDownloadPath->currentIndex(); + const bool useDownloadPath = (index == 1) || ((index == 0) && BitTorrent::Session::instance()->isDownloadPathEnabled()); + if (useDownloadPath) + m_ui->comboDownloadPath->setPlaceholder(Utils::Fs::resolvePath(categoryPath, btSession->downloadPath())); + + m_ui->buttonBox->button(QDialogButtonBox::Ok)->setEnabled(!categoryName.isEmpty()); +} + +void TorrentCategoryDialog::useDownloadPathChanged(const int index) +{ + if (const QString selectedPath = m_ui->comboDownloadPath->selectedPath(); !selectedPath.isEmpty()) + m_lastEnteredDownloadPath = selectedPath; + + m_ui->labelDownloadPath->setEnabled(index == 1); + m_ui->comboDownloadPath->setEnabled(index == 1); + m_ui->comboDownloadPath->setSelectedPath((index == 1) ? m_lastEnteredDownloadPath : QString()); + + const QString categoryName = m_ui->textCategoryName->text(); + const QString categoryPath = Utils::Fs::resolvePath(Utils::Fs::toValidFileSystemName(categoryName, true) + , BitTorrent::Session::instance()->downloadPath()); + const bool useDownloadPath = (index == 1) || ((index == 0) && BitTorrent::Session::instance()->isDownloadPathEnabled()); + m_ui->comboDownloadPath->setPlaceholder(useDownloadPath ? categoryPath : QString()); } diff --git a/src/gui/torrentcategorydialog.h b/src/gui/torrentcategorydialog.h index a4f993cc7..074f16f88 100644 --- a/src/gui/torrentcategorydialog.h +++ b/src/gui/torrentcategorydialog.h @@ -1,6 +1,6 @@ /* * Bittorrent Client using Qt and libtorrent. - * Copyright (C) 2017 Vladimir Golovnev + * Copyright (C) 2017, 2021 Vladimir Golovnev * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License @@ -30,6 +30,11 @@ #include +namespace BitTorrent +{ + struct CategoryOptions; +} + namespace Ui { class TorrentCategoryDialog; @@ -50,9 +55,14 @@ public: void setCategoryNameEditable(bool editable); QString categoryName() const; void setCategoryName(const QString &categoryName); - QString savePath() const; - void setSavePath(const QString &savePath); + void setCategoryOptions(const BitTorrent::CategoryOptions &categoryOptions); + BitTorrent::CategoryOptions categoryOptions() const; + +private slots: + void categoryNameChanged(const QString &categoryName); + void useDownloadPathChanged(int index); private: Ui::TorrentCategoryDialog *m_ui; + QString m_lastEnteredDownloadPath; }; diff --git a/src/gui/torrentcategorydialog.ui b/src/gui/torrentcategorydialog.ui index 7ced242b4..7eaaa7909 100644 --- a/src/gui/torrentcategorydialog.ui +++ b/src/gui/torrentcategorydialog.ui @@ -6,8 +6,8 @@ 0 0 - 400 - 100 + 493 + 208 @@ -15,7 +15,7 @@ - + Qt::Vertical @@ -29,10 +29,13 @@ - - - - Name: + + + + + 0 + 0 + @@ -46,20 +49,99 @@ - - - - - 0 - 0 - + + + + Name: - + + + Save path for incomplete torrents: + + + + + + + + Use another path for incomplete torrents: + + + + + + + 3 + + + + Default + + + + + Yes + + + + + No + + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + + + + + 0 + 0 + + + + Path: + + + + + + + + 0 + 0 + + + + + + + + + + + Qt::Vertical diff --git a/src/gui/torrentoptionsdialog.cpp b/src/gui/torrentoptionsdialog.cpp index b348e1a00..471ec4bd4 100644 --- a/src/gui/torrentoptionsdialog.cpp +++ b/src/gui/torrentoptionsdialog.cpp @@ -63,19 +63,33 @@ TorrentOptionsDialog::TorrentOptionsDialog(QWidget *parent, const QVectorsetupUi(this); m_ui->savePath->setMode(FileSystemPathEdit::Mode::DirectorySave); m_ui->savePath->setDialogCaption(tr("Choose save path")); - Q_ASSERT(!torrents.empty()); + m_ui->downloadPath->setMode(FileSystemPathEdit::Mode::DirectorySave); + m_ui->downloadPath->setDialogCaption(tr("Choose save path")); const auto *session = BitTorrent::Session::instance(); - bool allSameUpLimit = true, allSameDownLimit = true, allSameRatio = true, allSameSeedingTime = true - , allTorrentsArePrivate = true, allSameDHT = true, allSamePEX = true, allSameLSD = true - , allSameSequential = true, allSameFirstLastPieces = true, allSameAutoTMM = true, allSameSavePath = true; + bool allSameUpLimit = true; + bool allSameDownLimit = true; + bool allSameRatio = true; + bool allSameSeedingTime = true; + bool allTorrentsArePrivate = true; + bool allSameDHT = true; + bool allSamePEX = true; + bool allSameLSD = true; + bool allSameSequential = true; + bool allSameFirstLastPieces = true; + bool allSameAutoTMM = true; + bool allSameSavePath = true; + bool allSameDownloadPath = true; const bool isFirstTorrentAutoTMMEnabled = torrents[0]->isAutoTMMEnabled(); const QString firstTorrentSavePath = torrents[0]->savePath(); + const QString firstTorrentDownloadPath = torrents[0]->downloadPath(); const QString firstTorrentCategory = torrents[0]->category(); const int firstTorrentUpLimit = qMax(0, torrents[0]->uploadLimit()); @@ -105,6 +119,11 @@ TorrentOptionsDialog::TorrentOptionsDialog(QWidget *parent, const QVectorsavePath() != firstTorrentSavePath) allSameSavePath = false; } + if (allSameDownloadPath) + { + if (torrent->downloadPath() != firstTorrentDownloadPath) + allSameDownloadPath = false; + } if (m_allSameCategory) { if (torrent->category() != firstTorrentCategory) @@ -170,6 +189,16 @@ TorrentOptionsDialog::TorrentOptionsDialog(QWidget *parent, const QVectorsavePath->setSelectedPath(firstTorrentSavePath); + if (allSameDownloadPath) + { + m_ui->downloadPath->setSelectedPath(firstTorrentDownloadPath); + m_ui->checkUseDownloadPath->setChecked(!firstTorrentDownloadPath.isEmpty()); + } + else + { + m_ui->checkUseDownloadPath->setCheckState(Qt::PartiallyChecked); + } + if (!m_allSameCategory) { m_ui->comboCategory->addItem(m_currentCategoriesString); @@ -183,7 +212,7 @@ TorrentOptionsDialog::TorrentOptionsDialog(QWidget *parent, const QVectorcomboCategory->addItem(QString()); - m_categories = session->categories().keys(); + m_categories = session->categories(); std::sort(m_categories.begin(), m_categories.end(), Utils::Compare::NaturalLessThan()); for (const QString &category : asConst(m_categories)) { @@ -322,12 +351,14 @@ TorrentOptionsDialog::TorrentOptionsDialog(QWidget *parent, const QVectorsavePath->selectedPath(), + m_ui->downloadPath->selectedPath(), m_ui->comboCategory->currentText(), getRatio(), getSeedingTime(), m_ui->spinUploadLimit->value(), m_ui->spinDownloadLimit->value(), m_ui->checkAutoTMM->checkState(), + m_ui->checkUseDownloadPath->checkState(), m_ui->checkDisableDHT->checkState(), m_ui->checkDisablePEX->checkState(), m_ui->checkDisableLSD->checkState(), @@ -337,9 +368,11 @@ TorrentOptionsDialog::TorrentOptionsDialog(QWidget *parent, const QVectorcheckAutoTMM, &QCheckBox::clicked, this, &TorrentOptionsDialog::handleTMMChanged); + connect(m_ui->checkUseDownloadPath, &QCheckBox::clicked, this, &TorrentOptionsDialog::handleUseDownloadPathChanged); connect(m_ui->comboCategory, &QComboBox::activated, this, &TorrentOptionsDialog::handleCategoryChanged); // Sync up/down speed limit sliders with their corresponding spinboxes @@ -378,11 +411,28 @@ void TorrentOptionsDialog::accept() BitTorrent::Torrent *torrent = session->findTorrent(id); if (!torrent) continue; - const QString savePath = m_ui->savePath->selectedPath(); if (m_initialValues.autoTMM != m_ui->checkAutoTMM->checkState()) torrent->setAutoTMMEnabled(m_ui->checkAutoTMM->isChecked()); - if (!m_ui->checkAutoTMM->isChecked() && (m_initialValues.savePath != savePath)) - torrent->move(Utils::Fs::expandPathAbs(savePath)); + + if (m_ui->checkAutoTMM->checkState() == Qt::Unchecked) + { + const QString savePath = m_ui->savePath->selectedPath(); + if (m_initialValues.savePath != savePath) + torrent->setSavePath(Utils::Fs::expandPathAbs(savePath)); + + const Qt::CheckState useDownloadPathState = m_ui->checkUseDownloadPath->checkState(); + if (useDownloadPathState == Qt::Checked) + { + const QString downloadPath = m_ui->downloadPath->selectedPath(); + if (m_initialValues.downloadPath != downloadPath) + torrent->setDownloadPath(Utils::Fs::expandPathAbs(downloadPath)); + } + else if (useDownloadPathState == Qt::Unchecked) + { + torrent->setDownloadPath(QString()); + } + } + const QString category = m_ui->comboCategory->currentText(); // index 0 is always the current category if ((m_initialValues.category != category) || (m_ui->comboCategory->currentIndex() != 0)) @@ -485,33 +535,48 @@ void TorrentOptionsDialog::handleTMMChanged() { if (m_ui->checkAutoTMM->checkState() == Qt::Unchecked) { - m_ui->labelSavePath->setEnabled(true); - m_ui->savePath->setEnabled(true); + m_ui->groupBoxSavePath->setEnabled(true); m_ui->savePath->setSelectedPath(Utils::Fs::toNativePath(m_initialValues.savePath)); + m_ui->downloadPath->setSelectedPath(Utils::Fs::toNativePath(m_initialValues.downloadPath)); + m_ui->checkUseDownloadPath->setCheckState(m_initialValues.useDownloadPath); } else { - m_ui->labelSavePath->setEnabled(false); - m_ui->savePath->setEnabled(false); + m_ui->groupBoxSavePath->setEnabled(false); if (m_ui->checkAutoTMM->checkState() == Qt::Checked) { if (!m_allSameCategory && (m_ui->comboCategory->currentIndex() == 0)) { m_ui->savePath->setSelectedPath(QString()); + m_ui->downloadPath->setSelectedPath(QString()); + m_ui->checkUseDownloadPath->setCheckState(Qt::PartiallyChecked); } else { const QString savePath = BitTorrent::Session::instance()->categorySavePath(m_ui->comboCategory->currentText()); m_ui->savePath->setSelectedPath(Utils::Fs::toNativePath(savePath)); + const QString downloadPath = BitTorrent::Session::instance()->categoryDownloadPath(m_ui->comboCategory->currentText()); + m_ui->downloadPath->setSelectedPath(Utils::Fs::toNativePath(downloadPath)); + m_ui->checkUseDownloadPath->setChecked(!downloadPath.isEmpty()); } } else // partially checked { m_ui->savePath->setSelectedPath(QString()); + m_ui->downloadPath->setSelectedPath(QString()); + m_ui->checkUseDownloadPath->setCheckState(Qt::PartiallyChecked); } } } +void TorrentOptionsDialog::handleUseDownloadPathChanged() +{ + const bool isChecked = m_ui->checkUseDownloadPath->checkState() == Qt::Checked; + m_ui->downloadPath->setEnabled(isChecked); + if (isChecked && m_ui->downloadPath->selectedPath().isEmpty()) + m_ui->downloadPath->setSelectedPath(BitTorrent::Session::instance()->downloadPath()); +} + void TorrentOptionsDialog::handleRatioTypeChanged() { if ((m_initialValues.ratio == MIXED_SHARE_LIMITS) || (m_initialValues.seedingTime == MIXED_SHARE_LIMITS)) diff --git a/src/gui/torrentoptionsdialog.h b/src/gui/torrentoptionsdialog.h index a07eaae66..82204e311 100644 --- a/src/gui/torrentoptionsdialog.h +++ b/src/gui/torrentoptionsdialog.h @@ -62,6 +62,7 @@ public slots: private slots: void handleCategoryChanged(int index); void handleTMMChanged(); + void handleUseDownloadPathChanged(); void handleUpSpeedLimitChanged(); void handleDownSpeedLimitChanged(); @@ -82,12 +83,14 @@ private: struct { QString savePath; + QString downloadPath; QString category; qreal ratio; int seedingTime; int upSpeedLimit; int downSpeedLimit; Qt::CheckState autoTMM; + Qt::CheckState useDownloadPath; Qt::CheckState disableDHT; Qt::CheckState disablePEX; Qt::CheckState disableLSD; diff --git a/src/gui/torrentoptionsdialog.ui b/src/gui/torrentoptionsdialog.ui index 29a346856..05d10121c 100644 --- a/src/gui/torrentoptionsdialog.ui +++ b/src/gui/torrentoptionsdialog.ui @@ -13,7 +13,7 @@ Torrent Options - + @@ -24,33 +24,35 @@ + + + + Save at + + + + + + + + + Use another path for incomplete torrent + + + + + + + false + + + + + + - - - - Save path: - - - - - - - 0 - 0 - - - - - - - - Category: - - - - @@ -69,6 +71,13 @@ + + + + Category: + + + @@ -310,7 +319,7 @@ - FileSystemPathLineEdit + FileSystemPathComboEdit QWidget
gui/fspathedit.h
1 diff --git a/src/gui/transferlistwidget.cpp b/src/gui/transferlistwidget.cpp index 834a5b8f0..4af935b79 100644 --- a/src/gui/transferlistwidget.cpp +++ b/src/gui/transferlistwidget.cpp @@ -106,12 +106,12 @@ namespace void openDestinationFolder(const BitTorrent::Torrent *const torrent) { #ifdef Q_OS_MACOS - MacUtils::openFiles({torrent->contentPath(true)}); + MacUtils::openFiles({torrent->contentPath()}); #else if (torrent->filesCount() == 1) - Utils::Gui::openFolderSelect(torrent->contentPath(true)); + Utils::Gui::openFolderSelect(torrent->contentPath()); else - Utils::Gui::openPath(torrent->contentPath(true)); + Utils::Gui::openPath(torrent->contentPath()); #endif } @@ -525,22 +525,22 @@ void TransferListWidget::openSelectedTorrentsFolder() const // folders prehilighted for opening, so we use a custom method. for (BitTorrent::Torrent *const torrent : asConst(getSelectedTorrents())) { - QString path = torrent->contentPath(true); - pathsList.insert(path); + const QString contentPath = QDir(torrent->actualStorageLocation()).absoluteFilePath(torrent->contentPath()); + pathsList.insert(contentPath); } MacUtils::openFiles(pathsList); #else for (BitTorrent::Torrent *const torrent : asConst(getSelectedTorrents())) { - QString path = torrent->contentPath(true); - if (!pathsList.contains(path)) + const QString contentPath = torrent->contentPath(); + if (!pathsList.contains(contentPath)) { if (torrent->filesCount() == 1) - Utils::Gui::openFolderSelect(path); + Utils::Gui::openFolderSelect(contentPath); else - Utils::Gui::openPath(path); + Utils::Gui::openPath(contentPath); } - pathsList.insert(path); + pathsList.insert(contentPath); } #endif // Q_OS_MACOS } diff --git a/src/gui/watchedfolderoptionsdialog.cpp b/src/gui/watchedfolderoptionsdialog.cpp index 517faf158..33612129b 100644 --- a/src/gui/watchedfolderoptionsdialog.cpp +++ b/src/gui/watchedfolderoptionsdialog.cpp @@ -44,6 +44,7 @@ WatchedFolderOptionsDialog::WatchedFolderOptionsDialog( : QDialog {parent} , m_ui {new Ui::WatchedFolderOptionsDialog} , m_savePath {watchedFolderOptions.addTorrentParams.savePath} + , m_downloadPath {watchedFolderOptions.addTorrentParams.downloadPath} , m_storeDialogSize {SETTINGS_KEY("DialogSize")} { m_ui->setupUi(this); @@ -51,13 +52,18 @@ WatchedFolderOptionsDialog::WatchedFolderOptionsDialog( m_ui->savePath->setMode(FileSystemPathEdit::Mode::DirectorySave); m_ui->savePath->setDialogCaption(tr("Choose save path")); + const auto *session = BitTorrent::Session::instance(); + + m_ui->downloadPath->setMode(FileSystemPathEdit::Mode::DirectorySave); + m_ui->downloadPath->setDialogCaption(tr("Choose save path")); + m_ui->groupBoxDownloadPath->setChecked(watchedFolderOptions.addTorrentParams.useDownloadPath.value_or(session->isDownloadPathEnabled())); + connect(m_ui->comboTTM, qOverload(&QComboBox::currentIndexChanged), this, &WatchedFolderOptionsDialog::onTMMChanged); connect(m_ui->categoryComboBox, qOverload(&QComboBox::currentIndexChanged), this, &WatchedFolderOptionsDialog::onCategoryChanged); m_ui->checkBoxRecursive->setChecked(watchedFolderOptions.recursive); - populateSavePathComboBox(); + populateSavePaths(); - const auto *session = BitTorrent::Session::instance(); const BitTorrent::AddTorrentParams &torrentParams = watchedFolderOptions.addTorrentParams; m_ui->startTorrentCheckBox->setChecked(!torrentParams.addPaused.value_or(session->isAddTorrentPaused())); m_ui->skipCheckingCheckBox->setChecked(torrentParams.skipChecking); @@ -66,7 +72,7 @@ WatchedFolderOptionsDialog::WatchedFolderOptionsDialog( static_cast(torrentParams.contentLayout.value_or(session->torrentContentLayout()))); // Load categories - QStringList categories = session->categories().keys(); + QStringList categories = session->categories(); std::sort(categories.begin(), categories.end(), Utils::Compare::NaturalLessThan()); if (!torrentParams.category.isEmpty()) @@ -96,10 +102,16 @@ TorrentFilesWatcher::WatchedFolderOptions WatchedFolderOptionsDialog::watchedFol watchedFolderOptions.recursive = m_ui->checkBoxRecursive->isChecked(); BitTorrent::AddTorrentParams ¶ms = watchedFolderOptions.addTorrentParams; - params.useAutoTMM = (m_ui->comboTTM->currentIndex() == 1); - if (!*params.useAutoTMM) + const bool useAutoTMM = (m_ui->comboTTM->currentIndex() == 1); + if (!useAutoTMM) + { params.savePath = m_ui->savePath->selectedPath(); - params.category = m_ui->categoryComboBox->currentText();; + params.useDownloadPath = m_ui->groupBoxDownloadPath->isChecked(); + if (params.useDownloadPath) + params.downloadPath = m_ui->downloadPath->selectedPath(); + } + params.useAutoTMM = useAutoTMM; + params.category = m_ui->categoryComboBox->currentText(); params.addPaused = !m_ui->startTorrentCheckBox->isChecked(); params.skipChecking = m_ui->skipCheckingCheckBox->isChecked(); params.contentLayout = static_cast(m_ui->contentLayoutComboBox->currentIndex()); @@ -121,34 +133,60 @@ void WatchedFolderOptionsDialog::onCategoryChanged(const int index) { Q_UNUSED(index); - const QString category = m_ui->categoryComboBox->currentText(); if (m_ui->comboTTM->currentIndex() == 1) { - const QString savePath = BitTorrent::Session::instance()->categorySavePath(category); + const auto *btSession = BitTorrent::Session::instance(); + const QString categoryName = m_ui->categoryComboBox->currentText(); + + const QString savePath = btSession->categorySavePath(categoryName); m_ui->savePath->setSelectedPath(Utils::Fs::toNativePath(savePath)); + + const QString finishedSavePath = btSession->categoryDownloadPath(categoryName); + m_ui->downloadPath->setSelectedPath(Utils::Fs::toNativePath(finishedSavePath)); + + m_ui->groupBoxDownloadPath->setChecked(!finishedSavePath.isEmpty()); } } -void WatchedFolderOptionsDialog::populateSavePathComboBox() +void WatchedFolderOptionsDialog::populateSavePaths() { - const QString defSavePath {BitTorrent::Session::instance()->defaultSavePath()}; - m_ui->savePath->setSelectedPath(!m_savePath.isEmpty() ? m_savePath : defSavePath); + const auto *btSession = BitTorrent::Session::instance(); + + const QString defaultSavePath {btSession->savePath()}; + m_ui->savePath->setSelectedPath(!m_savePath.isEmpty() ? m_savePath : defaultSavePath); + + const QString defaultFinishedSavePath {btSession->downloadPath()}; + m_ui->downloadPath->setSelectedPath(!m_downloadPath.isEmpty() ? m_downloadPath : defaultFinishedSavePath); + + m_ui->groupBoxDownloadPath->setChecked(m_useDownloadPath); } void WatchedFolderOptionsDialog::onTMMChanged(const int index) { if (index != 1) { // 0 is Manual mode and 1 is Automatic mode. Handle all non 1 values as manual mode. - populateSavePathComboBox(); + populateSavePaths(); m_ui->groupBoxSavePath->setEnabled(true); m_ui->savePath->blockSignals(false); + m_ui->downloadPath->blockSignals(false); } else { m_ui->groupBoxSavePath->setEnabled(false); + + const auto *btSession = BitTorrent::Session::instance(); + m_ui->savePath->blockSignals(true); m_savePath = m_ui->savePath->selectedPath(); - const QString savePath = BitTorrent::Session::instance()->categorySavePath(m_ui->categoryComboBox->currentText()); + const QString savePath = btSession->categorySavePath(m_ui->categoryComboBox->currentText()); m_ui->savePath->setSelectedPath(savePath); + + m_ui->downloadPath->blockSignals(true); + m_downloadPath = m_ui->downloadPath->selectedPath(); + const QString finishedSavePath = btSession->categoryDownloadPath(m_ui->categoryComboBox->currentText()); + m_ui->downloadPath->setSelectedPath(finishedSavePath); + + m_useDownloadPath = m_ui->groupBoxDownloadPath->isChecked(); + m_ui->groupBoxDownloadPath->setChecked(!finishedSavePath.isEmpty()); } } diff --git a/src/gui/watchedfolderoptionsdialog.h b/src/gui/watchedfolderoptionsdialog.h index 7d6aed296..19af8df04 100644 --- a/src/gui/watchedfolderoptionsdialog.h +++ b/src/gui/watchedfolderoptionsdialog.h @@ -50,7 +50,7 @@ public: TorrentFilesWatcher::WatchedFolderOptions watchedFolderOptions() const; private: - void populateSavePathComboBox(); + void populateSavePaths(); void loadState(); void saveState(); void onTMMChanged(int index); @@ -58,5 +58,7 @@ private: Ui::WatchedFolderOptionsDialog *m_ui; QString m_savePath; + QString m_downloadPath; + bool m_useDownloadPath = false; SettingValue m_storeDialogSize; }; diff --git a/src/gui/watchedfolderoptionsdialog.ui b/src/gui/watchedfolderoptionsdialog.ui index d4daabab5..8f1d8bc9d 100644 --- a/src/gui/watchedfolderoptionsdialog.ui +++ b/src/gui/watchedfolderoptionsdialog.ui @@ -7,7 +7,7 @@ 0 0 462 - 325 + 364 @@ -119,6 +119,24 @@ + + + + Use another path for incomplete torrents + + + true + + + false + + + + + + + +
@@ -296,6 +314,12 @@
gui/fspathedit.h
1 + + FileSystemPathLineEdit + QWidget +
gui/fspathedit.h
+ 1 +
diff --git a/src/webui/api/appcontroller.cpp b/src/webui/api/appcontroller.cpp index 36cbd80a9..7bed59982 100644 --- a/src/webui/api/appcontroller.cpp +++ b/src/webui/api/appcontroller.cpp @@ -112,9 +112,9 @@ void AppController::preferencesAction() data["torrent_changed_tmm_enabled"] = !session->isDisableAutoTMMWhenCategoryChanged(); data["save_path_changed_tmm_enabled"] = !session->isDisableAutoTMMWhenDefaultSavePathChanged(); data["category_changed_tmm_enabled"] = !session->isDisableAutoTMMWhenCategorySavePathChanged(); - data["save_path"] = Utils::Fs::toNativePath(session->defaultSavePath()); - data["temp_path_enabled"] = session->isTempPathEnabled(); - data["temp_path"] = Utils::Fs::toNativePath(session->tempPath()); + data["save_path"] = Utils::Fs::toNativePath(session->savePath()); + data["temp_path_enabled"] = session->isDownloadPathEnabled(); + data["temp_path"] = Utils::Fs::toNativePath(session->downloadPath()); data["export_dir"] = Utils::Fs::toNativePath(session->torrentExportDirectory()); data["export_dir_fin"] = Utils::Fs::toNativePath(session->finishedTorrentExportDirectory()); @@ -399,11 +399,11 @@ void AppController::setPreferencesAction() if (hasKey("category_changed_tmm_enabled")) session->setDisableAutoTMMWhenCategorySavePathChanged(!it.value().toBool()); if (hasKey("save_path")) - session->setDefaultSavePath(it.value().toString()); + session->setSavePath(it.value().toString()); if (hasKey("temp_path_enabled")) - session->setTempPathEnabled(it.value().toBool()); + session->setDownloadPathEnabled(it.value().toBool()); if (hasKey("temp_path")) - session->setTempPath(it.value().toString()); + session->setDownloadPath(it.value().toString()); if (hasKey("export_dir")) session->setTorrentExportDirectory(it.value().toString()); if (hasKey("export_dir_fin")) @@ -866,7 +866,7 @@ void AppController::setPreferencesAction() void AppController::defaultSavePathAction() { - setResult(BitTorrent::Session::instance()->defaultSavePath()); + setResult(BitTorrent::Session::instance()->savePath()); } void AppController::networkInterfaceListAction() diff --git a/src/webui/api/freediskspacechecker.cpp b/src/webui/api/freediskspacechecker.cpp index 9be812c53..1d1abbe6f 100644 --- a/src/webui/api/freediskspacechecker.cpp +++ b/src/webui/api/freediskspacechecker.cpp @@ -33,6 +33,6 @@ void FreeDiskSpaceChecker::check() { - const qint64 freeDiskSpace = Utils::Fs::freeDiskSpaceOnPath(BitTorrent::Session::instance()->defaultSavePath()); + const qint64 freeDiskSpace = Utils::Fs::freeDiskSpaceOnPath(BitTorrent::Session::instance()->savePath()); emit checked(freeDiskSpace); } diff --git a/src/webui/api/serialize/serialize_torrent.cpp b/src/webui/api/serialize/serialize_torrent.cpp index 9056bd039..3317c591d 100644 --- a/src/webui/api/serialize/serialize_torrent.cpp +++ b/src/webui/api/serialize/serialize_torrent.cpp @@ -131,6 +131,7 @@ QVariantMap serialize(const BitTorrent::Torrent &torrent) {KEY_TORRENT_SUPER_SEEDING, torrent.superSeeding()}, {KEY_TORRENT_FORCE_START, torrent.isForced()}, {KEY_TORRENT_SAVE_PATH, Utils::Fs::toNativePath(torrent.savePath())}, + {KEY_TORRENT_DOWNLOAD_PATH, Utils::Fs::toNativePath(torrent.downloadPath())}, {KEY_TORRENT_CONTENT_PATH, Utils::Fs::toNativePath(torrent.contentPath())}, {KEY_TORRENT_ADDED_ON, torrent.addedTime().toSecsSinceEpoch()}, {KEY_TORRENT_COMPLETION_ON, torrent.completedTime().toSecsSinceEpoch()}, diff --git a/src/webui/api/serialize/serialize_torrent.h b/src/webui/api/serialize/serialize_torrent.h index b4eb79d9f..54d50f616 100644 --- a/src/webui/api/serialize/serialize_torrent.h +++ b/src/webui/api/serialize/serialize_torrent.h @@ -61,6 +61,7 @@ inline const char KEY_TORRENT_TAGS[] = "tags"; inline const char KEY_TORRENT_SUPER_SEEDING[] = "super_seeding"; inline const char KEY_TORRENT_FORCE_START[] = "force_start"; inline const char KEY_TORRENT_SAVE_PATH[] = "save_path"; +inline const char KEY_TORRENT_DOWNLOAD_PATH[] = "download_path"; inline const char KEY_TORRENT_CONTENT_PATH[] = "content_path"; inline const char KEY_TORRENT_ADDED_ON[] = "added_on"; inline const char KEY_TORRENT_COMPLETION_ON[] = "completion_on"; diff --git a/src/webui/api/synccontroller.cpp b/src/webui/api/synccontroller.cpp index 106db64dd..7a5be46eb 100644 --- a/src/webui/api/synccontroller.cpp +++ b/src/webui/api/synccontroller.cpp @@ -209,6 +209,7 @@ namespace case QMetaType::ULongLong: case QMetaType::UInt: case QMetaType::QDateTime: + case QMetaType::Nullptr: if (prevData[key] != value) syncData[key] = value; break; @@ -426,6 +427,7 @@ SyncController::~SyncController() // - "uploaded_session": Amount of data uploaded since program open // - "amount_left": Amount of data left to download // - "save_path": Torrent save path +// - "download_path": Torrent download path // - "completed": Amount of data completed // - "max_ratio": Upload max share ratio // - "max_seeding_time": Upload max seeding time @@ -496,15 +498,13 @@ void SyncController::maindataAction() data["torrents"] = torrents; QVariantHash categories; - const QStringMap categoriesList = session->categories(); - for (auto it = categoriesList.cbegin(); it != categoriesList.cend(); ++it) + const QStringList categoriesList = session->categories(); + for (const auto &categoryName : categoriesList) { - const QString &key = it.key(); - categories[key] = QVariantMap - { - {"name", key}, - {"savePath", it.value()} - }; + const BitTorrent::CategoryOptions categoryOptions = session->categoryOptions(categoryName); + QJsonObject category = categoryOptions.toJSON(); + category.insert(QLatin1String("name"), categoryName); + categories[categoryName] = category.toVariantMap(); } data["categories"] = categories; diff --git a/src/webui/api/torrentscontroller.cpp b/src/webui/api/torrentscontroller.cpp index 27d376777..46dc23528 100644 --- a/src/webui/api/torrentscontroller.cpp +++ b/src/webui/api/torrentscontroller.cpp @@ -39,6 +39,7 @@ #include #include +#include "base/bittorrent/categoryoptions.h" #include "base/bittorrent/common.h" #include "base/bittorrent/downloadpriority.h" #include "base/bittorrent/infohash.h" @@ -103,6 +104,7 @@ const char KEY_PROP_ADDITION_DATE[] = "addition_date"; const char KEY_PROP_COMPLETION_DATE[] = "completion_date"; const char KEY_PROP_CREATION_DATE[] = "creation_date"; const char KEY_PROP_SAVE_PATH[] = "save_path"; +const char KEY_PROP_DOWNLOAD_PATH[] = "download_path"; const char KEY_PROP_COMMENT[] = "comment"; // File keys @@ -372,6 +374,7 @@ void TorrentsController::infoAction() // - "completion_date": Torrent completion date // - "creation_date": Torrent creation date // - "save_path": Torrent save path +// - "download_path": Torrent download path // - "comment": Torrent comment void TorrentsController::propertiesAction() { @@ -430,6 +433,7 @@ void TorrentsController::propertiesAction() dataDict[KEY_PROP_CREATION_DATE] = -1; } dataDict[KEY_PROP_SAVE_PATH] = Utils::Fs::toNativePath(torrent->savePath()); + dataDict[KEY_PROP_DOWNLOAD_PATH] = Utils::Fs::toNativePath(torrent->downloadPath()); dataDict[KEY_PROP_COMMENT] = torrent->comment(); setResult(dataDict); @@ -643,6 +647,8 @@ void TorrentsController::addAction() const bool firstLastPiece = parseBool(params()["firstLastPiecePrio"]).value_or(false); const std::optional addPaused = parseBool(params()["paused"]); const QString savepath = params()["savepath"].trimmed(); + const QString downloadPath = params()["downloadPath"].trimmed(); + const std::optional useDownloadPath = parseBool(params()["useDownloadPath"]); const QString category = params()["category"]; const QStringList tags = params()["tags"].split(',', Qt::SkipEmptyParts); const QString torrentName = params()["rename"].trimmed(); @@ -682,6 +688,8 @@ void TorrentsController::addAction() addTorrentParams.addPaused = addPaused; addTorrentParams.contentLayout = contentLayout; addTorrentParams.savePath = savepath; + addTorrentParams.downloadPath = downloadPath; + addTorrentParams.useDownloadPath = useDownloadPath; addTorrentParams.category = category; addTorrentParams.tags.insert(tags.cbegin(), tags.cend()); addTorrentParams.name = torrentName; @@ -1095,7 +1103,58 @@ void TorrentsController::setLocationAction() { LogMsg(tr("WebUI Set location: moving \"%1\", from \"%2\" to \"%3\"") .arg(torrent->name(), Utils::Fs::toNativePath(torrent->savePath()), Utils::Fs::toNativePath(newLocation))); - torrent->move(Utils::Fs::expandPathAbs(newLocation)); + torrent->setAutoTMMEnabled(false); + torrent->setSavePath(Utils::Fs::expandPathAbs(newLocation)); + }); +} + +void TorrentsController::setSavePathAction() +{ + requireParams({"id", "path"}); + + const QStringList ids {params()["id"].split('|')}; + const QString newPath {params()["path"]}; + + if (newPath.isEmpty()) + throw APIError(APIErrorType::BadParams, tr("Save path cannot be empty")); + + // try to create the directory if it does not exist + if (!QDir(newPath).mkpath(".")) + throw APIError(APIErrorType::Conflict, tr("Cannot create target directory")); + + // check permissions + if (!QFileInfo(newPath).isWritable()) + throw APIError(APIErrorType::AccessDenied, tr("Cannot write to directory")); + + applyToTorrents(ids, [&newPath](BitTorrent::Torrent *const torrent) + { + if (!torrent->isAutoTMMEnabled()) + torrent->setSavePath(newPath); + }); +} + +void TorrentsController::setDownloadPathAction() +{ + requireParams({"id", "path"}); + + const QStringList ids {params()["id"].split('|')}; + const QString newPath {params()["path"]}; + + if (!newPath.isEmpty()) + { + // try to create the directory if it does not exist + if (!QDir(newPath).mkpath(".")) + throw APIError(APIErrorType::Conflict, tr("Cannot create target directory")); + + // check permissions + if (!QFileInfo(newPath).isWritable()) + throw APIError(APIErrorType::AccessDenied, tr("Cannot write to directory")); + } + + applyToTorrents(ids, [&newPath](BitTorrent::Torrent *const torrent) + { + if (!torrent->isAutoTMMEnabled()) + torrent->setDownloadPath(newPath); }); } @@ -1164,16 +1223,24 @@ void TorrentsController::createCategoryAction() { requireParams({"category"}); - const QString category {params()["category"]}; - const QString savePath {params()["savePath"]}; - + const QString category = params()["category"]; if (category.isEmpty()) throw APIError(APIErrorType::BadParams, tr("Category cannot be empty")); if (!BitTorrent::Session::isValidCategoryName(category)) throw APIError(APIErrorType::Conflict, tr("Incorrect category name")); - if (!BitTorrent::Session::instance()->addCategory(category, savePath)) + const QString savePath = params()["savePath"]; + const auto useDownloadPath = parseBool(params()["downloadPathEnabled"]); + BitTorrent::CategoryOptions categoryOptions; + categoryOptions.savePath = savePath; + if (useDownloadPath.has_value()) + { + const QString downloadPath = params()["downloadPath"]; + categoryOptions.downloadPath = {useDownloadPath.value(), downloadPath}; + } + + if (!BitTorrent::Session::instance()->addCategory(category, categoryOptions)) throw APIError(APIErrorType::Conflict, tr("Unable to create category")); } @@ -1181,13 +1248,21 @@ void TorrentsController::editCategoryAction() { requireParams({"category", "savePath"}); - const QString category {params()["category"]}; - const QString savePath {params()["savePath"]}; - + const QString category = params()["category"]; if (category.isEmpty()) throw APIError(APIErrorType::BadParams, tr("Category cannot be empty")); - if (!BitTorrent::Session::instance()->editCategory(category, savePath)) + const QString savePath = params()["savePath"]; + const auto useDownloadPath = parseBool(params()["downloadPathEnabled"]); + BitTorrent::CategoryOptions categoryOptions; + categoryOptions.savePath = savePath; + if (useDownloadPath.has_value()) + { + const QString downloadPath = params()["downloadPath"]; + categoryOptions.downloadPath = {useDownloadPath.value(), downloadPath}; + } + + if (!BitTorrent::Session::instance()->editCategory(category, categoryOptions)) throw APIError(APIErrorType::Conflict, tr("Unable to edit category")); } @@ -1202,16 +1277,16 @@ void TorrentsController::removeCategoriesAction() void TorrentsController::categoriesAction() { + const auto session = BitTorrent::Session::instance(); + QJsonObject categories; - const QStringMap categoriesMap = BitTorrent::Session::instance()->categories(); - for (auto it = categoriesMap.cbegin(); it != categoriesMap.cend(); ++it) + const QStringList categoriesList = session->categories(); + for (const auto &categoryName : categoriesList) { - const auto &key = it.key(); - categories[key] = QJsonObject - { - {"name", key}, - {"savePath", it.value()} - }; + const BitTorrent::CategoryOptions categoryOptions = session->categoryOptions(categoryName); + QJsonObject category = categoryOptions.toJSON(); + category.insert(QLatin1String("name"), categoryName); + categories[categoryName] = category; } setResult(categories); diff --git a/src/webui/api/torrentscontroller.h b/src/webui/api/torrentscontroller.h index 419cdd319..61487e0b6 100644 --- a/src/webui/api/torrentscontroller.h +++ b/src/webui/api/torrentscontroller.h @@ -78,6 +78,8 @@ private slots: void topPrioAction(); void bottomPrioAction(); void setLocationAction(); + void setSavePathAction(); + void setDownloadPathAction(); void setAutoManagementAction(); void setSuperSeedingAction(); void setForceStartAction(); diff --git a/src/webui/webapplication.h b/src/webui/webapplication.h index 2cd3b4634..339d805a5 100644 --- a/src/webui/webapplication.h +++ b/src/webui/webapplication.h @@ -43,7 +43,7 @@ #include "base/utils/net.h" #include "base/utils/version.h" -inline const Utils::Version API_VERSION {2, 8, 3}; +inline const Utils::Version API_VERSION {2, 8, 4}; class APIController; class WebApplication;