1 /*
2 * Bittorrent Client using Qt and libtorrent.
3 * Copyright (C) 2017 Vladimir Golovnev <glassez@yandex.ru>
4 * Copyright (C) 2010 Christophe Dumez <chris@qbittorrent.org>
5 *
6 * This program is free software; you can redistribute it and/or
7 * modify it under the terms of the GNU General Public License
8 * as published by the Free Software Foundation; either version 2
9 * of the License, or (at your option) any later version.
10 *
11 * This program is distributed in the hope that it will be useful,
12 * but WITHOUT ANY WARRANTY; without even the implied warranty of
13 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 * GNU General Public License for more details.
15 *
16 * You should have received a copy of the GNU General Public License
17 * along with this program; if not, write to the Free Software
18 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
19 *
20 * In addition, as a special exception, the copyright holders give permission to
21 * link this program with the OpenSSL project's "OpenSSL" library (or with
22 * modified versions of it that use the same license as the "OpenSSL" library),
23 * and distribute the linked executables. You must obey the GNU General Public
24 * License in all respects for all of the code used other than "OpenSSL". If you
25 * modify file(s), you may extend this exception to your version of the file(s),
26 * but you are not obligated to do so. If you do not wish to do so, delete this
27 * exception statement from your version.
28 */
29
30 #include "rss_autodownloadrule.h"
31
32 #include <algorithm>
33
34 #include <QDebug>
35 #include <QHash>
36 #include <QJsonArray>
37 #include <QJsonObject>
38 #include <QRegularExpression>
39 #include <QSharedData>
40 #include <QString>
41 #include <QStringList>
42
43 #include "base/global.h"
44 #include "base/preferences.h"
45 #include "base/utils/fs.h"
46 #include "base/utils/string.h"
47 #include "rss_article.h"
48 #include "rss_autodownloader.h"
49 #include "rss_feed.h"
50
51 namespace
52 {
toOptionalBool(const QJsonValue & jsonVal)53 std::optional<bool> toOptionalBool(const QJsonValue &jsonVal)
54 {
55 if (jsonVal.isBool())
56 return jsonVal.toBool();
57
58 return std::nullopt;
59 }
60
toJsonValue(const std::optional<bool> boolValue)61 QJsonValue toJsonValue(const std::optional<bool> boolValue)
62 {
63 return boolValue.has_value() ? *boolValue : QJsonValue {};
64 }
65
addPausedLegacyToOptionalBool(const int val)66 std::optional<bool> addPausedLegacyToOptionalBool(const int val)
67 {
68 switch (val)
69 {
70 case 1:
71 return true; // always
72 case 2:
73 return false; // never
74 default:
75 return std::nullopt; // default
76 }
77 }
78
toAddPausedLegacy(const std::optional<bool> boolValue)79 int toAddPausedLegacy(const std::optional<bool> boolValue)
80 {
81 if (!boolValue.has_value())
82 return 0; // default
83
84 return (*boolValue ? 1 /* always */ : 2 /* never */);
85 }
86
jsonValueToContentLayout(const QJsonValue & jsonVal)87 std::optional<BitTorrent::TorrentContentLayout> jsonValueToContentLayout(const QJsonValue &jsonVal)
88 {
89 const QString str = jsonVal.toString();
90 if (str.isEmpty())
91 return std::nullopt;
92 return Utils::String::toEnum(str, BitTorrent::TorrentContentLayout::Original);
93 }
94
contentLayoutToJsonValue(const std::optional<BitTorrent::TorrentContentLayout> contentLayout)95 QJsonValue contentLayoutToJsonValue(const std::optional<BitTorrent::TorrentContentLayout> contentLayout)
96 {
97 if (!contentLayout)
98 return {};
99 return Utils::String::fromEnum(*contentLayout);
100 }
101 }
102
103 const QString Str_Name(QStringLiteral("name"));
104 const QString Str_Enabled(QStringLiteral("enabled"));
105 const QString Str_UseRegex(QStringLiteral("useRegex"));
106 const QString Str_MustContain(QStringLiteral("mustContain"));
107 const QString Str_MustNotContain(QStringLiteral("mustNotContain"));
108 const QString Str_EpisodeFilter(QStringLiteral("episodeFilter"));
109 const QString Str_AffectedFeeds(QStringLiteral("affectedFeeds"));
110 const QString Str_SavePath(QStringLiteral("savePath"));
111 const QString Str_AssignedCategory(QStringLiteral("assignedCategory"));
112 const QString Str_LastMatch(QStringLiteral("lastMatch"));
113 const QString Str_IgnoreDays(QStringLiteral("ignoreDays"));
114 const QString Str_AddPaused(QStringLiteral("addPaused"));
115 const QString Str_CreateSubfolder(QStringLiteral("createSubfolder"));
116 const QString Str_ContentLayout(QStringLiteral("torrentContentLayout"));
117 const QString Str_SmartFilter(QStringLiteral("smartFilter"));
118 const QString Str_PreviouslyMatched(QStringLiteral("previouslyMatchedEpisodes"));
119
120 namespace RSS
121 {
122 struct AutoDownloadRuleData : public QSharedData
123 {
124 QString name;
125 bool enabled = true;
126
127 QStringList mustContain;
128 QStringList mustNotContain;
129 QString episodeFilter;
130 QStringList feedURLs;
131 bool useRegex = false;
132 int ignoreDays = 0;
133 QDateTime lastMatch;
134
135 QString savePath;
136 QString category;
137 std::optional<bool> addPaused;
138 std::optional<BitTorrent::TorrentContentLayout> contentLayout;
139
140 bool smartFilter = false;
141 QStringList previouslyMatchedEpisodes;
142
143 mutable QStringList lastComputedEpisodes;
144 mutable QHash<QString, QRegularExpression> cachedRegexes;
145
operator ==RSS::AutoDownloadRuleData146 bool operator==(const AutoDownloadRuleData &other) const
147 {
148 return (name == other.name)
149 && (enabled == other.enabled)
150 && (mustContain == other.mustContain)
151 && (mustNotContain == other.mustNotContain)
152 && (episodeFilter == other.episodeFilter)
153 && (feedURLs == other.feedURLs)
154 && (useRegex == other.useRegex)
155 && (ignoreDays == other.ignoreDays)
156 && (lastMatch == other.lastMatch)
157 && (savePath == other.savePath)
158 && (category == other.category)
159 && (addPaused == other.addPaused)
160 && (contentLayout == other.contentLayout)
161 && (smartFilter == other.smartFilter);
162 }
163 };
164 }
165
166 using namespace RSS;
167
computeEpisodeName(const QString & article)168 QString computeEpisodeName(const QString &article)
169 {
170 const QRegularExpression episodeRegex = AutoDownloader::instance()->smartEpisodeRegex();
171 const QRegularExpressionMatch match = episodeRegex.match(article);
172
173 // See if we can extract an season/episode number or date from the title
174 if (!match.hasMatch())
175 return {};
176
177 QStringList ret;
178 for (int i = 1; i <= match.lastCapturedIndex(); ++i)
179 {
180 const QString cap = match.captured(i);
181
182 if (cap.isEmpty())
183 continue;
184
185 bool isInt = false;
186 const int x = cap.toInt(&isInt);
187
188 ret.append(isInt ? QString::number(x) : cap);
189 }
190 return ret.join('x');
191 }
192
AutoDownloadRule(const QString & name)193 AutoDownloadRule::AutoDownloadRule(const QString &name)
194 : m_dataPtr(new AutoDownloadRuleData)
195 {
196 setName(name);
197 }
198
AutoDownloadRule(const AutoDownloadRule & other)199 AutoDownloadRule::AutoDownloadRule(const AutoDownloadRule &other)
200 : m_dataPtr(other.m_dataPtr)
201 {
202 }
203
~AutoDownloadRule()204 AutoDownloadRule::~AutoDownloadRule() {}
205
cachedRegex(const QString & expression,const bool isRegex) const206 QRegularExpression AutoDownloadRule::cachedRegex(const QString &expression, const bool isRegex) const
207 {
208 // Use a cache of regexes so we don't have to continually recompile - big performance increase.
209 // The cache is cleared whenever the regex/wildcard, must or must not contain fields or
210 // episode filter are modified.
211 Q_ASSERT(!expression.isEmpty());
212
213 QRegularExpression ®ex = m_dataPtr->cachedRegexes[expression];
214 if (regex.pattern().isEmpty())
215 {
216 regex = QRegularExpression
217 {
218 (isRegex ? expression : Utils::String::wildcardToRegex(expression))
219 , QRegularExpression::CaseInsensitiveOption};
220 }
221
222 return regex;
223 }
224
matchesExpression(const QString & articleTitle,const QString & expression) const225 bool AutoDownloadRule::matchesExpression(const QString &articleTitle, const QString &expression) const
226 {
227 const QRegularExpression whitespace {"\\s+"};
228
229 if (expression.isEmpty())
230 {
231 // A regex of the form "expr|" will always match, so do the same for wildcards
232 return true;
233 }
234
235 if (m_dataPtr->useRegex)
236 {
237 const QRegularExpression reg(cachedRegex(expression));
238 return reg.match(articleTitle).hasMatch();
239 }
240
241 // Only match if every wildcard token (separated by spaces) is present in the article name.
242 // Order of wildcard tokens is unimportant (if order is important, they should have used *).
243 const QStringList wildcards {expression.split(whitespace, QString::SplitBehavior::SkipEmptyParts)};
244 for (const QString &wildcard : wildcards)
245 {
246 const QRegularExpression reg {cachedRegex(wildcard, false)};
247 if (!reg.match(articleTitle).hasMatch())
248 return false;
249 }
250
251 return true;
252 }
253
matchesMustContainExpression(const QString & articleTitle) const254 bool AutoDownloadRule::matchesMustContainExpression(const QString &articleTitle) const
255 {
256 if (m_dataPtr->mustContain.empty())
257 return true;
258
259 // Each expression is either a regex, or a set of wildcards separated by whitespace.
260 // Accept if any complete expression matches.
261 return std::any_of(m_dataPtr->mustContain.cbegin(), m_dataPtr->mustContain.cend(), [this, &articleTitle](const QString &expression)
262 {
263 // A regex of the form "expr|" will always match, so do the same for wildcards
264 return matchesExpression(articleTitle, expression);
265 });
266 }
267
matchesMustNotContainExpression(const QString & articleTitle) const268 bool AutoDownloadRule::matchesMustNotContainExpression(const QString &articleTitle) const
269 {
270 if (m_dataPtr->mustNotContain.empty())
271 return true;
272
273 // Each expression is either a regex, or a set of wildcards separated by whitespace.
274 // Reject if any complete expression matches.
275 return std::none_of(m_dataPtr->mustNotContain.cbegin(), m_dataPtr->mustNotContain.cend(), [this, &articleTitle](const QString &expression)
276 {
277 // A regex of the form "expr|" will always match, so do the same for wildcards
278 return matchesExpression(articleTitle, expression);
279 });
280 }
281
matchesEpisodeFilterExpression(const QString & articleTitle) const282 bool AutoDownloadRule::matchesEpisodeFilterExpression(const QString &articleTitle) const
283 {
284 // Reset the lastComputedEpisode, we don't want to leak it between matches
285 m_dataPtr->lastComputedEpisodes.clear();
286
287 if (m_dataPtr->episodeFilter.isEmpty())
288 return true;
289
290 const QRegularExpression filterRegex {cachedRegex("(^\\d{1,4})x(.*;$)")};
291 const QRegularExpressionMatch matcher {filterRegex.match(m_dataPtr->episodeFilter)};
292 if (!matcher.hasMatch())
293 return false;
294
295 const QString season {matcher.captured(1)};
296 const QStringList episodes {matcher.captured(2).split(';')};
297 const int seasonOurs {season.toInt()};
298
299 for (QString episode : episodes)
300 {
301 if (episode.isEmpty())
302 continue;
303
304 // We need to trim leading zeroes, but if it's all zeros then we want episode zero.
305 while ((episode.size() > 1) && episode.startsWith('0'))
306 episode = episode.right(episode.size() - 1);
307
308 if (episode.indexOf('-') != -1)
309 { // Range detected
310 const QString partialPattern1 {"\\bs0?(\\d{1,4})[ -_\\.]?e(0?\\d{1,4})(?:\\D|\\b)"};
311 const QString partialPattern2 {"\\b(\\d{1,4})x(0?\\d{1,4})(?:\\D|\\b)"};
312
313 // Extract partial match from article and compare as digits
314 QRegularExpressionMatch matcher = cachedRegex(partialPattern1).match(articleTitle);
315 bool matched = matcher.hasMatch();
316
317 if (!matched)
318 {
319 matcher = cachedRegex(partialPattern2).match(articleTitle);
320 matched = matcher.hasMatch();
321 }
322
323 if (matched)
324 {
325 const int seasonTheirs {matcher.captured(1).toInt()};
326 const int episodeTheirs {matcher.captured(2).toInt()};
327
328 if (episode.endsWith('-'))
329 { // Infinite range
330 const int episodeOurs {episode.leftRef(episode.size() - 1).toInt()};
331 if (((seasonTheirs == seasonOurs) && (episodeTheirs >= episodeOurs)) || (seasonTheirs > seasonOurs))
332 return true;
333 }
334 else
335 { // Normal range
336 const QStringList range {episode.split('-')};
337 Q_ASSERT(range.size() == 2);
338 if (range.first().toInt() > range.last().toInt())
339 continue; // Ignore this subrule completely
340
341 const int episodeOursFirst {range.first().toInt()};
342 const int episodeOursLast {range.last().toInt()};
343 if ((seasonTheirs == seasonOurs) && ((episodeOursFirst <= episodeTheirs) && (episodeOursLast >= episodeTheirs)))
344 return true;
345 }
346 }
347 }
348 else
349 { // Single number
350 const QString expStr {QString::fromLatin1("\\b(?:s0?%1[ -_\\.]?e0?%2|%1x0?%2)(?:\\D|\\b)").arg(season, episode)};
351 if (cachedRegex(expStr).match(articleTitle).hasMatch())
352 return true;
353 }
354 }
355
356 return false;
357 }
358
matchesSmartEpisodeFilter(const QString & articleTitle) const359 bool AutoDownloadRule::matchesSmartEpisodeFilter(const QString &articleTitle) const
360 {
361 if (!useSmartFilter())
362 return true;
363
364 const QString episodeStr = computeEpisodeName(articleTitle);
365 if (episodeStr.isEmpty())
366 return true;
367
368 // See if this episode has been downloaded before
369 const bool previouslyMatched = m_dataPtr->previouslyMatchedEpisodes.contains(episodeStr);
370 if (previouslyMatched)
371 {
372 if (!AutoDownloader::instance()->downloadRepacks())
373 return false;
374
375 // Now see if we've downloaded this particular repack/proper combination
376 const bool isRepack = articleTitle.contains("REPACK", Qt::CaseInsensitive);
377 const bool isProper = articleTitle.contains("PROPER", Qt::CaseInsensitive);
378
379 if (!isRepack && !isProper)
380 return false;
381
382 const QString fullEpisodeStr = QString::fromLatin1("%1%2%3").arg(episodeStr,
383 isRepack ? "-REPACK" : "",
384 isProper ? "-PROPER" : "");
385 const bool previouslyMatchedFull = m_dataPtr->previouslyMatchedEpisodes.contains(fullEpisodeStr);
386 if (previouslyMatchedFull)
387 return false;
388
389 m_dataPtr->lastComputedEpisodes.append(fullEpisodeStr);
390
391 // If this is a REPACK and PROPER download, add the individual entries to the list
392 // so we don't download those
393 if (isRepack && isProper)
394 {
395 m_dataPtr->lastComputedEpisodes.append(episodeStr + QLatin1String("-REPACK"));
396 m_dataPtr->lastComputedEpisodes.append(episodeStr + QLatin1String("-PROPER"));
397 }
398 }
399
400 m_dataPtr->lastComputedEpisodes.append(episodeStr);
401 return true;
402 }
403
matches(const QVariantHash & articleData) const404 bool AutoDownloadRule::matches(const QVariantHash &articleData) const
405 {
406 const QDateTime articleDate {articleData[Article::KeyDate].toDateTime()};
407 if (ignoreDays() > 0)
408 {
409 if (lastMatch().isValid() && (articleDate < lastMatch().addDays(ignoreDays())))
410 return false;
411 }
412
413 const QString articleTitle {articleData[Article::KeyTitle].toString()};
414 if (!matchesMustContainExpression(articleTitle))
415 return false;
416 if (!matchesMustNotContainExpression(articleTitle))
417 return false;
418 if (!matchesEpisodeFilterExpression(articleTitle))
419 return false;
420 if (!matchesSmartEpisodeFilter(articleTitle))
421 return false;
422
423 return true;
424 }
425
accepts(const QVariantHash & articleData)426 bool AutoDownloadRule::accepts(const QVariantHash &articleData)
427 {
428 if (!matches(articleData))
429 return false;
430
431 setLastMatch(articleData[Article::KeyDate].toDateTime());
432
433 // If there's a matched episode string, add that to the previously matched list
434 if (!m_dataPtr->lastComputedEpisodes.isEmpty())
435 {
436 m_dataPtr->previouslyMatchedEpisodes.append(m_dataPtr->lastComputedEpisodes);
437 m_dataPtr->lastComputedEpisodes.clear();
438 }
439
440 return true;
441 }
442
operator =(const AutoDownloadRule & other)443 AutoDownloadRule &AutoDownloadRule::operator=(const AutoDownloadRule &other)
444 {
445 if (this != &other)
446 {
447 m_dataPtr = other.m_dataPtr;
448 }
449 return *this;
450 }
451
operator ==(const AutoDownloadRule & other) const452 bool AutoDownloadRule::operator==(const AutoDownloadRule &other) const
453 {
454 return (m_dataPtr == other.m_dataPtr) // optimization
455 || (*m_dataPtr == *other.m_dataPtr);
456 }
457
operator !=(const AutoDownloadRule & other) const458 bool AutoDownloadRule::operator!=(const AutoDownloadRule &other) const
459 {
460 return !operator==(other);
461 }
462
toJsonObject() const463 QJsonObject AutoDownloadRule::toJsonObject() const
464 {
465 return {{Str_Enabled, isEnabled()}
466 , {Str_UseRegex, useRegex()}
467 , {Str_MustContain, mustContain()}
468 , {Str_MustNotContain, mustNotContain()}
469 , {Str_EpisodeFilter, episodeFilter()}
470 , {Str_AffectedFeeds, QJsonArray::fromStringList(feedURLs())}
471 , {Str_SavePath, savePath()}
472 , {Str_AssignedCategory, assignedCategory()}
473 , {Str_LastMatch, lastMatch().toString(Qt::RFC2822Date)}
474 , {Str_IgnoreDays, ignoreDays()}
475 , {Str_AddPaused, toJsonValue(addPaused())}
476 , {Str_ContentLayout, contentLayoutToJsonValue(torrentContentLayout())}
477 , {Str_SmartFilter, useSmartFilter()}
478 , {Str_PreviouslyMatched, QJsonArray::fromStringList(previouslyMatchedEpisodes())}};
479 }
480
fromJsonObject(const QJsonObject & jsonObj,const QString & name)481 AutoDownloadRule AutoDownloadRule::fromJsonObject(const QJsonObject &jsonObj, const QString &name)
482 {
483 AutoDownloadRule rule(name.isEmpty() ? jsonObj.value(Str_Name).toString() : name);
484
485 rule.setUseRegex(jsonObj.value(Str_UseRegex).toBool(false));
486 rule.setMustContain(jsonObj.value(Str_MustContain).toString());
487 rule.setMustNotContain(jsonObj.value(Str_MustNotContain).toString());
488 rule.setEpisodeFilter(jsonObj.value(Str_EpisodeFilter).toString());
489 rule.setEnabled(jsonObj.value(Str_Enabled).toBool(true));
490 rule.setSavePath(jsonObj.value(Str_SavePath).toString());
491 rule.setCategory(jsonObj.value(Str_AssignedCategory).toString());
492 rule.setAddPaused(toOptionalBool(jsonObj.value(Str_AddPaused)));
493
494 // TODO: The following code is deprecated. Replace with the commented one after several releases in 4.4.x.
495 // === BEGIN DEPRECATED CODE === //
496 if (jsonObj.contains(Str_ContentLayout))
497 {
498 rule.setTorrentContentLayout(jsonValueToContentLayout(jsonObj.value(Str_ContentLayout)));
499 }
500 else
501 {
502 const std::optional<bool> createSubfolder = toOptionalBool(jsonObj.value(Str_CreateSubfolder));
503 std::optional<BitTorrent::TorrentContentLayout> contentLayout;
504 if (createSubfolder.has_value())
505 {
506 contentLayout = (*createSubfolder
507 ? BitTorrent::TorrentContentLayout::Original
508 : BitTorrent::TorrentContentLayout::NoSubfolder);
509 }
510
511 rule.setTorrentContentLayout(contentLayout);
512 }
513 // === END DEPRECATED CODE === //
514 // === BEGIN REPLACEMENT CODE === //
515 // rule.setTorrentContentLayout(jsonValueToContentLayout(jsonObj.value(Str_ContentLayout)));
516 // === END REPLACEMENT CODE === //
517
518 rule.setLastMatch(QDateTime::fromString(jsonObj.value(Str_LastMatch).toString(), Qt::RFC2822Date));
519 rule.setIgnoreDays(jsonObj.value(Str_IgnoreDays).toInt());
520 rule.setUseSmartFilter(jsonObj.value(Str_SmartFilter).toBool(false));
521
522 const QJsonValue feedsVal = jsonObj.value(Str_AffectedFeeds);
523 QStringList feedURLs;
524 if (feedsVal.isString())
525 feedURLs << feedsVal.toString();
526 else for (const QJsonValue &urlVal : asConst(feedsVal.toArray()))
527 feedURLs << urlVal.toString();
528 rule.setFeedURLs(feedURLs);
529
530 const QJsonValue previouslyMatchedVal = jsonObj.value(Str_PreviouslyMatched);
531 QStringList previouslyMatched;
532 if (previouslyMatchedVal.isString())
533 {
534 previouslyMatched << previouslyMatchedVal.toString();
535 }
536 else
537 {
538 for (const QJsonValue &val : asConst(previouslyMatchedVal.toArray()))
539 previouslyMatched << val.toString();
540 }
541 rule.setPreviouslyMatchedEpisodes(previouslyMatched);
542
543 return rule;
544 }
545
toLegacyDict() const546 QVariantHash AutoDownloadRule::toLegacyDict() const
547 {
548 return {{"name", name()},
549 {"must_contain", mustContain()},
550 {"must_not_contain", mustNotContain()},
551 {"save_path", savePath()},
552 {"affected_feeds", feedURLs()},
553 {"enabled", isEnabled()},
554 {"category_assigned", assignedCategory()},
555 {"use_regex", useRegex()},
556 {"add_paused", toAddPausedLegacy(addPaused())},
557 {"episode_filter", episodeFilter()},
558 {"last_match", lastMatch()},
559 {"ignore_days", ignoreDays()}};
560 }
561
fromLegacyDict(const QVariantHash & dict)562 AutoDownloadRule AutoDownloadRule::fromLegacyDict(const QVariantHash &dict)
563 {
564 AutoDownloadRule rule(dict.value("name").toString());
565
566 rule.setUseRegex(dict.value("use_regex", false).toBool());
567 rule.setMustContain(dict.value("must_contain").toString());
568 rule.setMustNotContain(dict.value("must_not_contain").toString());
569 rule.setEpisodeFilter(dict.value("episode_filter").toString());
570 rule.setFeedURLs(dict.value("affected_feeds").toStringList());
571 rule.setEnabled(dict.value("enabled", false).toBool());
572 rule.setSavePath(dict.value("save_path").toString());
573 rule.setCategory(dict.value("category_assigned").toString());
574 rule.setAddPaused(addPausedLegacyToOptionalBool(dict.value("add_paused").toInt()));
575 rule.setLastMatch(dict.value("last_match").toDateTime());
576 rule.setIgnoreDays(dict.value("ignore_days").toInt());
577
578 return rule;
579 }
580
setMustContain(const QString & tokens)581 void AutoDownloadRule::setMustContain(const QString &tokens)
582 {
583 m_dataPtr->cachedRegexes.clear();
584
585 if (m_dataPtr->useRegex)
586 m_dataPtr->mustContain = QStringList() << tokens;
587 else
588 m_dataPtr->mustContain = tokens.split('|');
589
590 // Check for single empty string - if so, no condition
591 if ((m_dataPtr->mustContain.size() == 1) && m_dataPtr->mustContain[0].isEmpty())
592 m_dataPtr->mustContain.clear();
593 }
594
setMustNotContain(const QString & tokens)595 void AutoDownloadRule::setMustNotContain(const QString &tokens)
596 {
597 m_dataPtr->cachedRegexes.clear();
598
599 if (m_dataPtr->useRegex)
600 m_dataPtr->mustNotContain = QStringList() << tokens;
601 else
602 m_dataPtr->mustNotContain = tokens.split('|');
603
604 // Check for single empty string - if so, no condition
605 if ((m_dataPtr->mustNotContain.size() == 1) && m_dataPtr->mustNotContain[0].isEmpty())
606 m_dataPtr->mustNotContain.clear();
607 }
608
feedURLs() const609 QStringList AutoDownloadRule::feedURLs() const
610 {
611 return m_dataPtr->feedURLs;
612 }
613
setFeedURLs(const QStringList & urls)614 void AutoDownloadRule::setFeedURLs(const QStringList &urls)
615 {
616 m_dataPtr->feedURLs = urls;
617 }
618
name() const619 QString AutoDownloadRule::name() const
620 {
621 return m_dataPtr->name;
622 }
623
setName(const QString & name)624 void AutoDownloadRule::setName(const QString &name)
625 {
626 m_dataPtr->name = name;
627 }
628
savePath() const629 QString AutoDownloadRule::savePath() const
630 {
631 return m_dataPtr->savePath;
632 }
633
setSavePath(const QString & savePath)634 void AutoDownloadRule::setSavePath(const QString &savePath)
635 {
636 m_dataPtr->savePath = Utils::Fs::toUniformPath(savePath);
637 }
638
addPaused() const639 std::optional<bool> AutoDownloadRule::addPaused() const
640 {
641 return m_dataPtr->addPaused;
642 }
643
setAddPaused(const std::optional<bool> addPaused)644 void AutoDownloadRule::setAddPaused(const std::optional<bool> addPaused)
645 {
646 m_dataPtr->addPaused = addPaused;
647 }
648
torrentContentLayout() const649 std::optional<BitTorrent::TorrentContentLayout> AutoDownloadRule::torrentContentLayout() const
650 {
651 return m_dataPtr->contentLayout;
652 }
653
setTorrentContentLayout(const std::optional<BitTorrent::TorrentContentLayout> contentLayout)654 void AutoDownloadRule::setTorrentContentLayout(const std::optional<BitTorrent::TorrentContentLayout> contentLayout)
655 {
656 m_dataPtr->contentLayout = contentLayout;
657 }
658
assignedCategory() const659 QString AutoDownloadRule::assignedCategory() const
660 {
661 return m_dataPtr->category;
662 }
663
setCategory(const QString & category)664 void AutoDownloadRule::setCategory(const QString &category)
665 {
666 m_dataPtr->category = category;
667 }
668
isEnabled() const669 bool AutoDownloadRule::isEnabled() const
670 {
671 return m_dataPtr->enabled;
672 }
673
setEnabled(const bool enable)674 void AutoDownloadRule::setEnabled(const bool enable)
675 {
676 m_dataPtr->enabled = enable;
677 }
678
lastMatch() const679 QDateTime AutoDownloadRule::lastMatch() const
680 {
681 return m_dataPtr->lastMatch;
682 }
683
setLastMatch(const QDateTime & lastMatch)684 void AutoDownloadRule::setLastMatch(const QDateTime &lastMatch)
685 {
686 m_dataPtr->lastMatch = lastMatch;
687 }
688
setIgnoreDays(const int d)689 void AutoDownloadRule::setIgnoreDays(const int d)
690 {
691 m_dataPtr->ignoreDays = d;
692 }
693
ignoreDays() const694 int AutoDownloadRule::ignoreDays() const
695 {
696 return m_dataPtr->ignoreDays;
697 }
698
mustContain() const699 QString AutoDownloadRule::mustContain() const
700 {
701 return m_dataPtr->mustContain.join('|');
702 }
703
mustNotContain() const704 QString AutoDownloadRule::mustNotContain() const
705 {
706 return m_dataPtr->mustNotContain.join('|');
707 }
708
useSmartFilter() const709 bool AutoDownloadRule::useSmartFilter() const
710 {
711 return m_dataPtr->smartFilter;
712 }
713
setUseSmartFilter(const bool enabled)714 void AutoDownloadRule::setUseSmartFilter(const bool enabled)
715 {
716 m_dataPtr->smartFilter = enabled;
717 }
718
useRegex() const719 bool AutoDownloadRule::useRegex() const
720 {
721 return m_dataPtr->useRegex;
722 }
723
setUseRegex(const bool enabled)724 void AutoDownloadRule::setUseRegex(const bool enabled)
725 {
726 m_dataPtr->useRegex = enabled;
727 m_dataPtr->cachedRegexes.clear();
728 }
729
previouslyMatchedEpisodes() const730 QStringList AutoDownloadRule::previouslyMatchedEpisodes() const
731 {
732 return m_dataPtr->previouslyMatchedEpisodes;
733 }
734
setPreviouslyMatchedEpisodes(const QStringList & previouslyMatchedEpisodes)735 void AutoDownloadRule::setPreviouslyMatchedEpisodes(const QStringList &previouslyMatchedEpisodes)
736 {
737 m_dataPtr->previouslyMatchedEpisodes = previouslyMatchedEpisodes;
738 }
739
episodeFilter() const740 QString AutoDownloadRule::episodeFilter() const
741 {
742 return m_dataPtr->episodeFilter;
743 }
744
setEpisodeFilter(const QString & e)745 void AutoDownloadRule::setEpisodeFilter(const QString &e)
746 {
747 m_dataPtr->episodeFilter = e;
748 m_dataPtr->cachedRegexes.clear();
749 }
750