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'));