Merge pull request #7832 from glassez/rss-import

Implement Import/Export RSS rules. Closes #7721
This commit is contained in:
Vladimir Golovnev 2017-11-25 10:35:24 +03:00 committed by GitHub
commit a7da714b13
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 261 additions and 70 deletions

View file

@ -28,6 +28,7 @@
#include "rss_autodownloader.h" #include "rss_autodownloader.h"
#include <QDataStream>
#include <QDebug> #include <QDebug>
#include <QJsonArray> #include <QJsonArray>
#include <QJsonDocument> #include <QJsonDocument>
@ -37,6 +38,7 @@
#include <QThread> #include <QThread>
#include <QTimer> #include <QTimer>
#include <QVariant> #include <QVariant>
#include <QVector>
#include "../bittorrent/magneturi.h" #include "../bittorrent/magneturi.h"
#include "../bittorrent/session.h" #include "../bittorrent/session.h"
@ -63,6 +65,32 @@ const QString RulesFileName(QStringLiteral("download_rules.json"));
const QString SettingsKey_ProcessingEnabled(QStringLiteral("RSS/AutoDownloader/EnableProcessing")); const QString SettingsKey_ProcessingEnabled(QStringLiteral("RSS/AutoDownloader/EnableProcessing"));
namespace
{
QVector<RSS::AutoDownloadRule> 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<RSS::AutoDownloadRule> 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; using namespace RSS;
QPointer<AutoDownloader> AutoDownloader::m_instance = nullptr; QPointer<AutoDownloader> AutoDownloader::m_instance = nullptr;
@ -84,8 +112,8 @@ AutoDownloader::AutoDownloader()
connect(m_ioThread, &QThread::finished, m_fileStorage, &AsyncFileStorage::deleteLater); connect(m_ioThread, &QThread::finished, m_fileStorage, &AsyncFileStorage::deleteLater);
connect(m_fileStorage, &AsyncFileStorage::failed, [](const QString &fileName, const QString &errorString) 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") LogMsg(tr("Couldn't save RSS AutoDownloader data in %1. Error: %2")
.arg(fileName).arg(errorString), Log::WARNING); .arg(fileName).arg(errorString), Log::CRITICAL);
}); });
m_ioThread->start(); 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() void AutoDownloader::process()
{ {
if (m_processingQueue.isEmpty()) return; // processing was disabled if (m_processingQueue.isEmpty()) return; // processing was disabled
@ -276,39 +368,20 @@ void AutoDownloader::load()
else if (rulesFile.open(QFile::ReadOnly)) else if (rulesFile.open(QFile::ReadOnly))
loadRules(rulesFile.readAll()); loadRules(rulesFile.readAll());
else else
Logger::instance()->addMessage( LogMsg(tr("Couldn't read RSS AutoDownloader rules from %1. Error: %2")
QString("Couldn't read RSS AutoDownloader rules from %1. Error: %2") .arg(rulesFile.fileName()).arg(rulesFile.errorString()), Log::CRITICAL);
.arg(rulesFile.fileName()).arg(rulesFile.errorString()), Log::WARNING);
} }
void AutoDownloader::loadRules(const QByteArray &data) void AutoDownloader::loadRules(const QByteArray &data)
{ {
QJsonParseError jsonError; try {
QJsonDocument jsonDoc = QJsonDocument::fromJson(data, &jsonError); const auto rules = rulesFromJSON(data);
if (jsonError.error != QJsonParseError::NoError) { for (const auto &rule : rules)
Logger::instance()->addMessage( setRule_impl(rule);
QString("Couldn't parse RSS AutoDownloader rules. Error: %1")
.arg(jsonError.errorString()), Log::WARNING);
return;
} }
catch (const ParsingError &error) {
if (!jsonDoc.isObject()) { LogMsg(tr("Couldn't load RSS AutoDownloader rules. Reason: %1")
Logger::instance()->addMessage( .arg(error.message()), Log::CRITICAL);
QString("Couldn't load RSS AutoDownloader rules. Invalid data format."), Log::WARNING);
return;
}
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));
} }
} }
@ -317,7 +390,7 @@ void AutoDownloader::loadRulesLegacy()
SettingsPtr settings = Profile::instance().applicationSettings(QStringLiteral("qBittorrent-rss")); SettingsPtr settings = Profile::instance().applicationSettings(QStringLiteral("qBittorrent-rss"));
QVariantHash rules = settings->value(QStringLiteral("download_rules")).toHash(); QVariantHash rules = settings->value(QStringLiteral("download_rules")).toHash();
foreach (const QVariant &ruleVar, rules) { foreach (const QVariant &ruleVar, rules) {
auto rule = AutoDownloadRule::fromVariantHash(ruleVar.toHash()); auto rule = AutoDownloadRule::fromLegacyDict(ruleVar.toHash());
if (!rule.name().isEmpty()) if (!rule.name().isEmpty())
insertRule(rule); insertRule(rule);
} }
@ -385,3 +458,13 @@ void AutoDownloader::timerEvent(QTimerEvent *event)
Q_UNUSED(event); Q_UNUSED(event);
store(); store();
} }
ParsingError::ParsingError(const QString &message)
: std::runtime_error(message.toUtf8().data())
{
}
QString ParsingError::message() const
{
return what();
}

View file

@ -28,6 +28,8 @@
#pragma once #pragma once
#include <stdexcept>
#include <QBasicTimer> #include <QBasicTimer>
#include <QHash> #include <QHash>
#include <QList> #include <QList>
@ -49,6 +51,13 @@ namespace RSS
class AutoDownloadRule; class AutoDownloadRule;
class ParsingError : public std::runtime_error
{
public:
explicit ParsingError(const QString &message);
QString message() const;
};
class AutoDownloader final: public QObject class AutoDownloader final: public QObject
{ {
Q_OBJECT Q_OBJECT
@ -60,6 +69,12 @@ namespace RSS
~AutoDownloader() override; ~AutoDownloader() override;
public: public:
enum class RulesFileFormat
{
Legacy,
JSON
};
static AutoDownloader *instance(); static AutoDownloader *instance();
bool isProcessingEnabled() const; bool isProcessingEnabled() const;
@ -73,6 +88,9 @@ namespace RSS
bool renameRule(const QString &ruleName, const QString &newRuleName); bool renameRule(const QString &ruleName, const QString &newRuleName);
void removeRule(const QString &ruleName); void removeRule(const QString &ruleName);
QByteArray exportRules(RulesFileFormat format = RulesFileFormat::JSON) const;
void importRules(const QByteArray &data, RulesFileFormat format = RulesFileFormat::JSON);
signals: signals:
void processingStateChanged(bool enabled); void processingStateChanged(bool enabled);
void ruleAdded(const QString &ruleName); void ruleAdded(const QString &ruleName);
@ -98,6 +116,10 @@ namespace RSS
void loadRulesLegacy(); void loadRulesLegacy();
void store(); void store();
void storeDeferred(); void storeDeferred();
QByteArray exportRulesToJSONFormat() const;
void importRulesFromJSONFormat(const QByteArray &data);
QByteArray exportRulesToLegacyFormat() const;
void importRulesFromLegacyFormat(const QByteArray &data);
static QPointer<AutoDownloader> m_instance; static QPointer<AutoDownloader> m_instance;

View file

@ -63,11 +63,29 @@ namespace
QJsonValue triStateBoolToJsonValue(const TriStateBool &triStateBool) QJsonValue triStateBoolToJsonValue(const TriStateBool &triStateBool)
{ {
switch (static_cast<int>(triStateBool)) { switch (static_cast<int>(triStateBool)) {
case 0: return false; break; case 0: return false;
case 1: return true; break; case 1: return true;
default: return QJsonValue(); 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<int>(triStateBool)) {
case 0: return 2; // never
case 1: return 1; // always
default: return 0; // default
}
}
} }
const QString Str_Name(QStringLiteral("name")); const QString Str_Name(QStringLiteral("name"));
@ -378,21 +396,37 @@ AutoDownloadRule AutoDownloadRule::fromJsonObject(const QJsonObject &jsonObj, co
return rule; return rule;
} }
AutoDownloadRule AutoDownloadRule::fromVariantHash(const QVariantHash &varHash) QVariantHash AutoDownloadRule::toLegacyDict() const
{ {
AutoDownloadRule rule(varHash.value("name").toString()); 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()}};
}
rule.setUseRegex(varHash.value("use_regex", false).toBool()); AutoDownloadRule AutoDownloadRule::fromLegacyDict(const QVariantHash &dict)
rule.setMustContain(varHash.value("must_contain").toString()); {
rule.setMustNotContain(varHash.value("must_not_contain").toString()); AutoDownloadRule rule(dict.value("name").toString());
rule.setEpisodeFilter(varHash.value("episode_filter").toString());
rule.setFeedURLs(varHash.value("affected_feeds").toStringList()); rule.setUseRegex(dict.value("use_regex", false).toBool());
rule.setEnabled(varHash.value("enabled", false).toBool()); rule.setMustContain(dict.value("must_contain").toString());
rule.setSavePath(varHash.value("save_path").toString()); rule.setMustNotContain(dict.value("must_not_contain").toString());
rule.setCategory(varHash.value("category_assigned").toString()); rule.setEpisodeFilter(dict.value("episode_filter").toString());
rule.setAddPaused(TriStateBool(varHash.value("add_paused").toInt() - 1)); rule.setFeedURLs(dict.value("affected_feeds").toStringList());
rule.setLastMatch(varHash.value("last_match").toDateTime()); rule.setEnabled(dict.value("enabled", false).toBool());
rule.setIgnoreDays(varHash.value("ignore_days").toInt()); 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; return rule;
} }

View file

@ -84,7 +84,9 @@ namespace RSS
QJsonObject toJsonObject() const; QJsonObject toJsonObject() const;
static AutoDownloadRule fromJsonObject(const QJsonObject &jsonObj, const QString &name = ""); 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: private:
bool matches(const QString &articleTitle, const QString &expression) const; bool matches(const QString &articleTitle, const QString &expression) const;

View file

@ -54,8 +54,13 @@
#include "autoexpandabledialog.h" #include "autoexpandabledialog.h"
#include "ui_automatedrssdownloader.h" #include "ui_automatedrssdownloader.h"
const QString EXT_JSON {QStringLiteral(".json")};
const QString EXT_LEGACY {QStringLiteral(".rssrules")};
AutomatedRssDownloader::AutomatedRssDownloader(QWidget *parent) AutomatedRssDownloader::AutomatedRssDownloader(QWidget *parent)
: QDialog(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_ui(new Ui::AutomatedRssDownloader)
, m_currentRuleItem(nullptr) , m_currentRuleItem(nullptr)
{ {
@ -384,31 +389,73 @@ void AutomatedRssDownloader::on_browseSP_clicked()
void AutomatedRssDownloader::on_exportBtn_clicked() void AutomatedRssDownloader::on_exportBtn_clicked()
{ {
// if (m_editableRuleList->isEmpty()) { if (RSS::AutoDownloader::instance()->rules().isEmpty()) {
// QMessageBox::warning(this, tr("Invalid action"), tr("The list is empty, there is nothing to export.")); QMessageBox::warning(this, tr("Invalid action")
// return; , 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; QString selectedFilter {m_formatFilterJSON};
// if (!save_path.endsWith(".rssrules", Qt::CaseInsensitive)) QString path = QFileDialog::getSaveFileName(
// save_path += ".rssrules"; this, tr("Export RSS rules"), QDir::homePath()
// if (!m_editableRuleList->serialize(save_path)) { , QString("%1;;%2").arg(m_formatFilterJSON).arg(m_formatFilterLegacy), &selectedFilter);
// QMessageBox::warning(this, tr("I/O Error"), tr("Failed to create the destination file")); if (path.isEmpty()) return;
// 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() void AutomatedRssDownloader::on_importBtn_clicked()
{ {
// // Ask for filter path QString selectedFilter {m_formatFilterJSON};
// QString load_path = QFileDialog::getOpenFileName(this, tr("Please point to the RSS download rules file"), QDir::homePath(), tr("Rules list") + QString(" (*.rssrules *.filters)")); QString path = QFileDialog::getOpenFileName(
// if (load_path.isEmpty() || !QFile::exists(load_path)) return; this, tr("Import RSS rules"), QDir::homePath()
// // Load it , QString("%1;;%2").arg(m_formatFilterJSON).arg(m_formatFilterLegacy), &selectedFilter);
// if (!m_editableRuleList->unserialize(load_path)) { if (path.isEmpty() || !QFile::exists(path))
// QMessageBox::warning(this, tr("Import Error"), tr("Failed to import the selected rules file")); return;
// 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() void AutomatedRssDownloader::displayRulesListMenu()

View file

@ -96,6 +96,9 @@ private:
void updateFeedList(); void updateFeedList();
void addFeedArticlesToTree(RSS::Feed *feed, const QStringList &articles); void addFeedArticlesToTree(RSS::Feed *feed, const QStringList &articles);
const QString m_formatFilterJSON;
const QString m_formatFilterLegacy;
Ui::AutomatedRssDownloader *m_ui; Ui::AutomatedRssDownloader *m_ui;
QListWidgetItem *m_currentRuleItem; QListWidgetItem *m_currentRuleItem;
QShortcut *m_editHotkey; QShortcut *m_editHotkey;

View file

@ -386,7 +386,7 @@
<item> <item>
<widget class="QPushButton" name="importBtn"> <widget class="QPushButton" name="importBtn">
<property name="enabled"> <property name="enabled">
<bool>false</bool> <bool>true</bool>
</property> </property>
<property name="text"> <property name="text">
<string>&amp;Import...</string> <string>&amp;Import...</string>
@ -396,7 +396,7 @@
<item> <item>
<widget class="QPushButton" name="exportBtn"> <widget class="QPushButton" name="exportBtn">
<property name="enabled"> <property name="enabled">
<bool>false</bool> <bool>true</bool>
</property> </property>
<property name="text"> <property name="text">
<string>&amp;Export...</string> <string>&amp;Export...</string>