diff --git a/.github/workflows/ci_macos.yaml b/.github/workflows/ci_macos.yaml index 6180a44ea..e3f14f115 100644 --- a/.github/workflows/ci_macos.yaml +++ b/.github/workflows/ci_macos.yaml @@ -23,7 +23,6 @@ jobs: env: boost_path: "${{ github.workspace }}/../boost" - openssl_root: "$(brew --prefix openssl@3)" libtorrent_path: "${{ github.workspace }}/../libtorrent" steps: @@ -70,7 +69,7 @@ jobs: mv "${{ github.workspace }}/.."/boost_* "${{ env.boost_path }}" - name: Install Qt - uses: jurplel/install-qt-action@v3 + uses: jurplel/install-qt-action@v4 with: version: ${{ matrix.qt_version }} archives: qtbase qtdeclarative qtsvg qttools @@ -94,8 +93,7 @@ jobs: -DCMAKE_CXX_STANDARD=17 \ -DCMAKE_EXPORT_COMPILE_COMMANDS=ON \ -DBOOST_ROOT="${{ env.boost_path }}" \ - -Ddeprecated-functions=OFF \ - -DOPENSSL_ROOT_DIR="${{ env.openssl_root }}" + -Ddeprecated-functions=OFF cmake --build build sudo cmake --install build @@ -109,7 +107,6 @@ jobs: -DCMAKE_BUILD_TYPE=RelWithDebInfo \ -DCMAKE_EXPORT_COMPILE_COMMANDS=ON \ -DBOOST_ROOT="${{ env.boost_path }}" \ - -DOPENSSL_ROOT_DIR="${{ env.openssl_root }}" \ -DTESTING=ON \ -DVERBOSE_CONFIGURE=ON \ -D${{ matrix.qbt_gui }} diff --git a/.github/workflows/ci_python.yaml b/.github/workflows/ci_python.yaml index f08b382b4..fe525c8c6 100644 --- a/.github/workflows/ci_python.yaml +++ b/.github/workflows/ci_python.yaml @@ -53,7 +53,7 @@ jobs: python-version: '3.7' - name: Install tools (search engine) - run: pip install bandit pycodestyle pyflakes + run: pip install bandit mypy pycodestyle pyflakes pyright - name: Gather files (search engine) run: | @@ -61,6 +61,16 @@ jobs: echo $PY_FILES echo "PY_FILES=$PY_FILES" >> "$GITHUB_ENV" + - name: Check typings (search engine) + run: | + MYPYPATH="src/searchengine/nova3" \ + mypy \ + --follow-imports skip \ + --strict \ + $PY_FILES + pyright \ + $PY_FILES + - name: Lint code (search engine) run: | pyflakes $PY_FILES diff --git a/.github/workflows/ci_ubuntu.yaml b/.github/workflows/ci_ubuntu.yaml index bcae07196..172876852 100644 --- a/.github/workflows/ci_ubuntu.yaml +++ b/.github/workflows/ci_ubuntu.yaml @@ -64,7 +64,7 @@ jobs: mv "${{ github.workspace }}/.."/boost_* "${{ env.boost_path }}" - name: Install Qt - uses: jurplel/install-qt-action@v3 + uses: jurplel/install-qt-action@v4 with: version: ${{ matrix.qt_version }} archives: icu qtbase qtdeclarative qtsvg qttools @@ -138,12 +138,12 @@ jobs: curl \ -L \ -Z \ - -O https://github.com/linuxdeploy/linuxdeploy/releases/download/continuous/linuxdeploy-x86_64.AppImage \ - -O https://github.com/linuxdeploy/linuxdeploy-plugin-qt/releases/download/continuous/linuxdeploy-plugin-qt-x86_64.AppImage \ + -O https://github.com/linuxdeploy/linuxdeploy/releases/download/continuous/linuxdeploy-static-x86_64.AppImage \ + -O https://github.com/linuxdeploy/linuxdeploy-plugin-qt/releases/download/continuous/linuxdeploy-plugin-qt-static-x86_64.AppImage \ -O https://github.com/linuxdeploy/linuxdeploy-plugin-appimage/releases/download/continuous/linuxdeploy-plugin-appimage-x86_64.AppImage chmod +x \ - linuxdeploy-x86_64.AppImage \ - linuxdeploy-plugin-qt-x86_64.AppImage \ + linuxdeploy-static-x86_64.AppImage \ + linuxdeploy-plugin-qt-static-x86_64.AppImage \ linuxdeploy-plugin-appimage-x86_64.AppImage - name: Prepare files for AppImage @@ -156,12 +156,12 @@ jobs: - name: Package AppImage run: | - ./linuxdeploy-x86_64.AppImage --appdir qbittorrent --plugin qt + ./linuxdeploy-static-x86_64.AppImage --appdir qbittorrent --plugin qt rm qbittorrent/apprun-hooks/* cp .github/workflows/helper/appimage/export_vars.sh qbittorrent/apprun-hooks/export_vars.sh NO_APPSTREAM=1 \ OUTPUT=upload/qbittorrent-CI_Ubuntu_x86_64.AppImage \ - ./linuxdeploy-x86_64.AppImage --appdir qbittorrent --output appimage + ./linuxdeploy-static-x86_64.AppImage --appdir qbittorrent --output appimage - name: Upload build artifacts uses: actions/upload-artifact@v4 diff --git a/.github/workflows/ci_windows.yaml b/.github/workflows/ci_windows.yaml index d6050d2fa..df7134182 100644 --- a/.github/workflows/ci_windows.yaml +++ b/.github/workflows/ci_windows.yaml @@ -93,7 +93,7 @@ jobs: move "${{ github.workspace }}/../boost_*" "${{ env.boost_path }}" - name: Install Qt - uses: jurplel/install-qt-action@v3 + uses: jurplel/install-qt-action@v4 with: version: "6.7.0" archives: qtbase qtsvg qttools @@ -153,26 +153,26 @@ jobs: copy build/qbittorrent.pdb upload/qBittorrent copy dist/windows/qt.conf upload/qBittorrent # runtimes - copy "${{ env.Qt6_DIR }}/bin/Qt6Core.dll" upload/qBittorrent - copy "${{ env.Qt6_DIR }}/bin/Qt6Gui.dll" upload/qBittorrent - copy "${{ env.Qt6_DIR }}/bin/Qt6Network.dll" upload/qBittorrent - copy "${{ env.Qt6_DIR }}/bin/Qt6Sql.dll" upload/qBittorrent - copy "${{ env.Qt6_DIR }}/bin/Qt6Svg.dll" upload/qBittorrent - copy "${{ env.Qt6_DIR }}/bin/Qt6Widgets.dll" upload/qBittorrent - copy "${{ env.Qt6_DIR }}/bin/Qt6Xml.dll" upload/qBittorrent + copy "${{ env.Qt_ROOT_DIR }}/bin/Qt6Core.dll" upload/qBittorrent + copy "${{ env.Qt_ROOT_DIR }}/bin/Qt6Gui.dll" upload/qBittorrent + copy "${{ env.Qt_ROOT_DIR }}/bin/Qt6Network.dll" upload/qBittorrent + copy "${{ env.Qt_ROOT_DIR }}/bin/Qt6Sql.dll" upload/qBittorrent + copy "${{ env.Qt_ROOT_DIR }}/bin/Qt6Svg.dll" upload/qBittorrent + copy "${{ env.Qt_ROOT_DIR }}/bin/Qt6Widgets.dll" upload/qBittorrent + copy "${{ env.Qt_ROOT_DIR }}/bin/Qt6Xml.dll" upload/qBittorrent mkdir upload/qBittorrent/plugins/iconengines - copy "${{ env.Qt6_DIR }}/plugins/iconengines/qsvgicon.dll" upload/qBittorrent/plugins/iconengines + copy "${{ env.Qt_ROOT_DIR }}/plugins/iconengines/qsvgicon.dll" upload/qBittorrent/plugins/iconengines mkdir upload/qBittorrent/plugins/imageformats - copy "${{ env.Qt6_DIR }}/plugins/imageformats/qico.dll" upload/qBittorrent/plugins/imageformats - copy "${{ env.Qt6_DIR }}/plugins/imageformats/qsvg.dll" upload/qBittorrent/plugins/imageformats + copy "${{ env.Qt_ROOT_DIR }}/plugins/imageformats/qico.dll" upload/qBittorrent/plugins/imageformats + copy "${{ env.Qt_ROOT_DIR }}/plugins/imageformats/qsvg.dll" upload/qBittorrent/plugins/imageformats mkdir upload/qBittorrent/plugins/platforms - copy "${{ env.Qt6_DIR }}/plugins/platforms/qwindows.dll" upload/qBittorrent/plugins/platforms + copy "${{ env.Qt_ROOT_DIR }}/plugins/platforms/qwindows.dll" upload/qBittorrent/plugins/platforms mkdir upload/qBittorrent/plugins/sqldrivers - copy "${{ env.Qt6_DIR }}/plugins/sqldrivers/qsqlite.dll" upload/qBittorrent/plugins/sqldrivers + copy "${{ env.Qt_ROOT_DIR }}/plugins/sqldrivers/qsqlite.dll" upload/qBittorrent/plugins/sqldrivers mkdir upload/qBittorrent/plugins/styles - copy "${{ env.Qt6_DIR }}/plugins/styles/qmodernwindowsstyle.dll" upload/qBittorrent/plugins/styles + copy "${{ env.Qt_ROOT_DIR }}/plugins/styles/qmodernwindowsstyle.dll" upload/qBittorrent/plugins/styles mkdir upload/qBittorrent/plugins/tls - copy "${{ env.Qt6_DIR }}/plugins/tls/qschannelbackend.dll" upload/qBittorrent/plugins/tls + copy "${{ env.Qt_ROOT_DIR }}/plugins/tls/qschannelbackend.dll" upload/qBittorrent/plugins/tls # cmake additionals mkdir upload/cmake copy build/compile_commands.json upload/cmake diff --git a/.github/workflows/coverity-scan.yaml b/.github/workflows/coverity-scan.yaml index cf3c3dc3b..fa66c6cb3 100644 --- a/.github/workflows/coverity-scan.yaml +++ b/.github/workflows/coverity-scan.yaml @@ -52,7 +52,7 @@ jobs: mv "${{ github.workspace }}/.."/boost_* "${{ env.boost_path }}" - name: Install Qt - uses: jurplel/install-qt-action@v3 + uses: jurplel/install-qt-action@v4 with: version: ${{ matrix.qt_version }} archives: icu qtbase qtdeclarative qtsvg qttools diff --git a/src/base/CMakeLists.txt b/src/base/CMakeLists.txt index d764871d7..38154e98a 100644 --- a/src/base/CMakeLists.txt +++ b/src/base/CMakeLists.txt @@ -38,6 +38,8 @@ add_library(qbt_base STATIC bittorrent/torrent.h bittorrent/torrentcontenthandler.h bittorrent/torrentcontentlayout.h + bittorrent/torrentcontentremoveoption.h + bittorrent/torrentcontentremover.h bittorrent/torrentcreationmanager.h bittorrent/torrentcreationtask.h bittorrent/torrentcreator.h @@ -145,6 +147,7 @@ add_library(qbt_base STATIC bittorrent/sslparameters.cpp bittorrent/torrent.cpp bittorrent/torrentcontenthandler.cpp + bittorrent/torrentcontentremover.cpp bittorrent/torrentcreationmanager.cpp bittorrent/torrentcreationtask.cpp bittorrent/torrentcreator.cpp diff --git a/src/base/bittorrent/bencoderesumedatastorage.cpp b/src/base/bittorrent/bencoderesumedatastorage.cpp index 4107c5aa9..a34e5a34a 100644 --- a/src/base/bittorrent/bencoderesumedatastorage.cpp +++ b/src/base/bittorrent/bencoderesumedatastorage.cpp @@ -40,7 +40,6 @@ #include #include -#include "base/algorithm.h" #include "base/exceptions.h" #include "base/global.h" #include "base/logger.h" diff --git a/src/base/bittorrent/customstorage.cpp b/src/base/bittorrent/customstorage.cpp index c4957ef07..9134b1507 100644 --- a/src/base/bittorrent/customstorage.cpp +++ b/src/base/bittorrent/customstorage.cpp @@ -240,11 +240,11 @@ void CustomDiskIOThread::handleCompleteFiles(lt::storage_index_t storage, const lt::storage_interface *customStorageConstructor(const lt::storage_params ¶ms, lt::file_pool &pool) { - return new CustomStorage {params, pool}; + return new CustomStorage(params, pool); } CustomStorage::CustomStorage(const lt::storage_params ¶ms, lt::file_pool &filePool) - : lt::default_storage {params, filePool} + : lt::default_storage(params, filePool) , m_savePath {params.path} { } diff --git a/src/base/bittorrent/session.h b/src/base/bittorrent/session.h index d4d3e758a..96f4816fd 100644 --- a/src/base/bittorrent/session.h +++ b/src/base/bittorrent/session.h @@ -37,17 +37,12 @@ #include "addtorrentparams.h" #include "categoryoptions.h" #include "sharelimitaction.h" +#include "torrentcontentremoveoption.h" #include "trackerentry.h" #include "trackerentrystatus.h" class QString; -enum DeleteOption -{ - DeleteTorrent, - DeleteTorrentAndFiles -}; - namespace BitTorrent { class InfoHash; @@ -58,6 +53,12 @@ namespace BitTorrent struct CacheStatus; struct SessionStatus; + enum class TorrentRemoveOption + { + KeepContent, + RemoveContent + }; + // Using `Q_ENUM_NS()` without a wrapper namespace in our case is not advised // since `Q_NAMESPACE` cannot be used when the same namespace resides at different files. // https://www.kdab.com/new-qt-5-8-meta-object-support-namespaces/#comment-143779 @@ -425,7 +426,7 @@ namespace BitTorrent virtual void setExcludedFileNamesEnabled(bool enabled) = 0; virtual QStringList excludedFileNames() const = 0; virtual void setExcludedFileNames(const QStringList &newList) = 0; - virtual bool isFilenameExcluded(const QString &fileName) const = 0; + virtual void applyFilenameFilter(const PathList &files, QList &priorities) = 0; virtual QStringList bannedIPs() const = 0; virtual void setBannedIPs(const QStringList &newList) = 0; virtual ResumeDataStorageType resumeDataStorageType() const = 0; @@ -434,6 +435,8 @@ namespace BitTorrent virtual void setMergeTrackersEnabled(bool enabled) = 0; virtual bool isStartPaused() const = 0; virtual void setStartPaused(bool value) = 0; + virtual TorrentContentRemoveOption torrentContentRemoveOption() const = 0; + virtual void setTorrentContentRemoveOption(TorrentContentRemoveOption option) = 0; virtual bool isRestored() const = 0; @@ -453,7 +456,7 @@ namespace BitTorrent virtual bool isKnownTorrent(const InfoHash &infoHash) const = 0; virtual bool addTorrent(const TorrentDescriptor &torrentDescr, const AddTorrentParams ¶ms = {}) = 0; - virtual bool deleteTorrent(const TorrentID &id, DeleteOption deleteOption = DeleteOption::DeleteTorrent) = 0; + virtual bool removeTorrent(const TorrentID &id, TorrentRemoveOption deleteOption = TorrentRemoveOption::KeepContent) = 0; virtual bool downloadMetadata(const TorrentDescriptor &torrentDescr) = 0; virtual bool cancelDownloadMetadata(const TorrentID &id) = 0; diff --git a/src/base/bittorrent/sessionimpl.cpp b/src/base/bittorrent/sessionimpl.cpp index 5822bcb63..8c21682e8 100644 --- a/src/base/bittorrent/sessionimpl.cpp +++ b/src/base/bittorrent/sessionimpl.cpp @@ -101,6 +101,7 @@ #include "nativesessionextension.h" #include "portforwarderimpl.h" #include "resumedatastorage.h" +#include "torrentcontentremover.h" #include "torrentdescriptor.h" #include "torrentimpl.h" #include "tracker.h" @@ -525,6 +526,7 @@ SessionImpl::SessionImpl(QObject *parent) , m_I2POutboundQuantity {BITTORRENT_SESSION_KEY(u"I2P/OutboundQuantity"_s), 3} , m_I2PInboundLength {BITTORRENT_SESSION_KEY(u"I2P/InboundLength"_s), 3} , m_I2POutboundLength {BITTORRENT_SESSION_KEY(u"I2P/OutboundLength"_s), 3} + , m_torrentContentRemoveOption {BITTORRENT_SESSION_KEY(u"TorrentContentRemoveOption"_s), TorrentContentRemoveOption::MoveToTrash} , m_startPaused {BITTORRENT_SESSION_KEY(u"StartPaused"_s)} , m_seedingLimitTimer {new QTimer(this)} , m_resumeDataTimer {new QTimer(this)} @@ -550,7 +552,14 @@ SessionImpl::SessionImpl(QObject *parent) , this, [this]() { m_recentErroredTorrents.clear(); }); m_seedingLimitTimer->setInterval(10s); - connect(m_seedingLimitTimer, &QTimer::timeout, this, &SessionImpl::processShareLimits); + connect(m_seedingLimitTimer, &QTimer::timeout, this, [this] + { + // We shouldn't iterate over `m_torrents` in the loop below + // since `deleteTorrent()` modifies it indirectly + const QHash torrents {m_torrents}; + for (TorrentImpl *torrent : torrents) + processTorrentShareLimits(torrent); + }); initializeNativeSession(); configureComponents(); @@ -586,6 +595,11 @@ SessionImpl::SessionImpl(QObject *parent) connect(m_ioThread.get(), &QThread::finished, m_fileSearcher, &QObject::deleteLater); connect(m_fileSearcher, &FileSearcher::searchFinished, this, &SessionImpl::fileSearchFinished); + m_torrentContentRemover = new TorrentContentRemover; + m_torrentContentRemover->moveToThread(m_ioThread.get()); + connect(m_ioThread.get(), &QThread::finished, m_torrentContentRemover, &QObject::deleteLater); + connect(m_torrentContentRemover, &TorrentContentRemover::jobFinished, this, &SessionImpl::torrentContentRemovingFinished); + m_ioThread->start(); initMetrics(); @@ -604,7 +618,7 @@ SessionImpl::~SessionImpl() { m_nativeSession->pause(); - const qint64 timeout = (m_shutdownTimeout >= 0) ? (m_shutdownTimeout * 1000) : -1; + const auto timeout = (m_shutdownTimeout >= 0) ? (static_cast(m_shutdownTimeout) * 1000) : -1; const QDeadlineTimer shutdownDeadlineTimer {timeout}; if (m_torrentsQueueChanged) @@ -2236,72 +2250,66 @@ void SessionImpl::populateAdditionalTrackers() m_additionalTrackerEntries = parseTrackerEntries(additionalTrackers()); } -void SessionImpl::processShareLimits() +void SessionImpl::processTorrentShareLimits(TorrentImpl *torrent) { + if (!torrent->isFinished() || torrent->isForced()) + return; + const auto effectiveLimit = [](const T limit, const T useGlobalLimit, const T globalLimit) -> T { return (limit == useGlobalLimit) ? globalLimit : limit; }; - // We shouldn't iterate over `m_torrents` in the loop below - // since `deleteTorrent()` modifies it indirectly - const QHash torrents {m_torrents}; - for (const auto &[torrentID, torrent] : torrents.asKeyValueRange()) + const qreal ratioLimit = effectiveLimit(torrent->ratioLimit(), Torrent::USE_GLOBAL_RATIO, globalMaxRatio()); + const int seedingTimeLimit = effectiveLimit(torrent->seedingTimeLimit(), Torrent::USE_GLOBAL_SEEDING_TIME, globalMaxSeedingMinutes()); + const int inactiveSeedingTimeLimit = effectiveLimit(torrent->inactiveSeedingTimeLimit(), Torrent::USE_GLOBAL_INACTIVE_SEEDING_TIME, globalMaxInactiveSeedingMinutes()); + + bool reached = false; + QString description; + + if (const qreal ratio = torrent->realRatio(); + (ratioLimit >= 0) && (ratio <= Torrent::MAX_RATIO) && (ratio >= ratioLimit)) { - if (!torrent->isFinished() || torrent->isForced()) - continue; + reached = true; + description = tr("Torrent reached the share ratio limit."); + } + else if (const qlonglong seedingTimeInMinutes = torrent->finishedTime() / 60; + (seedingTimeLimit >= 0) && (seedingTimeInMinutes <= Torrent::MAX_SEEDING_TIME) && (seedingTimeInMinutes >= seedingTimeLimit)) + { + reached = true; + description = tr("Torrent reached the seeding time limit."); + } + else if (const qlonglong inactiveSeedingTimeInMinutes = torrent->timeSinceActivity() / 60; + (inactiveSeedingTimeLimit >= 0) && (inactiveSeedingTimeInMinutes <= Torrent::MAX_INACTIVE_SEEDING_TIME) && (inactiveSeedingTimeInMinutes >= inactiveSeedingTimeLimit)) + { + reached = true; + description = tr("Torrent reached the inactive seeding time limit."); + } - const qreal ratioLimit = effectiveLimit(torrent->ratioLimit(), Torrent::USE_GLOBAL_RATIO, globalMaxRatio()); - const int seedingTimeLimit = effectiveLimit(torrent->seedingTimeLimit(), Torrent::USE_GLOBAL_SEEDING_TIME, globalMaxSeedingMinutes()); - const int inactiveSeedingTimeLimit = effectiveLimit(torrent->inactiveSeedingTimeLimit(), Torrent::USE_GLOBAL_INACTIVE_SEEDING_TIME, globalMaxInactiveSeedingMinutes()); + if (reached) + { + const QString torrentName = tr("Torrent: \"%1\".").arg(torrent->name()); + const ShareLimitAction shareLimitAction = (torrent->shareLimitAction() == ShareLimitAction::Default) ? m_shareLimitAction : torrent->shareLimitAction(); - bool reached = false; - QString description; - - if (const qreal ratio = torrent->realRatio(); - (ratioLimit >= 0) && (ratio <= Torrent::MAX_RATIO) && (ratio >= ratioLimit)) + if (shareLimitAction == ShareLimitAction::Remove) { - reached = true; - description = tr("Torrent reached the share ratio limit."); + LogMsg(u"%1 %2 %3"_s.arg(description, tr("Removing torrent."), torrentName)); + removeTorrent(torrent->id(), TorrentRemoveOption::KeepContent); } - else if (const qlonglong seedingTimeInMinutes = torrent->finishedTime() / 60; - (seedingTimeLimit >= 0) && (seedingTimeInMinutes <= Torrent::MAX_SEEDING_TIME) && (seedingTimeInMinutes >= seedingTimeLimit)) + else if (shareLimitAction == ShareLimitAction::RemoveWithContent) { - reached = true; - description = tr("Torrent reached the seeding time limit."); + LogMsg(u"%1 %2 %3"_s.arg(description, tr("Removing torrent and deleting its content."), torrentName)); + removeTorrent(torrent->id(), TorrentRemoveOption::RemoveContent); } - else if (const qlonglong inactiveSeedingTimeInMinutes = torrent->timeSinceActivity() / 60; - (inactiveSeedingTimeLimit >= 0) && (inactiveSeedingTimeInMinutes <= Torrent::MAX_INACTIVE_SEEDING_TIME) && (inactiveSeedingTimeInMinutes >= inactiveSeedingTimeLimit)) + else if ((shareLimitAction == ShareLimitAction::Stop) && !torrent->isStopped()) { - reached = true; - description = tr("Torrent reached the inactive seeding time limit."); + torrent->stop(); + LogMsg(u"%1 %2 %3"_s.arg(description, tr("Torrent stopped."), torrentName)); } - - if (reached) + else if ((shareLimitAction == ShareLimitAction::EnableSuperSeeding) && !torrent->isStopped() && !torrent->superSeeding()) { - const QString torrentName = tr("Torrent: \"%1\".").arg(torrent->name()); - const ShareLimitAction shareLimitAction = (torrent->shareLimitAction() == ShareLimitAction::Default) ? m_shareLimitAction : torrent->shareLimitAction(); - - if (shareLimitAction == ShareLimitAction::Remove) - { - LogMsg(u"%1 %2 %3"_s.arg(description, tr("Removing torrent."), torrentName)); - deleteTorrent(torrentID); - } - else if (shareLimitAction == ShareLimitAction::RemoveWithContent) - { - LogMsg(u"%1 %2 %3"_s.arg(description, tr("Removing torrent and deleting its content."), torrentName)); - deleteTorrent(torrentID, DeleteTorrentAndFiles); - } - else if ((shareLimitAction == ShareLimitAction::Stop) && !torrent->isStopped()) - { - torrent->stop(); - LogMsg(u"%1 %2 %3"_s.arg(description, tr("Torrent stopped."), torrentName)); - } - else if ((shareLimitAction == ShareLimitAction::EnableSuperSeeding) && !torrent->isStopped() && !torrent->superSeeding()) - { - torrent->setSuperSeeding(true); - LogMsg(u"%1 %2 %3"_s.arg(description, tr("Super seeding enabled."), torrentName)); - } + torrent->setSuperSeeding(true); + LogMsg(u"%1 %2 %3"_s.arg(description, tr("Super seeding enabled."), torrentName)); } } } @@ -2331,6 +2339,19 @@ void SessionImpl::fileSearchFinished(const TorrentID &id, const Path &savePath, } } +void SessionImpl::torrentContentRemovingFinished(const QString &torrentName, const QString &errorMessage) +{ + if (errorMessage.isEmpty()) + { + LogMsg(tr("Torrent content removed. Torrent: \"%1\"").arg(torrentName)); + } + else + { + LogMsg(tr("Failed to remove torrent content. Torrent: \"%1\". Error: \"%2\"") + .arg(torrentName, errorMessage), Log::WARNING); + } +} + Torrent *SessionImpl::getTorrent(const TorrentID &id) const { return m_torrents.value(id); @@ -2377,26 +2398,29 @@ void SessionImpl::banIP(const QString &ip) // Delete a torrent from the session, given its hash // and from the disk, if the corresponding deleteOption is chosen -bool SessionImpl::deleteTorrent(const TorrentID &id, const DeleteOption deleteOption) +bool SessionImpl::removeTorrent(const TorrentID &id, const TorrentRemoveOption deleteOption) { TorrentImpl *const torrent = m_torrents.take(id); if (!torrent) return false; - qDebug("Deleting torrent with ID: %s", qUtf8Printable(torrent->id().toString())); + const TorrentID torrentID = torrent->id(); + const QString torrentName = torrent->name(); + + qDebug("Deleting torrent with ID: %s", qUtf8Printable(torrentID.toString())); emit torrentAboutToBeRemoved(torrent); if (const InfoHash infoHash = torrent->infoHash(); infoHash.isHybrid()) m_hybridTorrentsByAltID.remove(TorrentID::fromSHA1Hash(infoHash.v1())); // Remove it from session - if (deleteOption == DeleteTorrent) + if (deleteOption == TorrentRemoveOption::KeepContent) { - m_removingTorrents[torrent->id()] = {torrent->name(), {}, deleteOption}; + m_removingTorrents[torrentID] = {torrentName, torrent->actualStorageLocation(), {}, deleteOption}; const lt::torrent_handle nativeHandle {torrent->nativeHandle()}; const auto iter = std::find_if(m_moveStorageQueue.begin(), m_moveStorageQueue.end() - , [&nativeHandle](const MoveStorageJob &job) + , [&nativeHandle](const MoveStorageJob &job) { return job.torrentHandle == nativeHandle; }); @@ -2414,14 +2438,14 @@ bool SessionImpl::deleteTorrent(const TorrentID &id, const DeleteOption deleteOp } else { - m_removingTorrents[torrent->id()] = {torrent->name(), torrent->rootPath(), deleteOption}; + m_removingTorrents[torrentID] = {torrentName, torrent->actualStorageLocation(), torrent->actualFilePaths(), deleteOption}; if (m_moveStorageQueue.size() > 1) { // Delete "move storage job" for the deleted torrent // (note: we shouldn't delete active job) const auto iter = std::find_if((m_moveStorageQueue.begin() + 1), m_moveStorageQueue.end() - , [torrent](const MoveStorageJob &job) + , [torrent](const MoveStorageJob &job) { return job.torrentHandle == torrent->nativeHandle(); }); @@ -2429,12 +2453,13 @@ bool SessionImpl::deleteTorrent(const TorrentID &id, const DeleteOption deleteOp m_moveStorageQueue.erase(iter); } - m_nativeSession->remove_torrent(torrent->nativeHandle(), lt::session::delete_files); + m_nativeSession->remove_torrent(torrent->nativeHandle(), lt::session::delete_partfile); } // Remove it from torrent resume directory - m_resumeDataStorage->remove(torrent->id()); + m_resumeDataStorage->remove(torrentID); + LogMsg(tr("Torrent removed. Torrent: \"%1\"").arg(torrentName)); delete torrent; return true; } @@ -2462,7 +2487,7 @@ bool SessionImpl::cancelDownloadMetadata(const TorrentID &id) } #endif - m_nativeSession->remove_torrent(nativeHandle, lt::session::delete_files); + m_nativeSession->remove_torrent(nativeHandle); return true; } @@ -2769,26 +2794,22 @@ bool SessionImpl::addTorrent_impl(const TorrentDescriptor &source, const AddTorr Q_ASSERT(p.file_priorities.empty()); Q_ASSERT(addTorrentParams.filePriorities.isEmpty() || (addTorrentParams.filePriorities.size() == nativeIndexes.size())); + QList filePriorities = addTorrentParams.filePriorities; + + if (filePriorities.isEmpty() && isExcludedFileNamesEnabled()) + { + // Check file name blacklist when priorities are not explicitly set + applyFilenameFilter(filePaths, filePriorities); + } + const int internalFilesCount = torrentInfo.nativeInfo()->files().num_files(); // including .pad files // Use qBittorrent default priority rather than libtorrent's (4) p.file_priorities = std::vector(internalFilesCount, LT::toNative(DownloadPriority::Normal)); - if (addTorrentParams.filePriorities.isEmpty()) + if (!filePriorities.isEmpty()) { - if (isExcludedFileNamesEnabled()) - { - // Check file name blacklist when priorities are not explicitly set - for (int i = 0; i < filePaths.size(); ++i) - { - if (isFilenameExcluded(filePaths.at(i).filename())) - p.file_priorities[LT::toUnderlyingType(nativeIndexes[i])] = lt::dont_download; - } - } - } - else - { - for (int i = 0; i < addTorrentParams.filePriorities.size(); ++i) - p.file_priorities[LT::toUnderlyingType(nativeIndexes[i])] = LT::toNative(addTorrentParams.filePriorities[i]); + for (int i = 0; i < filePriorities.size(); ++i) + p.file_priorities[LT::toUnderlyingType(nativeIndexes[i])] = LT::toNative(filePriorities[i]); } Q_ASSERT(p.ti); @@ -3874,21 +3895,41 @@ void SessionImpl::populateExcludedFileNamesRegExpList() for (const QString &str : excludedNames) { - const QString pattern = QRegularExpression::anchoredPattern(QRegularExpression::wildcardToRegularExpression(str)); + const QString pattern = QRegularExpression::wildcardToRegularExpression(str); const QRegularExpression re {pattern, QRegularExpression::CaseInsensitiveOption}; m_excludedFileNamesRegExpList.append(re); } } -bool SessionImpl::isFilenameExcluded(const QString &fileName) const +void SessionImpl::applyFilenameFilter(const PathList &files, QList &priorities) { if (!isExcludedFileNamesEnabled()) - return false; + return; - return std::any_of(m_excludedFileNamesRegExpList.begin(), m_excludedFileNamesRegExpList.end(), [&fileName](const QRegularExpression &re) + const auto isFilenameExcluded = [patterns = m_excludedFileNamesRegExpList](const Path &fileName) { - return re.match(fileName).hasMatch(); - }); + return std::any_of(patterns.begin(), patterns.end(), [&fileName](const QRegularExpression &re) + { + Path path = fileName; + while (!re.match(path.filename()).hasMatch()) + { + path = path.parentPath(); + if (path.isEmpty()) + return false; + } + return true; + }); + }; + + priorities.resize(files.count(), DownloadPriority::Normal); + for (int i = 0; i < priorities.size(); ++i) + { + if (priorities[i] == BitTorrent::DownloadPriority::Ignored) + continue; + + if (isFilenameExcluded(files.at(i))) + priorities[i] = BitTorrent::DownloadPriority::Ignored; + } } void SessionImpl::setBannedIPs(const QStringList &newList) @@ -3957,6 +3998,16 @@ void SessionImpl::setStartPaused(const bool value) m_startPaused = value; } +TorrentContentRemoveOption SessionImpl::torrentContentRemoveOption() const +{ + return m_torrentContentRemoveOption; +} + +void SessionImpl::setTorrentContentRemoveOption(const TorrentContentRemoveOption option) +{ + m_torrentContentRemoveOption = option; +} + QStringList SessionImpl::bannedIPs() const { return m_bannedIPs; @@ -4890,7 +4941,7 @@ void SessionImpl::updateSeedingLimitTimer() if ((globalMaxRatio() == Torrent::NO_RATIO_LIMIT) && !hasPerTorrentRatioLimit() && (globalMaxSeedingMinutes() == Torrent::NO_SEEDING_TIME_LIMIT) && !hasPerTorrentSeedingTimeLimit() && (globalMaxInactiveSeedingMinutes() == Torrent::NO_INACTIVE_SEEDING_TIME_LIMIT) && !hasPerTorrentInactiveSeedingTimeLimit()) - { + { if (m_seedingLimitTimer->isActive()) m_seedingLimitTimer->stop(); } @@ -5002,18 +5053,7 @@ void SessionImpl::handleTorrentChecked(TorrentImpl *const torrent) void SessionImpl::handleTorrentFinished(TorrentImpl *const torrent) { - LogMsg(tr("Torrent download finished. Torrent: \"%1\"").arg(torrent->name())); - emit torrentFinished(torrent); - - if (const Path exportPath = finishedTorrentExportDirectory(); !exportPath.isEmpty()) - exportTorrentFile(torrent, exportPath); - - const bool hasUnfinishedTorrents = std::any_of(m_torrents.cbegin(), m_torrents.cend(), [](const TorrentImpl *torrent) - { - return !(torrent->isFinished() || torrent->isStopped() || torrent->isErrored()); - }); - if (!hasUnfinishedTorrents) - emit allTorrentsFinished(); + m_pendingFinishedTorrents.append(torrent); } void SessionImpl::handleTorrentResumeDataReady(TorrentImpl *const torrent, const LoadTorrentParams &data) @@ -5141,7 +5181,7 @@ void SessionImpl::handleMoveTorrentStorageJobFinished(const Path &newPath) // Last job is completed for torrent that being removing, so actually remove it const lt::torrent_handle nativeHandle {finishedJob.torrentHandle}; const RemovingTorrentData &removingTorrentData = m_removingTorrents[nativeHandle.info_hash()]; - if (removingTorrentData.deleteOption == DeleteTorrent) + if (removingTorrentData.removeOption == TorrentRemoveOption::KeepContent) m_nativeSession->remove_torrent(nativeHandle, lt::session::delete_partfile); } } @@ -5660,74 +5700,32 @@ TorrentImpl *SessionImpl::createTorrent(const lt::torrent_handle &nativeHandle, return torrent; } -void SessionImpl::handleTorrentRemovedAlert(const lt::torrent_removed_alert *alert) +void SessionImpl::handleTorrentRemovedAlert(const lt::torrent_removed_alert */*alert*/) { -#ifdef QBT_USES_LIBTORRENT2 - const auto id = TorrentID::fromInfoHash(alert->info_hashes); -#else - const auto id = TorrentID::fromInfoHash(alert->info_hash); -#endif - - const auto removingTorrentDataIter = m_removingTorrents.find(id); - if (removingTorrentDataIter != m_removingTorrents.end()) - { - if (removingTorrentDataIter->deleteOption == DeleteTorrent) - { - LogMsg(tr("Removed torrent. Torrent: \"%1\"").arg(removingTorrentDataIter->name)); - m_removingTorrents.erase(removingTorrentDataIter); - } - } + // We cannot consider `torrent_removed_alert` as a starting point for removing content, + // because it has an inconsistent posting time between different versions of libtorrent, + // so files may still be in use in some cases. } void SessionImpl::handleTorrentDeletedAlert(const lt::torrent_deleted_alert *alert) { #ifdef QBT_USES_LIBTORRENT2 - const auto id = TorrentID::fromInfoHash(alert->info_hashes); + const auto torrentID = TorrentID::fromInfoHash(alert->info_hashes); #else - const auto id = TorrentID::fromInfoHash(alert->info_hash); + const auto torrentID = TorrentID::fromInfoHash(alert->info_hash); #endif - - const auto removingTorrentDataIter = m_removingTorrents.find(id); - if (removingTorrentDataIter == m_removingTorrents.end()) - return; - - // torrent_deleted_alert can also be posted due to deletion of partfile. Ignore it in such a case. - if (removingTorrentDataIter->deleteOption == DeleteTorrent) - return; - - Utils::Fs::smartRemoveEmptyFolderTree(removingTorrentDataIter->pathToRemove); - LogMsg(tr("Removed torrent and deleted its content. Torrent: \"%1\"").arg(removingTorrentDataIter->name)); - m_removingTorrents.erase(removingTorrentDataIter); + handleRemovedTorrent(torrentID); } void SessionImpl::handleTorrentDeleteFailedAlert(const lt::torrent_delete_failed_alert *alert) { #ifdef QBT_USES_LIBTORRENT2 - const auto id = TorrentID::fromInfoHash(alert->info_hashes); + const auto torrentID = TorrentID::fromInfoHash(alert->info_hashes); #else - const auto id = TorrentID::fromInfoHash(alert->info_hash); + const auto torrentID = TorrentID::fromInfoHash(alert->info_hash); #endif - - const auto removingTorrentDataIter = m_removingTorrents.find(id); - if (removingTorrentDataIter == m_removingTorrents.end()) - return; - - if (alert->error) - { - // libtorrent won't delete the directory if it contains files not listed in the torrent, - // so we remove the directory ourselves - Utils::Fs::smartRemoveEmptyFolderTree(removingTorrentDataIter->pathToRemove); - - LogMsg(tr("Removed torrent but failed to delete its content and/or partfile. Torrent: \"%1\". Error: \"%2\"") - .arg(removingTorrentDataIter->name, QString::fromLocal8Bit(alert->error.message().c_str())) - , Log::WARNING); - } - else // torrent without metadata, hence no files on disk - { - LogMsg(tr("Removed torrent. Torrent: \"%1\"").arg(removingTorrentDataIter->name)); - } - - m_removingTorrents.erase(removingTorrentDataIter); + const auto errorMessage = alert->error ? QString::fromLocal8Bit(alert->error.message().c_str()) : QString(); + handleRemovedTorrent(torrentID, errorMessage); } void SessionImpl::handleTorrentNeedCertAlert(const lt::torrent_need_cert_alert *alert) @@ -6079,6 +6077,29 @@ void SessionImpl::handleStateUpdateAlert(const lt::state_update_alert *alert) if (!updatedTorrents.isEmpty()) emit torrentsUpdated(updatedTorrents); + if (!m_pendingFinishedTorrents.isEmpty()) + { + for (TorrentImpl *torrent : m_pendingFinishedTorrents) + { + LogMsg(tr("Torrent download finished. Torrent: \"%1\"").arg(torrent->name())); + emit torrentFinished(torrent); + + if (const Path exportPath = finishedTorrentExportDirectory(); !exportPath.isEmpty()) + exportTorrentFile(torrent, exportPath); + + processTorrentShareLimits(torrent); + } + + m_pendingFinishedTorrents.clear(); + + const bool hasUnfinishedTorrents = std::any_of(m_torrents.cbegin(), m_torrents.cend(), [](const TorrentImpl *torrent) + { + return !(torrent->isFinished() || torrent->isStopped() || torrent->isErrored()); + }); + if (!hasUnfinishedTorrents) + emit allTorrentsFinished(); + } + if (m_needSaveTorrentsQueue) saveTorrentsQueue(); @@ -6140,7 +6161,7 @@ void SessionImpl::handleTorrentConflictAlert(const lt::torrent_conflict_alert *a if (torrent2) { if (torrent1) - deleteTorrent(torrentIDv1); + removeTorrent(torrentIDv1); else cancelDownloadMetadata(torrentIDv1); @@ -6249,3 +6270,29 @@ void SessionImpl::updateTrackerEntryStatuses(lt::torrent_handle torrentHandle, Q } }); } + +void SessionImpl::handleRemovedTorrent(const TorrentID &torrentID, const QString &partfileRemoveError) +{ + const auto removingTorrentDataIter = m_removingTorrents.find(torrentID); + if (removingTorrentDataIter == m_removingTorrents.end()) + return; + + if (!partfileRemoveError.isEmpty()) + { + LogMsg(tr("Failed to remove partfile. Torrent: \"%1\". Reason: \"%2\".") + .arg(removingTorrentDataIter->name, partfileRemoveError) + , Log::WARNING); + } + + if ((removingTorrentDataIter->removeOption == TorrentRemoveOption::RemoveContent) + && !removingTorrentDataIter->contentStoragePath.isEmpty()) + { + QMetaObject::invokeMethod(m_torrentContentRemover, [this, jobData = *removingTorrentDataIter] + { + m_torrentContentRemover->performJob(jobData.name, jobData.contentStoragePath + , jobData.fileNames, m_torrentContentRemoveOption); + }); + } + + m_removingTorrents.erase(removingTorrentDataIter); +} diff --git a/src/base/bittorrent/sessionimpl.h b/src/base/bittorrent/sessionimpl.h index 40ffbfffa..a5bac0847 100644 --- a/src/base/bittorrent/sessionimpl.h +++ b/src/base/bittorrent/sessionimpl.h @@ -75,6 +75,7 @@ namespace BitTorrent class InfoHash; class ResumeDataStorage; class Torrent; + class TorrentContentRemover; class TorrentDescriptor; class TorrentImpl; class Tracker; @@ -402,7 +403,7 @@ namespace BitTorrent void setExcludedFileNamesEnabled(bool enabled) override; QStringList excludedFileNames() const override; void setExcludedFileNames(const QStringList &excludedFileNames) override; - bool isFilenameExcluded(const QString &fileName) const override; + void applyFilenameFilter(const PathList &files, QList &priorities) override; QStringList bannedIPs() const override; void setBannedIPs(const QStringList &newList) override; ResumeDataStorageType resumeDataStorageType() const override; @@ -411,6 +412,8 @@ namespace BitTorrent void setMergeTrackersEnabled(bool enabled) override; bool isStartPaused() const override; void setStartPaused(bool value) override; + TorrentContentRemoveOption torrentContentRemoveOption() const override; + void setTorrentContentRemoveOption(TorrentContentRemoveOption option) override; bool isRestored() const override; @@ -430,7 +433,7 @@ namespace BitTorrent bool isKnownTorrent(const InfoHash &infoHash) const override; bool addTorrent(const TorrentDescriptor &torrentDescr, const AddTorrentParams ¶ms = {}) override; - bool deleteTorrent(const TorrentID &id, DeleteOption deleteOption = DeleteTorrent) override; + bool removeTorrent(const TorrentID &id, TorrentRemoveOption deleteOption = TorrentRemoveOption::KeepContent) override; bool downloadMetadata(const TorrentDescriptor &torrentDescr) override; bool cancelDownloadMetadata(const TorrentID &id) override; @@ -487,11 +490,11 @@ namespace BitTorrent void configureDeferred(); void readAlerts(); void enqueueRefresh(); - void processShareLimits(); void generateResumeData(); void handleIPFilterParsed(int ruleCount); void handleIPFilterError(); void fileSearchFinished(const TorrentID &id, const Path &savePath, const PathList &fileNames); + void torrentContentRemovingFinished(const QString &torrentName, const QString &errorMessage); private: struct ResumeSessionContext; @@ -507,8 +510,9 @@ namespace BitTorrent struct RemovingTorrentData { QString name; - Path pathToRemove; - DeleteOption deleteOption {}; + Path contentStoragePath; + PathList fileNames; + TorrentRemoveOption removeOption {}; }; explicit SessionImpl(QObject *parent = nullptr); @@ -536,6 +540,7 @@ namespace BitTorrent void enableIPFilter(); void disableIPFilter(); void processTrackerStatuses(); + void processTorrentShareLimits(TorrentImpl *torrent); void populateExcludedFileNamesRegExpList(); void prepareStartup(); void handleLoadedResumeData(ResumeSessionContext *context); @@ -599,13 +604,7 @@ namespace BitTorrent void updateTrackerEntryStatuses(lt::torrent_handle torrentHandle, QHash>> updatedTrackers); - // BitTorrent - lt::session *m_nativeSession = nullptr; - NativeSessionExtension *m_nativeSessionExtension = nullptr; - - bool m_deferredConfigureScheduled = false; - bool m_IPFilteringConfigured = false; - mutable bool m_listenInterfaceConfigured = false; + void handleRemovedTorrent(const TorrentID &torrentID, const QString &partfileRemoveError = {}); CachedSettingValue m_DHTBootstrapNodes; CachedSettingValue m_isDHTEnabled; @@ -731,8 +730,16 @@ namespace BitTorrent CachedSettingValue m_I2POutboundQuantity; CachedSettingValue m_I2PInboundLength; CachedSettingValue m_I2POutboundLength; + CachedSettingValue m_torrentContentRemoveOption; SettingValue m_startPaused; + lt::session *m_nativeSession = nullptr; + NativeSessionExtension *m_nativeSessionExtension = nullptr; + + bool m_deferredConfigureScheduled = false; + bool m_IPFilteringConfigured = false; + mutable bool m_listenInterfaceConfigured = false; + bool m_isRestored = false; bool m_isPaused = isStartPaused(); @@ -766,6 +773,7 @@ namespace BitTorrent QThreadPool *m_asyncWorker = nullptr; ResumeDataStorage *m_resumeDataStorage = nullptr; FileSearcher *m_fileSearcher = nullptr; + TorrentContentRemover *m_torrentContentRemover = nullptr; QHash m_downloadedMetadata; @@ -809,6 +817,8 @@ namespace BitTorrent QTimer *m_wakeupCheckTimer = nullptr; QDateTime m_wakeupCheckTimestamp; + QList m_pendingFinishedTorrents; + friend void Session::initInstance(); friend void Session::freeInstance(); friend Session *Session::instance(); diff --git a/src/base/bittorrent/torrent.h b/src/base/bittorrent/torrent.h index 34cccd4ff..2dcffdf85 100644 --- a/src/base/bittorrent/torrent.h +++ b/src/base/bittorrent/torrent.h @@ -228,6 +228,7 @@ namespace BitTorrent virtual void setShareLimitAction(ShareLimitAction action) = 0; virtual PathList filePaths() const = 0; + virtual PathList actualFilePaths() const = 0; virtual TorrentInfo info() const = 0; virtual bool isFinished() const = 0; diff --git a/src/base/bittorrent/torrentcontentremoveoption.h b/src/base/bittorrent/torrentcontentremoveoption.h new file mode 100644 index 000000000..1083e5ec0 --- /dev/null +++ b/src/base/bittorrent/torrentcontentremoveoption.h @@ -0,0 +1,50 @@ +/* + * Bittorrent Client using Qt and libtorrent. + * Copyright (C) 2024 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 + +namespace BitTorrent +{ + // Using `Q_ENUM_NS()` without a wrapper namespace in our case is not advised + // since `Q_NAMESPACE` cannot be used when the same namespace resides at different files. + // https://www.kdab.com/new-qt-5-8-meta-object-support-namespaces/#comment-143779 + inline namespace TorrentContentRemoveOptionNS + { + Q_NAMESPACE + + enum class TorrentContentRemoveOption + { + Delete, + MoveToTrash + }; + + Q_ENUM_NS(TorrentContentRemoveOption) + } +} diff --git a/src/base/bittorrent/torrentcontentremover.cpp b/src/base/bittorrent/torrentcontentremover.cpp new file mode 100644 index 000000000..861f4be96 --- /dev/null +++ b/src/base/bittorrent/torrentcontentremover.cpp @@ -0,0 +1,61 @@ +/* + * Bittorrent Client using Qt and libtorrent. + * Copyright (C) 2024 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 "torrentcontentremover.h" + +#include "base/utils/fs.h" + +void BitTorrent::TorrentContentRemover::performJob(const QString &torrentName, const Path &basePath + , const PathList &fileNames, const TorrentContentRemoveOption option) +{ + QString errorMessage; + + if (!fileNames.isEmpty()) + { + const auto removeFileFn = [&option](const Path &filePath) + { + return ((option == TorrentContentRemoveOption::MoveToTrash) + ? Utils::Fs::moveFileToTrash : Utils::Fs::removeFile)(filePath); + }; + + for (const Path &fileName : fileNames) + { + if (const auto result = removeFileFn(basePath / fileName) + ; !result && errorMessage.isEmpty()) + { + errorMessage = result.error(); + } + } + + const Path rootPath = Path::findRootFolder(fileNames); + if (!rootPath.isEmpty()) + Utils::Fs::smartRemoveEmptyFolderTree(basePath / rootPath); + } + + emit jobFinished(torrentName, errorMessage); +} diff --git a/src/base/bittorrent/torrentcontentremover.h b/src/base/bittorrent/torrentcontentremover.h new file mode 100644 index 000000000..a18161493 --- /dev/null +++ b/src/base/bittorrent/torrentcontentremover.h @@ -0,0 +1,53 @@ +/* + * Bittorrent Client using Qt and libtorrent. + * Copyright (C) 2024 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 "base/path.h" +#include "torrentcontentremoveoption.h" + +namespace BitTorrent +{ + class TorrentContentRemover final : public QObject + { + Q_OBJECT + Q_DISABLE_COPY_MOVE(TorrentContentRemover) + + public: + using QObject::QObject; + + public slots: + void performJob(const QString &torrentName, const Path &basePath + , const PathList &fileNames, TorrentContentRemoveOption option); + + signals: + void jobFinished(const QString &torrentName, const QString &errorMessage); + }; +} diff --git a/src/base/bittorrent/torrentimpl.cpp b/src/base/bittorrent/torrentimpl.cpp index 4cc41c084..b29f8f06e 100644 --- a/src/base/bittorrent/torrentimpl.cpp +++ b/src/base/bittorrent/torrentimpl.cpp @@ -77,6 +77,10 @@ #include "base/utils/os.h" #endif // Q_OS_MACOS || Q_OS_WIN +#ifndef QBT_USES_LIBTORRENT2 +#include "customstorage.h" +#endif + using namespace BitTorrent; namespace @@ -982,6 +986,21 @@ PathList TorrentImpl::filePaths() const return m_filePaths; } +PathList TorrentImpl::actualFilePaths() const +{ + if (!hasMetadata()) + return {}; + + PathList paths; + paths.reserve(filesCount()); + + const lt::file_storage files = nativeTorrentInfo()->files(); + for (const lt::file_index_t &nativeIndex : asConst(m_torrentInfo.nativeIndexes())) + paths.emplaceBack(files.file_path(nativeIndex)); + + return paths; +} + QVector TorrentImpl::filePriorities() const { return m_filePriorities; @@ -1447,11 +1466,13 @@ QBitArray TorrentImpl::pieces() const QBitArray TorrentImpl::downloadingPieces() const { - QBitArray result(piecesCount()); + if (!hasMetadata()) + return {}; std::vector queue; m_nativeHandle.get_download_queue(queue); + QBitArray result {piecesCount()}; for (const lt::partial_piece_info &info : queue) result.setBit(LT::toUnderlyingType(info.piece_index)); @@ -1791,12 +1812,13 @@ void TorrentImpl::endReceivedMetadataHandling(const Path &savePath, const PathLi const Path filePath = actualFilePath.removedExtension(QB_EXT); m_filePaths.append(filePath); - lt::download_priority_t &nativePriority = p.file_priorities[LT::toUnderlyingType(nativeIndex)]; - if ((nativePriority != lt::dont_download) && m_session->isFilenameExcluded(filePath.filename())) - nativePriority = lt::dont_download; - const auto priority = LT::fromNative(nativePriority); - m_filePriorities.append(priority); + m_filePriorities.append(LT::fromNative(p.file_priorities[LT::toUnderlyingType(nativeIndex)])); } + + m_session->applyFilenameFilter(fileNames, m_filePriorities); + for (int i = 0; i < m_filePriorities.size(); ++i) + p.file_priorities[LT::toUnderlyingType(nativeIndexes[i])] = LT::toNative(m_filePriorities[i]); + p.save_path = savePath.toString().toStdString(); p.ti = metadata; @@ -1859,6 +1881,9 @@ void TorrentImpl::reload() auto *const extensionData = new ExtensionData; p.userdata = LTClientData(extensionData); +#ifndef QBT_USES_LIBTORRENT2 + p.storage = customStorageConstructor; +#endif m_nativeHandle = m_nativeSession->add_torrent(p); m_nativeStatus = extensionData->status; diff --git a/src/base/bittorrent/torrentimpl.h b/src/base/bittorrent/torrentimpl.h index 54b84c1f4..ffc83b202 100644 --- a/src/base/bittorrent/torrentimpl.h +++ b/src/base/bittorrent/torrentimpl.h @@ -153,6 +153,7 @@ namespace BitTorrent Path actualFilePath(int index) const override; qlonglong fileSize(int index) const override; PathList filePaths() const override; + PathList actualFilePaths() const override; QVector filePriorities() const override; TorrentInfo info() const override; diff --git a/src/base/http/connection.cpp b/src/base/http/connection.cpp index 4a96295e4..32a7ce840 100644 --- a/src/base/http/connection.cpp +++ b/src/base/http/connection.cpp @@ -44,6 +44,7 @@ Connection::Connection(QTcpSocket *socket, IRequestHandler *requestHandler, QObj , m_requestHandler(requestHandler) { m_socket->setParent(this); + connect(m_socket, &QAbstractSocket::disconnected, this, &Connection::closed); // reserve common size for requests, don't use the max allowed size which is too big for // memory constrained platforms @@ -62,11 +63,6 @@ Connection::Connection(QTcpSocket *socket, IRequestHandler *requestHandler, QObj }); } -Connection::~Connection() -{ - m_socket->close(); -} - void Connection::read() { // reuse existing buffer and avoid unnecessary memory allocation/relocation @@ -182,11 +178,6 @@ bool Connection::hasExpired(const qint64 timeout) const && m_idleTimer.hasExpired(timeout); } -bool Connection::isClosed() const -{ - return (m_socket->state() == QAbstractSocket::UnconnectedState); -} - bool Connection::acceptsGzipEncoding(QString codings) { // [rfc7231] 5.3.4. Accept-Encoding diff --git a/src/base/http/connection.h b/src/base/http/connection.h index 5b8f67a78..46a263106 100644 --- a/src/base/http/connection.h +++ b/src/base/http/connection.h @@ -47,10 +47,11 @@ namespace Http public: Connection(QTcpSocket *socket, IRequestHandler *requestHandler, QObject *parent = nullptr); - ~Connection(); bool hasExpired(qint64 timeout) const; - bool isClosed() const; + + signals: + void closed(); private: static bool acceptsGzipEncoding(QString codings); diff --git a/src/base/http/server.cpp b/src/base/http/server.cpp index 1e2320e11..19233dc27 100644 --- a/src/base/http/server.cpp +++ b/src/base/http/server.cpp @@ -32,7 +32,10 @@ #include #include +#include +#include +#include #include #include #include @@ -40,7 +43,6 @@ #include #include -#include "base/algorithm.h" #include "base/global.h" #include "base/utils/net.h" #include "base/utils/sslkey.h" @@ -113,32 +115,38 @@ Server::Server(IRequestHandler *requestHandler, QObject *parent) void Server::incomingConnection(const qintptr socketDescriptor) { - if (m_connections.size() >= CONNECTIONS_LIMIT) return; - - QTcpSocket *serverSocket = nullptr; - if (m_https) - serverSocket = new QSslSocket(this); - else - serverSocket = new QTcpSocket(this); - + std::unique_ptr serverSocket = m_https ? std::make_unique(this) : std::make_unique(this); if (!serverSocket->setSocketDescriptor(socketDescriptor)) + return; + + if (m_connections.size() >= CONNECTIONS_LIMIT) { - delete serverSocket; + qWarning("Too many connections. Exceeded CONNECTIONS_LIMIT (%d). Connection closed.", CONNECTIONS_LIMIT); return; } - if (m_https) + try { - static_cast(serverSocket)->setProtocol(QSsl::SecureProtocols); - static_cast(serverSocket)->setPrivateKey(m_key); - static_cast(serverSocket)->setLocalCertificateChain(m_certificates); - static_cast(serverSocket)->setPeerVerifyMode(QSslSocket::VerifyNone); - static_cast(serverSocket)->startServerEncryption(); - } + if (m_https) + { + auto *sslSocket = static_cast(serverSocket.get()); + sslSocket->setProtocol(QSsl::SecureProtocols); + sslSocket->setPrivateKey(m_key); + sslSocket->setLocalCertificateChain(m_certificates); + sslSocket->setPeerVerifyMode(QSslSocket::VerifyNone); + sslSocket->startServerEncryption(); + } - auto *c = new Connection(serverSocket, m_requestHandler, this); - m_connections.insert(c); - connect(serverSocket, &QAbstractSocket::disconnected, this, [c, this]() { removeConnection(c); }); + auto *connection = new Connection(serverSocket.release(), m_requestHandler, this); + m_connections.insert(connection); + connect(connection, &Connection::closed, this, [this, connection] { removeConnection(connection); }); + } + catch (const std::bad_alloc &exception) + { + // drop the connection instead of throwing exception and crash + qWarning("Failed to allocate memory for HTTP connection. Connection closed."); + return; + } } void Server::removeConnection(Connection *connection) diff --git a/src/base/preferences.cpp b/src/base/preferences.cpp index 13c1c8b00..d97390cbb 100644 --- a/src/base/preferences.cpp +++ b/src/base/preferences.cpp @@ -134,17 +134,17 @@ void Preferences::setCustomUIThemePath(const Path &path) setValue(u"Preferences/General/CustomUIThemePath"_s, path); } -bool Preferences::deleteTorrentFilesAsDefault() const +bool Preferences::removeTorrentContent() const { return value(u"Preferences/General/DeleteTorrentsFilesAsDefault"_s, false); } -void Preferences::setDeleteTorrentFilesAsDefault(const bool del) +void Preferences::setRemoveTorrentContent(const bool remove) { - if (del == deleteTorrentFilesAsDefault()) + if (remove == removeTorrentContent()) return; - setValue(u"Preferences/General/DeleteTorrentsFilesAsDefault"_s, del); + setValue(u"Preferences/General/DeleteTorrentsFilesAsDefault"_s, remove); } bool Preferences::confirmOnExit() const diff --git a/src/base/preferences.h b/src/base/preferences.h index 90f26b055..fcc70bb97 100644 --- a/src/base/preferences.h +++ b/src/base/preferences.h @@ -105,8 +105,8 @@ public: void setUseCustomUITheme(bool use); Path customUIThemePath() const; void setCustomUIThemePath(const Path &path); - bool deleteTorrentFilesAsDefault() const; - void setDeleteTorrentFilesAsDefault(bool del); + bool removeTorrentContent() const; + void setRemoveTorrentContent(bool remove); bool confirmOnExit() const; void setConfirmOnExit(bool confirm); bool speedInTitleBar() const; diff --git a/src/base/torrentfilter.cpp b/src/base/torrentfilter.cpp index 7ad8cf112..89d9a97e9 100644 --- a/src/base/torrentfilter.cpp +++ b/src/base/torrentfilter.cpp @@ -52,19 +52,21 @@ const TorrentFilter TorrentFilter::ErroredTorrent(TorrentFilter::Errored); using BitTorrent::Torrent; TorrentFilter::TorrentFilter(const Type type, const std::optional &idSet - , const std::optional &category, const std::optional &tag) + , const std::optional &category, const std::optional &tag, const std::optional isPrivate) : m_type {type} , m_category {category} , m_tag {tag} , m_idSet {idSet} + , m_private {isPrivate} { } TorrentFilter::TorrentFilter(const QString &filter, const std::optional &idSet - , const std::optional &category, const std::optional &tag) + , const std::optional &category, const std::optional &tag, const std::optional isPrivate) : m_category {category} , m_tag {tag} , m_idSet {idSet} + , m_private {isPrivate} { setTypeByName(filter); } @@ -147,11 +149,22 @@ bool TorrentFilter::setTag(const std::optional &tag) return false; } +bool TorrentFilter::setPrivate(const std::optional isPrivate) +{ + if (m_private != isPrivate) + { + m_private = isPrivate; + return true; + } + + return false; +} + bool TorrentFilter::match(const Torrent *const torrent) const { if (!torrent) return false; - return (matchState(torrent) && matchHash(torrent) && matchCategory(torrent) && matchTag(torrent)); + return (matchState(torrent) && matchHash(torrent) && matchCategory(torrent) && matchTag(torrent) && matchPrivate(torrent)); } bool TorrentFilter::matchState(const BitTorrent::Torrent *const torrent) const @@ -224,3 +237,11 @@ bool TorrentFilter::matchTag(const BitTorrent::Torrent *const torrent) const return torrent->hasTag(*m_tag); } + +bool TorrentFilter::matchPrivate(const BitTorrent::Torrent *const torrent) const +{ + if (!m_private) + return true; + + return m_private == torrent->isPrivate(); +} diff --git a/src/base/torrentfilter.h b/src/base/torrentfilter.h index 19092fb8e..39fd3e06f 100644 --- a/src/base/torrentfilter.h +++ b/src/base/torrentfilter.h @@ -87,16 +87,24 @@ public: TorrentFilter() = default; // category & tags: pass empty string for uncategorized / untagged torrents. - TorrentFilter(Type type, const std::optional &idSet = AnyID - , const std::optional &category = AnyCategory, const std::optional &tag = AnyTag); - TorrentFilter(const QString &filter, const std::optional &idSet = AnyID - , const std::optional &category = AnyCategory, const std::optional &tags = AnyTag); + TorrentFilter(Type type + , const std::optional &idSet = AnyID + , const std::optional &category = AnyCategory + , const std::optional &tag = AnyTag + , std::optional isPrivate = {}); + TorrentFilter(const QString &filter + , const std::optional &idSet = AnyID + , const std::optional &category = AnyCategory + , const std::optional &tags = AnyTag + , std::optional isPrivate = {}); + bool setType(Type type); bool setTypeByName(const QString &filter); bool setTorrentIDSet(const std::optional &idSet); bool setCategory(const std::optional &category); bool setTag(const std::optional &tag); + bool setPrivate(std::optional isPrivate); bool match(const BitTorrent::Torrent *torrent) const; @@ -105,9 +113,11 @@ private: bool matchHash(const BitTorrent::Torrent *torrent) const; bool matchCategory(const BitTorrent::Torrent *torrent) const; bool matchTag(const BitTorrent::Torrent *torrent) const; + bool matchPrivate(const BitTorrent::Torrent *torrent) const; Type m_type {All}; std::optional m_category; std::optional m_tag; std::optional m_idSet; + std::optional m_private; }; diff --git a/src/base/utils/fs.cpp b/src/base/utils/fs.cpp index 4d89f49a0..dbcfafb74 100644 --- a/src/base/utils/fs.cpp +++ b/src/base/utils/fs.cpp @@ -1,6 +1,6 @@ /* * Bittorrent Client using Qt and libtorrent. - * Copyright (C) 2022 Vladimir Golovnev + * Copyright (C) 2022-2024 Vladimir Golovnev * Copyright (C) 2012 Christophe Dumez * * This program is free software; you can redistribute it and/or @@ -29,8 +29,6 @@ #include "fs.h" -#include -#include #include #if defined(Q_OS_WIN) @@ -52,6 +50,7 @@ #include #endif +#include #include #include #include @@ -311,20 +310,42 @@ bool Utils::Fs::renameFile(const Path &from, const Path &to) * * This function will try to fix the file permissions before removing it. */ -bool Utils::Fs::removeFile(const Path &path) +nonstd::expected Utils::Fs::removeFile(const Path &path) { - if (QFile::remove(path.data())) - return true; - QFile file {path.data()}; + if (file.remove()) + return {}; + if (!file.exists()) - return true; + return {}; // Make sure we have read/write permissions file.setPermissions(file.permissions() | QFile::ReadOwner | QFile::WriteOwner | QFile::ReadUser | QFile::WriteUser); - return file.remove(); + if (file.remove()) + return {}; + + return nonstd::make_unexpected(file.errorString()); } +nonstd::expected Utils::Fs::moveFileToTrash(const Path &path) +{ + QFile file {path.data()}; + if (file.moveToTrash()) + return {}; + + if (!file.exists()) + return {}; + + // Make sure we have read/write permissions + file.setPermissions(file.permissions() | QFile::ReadOwner | QFile::WriteOwner | QFile::ReadUser | QFile::WriteUser); + if (file.moveToTrash()) + return {}; + + const QString errorMessage = file.errorString(); + return nonstd::make_unexpected(!errorMessage.isEmpty() ? errorMessage : QCoreApplication::translate("fs", "Unknown error")); +} + + bool Utils::Fs::isReadable(const Path &path) { return QFileInfo(path.data()).isReadable(); diff --git a/src/base/utils/fs.h b/src/base/utils/fs.h index 3dccb1d2d..eced8b072 100644 --- a/src/base/utils/fs.h +++ b/src/base/utils/fs.h @@ -1,6 +1,6 @@ /* * Bittorrent Client using Qt and libtorrent. - * Copyright (C) 2022 Vladimir Golovnev + * Copyright (C) 2022-2024 Vladimir Golovnev * Copyright (C) 2012 Christophe Dumez * * This program is free software; you can redistribute it and/or @@ -35,6 +35,7 @@ #include +#include "base/3rdparty/expected.hpp" #include "base/global.h" #include "base/pathfwd.h" @@ -60,7 +61,8 @@ namespace Utils::Fs bool copyFile(const Path &from, const Path &to); bool renameFile(const Path &from, const Path &to); - bool removeFile(const Path &path); + nonstd::expected removeFile(const Path &path); + nonstd::expected moveFileToTrash(const Path &path); bool mkdir(const Path &dirPath); bool mkpath(const Path &dirPath); bool rmdir(const Path &dirPath); diff --git a/src/gui/CMakeLists.txt b/src/gui/CMakeLists.txt index 1a426ed6e..9db94e77e 100644 --- a/src/gui/CMakeLists.txt +++ b/src/gui/CMakeLists.txt @@ -52,6 +52,8 @@ add_library(qbt_gui STATIC desktopintegration.h downloadfromurldialog.h executionlogwidget.h + filterpatternformat.h + filterpatternformatmenu.h flowlayout.h fspathedit.h fspathedit_p.h @@ -151,6 +153,7 @@ add_library(qbt_gui STATIC desktopintegration.cpp downloadfromurldialog.cpp executionlogwidget.cpp + filterpatternformatmenu.cpp flowlayout.cpp fspathedit.cpp fspathedit_p.cpp diff --git a/src/gui/addnewtorrentdialog.cpp b/src/gui/addnewtorrentdialog.cpp index 99ecdda59..07eb4cc16 100644 --- a/src/gui/addnewtorrentdialog.cpp +++ b/src/gui/addnewtorrentdialog.cpp @@ -1,6 +1,6 @@ /* * Bittorrent Client using Qt and libtorrent. - * Copyright (C) 2022-2023 Vladimir Golovnev + * Copyright (C) 2022-2024 Vladimir Golovnev * Copyright (C) 2012 Christophe Dumez * * This program is free software; you can redistribute it and/or @@ -64,6 +64,7 @@ #include "base/utils/fs.h" #include "base/utils/misc.h" #include "base/utils/string.h" +#include "filterpatternformatmenu.h" #include "lineedit.h" #include "torrenttagsdialog.h" @@ -181,6 +182,11 @@ public: return (m_filePaths.isEmpty() ? m_torrentInfo.filePath(index) : m_filePaths.at(index)); } + PathList filePaths() const + { + return (m_filePaths.isEmpty() ? m_torrentInfo.filePaths() : m_filePaths); + } + void renameFile(const int index, const Path &newFilePath) override { Q_ASSERT((index >= 0) && (index < filesCount())); @@ -290,6 +296,7 @@ AddNewTorrentDialog::AddNewTorrentDialog(const BitTorrent::TorrentDescriptor &to , m_storeRememberLastSavePath {SETTINGS_KEY(u"RememberLastSavePath"_s)} , m_storeTreeHeaderState {u"GUI/Qt6/" SETTINGS_KEY(u"TreeHeaderState"_s)} , m_storeSplitterState {u"GUI/Qt6/" SETTINGS_KEY(u"SplitterState"_s)} + , m_storeFilterPatternFormat {u"GUI/" SETTINGS_KEY(u"FilterPatternFormat"_s)} { m_ui->setupUi(this); @@ -316,6 +323,8 @@ AddNewTorrentDialog::AddNewTorrentDialog(const BitTorrent::TorrentDescriptor &to // Torrent content filtering m_filterLine->setPlaceholderText(tr("Filter files...")); m_filterLine->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Fixed); + m_filterLine->setContextMenuPolicy(Qt::CustomContextMenu); + connect(m_filterLine, &QWidget::customContextMenuRequested, this, &AddNewTorrentDialog::showContentFilterContextMenu); m_ui->contentFilterLayout->insertWidget(3, m_filterLine); const auto *focusSearchHotkey = new QShortcut(QKeySequence::Find, this); connect(focusSearchHotkey, &QShortcut::activated, this, [this]() @@ -360,7 +369,7 @@ AddNewTorrentDialog::AddNewTorrentDialog(const BitTorrent::TorrentDescriptor &to }); dlg->open(); }); - connect(m_filterLine, &LineEdit::textChanged, m_ui->contentTreeView, &TorrentContentWidget::setFilterPattern); + connect(m_filterLine, &LineEdit::textChanged, this, &AddNewTorrentDialog::setContentFilterPattern); connect(m_ui->buttonSelectAll, &QPushButton::clicked, m_ui->contentTreeView, &TorrentContentWidget::checkAll); connect(m_ui->buttonSelectNone, &QPushButton::clicked, m_ui->contentTreeView, &TorrentContentWidget::checkNone); connect(Preferences::instance(), &Preferences::changed, [] @@ -691,6 +700,28 @@ void AddNewTorrentDialog::saveTorrentFile() } } +void AddNewTorrentDialog::showContentFilterContextMenu() +{ + QMenu *menu = m_filterLine->createStandardContextMenu(); + + auto *formatMenu = new FilterPatternFormatMenu(m_storeFilterPatternFormat.get(FilterPatternFormat::Wildcards), menu); + connect(formatMenu, &FilterPatternFormatMenu::patternFormatChanged, this, [this](const FilterPatternFormat format) + { + m_storeFilterPatternFormat = format; + setContentFilterPattern(); + }); + + menu->addSeparator(); + menu->addMenu(formatMenu); + menu->setAttribute(Qt::WA_DeleteOnClose); + menu->popup(QCursor::pos()); +} + +void AddNewTorrentDialog::setContentFilterPattern() +{ + m_ui->contentTreeView->setFilterPattern(m_filterLine->text(), m_storeFilterPatternFormat.get(FilterPatternFormat::Wildcards)); +} + void AddNewTorrentDialog::populateSavePaths() { Q_ASSERT(m_currentContext); @@ -886,15 +917,7 @@ void AddNewTorrentDialog::setupTreeview() { // Check file name blacklist for torrents that are manually added QVector priorities = m_contentAdaptor->filePriorities(); - for (int i = 0; i < priorities.size(); ++i) - { - if (priorities[i] == BitTorrent::DownloadPriority::Ignored) - continue; - - if (BitTorrent::Session::instance()->isFilenameExcluded(torrentInfo.filePath(i).filename())) - priorities[i] = BitTorrent::DownloadPriority::Ignored; - } - + BitTorrent::Session::instance()->applyFilenameFilter(m_contentAdaptor->filePaths(), priorities); m_contentAdaptor->prioritizeFiles(priorities); } diff --git a/src/gui/addnewtorrentdialog.h b/src/gui/addnewtorrentdialog.h index 1dd1eb1b1..f81bd305b 100644 --- a/src/gui/addnewtorrentdialog.h +++ b/src/gui/addnewtorrentdialog.h @@ -1,6 +1,6 @@ /* * Bittorrent Client using Qt and libtorrent. - * Copyright (C) 2022-2023 Vladimir Golovnev + * Copyright (C) 2022-2024 Vladimir Golovnev * Copyright (C) 2012 Christophe Dumez * * This program is free software; you can redistribute it and/or @@ -35,6 +35,7 @@ #include "base/path.h" #include "base/settingvalue.h" +#include "filterpatternformat.h" class LineEdit; @@ -92,6 +93,8 @@ private: void setMetadataProgressIndicator(bool visibleIndicator, const QString &labelText = {}); void setupTreeview(); void saveTorrentFile(); + void showContentFilterContextMenu(); + void setContentFilterPattern(); Ui::AddNewTorrentDialog *m_ui = nullptr; std::unique_ptr m_contentAdaptor; @@ -107,4 +110,5 @@ private: SettingValue m_storeRememberLastSavePath; SettingValue m_storeTreeHeaderState; SettingValue m_storeSplitterState; + SettingValue m_storeFilterPatternFormat; }; diff --git a/src/gui/advancedsettings.cpp b/src/gui/advancedsettings.cpp index d81cfecc6..15133c222 100644 --- a/src/gui/advancedsettings.cpp +++ b/src/gui/advancedsettings.cpp @@ -63,6 +63,7 @@ namespace // qBittorrent section QBITTORRENT_HEADER, RESUME_DATA_STORAGE, + TORRENT_CONTENT_REMOVE_OPTION, #if defined(QBT_USES_LIBTORRENT2) && !defined(Q_OS_MACOS) MEMORY_WORKING_SET_LIMIT, #endif @@ -364,6 +365,8 @@ void AdvancedSettings::saveAdvancedSettings() const session->setI2PInboundLength(m_spinBoxI2PInboundLength.value()); session->setI2POutboundLength(m_spinBoxI2POutboundLength.value()); #endif + + session->setTorrentContentRemoveOption(m_comboBoxTorrentContentRemoveOption.currentData().value()); } #ifndef QBT_USES_LIBTORRENT2 @@ -472,6 +475,11 @@ void AdvancedSettings::loadAdvancedSettings() m_comboBoxResumeDataStorage.setCurrentIndex(m_comboBoxResumeDataStorage.findData(QVariant::fromValue(session->resumeDataStorageType()))); addRow(RESUME_DATA_STORAGE, tr("Resume data storage type (requires restart)"), &m_comboBoxResumeDataStorage); + m_comboBoxTorrentContentRemoveOption.addItem(tr("Delete files permanently"), QVariant::fromValue(BitTorrent::TorrentContentRemoveOption::Delete)); + m_comboBoxTorrentContentRemoveOption.addItem(tr("Move files to trash (if possible)"), QVariant::fromValue(BitTorrent::TorrentContentRemoveOption::MoveToTrash)); + m_comboBoxTorrentContentRemoveOption.setCurrentIndex(m_comboBoxTorrentContentRemoveOption.findData(QVariant::fromValue(session->torrentContentRemoveOption()))); + addRow(TORRENT_CONTENT_REMOVE_OPTION, tr("Torrent content removing mode"), &m_comboBoxTorrentContentRemoveOption); + #if defined(QBT_USES_LIBTORRENT2) && !defined(Q_OS_MACOS) // Physical memory (RAM) usage limit m_spinBoxMemoryWorkingSetLimit.setMinimum(1); diff --git a/src/gui/advancedsettings.h b/src/gui/advancedsettings.h index 386a44d10..c1f7b9a66 100644 --- a/src/gui/advancedsettings.h +++ b/src/gui/advancedsettings.h @@ -81,7 +81,7 @@ private: m_checkBoxMultiConnectionsPerIp, m_checkBoxValidateHTTPSTrackerCertificate, m_checkBoxSSRFMitigation, m_checkBoxBlockPeersOnPrivilegedPorts, m_checkBoxPieceExtentAffinity, m_checkBoxSuggestMode, m_checkBoxSpeedWidgetEnabled, m_checkBoxIDNSupport, m_checkBoxConfirmRemoveTrackerFromAllTorrents, m_checkBoxStartSessionPaused; QComboBox m_comboBoxInterface, m_comboBoxInterfaceAddress, m_comboBoxDiskIOReadMode, m_comboBoxDiskIOWriteMode, m_comboBoxUtpMixedMode, m_comboBoxChokingAlgorithm, - m_comboBoxSeedChokingAlgorithm, m_comboBoxResumeDataStorage; + m_comboBoxSeedChokingAlgorithm, m_comboBoxResumeDataStorage, m_comboBoxTorrentContentRemoveOption; QLineEdit m_lineEditAppInstanceName, m_pythonExecutablePath, m_lineEditAnnounceIP, m_lineEditDHTBootstrapNodes; #ifndef QBT_USES_LIBTORRENT2 diff --git a/src/gui/deletionconfirmationdialog.cpp b/src/gui/deletionconfirmationdialog.cpp index e667a4ad3..9f5fe5452 100644 --- a/src/gui/deletionconfirmationdialog.cpp +++ b/src/gui/deletionconfirmationdialog.cpp @@ -1,5 +1,6 @@ /* * Bittorrent Client using Qt and libtorrent. + * Copyright (C) 2024 Vladimir Golovnev * Copyright (C) 2006 Christophe Dumez * * This program is free software; you can redistribute it and/or @@ -30,6 +31,7 @@ #include +#include "base/bittorrent/session.h" #include "base/global.h" #include "base/preferences.h" #include "uithememanager.h" @@ -53,8 +55,8 @@ DeletionConfirmationDialog::DeletionConfirmationDialog(QWidget *parent, const in m_ui->rememberBtn->setIcon(UIThemeManager::instance()->getIcon(u"object-locked"_s)); m_ui->rememberBtn->setIconSize(Utils::Gui::mediumIconSize()); - m_ui->checkPermDelete->setChecked(defaultDeleteFiles || Preferences::instance()->deleteTorrentFilesAsDefault()); - connect(m_ui->checkPermDelete, &QCheckBox::clicked, this, &DeletionConfirmationDialog::updateRememberButtonState); + m_ui->checkRemoveContent->setChecked(defaultDeleteFiles || Preferences::instance()->removeTorrentContent()); + connect(m_ui->checkRemoveContent, &QCheckBox::clicked, this, &DeletionConfirmationDialog::updateRememberButtonState); m_ui->buttonBox->button(QDialogButtonBox::Ok)->setText(tr("Remove")); m_ui->buttonBox->button(QDialogButtonBox::Cancel)->setFocus(); @@ -67,18 +69,18 @@ DeletionConfirmationDialog::~DeletionConfirmationDialog() delete m_ui; } -bool DeletionConfirmationDialog::isDeleteFileSelected() const +bool DeletionConfirmationDialog::isRemoveContentSelected() const { - return m_ui->checkPermDelete->isChecked(); + return m_ui->checkRemoveContent->isChecked(); } void DeletionConfirmationDialog::updateRememberButtonState() { - m_ui->rememberBtn->setEnabled(m_ui->checkPermDelete->isChecked() != Preferences::instance()->deleteTorrentFilesAsDefault()); + m_ui->rememberBtn->setEnabled(m_ui->checkRemoveContent->isChecked() != Preferences::instance()->removeTorrentContent()); } void DeletionConfirmationDialog::on_rememberBtn_clicked() { - Preferences::instance()->setDeleteTorrentFilesAsDefault(m_ui->checkPermDelete->isChecked()); + Preferences::instance()->setRemoveTorrentContent(m_ui->checkRemoveContent->isChecked()); m_ui->rememberBtn->setEnabled(false); } diff --git a/src/gui/deletionconfirmationdialog.h b/src/gui/deletionconfirmationdialog.h index f93f1746b..9ff657849 100644 --- a/src/gui/deletionconfirmationdialog.h +++ b/src/gui/deletionconfirmationdialog.h @@ -1,5 +1,6 @@ /* * Bittorrent Client using Qt and libtorrent. + * Copyright (C) 2024 Vladimir Golovnev * Copyright (C) 2006 Christophe Dumez * * This program is free software; you can redistribute it and/or @@ -37,16 +38,16 @@ namespace Ui class DeletionConfirmationDialog; } -class DeletionConfirmationDialog : public QDialog +class DeletionConfirmationDialog final : public QDialog { Q_OBJECT Q_DISABLE_COPY_MOVE(DeletionConfirmationDialog) public: DeletionConfirmationDialog(QWidget *parent, int size, const QString &name, bool defaultDeleteFiles); - ~DeletionConfirmationDialog(); + ~DeletionConfirmationDialog() override; - bool isDeleteFileSelected() const; + bool isRemoveContentSelected() const; private slots: void updateRememberButtonState(); diff --git a/src/gui/deletionconfirmationdialog.ui b/src/gui/deletionconfirmationdialog.ui index 9912e2b44..56834c918 100644 --- a/src/gui/deletionconfirmationdialog.ui +++ b/src/gui/deletionconfirmationdialog.ui @@ -75,7 +75,7 @@ - + 0 @@ -88,7 +88,7 @@ - Also permanently delete the files + Also remove the content files diff --git a/src/gui/filterpatternformat.h b/src/gui/filterpatternformat.h new file mode 100644 index 000000000..71b3c5c51 --- /dev/null +++ b/src/gui/filterpatternformat.h @@ -0,0 +1,48 @@ +/* + * Bittorrent Client using Qt and libtorrent. + * Copyright (C) 2024 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 + +// Using `Q_ENUM_NS()` without a wrapper namespace in our case is not advised +// since `Q_NAMESPACE` cannot be used when the same namespace resides at different files. +// https://www.kdab.com/new-qt-5-8-meta-object-support-namespaces/#comment-143779 +inline namespace FilterPatternFormatNS +{ + Q_NAMESPACE + + enum class FilterPatternFormat + { + PlainText, + Wildcards, + Regex + }; + + Q_ENUM_NS(FilterPatternFormat) +} diff --git a/src/gui/filterpatternformatmenu.cpp b/src/gui/filterpatternformatmenu.cpp new file mode 100644 index 000000000..d13ab685a --- /dev/null +++ b/src/gui/filterpatternformatmenu.cpp @@ -0,0 +1,82 @@ +/* + * Bittorrent Client using Qt and libtorrent. + * Copyright (C) 2024 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 "filterpatternformatmenu.h" + +#include + +FilterPatternFormatMenu::FilterPatternFormatMenu(const FilterPatternFormat format, QWidget *parent) + : QMenu(parent) +{ + setTitle(tr("Pattern Format")); + + auto *patternFormatGroup = new QActionGroup(this); + patternFormatGroup->setExclusive(true); + + QAction *plainTextAction = addAction(tr("Plain text")); + plainTextAction->setCheckable(true); + patternFormatGroup->addAction(plainTextAction); + + QAction *wildcardsAction = addAction(tr("Wildcards")); + wildcardsAction->setCheckable(true); + patternFormatGroup->addAction(wildcardsAction); + + QAction *regexAction = addAction(tr("Regular expression")); + regexAction->setCheckable(true); + patternFormatGroup->addAction(regexAction); + + switch (format) + { + case FilterPatternFormat::Wildcards: + default: + wildcardsAction->setChecked(true); + break; + case FilterPatternFormat::PlainText: + plainTextAction->setChecked(true); + break; + case FilterPatternFormat::Regex: + regexAction->setChecked(true); + break; + } + + connect(plainTextAction, &QAction::toggled, this, [this](const bool checked) + { + if (checked) + emit patternFormatChanged(FilterPatternFormat::PlainText); + }); + connect(wildcardsAction, &QAction::toggled, this, [this](const bool checked) + { + if (checked) + emit patternFormatChanged(FilterPatternFormat::Wildcards); + }); + connect(regexAction, &QAction::toggled, this, [this](const bool checked) + { + if (checked) + emit patternFormatChanged(FilterPatternFormat::Regex); + }); +} diff --git a/src/gui/filterpatternformatmenu.h b/src/gui/filterpatternformatmenu.h new file mode 100644 index 000000000..e201fd89d --- /dev/null +++ b/src/gui/filterpatternformatmenu.h @@ -0,0 +1,45 @@ +/* + * Bittorrent Client using Qt and libtorrent. + * Copyright (C) 2024 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 "filterpatternformat.h" + +class FilterPatternFormatMenu final : public QMenu +{ + Q_OBJECT + Q_DISABLE_COPY_MOVE(FilterPatternFormatMenu) + +public: + explicit FilterPatternFormatMenu(FilterPatternFormat format, QWidget *parent = nullptr); + +signals: + void patternFormatChanged(FilterPatternFormat format); +}; diff --git a/src/gui/properties/peerlistwidget.cpp b/src/gui/properties/peerlistwidget.cpp index 02f0cc609..87acbc666 100644 --- a/src/gui/properties/peerlistwidget.cpp +++ b/src/gui/properties/peerlistwidget.cpp @@ -411,7 +411,7 @@ void PeerListWidget::loadPeers(const BitTorrent::Torrent *torrent) return; // Remove I2P peers since they will be completely reloaded. - for (QStandardItem *item : asConst(m_I2PPeerItems)) + for (const QStandardItem *item : asConst(m_I2PPeerItems)) m_listModel->removeRow(item->row()); m_I2PPeerItems.clear(); @@ -466,10 +466,14 @@ void PeerListWidget::loadPeers(const BitTorrent::Torrent *torrent) { QStandardItem *item = m_peerItems.take(peerEndpoint); - QSet &items = m_itemsByIP[peerEndpoint.address.ip]; - items.remove(item); - if (items.isEmpty()) - m_itemsByIP.remove(peerEndpoint.address.ip); + const auto items = m_itemsByIP.find(peerEndpoint.address.ip); + Q_ASSERT(items != m_itemsByIP.end()); + if (items == m_itemsByIP.end()) [[unlikely]] + continue; + + items->remove(item); + if (items->isEmpty()) + m_itemsByIP.erase(items); m_listModel->removeRow(item->row()); } diff --git a/src/gui/properties/propertieswidget.cpp b/src/gui/properties/propertieswidget.cpp index 980978cb9..26d737732 100644 --- a/src/gui/properties/propertieswidget.cpp +++ b/src/gui/properties/propertieswidget.cpp @@ -1,6 +1,6 @@ /* * Bittorrent Client using Qt and libtorrent. - * Copyright (C) 2022 Vladimir Golovnev + * Copyright (C) 2022-2024 Vladimir Golovnev * Copyright (C) 2006 Christophe Dumez * * This program is free software; you can redistribute it and/or @@ -52,6 +52,7 @@ #include "base/utils/misc.h" #include "base/utils/string.h" #include "gui/autoexpandabledialog.h" +#include "gui/filterpatternformatmenu.h" #include "gui/lineedit.h" #include "gui/trackerlist/trackerlistwidget.h" #include "gui/uithememanager.h" @@ -66,6 +67,7 @@ PropertiesWidget::PropertiesWidget(QWidget *parent) : QWidget(parent) , m_ui {new Ui::PropertiesWidget} + , m_storeFilterPatternFormat {u"GUI/PropertiesWidget/FilterPatternFormat"_s} { m_ui->setupUi(this); #ifndef Q_OS_MACOS @@ -78,7 +80,9 @@ PropertiesWidget::PropertiesWidget(QWidget *parent) m_contentFilterLine = new LineEdit(this); m_contentFilterLine->setPlaceholderText(tr("Filter files...")); m_contentFilterLine->setFixedWidth(300); - connect(m_contentFilterLine, &LineEdit::textChanged, m_ui->filesList, &TorrentContentWidget::setFilterPattern); + m_contentFilterLine->setContextMenuPolicy(Qt::CustomContextMenu); + connect(m_contentFilterLine, &QWidget::customContextMenuRequested, this, &PropertiesWidget::showContentFilterContextMenu); + connect(m_contentFilterLine, &LineEdit::textChanged, this, &PropertiesWidget::setContentFilterPattern); m_ui->contentFilterLayout->insertWidget(3, m_contentFilterLine); m_ui->filesList->setDoubleClickAction(TorrentContentWidget::DoubleClickAction::Open); @@ -206,6 +210,7 @@ void PropertiesWidget::clear() m_ui->labelSavePathVal->clear(); m_ui->labelCreatedOnVal->clear(); m_ui->labelTotalPiecesVal->clear(); + m_ui->labelPrivateVal->clear(); m_ui->labelInfohash1Val->clear(); m_ui->labelInfohash2Val->clear(); m_ui->labelCommentVal->clear(); @@ -274,6 +279,28 @@ void PropertiesWidget::updateSavePath(BitTorrent::Torrent *const torrent) m_ui->labelSavePathVal->setText(m_torrent->savePath().toString()); } +void PropertiesWidget::showContentFilterContextMenu() +{ + QMenu *menu = m_contentFilterLine->createStandardContextMenu(); + + auto *formatMenu = new FilterPatternFormatMenu(m_storeFilterPatternFormat.get(FilterPatternFormat::Wildcards), menu); + connect(formatMenu, &FilterPatternFormatMenu::patternFormatChanged, this, [this](const FilterPatternFormat format) + { + m_storeFilterPatternFormat = format; + setContentFilterPattern(); + }); + + menu->addSeparator(); + menu->addMenu(formatMenu); + menu->setAttribute(Qt::WA_DeleteOnClose); + menu->popup(QCursor::pos()); +} + +void PropertiesWidget::setContentFilterPattern() +{ + m_ui->filesList->setFilterPattern(m_contentFilterLine->text(), m_storeFilterPatternFormat.get(FilterPatternFormat::Wildcards)); +} + void PropertiesWidget::updateTorrentInfos(BitTorrent::Torrent *const torrent) { if (torrent == m_torrent) @@ -309,7 +336,14 @@ void PropertiesWidget::loadTorrentInfos(BitTorrent::Torrent *const torrent) m_ui->labelCommentVal->setText(Utils::Misc::parseHtmlLinks(m_torrent->comment().toHtmlEscaped())); m_ui->labelCreatedByVal->setText(m_torrent->creator()); + + m_ui->labelPrivateVal->setText(m_torrent->isPrivate() ? tr("Yes") : tr("No")); } + else + { + m_ui->labelPrivateVal->setText(tr("N/A")); + } + // Load dynamic data loadDynamicData(); } diff --git a/src/gui/properties/propertieswidget.h b/src/gui/properties/propertieswidget.h index 31a1963b0..21b1b5fc5 100644 --- a/src/gui/properties/propertieswidget.h +++ b/src/gui/properties/propertieswidget.h @@ -1,6 +1,6 @@ /* * Bittorrent Client using Qt and libtorrent. - * Copyright (C) 2022 Vladimir Golovnev + * Copyright (C) 2022-2024 Vladimir Golovnev * Copyright (C) 2006 Christophe Dumez * * This program is free software; you can redistribute it and/or @@ -32,7 +32,8 @@ #include #include -#include "base/pathfwd.h" +#include "base/settingvalue.h" +#include "gui/filterpatternformat.h" class QPushButton; class QTreeView; @@ -102,6 +103,8 @@ private slots: private: QPushButton *getButtonFromIndex(int index); + void showContentFilterContextMenu(); + void setContentFilterPattern(); Ui::PropertiesWidget *m_ui = nullptr; BitTorrent::Torrent *m_torrent = nullptr; @@ -115,4 +118,6 @@ private: PropTabBar *m_tabBar = nullptr; LineEdit *m_contentFilterLine = nullptr; int m_handleWidth = -1; + + SettingValue m_storeFilterPatternFormat; }; diff --git a/src/gui/properties/propertieswidget.ui b/src/gui/properties/propertieswidget.ui index 17ffa6b4f..71b3864f9 100644 --- a/src/gui/properties/propertieswidget.ui +++ b/src/gui/properties/propertieswidget.ui @@ -823,6 +823,38 @@ + + + + 0 + 0 + + + + Private: + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + + + + + 0 + 0 + + + + Qt::PlainText + + + Qt::TextSelectableByMouse + + + + @@ -838,71 +870,7 @@ - - - - - 0 - 0 - - - - Info Hash v2: - - - Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter - - - - - - - 0 - 0 - - - - Qt::PlainText - - - Qt::TextSelectableByMouse - - - - - - - - 0 - 0 - - - - Save Path: - - - Qt::AlignRight|Qt::AlignTop|Qt::AlignTrailing - - - - - - - - 0 - 0 - - - - Comment: - - - Qt::AlignRight|Qt::AlignTop|Qt::AlignTrailing - - - - @@ -918,7 +886,55 @@ + + + + + 0 + 0 + + + + Info Hash v2: + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + + + + 0 + 0 + + + + Qt::PlainText + + + Qt::TextSelectableByMouse + + + + + + + + 0 + 0 + + + + Save Path: + + + Qt::AlignRight|Qt::AlignTop|Qt::AlignTrailing + + + + @@ -937,7 +953,23 @@ - + + + + + 0 + 0 + + + + Comment: + + + Qt::AlignRight|Qt::AlignTop|Qt::AlignTrailing + + + + diff --git a/src/gui/torrentcontentmodel.cpp b/src/gui/torrentcontentmodel.cpp index 61c43cebc..694e75958 100644 --- a/src/gui/torrentcontentmodel.cpp +++ b/src/gui/torrentcontentmodel.cpp @@ -1,6 +1,6 @@ /* * Bittorrent Client using Qt and libtorrent. - * Copyright (C) 2022-2023 Vladimir Golovnev + * Copyright (C) 2022-2024 Vladimir Golovnev * Copyright (C) 2006-2012 Christophe Dumez * * This program is free software; you can redistribute it and/or @@ -37,17 +37,12 @@ #include #include -#if defined(Q_OS_WIN) -#include -#include -#else -#include -#include -#endif - -#if defined Q_OS_WIN || defined Q_OS_MACOS +#if defined(Q_OS_MACOS) #define QBT_PIXMAP_CACHE_FOR_FILE_ICONS #include +#elif !defined(Q_OS_WIN) +#include +#include #endif #include "base/bittorrent/downloadpriority.h" @@ -116,27 +111,8 @@ namespace }; #endif // QBT_PIXMAP_CACHE_FOR_FILE_ICONS -#if defined(Q_OS_WIN) - // See QTBUG-25319 for explanation why this is required - class WinShellFileIconProvider final : public CachingFileIconProvider - { - QPixmap pixmapForExtension(const QString &ext) const override - { - const std::wstring extWStr = QString(u'.' + ext).toStdWString(); - - SHFILEINFOW sfi {}; - const HRESULT hr = ::SHGetFileInfoW(extWStr.c_str(), - FILE_ATTRIBUTE_NORMAL, &sfi, sizeof(sfi), (SHGFI_ICON | SHGFI_USEFILEATTRIBUTES)); - if (FAILED(hr)) - return {}; - - const auto iconPixmap = QPixmap::fromImage(QImage::fromHICON(sfi.hIcon)); - ::DestroyIcon(sfi.hIcon); - return iconPixmap; - } - }; -#elif defined(Q_OS_MACOS) - // There is a similar bug on macOS, to be reported to Qt +#if defined(Q_OS_MACOS) + // There is a bug on macOS, to be reported to Qt // https://github.com/qbittorrent/qBittorrent/pull/6156#issuecomment-316302615 class MacFileIconProvider final : public CachingFileIconProvider { @@ -145,7 +121,7 @@ namespace return MacUtils::pixmapForExtension(ext, QSize(32, 32)); } }; -#else +#elif !defined(Q_OS_WIN) /** * @brief Tests whether QFileIconProvider actually works * @@ -189,7 +165,7 @@ TorrentContentModel::TorrentContentModel(QObject *parent) : QAbstractItemModel(parent) , m_rootItem(new TorrentContentModelFolder(QVector({ tr("Name"), tr("Total Size"), tr("Progress"), tr("Download Priority"), tr("Remaining"), tr("Availability") }))) #if defined(Q_OS_WIN) - , m_fileIconProvider {new WinShellFileIconProvider} + , m_fileIconProvider {new QFileIconProvider} #elif defined(Q_OS_MACOS) , m_fileIconProvider {new MacFileIconProvider} #else diff --git a/src/gui/torrentcontentmodelfolder.cpp b/src/gui/torrentcontentmodelfolder.cpp index abece9c9b..b8a536216 100644 --- a/src/gui/torrentcontentmodelfolder.cpp +++ b/src/gui/torrentcontentmodelfolder.cpp @@ -147,10 +147,19 @@ void TorrentContentModelFolder::recalculateProgress() tRemaining += child->remaining(); } - if (!isRootItem() && (tSize > 0)) + if (!isRootItem()) { - m_progress = tProgress / tSize; - m_remaining = tRemaining; + if (tSize > 0) + { + m_progress = tProgress / tSize; + m_remaining = tRemaining; + } + else + { + m_progress = 1.0; + m_remaining = 0; + } + Q_ASSERT(m_progress <= 1.); } } diff --git a/src/gui/torrentcontentwidget.cpp b/src/gui/torrentcontentwidget.cpp index 1b3817408..59c401871 100644 --- a/src/gui/torrentcontentwidget.cpp +++ b/src/gui/torrentcontentwidget.cpp @@ -1,6 +1,6 @@ /* * Bittorrent Client using Qt and libtorrent. - * Copyright (C) 2022 Vladimir Golovnev + * Copyright (C) 2022-2024 Vladimir Golovnev * Copyright (C) 2014 Ivan Sorokin * * This program is free software; you can redistribute it and/or @@ -56,6 +56,19 @@ #include "gui/macutilities.h" #endif +namespace +{ + QList toPersistentIndexes(const QModelIndexList &indexes) + { + QList persistentIndexes; + persistentIndexes.reserve(indexes.size()); + for (const QModelIndex &index : indexes) + persistentIndexes.emplaceBack(index); + + return persistentIndexes; + } +} + TorrentContentWidget::TorrentContentWidget(QWidget *parent) : QTreeView(parent) { @@ -173,10 +186,20 @@ Path TorrentContentWidget::getItemPath(const QModelIndex &index) const return path; } -void TorrentContentWidget::setFilterPattern(const QString &patternText) +void TorrentContentWidget::setFilterPattern(const QString &patternText, const FilterPatternFormat format) { - const QString pattern = Utils::String::wildcardToRegexPattern(patternText); - m_filterModel->setFilterRegularExpression(QRegularExpression(pattern, QRegularExpression::CaseInsensitiveOption)); + if (format == FilterPatternFormat::PlainText) + { + m_filterModel->setFilterFixedString(patternText); + m_filterModel->setFilterCaseSensitivity(Qt::CaseInsensitive); + } + else + { + const QString pattern = ((format == FilterPatternFormat::Regex) + ? patternText : Utils::String::wildcardToRegexPattern(patternText)); + m_filterModel->setFilterRegularExpression(QRegularExpression(pattern, QRegularExpression::CaseInsensitiveOption)); + } + if (patternText.isEmpty()) { collapseAll(); @@ -219,9 +242,9 @@ void TorrentContentWidget::keyPressEvent(QKeyEvent *event) const Qt::CheckState state = (static_cast(value.toInt()) == Qt::Checked) ? Qt::Unchecked : Qt::Checked; - const QModelIndexList selection = selectionModel()->selectedRows(TorrentContentModelItem::COL_NAME); + const QList selection = toPersistentIndexes(selectionModel()->selectedRows(TorrentContentModelItem::COL_NAME)); - for (const QModelIndex &index : selection) + for (const QPersistentModelIndex &index : selection) model()->setData(index, state, Qt::CheckStateRole); } @@ -248,10 +271,10 @@ void TorrentContentWidget::renameSelectedFile() void TorrentContentWidget::applyPriorities(const BitTorrent::DownloadPriority priority) { - const QModelIndexList selectedRows = selectionModel()->selectedRows(0); - for (const QModelIndex &index : selectedRows) + const QList selectedRows = toPersistentIndexes(selectionModel()->selectedRows(Priority)); + for (const QPersistentModelIndex &index : selectedRows) { - model()->setData(index.sibling(index.row(), Priority), static_cast(priority)); + model()->setData(index, static_cast(priority)); } } @@ -261,7 +284,7 @@ void TorrentContentWidget::applyPrioritiesByOrder() // a download priority that will apply to each item. The number of groups depends on how // many "download priority" are available to be assigned - const QModelIndexList selectedRows = selectionModel()->selectedRows(0); + const QList selectedRows = toPersistentIndexes(selectionModel()->selectedRows(Priority)); const qsizetype priorityGroups = 3; const auto priorityGroupSize = std::max((selectedRows.length() / priorityGroups), 1); @@ -283,8 +306,8 @@ void TorrentContentWidget::applyPrioritiesByOrder() break; } - const QModelIndex &index = selectedRows[i]; - model()->setData(index.sibling(index.row(), Priority), static_cast(priority)); + const QPersistentModelIndex &index = selectedRows[i]; + model()->setData(index, static_cast(priority)); } } diff --git a/src/gui/torrentcontentwidget.h b/src/gui/torrentcontentwidget.h index 4baccb883..1e9f0835c 100644 --- a/src/gui/torrentcontentwidget.h +++ b/src/gui/torrentcontentwidget.h @@ -1,6 +1,6 @@ /* * Bittorrent Client using Qt and libtorrent. - * Copyright (C) 2022 Vladimir Golovnev + * Copyright (C) 2022-2024 Vladimir Golovnev * Copyright (C) 2014 Ivan Sorokin * * This program is free software; you can redistribute it and/or @@ -33,6 +33,7 @@ #include "base/bittorrent/downloadpriority.h" #include "base/pathfwd.h" +#include "filterpatternformat.h" class QShortcut; @@ -92,7 +93,7 @@ public: int getFileIndex(const QModelIndex &index) const; Path getItemPath(const QModelIndex &index) const; - void setFilterPattern(const QString &patternText); + void setFilterPattern(const QString &patternText, FilterPatternFormat format = FilterPatternFormat::Wildcards); void checkAll(); void checkNone(); diff --git a/src/gui/torrenttagsdialog.cpp b/src/gui/torrenttagsdialog.cpp index 9e48443b0..9901a4ba8 100644 --- a/src/gui/torrenttagsdialog.cpp +++ b/src/gui/torrenttagsdialog.cpp @@ -1,6 +1,6 @@ /* * Bittorrent Client using Qt and libtorrent. - * Copyright (C) 2023 Vladimir Golovnev + * Copyright (C) 2023-2024 Vladimir Golovnev * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License @@ -37,6 +37,7 @@ #include "base/global.h" #include "autoexpandabledialog.h" #include "flowlayout.h" +#include "utils.h" #include "ui_torrenttagsdialog.h" @@ -52,10 +53,10 @@ TorrentTagsDialog::TorrentTagsDialog(const TagSet &initialTags, QWidget *parent) connect(m_ui->buttonBox, &QDialogButtonBox::accepted, this, &QDialog::accept); connect(m_ui->buttonBox, &QDialogButtonBox::rejected, this, &QDialog::reject); - auto *tagsLayout = new FlowLayout(m_ui->scrollArea); + auto *tagsLayout = new FlowLayout(m_ui->scrollArea->widget()); for (const Tag &tag : asConst(initialTags.united(BitTorrent::Session::instance()->tags()))) { - auto *tagWidget = new QCheckBox(tag.toString()); + auto *tagWidget = new QCheckBox(Utils::Gui::tagToWidgetText(tag)); if (initialTags.contains(tag)) tagWidget->setChecked(true); tagsLayout->addWidget(tagWidget); @@ -78,12 +79,12 @@ TorrentTagsDialog::~TorrentTagsDialog() TagSet TorrentTagsDialog::tags() const { TagSet tags; - auto *layout = m_ui->scrollArea->layout(); + auto *layout = m_ui->scrollArea->widget()->layout(); for (int i = 0; i < (layout->count() - 1); ++i) { const auto *tagWidget = static_cast(layout->itemAt(i)->widget()); if (tagWidget->isChecked()) - tags.insert(Tag(tagWidget->text())); + tags.insert(Utils::Gui::widgetTextToTag(tagWidget->text())); } return tags; @@ -111,9 +112,9 @@ void TorrentTagsDialog::addNewTag() } else { - auto *layout = m_ui->scrollArea->layout(); + auto *layout = m_ui->scrollArea->widget()->layout(); auto *btn = layout->takeAt(layout->count() - 1); - auto *tagWidget = new QCheckBox(tag.toString()); + auto *tagWidget = new QCheckBox(Utils::Gui::tagToWidgetText(tag)); tagWidget->setChecked(true); layout->addWidget(tagWidget); layout->addItem(btn); diff --git a/src/gui/trackerlist/trackerlistmodel.cpp b/src/gui/trackerlist/trackerlistmodel.cpp index 92fb8acb3..0d0b8fdf7 100644 --- a/src/gui/trackerlist/trackerlistmodel.cpp +++ b/src/gui/trackerlist/trackerlistmodel.cpp @@ -488,11 +488,11 @@ QVariant TrackerListModel::headerData(const int section, const Qt::Orientation o switch (section) { case COL_URL: - return tr("URL/Announce endpoint"); + return tr("URL/Announce Endpoint"); case COL_TIER: return tr("Tier"); case COL_PROTOCOL: - return tr("Protocol"); + return tr("BT Protocol"); case COL_STATUS: return tr("Status"); case COL_PEERS: @@ -506,9 +506,9 @@ QVariant TrackerListModel::headerData(const int section, const Qt::Orientation o case COL_MSG: return tr("Message"); case COL_NEXT_ANNOUNCE: - return tr("Next announce"); + return tr("Next Announce"); case COL_MIN_ANNOUNCE: - return tr("Min announce"); + return tr("Min Announce"); default: return {}; } @@ -585,7 +585,7 @@ QVariant TrackerListModel::data(const QModelIndex &index, const int role) const case COL_TIER: return (isEndpoint || (index.row() < STICKY_ROW_COUNT)) ? QString() : QString::number(itemPtr->tier); case COL_PROTOCOL: - return isEndpoint ? tr("v%1").arg(itemPtr->btVersion) : QString(); + return isEndpoint ? (u'v' + QString::number(itemPtr->btVersion)) : QString(); case COL_STATUS: if (isEndpoint) return toString(itemPtr->status); diff --git a/src/gui/transferlistfilterswidget.cpp b/src/gui/transferlistfilterswidget.cpp index feb93d1a6..348290ace 100644 --- a/src/gui/transferlistfilterswidget.cpp +++ b/src/gui/transferlistfilterswidget.cpp @@ -39,7 +39,6 @@ #include #include -#include "base/algorithm.h" #include "base/bittorrent/session.h" #include "base/bittorrent/torrent.h" #include "base/bittorrent/trackerentrystatus.h" diff --git a/src/gui/transferlistmodel.cpp b/src/gui/transferlistmodel.cpp index cb35a51c0..63f1b3ebb 100644 --- a/src/gui/transferlistmodel.cpp +++ b/src/gui/transferlistmodel.cpp @@ -193,6 +193,7 @@ QVariant TransferListModel::headerData(const int section, const Qt::Orientation case TR_INFOHASH_V1: return tr("Info Hash v1", "i.e: torrent info hash v1"); case TR_INFOHASH_V2: return tr("Info Hash v2", "i.e: torrent info hash v2"); case TR_REANNOUNCE: return tr("Reannounce In", "Indicates the time until next trackers reannounce"); + case TR_PRIVATE: return tr("Private", "Flags private torrents"); default: return {}; } } @@ -357,6 +358,15 @@ QString TransferListModel::displayValue(const BitTorrent::Torrent *torrent, cons return Utils::Misc::userFriendlyDuration(time); }; + const auto privateString = [hideValues](const bool isPrivate, const bool hasMetadata) -> QString + { + if (hideValues && !isPrivate) + return {}; + if (hasMetadata) + return isPrivate ? tr("Yes") : tr("No"); + return tr("N/A"); + }; + switch (column) { case TR_NAME: @@ -431,6 +441,8 @@ QString TransferListModel::displayValue(const BitTorrent::Torrent *torrent, cons return hashString(torrent->infoHash().v2()); case TR_REANNOUNCE: return reannounceString(torrent->nextAnnounce()); + case TR_PRIVATE: + return privateString(torrent->isPrivate(), torrent->hasMetadata()); } return {}; @@ -512,6 +524,8 @@ QVariant TransferListModel::internalValue(const BitTorrent::Torrent *torrent, co return QVariant::fromValue(torrent->infoHash().v2()); case TR_REANNOUNCE: return torrent->nextAnnounce(); + case TR_PRIVATE: + return (torrent->hasMetadata() ? torrent->isPrivate() : QVariant()); } return {}; diff --git a/src/gui/transferlistmodel.h b/src/gui/transferlistmodel.h index 061db4683..5942f77da 100644 --- a/src/gui/transferlistmodel.h +++ b/src/gui/transferlistmodel.h @@ -86,6 +86,7 @@ public: TR_INFOHASH_V1, TR_INFOHASH_V2, TR_REANNOUNCE, + TR_PRIVATE, NB_COLUMNS }; diff --git a/src/gui/transferlistsortmodel.cpp b/src/gui/transferlistsortmodel.cpp index 2f9ce6ff0..782aead74 100644 --- a/src/gui/transferlistsortmodel.cpp +++ b/src/gui/transferlistsortmodel.cpp @@ -59,8 +59,8 @@ namespace int customCompare(const TagSet &left, const TagSet &right, const Utils::Compare::NaturalCompare &compare) { for (auto leftIter = left.cbegin(), rightIter = right.cbegin(); - (leftIter != left.cend()) && (rightIter != right.cend()); - ++leftIter, ++rightIter) + (leftIter != left.cend()) && (rightIter != right.cend()); + ++leftIter, ++rightIter) { const int result = compare(leftIter->toString(), rightIter->toString()); if (result != 0) @@ -84,6 +84,17 @@ namespace return isLeftValid ? -1 : 1; } + int compareAsBool(const QVariant &left, const QVariant &right) + { + const bool leftValid = left.isValid(); + const bool rightValid = right.isValid(); + if (leftValid && rightValid) + return threeWayCompare(left.toBool(), right.toBool()); + if (!leftValid && !rightValid) + return 0; + return leftValid ? -1 : 1; + } + int adjustSubSortColumn(const int column) { return ((column >= 0) && (column < TransferListModel::NB_COLUMNS)) @@ -214,6 +225,9 @@ int TransferListSortModel::compare(const QModelIndex &left, const QModelIndex &r case TransferListModel::TR_UPSPEED: return customCompare(leftValue.toInt(), rightValue.toInt()); + case TransferListModel::TR_PRIVATE: + return compareAsBool(leftValue, rightValue); + case TransferListModel::TR_PEERS: case TransferListModel::TR_SEEDS: { diff --git a/src/gui/transferlistwidget.cpp b/src/gui/transferlistwidget.cpp index a1b79b29e..7b857e1a3 100644 --- a/src/gui/transferlistwidget.cpp +++ b/src/gui/transferlistwidget.cpp @@ -1,6 +1,6 @@ /* * Bittorrent Client using Qt and libtorrent. - * Copyright (C) 2023 Vladimir Golovnev + * Copyright (C) 2023-2024 Vladimir Golovnev * Copyright (C) 2006 Christophe Dumez * * This program is free software; you can redistribute it and/or @@ -116,9 +116,10 @@ namespace void removeTorrents(const QVector &torrents, const bool isDeleteFileSelected) { auto *session = BitTorrent::Session::instance(); - const DeleteOption deleteOption = isDeleteFileSelected ? DeleteTorrentAndFiles : DeleteTorrent; + const BitTorrent::TorrentRemoveOption removeOption = isDeleteFileSelected + ? BitTorrent::TorrentRemoveOption::RemoveContent : BitTorrent::TorrentRemoveOption::KeepContent; for (const BitTorrent::Torrent *torrent : torrents) - session->deleteTorrent(torrent->id(), deleteOption); + session->removeTorrent(torrent->id(), removeOption); } } @@ -183,6 +184,7 @@ TransferListWidget::TransferListWidget(QWidget *parent, MainWindow *mainWindow) setColumnHidden(TransferListModel::TR_LAST_ACTIVITY, true); setColumnHidden(TransferListModel::TR_TOTAL_SIZE, true); setColumnHidden(TransferListModel::TR_REANNOUNCE, true); + setColumnHidden(TransferListModel::TR_PRIVATE, true); } //Ensure that at least one column is visible at all times @@ -442,7 +444,7 @@ void TransferListWidget::deleteSelectedTorrents(const bool deleteLocalFiles) { // Some torrents might be removed when waiting for user input, so refetch the torrent list // NOTE: this will only work when dialog is modal - removeTorrents(getSelectedTorrents(), dialog->isDeleteFileSelected()); + removeTorrents(getSelectedTorrents(), dialog->isRemoveContentSelected()); }); dialog->open(); } @@ -465,7 +467,7 @@ void TransferListWidget::deleteVisibleTorrents() { // Some torrents might be removed when waiting for user input, so refetch the torrent list // NOTE: this will only work when dialog is modal - removeTorrents(getVisibleTorrents(), dialog->isDeleteFileSelected()); + removeTorrents(getVisibleTorrents(), dialog->isRemoveContentSelected()); }); dialog->open(); } @@ -1190,7 +1192,7 @@ void TransferListWidget::displayListMenu() const TagSet tags = BitTorrent::Session::instance()->tags(); for (const Tag &tag : asConst(tags)) { - auto *action = new TriStateAction(tag.toString(), tagsMenu); + auto *action = new TriStateAction(Utils::Gui::tagToWidgetText(tag), tagsMenu); action->setCloseOnInteraction(false); const Qt::CheckState initialState = tagsInAll.contains(tag) ? Qt::Checked diff --git a/src/gui/utils.cpp b/src/gui/utils.cpp index c7c1f24c4..f310c7f26 100644 --- a/src/gui/utils.cpp +++ b/src/gui/utils.cpp @@ -1,5 +1,6 @@ /* * Bittorrent Client using Qt and libtorrent. + * Copyright (C) 2024 Vladimir Golovnev * Copyright (C) 2017 Mike Tzou * * This program is free software; you can redistribute it and/or @@ -54,6 +55,7 @@ #include "base/global.h" #include "base/path.h" +#include "base/tag.h" #include "base/utils/fs.h" #include "base/utils/version.h" @@ -216,3 +218,29 @@ void Utils::Gui::openFolderSelect(const Path &path) openPath(path.parentPath()); #endif } + +QString Utils::Gui::tagToWidgetText(const Tag &tag) +{ + return tag.toString().replace(u'&', u"&&"_s); +} + +Tag Utils::Gui::widgetTextToTag(const QString &text) +{ + // replace pairs of '&' with single '&' and remove non-paired occurrences of '&' + QString cleanedText; + cleanedText.reserve(text.size()); + bool amp = false; + for (const QChar c : text) + { + if (c == u'&') + { + amp = !amp; + if (amp) + continue; + } + + cleanedText.append(c); + } + + return Tag(cleanedText); +} diff --git a/src/gui/utils.h b/src/gui/utils.h index c633cb45d..46a49c2f6 100644 --- a/src/gui/utils.h +++ b/src/gui/utils.h @@ -1,5 +1,6 @@ /* * Bittorrent Client using Qt and libtorrent. + * Copyright (C) 2024 Vladimir Golovnev * Copyright (C) 2017 Mike Tzou * * This program is free software; you can redistribute it and/or @@ -34,8 +35,11 @@ class QIcon; class QPixmap; class QPoint; class QSize; +class QString; class QWidget; +class Tag; + namespace Utils::Gui { bool isDarkTheme(); @@ -51,4 +55,7 @@ namespace Utils::Gui void openPath(const Path &path); void openFolderSelect(const Path &path); + + QString tagToWidgetText(const Tag &tag); + Tag widgetTextToTag(const QString &text); } diff --git a/src/qbittorrent.exe.manifest b/src/qbittorrent.exe.manifest index 939a56d1e..1725117fd 100644 --- a/src/qbittorrent.exe.manifest +++ b/src/qbittorrent.exe.manifest @@ -1,5 +1,12 @@ + + @@ -28,6 +35,7 @@ + diff --git a/src/searchengine/nova3/helpers.py b/src/searchengine/nova3/helpers.py index d453f1c4d..f0206e383 100644 --- a/src/searchengine/nova3/helpers.py +++ b/src/searchengine/nova3/helpers.py @@ -1,4 +1,4 @@ -#VERSION: 1.45 +#VERSION: 1.47 # Author: # Christophe DUMEZ (chris@qbittorrent.org) @@ -39,9 +39,11 @@ import tempfile import urllib.error import urllib.parse import urllib.request +from collections.abc import Mapping +from typing import Any, Dict, Optional -def getBrowserUserAgent(): +def getBrowserUserAgent() -> str: """ Disguise as browser to circumvent website blocking """ # Firefox release calendar @@ -57,7 +59,7 @@ def getBrowserUserAgent(): return f"Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:{nowVersion}.0) Gecko/20100101 Firefox/{nowVersion}.0" -headers = {'User-Agent': getBrowserUserAgent()} +headers: Dict[str, Any] = {'User-Agent': getBrowserUserAgent()} # SOCKS5 Proxy support if "sock_proxy" in os.environ and len(os.environ["sock_proxy"].strip()) > 0: @@ -67,13 +69,13 @@ if "sock_proxy" in os.environ and len(os.environ["sock_proxy"].strip()) > 0: if m is not None: socks.setdefaultproxy(socks.PROXY_TYPE_SOCKS5, m.group('host'), int(m.group('port')), True, m.group('username'), m.group('password')) - socket.socket = socks.socksocket + socket.socket = socks.socksocket # type: ignore[misc] -def htmlentitydecode(s): +def htmlentitydecode(s: str) -> str: # First convert alpha entities (such as é) # (Inspired from http://mail.python.org/pipermail/python-list/2007-June/443813.html) - def entity2char(m): + def entity2char(m: re.Match[str]) -> str: entity = m.group(1) if entity in html.entities.name2codepoint: return chr(html.entities.name2codepoint[entity]) @@ -87,15 +89,15 @@ def htmlentitydecode(s): return re.sub(r'&#x(\w+);', lambda x: chr(int(x.group(1), 16)), t) -def retrieve_url(url): +def retrieve_url(url: str, custom_headers: Mapping[str, Any] = {}) -> str: """ Return the content of the url page as a string """ - req = urllib.request.Request(url, headers=headers) + req = urllib.request.Request(url, headers={**headers, **custom_headers}) try: response = urllib.request.urlopen(req) except urllib.error.URLError as errno: print(" ".join(("Connection error:", str(errno.reason)))) return "" - dat = response.read() + dat: bytes = response.read() # Check if it is gzipped if dat[:2] == b'\x1f\x8b': # Data is gzip encoded, decode it @@ -109,16 +111,15 @@ def retrieve_url(url): ignore, charset = info['Content-Type'].split('charset=') except Exception: pass - dat = dat.decode(charset, 'replace') - dat = htmlentitydecode(dat) - # return dat.encode('utf-8', 'replace') - return dat + datStr = dat.decode(charset, 'replace') + datStr = htmlentitydecode(datStr) + return datStr -def download_file(url, referer=None): +def download_file(url: str, referer: Optional[str] = None) -> str: """ Download file at url and write it to a file, return the path to the file and the url """ - file, path = tempfile.mkstemp() - file = os.fdopen(file, "wb") + fileHandle, path = tempfile.mkstemp() + file = os.fdopen(fileHandle, "wb") # Download url req = urllib.request.Request(url, headers=headers) if referer is not None: diff --git a/src/searchengine/nova3/nova2.py b/src/searchengine/nova3/nova2.py index 2c5963beb..9db438b96 100644 --- a/src/searchengine/nova3/nova2.py +++ b/src/searchengine/nova3/nova2.py @@ -1,4 +1,4 @@ -#VERSION: 1.45 +#VERSION: 1.46 # Author: # Fabien Devaux @@ -37,17 +37,21 @@ import importlib import pathlib import sys import urllib.parse +from collections.abc import Iterable, Iterator, Sequence +from enum import Enum from glob import glob from multiprocessing import Pool, cpu_count from os import path +from typing import Dict, List, Optional, Set, Tuple, Type -THREADED = True +THREADED: bool = True try: - MAX_THREADS = cpu_count() + MAX_THREADS: int = cpu_count() except NotImplementedError: MAX_THREADS = 1 -CATEGORIES = {'all', 'movies', 'tv', 'music', 'games', 'anime', 'software', 'pictures', 'books'} +Category = Enum('Category', ['all', 'movies', 'tv', 'music', 'games', 'anime', 'software', 'pictures', 'books']) + ################################################################################ # Every engine should have a "search" method taking @@ -58,11 +62,29 @@ CATEGORIES = {'all', 'movies', 'tv', 'music', 'games', 'anime', 'software', 'pic ################################################################################ +EngineName = str + + +class Engine: + url: str + name: EngineName + supported_categories: Dict[str, str] + + def __init__(self) -> None: + pass + + def search(self, what: str, cat: str = Category.all.name) -> None: + pass + + def download_torrent(self, info: str) -> None: + pass + + # global state -engine_dict = dict() +engine_dict: Dict[EngineName, Optional[Type[Engine]]] = {} -def list_engines(): +def list_engines() -> List[EngineName]: """ List all engines, including broken engines that fail on import @@ -81,10 +103,10 @@ def list_engines(): return found_engines -def get_engine(engine_name): - #global engine_dict +def get_engine(engine_name: EngineName) -> Optional[Type[Engine]]: if engine_name in engine_dict: return engine_dict[engine_name] + # when import fails, engine is None engine = None try: @@ -97,35 +119,37 @@ def get_engine(engine_name): return engine -def initialize_engines(found_engines): +def initialize_engines(found_engines: Iterable[EngineName]) -> Set[EngineName]: """ Import available engines - Return list of available engines + Return set of available engines """ - supported_engines = [] + supported_engines = set() for engine_name in found_engines: # import engine engine = get_engine(engine_name) if engine is None: continue - supported_engines.append(engine_name) + supported_engines.add(engine_name) return supported_engines -def engines_to_xml(supported_engines): +def engines_to_xml(supported_engines: Iterable[EngineName]) -> Iterator[str]: """ Generates xml for supported engines """ tab = " " * 4 for engine_name in supported_engines: search_engine = get_engine(engine_name) + if search_engine is None: + continue supported_categories = "" if hasattr(search_engine, "supported_categories"): supported_categories = " ".join((key for key in search_engine.supported_categories.keys() - if key != "all")) + if key != Category.all.name)) yield "".join((tab, "<", engine_name, ">\n", tab, tab, "", search_engine.name, "\n", @@ -134,7 +158,7 @@ def engines_to_xml(supported_engines): tab, "\n")) -def displayCapabilities(supported_engines): +def displayCapabilities(supported_engines: Iterable[EngineName]) -> None: """ Display capabilities in XML format @@ -151,21 +175,24 @@ def displayCapabilities(supported_engines): print(xml) -def run_search(engine_list): +def run_search(engine_list: Tuple[Optional[Type[Engine]], str, Category]) -> bool: """ Run search in engine - @param engine_list List with engine, query and category + @param engine_list Tuple with engine, query and category @retval False if any exceptions occurred @retval True otherwise """ - engine, what, cat = engine_list + engine_class, what, cat = engine_list + if engine_class is None: + return False + try: - engine = engine() + engine = engine_class() # avoid exceptions due to invalid category if hasattr(engine, 'supported_categories'): - if cat in engine.supported_categories: - engine.search(what, cat) + if cat.name in engine.supported_categories: + engine.search(what, cat.name) else: engine.search(what) @@ -174,7 +201,7 @@ def run_search(engine_list): return False -def main(args): +def main(args: Sequence[str]) -> None: # qbt tend to run this script in 'isolate mode' so append the current path manually current_path = str(pathlib.Path(__file__).parent.resolve()) if current_path not in sys.path: @@ -182,7 +209,7 @@ def main(args): found_engines = list_engines() - def show_usage(): + def show_usage() -> None: print("./nova2.py all|engine1[,engine2]* ", file=sys.stderr) print("found engines: " + ','.join(found_engines), file=sys.stderr) print("to list available engines: ./nova2.py --capabilities [--names]", file=sys.stderr) @@ -190,7 +217,6 @@ def main(args): if not args: show_usage() sys.exit(1) - elif args[0] == "--capabilities": supported_engines = initialize_engines(found_engines) if "--names" in args: @@ -198,14 +224,14 @@ def main(args): return displayCapabilities(supported_engines) return - elif len(args) < 3: show_usage() sys.exit(1) cat = args[1].lower() - - if cat not in CATEGORIES: + try: + category = Category[cat] + except KeyError: print(" - ".join(('Invalid category', cat)), file=sys.stderr) sys.exit(1) @@ -223,16 +249,18 @@ def main(args): engines_list = initialize_engines(found_engines) else: # discard not-found engines - engines_list = [engine for engine in engines_list if engine in found_engines] + engines_list = {engine for engine in engines_list if engine in found_engines} what = urllib.parse.quote(' '.join(args[2:])) + params = ((get_engine(engine_name), what, category) for engine_name in engines_list) + if THREADED: # child process spawning is controlled min(number of searches, number of cpu) with Pool(min(len(engines_list), MAX_THREADS)) as pool: - pool.map(run_search, ([get_engine(engine_name), what, cat] for engine_name in engines_list)) + pool.map(run_search, params) else: # py3 note: map is needed to be evaluated for content to be executed - all(map(run_search, ([get_engine(engine_name), what, cat] for engine_name in engines_list))) + all(map(run_search, params)) if __name__ == "__main__": diff --git a/src/searchengine/nova3/novaprinter.py b/src/searchengine/nova3/novaprinter.py index 80f73aae3..f4c9dcbb0 100644 --- a/src/searchengine/nova3/novaprinter.py +++ b/src/searchengine/nova3/novaprinter.py @@ -1,4 +1,4 @@ -#VERSION: 1.48 +#VERSION: 1.50 # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are met: @@ -24,8 +24,25 @@ # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE # POSSIBILITY OF SUCH DAMAGE. +import re +from collections.abc import Mapping +from typing import Any, Union -def prettyPrinter(dictionary): +# TODO: enable the following when using Python >= 3.8 +#SearchResults = TypedDict('SearchResults', { +# 'link': str, +# 'name': str, +# 'size': Union[float, int, str], +# 'seeds': int, +# 'leech': int, +# 'engine_url': str, +# 'desc_link': str, # Optional # TODO: use `NotRequired[str]` when using Python >= 3.11 +# 'pub_date': int # Optional # TODO: use `NotRequired[int]` when using Python >= 3.11 +#}) +SearchResults = Mapping[str, Any] + + +def prettyPrinter(dictionary: SearchResults) -> None: outtext = "|".join(( dictionary["link"], dictionary["name"].replace("|", " "), @@ -34,7 +51,7 @@ def prettyPrinter(dictionary): str(dictionary["leech"]), dictionary["engine_url"], dictionary.get("desc_link", ""), # Optional - str(dictionary.get("pub_date", -1)), # Optional + str(dictionary.get("pub_date", -1)) # Optional )) # fd 1 is stdout @@ -42,30 +59,32 @@ def prettyPrinter(dictionary): print(outtext, file=utf8stdout) -def anySizeToBytes(size_string): +sizeUnitRegex: re.Pattern[str] = re.compile(r"^(?P\d*\.?\d+) *(?P[a-z]+)?", re.IGNORECASE) + + +def anySizeToBytes(size_string: Union[float, int, str]) -> int: """ Convert a string like '1 KB' to '1024' (bytes) - """ - # separate integer from unit - try: - size, unit = size_string.split() - except Exception: - try: - size = size_string.strip() - unit = ''.join([c for c in size if c.isalpha()]) - if len(unit) > 0: - size = size[:-len(unit)] - except Exception: - return -1 - if len(size) == 0: - return -1 - size = float(size) - if len(unit) == 0: - return int(size) - short_unit = unit.upper()[0] - # convert - units_dict = {'T': 40, 'G': 30, 'M': 20, 'K': 10} - if short_unit in units_dict: - size = size * 2**units_dict[short_unit] - return int(size) + The canonical type for `size_string` is `str`. However numeric types are also accepted in order to + accommodate poorly written plugins. + """ + + if isinstance(size_string, int): + return size_string + if isinstance(size_string, float): + return round(size_string) + + match = sizeUnitRegex.match(size_string.strip()) + if match is None: + return -1 + + size = float(match.group('size')) # need to match decimals + unit = match.group('unit') + + if unit is not None: + units_exponents = {'T': 40, 'G': 30, 'M': 20, 'K': 10} + exponent = units_exponents.get(unit[0].upper(), 0) + size *= 2**exponent + + return round(size) diff --git a/src/webui/api/appcontroller.cpp b/src/webui/api/appcontroller.cpp index 7a6fa4547..2d216ff92 100644 --- a/src/webui/api/appcontroller.cpp +++ b/src/webui/api/appcontroller.cpp @@ -37,6 +37,7 @@ #include #include #include +#include #include #include #include @@ -135,7 +136,7 @@ void AppController::preferencesAction() data[u"file_log_age"_s] = app()->fileLoggerAge(); data[u"file_log_age_type"_s] = app()->fileLoggerAgeType(); // Delete torrent contents files on torrent removal - data[u"delete_torrent_content_files"_s] = pref->deleteTorrentFilesAsDefault(); + data[u"delete_torrent_content_files"_s] = pref->removeTorrentContent(); // Downloads // When adding a torrent @@ -349,6 +350,8 @@ void AppController::preferencesAction() // qBitorrent preferences // Resume data storage type data[u"resume_data_storage_type"_s] = Utils::String::fromEnum(session->resumeDataStorageType()); + // Torrent content removing mode + data[u"torrent_content_remove_option"_s] = Utils::String::fromEnum(session->torrentContentRemoveOption()); // Physical memory (RAM) usage limit data[u"memory_working_set_limit"_s] = app()->memoryWorkingSetLimit(); // Current network interface @@ -518,7 +521,7 @@ void AppController::setPreferencesAction() app()->setFileLoggerAgeType(it.value().toInt()); // Delete torrent content files on torrent removal if (hasKey(u"delete_torrent_content_files"_s)) - pref->setDeleteTorrentFilesAsDefault(it.value().toBool()); + pref->setRemoveTorrentContent(it.value().toBool()); // Downloads // When adding a torrent @@ -930,6 +933,9 @@ void AppController::setPreferencesAction() // Resume data storage type if (hasKey(u"resume_data_storage_type"_s)) session->setResumeDataStorageType(Utils::String::toEnum(it.value().toString(), BitTorrent::ResumeDataStorageType::Legacy)); + // Torrent content removing mode + if (hasKey(u"torrent_content_remove_option"_s)) + session->setTorrentContentRemoveOption(Utils::String::toEnum(it.value().toString(), BitTorrent::TorrentContentRemoveOption::MoveToTrash)); // Physical memory (RAM) usage limit if (hasKey(u"memory_working_set_limit"_s)) app()->setMemoryWorkingSetLimit(it.value().toInt()); @@ -1159,8 +1165,11 @@ void AppController::getDirectoryContentAction() throw APIError(APIErrorType::BadParams, tr("Invalid mode, allowed values: %1").arg(u"all, dirs, files"_s)); }; - const QStringList dirs = dir.entryList(QDir::NoDotAndDotDot | parseDirectoryContentMode(visibility)); - setResult(QJsonArray::fromStringList(dirs)); + QJsonArray ret; + QDirIterator it {dirPath, (QDir::NoDotAndDotDot | parseDirectoryContentMode(visibility))}; + while (it.hasNext()) + ret.append(it.next()); + setResult(ret); } void AppController::networkInterfaceListAction() diff --git a/src/webui/api/serialize/serialize_torrent.cpp b/src/webui/api/serialize/serialize_torrent.cpp index 8883f4b82..d48a3a5c8 100644 --- a/src/webui/api/serialize/serialize_torrent.cpp +++ b/src/webui/api/serialize/serialize_torrent.cpp @@ -135,6 +135,7 @@ QVariantMap serialize(const BitTorrent::Torrent &torrent) {KEY_TORRENT_SAVE_PATH, torrent.savePath().toString()}, {KEY_TORRENT_DOWNLOAD_PATH, torrent.downloadPath().toString()}, {KEY_TORRENT_CONTENT_PATH, torrent.contentPath().toString()}, + {KEY_TORRENT_ROOT_PATH, torrent.rootPath().toString()}, {KEY_TORRENT_ADDED_ON, Utils::DateTime::toSecsSinceEpoch(torrent.addedTime())}, {KEY_TORRENT_COMPLETION_ON, Utils::DateTime::toSecsSinceEpoch(torrent.completedTime())}, {KEY_TORRENT_TRACKER, torrent.currentTracker()}, @@ -163,8 +164,8 @@ QVariantMap serialize(const BitTorrent::Torrent &torrent) {KEY_TORRENT_AVAILABILITY, torrent.distributedCopies()}, {KEY_TORRENT_REANNOUNCE, torrent.nextAnnounce()}, {KEY_TORRENT_COMMENT, torrent.comment()}, - {KEY_TORRENT_ISPRIVATE, torrent.isPrivate()}, - - {KEY_TORRENT_TOTAL_SIZE, torrent.totalSize()} + {KEY_TORRENT_PRIVATE, (torrent.hasMetadata() ? torrent.isPrivate() : QVariant())}, + {KEY_TORRENT_TOTAL_SIZE, torrent.totalSize()}, + {KEY_TORRENT_HAS_METADATA, torrent.hasMetadata()} }; } diff --git a/src/webui/api/serialize/serialize_torrent.h b/src/webui/api/serialize/serialize_torrent.h index ee9d20ab7..883efa6dd 100644 --- a/src/webui/api/serialize/serialize_torrent.h +++ b/src/webui/api/serialize/serialize_torrent.h @@ -66,6 +66,7 @@ inline const QString KEY_TORRENT_FORCE_START = u"force_start"_s; inline const QString KEY_TORRENT_SAVE_PATH = u"save_path"_s; inline const QString KEY_TORRENT_DOWNLOAD_PATH = u"download_path"_s; inline const QString KEY_TORRENT_CONTENT_PATH = u"content_path"_s; +inline const QString KEY_TORRENT_ROOT_PATH = u"root_path"_s; inline const QString KEY_TORRENT_ADDED_ON = u"added_on"_s; inline const QString KEY_TORRENT_COMPLETION_ON = u"completion_on"_s; inline const QString KEY_TORRENT_TRACKER = u"tracker"_s; @@ -93,6 +94,7 @@ inline const QString KEY_TORRENT_SEEDING_TIME = u"seeding_time"_s; inline const QString KEY_TORRENT_AVAILABILITY = u"availability"_s; inline const QString KEY_TORRENT_REANNOUNCE = u"reannounce"_s; inline const QString KEY_TORRENT_COMMENT = u"comment"_s; -inline const QString KEY_TORRENT_ISPRIVATE = u"is_private"_s; +inline const QString KEY_TORRENT_PRIVATE = u"private"_s; +inline const QString KEY_TORRENT_HAS_METADATA = u"has_metadata"_s; QVariantMap serialize(const BitTorrent::Torrent &torrent); diff --git a/src/webui/api/synccontroller.cpp b/src/webui/api/synccontroller.cpp index 99330ed59..dd883132a 100644 --- a/src/webui/api/synccontroller.cpp +++ b/src/webui/api/synccontroller.cpp @@ -222,6 +222,7 @@ namespace case QMetaType::UInt: case QMetaType::QDateTime: case QMetaType::Nullptr: + case QMetaType::UnknownType: if (prevData[key] != value) syncData[key] = value; break; diff --git a/src/webui/api/torrentscontroller.cpp b/src/webui/api/torrentscontroller.cpp index 2d40d3645..74272921b 100644 --- a/src/webui/api/torrentscontroller.cpp +++ b/src/webui/api/torrentscontroller.cpp @@ -111,10 +111,13 @@ const QString KEY_PROP_CREATION_DATE = u"creation_date"_s; const QString KEY_PROP_SAVE_PATH = u"save_path"_s; const QString KEY_PROP_DOWNLOAD_PATH = u"download_path"_s; const QString KEY_PROP_COMMENT = u"comment"_s; -const QString KEY_PROP_ISPRIVATE = u"is_private"_s; +const QString KEY_PROP_IS_PRIVATE = u"is_private"_s; // deprecated, "private" should be used instead +const QString KEY_PROP_PRIVATE = u"private"_s; const QString KEY_PROP_SSL_CERTIFICATE = u"ssl_certificate"_s; const QString KEY_PROP_SSL_PRIVATEKEY = u"ssl_private_key"_s; const QString KEY_PROP_SSL_DHPARAMS = u"ssl_dh_params"_s; +const QString KEY_PROP_HAS_METADATA = u"has_metadata"_s; + // File keys const QString KEY_FILE_INDEX = u"index"_s; @@ -282,6 +285,7 @@ void TorrentsController::countAction() // - category (string): torrent category for filtering by it (empty string means "uncategorized"; no "category" param presented means "any category") // - tag (string): torrent tag for filtering by it (empty string means "untagged"; no "tag" param presented means "any tag") // - hashes (string): filter by hashes, can contain multiple hashes separated by | +// - private (bool): filter torrents that are from private trackers (true) or not (false). Empty means any torrent (no filtering) // - sort (string): name of column for sorting by its value // - reverse (bool): enable reverse sorting // - limit (int): set limit number of torrents returned (if greater than 0, otherwise - unlimited) @@ -296,6 +300,7 @@ void TorrentsController::infoAction() int limit {params()[u"limit"_s].toInt()}; int offset {params()[u"offset"_s].toInt()}; const QStringList hashes {params()[u"hashes"_s].split(u'|', Qt::SkipEmptyParts)}; + const std::optional isPrivate = parseBool(params()[u"private"_s]); std::optional idSet; if (!hashes.isEmpty()) @@ -305,7 +310,7 @@ void TorrentsController::infoAction() idSet->insert(BitTorrent::TorrentID::fromString(hash)); } - const TorrentFilter torrentFilter {filter, idSet, category, tag}; + const TorrentFilter torrentFilter {filter, idSet, category, tag, isPrivate}; QVariantList torrentList; for (const BitTorrent::Torrent *torrent : asConst(BitTorrent::Session::instance()->torrents())) { @@ -435,6 +440,8 @@ void TorrentsController::propertiesAction() const int uploadLimit = torrent->uploadLimit(); const qreal ratio = torrent->realRatio(); const qreal popularity = torrent->popularity(); + const bool hasMetadata = torrent->hasMetadata(); + const bool isPrivate = torrent->isPrivate(); const QJsonObject ret { @@ -470,14 +477,16 @@ void TorrentsController::propertiesAction() {KEY_PROP_PIECE_SIZE, torrent->pieceLength()}, {KEY_PROP_PIECES_HAVE, torrent->piecesHave()}, {KEY_PROP_CREATED_BY, torrent->creator()}, - {KEY_PROP_ISPRIVATE, torrent->isPrivate()}, + {KEY_PROP_IS_PRIVATE, torrent->isPrivate()}, // used for maintaining backward compatibility + {KEY_PROP_PRIVATE, (hasMetadata ? isPrivate : QJsonValue())}, {KEY_PROP_ADDITION_DATE, Utils::DateTime::toSecsSinceEpoch(torrent->addedTime())}, {KEY_PROP_LAST_SEEN, Utils::DateTime::toSecsSinceEpoch(torrent->lastSeenComplete())}, {KEY_PROP_COMPLETION_DATE, Utils::DateTime::toSecsSinceEpoch(torrent->completedTime())}, {KEY_PROP_CREATION_DATE, Utils::DateTime::toSecsSinceEpoch(torrent->creationDate())}, {KEY_PROP_SAVE_PATH, torrent->savePath().toString()}, {KEY_PROP_DOWNLOAD_PATH, torrent->downloadPath().toString()}, - {KEY_PROP_COMMENT, torrent->comment()} + {KEY_PROP_COMMENT, torrent->comment()}, + {KEY_PROP_HAS_METADATA, torrent->hasMetadata()} }; setResult(ret); @@ -1092,11 +1101,11 @@ void TorrentsController::deleteAction() requireParams({u"hashes"_s, u"deleteFiles"_s}); const QStringList hashes {params()[u"hashes"_s].split(u'|')}; - const DeleteOption deleteOption = parseBool(params()[u"deleteFiles"_s]).value_or(false) - ? DeleteTorrentAndFiles : DeleteTorrent; + const BitTorrent::TorrentRemoveOption deleteOption = parseBool(params()[u"deleteFiles"_s]).value_or(false) + ? BitTorrent::TorrentRemoveOption::RemoveContent : BitTorrent::TorrentRemoveOption::KeepContent; applyToTorrents(hashes, [deleteOption](const BitTorrent::Torrent *torrent) { - BitTorrent::Session::instance()->deleteTorrent(torrent->id(), deleteOption); + BitTorrent::Session::instance()->removeTorrent(torrent->id(), deleteOption); }); } diff --git a/src/webui/webapplication.cpp b/src/webui/webapplication.cpp index e8661c16f..220767ff7 100644 --- a/src/webui/webapplication.cpp +++ b/src/webui/webapplication.cpp @@ -737,16 +737,15 @@ void WebApplication::sessionStart() connect(m_freeDiskSpaceChecker, &FreeDiskSpaceChecker::checked, syncController, &SyncController::updateFreeDiskSpace); m_currentSession->registerAPIController(u"sync"_s, syncController); - QNetworkCookie cookie {m_sessionCookieName.toLatin1(), m_currentSession->id().toUtf8()}; + QNetworkCookie cookie {m_sessionCookieName.toLatin1(), m_currentSession->id().toLatin1()}; cookie.setHttpOnly(true); cookie.setSecure(m_isSecureCookieEnabled && m_isHttpsEnabled); cookie.setPath(u"/"_s); - QByteArray cookieRawForm = cookie.toRawForm(); if (m_isCSRFProtectionEnabled) - cookieRawForm.append("; SameSite=Strict"); + cookie.setSameSitePolicy(QNetworkCookie::SameSite::Strict); else if (cookie.isSecure()) - cookieRawForm.append("; SameSite=None"); - setHeader({Http::HEADER_SET_COOKIE, QString::fromLatin1(cookieRawForm)}); + cookie.setSameSitePolicy(QNetworkCookie::SameSite::None); + setHeader({Http::HEADER_SET_COOKIE, QString::fromLatin1(cookie.toRawForm())}); } void WebApplication::sessionEnd() diff --git a/src/webui/webapplication.h b/src/webui/webapplication.h index b14ce770f..16b1f2950 100644 --- a/src/webui/webapplication.h +++ b/src/webui/webapplication.h @@ -54,7 +54,7 @@ #include "base/utils/version.h" #include "api/isessionmanager.h" -inline const Utils::Version<3, 2> API_VERSION {2, 11, 0}; +inline const Utils::Version<3, 2> API_VERSION {2, 11, 2}; class QTimer; diff --git a/src/webui/www/eslint.config.mjs b/src/webui/www/eslint.config.mjs index ba6f91a0d..e77e7d422 100644 --- a/src/webui/www/eslint.config.mjs +++ b/src/webui/www/eslint.config.mjs @@ -1,8 +1,8 @@ -import Globals from 'globals'; -import Html from 'eslint-plugin-html'; -import Js from '@eslint/js'; -import Stylistic from '@stylistic/eslint-plugin'; -import * as RegexpPlugin from 'eslint-plugin-regexp'; +import Globals from "globals"; +import Html from "eslint-plugin-html"; +import Js from "@eslint/js"; +import Stylistic from "@stylistic/eslint-plugin"; +import * as RegexpPlugin from "eslint-plugin-regexp"; export default [ Js.configs.recommended, @@ -26,9 +26,16 @@ export default [ Stylistic }, rules: { + "curly": ["error", "multi-or-nest", "consistent"], "eqeqeq": "error", + "guard-for-in": "error", "no-undef": "off", "no-unused-vars": "off", + "no-var": "error", + "operator-assignment": "error", + "prefer-arrow-callback": "error", + "prefer-const": "error", + "radix": "error", "Stylistic/no-mixed-operators": [ "error", { @@ -38,7 +45,16 @@ export default [ } ], "Stylistic/nonblock-statement-body-position": ["error", "below"], - "Stylistic/semi": "error" + "Stylistic/quotes": [ + "error", + "double", + { + "avoidEscape": true, + "allowTemplateLiterals": true + } + ], + "Stylistic/semi": "error", + "Stylistic/spaced-comment": ["error", "always", { "exceptions": ["*"] }] } } ]; diff --git a/src/webui/www/package.json b/src/webui/www/package.json index e5f2acd14..bad08c881 100644 --- a/src/webui/www/package.json +++ b/src/webui/www/package.json @@ -6,8 +6,8 @@ "url": "https://github.com/qbittorrent/qBittorrent.git" }, "scripts": { - "format": "js-beautify -r private/*.html private/scripts/*.js private/views/*.html public/*.html public/scripts/*.js && prettier --write **.css", - "lint": "eslint private/*.html private/scripts/*.js private/views/*.html public/*.html public/scripts/*.js && stylelint **/*.css && html-validate private public" + "format": "js-beautify -r *.mjs private/*.html private/scripts/*.js private/views/*.html public/*.html public/scripts/*.js && prettier --write **.css", + "lint": "eslint *.mjs private/*.html private/scripts/*.js private/views/*.html public/*.html public/scripts/*.js && stylelint **/*.css && html-validate private public" }, "devDependencies": { "@stylistic/eslint-plugin": "*", diff --git a/src/webui/www/private/addpeers.html b/src/webui/www/private/addpeers.html index a86673d14..b9be98ff1 100644 --- a/src/webui/www/private/addpeers.html +++ b/src/webui/www/private/addpeers.html @@ -8,42 +8,42 @@ diff --git a/src/webui/www/private/edittracker.html b/src/webui/www/private/edittracker.html index feb5f5766..8df7c5e9d 100644 --- a/src/webui/www/private/edittracker.html +++ b/src/webui/www/private/edittracker.html @@ -8,44 +8,44 @@ diff --git a/src/webui/www/private/upload.html b/src/webui/www/private/upload.html index ccdbc1124..3d2ee024b 100644 --- a/src/webui/www/private/upload.html +++ b/src/webui/www/private/upload.html @@ -150,28 +150,27 @@
diff --git a/src/webui/www/private/uploadlimit.html b/src/webui/www/private/uploadlimit.html index 87d0db7b6..ef3b2731b 100644 --- a/src/webui/www/private/uploadlimit.html +++ b/src/webui/www/private/uploadlimit.html @@ -25,17 +25,17 @@ diff --git a/src/webui/www/private/views/about.html b/src/webui/www/private/views/about.html index 450a0a46e..e566106a8 100644 --- a/src/webui/www/private/views/about.html +++ b/src/webui/www/private/views/about.html @@ -841,18 +841,18 @@ diff --git a/src/webui/www/private/views/aboutToolbar.html b/src/webui/www/private/views/aboutToolbar.html index 3d9afd0e6..771645098 100644 --- a/src/webui/www/private/views/aboutToolbar.html +++ b/src/webui/www/private/views/aboutToolbar.html @@ -11,39 +11,39 @@ diff --git a/src/webui/www/private/views/filters.html b/src/webui/www/private/views/filters.html index 9b65dfa40..cf73c2ba3 100644 --- a/src/webui/www/private/views/filters.html +++ b/src/webui/www/private/views/filters.html @@ -42,11 +42,10 @@ diff --git a/src/webui/www/private/views/properties.html b/src/webui/www/private/views/properties.html index 473221ed1..b42c8d19f 100644 --- a/src/webui/www/private/views/properties.html +++ b/src/webui/www/private/views/properties.html @@ -74,6 +74,10 @@ QBT_TR(Created On:)QBT_TR[CONTEXT=PropertiesWidget] + + QBT_TR(Private:)QBT_TR[CONTEXT=PropertiesWidget] + + QBT_TR(Info Hash v1:)QBT_TR[CONTEXT=PropertiesWidget] @@ -168,10 +172,10 @@ diff --git a/src/webui/www/private/views/searchplugins.html b/src/webui/www/private/views/searchplugins.html index 4e18d4923..e913ff670 100644 --- a/src/webui/www/private/views/searchplugins.html +++ b/src/webui/www/private/views/searchplugins.html @@ -77,11 +77,10 @@