qBittorrent
rss_autodownloadrule.cpp
Go to the documentation of this file.
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 {
53  std::optional<bool> toOptionalBool(const QJsonValue &jsonVal)
54  {
55  if (jsonVal.isBool())
56  return jsonVal.toBool();
57 
58  return std::nullopt;
59  }
60 
61  QJsonValue toJsonValue(const std::optional<bool> boolValue)
62  {
63  return boolValue.has_value() ? *boolValue : QJsonValue {};
64  }
65 
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 
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 
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 
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;
142 
143  mutable QStringList lastComputedEpisodes;
144  mutable QHash<QString, QRegularExpression> cachedRegexes;
145 
146  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 
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 
194  : m_dataPtr(new AutoDownloadRuleData)
195 {
196  setName(name);
197 }
198 
200  : m_dataPtr(other.m_dataPtr)
201 {
202 }
203 
205 
206 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 &regex = m_dataPtr->cachedRegexes[expression];
214  if (regex.pattern().isEmpty())
215  {
216  const QString pattern = (isRegex ? expression : Utils::String::wildcardToRegexPattern(expression));
217  regex = QRegularExpression {pattern, QRegularExpression::CaseInsensitiveOption};
218  }
219 
220  return regex;
221 }
222 
223 bool AutoDownloadRule::matchesExpression(const QString &articleTitle, const QString &expression) const
224 {
225  const QRegularExpression whitespace {"\\s+"};
226 
227  if (expression.isEmpty())
228  {
229  // A regex of the form "expr|" will always match, so do the same for wildcards
230  return true;
231  }
232 
233  if (m_dataPtr->useRegex)
234  {
235  const QRegularExpression reg(cachedRegex(expression));
236  return reg.match(articleTitle).hasMatch();
237  }
238 
239  // Only match if every wildcard token (separated by spaces) is present in the article name.
240  // Order of wildcard tokens is unimportant (if order is important, they should have used *).
241  const QStringList wildcards {expression.split(whitespace, Qt::SkipEmptyParts)};
242  for (const QString &wildcard : wildcards)
243  {
244  const QRegularExpression reg {cachedRegex(wildcard, false)};
245  if (!reg.match(articleTitle).hasMatch())
246  return false;
247  }
248 
249  return true;
250 }
251 
252 bool AutoDownloadRule::matchesMustContainExpression(const QString &articleTitle) const
253 {
254  if (m_dataPtr->mustContain.empty())
255  return true;
256 
257  // Each expression is either a regex, or a set of wildcards separated by whitespace.
258  // Accept if any complete expression matches.
259  return std::any_of(m_dataPtr->mustContain.cbegin(), m_dataPtr->mustContain.cend(), [this, &articleTitle](const QString &expression)
260  {
261  // A regex of the form "expr|" will always match, so do the same for wildcards
262  return matchesExpression(articleTitle, expression);
263  });
264 }
265 
266 bool AutoDownloadRule::matchesMustNotContainExpression(const QString &articleTitle) const
267 {
268  if (m_dataPtr->mustNotContain.empty())
269  return true;
270 
271  // Each expression is either a regex, or a set of wildcards separated by whitespace.
272  // Reject if any complete expression matches.
273  return std::none_of(m_dataPtr->mustNotContain.cbegin(), m_dataPtr->mustNotContain.cend(), [this, &articleTitle](const QString &expression)
274  {
275  // A regex of the form "expr|" will always match, so do the same for wildcards
276  return matchesExpression(articleTitle, expression);
277  });
278 }
279 
280 bool AutoDownloadRule::matchesEpisodeFilterExpression(const QString &articleTitle) const
281 {
282  // Reset the lastComputedEpisode, we don't want to leak it between matches
283  m_dataPtr->lastComputedEpisodes.clear();
284 
285  if (m_dataPtr->episodeFilter.isEmpty())
286  return true;
287 
288  const QRegularExpression filterRegex {cachedRegex("(^\\d{1,4})x(.*;$)")};
289  const QRegularExpressionMatch matcher {filterRegex.match(m_dataPtr->episodeFilter)};
290  if (!matcher.hasMatch())
291  return false;
292 
293  const QString season {matcher.captured(1)};
294  const QStringList episodes {matcher.captured(2).split(';')};
295  const int seasonOurs {season.toInt()};
296 
297  for (QString episode : episodes)
298  {
299  if (episode.isEmpty())
300  continue;
301 
302  // We need to trim leading zeroes, but if it's all zeros then we want episode zero.
303  while ((episode.size() > 1) && episode.startsWith('0'))
304  episode = episode.right(episode.size() - 1);
305 
306  if (episode.indexOf('-') != -1)
307  { // Range detected
308  const QString partialPattern1 {"\\bs0?(\\d{1,4})[ -_\\.]?e(0?\\d{1,4})(?:\\D|\\b)"};
309  const QString partialPattern2 {"\\b(\\d{1,4})x(0?\\d{1,4})(?:\\D|\\b)"};
310 
311  // Extract partial match from article and compare as digits
312  QRegularExpressionMatch matcher = cachedRegex(partialPattern1).match(articleTitle);
313  bool matched = matcher.hasMatch();
314 
315  if (!matched)
316  {
317  matcher = cachedRegex(partialPattern2).match(articleTitle);
318  matched = matcher.hasMatch();
319  }
320 
321  if (matched)
322  {
323  const int seasonTheirs {matcher.captured(1).toInt()};
324  const int episodeTheirs {matcher.captured(2).toInt()};
325 
326  if (episode.endsWith('-'))
327  { // Infinite range
328  const int episodeOurs {QStringView(episode).left(episode.size() - 1).toInt()};
329  if (((seasonTheirs == seasonOurs) && (episodeTheirs >= episodeOurs)) || (seasonTheirs > seasonOurs))
330  return true;
331  }
332  else
333  { // Normal range
334  const QStringList range {episode.split('-')};
335  Q_ASSERT(range.size() == 2);
336  if (range.first().toInt() > range.last().toInt())
337  continue; // Ignore this subrule completely
338 
339  const int episodeOursFirst {range.first().toInt()};
340  const int episodeOursLast {range.last().toInt()};
341  if ((seasonTheirs == seasonOurs) && ((episodeOursFirst <= episodeTheirs) && (episodeOursLast >= episodeTheirs)))
342  return true;
343  }
344  }
345  }
346  else
347  { // Single number
348  const QString expStr {QString::fromLatin1("\\b(?:s0?%1[ -_\\.]?e0?%2|%1x0?%2)(?:\\D|\\b)").arg(season, episode)};
349  if (cachedRegex(expStr).match(articleTitle).hasMatch())
350  return true;
351  }
352  }
353 
354  return false;
355 }
356 
357 bool AutoDownloadRule::matchesSmartEpisodeFilter(const QString &articleTitle) const
358 {
359  if (!useSmartFilter())
360  return true;
361 
362  const QString episodeStr = computeEpisodeName(articleTitle);
363  if (episodeStr.isEmpty())
364  return true;
365 
366  // See if this episode has been downloaded before
367  const bool previouslyMatched = m_dataPtr->previouslyMatchedEpisodes.contains(episodeStr);
368  if (previouslyMatched)
369  {
370  if (!AutoDownloader::instance()->downloadRepacks())
371  return false;
372 
373  // Now see if we've downloaded this particular repack/proper combination
374  const bool isRepack = articleTitle.contains("REPACK", Qt::CaseInsensitive);
375  const bool isProper = articleTitle.contains("PROPER", Qt::CaseInsensitive);
376 
377  if (!isRepack && !isProper)
378  return false;
379 
380  const QString fullEpisodeStr = QString::fromLatin1("%1%2%3").arg(episodeStr,
381  isRepack ? "-REPACK" : "",
382  isProper ? "-PROPER" : "");
383  const bool previouslyMatchedFull = m_dataPtr->previouslyMatchedEpisodes.contains(fullEpisodeStr);
384  if (previouslyMatchedFull)
385  return false;
386 
387  m_dataPtr->lastComputedEpisodes.append(fullEpisodeStr);
388 
389  // If this is a REPACK and PROPER download, add the individual entries to the list
390  // so we don't download those
391  if (isRepack && isProper)
392  {
393  m_dataPtr->lastComputedEpisodes.append(episodeStr + QLatin1String("-REPACK"));
394  m_dataPtr->lastComputedEpisodes.append(episodeStr + QLatin1String("-PROPER"));
395  }
396  }
397 
398  m_dataPtr->lastComputedEpisodes.append(episodeStr);
399  return true;
400 }
401 
402 bool AutoDownloadRule::matches(const QVariantHash &articleData) const
403 {
404  const QDateTime articleDate {articleData[Article::KeyDate].toDateTime()};
405  if (ignoreDays() > 0)
406  {
407  if (lastMatch().isValid() && (articleDate < lastMatch().addDays(ignoreDays())))
408  return false;
409  }
410 
411  const QString articleTitle {articleData[Article::KeyTitle].toString()};
412  if (!matchesMustContainExpression(articleTitle))
413  return false;
414  if (!matchesMustNotContainExpression(articleTitle))
415  return false;
416  if (!matchesEpisodeFilterExpression(articleTitle))
417  return false;
418  if (!matchesSmartEpisodeFilter(articleTitle))
419  return false;
420 
421  return true;
422 }
423 
424 bool AutoDownloadRule::accepts(const QVariantHash &articleData)
425 {
426  if (!matches(articleData))
427  return false;
428 
429  setLastMatch(articleData[Article::KeyDate].toDateTime());
430 
431  // If there's a matched episode string, add that to the previously matched list
432  if (!m_dataPtr->lastComputedEpisodes.isEmpty())
433  {
434  m_dataPtr->previouslyMatchedEpisodes.append(m_dataPtr->lastComputedEpisodes);
435  m_dataPtr->lastComputedEpisodes.clear();
436  }
437 
438  return true;
439 }
440 
442 {
443  if (this != &other)
444  {
445  m_dataPtr = other.m_dataPtr;
446  }
447  return *this;
448 }
449 
451 {
452  return (m_dataPtr == other.m_dataPtr) // optimization
453  || (*m_dataPtr == *other.m_dataPtr);
454 }
455 
457 {
458  return !operator==(other);
459 }
460 
462 {
463  return {{Str_Enabled, isEnabled()}
464  , {Str_UseRegex, useRegex()}
468  , {Str_AffectedFeeds, QJsonArray::fromStringList(feedURLs())}
469  , {Str_SavePath, savePath()}
471  , {Str_LastMatch, lastMatch().toString(Qt::RFC2822Date)}
476  , {Str_PreviouslyMatched, QJsonArray::fromStringList(previouslyMatchedEpisodes())}};
477 }
478 
479 AutoDownloadRule AutoDownloadRule::fromJsonObject(const QJsonObject &jsonObj, const QString &name)
480 {
481  AutoDownloadRule rule(name.isEmpty() ? jsonObj.value(Str_Name).toString() : name);
482 
483  rule.setUseRegex(jsonObj.value(Str_UseRegex).toBool(false));
484  rule.setMustContain(jsonObj.value(Str_MustContain).toString());
485  rule.setMustNotContain(jsonObj.value(Str_MustNotContain).toString());
486  rule.setEpisodeFilter(jsonObj.value(Str_EpisodeFilter).toString());
487  rule.setEnabled(jsonObj.value(Str_Enabled).toBool(true));
488  rule.setSavePath(jsonObj.value(Str_SavePath).toString());
489  rule.setCategory(jsonObj.value(Str_AssignedCategory).toString());
490  rule.setAddPaused(toOptionalBool(jsonObj.value(Str_AddPaused)));
491 
492  // TODO: The following code is deprecated. Replace with the commented one after several releases in 4.4.x.
493  // === BEGIN DEPRECATED CODE === //
494  if (jsonObj.contains(Str_ContentLayout))
495  {
497  }
498  else
499  {
500  const std::optional<bool> createSubfolder = toOptionalBool(jsonObj.value(Str_CreateSubfolder));
501  std::optional<BitTorrent::TorrentContentLayout> contentLayout;
502  if (createSubfolder.has_value())
503  {
504  contentLayout = (*createSubfolder
505  ? BitTorrent::TorrentContentLayout::Original
506  : BitTorrent::TorrentContentLayout::NoSubfolder);
507  }
508 
509  rule.setTorrentContentLayout(contentLayout);
510  }
511  // === END DEPRECATED CODE === //
512  // === BEGIN REPLACEMENT CODE === //
513 // rule.setTorrentContentLayout(jsonValueToContentLayout(jsonObj.value(Str_ContentLayout)));
514  // === END REPLACEMENT CODE === //
515 
516  rule.setLastMatch(QDateTime::fromString(jsonObj.value(Str_LastMatch).toString(), Qt::RFC2822Date));
517  rule.setIgnoreDays(jsonObj.value(Str_IgnoreDays).toInt());
518  rule.setUseSmartFilter(jsonObj.value(Str_SmartFilter).toBool(false));
519 
520  const QJsonValue feedsVal = jsonObj.value(Str_AffectedFeeds);
521  QStringList feedURLs;
522  if (feedsVal.isString())
523  feedURLs << feedsVal.toString();
524  else for (const QJsonValue &urlVal : asConst(feedsVal.toArray()))
525  feedURLs << urlVal.toString();
526  rule.setFeedURLs(feedURLs);
527 
528  const QJsonValue previouslyMatchedVal = jsonObj.value(Str_PreviouslyMatched);
529  QStringList previouslyMatched;
530  if (previouslyMatchedVal.isString())
531  {
532  previouslyMatched << previouslyMatchedVal.toString();
533  }
534  else
535  {
536  for (const QJsonValue &val : asConst(previouslyMatchedVal.toArray()))
537  previouslyMatched << val.toString();
538  }
539  rule.setPreviouslyMatchedEpisodes(previouslyMatched);
540 
541  return rule;
542 }
543 
544 QVariantHash AutoDownloadRule::toLegacyDict() const
545 {
546  return {{"name", name()},
547  {"must_contain", mustContain()},
548  {"must_not_contain", mustNotContain()},
549  {"save_path", savePath()},
550  {"affected_feeds", feedURLs()},
551  {"enabled", isEnabled()},
552  {"category_assigned", assignedCategory()},
553  {"use_regex", useRegex()},
554  {"add_paused", toAddPausedLegacy(addPaused())},
555  {"episode_filter", episodeFilter()},
556  {"last_match", lastMatch()},
557  {"ignore_days", ignoreDays()}};
558 }
559 
561 {
562  AutoDownloadRule rule(dict.value("name").toString());
563 
564  rule.setUseRegex(dict.value("use_regex", false).toBool());
565  rule.setMustContain(dict.value("must_contain").toString());
566  rule.setMustNotContain(dict.value("must_not_contain").toString());
567  rule.setEpisodeFilter(dict.value("episode_filter").toString());
568  rule.setFeedURLs(dict.value("affected_feeds").toStringList());
569  rule.setEnabled(dict.value("enabled", false).toBool());
570  rule.setSavePath(dict.value("save_path").toString());
571  rule.setCategory(dict.value("category_assigned").toString());
572  rule.setAddPaused(addPausedLegacyToOptionalBool(dict.value("add_paused").toInt()));
573  rule.setLastMatch(dict.value("last_match").toDateTime());
574  rule.setIgnoreDays(dict.value("ignore_days").toInt());
575 
576  return rule;
577 }
578 
579 void AutoDownloadRule::setMustContain(const QString &tokens)
580 {
581  m_dataPtr->cachedRegexes.clear();
582 
583  if (m_dataPtr->useRegex)
584  m_dataPtr->mustContain = QStringList() << tokens;
585  else
586  m_dataPtr->mustContain = tokens.split('|');
587 
588  // Check for single empty string - if so, no condition
589  if ((m_dataPtr->mustContain.size() == 1) && m_dataPtr->mustContain[0].isEmpty())
590  m_dataPtr->mustContain.clear();
591 }
592 
593 void AutoDownloadRule::setMustNotContain(const QString &tokens)
594 {
595  m_dataPtr->cachedRegexes.clear();
596 
597  if (m_dataPtr->useRegex)
598  m_dataPtr->mustNotContain = QStringList() << tokens;
599  else
600  m_dataPtr->mustNotContain = tokens.split('|');
601 
602  // Check for single empty string - if so, no condition
603  if ((m_dataPtr->mustNotContain.size() == 1) && m_dataPtr->mustNotContain[0].isEmpty())
604  m_dataPtr->mustNotContain.clear();
605 }
606 
607 QStringList AutoDownloadRule::feedURLs() const
608 {
609  return m_dataPtr->feedURLs;
610 }
611 
612 void AutoDownloadRule::setFeedURLs(const QStringList &urls)
613 {
614  m_dataPtr->feedURLs = urls;
615 }
616 
617 QString AutoDownloadRule::name() const
618 {
619  return m_dataPtr->name;
620 }
621 
622 void AutoDownloadRule::setName(const QString &name)
623 {
624  m_dataPtr->name = name;
625 }
626 
628 {
629  return m_dataPtr->savePath;
630 }
631 
632 void AutoDownloadRule::setSavePath(const QString &savePath)
633 {
635 }
636 
637 std::optional<bool> AutoDownloadRule::addPaused() const
638 {
639  return m_dataPtr->addPaused;
640 }
641 
642 void AutoDownloadRule::setAddPaused(const std::optional<bool> addPaused)
643 {
644  m_dataPtr->addPaused = addPaused;
645 }
646 
647 std::optional<BitTorrent::TorrentContentLayout> AutoDownloadRule::torrentContentLayout() const
648 {
649  return m_dataPtr->contentLayout;
650 }
651 
652 void AutoDownloadRule::setTorrentContentLayout(const std::optional<BitTorrent::TorrentContentLayout> contentLayout)
653 {
654  m_dataPtr->contentLayout = contentLayout;
655 }
656 
658 {
659  return m_dataPtr->category;
660 }
661 
662 void AutoDownloadRule::setCategory(const QString &category)
663 {
664  m_dataPtr->category = category;
665 }
666 
668 {
669  return m_dataPtr->enabled;
670 }
671 
672 void AutoDownloadRule::setEnabled(const bool enable)
673 {
674  m_dataPtr->enabled = enable;
675 }
676 
678 {
679  return m_dataPtr->lastMatch;
680 }
681 
682 void AutoDownloadRule::setLastMatch(const QDateTime &lastMatch)
683 {
684  m_dataPtr->lastMatch = lastMatch;
685 }
686 
688 {
689  m_dataPtr->ignoreDays = d;
690 }
691 
693 {
694  return m_dataPtr->ignoreDays;
695 }
696 
698 {
699  return m_dataPtr->mustContain.join('|');
700 }
701 
703 {
704  return m_dataPtr->mustNotContain.join('|');
705 }
706 
708 {
709  return m_dataPtr->smartFilter;
710 }
711 
712 void AutoDownloadRule::setUseSmartFilter(const bool enabled)
713 {
714  m_dataPtr->smartFilter = enabled;
715 }
716 
718 {
719  return m_dataPtr->useRegex;
720 }
721 
722 void AutoDownloadRule::setUseRegex(const bool enabled)
723 {
724  m_dataPtr->useRegex = enabled;
725  m_dataPtr->cachedRegexes.clear();
726 }
727 
729 {
730  return m_dataPtr->previouslyMatchedEpisodes;
731 }
732 
733 void AutoDownloadRule::setPreviouslyMatchedEpisodes(const QStringList &previouslyMatchedEpisodes)
734 {
735  m_dataPtr->previouslyMatchedEpisodes = previouslyMatchedEpisodes;
736 }
737 
739 {
740  return m_dataPtr->episodeFilter;
741 }
742 
744 {
745  m_dataPtr->episodeFilter = e;
746  m_dataPtr->cachedRegexes.clear();
747 }
static const QString KeyDate
Definition: rss_article.h:54
static const QString KeyTitle
Definition: rss_article.h:55
void setTorrentContentLayout(std::optional< BitTorrent::TorrentContentLayout > contentLayout)
void setEpisodeFilter(const QString &e)
QStringList previouslyMatchedEpisodes() const
std::optional< BitTorrent::TorrentContentLayout > torrentContentLayout() const
bool accepts(const QVariantHash &articleData)
void setPreviouslyMatchedEpisodes(const QStringList &previouslyMatchedEpisodes)
bool matchesEpisodeFilterExpression(const QString &articleTitle) const
bool matchesSmartEpisodeFilter(const QString &articleTitle) const
QJsonObject toJsonObject() const
void setLastMatch(const QDateTime &lastMatch)
bool operator==(const AutoDownloadRule &other) const
void setUseRegex(bool enabled)
void setMustContain(const QString &tokens)
static AutoDownloadRule fromJsonObject(const QJsonObject &jsonObj, const QString &name="")
QVariantHash toLegacyDict() const
void setSavePath(const QString &savePath)
AutoDownloadRule(const QString &name="")
QSharedDataPointer< AutoDownloadRuleData > m_dataPtr
std::optional< bool > addPaused() const
void setCategory(const QString &category)
AutoDownloadRule & operator=(const AutoDownloadRule &other)
void setUseSmartFilter(bool enabled)
static AutoDownloadRule fromLegacyDict(const QVariantHash &dict)
bool matchesMustNotContainExpression(const QString &articleTitle) const
void setFeedURLs(const QStringList &urls)
void setName(const QString &name)
QStringList feedURLs() const
bool matchesExpression(const QString &articleTitle, const QString &expression) const
bool operator!=(const AutoDownloadRule &other) const
bool matches(const QVariantHash &articleData) const
bool matchesMustContainExpression(const QString &articleTitle) const
QRegularExpression cachedRegex(const QString &expression, bool isRegex=true) const
void setMustNotContain(const QString &tokens)
void setAddPaused(std::optional< bool > addPaused)
static AutoDownloader * instance()
QRegularExpression smartEpisodeRegex() const
constexpr std::add_const_t< T > & asConst(T &t) noexcept
Definition: global.h:42
QString toUniformPath(const QString &path)
Definition: fs.cpp:69
QString fromEnum(const T &value)
Definition: string.h:67
T toEnum(const QString &serializedValue, const T &defaultValue)
Definition: string.h:77
QString wildcardToRegexPattern(const QString &pattern)
Definition: string.cpp:57
std::optional< bool > toOptionalBool(const QJsonValue &jsonVal)
int toAddPausedLegacy(const std::optional< bool > boolValue)
QJsonValue contentLayoutToJsonValue(const std::optional< BitTorrent::TorrentContentLayout > contentLayout)
std::optional< BitTorrent::TorrentContentLayout > jsonValueToContentLayout(const QJsonValue &jsonVal)
std::optional< bool > addPausedLegacyToOptionalBool(const int val)
QJsonValue toJsonValue(const std::optional< bool > boolValue)
const QString Str_SavePath(QStringLiteral("savePath"))
const QString Str_UseRegex(QStringLiteral("useRegex"))
QString computeEpisodeName(const QString &article)
const QString Str_AffectedFeeds(QStringLiteral("affectedFeeds"))
const QString Str_EpisodeFilter(QStringLiteral("episodeFilter"))
const QString Str_CreateSubfolder(QStringLiteral("createSubfolder"))
const QString Str_MustNotContain(QStringLiteral("mustNotContain"))
const QString Str_PreviouslyMatched(QStringLiteral("previouslyMatchedEpisodes"))
const QString Str_IgnoreDays(QStringLiteral("ignoreDays"))
const QString Str_ContentLayout(QStringLiteral("torrentContentLayout"))
const QString Str_MustContain(QStringLiteral("mustContain"))
const QString Str_SmartFilter(QStringLiteral("smartFilter"))
const QString Str_AssignedCategory(QStringLiteral("assignedCategory"))
const QString Str_AddPaused(QStringLiteral("addPaused"))
const QString Str_LastMatch(QStringLiteral("lastMatch"))
const QString Str_Name(QStringLiteral("name"))
const QString Str_Enabled(QStringLiteral("enabled"))
QHash< QString, QRegularExpression > cachedRegexes
std::optional< bool > addPaused
bool operator==(const AutoDownloadRuleData &other) const
std::optional< BitTorrent::TorrentContentLayout > contentLayout