From 5e6174c087283c1447c6a7dd77902d727718e4d2 Mon Sep 17 00:00:00 2001 From: mxtsdev <58796811+mxtsdev@users.noreply.github.com> Date: Thu, 9 Jun 2022 22:37:46 -0700 Subject: [PATCH] Add file name filter/blacklist Blacklist filtered file names from being downloaded from torrent(s). Files matching any of the filters in this list will have their priority automatically set to "Do not download". See Options > Downloads >Do not download. Closes #3369. PR #17106. --- src/base/bittorrent/session.cpp | 66 +++++++++++++++++--- src/base/bittorrent/session.h | 6 ++ src/base/bittorrent/torrentimpl.cpp | 18 +++--- src/gui/addnewtorrentdialog.cpp | 15 +++++ src/gui/optionsdialog.cpp | 3 + src/gui/optionsdialog.ui | 38 +++++++++++ src/webui/api/appcontroller.cpp | 5 ++ src/webui/webapplication.h | 2 +- src/webui/www/private/views/preferences.html | 8 +++ 9 files changed, 144 insertions(+), 17 deletions(-) diff --git a/src/base/bittorrent/session.cpp b/src/base/bittorrent/session.cpp index 89544084e..def8e5e05 100644 --- a/src/base/bittorrent/session.cpp +++ b/src/base/bittorrent/session.cpp @@ -414,6 +414,7 @@ Session::Session(QObject *parent) , m_peerTurnoverCutoff(BITTORRENT_SESSION_KEY(u"PeerTurnoverCutOff"_qs), 90) , m_peerTurnoverInterval(BITTORRENT_SESSION_KEY(u"PeerTurnoverInterval"_qs), 300) , m_requestQueueSize(BITTORRENT_SESSION_KEY(u"RequestQueueSize"_qs), 500) + , m_excludedFileNames(BITTORRENT_SESSION_KEY(u"ExcludedFileNames"_qs)) , m_bannedIPs(u"State/BannedIPs"_qs , QStringList() , [](const QStringList &value) @@ -466,6 +467,7 @@ Session::Session(QObject *parent) enqueueRefresh(); updateSeedingLimitTimer(); populateAdditionalTrackers(); + populateExcludedFileNamesRegExpList(); enableTracker(isTrackerEnabled()); @@ -2244,19 +2246,30 @@ bool Session::addTorrent_impl(const std::variant &source } const auto nativeIndexes = torrentInfo.nativeIndexes(); - if (!filePaths.isEmpty()) - { - for (int index = 0; index < filePaths.size(); ++index) - p.renamed_files[nativeIndexes[index]] = filePaths.at(index).toString().toStdString(); - } + for (int index = 0; index < filePaths.size(); ++index) + p.renamed_files[nativeIndexes[index]] = filePaths.at(index).toString().toStdString(); Q_ASSERT(p.file_priorities.empty()); + Q_ASSERT(addTorrentParams.filePriorities.isEmpty() || (addTorrentParams.filePriorities.size() == nativeIndexes.size())); + 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)); - Q_ASSERT(addTorrentParams.filePriorities.isEmpty() || (addTorrentParams.filePriorities.size() == nativeIndexes.size())); - for (int i = 0; i < addTorrentParams.filePriorities.size(); ++i) - p.file_priorities[LT::toUnderlyingType(nativeIndexes[i])] = LT::toNative(addTorrentParams.filePriorities[i]); + + if (addTorrentParams.filePriorities.size() == 0) + { + // 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]); + } p.ti = torrentInfo.nativeInfo(); } @@ -3078,6 +3091,43 @@ void Session::setIPFilterFile(const Path &path) } } +QStringList Session::excludedFileNames() const +{ + return m_excludedFileNames; +} + +void Session::setExcludedFileNames(const QStringList &excludedFileNames) +{ + if (excludedFileNames != m_excludedFileNames) + { + m_excludedFileNames = excludedFileNames; + populateExcludedFileNamesRegExpList(); + } +} + +void Session::populateExcludedFileNamesRegExpList() +{ + const QStringList excludedNames = excludedFileNames(); + + m_excludedFileNamesRegExpList.clear(); + m_excludedFileNamesRegExpList.reserve(excludedNames.size()); + + for (const QString &str : excludedNames) + { + const QString pattern = QRegularExpression::anchoredPattern(QRegularExpression::wildcardToRegularExpression(str)); + const QRegularExpression re {pattern, QRegularExpression::CaseInsensitiveOption}; + m_excludedFileNamesRegExpList.append(re); + } +} + +bool Session::isFilenameExcluded(const QString &fileName) const +{ + return std::any_of(m_excludedFileNamesRegExpList.begin(), m_excludedFileNamesRegExpList.end(), [&fileName](const QRegularExpression &re) + { + return re.match(fileName).hasMatch(); + }); +} + void Session::setBannedIPs(const QStringList &newList) { if (newList == m_bannedIPs) diff --git a/src/base/bittorrent/session.h b/src/base/bittorrent/session.h index cd5e2a3ac..5406eab2e 100644 --- a/src/base/bittorrent/session.h +++ b/src/base/bittorrent/session.h @@ -455,6 +455,9 @@ namespace BitTorrent void setBlockPeersOnPrivilegedPorts(bool enabled); bool isTrackerFilteringEnabled() const; void setTrackerFilteringEnabled(bool enabled); + QStringList excludedFileNames() const; + void setExcludedFileNames(const QStringList &newList); + bool isFilenameExcluded(const QString &fileName) const; QStringList bannedIPs() const; void setBannedIPs(const QStringList &newList); ResumeDataStorageType resumeDataStorageType() const; @@ -624,6 +627,7 @@ namespace BitTorrent void applyOSMemoryPriority() const; #endif void processTrackerStatuses(); + void populateExcludedFileNamesRegExpList(); bool loadTorrent(LoadTorrentParams params); LoadTorrentParams initLoadTorrentParams(const AddTorrentParams &addTorrentParams); @@ -778,6 +782,7 @@ namespace BitTorrent CachedSettingValue m_peerTurnoverCutoff; CachedSettingValue m_peerTurnoverInterval; CachedSettingValue m_requestQueueSize; + CachedSettingValue m_excludedFileNames; CachedSettingValue m_bannedIPs; CachedSettingValue m_resumeDataStorageType; #if defined(Q_OS_WIN) @@ -792,6 +797,7 @@ namespace BitTorrent int m_numResumeData = 0; int m_extraLimit = 0; QVector m_additionalTrackerList; + QVector m_excludedFileNamesRegExpList; bool m_refreshEnqueued = false; QTimer *m_seedingLimitTimer = nullptr; diff --git a/src/base/bittorrent/torrentimpl.cpp b/src/base/bittorrent/torrentimpl.cpp index 6a2d1dcd4..fd08c5bdc 100644 --- a/src/base/bittorrent/torrentimpl.cpp +++ b/src/base/bittorrent/torrentimpl.cpp @@ -1519,9 +1519,8 @@ void TorrentImpl::endReceivedMetadataHandling(const Path &savePath, const PathLi m_torrentInfo = TorrentInfo(*metadata); m_filePriorities.reserve(filesCount()); const auto nativeIndexes = m_torrentInfo.nativeIndexes(); - const std::vector filePriorities = - resized(p.file_priorities, metadata->files().num_files() - , LT::toNative(p.file_priorities.empty() ? DownloadPriority::Normal : DownloadPriority::Ignored)); + p.file_priorities = resized(p.file_priorities, metadata->files().num_files() + , LT::toNative(p.file_priorities.empty() ? DownloadPriority::Normal : DownloadPriority::Ignored)); m_completedFiles.fill(static_cast(p.flags & lt::torrent_flags::seed_mode), filesCount()); @@ -1529,17 +1528,20 @@ void TorrentImpl::endReceivedMetadataHandling(const Path &savePath, const PathLi { const auto nativeIndex = nativeIndexes.at(i); - const Path filePath = fileNames.at(i); - p.renamed_files[nativeIndex] = filePath.toString().toStdString(); + const Path actualFilePath = fileNames.at(i); + p.renamed_files[nativeIndex] = actualFilePath.toString().toStdString(); - m_filePaths.append(filePath.removedExtension(QB_EXT)); + const Path filePath = actualFilePath.removedExtension(QB_EXT); + m_filePaths.append(filePath); - const auto priority = LT::fromNative(filePriorities[LT::toUnderlyingType(nativeIndex)]); + 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); } p.save_path = savePath.toString().toStdString(); p.ti = metadata; - p.file_priorities = filePriorities; reload(); diff --git a/src/gui/addnewtorrentdialog.cpp b/src/gui/addnewtorrentdialog.cpp index 7875ef2aa..846289ccc 100644 --- a/src/gui/addnewtorrentdialog.cpp +++ b/src/gui/addnewtorrentdialog.cpp @@ -975,6 +975,21 @@ void AddNewTorrentDialog::setupTreeview() currentIndex = m_contentModel->index(0, 0, currentIndex); m_ui->contentTreeView->setExpanded(currentIndex, true); } + + // Check file name blacklist for torrents that are manually added + QVector priorities = m_contentModel->model()->getFilePriorities(); + Q_ASSERT(priorities.size() == m_torrentInfo.filesCount()); + + for (int i = 0; i < priorities.size(); ++i) + { + if (priorities[i] == BitTorrent::DownloadPriority::Ignored) + continue; + + if (BitTorrent::Session::instance()->isFilenameExcluded(m_torrentInfo.filePath(i).filename())) + priorities[i] = BitTorrent::DownloadPriority::Ignored; + } + + m_contentModel->model()->updateFilesPriorities(priorities); } updateDiskSpaceLabel(); diff --git a/src/gui/optionsdialog.cpp b/src/gui/optionsdialog.cpp index 9b79240b8..aab94e7f2 100644 --- a/src/gui/optionsdialog.cpp +++ b/src/gui/optionsdialog.cpp @@ -375,6 +375,7 @@ OptionsDialog::OptionsDialog(QWidget *parent) connect(m_ui->checkUseDownloadPath, &QAbstractButton::toggled, this, &ThisType::enableApplyButton); connect(m_ui->checkUseDownloadPath, &QAbstractButton::toggled, m_ui->textDownloadPath, &QWidget::setEnabled); connect(m_ui->addWatchedFolderButton, &QAbstractButton::clicked, this, &ThisType::enableApplyButton); + connect(m_ui->textExcludedFileNames, &QPlainTextEdit::textChanged, this, &ThisType::enableApplyButton); connect(m_ui->removeWatchedFolderButton, &QAbstractButton::clicked, this, &ThisType::enableApplyButton); connect(m_ui->groupMailNotification, &QGroupBox::toggled, this, &ThisType::enableApplyButton); connect(m_ui->senderEmailTxt, &QLineEdit::textChanged, this, &ThisType::enableApplyButton); @@ -757,6 +758,7 @@ void OptionsDialog::saveOptions() session->setTorrentContentLayout(static_cast(m_ui->contentLayoutComboBox->currentIndex())); auto watchedFoldersModel = static_cast(m_ui->scanFoldersView->model()); watchedFoldersModel->apply(); + session->setExcludedFileNames(m_ui->textExcludedFileNames->toPlainText().split(u'\n', Qt::SkipEmptyParts)); session->setTorrentExportDirectory(getTorrentExportDir()); session->setFinishedTorrentExportDirectory(getFinishedTorrentExportDir()); pref->setMailNotificationEnabled(m_ui->groupMailNotification->isChecked()); @@ -1016,6 +1018,7 @@ void OptionsDialog::loadOptions() m_ui->checkAppendqB->setChecked(session->isAppendExtensionEnabled()); m_ui->checkPreallocateAll->setChecked(session->isPreallocationEnabled()); m_ui->checkRecursiveDownload->setChecked(!pref->recursiveDownloadDisabled()); + m_ui->textExcludedFileNames->setPlainText(session->excludedFileNames().join(u'\n')); if (session->torrentExportDirectory().isEmpty()) { diff --git a/src/gui/optionsdialog.ui b/src/gui/optionsdialog.ui index 8193256b8..75b7867f4 100644 --- a/src/gui/optionsdialog.ui +++ b/src/gui/optionsdialog.ui @@ -1261,6 +1261,44 @@ Manual: Various torrent properties (e.g. save path) must be assigned manually + + + + Excluded file names + + + + + + Filters: + + + + + + + Blacklist filtered file names from being downloaded from torrent(s). +Files matching any of the filters in this list will have their priority automatically set to "Do not download". + +Use newlines to separate multiple entries. Can use wildcards as outlined below. +*: matches zero or more of any characters. +?: matches any single character. +[...]: sets of characters can be represented in square brackets. + +Examples +*.exe: filter '.exe' file extension. +readme.txt: filter exact file name. +?.txt: filter 'a.txt', 'b.txt' but not 'aa.txt'. +readme[0-9].txt: filter 'readme1.txt', 'readme2.txt' but not 'readme10.txt'. + + + QPlainTextEdit::NoWrap + + + + + + diff --git a/src/webui/api/appcontroller.cpp b/src/webui/api/appcontroller.cpp index 987769eed..68351d9b6 100644 --- a/src/webui/api/appcontroller.cpp +++ b/src/webui/api/appcontroller.cpp @@ -141,6 +141,8 @@ void AppController::preferencesAction() data[u"scan_dirs"_qs] = nativeDirs; // === END DEPRECATED CODE === // + data[u"excluded_file_names"_qs] = session->excludedFileNames().join(u'\n'); + // Email notification upon download completion data[u"mail_notification_enabled"_qs] = pref->isMailNotificationEnabled(); data[u"mail_notification_sender"_qs] = pref->getMailNotificationSender(); @@ -476,6 +478,9 @@ void AppController::setPreferencesAction() } // === END DEPRECATED CODE === // + if (hasKey(u"excluded_file_names"_qs)) + session->setExcludedFileNames(it.value().toString().split(u'\n')); + // Email notification upon download completion if (hasKey(u"mail_notification_enabled"_qs)) pref->setMailNotificationEnabled(it.value().toBool()); diff --git a/src/webui/webapplication.h b/src/webui/webapplication.h index 47bafd24f..d6581e578 100644 --- a/src/webui/webapplication.h +++ b/src/webui/webapplication.h @@ -48,7 +48,7 @@ #include "base/utils/version.h" #include "api/isessionmanager.h" -inline const Utils::Version API_VERSION {2, 8, 11}; +inline const Utils::Version API_VERSION {2, 8, 12}; class APIController; class AuthController; diff --git a/src/webui/www/private/views/preferences.html b/src/webui/www/private/views/preferences.html index 98d71c480..de274d2d5 100644 --- a/src/webui/www/private/views/preferences.html +++ b/src/webui/www/private/views/preferences.html @@ -130,6 +130,12 @@ +
+ QBT_TR(Excluded file names)QBT_TR[CONTEXT=OptionsDialog] +
+ +
+
@@ -1733,6 +1739,7 @@ addWatchFolder(folder, sel, other); } addWatchFolder(); + $('excludedFileNamesTextarea').setProperty('value', pref.excluded_file_names); // Email notification upon download completion $('mail_notification_checkbox').setProperty('checked', pref.mail_notification_enabled); @@ -2053,6 +2060,7 @@ // Automatically add torrents from settings.set('scan_dirs', getWatchedFolders()); + settings.set('excluded_file_names', $('excludedFileNamesTextarea').getProperty('value')); // Email notification upon download completion settings.set('mail_notification_enabled', $('mail_notification_checkbox').getProperty('checked'));