diff --git a/src/base/rss/rss_autodownloader.cpp b/src/base/rss/rss_autodownloader.cpp index 7707983de..438509bc8 100644 --- a/src/base/rss/rss_autodownloader.cpp +++ b/src/base/rss/rss_autodownloader.cpp @@ -28,6 +28,7 @@ #include "rss_autodownloader.h" +#include #include #include #include @@ -37,6 +38,7 @@ #include #include #include +#include #include "../bittorrent/magneturi.h" #include "../bittorrent/session.h" @@ -63,6 +65,32 @@ const QString RulesFileName(QStringLiteral("download_rules.json")); const QString SettingsKey_ProcessingEnabled(QStringLiteral("RSS/AutoDownloader/EnableProcessing")); +namespace +{ + QVector rulesFromJSON(const QByteArray &jsonData) + { + QJsonParseError jsonError; + QJsonDocument jsonDoc = QJsonDocument::fromJson(jsonData, &jsonError); + if (jsonError.error != QJsonParseError::NoError) + throw RSS::ParsingError(jsonError.errorString()); + + if (!jsonDoc.isObject()) + throw RSS::ParsingError(RSS::AutoDownloader::tr("Invalid data format.")); + + const QJsonObject jsonObj {jsonDoc.object()}; + QVector rules; + for (auto it = jsonObj.begin(); it != jsonObj.end(); ++it) { + const QJsonValue jsonVal {it.value()}; + if (!jsonVal.isObject()) + throw RSS::ParsingError(RSS::AutoDownloader::tr("Invalid data format.")); + + rules.append(RSS::AutoDownloadRule::fromJsonObject(jsonVal.toObject(), it.key())); + } + + return rules; + } +} + using namespace RSS; QPointer AutoDownloader::m_instance = nullptr; @@ -84,8 +112,8 @@ AutoDownloader::AutoDownloader() connect(m_ioThread, &QThread::finished, m_fileStorage, &AsyncFileStorage::deleteLater); connect(m_fileStorage, &AsyncFileStorage::failed, [](const QString &fileName, const QString &errorString) { - Logger::instance()->addMessage(QString("Couldn't save RSS AutoDownloader data in %1. Error: %2") - .arg(fileName).arg(errorString), Log::WARNING); + LogMsg(tr("Couldn't save RSS AutoDownloader data in %1. Error: %2") + .arg(fileName).arg(errorString), Log::CRITICAL); }); m_ioThread->start(); @@ -174,6 +202,70 @@ void AutoDownloader::removeRule(const QString &ruleName) } } +QByteArray AutoDownloader::exportRules(AutoDownloader::RulesFileFormat format) const +{ + switch (format) { + case RulesFileFormat::Legacy: + return exportRulesToLegacyFormat(); + default: + return exportRulesToJSONFormat(); + } +} + +void AutoDownloader::importRules(const QByteArray &data, AutoDownloader::RulesFileFormat format) +{ + switch (format) { + case RulesFileFormat::Legacy: + importRulesFromLegacyFormat(data); + break; + default: + importRulesFromJSONFormat(data); + } +} + +QByteArray AutoDownloader::exportRulesToJSONFormat() const +{ + QJsonObject jsonObj; + for (const auto &rule : rules()) + jsonObj.insert(rule.name(), rule.toJsonObject()); + + return QJsonDocument(jsonObj).toJson(); +} + +void AutoDownloader::importRulesFromJSONFormat(const QByteArray &data) +{ + const auto rules = rulesFromJSON(data); + for (const auto &rule : rules) + insertRule(rule); +} + +QByteArray AutoDownloader::exportRulesToLegacyFormat() const +{ + QVariantHash dict; + for (const auto &rule : rules()) + dict[rule.name()] = rule.toLegacyDict(); + + QByteArray data; + QDataStream out(&data, QIODevice::WriteOnly); + out.setVersion(QDataStream::Qt_4_5); + out << dict; + + return data; +} + +void AutoDownloader::importRulesFromLegacyFormat(const QByteArray &data) +{ + QDataStream in(data); + in.setVersion(QDataStream::Qt_4_5); + QVariantHash dict; + in >> dict; + if (in.status() != QDataStream::Ok) + throw ParsingError(tr("Invalid data format")); + + for (const QVariant &val : dict) + insertRule(AutoDownloadRule::fromLegacyDict(val.toHash())); +} + void AutoDownloader::process() { if (m_processingQueue.isEmpty()) return; // processing was disabled @@ -276,39 +368,20 @@ void AutoDownloader::load() else if (rulesFile.open(QFile::ReadOnly)) loadRules(rulesFile.readAll()); else - Logger::instance()->addMessage( - QString("Couldn't read RSS AutoDownloader rules from %1. Error: %2") - .arg(rulesFile.fileName()).arg(rulesFile.errorString()), Log::WARNING); + LogMsg(tr("Couldn't read RSS AutoDownloader rules from %1. Error: %2") + .arg(rulesFile.fileName()).arg(rulesFile.errorString()), Log::CRITICAL); } void AutoDownloader::loadRules(const QByteArray &data) { - QJsonParseError jsonError; - QJsonDocument jsonDoc = QJsonDocument::fromJson(data, &jsonError); - if (jsonError.error != QJsonParseError::NoError) { - Logger::instance()->addMessage( - QString("Couldn't parse RSS AutoDownloader rules. Error: %1") - .arg(jsonError.errorString()), Log::WARNING); - return; - } - - if (!jsonDoc.isObject()) { - Logger::instance()->addMessage( - QString("Couldn't load RSS AutoDownloader rules. Invalid data format."), Log::WARNING); - return; + try { + const auto rules = rulesFromJSON(data); + for (const auto &rule : rules) + setRule_impl(rule); } - - QJsonObject jsonObj = jsonDoc.object(); - foreach (const QString &key, jsonObj.keys()) { - const QJsonValue jsonVal = jsonObj.value(key); - if (!jsonVal.isObject()) { - Logger::instance()->addMessage( - QString("Couldn't load RSS AutoDownloader rule '%1'. Invalid data format.") - .arg(key), Log::WARNING); - continue; - } - - setRule_impl(AutoDownloadRule::fromJsonObject(jsonVal.toObject(), key)); + catch (const ParsingError &error) { + LogMsg(tr("Couldn't load RSS AutoDownloader rules. Reason: %1") + .arg(error.message()), Log::CRITICAL); } } @@ -317,7 +390,7 @@ void AutoDownloader::loadRulesLegacy() SettingsPtr settings = Profile::instance().applicationSettings(QStringLiteral("qBittorrent-rss")); QVariantHash rules = settings->value(QStringLiteral("download_rules")).toHash(); foreach (const QVariant &ruleVar, rules) { - auto rule = AutoDownloadRule::fromVariantHash(ruleVar.toHash()); + auto rule = AutoDownloadRule::fromLegacyDict(ruleVar.toHash()); if (!rule.name().isEmpty()) insertRule(rule); } @@ -385,3 +458,13 @@ void AutoDownloader::timerEvent(QTimerEvent *event) Q_UNUSED(event); store(); } + +ParsingError::ParsingError(const QString &message) + : std::runtime_error(message.toUtf8().data()) +{ +} + +QString ParsingError::message() const +{ + return what(); +} diff --git a/src/base/rss/rss_autodownloader.h b/src/base/rss/rss_autodownloader.h index 50919f978..ba9c0ac39 100644 --- a/src/base/rss/rss_autodownloader.h +++ b/src/base/rss/rss_autodownloader.h @@ -28,6 +28,8 @@ #pragma once +#include + #include #include #include @@ -49,6 +51,13 @@ namespace RSS class AutoDownloadRule; + class ParsingError : public std::runtime_error + { + public: + explicit ParsingError(const QString &message); + QString message() const; + }; + class AutoDownloader final: public QObject { Q_OBJECT @@ -60,6 +69,12 @@ namespace RSS ~AutoDownloader() override; public: + enum class RulesFileFormat + { + Legacy, + JSON + }; + static AutoDownloader *instance(); bool isProcessingEnabled() const; @@ -73,6 +88,9 @@ namespace RSS bool renameRule(const QString &ruleName, const QString &newRuleName); void removeRule(const QString &ruleName); + QByteArray exportRules(RulesFileFormat format = RulesFileFormat::JSON) const; + void importRules(const QByteArray &data, RulesFileFormat format = RulesFileFormat::JSON); + signals: void processingStateChanged(bool enabled); void ruleAdded(const QString &ruleName); @@ -98,6 +116,10 @@ namespace RSS void loadRulesLegacy(); void store(); void storeDeferred(); + QByteArray exportRulesToJSONFormat() const; + void importRulesFromJSONFormat(const QByteArray &data); + QByteArray exportRulesToLegacyFormat() const; + void importRulesFromLegacyFormat(const QByteArray &data); static QPointer m_instance; diff --git a/src/base/rss/rss_autodownloadrule.cpp b/src/base/rss/rss_autodownloadrule.cpp index 48c194621..83a9c343e 100644 --- a/src/base/rss/rss_autodownloadrule.cpp +++ b/src/base/rss/rss_autodownloadrule.cpp @@ -63,11 +63,29 @@ namespace QJsonValue triStateBoolToJsonValue(const TriStateBool &triStateBool) { switch (static_cast(triStateBool)) { - case 0: return false; break; - case 1: return true; break; + case 0: return false; + case 1: return true; default: return QJsonValue(); } } + + TriStateBool addPausedLegacyToTriStateBool(int val) + { + switch (val) { + case 1: return TriStateBool::True; // always + case 2: return TriStateBool::False; // never + default: return TriStateBool::Undefined; // default + } + } + + int triStateBoolToAddPausedLegacy(const TriStateBool &triStateBool) + { + switch (static_cast(triStateBool)) { + case 0: return 2; // never + case 1: return 1; // always + default: return 0; // default + } + } } const QString Str_Name(QStringLiteral("name")); @@ -378,21 +396,37 @@ AutoDownloadRule AutoDownloadRule::fromJsonObject(const QJsonObject &jsonObj, co return rule; } -AutoDownloadRule AutoDownloadRule::fromVariantHash(const QVariantHash &varHash) -{ - AutoDownloadRule rule(varHash.value("name").toString()); - - rule.setUseRegex(varHash.value("use_regex", false).toBool()); - rule.setMustContain(varHash.value("must_contain").toString()); - rule.setMustNotContain(varHash.value("must_not_contain").toString()); - rule.setEpisodeFilter(varHash.value("episode_filter").toString()); - rule.setFeedURLs(varHash.value("affected_feeds").toStringList()); - rule.setEnabled(varHash.value("enabled", false).toBool()); - rule.setSavePath(varHash.value("save_path").toString()); - rule.setCategory(varHash.value("category_assigned").toString()); - rule.setAddPaused(TriStateBool(varHash.value("add_paused").toInt() - 1)); - rule.setLastMatch(varHash.value("last_match").toDateTime()); - rule.setIgnoreDays(varHash.value("ignore_days").toInt()); +QVariantHash AutoDownloadRule::toLegacyDict() const +{ + return {{"name", name()}, + {"must_contain", mustContain()}, + {"must_not_contain", mustNotContain()}, + {"save_path", savePath()}, + {"affected_feeds", feedURLs()}, + {"enabled", isEnabled()}, + {"category_assigned", assignedCategory()}, + {"use_regex", useRegex()}, + {"add_paused", triStateBoolToAddPausedLegacy(addPaused())}, + {"episode_filter", episodeFilter()}, + {"last_match", lastMatch()}, + {"ignore_days", ignoreDays()}}; +} + +AutoDownloadRule AutoDownloadRule::fromLegacyDict(const QVariantHash &dict) +{ + AutoDownloadRule rule(dict.value("name").toString()); + + rule.setUseRegex(dict.value("use_regex", false).toBool()); + rule.setMustContain(dict.value("must_contain").toString()); + rule.setMustNotContain(dict.value("must_not_contain").toString()); + rule.setEpisodeFilter(dict.value("episode_filter").toString()); + rule.setFeedURLs(dict.value("affected_feeds").toStringList()); + rule.setEnabled(dict.value("enabled", false).toBool()); + rule.setSavePath(dict.value("save_path").toString()); + rule.setCategory(dict.value("category_assigned").toString()); + rule.setAddPaused(addPausedLegacyToTriStateBool(dict.value("add_paused").toInt())); + rule.setLastMatch(dict.value("last_match").toDateTime()); + rule.setIgnoreDays(dict.value("ignore_days").toInt()); return rule; } diff --git a/src/base/rss/rss_autodownloadrule.h b/src/base/rss/rss_autodownloadrule.h index 6b79fe3a5..b28d7b47a 100644 --- a/src/base/rss/rss_autodownloadrule.h +++ b/src/base/rss/rss_autodownloadrule.h @@ -84,7 +84,9 @@ namespace RSS QJsonObject toJsonObject() const; static AutoDownloadRule fromJsonObject(const QJsonObject &jsonObj, const QString &name = ""); - static AutoDownloadRule fromVariantHash(const QVariantHash &varHash); + + QVariantHash toLegacyDict() const; + static AutoDownloadRule fromLegacyDict(const QVariantHash &dict); private: bool matches(const QString &articleTitle, const QString &expression) const; diff --git a/src/gui/rss/automatedrssdownloader.cpp b/src/gui/rss/automatedrssdownloader.cpp index a97ebcbb9..dc2cf0aa6 100644 --- a/src/gui/rss/automatedrssdownloader.cpp +++ b/src/gui/rss/automatedrssdownloader.cpp @@ -54,8 +54,13 @@ #include "autoexpandabledialog.h" #include "ui_automatedrssdownloader.h" +const QString EXT_JSON {QStringLiteral(".json")}; +const QString EXT_LEGACY {QStringLiteral(".rssrules")}; + AutomatedRssDownloader::AutomatedRssDownloader(QWidget *parent) : QDialog(parent) + , m_formatFilterJSON(QString("%1 (*%2)").arg(tr("Rules")).arg(EXT_JSON)) + , m_formatFilterLegacy(QString("%1 (*%2)").arg(tr("Rules (legacy)")).arg(EXT_LEGACY)) , m_ui(new Ui::AutomatedRssDownloader) , m_currentRuleItem(nullptr) { @@ -384,31 +389,73 @@ void AutomatedRssDownloader::on_browseSP_clicked() void AutomatedRssDownloader::on_exportBtn_clicked() { -// if (m_editableRuleList->isEmpty()) { -// QMessageBox::warning(this, tr("Invalid action"), tr("The list is empty, there is nothing to export.")); -// return; -// } -// // Ask for a save path -// QString save_path = QFileDialog::getSaveFileName(this, tr("Where would you like to save the list?"), QDir::homePath(), tr("Rules list (*.rssrules)")); -// if (save_path.isEmpty()) return; -// if (!save_path.endsWith(".rssrules", Qt::CaseInsensitive)) -// save_path += ".rssrules"; -// if (!m_editableRuleList->serialize(save_path)) { -// QMessageBox::warning(this, tr("I/O Error"), tr("Failed to create the destination file")); -// return; -// } + if (RSS::AutoDownloader::instance()->rules().isEmpty()) { + QMessageBox::warning(this, tr("Invalid action") + , tr("The list is empty, there is nothing to export.")); + return; + } + + QString selectedFilter {m_formatFilterJSON}; + QString path = QFileDialog::getSaveFileName( + this, tr("Export RSS rules"), QDir::homePath() + , QString("%1;;%2").arg(m_formatFilterJSON).arg(m_formatFilterLegacy), &selectedFilter); + if (path.isEmpty()) return; + + const RSS::AutoDownloader::RulesFileFormat format { + (selectedFilter == m_formatFilterJSON) + ? RSS::AutoDownloader::RulesFileFormat::JSON + : RSS::AutoDownloader::RulesFileFormat::Legacy + }; + + if (format == RSS::AutoDownloader::RulesFileFormat::JSON) { + if (!path.endsWith(EXT_JSON, Qt::CaseInsensitive)) + path += EXT_JSON; + } + else { + if (!path.endsWith(EXT_LEGACY, Qt::CaseInsensitive)) + path += EXT_LEGACY; + } + + QFile file {path}; + if (!file.open(QFile::WriteOnly) + || (file.write(RSS::AutoDownloader::instance()->exportRules(format)) == -1)) { + QMessageBox::critical( + this, tr("I/O Error") + , tr("Failed to create the destination file. Reason: %1").arg(file.errorString())); + } } void AutomatedRssDownloader::on_importBtn_clicked() { -// // Ask for filter path -// QString load_path = QFileDialog::getOpenFileName(this, tr("Please point to the RSS download rules file"), QDir::homePath(), tr("Rules list") + QString(" (*.rssrules *.filters)")); -// if (load_path.isEmpty() || !QFile::exists(load_path)) return; -// // Load it -// if (!m_editableRuleList->unserialize(load_path)) { -// QMessageBox::warning(this, tr("Import Error"), tr("Failed to import the selected rules file")); -// return; -// } + QString selectedFilter {m_formatFilterJSON}; + QString path = QFileDialog::getOpenFileName( + this, tr("Import RSS rules"), QDir::homePath() + , QString("%1;;%2").arg(m_formatFilterJSON).arg(m_formatFilterLegacy), &selectedFilter); + if (path.isEmpty() || !QFile::exists(path)) + return; + + QFile file {path}; + if (!file.open(QIODevice::ReadOnly)) { + QMessageBox::critical( + this, tr("I/O Error") + , tr("Failed to open the file. Reason: %1").arg(file.errorString())); + return; + } + + const RSS::AutoDownloader::RulesFileFormat format { + (selectedFilter == m_formatFilterJSON) + ? RSS::AutoDownloader::RulesFileFormat::JSON + : RSS::AutoDownloader::RulesFileFormat::Legacy + }; + + try { + RSS::AutoDownloader::instance()->importRules(file.readAll(),format); + } + catch (const RSS::ParsingError &error) { + QMessageBox::critical( + this, tr("Import Error") + , tr("Failed to import the selected rules file. Reason: %1").arg(error.message())); + } } void AutomatedRssDownloader::displayRulesListMenu() diff --git a/src/gui/rss/automatedrssdownloader.h b/src/gui/rss/automatedrssdownloader.h index acbd9b955..d336fbc60 100644 --- a/src/gui/rss/automatedrssdownloader.h +++ b/src/gui/rss/automatedrssdownloader.h @@ -96,6 +96,9 @@ private: void updateFeedList(); void addFeedArticlesToTree(RSS::Feed *feed, const QStringList &articles); + const QString m_formatFilterJSON; + const QString m_formatFilterLegacy; + Ui::AutomatedRssDownloader *m_ui; QListWidgetItem *m_currentRuleItem; QShortcut *m_editHotkey; diff --git a/src/gui/rss/automatedrssdownloader.ui b/src/gui/rss/automatedrssdownloader.ui index 6c9db0cdf..e77b1385a 100644 --- a/src/gui/rss/automatedrssdownloader.ui +++ b/src/gui/rss/automatedrssdownloader.ui @@ -386,7 +386,7 @@ - false + true &Import... @@ -396,7 +396,7 @@ - false + true &Export...