mirror of
https://github.com/qbittorrent/qBittorrent
synced 2025-07-07 05:31:25 -07:00
Use Perl-compatible regexes for RSS rules. Closes #6367.
--HG-- branch : magao-dev
This commit is contained in:
parent
d045f64ebb
commit
f9abd254f4
6 changed files with 81 additions and 42 deletions
|
@ -28,14 +28,16 @@
|
||||||
* Contact : chris@qbittorrent.org
|
* Contact : chris@qbittorrent.org
|
||||||
*/
|
*/
|
||||||
|
|
||||||
#include <QRegExp>
|
|
||||||
#include <QDebug>
|
#include <QDebug>
|
||||||
#include <QDir>
|
#include <QDir>
|
||||||
|
#include <QRegExp>
|
||||||
|
#include <QRegularExpression>
|
||||||
#include <QString>
|
#include <QString>
|
||||||
#include <QStringList>
|
#include <QStringList>
|
||||||
|
|
||||||
#include "base/preferences.h"
|
#include "base/preferences.h"
|
||||||
#include "base/utils/fs.h"
|
#include "base/utils/fs.h"
|
||||||
|
#include "base/utils/string.h"
|
||||||
#include "rssfeed.h"
|
#include "rssfeed.h"
|
||||||
#include "rssarticle.h"
|
#include "rssarticle.h"
|
||||||
#include "rssdownloadrule.h"
|
#include "rssdownloadrule.h"
|
||||||
|
@ -52,23 +54,23 @@ DownloadRule::DownloadRule()
|
||||||
|
|
||||||
bool DownloadRule::matches(const QString &articleTitle, const QString &expression) const
|
bool DownloadRule::matches(const QString &articleTitle, const QString &expression) const
|
||||||
{
|
{
|
||||||
static QRegExp whitespace("\\s+");
|
static QRegularExpression whitespace("\\s+");
|
||||||
|
|
||||||
if (expression.isEmpty()) {
|
if (expression.isEmpty()) {
|
||||||
// A regex of the form "expr|" will always match, so do the same for wildcards
|
// A regex of the form "expr|" will always match, so do the same for wildcards
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
else if (m_useRegex) {
|
else if (m_useRegex) {
|
||||||
QRegExp reg(expression, Qt::CaseInsensitive, QRegExp::RegExp);
|
QRegularExpression reg(expression, QRegularExpression::CaseInsensitiveOption);
|
||||||
return reg.indexIn(articleTitle) > -1;
|
return reg.match(articleTitle).hasMatch();
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
// Only match if every wildcard token (separated by spaces) is present in the article name.
|
// Only match if every wildcard token (separated by spaces) is present in the article name.
|
||||||
// Order of wildcard tokens is unimportant (if order is important, they should have used *).
|
// Order of wildcard tokens is unimportant (if order is important, they should have used *).
|
||||||
foreach (const QString &wildcard, expression.split(whitespace, QString::SplitBehavior::SkipEmptyParts)) {
|
foreach (const QString &wildcard, expression.split(whitespace, QString::SplitBehavior::SkipEmptyParts)) {
|
||||||
QRegExp reg(wildcard, Qt::CaseInsensitive, QRegExp::Wildcard);
|
QRegularExpression reg(Utils::String::wildcardToRegex(wildcard), QRegularExpression::CaseInsensitiveOption);
|
||||||
|
|
||||||
if (reg.indexIn(articleTitle) == -1)
|
if (!reg.match(articleTitle).hasMatch())
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -124,14 +126,15 @@ bool DownloadRule::matches(const QString &articleTitle) const
|
||||||
|
|
||||||
if (!m_episodeFilter.isEmpty()) {
|
if (!m_episodeFilter.isEmpty()) {
|
||||||
qDebug() << "Checking episode filter:" << m_episodeFilter;
|
qDebug() << "Checking episode filter:" << m_episodeFilter;
|
||||||
QRegExp f("(^\\d{1,4})x(.*;$)");
|
QRegularExpression f("(^\\d{1,4})x(.*;$)");
|
||||||
int pos = f.indexIn(m_episodeFilter);
|
QRegularExpressionMatch matcher = f.match(m_episodeFilter);
|
||||||
|
bool matched = matcher.hasMatch();
|
||||||
|
|
||||||
if (pos < 0)
|
if (!matched)
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
QString s = f.cap(1);
|
QString s = matcher.captured(1);
|
||||||
QStringList eps = f.cap(2).split(";");
|
QStringList eps = matcher.captured(2).split(";");
|
||||||
int sOurs = s.toInt();
|
int sOurs = s.toInt();
|
||||||
|
|
||||||
foreach (QString ep, eps) {
|
foreach (QString ep, eps) {
|
||||||
|
@ -145,22 +148,24 @@ bool DownloadRule::matches(const QString &articleTitle) const
|
||||||
if (ep.indexOf('-') != -1) { // Range detected
|
if (ep.indexOf('-') != -1) { // Range detected
|
||||||
QString partialPattern1 = "\\bs0?(\\d{1,4})[ -_\\.]?e(0?\\d{1,4})(?:\\D|\\b)";
|
QString partialPattern1 = "\\bs0?(\\d{1,4})[ -_\\.]?e(0?\\d{1,4})(?:\\D|\\b)";
|
||||||
QString partialPattern2 = "\\b(\\d{1,4})x(0?\\d{1,4})(?:\\D|\\b)";
|
QString partialPattern2 = "\\b(\\d{1,4})x(0?\\d{1,4})(?:\\D|\\b)";
|
||||||
QRegExp reg(partialPattern1, Qt::CaseInsensitive);
|
QRegularExpression reg(partialPattern1, QRegularExpression::CaseInsensitiveOption);
|
||||||
|
|
||||||
if (ep.endsWith('-')) { // Infinite range
|
if (ep.endsWith('-')) { // Infinite range
|
||||||
int epOurs = ep.left(ep.size() - 1).toInt();
|
int epOurs = ep.left(ep.size() - 1).toInt();
|
||||||
|
|
||||||
// Extract partial match from article and compare as digits
|
// Extract partial match from article and compare as digits
|
||||||
pos = reg.indexIn(articleTitle);
|
matcher = reg.match(articleTitle);
|
||||||
|
matched = matcher.hasMatch();
|
||||||
|
|
||||||
if (pos == -1) {
|
if (!matched) {
|
||||||
reg = QRegExp(partialPattern2, Qt::CaseInsensitive);
|
reg = QRegularExpression(partialPattern2, QRegularExpression::CaseInsensitiveOption);
|
||||||
pos = reg.indexIn(articleTitle);
|
matcher = reg.match(articleTitle);
|
||||||
|
matched = matcher.hasMatch();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (pos != -1) {
|
if (matched) {
|
||||||
int sTheirs = reg.cap(1).toInt();
|
int sTheirs = matcher.captured(1).toInt();
|
||||||
int epTheirs = reg.cap(2).toInt();
|
int epTheirs = matcher.captured(2).toInt();
|
||||||
if (((sTheirs == sOurs) && (epTheirs >= epOurs)) || (sTheirs > sOurs)) {
|
if (((sTheirs == sOurs) && (epTheirs >= epOurs)) || (sTheirs > sOurs)) {
|
||||||
qDebug() << "Matched episode:" << ep;
|
qDebug() << "Matched episode:" << ep;
|
||||||
qDebug() << "Matched article:" << articleTitle;
|
qDebug() << "Matched article:" << articleTitle;
|
||||||
|
@ -178,16 +183,18 @@ bool DownloadRule::matches(const QString &articleTitle) const
|
||||||
int epOursLast = range.last().toInt();
|
int epOursLast = range.last().toInt();
|
||||||
|
|
||||||
// Extract partial match from article and compare as digits
|
// Extract partial match from article and compare as digits
|
||||||
pos = reg.indexIn(articleTitle);
|
matcher = reg.match(articleTitle);
|
||||||
|
matched = matcher.hasMatch();
|
||||||
|
|
||||||
if (pos == -1) {
|
if (!matched) {
|
||||||
reg = QRegExp(partialPattern2, Qt::CaseInsensitive);
|
reg = QRegularExpression(partialPattern2, QRegularExpression::CaseInsensitiveOption);
|
||||||
pos = reg.indexIn(articleTitle);
|
matcher = reg.match(articleTitle);
|
||||||
|
matched = matcher.hasMatch();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (pos != -1) {
|
if (matched) {
|
||||||
int sTheirs = reg.cap(1).toInt();
|
int sTheirs = matcher.captured(1).toInt();
|
||||||
int epTheirs = reg.cap(2).toInt();
|
int epTheirs = matcher.captured(2).toInt();
|
||||||
if ((sTheirs == sOurs) && ((epOursFirst <= epTheirs) && (epOursLast >= epTheirs))) {
|
if ((sTheirs == sOurs) && ((epOursFirst <= epTheirs) && (epOursLast >= epTheirs))) {
|
||||||
qDebug() << "Matched episode:" << ep;
|
qDebug() << "Matched episode:" << ep;
|
||||||
qDebug() << "Matched article:" << articleTitle;
|
qDebug() << "Matched article:" << articleTitle;
|
||||||
|
@ -198,8 +205,8 @@ bool DownloadRule::matches(const QString &articleTitle) const
|
||||||
}
|
}
|
||||||
else { // Single number
|
else { // Single number
|
||||||
QString expStr("\\b(?:s0?" + s + "[ -_\\.]?" + "e0?" + ep + "|" + s + "x" + "0?" + ep + ")(?:\\D|\\b)");
|
QString expStr("\\b(?:s0?" + s + "[ -_\\.]?" + "e0?" + ep + "|" + s + "x" + "0?" + ep + ")(?:\\D|\\b)");
|
||||||
QRegExp reg(expStr, Qt::CaseInsensitive);
|
QRegularExpression reg(expStr, QRegularExpression::CaseInsensitiveOption);
|
||||||
if (reg.indexIn(articleTitle) != -1) {
|
if (reg.match(articleTitle).hasMatch()) {
|
||||||
qDebug() << "Matched episode:" << ep;
|
qDebug() << "Matched episode:" << ep;
|
||||||
qDebug() << "Matched article:" << articleTitle;
|
qDebug() << "Matched article:" << articleTitle;
|
||||||
return true;
|
return true;
|
||||||
|
|
|
@ -31,10 +31,12 @@
|
||||||
#ifndef RSSDOWNLOADRULE_H
|
#ifndef RSSDOWNLOADRULE_H
|
||||||
#define RSSDOWNLOADRULE_H
|
#define RSSDOWNLOADRULE_H
|
||||||
|
|
||||||
#include <QStringList>
|
#include <QDateTime>
|
||||||
#include <QVariantHash>
|
#include <QVariantHash>
|
||||||
#include <QSharedPointer>
|
#include <QSharedPointer>
|
||||||
#include <QDateTime>
|
#include <QStringList>
|
||||||
|
|
||||||
|
class QRegularExpression;
|
||||||
|
|
||||||
namespace Rss
|
namespace Rss
|
||||||
{
|
{
|
||||||
|
|
|
@ -37,6 +37,7 @@
|
||||||
#ifdef QBT_USES_QT5
|
#ifdef QBT_USES_QT5
|
||||||
#include <QCollator>
|
#include <QCollator>
|
||||||
#endif
|
#endif
|
||||||
|
#include <QRegExp>
|
||||||
#ifdef Q_OS_MAC
|
#ifdef Q_OS_MAC
|
||||||
#include <QThreadStorage>
|
#include <QThreadStorage>
|
||||||
#endif
|
#endif
|
||||||
|
@ -211,3 +212,13 @@ bool Utils::String::slowEquals(const QByteArray &a, const QByteArray &b)
|
||||||
|
|
||||||
return (diff == 0);
|
return (diff == 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// This is marked as internal in QRegExp.cpp, but is exported. The alternative would be to
|
||||||
|
// copy the code from QRegExp::wc2rx().
|
||||||
|
QString qt_regexp_toCanonical(const QString &pattern, QRegExp::PatternSyntax patternSyntax);
|
||||||
|
|
||||||
|
QString Utils::String::wildcardToRegex(const QString &pattern)
|
||||||
|
{
|
||||||
|
return qt_regexp_toCanonical(pattern, QRegExp::Wildcard);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
|
@ -49,6 +49,8 @@ namespace Utils
|
||||||
|
|
||||||
bool naturalCompareCaseSensitive(const QString &left, const QString &right);
|
bool naturalCompareCaseSensitive(const QString &left, const QString &right);
|
||||||
bool naturalCompareCaseInsensitive(const QString &left, const QString &right);
|
bool naturalCompareCaseInsensitive(const QString &left, const QString &right);
|
||||||
|
|
||||||
|
QString wildcardToRegex(const QString &pattern);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -33,6 +33,7 @@
|
||||||
#include <QFileDialog>
|
#include <QFileDialog>
|
||||||
#include <QMessageBox>
|
#include <QMessageBox>
|
||||||
#include <QMenu>
|
#include <QMenu>
|
||||||
|
#include <QRegularExpression>
|
||||||
|
|
||||||
#include "base/bittorrent/session.h"
|
#include "base/bittorrent/session.h"
|
||||||
#include "base/preferences.h"
|
#include "base/preferences.h"
|
||||||
|
@ -73,8 +74,8 @@ AutomatedRssDownloader::AutomatedRssDownloader(const QWeakPointer<Rss::Manager>
|
||||||
Q_ASSERT(ok);
|
Q_ASSERT(ok);
|
||||||
m_ruleList = manager.toStrongRef()->downloadRules();
|
m_ruleList = manager.toStrongRef()->downloadRules();
|
||||||
m_editableRuleList = new Rss::DownloadRuleList; // Read rule list from disk
|
m_editableRuleList = new Rss::DownloadRuleList; // Read rule list from disk
|
||||||
m_episodeRegex = new QRegExp("^(^\\d{1,4}x(\\d{1,4}(-(\\d{1,4})?)?;){1,}){1,1}",
|
m_episodeRegex = new QRegularExpression("^(^\\d{1,4}x(\\d{1,4}(-(\\d{1,4})?)?;){1,}){1,1}",
|
||||||
Qt::CaseInsensitive);
|
QRegularExpression::CaseInsensitiveOption);
|
||||||
QString tip = "<p>" + tr("Matches articles based on episode filter.") + "</p><p><b>" + tr("Example: ")
|
QString tip = "<p>" + tr("Matches articles based on episode filter.") + "</p><p><b>" + tr("Example: ")
|
||||||
+ "1x2;8-15;5;30-;</b>" + tr(" will match 2, 5, 8 through 15, 30 and onward episodes of season one", "example X will match") + "</p>";
|
+ "1x2;8-15;5;30-;</b>" + tr(" will match 2, 5, 8 through 15, 30 and onward episodes of season one", "example X will match") + "</p>";
|
||||||
tip += "<p>" + tr("Episode filter rules: ") + "</p><ul><li>" + tr("Season number is a mandatory non-zero value") + "</li>"
|
tip += "<p>" + tr("Episode filter rules: ") + "</p><ul><li>" + tr("Season number is a mandatory non-zero value") + "</li>"
|
||||||
|
@ -672,7 +673,7 @@ void AutomatedRssDownloader::updateFieldsToolTips(bool regex)
|
||||||
{
|
{
|
||||||
QString tip;
|
QString tip;
|
||||||
if (regex) {
|
if (regex) {
|
||||||
tip = "<p>" + tr("Regex mode: use Perl-like regular expressions") + "</p>";
|
tip = "<p>" + tr("Regex mode: use Perl-compatible regular expressions") + "</p>";
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
tip = "<p>" + tr("Wildcard mode: you can use") + "<ul>"
|
tip = "<p>" + tr("Wildcard mode: you can use") + "<ul>"
|
||||||
|
@ -698,17 +699,23 @@ void AutomatedRssDownloader::updateFieldsToolTips(bool regex)
|
||||||
void AutomatedRssDownloader::updateMustLineValidity()
|
void AutomatedRssDownloader::updateMustLineValidity()
|
||||||
{
|
{
|
||||||
const QString text = ui->lineContains->text();
|
const QString text = ui->lineContains->text();
|
||||||
|
bool isRegex = ui->checkRegex->isChecked();
|
||||||
bool valid = true;
|
bool valid = true;
|
||||||
|
QString error;
|
||||||
|
|
||||||
if (!text.isEmpty()) {
|
if (!text.isEmpty()) {
|
||||||
QStringList tokens;
|
QStringList tokens;
|
||||||
if (ui->checkRegex->isChecked())
|
if (isRegex)
|
||||||
tokens << text;
|
tokens << text;
|
||||||
else
|
else
|
||||||
tokens << text.split("|");
|
foreach (const QString &token, text.split("|"))
|
||||||
|
tokens << Utils::String::wildcardToRegex(token);
|
||||||
|
|
||||||
foreach (const QString &token, tokens) {
|
foreach (const QString &token, tokens) {
|
||||||
QRegExp reg(token, Qt::CaseInsensitive, ui->checkRegex->isChecked() ? QRegExp::RegExp : QRegExp::Wildcard);
|
QRegularExpression reg(token, QRegularExpression::CaseInsensitiveOption);
|
||||||
if (!reg.isValid()) {
|
if (!reg.isValid()) {
|
||||||
|
if (isRegex)
|
||||||
|
error = tr("Position %1: %2").arg(reg.patternErrorOffset()).arg(reg.errorString());
|
||||||
valid = false;
|
valid = false;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
@ -718,27 +725,35 @@ void AutomatedRssDownloader::updateMustLineValidity()
|
||||||
if (valid) {
|
if (valid) {
|
||||||
ui->lineContains->setStyleSheet("");
|
ui->lineContains->setStyleSheet("");
|
||||||
ui->lbl_must_stat->setPixmap(QPixmap());
|
ui->lbl_must_stat->setPixmap(QPixmap());
|
||||||
|
ui->lbl_must_stat->setToolTip(QLatin1String(""));
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
ui->lineContains->setStyleSheet("QLineEdit { color: #ff0000; }");
|
ui->lineContains->setStyleSheet("QLineEdit { color: #ff0000; }");
|
||||||
ui->lbl_must_stat->setPixmap(GuiIconProvider::instance()->getIcon("task-attention").pixmap(16, 16));
|
ui->lbl_must_stat->setPixmap(GuiIconProvider::instance()->getIcon("task-attention").pixmap(16, 16));
|
||||||
|
ui->lbl_must_stat->setToolTip(error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void AutomatedRssDownloader::updateMustNotLineValidity()
|
void AutomatedRssDownloader::updateMustNotLineValidity()
|
||||||
{
|
{
|
||||||
const QString text = ui->lineNotContains->text();
|
const QString text = ui->lineNotContains->text();
|
||||||
|
bool isRegex = ui->checkRegex->isChecked();
|
||||||
bool valid = true;
|
bool valid = true;
|
||||||
|
QString error;
|
||||||
|
|
||||||
if (!text.isEmpty()) {
|
if (!text.isEmpty()) {
|
||||||
QStringList tokens;
|
QStringList tokens;
|
||||||
if (ui->checkRegex->isChecked())
|
if (isRegex)
|
||||||
tokens << text;
|
tokens << text;
|
||||||
else
|
else
|
||||||
tokens << text.split("|");
|
foreach (const QString &token, text.split("|"))
|
||||||
|
tokens << Utils::String::wildcardToRegex(token);
|
||||||
|
|
||||||
foreach (const QString &token, tokens) {
|
foreach (const QString &token, tokens) {
|
||||||
QRegExp reg(token, Qt::CaseInsensitive, ui->checkRegex->isChecked() ? QRegExp::RegExp : QRegExp::Wildcard);
|
QRegularExpression reg(token, QRegularExpression::CaseInsensitiveOption);
|
||||||
if (!reg.isValid()) {
|
if (!reg.isValid()) {
|
||||||
|
if (isRegex)
|
||||||
|
error = tr("Position %1: %2").arg(reg.patternErrorOffset()).arg(reg.errorString());
|
||||||
valid = false;
|
valid = false;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
@ -748,17 +763,19 @@ void AutomatedRssDownloader::updateMustNotLineValidity()
|
||||||
if (valid) {
|
if (valid) {
|
||||||
ui->lineNotContains->setStyleSheet("");
|
ui->lineNotContains->setStyleSheet("");
|
||||||
ui->lbl_mustnot_stat->setPixmap(QPixmap());
|
ui->lbl_mustnot_stat->setPixmap(QPixmap());
|
||||||
|
ui->lbl_mustnot_stat->setToolTip(QLatin1String(""));
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
ui->lineNotContains->setStyleSheet("QLineEdit { color: #ff0000; }");
|
ui->lineNotContains->setStyleSheet("QLineEdit { color: #ff0000; }");
|
||||||
ui->lbl_mustnot_stat->setPixmap(GuiIconProvider::instance()->getIcon("task-attention").pixmap(16, 16));
|
ui->lbl_mustnot_stat->setPixmap(GuiIconProvider::instance()->getIcon("task-attention").pixmap(16, 16));
|
||||||
|
ui->lbl_mustnot_stat->setToolTip(error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void AutomatedRssDownloader::updateEpisodeFilterValidity()
|
void AutomatedRssDownloader::updateEpisodeFilterValidity()
|
||||||
{
|
{
|
||||||
const QString text = ui->lineEFilter->text();
|
const QString text = ui->lineEFilter->text();
|
||||||
bool valid = text.isEmpty() || m_episodeRegex->indexIn(text) != -1;
|
bool valid = text.isEmpty() || m_episodeRegex->match(text).hasMatch();
|
||||||
|
|
||||||
if (valid) {
|
if (valid) {
|
||||||
ui->lineEFilter->setStyleSheet("");
|
ui->lineEFilter->setStyleSheet("");
|
||||||
|
|
|
@ -34,7 +34,6 @@
|
||||||
#include <QDialog>
|
#include <QDialog>
|
||||||
#include <QHideEvent>
|
#include <QHideEvent>
|
||||||
#include <QPair>
|
#include <QPair>
|
||||||
#include <QRegExpValidator>
|
|
||||||
#include <QSet>
|
#include <QSet>
|
||||||
#include <QShortcut>
|
#include <QShortcut>
|
||||||
#include <QShowEvent>
|
#include <QShowEvent>
|
||||||
|
@ -58,6 +57,7 @@ namespace Rss
|
||||||
|
|
||||||
QT_BEGIN_NAMESPACE
|
QT_BEGIN_NAMESPACE
|
||||||
class QListWidgetItem;
|
class QListWidgetItem;
|
||||||
|
class QRegularExpression;
|
||||||
QT_END_NAMESPACE
|
QT_END_NAMESPACE
|
||||||
|
|
||||||
class AutomatedRssDownloader: public QDialog
|
class AutomatedRssDownloader: public QDialog
|
||||||
|
@ -113,7 +113,7 @@ private:
|
||||||
QListWidgetItem *m_editedRule;
|
QListWidgetItem *m_editedRule;
|
||||||
Rss::DownloadRuleList *m_ruleList;
|
Rss::DownloadRuleList *m_ruleList;
|
||||||
Rss::DownloadRuleList *m_editableRuleList;
|
Rss::DownloadRuleList *m_editableRuleList;
|
||||||
QRegExp *m_episodeRegex;
|
QRegularExpression *m_episodeRegex;
|
||||||
QShortcut *editHotkey;
|
QShortcut *editHotkey;
|
||||||
QShortcut *deleteHotkey;
|
QShortcut *deleteHotkey;
|
||||||
QSet<QPair<QString, QString >> m_treeListEntries;
|
QSet<QPair<QString, QString >> m_treeListEntries;
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue