37 #include <QJsonObject>
38 #include <QRegularExpression>
39 #include <QSharedData>
41 #include <QStringList>
56 return jsonVal.toBool();
63 return boolValue.has_value() ? *boolValue : QJsonValue {};
81 if (!boolValue.has_value())
84 return (*boolValue ? 1 : 2 );
89 const QString str = jsonVal.toString();
171 const QRegularExpressionMatch match = episodeRegex.match(article);
174 if (!match.hasMatch())
178 for (
int i = 1; i <= match.lastCapturedIndex(); ++i)
180 const QString cap = match.captured(i);
186 const int x = cap.toInt(&isInt);
188 ret.append(isInt ? QString::number(x) : cap);
190 return ret.join(
'x');
200 : m_dataPtr(other.m_dataPtr)
211 Q_ASSERT(!expression.isEmpty());
213 QRegularExpression ®ex =
m_dataPtr->cachedRegexes[expression];
214 if (regex.pattern().isEmpty())
217 regex = QRegularExpression {pattern, QRegularExpression::CaseInsensitiveOption};
225 const QRegularExpression whitespace {
"\\s+"};
227 if (expression.isEmpty())
235 const QRegularExpression reg(
cachedRegex(expression));
236 return reg.match(articleTitle).hasMatch();
241 const QStringList wildcards {expression.split(whitespace, Qt::SkipEmptyParts)};
242 for (
const QString &wildcard : wildcards)
244 const QRegularExpression reg {
cachedRegex(wildcard,
false)};
245 if (!reg.match(articleTitle).hasMatch())
259 return std::any_of(
m_dataPtr->mustContain.cbegin(),
m_dataPtr->mustContain.cend(), [
this, &articleTitle](
const QString &expression)
262 return matchesExpression(articleTitle, expression);
273 return std::none_of(
m_dataPtr->mustNotContain.cbegin(),
m_dataPtr->mustNotContain.cend(), [
this, &articleTitle](
const QString &expression)
276 return matchesExpression(articleTitle, expression);
288 const QRegularExpression filterRegex {
cachedRegex(
"(^\\d{1,4})x(.*;$)")};
289 const QRegularExpressionMatch matcher {filterRegex.match(
m_dataPtr->episodeFilter)};
290 if (!matcher.hasMatch())
293 const QString season {matcher.captured(1)};
294 const QStringList episodes {matcher.captured(2).split(
';')};
295 const int seasonOurs {season.toInt()};
297 for (QString episode : episodes)
299 if (episode.isEmpty())
303 while ((episode.size() > 1) && episode.startsWith(
'0'))
304 episode = episode.right(episode.size() - 1);
306 if (episode.indexOf(
'-') != -1)
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)"};
312 QRegularExpressionMatch matcher =
cachedRegex(partialPattern1).match(articleTitle);
313 bool matched = matcher.hasMatch();
317 matcher =
cachedRegex(partialPattern2).match(articleTitle);
318 matched = matcher.hasMatch();
323 const int seasonTheirs {matcher.captured(1).toInt()};
324 const int episodeTheirs {matcher.captured(2).toInt()};
326 if (episode.endsWith(
'-'))
328 const int episodeOurs {QStringView(episode).left(episode.size() - 1).toInt()};
329 if (((seasonTheirs == seasonOurs) && (episodeTheirs >= episodeOurs)) || (seasonTheirs > seasonOurs))
334 const QStringList range {episode.split(
'-')};
335 Q_ASSERT(range.size() == 2);
336 if (range.first().toInt() > range.last().toInt())
339 const int episodeOursFirst {range.first().toInt()};
340 const int episodeOursLast {range.last().toInt()};
341 if ((seasonTheirs == seasonOurs) && ((episodeOursFirst <= episodeTheirs) && (episodeOursLast >= episodeTheirs)))
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())
363 if (episodeStr.isEmpty())
367 const bool previouslyMatched =
m_dataPtr->previouslyMatchedEpisodes.contains(episodeStr);
368 if (previouslyMatched)
374 const bool isRepack = articleTitle.contains(
"REPACK", Qt::CaseInsensitive);
375 const bool isProper = articleTitle.contains(
"PROPER", Qt::CaseInsensitive);
377 if (!isRepack && !isProper)
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)
387 m_dataPtr->lastComputedEpisodes.append(fullEpisodeStr);
391 if (isRepack && isProper)
393 m_dataPtr->lastComputedEpisodes.append(episodeStr + QLatin1String(
"-REPACK"));
394 m_dataPtr->lastComputedEpisodes.append(episodeStr + QLatin1String(
"-PROPER"));
398 m_dataPtr->lastComputedEpisodes.append(episodeStr);
432 if (!
m_dataPtr->lastComputedEpisodes.isEmpty())
501 std::optional<BitTorrent::TorrentContentLayout> contentLayout;
502 if (createSubfolder.has_value())
504 contentLayout = (*createSubfolder
505 ? BitTorrent::TorrentContentLayout::Original
506 : BitTorrent::TorrentContentLayout::NoSubfolder);
522 if (feedsVal.isString())
524 else for (
const QJsonValue &urlVal :
asConst(feedsVal.toArray()))
529 QStringList previouslyMatched;
530 if (previouslyMatchedVal.isString())
532 previouslyMatched << previouslyMatchedVal.toString();
536 for (
const QJsonValue &val :
asConst(previouslyMatchedVal.toArray()))
537 previouslyMatched << val.toString();
546 return {{
"name",
name()},
564 rule.
setUseRegex(dict.value(
"use_regex",
false).toBool());
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());
573 rule.
setLastMatch(dict.value(
"last_match").toDateTime());
584 m_dataPtr->mustContain = QStringList() << tokens;
586 m_dataPtr->mustContain = tokens.split(
'|');
598 m_dataPtr->mustNotContain = QStringList() << tokens;
600 m_dataPtr->mustNotContain = tokens.split(
'|');
603 if ((
m_dataPtr->mustNotContain.size() == 1) &&
m_dataPtr->mustNotContain[0].isEmpty())
654 m_dataPtr->contentLayout = contentLayout;
704 return m_dataPtr->mustNotContain.join(
'|');
730 return m_dataPtr->previouslyMatchedEpisodes;
static const QString KeyDate
static const QString KeyTitle
void setTorrentContentLayout(std::optional< BitTorrent::TorrentContentLayout > contentLayout)
bool useSmartFilter() const
void setEpisodeFilter(const QString &e)
QStringList previouslyMatchedEpisodes() const
std::optional< BitTorrent::TorrentContentLayout > torrentContentLayout() const
bool accepts(const QVariantHash &articleData)
void setPreviouslyMatchedEpisodes(const QStringList &previouslyMatchedEpisodes)
QString assignedCategory() const
bool matchesEpisodeFilterExpression(const QString &articleTitle) const
QString mustContain() const
bool matchesSmartEpisodeFilter(const QString &articleTitle) const
void setEnabled(bool enable)
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)
QString mustNotContain() const
QDateTime lastMatch() const
void setIgnoreDays(int d)
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
QString episodeFilter() 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
QString toUniformPath(const QString &path)
QString fromEnum(const T &value)
T toEnum(const QString &serializedValue, const T &defaultValue)
QString wildcardToRegexPattern(const QString &pattern)
QStringList previouslyMatchedEpisodes
QHash< QString, QRegularExpression > cachedRegexes
std::optional< bool > addPaused
QStringList lastComputedEpisodes
QStringList mustNotContain
bool operator==(const AutoDownloadRuleData &other) const
std::optional< BitTorrent::TorrentContentLayout > contentLayout