mirror of
https://github.com/qbittorrent/qBittorrent.git
synced 2026-01-02 13:48:05 -06:00
Merge pull request #5287 from elFarto/master
Implement RSS Smart Filter
This commit is contained in:
@@ -64,6 +64,7 @@ const QString ConfFolderName(QStringLiteral("rss"));
|
||||
const QString RulesFileName(QStringLiteral("download_rules.json"));
|
||||
|
||||
const QString SettingsKey_ProcessingEnabled(QStringLiteral("RSS/AutoDownloader/EnableProcessing"));
|
||||
const QString SettingsKey_SmartEpisodeFilter(QStringLiteral("RSS/AutoDownloader/SmartEpisodeFilter"));
|
||||
|
||||
namespace
|
||||
{
|
||||
@@ -95,6 +96,11 @@ using namespace RSS;
|
||||
|
||||
QPointer<AutoDownloader> AutoDownloader::m_instance = nullptr;
|
||||
|
||||
QString computeSmartFilterRegex(const QStringList &filters)
|
||||
{
|
||||
return QString("(?:_|\\b)(?:%1)(?:_|\\b)").arg(filters.join(QString(")|(?:")));
|
||||
}
|
||||
|
||||
AutoDownloader::AutoDownloader()
|
||||
: m_processingEnabled(SettingsStorage::instance()->loadValue(SettingsKey_ProcessingEnabled, false).toBool())
|
||||
, m_processingTimer(new QTimer(this))
|
||||
@@ -123,6 +129,13 @@ AutoDownloader::AutoDownloader()
|
||||
connect(BitTorrent::Session::instance(), &BitTorrent::Session::downloadFromUrlFailed
|
||||
, this, &AutoDownloader::handleTorrentDownloadFailed);
|
||||
|
||||
// initialise the smart episode regex
|
||||
const QString regex = computeSmartFilterRegex(smartEpisodeFilters());
|
||||
m_smartEpisodeRegex = QRegularExpression(regex,
|
||||
QRegularExpression::CaseInsensitiveOption
|
||||
| QRegularExpression::ExtendedPatternSyntaxOption
|
||||
| QRegularExpression::UseUnicodePropertiesOption);
|
||||
|
||||
load();
|
||||
|
||||
m_processingTimer->setSingleShot(true);
|
||||
@@ -266,6 +279,37 @@ void AutoDownloader::importRulesFromLegacyFormat(const QByteArray &data)
|
||||
insertRule(AutoDownloadRule::fromLegacyDict(val.toHash()));
|
||||
}
|
||||
|
||||
QStringList AutoDownloader::smartEpisodeFilters() const
|
||||
{
|
||||
const QVariant filtersSetting = SettingsStorage::instance()->loadValue(SettingsKey_SmartEpisodeFilter);
|
||||
|
||||
if (filtersSetting.isNull()) {
|
||||
QStringList filters = {
|
||||
"s(\\d+)e(\\d+)", // Format 1: s01e01
|
||||
"(\\d+)x(\\d+)", // Format 2: 01x01
|
||||
"(\\d{4}[.\\-]\\d{1,2}[.\\-]\\d{1,2})", // Format 3: 2017.01.01
|
||||
"(\\d{1,2}[.\\-]\\d{1,2}[.\\-]\\d{4})" // Format 4: 01.01.2017
|
||||
};
|
||||
|
||||
return filters;
|
||||
}
|
||||
|
||||
return filtersSetting.toStringList();
|
||||
}
|
||||
|
||||
QRegularExpression AutoDownloader::smartEpisodeRegex() const
|
||||
{
|
||||
return m_smartEpisodeRegex;
|
||||
}
|
||||
|
||||
void AutoDownloader::setSmartEpisodeFilters(const QStringList &filters)
|
||||
{
|
||||
SettingsStorage::instance()->storeValue(SettingsKey_SmartEpisodeFilter, filters);
|
||||
|
||||
const QString regex = computeSmartFilterRegex(filters);
|
||||
m_smartEpisodeRegex.setPattern(regex);
|
||||
}
|
||||
|
||||
void AutoDownloader::process()
|
||||
{
|
||||
if (m_processingQueue.isEmpty()) return; // processing was disabled
|
||||
@@ -333,6 +377,8 @@ void AutoDownloader::processJob(const QSharedPointer<ProcessingJob> &job)
|
||||
}
|
||||
|
||||
rule.setLastMatch(articleDate);
|
||||
rule.appendLastComputedEpisode();
|
||||
|
||||
m_dirty = true;
|
||||
storeDeferred();
|
||||
|
||||
|
||||
@@ -35,6 +35,7 @@
|
||||
#include <QList>
|
||||
#include <QObject>
|
||||
#include <QPointer>
|
||||
#include <QRegularExpression>
|
||||
#include <QSharedPointer>
|
||||
|
||||
class QThread;
|
||||
@@ -80,6 +81,10 @@ namespace RSS
|
||||
bool isProcessingEnabled() const;
|
||||
void setProcessingEnabled(bool enabled);
|
||||
|
||||
QStringList smartEpisodeFilters() const;
|
||||
void setSmartEpisodeFilters(const QStringList &filters);
|
||||
QRegularExpression smartEpisodeRegex() const;
|
||||
|
||||
bool hasRule(const QString &ruleName) const;
|
||||
AutoDownloadRule ruleByName(const QString &ruleName) const;
|
||||
QList<AutoDownloadRule> rules() const;
|
||||
@@ -132,5 +137,6 @@ namespace RSS
|
||||
QHash<QString, QSharedPointer<ProcessingJob>> m_waitingJobs;
|
||||
bool m_dirty = false;
|
||||
QBasicTimer m_savingTimer;
|
||||
QRegularExpression m_smartEpisodeRegex;
|
||||
};
|
||||
}
|
||||
|
||||
@@ -34,7 +34,6 @@
|
||||
#include <QHash>
|
||||
#include <QJsonArray>
|
||||
#include <QJsonObject>
|
||||
#include <QRegExp>
|
||||
#include <QRegularExpression>
|
||||
#include <QSharedData>
|
||||
#include <QString>
|
||||
@@ -46,6 +45,7 @@
|
||||
#include "../utils/string.h"
|
||||
#include "rss_feed.h"
|
||||
#include "rss_article.h"
|
||||
#include "rss_autodownloader.h"
|
||||
|
||||
namespace
|
||||
{
|
||||
@@ -100,6 +100,8 @@ const QString Str_AssignedCategory(QStringLiteral("assignedCategory"));
|
||||
const QString Str_LastMatch(QStringLiteral("lastMatch"));
|
||||
const QString Str_IgnoreDays(QStringLiteral("ignoreDays"));
|
||||
const QString Str_AddPaused(QStringLiteral("addPaused"));
|
||||
const QString Str_SmartFilter(QStringLiteral("smartFilter"));
|
||||
const QString Str_PreviouslyMatched(QStringLiteral("previouslyMatchedEpisodes"));
|
||||
|
||||
namespace RSS
|
||||
{
|
||||
@@ -120,6 +122,10 @@ namespace RSS
|
||||
QString category;
|
||||
TriStateBool addPaused = TriStateBool::Undefined;
|
||||
|
||||
bool smartFilter = false;
|
||||
QStringList previouslyMatchedEpisodes;
|
||||
|
||||
mutable QString lastComputedEpisode;
|
||||
mutable QHash<QString, QRegularExpression> cachedRegexes;
|
||||
|
||||
bool operator==(const AutoDownloadRuleData &other) const
|
||||
@@ -135,13 +141,38 @@ namespace RSS
|
||||
&& (lastMatch == other.lastMatch)
|
||||
&& (savePath == other.savePath)
|
||||
&& (category == other.category)
|
||||
&& (addPaused == other.addPaused);
|
||||
&& (addPaused == other.addPaused)
|
||||
&& (smartFilter == other.smartFilter);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
using namespace RSS;
|
||||
|
||||
QString computeEpisodeName(const QString &article)
|
||||
{
|
||||
const QRegularExpression episodeRegex = AutoDownloader::instance()->smartEpisodeRegex();
|
||||
const QRegularExpressionMatch match = episodeRegex.match(article);
|
||||
|
||||
// See if we can extract an season/episode number or date from the title
|
||||
if (!match.hasMatch())
|
||||
return QString();
|
||||
|
||||
QStringList ret;
|
||||
for (int i = 1; i <= match.lastCapturedIndex(); ++i) {
|
||||
QString cap = match.captured(i);
|
||||
|
||||
if (cap.isEmpty())
|
||||
continue;
|
||||
|
||||
bool isInt = false;
|
||||
int x = cap.toInt(&isInt);
|
||||
|
||||
ret.append(isInt ? QString::number(x) : cap);
|
||||
}
|
||||
return ret.join('x');
|
||||
}
|
||||
|
||||
AutoDownloadRule::AutoDownloadRule(const QString &name)
|
||||
: m_dataPtr(new AutoDownloadRuleData)
|
||||
{
|
||||
@@ -197,6 +228,9 @@ bool AutoDownloadRule::matches(const QString &articleTitle, const QString &expre
|
||||
|
||||
bool AutoDownloadRule::matches(const QString &articleTitle) const
|
||||
{
|
||||
// Reset the lastComputedEpisode, we don't want to leak it between matches
|
||||
m_dataPtr->lastComputedEpisode.clear();
|
||||
|
||||
if (!m_dataPtr->mustContain.empty()) {
|
||||
bool logged = false;
|
||||
bool foundMustContain = false;
|
||||
@@ -334,6 +368,20 @@ bool AutoDownloadRule::matches(const QString &articleTitle) const
|
||||
return false;
|
||||
}
|
||||
|
||||
if (useSmartFilter()) {
|
||||
// now see if this episode has been downloaded before
|
||||
const QString episodeStr = computeEpisodeName(articleTitle);
|
||||
|
||||
if (!episodeStr.isEmpty()) {
|
||||
bool previouslyMatched = m_dataPtr->previouslyMatchedEpisodes.contains(episodeStr);
|
||||
bool isRepack = articleTitle.contains("REPACK", Qt::CaseInsensitive) || articleTitle.contains("PROPER", Qt::CaseInsensitive);
|
||||
if (previouslyMatched && !isRepack)
|
||||
return false;
|
||||
|
||||
m_dataPtr->lastComputedEpisode = episodeStr;
|
||||
}
|
||||
}
|
||||
|
||||
// qDebug() << "Matched article:" << articleTitle;
|
||||
return true;
|
||||
}
|
||||
@@ -367,7 +415,9 @@ QJsonObject AutoDownloadRule::toJsonObject() const
|
||||
, {Str_AssignedCategory, assignedCategory()}
|
||||
, {Str_LastMatch, lastMatch().toString(Qt::RFC2822Date)}
|
||||
, {Str_IgnoreDays, ignoreDays()}
|
||||
, {Str_AddPaused, triStateBoolToJsonValue(addPaused())}};
|
||||
, {Str_AddPaused, triStateBoolToJsonValue(addPaused())}
|
||||
, {Str_SmartFilter, useSmartFilter()}
|
||||
, {Str_PreviouslyMatched, QJsonArray::fromStringList(previouslyMatchedEpisodes())}};
|
||||
}
|
||||
|
||||
AutoDownloadRule AutoDownloadRule::fromJsonObject(const QJsonObject &jsonObj, const QString &name)
|
||||
@@ -384,6 +434,7 @@ AutoDownloadRule AutoDownloadRule::fromJsonObject(const QJsonObject &jsonObj, co
|
||||
rule.setAddPaused(jsonValueToTriStateBool(jsonObj.value(Str_AddPaused)));
|
||||
rule.setLastMatch(QDateTime::fromString(jsonObj.value(Str_LastMatch).toString(), Qt::RFC2822Date));
|
||||
rule.setIgnoreDays(jsonObj.value(Str_IgnoreDays).toInt());
|
||||
rule.setUseSmartFilter(jsonObj.value(Str_SmartFilter).toBool(false));
|
||||
|
||||
const QJsonValue feedsVal = jsonObj.value(Str_AffectedFeeds);
|
||||
QStringList feedURLs;
|
||||
@@ -393,6 +444,17 @@ AutoDownloadRule AutoDownloadRule::fromJsonObject(const QJsonObject &jsonObj, co
|
||||
feedURLs << urlVal.toString();
|
||||
rule.setFeedURLs(feedURLs);
|
||||
|
||||
const QJsonValue previouslyMatchedVal = jsonObj.value(Str_PreviouslyMatched);
|
||||
QStringList previouslyMatched;
|
||||
if (previouslyMatchedVal.isString()) {
|
||||
previouslyMatched << previouslyMatchedVal.toString();
|
||||
}
|
||||
else {
|
||||
foreach (const QJsonValue &val, previouslyMatchedVal.toArray())
|
||||
previouslyMatched << val.toString();
|
||||
}
|
||||
rule.setPreviouslyMatchedEpisodes(previouslyMatched);
|
||||
|
||||
return rule;
|
||||
}
|
||||
|
||||
@@ -549,6 +611,16 @@ QString AutoDownloadRule::mustNotContain() const
|
||||
return m_dataPtr->mustNotContain.join("|");
|
||||
}
|
||||
|
||||
bool AutoDownloadRule::useSmartFilter() const
|
||||
{
|
||||
return m_dataPtr->smartFilter;
|
||||
}
|
||||
|
||||
void AutoDownloadRule::setUseSmartFilter(bool enabled)
|
||||
{
|
||||
m_dataPtr->smartFilter = enabled;
|
||||
}
|
||||
|
||||
bool AutoDownloadRule::useRegex() const
|
||||
{
|
||||
return m_dataPtr->useRegex;
|
||||
@@ -560,6 +632,25 @@ void AutoDownloadRule::setUseRegex(bool enabled)
|
||||
m_dataPtr->cachedRegexes.clear();
|
||||
}
|
||||
|
||||
QStringList AutoDownloadRule::previouslyMatchedEpisodes() const
|
||||
{
|
||||
return m_dataPtr->previouslyMatchedEpisodes;
|
||||
}
|
||||
|
||||
void AutoDownloadRule::setPreviouslyMatchedEpisodes(const QStringList &previouslyMatchedEpisodes)
|
||||
{
|
||||
m_dataPtr->previouslyMatchedEpisodes = previouslyMatchedEpisodes;
|
||||
}
|
||||
|
||||
void AutoDownloadRule::appendLastComputedEpisode()
|
||||
{
|
||||
if (!m_dataPtr->lastComputedEpisode.isEmpty()) {
|
||||
// TODO: probably need to add a marker for PROPER/REPACK to avoid duplicate downloads
|
||||
m_dataPtr->previouslyMatchedEpisodes.append(m_dataPtr->lastComputedEpisode);
|
||||
m_dataPtr->lastComputedEpisode.clear();
|
||||
}
|
||||
}
|
||||
|
||||
QString AutoDownloadRule::episodeFilter() const
|
||||
{
|
||||
return m_dataPtr->episodeFilter;
|
||||
|
||||
@@ -66,9 +66,15 @@ namespace RSS
|
||||
void setLastMatch(const QDateTime &lastMatch);
|
||||
bool useRegex() const;
|
||||
void setUseRegex(bool enabled);
|
||||
bool useSmartFilter() const;
|
||||
void setUseSmartFilter(bool enabled);
|
||||
QString episodeFilter() const;
|
||||
void setEpisodeFilter(const QString &e);
|
||||
|
||||
void appendLastComputedEpisode();
|
||||
QStringList previouslyMatchedEpisodes() const;
|
||||
void setPreviouslyMatchedEpisodes(const QStringList &previouslyMatchedEpisodes);
|
||||
|
||||
QString savePath() const;
|
||||
void setSavePath(const QString &savePath);
|
||||
TriStateBool addPaused() const;
|
||||
|
||||
Reference in New Issue
Block a user