Skip to content

Commit 98a1c11

Browse files
authored
Merge pull request #5287 from elFarto/master
Implement RSS Smart Filter
2 parents 47048d8 + 48cbccf commit 98a1c11

File tree

9 files changed

+233
-7
lines changed

9 files changed

+233
-7
lines changed

src/base/rss/rss_autodownloader.cpp

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,7 @@ const QString ConfFolderName(QStringLiteral("rss"));
6464
const QString RulesFileName(QStringLiteral("download_rules.json"));
6565

6666
const QString SettingsKey_ProcessingEnabled(QStringLiteral("RSS/AutoDownloader/EnableProcessing"));
67+
const QString SettingsKey_SmartEpisodeFilter(QStringLiteral("RSS/AutoDownloader/SmartEpisodeFilter"));
6768

6869
namespace
6970
{
@@ -95,6 +96,11 @@ using namespace RSS;
9596

9697
QPointer<AutoDownloader> AutoDownloader::m_instance = nullptr;
9798

99+
QString computeSmartFilterRegex(const QStringList &filters)
100+
{
101+
return QString("(?:_|\\b)(?:%1)(?:_|\\b)").arg(filters.join(QString(")|(?:")));
102+
}
103+
98104
AutoDownloader::AutoDownloader()
99105
: m_processingEnabled(SettingsStorage::instance()->loadValue(SettingsKey_ProcessingEnabled, false).toBool())
100106
, m_processingTimer(new QTimer(this))
@@ -123,6 +129,13 @@ AutoDownloader::AutoDownloader()
123129
connect(BitTorrent::Session::instance(), &BitTorrent::Session::downloadFromUrlFailed
124130
, this, &AutoDownloader::handleTorrentDownloadFailed);
125131

132+
// initialise the smart episode regex
133+
const QString regex = computeSmartFilterRegex(smartEpisodeFilters());
134+
m_smartEpisodeRegex = QRegularExpression(regex,
135+
QRegularExpression::CaseInsensitiveOption
136+
| QRegularExpression::ExtendedPatternSyntaxOption
137+
| QRegularExpression::UseUnicodePropertiesOption);
138+
126139
load();
127140

128141
m_processingTimer->setSingleShot(true);
@@ -266,6 +279,37 @@ void AutoDownloader::importRulesFromLegacyFormat(const QByteArray &data)
266279
insertRule(AutoDownloadRule::fromLegacyDict(val.toHash()));
267280
}
268281

282+
QStringList AutoDownloader::smartEpisodeFilters() const
283+
{
284+
const QVariant filtersSetting = SettingsStorage::instance()->loadValue(SettingsKey_SmartEpisodeFilter);
285+
286+
if (filtersSetting.isNull()) {
287+
QStringList filters = {
288+
"s(\\d+)e(\\d+)", // Format 1: s01e01
289+
"(\\d+)x(\\d+)", // Format 2: 01x01
290+
"(\\d{4}[.\\-]\\d{1,2}[.\\-]\\d{1,2})", // Format 3: 2017.01.01
291+
"(\\d{1,2}[.\\-]\\d{1,2}[.\\-]\\d{4})" // Format 4: 01.01.2017
292+
};
293+
294+
return filters;
295+
}
296+
297+
return filtersSetting.toStringList();
298+
}
299+
300+
QRegularExpression AutoDownloader::smartEpisodeRegex() const
301+
{
302+
return m_smartEpisodeRegex;
303+
}
304+
305+
void AutoDownloader::setSmartEpisodeFilters(const QStringList &filters)
306+
{
307+
SettingsStorage::instance()->storeValue(SettingsKey_SmartEpisodeFilter, filters);
308+
309+
const QString regex = computeSmartFilterRegex(filters);
310+
m_smartEpisodeRegex.setPattern(regex);
311+
}
312+
269313
void AutoDownloader::process()
270314
{
271315
if (m_processingQueue.isEmpty()) return; // processing was disabled
@@ -333,6 +377,8 @@ void AutoDownloader::processJob(const QSharedPointer<ProcessingJob> &job)
333377
}
334378

335379
rule.setLastMatch(articleDate);
380+
rule.appendLastComputedEpisode();
381+
336382
m_dirty = true;
337383
storeDeferred();
338384

src/base/rss/rss_autodownloader.h

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@
3535
#include <QList>
3636
#include <QObject>
3737
#include <QPointer>
38+
#include <QRegularExpression>
3839
#include <QSharedPointer>
3940

4041
class QThread;
@@ -80,6 +81,10 @@ namespace RSS
8081
bool isProcessingEnabled() const;
8182
void setProcessingEnabled(bool enabled);
8283

84+
QStringList smartEpisodeFilters() const;
85+
void setSmartEpisodeFilters(const QStringList &filters);
86+
QRegularExpression smartEpisodeRegex() const;
87+
8388
bool hasRule(const QString &ruleName) const;
8489
AutoDownloadRule ruleByName(const QString &ruleName) const;
8590
QList<AutoDownloadRule> rules() const;
@@ -132,5 +137,6 @@ namespace RSS
132137
QHash<QString, QSharedPointer<ProcessingJob>> m_waitingJobs;
133138
bool m_dirty = false;
134139
QBasicTimer m_savingTimer;
140+
QRegularExpression m_smartEpisodeRegex;
135141
};
136142
}

src/base/rss/rss_autodownloadrule.cpp

Lines changed: 94 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,6 @@
3434
#include <QHash>
3535
#include <QJsonArray>
3636
#include <QJsonObject>
37-
#include <QRegExp>
3837
#include <QRegularExpression>
3938
#include <QSharedData>
4039
#include <QString>
@@ -46,6 +45,7 @@
4645
#include "../utils/string.h"
4746
#include "rss_feed.h"
4847
#include "rss_article.h"
48+
#include "rss_autodownloader.h"
4949

5050
namespace
5151
{
@@ -100,6 +100,8 @@ const QString Str_AssignedCategory(QStringLiteral("assignedCategory"));
100100
const QString Str_LastMatch(QStringLiteral("lastMatch"));
101101
const QString Str_IgnoreDays(QStringLiteral("ignoreDays"));
102102
const QString Str_AddPaused(QStringLiteral("addPaused"));
103+
const QString Str_SmartFilter(QStringLiteral("smartFilter"));
104+
const QString Str_PreviouslyMatched(QStringLiteral("previouslyMatchedEpisodes"));
103105

104106
namespace RSS
105107
{
@@ -120,6 +122,10 @@ namespace RSS
120122
QString category;
121123
TriStateBool addPaused = TriStateBool::Undefined;
122124

125+
bool smartFilter = false;
126+
QStringList previouslyMatchedEpisodes;
127+
128+
mutable QString lastComputedEpisode;
123129
mutable QHash<QString, QRegularExpression> cachedRegexes;
124130

125131
bool operator==(const AutoDownloadRuleData &other) const
@@ -135,13 +141,38 @@ namespace RSS
135141
&& (lastMatch == other.lastMatch)
136142
&& (savePath == other.savePath)
137143
&& (category == other.category)
138-
&& (addPaused == other.addPaused);
144+
&& (addPaused == other.addPaused)
145+
&& (smartFilter == other.smartFilter);
139146
}
140147
};
141148
}
142149

143150
using namespace RSS;
144151

152+
QString computeEpisodeName(const QString &article)
153+
{
154+
const QRegularExpression episodeRegex = AutoDownloader::instance()->smartEpisodeRegex();
155+
const QRegularExpressionMatch match = episodeRegex.match(article);
156+
157+
// See if we can extract an season/episode number or date from the title
158+
if (!match.hasMatch())
159+
return QString();
160+
161+
QStringList ret;
162+
for (int i = 1; i <= match.lastCapturedIndex(); ++i) {
163+
QString cap = match.captured(i);
164+
165+
if (cap.isEmpty())
166+
continue;
167+
168+
bool isInt = false;
169+
int x = cap.toInt(&isInt);
170+
171+
ret.append(isInt ? QString::number(x) : cap);
172+
}
173+
return ret.join('x');
174+
}
175+
145176
AutoDownloadRule::AutoDownloadRule(const QString &name)
146177
: m_dataPtr(new AutoDownloadRuleData)
147178
{
@@ -197,6 +228,9 @@ bool AutoDownloadRule::matches(const QString &articleTitle, const QString &expre
197228

198229
bool AutoDownloadRule::matches(const QString &articleTitle) const
199230
{
231+
// Reset the lastComputedEpisode, we don't want to leak it between matches
232+
m_dataPtr->lastComputedEpisode.clear();
233+
200234
if (!m_dataPtr->mustContain.empty()) {
201235
bool logged = false;
202236
bool foundMustContain = false;
@@ -334,6 +368,20 @@ bool AutoDownloadRule::matches(const QString &articleTitle) const
334368
return false;
335369
}
336370

371+
if (useSmartFilter()) {
372+
// now see if this episode has been downloaded before
373+
const QString episodeStr = computeEpisodeName(articleTitle);
374+
375+
if (!episodeStr.isEmpty()) {
376+
bool previouslyMatched = m_dataPtr->previouslyMatchedEpisodes.contains(episodeStr);
377+
bool isRepack = articleTitle.contains("REPACK", Qt::CaseInsensitive) || articleTitle.contains("PROPER", Qt::CaseInsensitive);
378+
if (previouslyMatched && !isRepack)
379+
return false;
380+
381+
m_dataPtr->lastComputedEpisode = episodeStr;
382+
}
383+
}
384+
337385
// qDebug() << "Matched article:" << articleTitle;
338386
return true;
339387
}
@@ -367,7 +415,9 @@ QJsonObject AutoDownloadRule::toJsonObject() const
367415
, {Str_AssignedCategory, assignedCategory()}
368416
, {Str_LastMatch, lastMatch().toString(Qt::RFC2822Date)}
369417
, {Str_IgnoreDays, ignoreDays()}
370-
, {Str_AddPaused, triStateBoolToJsonValue(addPaused())}};
418+
, {Str_AddPaused, triStateBoolToJsonValue(addPaused())}
419+
, {Str_SmartFilter, useSmartFilter()}
420+
, {Str_PreviouslyMatched, QJsonArray::fromStringList(previouslyMatchedEpisodes())}};
371421
}
372422

373423
AutoDownloadRule AutoDownloadRule::fromJsonObject(const QJsonObject &jsonObj, const QString &name)
@@ -384,6 +434,7 @@ AutoDownloadRule AutoDownloadRule::fromJsonObject(const QJsonObject &jsonObj, co
384434
rule.setAddPaused(jsonValueToTriStateBool(jsonObj.value(Str_AddPaused)));
385435
rule.setLastMatch(QDateTime::fromString(jsonObj.value(Str_LastMatch).toString(), Qt::RFC2822Date));
386436
rule.setIgnoreDays(jsonObj.value(Str_IgnoreDays).toInt());
437+
rule.setUseSmartFilter(jsonObj.value(Str_SmartFilter).toBool(false));
387438

388439
const QJsonValue feedsVal = jsonObj.value(Str_AffectedFeeds);
389440
QStringList feedURLs;
@@ -393,6 +444,17 @@ AutoDownloadRule AutoDownloadRule::fromJsonObject(const QJsonObject &jsonObj, co
393444
feedURLs << urlVal.toString();
394445
rule.setFeedURLs(feedURLs);
395446

447+
const QJsonValue previouslyMatchedVal = jsonObj.value(Str_PreviouslyMatched);
448+
QStringList previouslyMatched;
449+
if (previouslyMatchedVal.isString()) {
450+
previouslyMatched << previouslyMatchedVal.toString();
451+
}
452+
else {
453+
foreach (const QJsonValue &val, previouslyMatchedVal.toArray())
454+
previouslyMatched << val.toString();
455+
}
456+
rule.setPreviouslyMatchedEpisodes(previouslyMatched);
457+
396458
return rule;
397459
}
398460

@@ -549,6 +611,16 @@ QString AutoDownloadRule::mustNotContain() const
549611
return m_dataPtr->mustNotContain.join("|");
550612
}
551613

614+
bool AutoDownloadRule::useSmartFilter() const
615+
{
616+
return m_dataPtr->smartFilter;
617+
}
618+
619+
void AutoDownloadRule::setUseSmartFilter(bool enabled)
620+
{
621+
m_dataPtr->smartFilter = enabled;
622+
}
623+
552624
bool AutoDownloadRule::useRegex() const
553625
{
554626
return m_dataPtr->useRegex;
@@ -560,6 +632,25 @@ void AutoDownloadRule::setUseRegex(bool enabled)
560632
m_dataPtr->cachedRegexes.clear();
561633
}
562634

635+
QStringList AutoDownloadRule::previouslyMatchedEpisodes() const
636+
{
637+
return m_dataPtr->previouslyMatchedEpisodes;
638+
}
639+
640+
void AutoDownloadRule::setPreviouslyMatchedEpisodes(const QStringList &previouslyMatchedEpisodes)
641+
{
642+
m_dataPtr->previouslyMatchedEpisodes = previouslyMatchedEpisodes;
643+
}
644+
645+
void AutoDownloadRule::appendLastComputedEpisode()
646+
{
647+
if (!m_dataPtr->lastComputedEpisode.isEmpty()) {
648+
// TODO: probably need to add a marker for PROPER/REPACK to avoid duplicate downloads
649+
m_dataPtr->previouslyMatchedEpisodes.append(m_dataPtr->lastComputedEpisode);
650+
m_dataPtr->lastComputedEpisode.clear();
651+
}
652+
}
653+
563654
QString AutoDownloadRule::episodeFilter() const
564655
{
565656
return m_dataPtr->episodeFilter;

src/base/rss/rss_autodownloadrule.h

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,9 +66,15 @@ namespace RSS
6666
void setLastMatch(const QDateTime &lastMatch);
6767
bool useRegex() const;
6868
void setUseRegex(bool enabled);
69+
bool useSmartFilter() const;
70+
void setUseSmartFilter(bool enabled);
6971
QString episodeFilter() const;
7072
void setEpisodeFilter(const QString &e);
7173

74+
void appendLastComputedEpisode();
75+
QStringList previouslyMatchedEpisodes() const;
76+
void setPreviouslyMatchedEpisodes(const QStringList &previouslyMatchedEpisodes);
77+
7278
QString savePath() const;
7379
void setSavePath(const QString &savePath);
7480
TriStateBool addPaused() const;

src/gui/optionsdlg.cpp

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -377,6 +377,7 @@ OptionsDialog::OptionsDialog(QWidget *parent)
377377
// RSS tab
378378
connect(m_ui->checkRSSEnable, &QCheckBox::toggled, this, &OptionsDialog::enableApplyButton);
379379
connect(m_ui->checkRSSAutoDownloaderEnable, &QCheckBox::toggled, this, &OptionsDialog::enableApplyButton);
380+
connect(m_ui->textSmartEpisodeFilters, &QPlainTextEdit::textChanged, this, &OptionsDialog::enableApplyButton);
380381
connect(m_ui->spinRSSRefreshInterval, qSpinBoxValueChanged, this, &OptionsDialog::enableApplyButton);
381382
connect(m_ui->spinRSSMaxArticlesPerFeed, qSpinBoxValueChanged, this, &OptionsDialog::enableApplyButton);
382383
connect(m_ui->btnEditRules, &QPushButton::clicked, [this]() { AutomatedRssDownloader(this).exec(); });
@@ -553,6 +554,7 @@ void OptionsDialog::saveOptions()
553554
RSS::Session::instance()->setMaxArticlesPerFeed(m_ui->spinRSSMaxArticlesPerFeed->value());
554555
RSS::Session::instance()->setProcessingEnabled(m_ui->checkRSSEnable->isChecked());
555556
RSS::AutoDownloader::instance()->setProcessingEnabled(m_ui->checkRSSAutoDownloaderEnable->isChecked());
557+
RSS::AutoDownloader::instance()->setSmartEpisodeFilters(m_ui->textSmartEpisodeFilters->toPlainText().split('\n', QString::SplitBehavior::SkipEmptyParts));
556558

557559
auto session = BitTorrent::Session::instance();
558560

@@ -780,6 +782,8 @@ void OptionsDialog::loadOptions()
780782

781783
m_ui->checkRSSEnable->setChecked(RSS::Session::instance()->isProcessingEnabled());
782784
m_ui->checkRSSAutoDownloaderEnable->setChecked(RSS::AutoDownloader::instance()->isProcessingEnabled());
785+
m_ui->textSmartEpisodeFilters->setPlainText(RSS::AutoDownloader::instance()->smartEpisodeFilters().join('\n'));
786+
783787
m_ui->spinRSSRefreshInterval->setValue(RSS::Session::instance()->refreshInterval());
784788
m_ui->spinRSSMaxArticlesPerFeed->setValue(RSS::Session::instance()->maxArticlesPerFeed());
785789

src/gui/optionsdlg.ui

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2699,6 +2699,18 @@
26992699
</layout>
27002700
</widget>
27012701
</item>
2702+
<item>
2703+
<widget class="QGroupBox" name="groupRSSSmartEpisodeFilter">
2704+
<property name="title">
2705+
<string>RSS Smart Episode Filters</string>
2706+
</property>
2707+
<layout class="QVBoxLayout" name="verticalLayout_31">
2708+
<item>
2709+
<widget class="QPlainTextEdit" name="textSmartEpisodeFilters"/>
2710+
</item>
2711+
</layout>
2712+
</widget>
2713+
</item>
27022714
<item>
27032715
<spacer name="verticalSpacer_5">
27042716
<property name="orientation">
@@ -2707,7 +2719,7 @@
27072719
<property name="sizeHint" stdset="0">
27082720
<size>
27092721
<width>20</width>
2710-
<height>267</height>
2722+
<height>200</height>
27112723
</size>
27122724
</property>
27132725
</spacer>

0 commit comments

Comments
 (0)