Some work about adaptive color scheme for Web UI (PR #19901)
http://[316:c51a:62a3:8b9::4]/d4708/qBittorrent/src/branch/adaptive-webui
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
508 lines
15 KiB
508 lines
15 KiB
/* |
|
* Bittorrent Client using Qt and libtorrent. |
|
* Copyright (C) 2017 Vladimir Golovnev <glassez@yandex.ru> |
|
* |
|
* This program is free software; you can redistribute it and/or |
|
* modify it under the terms of the GNU General Public License |
|
* as published by the Free Software Foundation; either version 2 |
|
* of the License, or (at your option) any later version. |
|
* |
|
* This program is distributed in the hope that it will be useful, |
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of |
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|
* GNU General Public License for more details. |
|
* |
|
* You should have received a copy of the GNU General Public License |
|
* along with this program; if not, write to the Free Software |
|
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. |
|
* |
|
* In addition, as a special exception, the copyright holders give permission to |
|
* link this program with the OpenSSL project's "OpenSSL" library (or with |
|
* modified versions of it that use the same license as the "OpenSSL" library), |
|
* and distribute the linked executables. You must obey the GNU General Public |
|
* License in all respects for all of the code used other than "OpenSSL". If you |
|
* modify file(s), you may extend this exception to your version of the file(s), |
|
* but you are not obligated to do so. If you do not wish to do so, delete this |
|
* exception statement from your version. |
|
*/ |
|
|
|
#include "rss_autodownloader.h" |
|
|
|
#include <QDataStream> |
|
#include <QDebug> |
|
#include <QJsonArray> |
|
#include <QJsonDocument> |
|
#include <QJsonObject> |
|
#include <QJsonValue> |
|
#include <QSaveFile> |
|
#include <QThread> |
|
#include <QTimer> |
|
#include <QVariant> |
|
#include <QVector> |
|
|
|
#include "../bittorrent/magneturi.h" |
|
#include "../bittorrent/session.h" |
|
#include "../asyncfilestorage.h" |
|
#include "../global.h" |
|
#include "../logger.h" |
|
#include "../profile.h" |
|
#include "../settingsstorage.h" |
|
#include "../tristatebool.h" |
|
#include "../utils/fs.h" |
|
#include "rss_article.h" |
|
#include "rss_autodownloadrule.h" |
|
#include "rss_feed.h" |
|
#include "rss_folder.h" |
|
#include "rss_session.h" |
|
|
|
struct ProcessingJob |
|
{ |
|
QString feedURL; |
|
QVariantHash articleData; |
|
}; |
|
|
|
const QString ConfFolderName(QStringLiteral("rss")); |
|
const QString RulesFileName(QStringLiteral("download_rules.json")); |
|
|
|
const QString SettingsKey_ProcessingEnabled(QStringLiteral("RSS/AutoDownloader/EnableProcessing")); |
|
const QString SettingsKey_SmartEpisodeFilter(QStringLiteral("RSS/AutoDownloader/SmartEpisodeFilter")); |
|
|
|
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; |
|
|
|
QPointer<AutoDownloader> AutoDownloader::m_instance = nullptr; |
|
|
|
QString computeSmartFilterRegex(const QStringList &filters) |
|
{ |
|
return QString("(?:_|\\b)(?:%1)(?:_|\\b)").arg(filters.join(QString(")|(?:"))); |
|
} |
|
|
|
AutoDownloader::AutoDownloader() |
|
: m_processingEnabled(SettingsStorage::instance()->loadValue(SettingsKey_ProcessingEnabled, false).toBool()) |
|
, m_processingTimer(new QTimer(this)) |
|
, m_ioThread(new QThread(this)) |
|
{ |
|
Q_ASSERT(!m_instance); // only one instance is allowed |
|
m_instance = this; |
|
|
|
m_fileStorage = new AsyncFileStorage( |
|
Utils::Fs::expandPathAbs(specialFolderLocation(SpecialFolder::Config) + ConfFolderName)); |
|
if (!m_fileStorage) |
|
throw std::runtime_error("Directory for RSS AutoDownloader data is unavailable."); |
|
|
|
m_fileStorage->moveToThread(m_ioThread); |
|
connect(m_ioThread, &QThread::finished, m_fileStorage, &AsyncFileStorage::deleteLater); |
|
connect(m_fileStorage, &AsyncFileStorage::failed, [](const QString &fileName, const QString &errorString) |
|
{ |
|
LogMsg(tr("Couldn't save RSS AutoDownloader data in %1. Error: %2") |
|
.arg(fileName, errorString), Log::CRITICAL); |
|
}); |
|
|
|
m_ioThread->start(); |
|
|
|
connect(BitTorrent::Session::instance(), &BitTorrent::Session::downloadFromUrlFinished |
|
, this, &AutoDownloader::handleTorrentDownloadFinished); |
|
connect(BitTorrent::Session::instance(), &BitTorrent::Session::downloadFromUrlFailed |
|
, this, &AutoDownloader::handleTorrentDownloadFailed); |
|
|
|
// initialise the smart episode regex |
|
const QString regex = computeSmartFilterRegex(smartEpisodeFilters()); |
|
m_smartEpisodeRegex = QRegularExpression(regex, |
|
QRegularExpression::CaseInsensitiveOption |
|
| QRegularExpression::ExtendedPatternSyntaxOption |
|
| QRegularExpression::UseUnicodePropertiesOption); |
|
|
|
load(); |
|
|
|
m_processingTimer->setSingleShot(true); |
|
connect(m_processingTimer, &QTimer::timeout, this, &AutoDownloader::process); |
|
|
|
if (m_processingEnabled) |
|
startProcessing(); |
|
} |
|
|
|
AutoDownloader::~AutoDownloader() |
|
{ |
|
store(); |
|
|
|
m_ioThread->quit(); |
|
m_ioThread->wait(); |
|
} |
|
|
|
AutoDownloader *AutoDownloader::instance() |
|
{ |
|
return m_instance; |
|
} |
|
|
|
bool AutoDownloader::hasRule(const QString &ruleName) const |
|
{ |
|
return m_rules.contains(ruleName); |
|
} |
|
|
|
AutoDownloadRule AutoDownloader::ruleByName(const QString &ruleName) const |
|
{ |
|
return m_rules.value(ruleName, AutoDownloadRule("Unknown Rule")); |
|
} |
|
|
|
QList<AutoDownloadRule> AutoDownloader::rules() const |
|
{ |
|
return m_rules.values(); |
|
} |
|
|
|
void AutoDownloader::insertRule(const AutoDownloadRule &rule) |
|
{ |
|
if (!hasRule(rule.name())) { |
|
// Insert new rule |
|
setRule_impl(rule); |
|
m_dirty = true; |
|
store(); |
|
emit ruleAdded(rule.name()); |
|
resetProcessingQueue(); |
|
} |
|
else if (ruleByName(rule.name()) != rule) { |
|
// Update existing rule |
|
setRule_impl(rule); |
|
m_dirty = true; |
|
storeDeferred(); |
|
emit ruleChanged(rule.name()); |
|
resetProcessingQueue(); |
|
} |
|
} |
|
|
|
bool AutoDownloader::renameRule(const QString &ruleName, const QString &newRuleName) |
|
{ |
|
if (!hasRule(ruleName)) return false; |
|
if (hasRule(newRuleName)) return false; |
|
|
|
m_rules.insert(newRuleName, m_rules.take(ruleName)); |
|
m_dirty = true; |
|
store(); |
|
emit ruleRenamed(newRuleName, ruleName); |
|
return true; |
|
} |
|
|
|
void AutoDownloader::removeRule(const QString &ruleName) |
|
{ |
|
if (m_rules.contains(ruleName)) { |
|
emit ruleAboutToBeRemoved(ruleName); |
|
m_rules.remove(ruleName); |
|
m_dirty = true; |
|
store(); |
|
} |
|
} |
|
|
|
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 : copyAsConst(rules())) |
|
jsonObj.insert(rule.name(), rule.toJsonObject()); |
|
|
|
return QJsonDocument(jsonObj).toJson(); |
|
} |
|
|
|
void AutoDownloader::importRulesFromJSONFormat(const QByteArray &data) |
|
{ |
|
for (const auto &rule : copyAsConst(rulesFromJSON(data))) |
|
insertRule(rule); |
|
} |
|
|
|
QByteArray AutoDownloader::exportRulesToLegacyFormat() const |
|
{ |
|
QVariantHash dict; |
|
for (const auto &rule : copyAsConst(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 : qAsConst(dict)) |
|
insertRule(AutoDownloadRule::fromLegacyDict(val.toHash())); |
|
} |
|
|
|
QStringList AutoDownloader::smartEpisodeFilters() const |
|
{ |
|
const QVariant filtersSetting = SettingsStorage::instance()->loadValue(SettingsKey_SmartEpisodeFilter); |
|
|
|
if (filtersSetting.isNull()) { |
|
QStringList filters = { |
|
"s(\\d+)e(\\d+)", // Format 1: s01e01 |
|
"(\\d+)x(\\d+)", // Format 2: 01x01 |
|
"(\\d{4}[.\\-]\\d{1,2}[.\\-]\\d{1,2})", // Format 3: 2017.01.01 |
|
"(\\d{1,2}[.\\-]\\d{1,2}[.\\-]\\d{4})" // Format 4: 01.01.2017 |
|
}; |
|
|
|
return filters; |
|
} |
|
|
|
return filtersSetting.toStringList(); |
|
} |
|
|
|
QRegularExpression AutoDownloader::smartEpisodeRegex() const |
|
{ |
|
return m_smartEpisodeRegex; |
|
} |
|
|
|
void AutoDownloader::setSmartEpisodeFilters(const QStringList &filters) |
|
{ |
|
SettingsStorage::instance()->storeValue(SettingsKey_SmartEpisodeFilter, filters); |
|
|
|
const QString regex = computeSmartFilterRegex(filters); |
|
m_smartEpisodeRegex.setPattern(regex); |
|
} |
|
|
|
void AutoDownloader::process() |
|
{ |
|
if (m_processingQueue.isEmpty()) return; // processing was disabled |
|
|
|
processJob(m_processingQueue.takeFirst()); |
|
if (!m_processingQueue.isEmpty()) |
|
// Schedule to process the next torrent (if any) |
|
m_processingTimer->start(); |
|
} |
|
|
|
void AutoDownloader::handleTorrentDownloadFinished(const QString &url) |
|
{ |
|
auto job = m_waitingJobs.take(url); |
|
if (!job) return; |
|
|
|
if (Feed *feed = Session::instance()->feedByURL(job->feedURL)) |
|
if (Article *article = feed->articleByGUID(job->articleData.value(Article::KeyId).toString())) |
|
article->markAsRead(); |
|
} |
|
|
|
void AutoDownloader::handleTorrentDownloadFailed(const QString &url) |
|
{ |
|
m_waitingJobs.remove(url); |
|
// TODO: Re-schedule job here. |
|
} |
|
|
|
void AutoDownloader::handleNewArticle(Article *article) |
|
{ |
|
if (!article->isRead() && !article->torrentUrl().isEmpty()) |
|
addJobForArticle(article); |
|
} |
|
|
|
void AutoDownloader::setRule_impl(const AutoDownloadRule &rule) |
|
{ |
|
m_rules.insert(rule.name(), rule); |
|
} |
|
|
|
void AutoDownloader::addJobForArticle(Article *article) |
|
{ |
|
const QString torrentURL = article->torrentUrl(); |
|
if (m_waitingJobs.contains(torrentURL)) return; |
|
|
|
QSharedPointer<ProcessingJob> job(new ProcessingJob); |
|
job->feedURL = article->feed()->url(); |
|
job->articleData = article->data(); |
|
m_processingQueue.append(job); |
|
if (!m_processingTimer->isActive()) |
|
m_processingTimer->start(); |
|
} |
|
|
|
void AutoDownloader::processJob(const QSharedPointer<ProcessingJob> &job) |
|
{ |
|
for (AutoDownloadRule &rule: m_rules) { |
|
if (!rule.isEnabled()) continue; |
|
if (!rule.feedURLs().contains(job->feedURL)) continue; |
|
if (!rule.accepts(job->articleData)) continue; |
|
|
|
m_dirty = true; |
|
storeDeferred(); |
|
|
|
BitTorrent::AddTorrentParams params; |
|
params.savePath = rule.savePath(); |
|
params.category = rule.assignedCategory(); |
|
params.addPaused = rule.addPaused(); |
|
if (!rule.savePath().isEmpty()) |
|
params.useAutoTMM = TriStateBool::False; |
|
auto torrentURL = job->articleData.value(Article::KeyTorrentURL).toString(); |
|
BitTorrent::Session::instance()->addTorrent(torrentURL, params); |
|
|
|
if (BitTorrent::MagnetUri(torrentURL).isValid()) { |
|
if (Feed *feed = Session::instance()->feedByURL(job->feedURL)) { |
|
if (Article *article = feed->articleByGUID(job->articleData.value(Article::KeyId).toString())) |
|
article->markAsRead(); |
|
} |
|
} |
|
else { |
|
// waiting for torrent file downloading |
|
// normalize URL string via QUrl since DownloadManager do it |
|
m_waitingJobs.insert(QUrl(torrentURL).toString(), job); |
|
} |
|
|
|
return; |
|
} |
|
} |
|
|
|
void AutoDownloader::load() |
|
{ |
|
QFile rulesFile(m_fileStorage->storageDir().absoluteFilePath(RulesFileName)); |
|
|
|
if (!rulesFile.exists()) |
|
loadRulesLegacy(); |
|
else if (rulesFile.open(QFile::ReadOnly)) |
|
loadRules(rulesFile.readAll()); |
|
else |
|
LogMsg(tr("Couldn't read RSS AutoDownloader rules from %1. Error: %2") |
|
.arg(rulesFile.fileName(), rulesFile.errorString()), Log::CRITICAL); |
|
} |
|
|
|
void AutoDownloader::loadRules(const QByteArray &data) |
|
{ |
|
try { |
|
const auto rules = rulesFromJSON(data); |
|
for (const auto &rule : rules) |
|
setRule_impl(rule); |
|
} |
|
catch (const ParsingError &error) { |
|
LogMsg(tr("Couldn't load RSS AutoDownloader rules. Reason: %1") |
|
.arg(error.message()), Log::CRITICAL); |
|
} |
|
} |
|
|
|
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::fromLegacyDict(ruleVar.toHash()); |
|
if (!rule.name().isEmpty()) |
|
insertRule(rule); |
|
} |
|
} |
|
|
|
void AutoDownloader::store() |
|
{ |
|
if (!m_dirty) return; |
|
|
|
m_dirty = false; |
|
m_savingTimer.stop(); |
|
|
|
QJsonObject jsonObj; |
|
foreach (auto rule, m_rules) |
|
jsonObj.insert(rule.name(), rule.toJsonObject()); |
|
|
|
m_fileStorage->store(RulesFileName, QJsonDocument(jsonObj).toJson()); |
|
} |
|
|
|
void AutoDownloader::storeDeferred() |
|
{ |
|
if (!m_savingTimer.isActive()) |
|
m_savingTimer.start(5 * 1000, this); |
|
} |
|
|
|
bool AutoDownloader::isProcessingEnabled() const |
|
{ |
|
return m_processingEnabled; |
|
} |
|
|
|
void AutoDownloader::resetProcessingQueue() |
|
{ |
|
m_processingQueue.clear(); |
|
if (!m_processingEnabled) return; |
|
|
|
foreach (Article *article, Session::instance()->rootFolder()->articles()) { |
|
if (!article->isRead() && !article->torrentUrl().isEmpty()) |
|
addJobForArticle(article); |
|
} |
|
} |
|
|
|
void AutoDownloader::startProcessing() |
|
{ |
|
resetProcessingQueue(); |
|
connect(Session::instance()->rootFolder(), &Folder::newArticle, this, &AutoDownloader::handleNewArticle); |
|
} |
|
|
|
void AutoDownloader::setProcessingEnabled(bool enabled) |
|
{ |
|
if (m_processingEnabled != enabled) { |
|
m_processingEnabled = enabled; |
|
SettingsStorage::instance()->storeValue(SettingsKey_ProcessingEnabled, m_processingEnabled); |
|
if (m_processingEnabled) { |
|
startProcessing(); |
|
} |
|
else { |
|
m_processingQueue.clear(); |
|
disconnect(Session::instance()->rootFolder(), &Folder::newArticle, this, &AutoDownloader::handleNewArticle); |
|
} |
|
|
|
emit processingStateChanged(m_processingEnabled); |
|
} |
|
} |
|
|
|
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(); |
|
}
|
|
|