Redesign RSS subsystem

This commit is contained in:
Vladimir Golovnev (Glassez)
2017-03-07 16:10:42 +03:00
parent 090a2edc1a
commit 989a70fe60
64 changed files with 5116 additions and 4727 deletions

View File

@@ -29,15 +29,16 @@
* Contact : chris@qbittorrent.org
*/
#include "rss_parser.h"
#include <QDebug>
#include <QDateTime>
#include <QMetaObject>
#include <QRegExp>
#include <QStringList>
#include <QVariant>
#include <QXmlStreamReader>
#include "rssparser.h"
namespace
{
const char shortDay[][4] = {
@@ -206,10 +207,23 @@ namespace
}
}
using namespace Rss::Private;
using namespace RSS::Private;
const int ParsingResultTypeId = qRegisterMetaType<ParsingResult>();
Parser::Parser(QString lastBuildDate)
{
m_result.lastBuildDate = lastBuildDate;
}
void Parser::parse(const QByteArray &feedData)
{
QMetaObject::invokeMethod(this, "parse_impl", Qt::QueuedConnection
, Q_ARG(QByteArray, feedData));
}
// read and create items from a rss document
void Parser::parse(const QByteArray &feedData)
void Parser::parse_impl(const QByteArray &feedData)
{
qDebug() << Q_FUNC_INFO;
@@ -243,18 +257,28 @@ void Parser::parse(const QByteArray &feedData)
}
if (xml.hasError())
emit finished(xml.errorString());
m_result.error = xml.errorString();
else if (!foundChannel)
emit finished(tr("Invalid RSS feed."));
m_result.error = tr("Invalid RSS feed.");
else
emit finished(QString());
// Sort article list chronologically
// NOTE: We don't need to sort it here if articles are always
// sorted in fetched XML in reverse chronological order
std::sort(m_result.articles.begin(), m_result.articles.end()
, [](const QVariantHash &a1, const QVariantHash &a2)
{
return a1["date"].toDateTime() < a2["date"].toDateTime();
});
emit finished(m_result);
m_result.articles.clear(); // clear articles only
}
void Parser::parseRssArticle(QXmlStreamReader &xml)
{
QVariantHash article;
while(!xml.atEnd()) {
while (!xml.atEnd()) {
xml.readNext();
if(xml.isEndElement() && xml.name() == "item")
@@ -290,28 +314,7 @@ void Parser::parseRssArticle(QXmlStreamReader &xml)
}
}
if (!article.contains("torrent_url") && article.contains("news_link"))
article["torrent_url"] = article["news_link"];
if (!article.contains("id")) {
// Item does not have a guid, fall back to some other identifier
const QString link = article.value("news_link").toString();
if (!link.isEmpty()) {
article["id"] = link;
}
else {
const QString title = article.value("title").toString();
if (!title.isEmpty()) {
article["id"] = title;
}
else {
qWarning() << "Item has no guid, link or title, ignoring it...";
return;
}
}
}
emit newArticle(article);
m_result.articles.prepend(article);
}
void Parser::parseRSSChannel(QXmlStreamReader &xml)
@@ -319,22 +322,21 @@ void Parser::parseRSSChannel(QXmlStreamReader &xml)
qDebug() << Q_FUNC_INFO;
Q_ASSERT(xml.isStartElement() && xml.name() == "channel");
while(!xml.atEnd()) {
while (!xml.atEnd()) {
xml.readNext();
if (xml.isStartElement()) {
if (xml.name() == "title") {
QString title = xml.readElementText();
emit feedTitle(title);
m_result.title = xml.readElementText();
}
else if (xml.name() == "lastBuildDate") {
QString lastBuildDate = xml.readElementText();
if (!lastBuildDate.isEmpty()) {
if (m_lastBuildDate == lastBuildDate) {
if (m_result.lastBuildDate == lastBuildDate) {
qDebug() << "The RSS feed has not changed since last time, aborting parsing.";
return;
}
m_lastBuildDate = lastBuildDate;
m_result.lastBuildDate = lastBuildDate;
}
}
else if (xml.name() == "item") {
@@ -349,10 +351,10 @@ void Parser::parseAtomArticle(QXmlStreamReader &xml)
QVariantHash article;
bool doubleContent = false;
while(!xml.atEnd()) {
while (!xml.atEnd()) {
xml.readNext();
if(xml.isEndElement() && (xml.name() == "entry"))
if (xml.isEndElement() && (xml.name() == "entry"))
break;
if (xml.isStartElement()) {
@@ -360,9 +362,9 @@ void Parser::parseAtomArticle(QXmlStreamReader &xml)
article["title"] = xml.readElementText().trimmed();
}
else if (xml.name() == "link") {
QString link = ( xml.attributes().isEmpty() ?
xml.readElementText().trimmed() :
xml.attributes().value("href").toString() );
QString link = (xml.attributes().isEmpty()
? xml.readElementText().trimmed()
: xml.attributes().value("href").toString());
if (link.startsWith("magnet:", Qt::CaseInsensitive))
article["torrent_url"] = link; // magnet link instead of a news URL
@@ -370,7 +372,7 @@ void Parser::parseAtomArticle(QXmlStreamReader &xml)
// Atom feeds can have relative links, work around this and
// take the stress of figuring article full URI from UI
// Assemble full URI
article["news_link"] = ( m_baseUrl.isEmpty() ? link : m_baseUrl + link );
article["news_link"] = (m_baseUrl.isEmpty() ? link : m_baseUrl + link);
}
else if ((xml.name() == "summary") || (xml.name() == "content")){
@@ -398,8 +400,8 @@ void Parser::parseAtomArticle(QXmlStreamReader &xml)
}
else if (xml.name() == "author") {
xml.readNext();
while(xml.name() != "author") {
if(xml.name() == "name")
while (xml.name() != "author") {
if (xml.name() == "name")
article["author"] = xml.readElementText().trimmed();
xml.readNext();
}
@@ -410,28 +412,7 @@ void Parser::parseAtomArticle(QXmlStreamReader &xml)
}
}
if (!article.contains("torrent_url") && article.contains("news_link"))
article["torrent_url"] = article["news_link"];
if (!article.contains("id")) {
// Item does not have a guid, fall back to some other identifier
const QString link = article.value("news_link").toString();
if (!link.isEmpty()) {
article["id"] = link;
}
else {
const QString title = article.value("title").toString();
if (!title.isEmpty()) {
article["id"] = title;
}
else {
qWarning() << "Item has no guid, link or title, ignoring it...";
return;
}
}
}
emit newArticle(article);
m_result.articles.prepend(article);
}
void Parser::parseAtomChannel(QXmlStreamReader &xml)
@@ -446,17 +427,16 @@ void Parser::parseAtomChannel(QXmlStreamReader &xml)
if (xml.isStartElement()) {
if (xml.name() == "title") {
QString title = xml.readElementText();
emit feedTitle(title);
m_result.title = xml.readElementText();
}
else if (xml.name() == "updated") {
QString lastBuildDate = xml.readElementText();
if (!lastBuildDate.isEmpty()) {
if (m_lastBuildDate == lastBuildDate) {
if (m_result.lastBuildDate == lastBuildDate) {
qDebug() << "The RSS feed has not changed since last time, aborting parsing.";
return;
}
m_lastBuildDate = lastBuildDate;
m_result.lastBuildDate = lastBuildDate;
}
}
else if (xml.name() == "entry") {

View File

@@ -29,41 +29,49 @@
* Contact : chris@qbittorrent.org
*/
#ifndef RSSPARSER_H
#define RSSPARSER_H
#pragma once
#include <QList>
#include <QObject>
#include <QString>
#include <QVariantHash>
class QXmlStreamReader;
namespace Rss
namespace RSS
{
namespace Private
{
struct ParsingResult
{
QString error;
QString lastBuildDate;
QString title;
QList<QVariantHash> articles;
};
class Parser: public QObject
{
Q_OBJECT
public slots:
public:
explicit Parser(QString lastBuildDate);
void parse(const QByteArray &feedData);
signals:
void newArticle(const QVariantHash &rssArticle);
void feedTitle(const QString &title);
void finished(const QString &error);
void finished(const RSS::Private::ParsingResult &result);
private:
Q_INVOKABLE void parse_impl(const QByteArray &feedData);
void parseRssArticle(QXmlStreamReader &xml);
void parseRSSChannel(QXmlStreamReader &xml);
void parseAtomArticle(QXmlStreamReader &xml);
void parseAtomChannel(QXmlStreamReader &xml);
QString m_lastBuildDate; // Optimization
QString m_baseUrl;
ParsingResult m_result;
};
}
}
#endif // RSSPARSER_H
Q_DECLARE_METATYPE(RSS::Private::ParsingResult)

View File

@@ -0,0 +1,178 @@
/*
* Bittorrent Client using Qt and libtorrent.
* Copyright (C) 2017 Vladimir Golovnev <glassez@yandex.ru>
* Copyright (C) 2010 Christophe Dumez <chris@qbittorrent.org>
* Copyright (C) 2010 Arnaud Demaiziere <arnaud@qbittorrent.org>
*
* 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_article.h"
#include <QJsonObject>
#include <QVariant>
#include "rss_feed.h"
const QString Str_Id(QStringLiteral("id"));
const QString Str_Date(QStringLiteral("date"));
const QString Str_Title(QStringLiteral("title"));
const QString Str_Author(QStringLiteral("author"));
const QString Str_Description(QStringLiteral("description"));
const QString Str_TorrentURL(QStringLiteral("torrentURL"));
const QString Str_Torrent_Url(QStringLiteral("torrent_url"));
const QString Str_Link(QStringLiteral("link"));
const QString Str_News_Link(QStringLiteral("news_link"));
const QString Str_IsRead(QStringLiteral("isRead"));
const QString Str_Read(QStringLiteral("read"));
using namespace RSS;
Article::Article(Feed *feed, QString guid, QDateTime date, QString title, QString author
, QString description, QString torrentUrl, QString link, bool isRead)
: QObject(feed)
, m_feed(feed)
, m_guid(guid)
, m_date(date)
, m_title(title)
, m_author(author)
, m_description(description)
, m_torrentURL(torrentUrl)
, m_link(link)
, m_isRead(isRead)
{
}
QString Article::guid() const
{
return m_guid;
}
QDateTime Article::date() const
{
return m_date;
}
QString Article::title() const
{
return m_title;
}
QString Article::author() const
{
return m_author;
}
QString Article::description() const
{
return m_description;
}
QString Article::torrentUrl() const
{
return (m_torrentURL.isEmpty() ? m_link : m_torrentURL);
}
QString Article::link() const
{
return m_link;
}
bool Article::isRead() const
{
return m_isRead;
}
void Article::markAsRead()
{
if (!m_isRead) {
m_isRead = true;
emit read(this);
}
}
QJsonObject Article::toJsonObject() const
{
return {
{Str_Id, m_guid},
{Str_Date, m_date.toString(Qt::RFC2822Date)},
{Str_Title, m_title},
{Str_Author, m_author},
{Str_Description, m_description},
{Str_TorrentURL, m_torrentURL},
{Str_Link, m_link},
{Str_IsRead, m_isRead}
};
}
bool Article::articleDateRecentThan(Article *article, const QDateTime &date)
{
return article->date() > date;
}
Article *Article::fromJsonObject(Feed *feed, const QJsonObject &jsonObj)
{
QString guid = jsonObj.value(Str_Id).toString();
// If item does not have a guid, fall back to some other identifier
if (guid.isEmpty())
guid = jsonObj.value(Str_Torrent_Url).toString();
if (guid.isEmpty())
guid = jsonObj.value(Str_Title).toString();
if (guid.isEmpty()) return nullptr;
return new Article(
feed, guid
, QDateTime::fromString(jsonObj.value(Str_Date).toString(), Qt::RFC2822Date)
, jsonObj.value(Str_Title).toString()
, jsonObj.value(Str_Author).toString()
, jsonObj.value(Str_Description).toString()
, jsonObj.value(Str_TorrentURL).toString()
, jsonObj.value(Str_Link).toString()
, jsonObj.value(Str_IsRead).toBool(false));
}
Article *Article::fromVariantHash(Feed *feed, const QVariantHash &varHash)
{
QString guid = varHash[Str_Id].toString();
// If item does not have a guid, fall back to some other identifier
if (guid.isEmpty())
guid = varHash.value(Str_Torrent_Url).toString();
if (guid.isEmpty())
guid = varHash.value(Str_Title).toString();
if (guid.isEmpty()) nullptr;
return new Article(feed, guid
, varHash.value(Str_Date).toDateTime()
, varHash.value(Str_Title).toString()
, varHash.value(Str_Author).toString()
, varHash.value(Str_Description).toString()
, varHash.value(Str_Torrent_Url).toString()
, varHash.value(Str_News_Link).toString()
, varHash.value(Str_Read, false).toBool());
}
Feed *Article::feed() const
{
return m_feed;
}

View File

@@ -1,5 +1,6 @@
/*
* Bittorrent Client using Qt and libtorrent.
* Copyright (C) 2017 Vladimir Golovnev <glassez@yandex.ru>
* Copyright (C) 2010 Christophe Dumez <chris@qbittorrent.org>
* Copyright (C) 2010 Arnaud Demaiziere <arnaud@qbittorrent.org>
*
@@ -25,67 +26,59 @@
* 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.
*
* Contact: chris@qbittorrent.org, arnaud@qbittorrent.org
*/
#ifndef RSSARTICLE_H
#define RSSARTICLE_H
#pragma once
#include <QDateTime>
#include <QVariantHash>
#include <QSharedPointer>
#include <QObject>
#include <QString>
namespace Rss
namespace RSS
{
class Feed;
class Article;
typedef QSharedPointer<Article> ArticlePtr;
// Item of a rss stream, single information
class Article: public QObject
{
Q_OBJECT
Q_DISABLE_COPY(Article)
friend class Feed;
Article(Feed *feed, QString guid, QDateTime date, QString title, QString author
, QString description, QString torrentUrl, QString link, bool isRead = false);
static Article *fromJsonObject(Feed *feed, const QJsonObject &jsonObj);
static Article *fromVariantHash(Feed *feed, const QVariantHash &varHash);
public:
Article(Feed *parent, const QString &guid);
// Accessors
bool hasAttachment() const;
const QString &guid() const;
Feed *parent() const;
const QString &title() const;
const QString &author() const;
const QString &torrentUrl() const;
const QString &link() const;
Feed *feed() const;
QString guid() const;
QDateTime date() const;
QString title() const;
QString author() const;
QString description() const;
const QDateTime &date() const;
QString torrentUrl() const;
QString link() const;
bool isRead() const;
// Setters
void markAsRead();
// Serialization
QVariantHash toHash() const;
static ArticlePtr fromHash(Feed *parent, const QVariantHash &hash);
QJsonObject toJsonObject() const;
static bool articleDateRecentThan(Article *article, const QDateTime &date);
signals:
void articleWasRead();
public slots:
void handleTorrentDownloadSuccess(const QString &url);
void read(Article *article = nullptr);
private:
Feed *m_parent;
Feed *m_feed = nullptr;
QString m_guid;
QString m_title;
QString m_torrentUrl;
QString m_link;
QString m_description;
QDateTime m_date;
QString m_title;
QString m_author;
bool m_read;
QString m_description;
QString m_torrentURL;
QString m_link;
bool m_isRead = false;
};
}
#endif // RSSARTICLE_H

View File

@@ -0,0 +1,390 @@
/*
* 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 <QDebug>
#include <QJsonArray>
#include <QJsonDocument>
#include <QJsonObject>
#include <QJsonValue>
#include <QSaveFile>
#include <QThread>
#include <QTimer>
#include "../bittorrent/magneturi.h"
#include "../bittorrent/session.h"
#include "../asyncfilestorage.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;
QString articleGUID;
QString articleTitle;
QDateTime articleDate;
QString torrentURL;
};
const QString ConfFolderName(QStringLiteral("rss"));
const QString RulesFileName(QStringLiteral("download_rules.json"));
const QString SettingsKey_ProcessingEnabled(QStringLiteral("RSS/AutoDownloader/EnableProcessing"));
using namespace RSS;
QPointer<AutoDownloader> AutoDownloader::m_instance = nullptr;
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)
{
Logger::instance()->addMessage(QString("Couldn't save RSS AutoDownloader data in %1. Error: %2")
.arg(fileName).arg(errorString), Log::WARNING);
});
m_ioThread->start();
connect(BitTorrent::Session::instance(), &BitTorrent::Session::downloadFromUrlFinished
, this, &AutoDownloader::handleTorrentDownloadFinished);
connect(BitTorrent::Session::instance(), &BitTorrent::Session::downloadFromUrlFailed
, this, &AutoDownloader::handleTorrentDownloadFailed);
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();
}
}
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 (auto feed = Session::instance()->feedByURL(job->feedURL))
if (auto article = feed->articleByGUID(job->articleGUID))
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->articleGUID = article->guid();
job->articleTitle = article->title();
job->articleDate = article->date();
job->torrentURL = torrentURL;
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.matches(job->articleTitle)) continue;
// if rule is in ignoring state do nothing with matched torrent
if (rule.ignoreDays() > 0) {
if (rule.lastMatch().isValid()) {
if (job->articleDate < rule.lastMatch().addDays(rule.ignoreDays()))
return;
}
}
rule.setLastMatch(job->articleDate);
m_dirty = true;
storeDeferred();
BitTorrent::AddTorrentParams params;
params.savePath = rule.savePath();
params.category = rule.assignedCategory();
params.addPaused = rule.addPaused();
BitTorrent::Session::instance()->addTorrent(job->torrentURL, params);
if (BitTorrent::MagnetUri(job->torrentURL).isValid()) {
if (auto feed = Session::instance()->feedByURL(job->feedURL)) {
if (auto article = feed->articleByGUID(job->articleGUID))
article->markAsRead();
}
}
else {
// waiting for torrent file downloading
// normalize URL string via QUrl since DownloadManager do it
m_waitingJobs.insert(QUrl(job->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
Logger::instance()->addMessage(
QString("Couldn't read RSS AutoDownloader rules from %1. Error: %2")
.arg(rulesFile.fileName()).arg(rulesFile.errorString()), Log::WARNING);
}
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;
}
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));
}
}
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());
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();
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();
}

View File

@@ -0,0 +1,114 @@
/*
* 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.
*/
#pragma once
#include <QBasicTimer>
#include <QHash>
#include <QList>
#include <QObject>
#include <QPointer>
#include <QSharedPointer>
class QThread;
class QTimer;
class Application;
class AsyncFileStorage;
struct ProcessingJob;
namespace RSS
{
class Article;
class Feed;
class Item;
class AutoDownloadRule;
class AutoDownloader final: public QObject
{
Q_OBJECT
Q_DISABLE_COPY(AutoDownloader)
friend class ::Application;
AutoDownloader();
~AutoDownloader() override;
public:
static AutoDownloader *instance();
bool isProcessingEnabled() const;
void setProcessingEnabled(bool enabled);
bool hasRule(const QString &ruleName) const;
AutoDownloadRule ruleByName(const QString &ruleName) const;
QList<AutoDownloadRule> rules() const;
void insertRule(const AutoDownloadRule &rule);
bool renameRule(const QString &ruleName, const QString &newRuleName);
void removeRule(const QString &ruleName);
signals:
void processingStateChanged(bool enabled);
void ruleAdded(const QString &ruleName);
void ruleChanged(const QString &ruleName);
void ruleRenamed(const QString &ruleName, const QString &oldRuleName);
void ruleAboutToBeRemoved(const QString &ruleName);
private slots:
void process();
void handleTorrentDownloadFinished(const QString &url);
void handleTorrentDownloadFailed(const QString &url);
void handleNewArticle(Article *article);
private:
void timerEvent(QTimerEvent *event) override;
void setRule_impl(const AutoDownloadRule &rule);
void resetProcessingQueue();
void startProcessing();
void addJobForArticle(Article *article);
void processJob(const QSharedPointer<ProcessingJob> &job);
void load();
void loadRules(const QByteArray &data);
void loadRulesLegacy();
void store();
void storeDeferred();
static QPointer<AutoDownloader> m_instance;
bool m_processingEnabled;
QTimer *m_processingTimer;
QThread *m_ioThread;
AsyncFileStorage *m_fileStorage;
QHash<QString, AutoDownloadRule> m_rules;
QList<QSharedPointer<ProcessingJob>> m_processingQueue;
QHash<QString, QSharedPointer<ProcessingJob>> m_waitingJobs;
bool m_dirty = false;
QBasicTimer m_savingTimer;
};
}

View File

@@ -0,0 +1,538 @@
/*
* Bittorrent Client using Qt and libtorrent.
* Copyright (C) 2017 Vladimir Golovnev <glassez@yandex.ru>
* Copyright (C) 2010 Christophe Dumez <chris@qbittorrent.org>
*
* 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_autodownloadrule.h"
#include <QDebug>
#include <QDir>
#include <QHash>
#include <QJsonArray>
#include <QJsonObject>
#include <QRegExp>
#include <QRegularExpression>
#include <QSharedData>
#include <QString>
#include <QStringList>
#include "../preferences.h"
#include "../tristatebool.h"
#include "../utils/fs.h"
#include "../utils/string.h"
#include "rss_feed.h"
#include "rss_article.h"
namespace
{
TriStateBool jsonValueToTriStateBool(const QJsonValue &jsonVal)
{
if (jsonVal.isBool())
return TriStateBool(jsonVal.toBool());
if (!jsonVal.isNull())
qDebug() << Q_FUNC_INFO << "Incorrect value" << jsonVal.toVariant();
return TriStateBool::Undefined;
}
QJsonValue triStateBoolToJsonValue(const TriStateBool &triStateBool)
{
switch (static_cast<int>(triStateBool)) {
case 0: return false; break;
case 1: return true; break;
default: return QJsonValue();
}
}
}
const QString Str_Name(QStringLiteral("name"));
const QString Str_Enabled(QStringLiteral("enabled"));
const QString Str_UseRegex(QStringLiteral("useRegex"));
const QString Str_MustContain(QStringLiteral("mustContain"));
const QString Str_MustNotContain(QStringLiteral("mustNotContain"));
const QString Str_EpisodeFilter(QStringLiteral("episodeFilter"));
const QString Str_AffectedFeeds(QStringLiteral("affectedFeeds"));
const QString Str_SavePath(QStringLiteral("savePath"));
const QString Str_AssignedCategory(QStringLiteral("assignedCategory"));
const QString Str_LastMatch(QStringLiteral("lastMatch"));
const QString Str_IgnoreDays(QStringLiteral("ignoreDays"));
const QString Str_AddPaused(QStringLiteral("addPaused"));
namespace RSS
{
struct AutoDownloadRuleData: public QSharedData
{
QString name;
bool enabled = true;
QStringList mustContain;
QStringList mustNotContain;
QString episodeFilter;
QStringList feedURLs;
bool useRegex = false;
int ignoreDays = 0;
QDateTime lastMatch;
QString savePath;
QString category;
TriStateBool addPaused = TriStateBool::Undefined;
mutable QHash<QString, QRegularExpression> cachedRegexes;
bool operator==(const AutoDownloadRuleData &other) const
{
return (name == other.name)
&& (enabled == other.enabled)
&& (mustContain == other.mustContain)
&& (mustNotContain == other.mustNotContain)
&& (episodeFilter == other.episodeFilter)
&& (feedURLs == other.feedURLs)
&& (useRegex == other.useRegex)
&& (ignoreDays == other.ignoreDays)
&& (lastMatch == other.lastMatch)
&& (savePath == other.savePath)
&& (category == other.category)
&& (addPaused == other.addPaused);
}
};
}
using namespace RSS;
AutoDownloadRule::AutoDownloadRule(const QString &name)
: m_dataPtr(new AutoDownloadRuleData)
{
setName(name);
}
AutoDownloadRule::AutoDownloadRule(const AutoDownloadRule &other)
: m_dataPtr(other.m_dataPtr)
{
}
AutoDownloadRule::~AutoDownloadRule() {}
QRegularExpression AutoDownloadRule::cachedRegex(const QString &expression, bool isRegex) const
{
// Use a cache of regexes so we don't have to continually recompile - big performance increase.
// The cache is cleared whenever the regex/wildcard, must or must not contain fields or
// episode filter are modified.
Q_ASSERT(!expression.isEmpty());
QRegularExpression regex(m_dataPtr->cachedRegexes[expression]);
if (!regex.pattern().isEmpty())
return regex;
return m_dataPtr->cachedRegexes[expression] = QRegularExpression(isRegex ? expression : Utils::String::wildcardToRegex(expression), QRegularExpression::CaseInsensitiveOption);
}
bool AutoDownloadRule::matches(const QString &articleTitle, const QString &expression) const
{
static QRegularExpression whitespace("\\s+");
if (expression.isEmpty()) {
// A regex of the form "expr|" will always match, so do the same for wildcards
return true;
}
else if (m_dataPtr->useRegex) {
QRegularExpression reg(cachedRegex(expression));
return reg.match(articleTitle).hasMatch();
}
else {
// Only match if every wildcard token (separated by spaces) is present in the article name.
// Order of wildcard tokens is unimportant (if order is important, they should have used *).
foreach (const QString &wildcard, expression.split(whitespace, QString::SplitBehavior::SkipEmptyParts)) {
QRegularExpression reg(cachedRegex(wildcard, false));
if (!reg.match(articleTitle).hasMatch())
return false;
}
}
return true;
}
bool AutoDownloadRule::matches(const QString &articleTitle) const
{
if (!m_dataPtr->mustContain.empty()) {
bool logged = false;
bool foundMustContain = false;
// Each expression is either a regex, or a set of wildcards separated by whitespace.
// Accept if any complete expression matches.
foreach (const QString &expression, m_dataPtr->mustContain) {
if (!logged) {
// qDebug() << "Checking matching" << (m_dataPtr->useRegex ? "regex:" : "wildcard expressions:") << m_dataPtr->mustContain.join("|");
logged = true;
}
// A regex of the form "expr|" will always match, so do the same for wildcards
foundMustContain = matches(articleTitle, expression);
if (foundMustContain) {
// qDebug() << "Found matching" << (m_dataPtr->useRegex ? "regex:" : "wildcard expression:") << expression;
break;
}
}
if (!foundMustContain)
return false;
}
if (!m_dataPtr->mustNotContain.empty()) {
bool logged = false;
// Each expression is either a regex, or a set of wildcards separated by whitespace.
// Reject if any complete expression matches.
foreach (const QString &expression, m_dataPtr->mustNotContain) {
if (!logged) {
// qDebug() << "Checking not matching" << (m_dataPtr->useRegex ? "regex:" : "wildcard expressions:") << m_dataPtr->mustNotContain.join("|");
logged = true;
}
// A regex of the form "expr|" will always match, so do the same for wildcards
if (matches(articleTitle, expression)) {
// qDebug() << "Found not matching" << (m_dataPtr->useRegex ? "regex:" : "wildcard expression:") << expression;
return false;
}
}
}
if (!m_dataPtr->episodeFilter.isEmpty()) {
// qDebug() << "Checking episode filter:" << m_dataPtr->episodeFilter;
QRegularExpression f(cachedRegex("(^\\d{1,4})x(.*;$)"));
QRegularExpressionMatch matcher = f.match(m_dataPtr->episodeFilter);
bool matched = matcher.hasMatch();
if (!matched)
return false;
QString s = matcher.captured(1);
QStringList eps = matcher.captured(2).split(";");
int sOurs = s.toInt();
foreach (QString ep, eps) {
if (ep.isEmpty())
continue;
// We need to trim leading zeroes, but if it's all zeros then we want episode zero.
while (ep.size() > 1 && ep.startsWith("0"))
ep = ep.right(ep.size() - 1);
if (ep.indexOf('-') != -1) { // Range detected
QString partialPattern1 = "\\bs0?(\\d{1,4})[ -_\\.]?e(0?\\d{1,4})(?:\\D|\\b)";
QString partialPattern2 = "\\b(\\d{1,4})x(0?\\d{1,4})(?:\\D|\\b)";
QRegularExpression reg(cachedRegex(partialPattern1));
if (ep.endsWith('-')) { // Infinite range
int epOurs = ep.left(ep.size() - 1).toInt();
// Extract partial match from article and compare as digits
matcher = reg.match(articleTitle);
matched = matcher.hasMatch();
if (!matched) {
reg = QRegularExpression(cachedRegex(partialPattern2));
matcher = reg.match(articleTitle);
matched = matcher.hasMatch();
}
if (matched) {
int sTheirs = matcher.captured(1).toInt();
int epTheirs = matcher.captured(2).toInt();
if (((sTheirs == sOurs) && (epTheirs >= epOurs)) || (sTheirs > sOurs)) {
// qDebug() << "Matched episode:" << ep;
// qDebug() << "Matched article:" << articleTitle;
return true;
}
}
}
else { // Normal range
QStringList range = ep.split('-');
Q_ASSERT(range.size() == 2);
if (range.first().toInt() > range.last().toInt())
continue; // Ignore this subrule completely
int epOursFirst = range.first().toInt();
int epOursLast = range.last().toInt();
// Extract partial match from article and compare as digits
matcher = reg.match(articleTitle);
matched = matcher.hasMatch();
if (!matched) {
reg = QRegularExpression(cachedRegex(partialPattern2));
matcher = reg.match(articleTitle);
matched = matcher.hasMatch();
}
if (matched) {
int sTheirs = matcher.captured(1).toInt();
int epTheirs = matcher.captured(2).toInt();
if ((sTheirs == sOurs) && ((epOursFirst <= epTheirs) && (epOursLast >= epTheirs))) {
// qDebug() << "Matched episode:" << ep;
// qDebug() << "Matched article:" << articleTitle;
return true;
}
}
}
}
else { // Single number
QString expStr("\\b(?:s0?" + s + "[ -_\\.]?" + "e0?" + ep + "|" + s + "x" + "0?" + ep + ")(?:\\D|\\b)");
QRegularExpression reg(cachedRegex(expStr));
if (reg.match(articleTitle).hasMatch()) {
// qDebug() << "Matched episode:" << ep;
// qDebug() << "Matched article:" << articleTitle;
return true;
}
}
}
return false;
}
// qDebug() << "Matched article:" << articleTitle;
return true;
}
AutoDownloadRule &AutoDownloadRule::operator=(const AutoDownloadRule &other)
{
m_dataPtr = other.m_dataPtr;
return *this;
}
bool AutoDownloadRule::operator==(const AutoDownloadRule &other) const
{
return (m_dataPtr == other.m_dataPtr) // optimization
|| (*m_dataPtr == *other.m_dataPtr);
}
bool AutoDownloadRule::operator!=(const AutoDownloadRule &other) const
{
return !operator==(other);
}
QJsonObject AutoDownloadRule::toJsonObject() const
{
return {{Str_Enabled, isEnabled()}
, {Str_UseRegex, useRegex()}
, {Str_MustContain, mustContain()}
, {Str_MustNotContain, mustNotContain()}
, {Str_EpisodeFilter, episodeFilter()}
, {Str_AffectedFeeds, QJsonArray::fromStringList(feedURLs())}
, {Str_SavePath, savePath()}
, {Str_AssignedCategory, assignedCategory()}
, {Str_LastMatch, lastMatch().toString(Qt::RFC2822Date)}
, {Str_IgnoreDays, ignoreDays()}
, {Str_AddPaused, triStateBoolToJsonValue(addPaused())}};
}
AutoDownloadRule AutoDownloadRule::fromJsonObject(const QJsonObject &jsonObj, const QString &name)
{
AutoDownloadRule rule(name.isEmpty() ? jsonObj.value(Str_Name).toString() : name);
rule.setUseRegex(jsonObj.value(Str_UseRegex).toBool(false));
rule.setMustContain(jsonObj.value(Str_MustContain).toString());
rule.setMustNotContain(jsonObj.value(Str_MustNotContain).toString());
rule.setEpisodeFilter(jsonObj.value(Str_EpisodeFilter).toString());
rule.setEnabled(jsonObj.value(Str_Enabled).toBool(true));
rule.setSavePath(jsonObj.value(Str_SavePath).toString());
rule.setCategory(jsonObj.value(Str_AssignedCategory).toString());
rule.setAddPaused(jsonValueToTriStateBool(jsonObj.value(Str_AddPaused)));
rule.setLastMatch(QDateTime::fromString(jsonObj.value(Str_LastMatch).toString(), Qt::RFC2822Date));
rule.setIgnoreDays(jsonObj.value(Str_IgnoreDays).toInt());
const QJsonValue feedsVal = jsonObj.value(Str_AffectedFeeds);
QStringList feedURLs;
if (feedsVal.isString())
feedURLs << feedsVal.toString();
else foreach (const QJsonValue &urlVal, feedsVal.toArray())
feedURLs << urlVal.toString();
rule.setFeedURLs(feedURLs);
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());
return rule;
}
void AutoDownloadRule::setMustContain(const QString &tokens)
{
m_dataPtr->cachedRegexes.clear();
if (m_dataPtr->useRegex)
m_dataPtr->mustContain = QStringList() << tokens;
else
m_dataPtr->mustContain = tokens.split("|");
// Check for single empty string - if so, no condition
if ((m_dataPtr->mustContain.size() == 1) && m_dataPtr->mustContain[0].isEmpty())
m_dataPtr->mustContain.clear();
}
void AutoDownloadRule::setMustNotContain(const QString &tokens)
{
m_dataPtr->cachedRegexes.clear();
if (m_dataPtr->useRegex)
m_dataPtr->mustNotContain = QStringList() << tokens;
else
m_dataPtr->mustNotContain = tokens.split("|");
// Check for single empty string - if so, no condition
if ((m_dataPtr->mustNotContain.size() == 1) && m_dataPtr->mustNotContain[0].isEmpty())
m_dataPtr->mustNotContain.clear();
}
QStringList AutoDownloadRule::feedURLs() const
{
return m_dataPtr->feedURLs;
}
void AutoDownloadRule::setFeedURLs(const QStringList &rssFeeds)
{
m_dataPtr->feedURLs = rssFeeds;
}
QString AutoDownloadRule::name() const
{
return m_dataPtr->name;
}
void AutoDownloadRule::setName(const QString &name)
{
m_dataPtr->name = name;
}
QString AutoDownloadRule::savePath() const
{
return m_dataPtr->savePath;
}
void AutoDownloadRule::setSavePath(const QString &savePath)
{
m_dataPtr->savePath = Utils::Fs::fromNativePath(savePath);
}
TriStateBool AutoDownloadRule::addPaused() const
{
return m_dataPtr->addPaused;
}
void AutoDownloadRule::setAddPaused(const TriStateBool &addPaused)
{
m_dataPtr->addPaused = addPaused;
}
QString AutoDownloadRule::assignedCategory() const
{
return m_dataPtr->category;
}
void AutoDownloadRule::setCategory(const QString &category)
{
m_dataPtr->category = category;
}
bool AutoDownloadRule::isEnabled() const
{
return m_dataPtr->enabled;
}
void AutoDownloadRule::setEnabled(bool enable)
{
m_dataPtr->enabled = enable;
}
QDateTime AutoDownloadRule::lastMatch() const
{
return m_dataPtr->lastMatch;
}
void AutoDownloadRule::setLastMatch(const QDateTime &lastMatch)
{
m_dataPtr->lastMatch = lastMatch;
}
void AutoDownloadRule::setIgnoreDays(int d)
{
m_dataPtr->ignoreDays = d;
}
int AutoDownloadRule::ignoreDays() const
{
return m_dataPtr->ignoreDays;
}
QString AutoDownloadRule::mustContain() const
{
return m_dataPtr->mustContain.join("|");
}
QString AutoDownloadRule::mustNotContain() const
{
return m_dataPtr->mustNotContain.join("|");
}
bool AutoDownloadRule::useRegex() const
{
return m_dataPtr->useRegex;
}
void AutoDownloadRule::setUseRegex(bool enabled)
{
m_dataPtr->useRegex = enabled;
m_dataPtr->cachedRegexes.clear();
}
QString AutoDownloadRule::episodeFilter() const
{
return m_dataPtr->episodeFilter;
}
void AutoDownloadRule::setEpisodeFilter(const QString &e)
{
m_dataPtr->episodeFilter = e;
m_dataPtr->cachedRegexes.clear();
}

View File

@@ -1,6 +1,7 @@
/*
* Bittorrent Client using Qt and libtorrent.
* Copyright (C) 2010 Christophe Dumez
* Copyright (C) 2017 Vladimir Golovnev <glassez@yandex.ru>
* Copyright (C) 2010 Christophe Dumez <chris@qbittorrent.org>
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License
@@ -24,91 +25,71 @@
* 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.
*
* Contact : chris@qbittorrent.org
*/
#ifndef RSSDOWNLOADRULE_H
#define RSSDOWNLOADRULE_H
#pragma once
#include <QDateTime>
#include <QVariantHash>
#include <QSharedPointer>
#include <QStringList>
#include <QSharedDataPointer>
#include <QVariant>
template <class T, class U> class QHash;
class QJsonObject;
class QRegularExpression;
class TriStateBool;
namespace Rss
namespace RSS
{
class Feed;
typedef QSharedPointer<Feed> FeedPtr;
struct AutoDownloadRuleData;
class DownloadRule;
typedef QSharedPointer<DownloadRule> DownloadRulePtr;
class DownloadRule
class AutoDownloadRule
{
public:
enum AddPausedState
{
USE_GLOBAL = 0,
ALWAYS_PAUSED,
NEVER_PAUSED
};
explicit AutoDownloadRule(const QString &name = "");
AutoDownloadRule(const AutoDownloadRule &other);
~AutoDownloadRule();
DownloadRule();
~DownloadRule();
static DownloadRulePtr fromVariantHash(const QVariantHash &ruleHash);
QVariantHash toVariantHash() const;
bool matches(const QString &articleTitle) const;
void setMustContain(const QString &tokens);
void setMustNotContain(const QString &tokens);
QStringList rssFeeds() const;
void setRssFeeds(const QStringList &rssFeeds);
QString name() const;
void setName(const QString &name);
QString savePath() const;
void setSavePath(const QString &savePath);
AddPausedState addPaused() const;
void setAddPaused(const AddPausedState &aps);
QString category() const;
void setCategory(const QString &category);
bool isEnabled() const;
void setEnabled(bool enable);
void setLastMatch(const QDateTime &d);
QDateTime lastMatch() const;
void setIgnoreDays(int d);
int ignoreDays() const;
QString mustContain() const;
void setMustContain(const QString &tokens);
QString mustNotContain() const;
void setMustNotContain(const QString &tokens);
QStringList feedURLs() const;
void setFeedURLs(const QStringList &feedURLs);
int ignoreDays() const;
void setIgnoreDays(int d);
QDateTime lastMatch() const;
void setLastMatch(const QDateTime &lastMatch);
bool useRegex() const;
void setUseRegex(bool enabled);
QString episodeFilter() const;
void setEpisodeFilter(const QString &e);
QStringList findMatchingArticles(const FeedPtr &feed) const;
// Operators
bool operator==(const DownloadRule &other) const;
QString savePath() const;
void setSavePath(const QString &savePath);
TriStateBool addPaused() const;
void setAddPaused(const TriStateBool &addPaused);
QString assignedCategory() const;
void setCategory(const QString &assignedCategory);
bool matches(const QString &articleTitle) const;
AutoDownloadRule &operator=(const AutoDownloadRule &other);
bool operator==(const AutoDownloadRule &other) const;
bool operator!=(const AutoDownloadRule &other) const;
QJsonObject toJsonObject() const;
static AutoDownloadRule fromJsonObject(const QJsonObject &jsonObj, const QString &name = "");
static AutoDownloadRule fromVariantHash(const QVariantHash &varHash);
private:
bool matches(const QString &articleTitle, const QString &expression) const;
QRegularExpression cachedRegex(const QString &expression, bool isRegex = true) const;
QString m_name;
QStringList m_mustContain;
QStringList m_mustNotContain;
QString m_episodeFilter;
QString m_savePath;
QString m_category;
bool m_enabled;
QStringList m_rssFeeds;
bool m_useRegex;
AddPausedState m_apstate;
QDateTime m_lastMatch;
int m_ignoreDays;
mutable QHash<QString, QRegularExpression> *m_cachedRegexes;
QSharedDataPointer<AutoDownloadRuleData> m_dataPtr;
};
}
#endif // RSSDOWNLOADRULE_H

437
src/base/rss/rss_feed.cpp Normal file
View File

@@ -0,0 +1,437 @@
/*
* Bittorrent Client using Qt and libtorrent.
* Copyright (C) 2015, 2017 Vladimir Golovnev <glassez@yandex.ru>
* Copyright (C) 2010 Christophe Dumez <chris@qbittorrent.org>
* Copyright (C) 2010 Arnaud Demaiziere <arnaud@qbittorrent.org>
*
* 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_feed.h"
#include <QCryptographicHash>
#include <QDir>
#include <QJsonArray>
#include <QJsonDocument>
#include <QJsonObject>
#include <QJsonValue>
#include <QScopedPointer>
#include <QUrl>
#include "../asyncfilestorage.h"
#include "../logger.h"
#include "../net/downloadhandler.h"
#include "../net/downloadmanager.h"
#include "../profile.h"
#include "../utils/fs.h"
#include "private/rss_parser.h"
#include "rss_article.h"
#include "rss_session.h"
const QString Str_Url(QStringLiteral("url"));
const QString Str_Title(QStringLiteral("title"));
const QString Str_LastBuildDate(QStringLiteral("lastBuildDate"));
const QString Str_IsLoading(QStringLiteral("isLoading"));
const QString Str_HasError(QStringLiteral("hasError"));
const QString Str_Articles(QStringLiteral("articles"));
using namespace RSS;
Feed::Feed(const QString &url, const QString &path, Session *session)
: Item(path)
, m_session(session)
, m_url(url)
{
m_dataFileName = QString("%1.json").arg(Utils::Fs::toValidFileSystemName(m_url, false, QLatin1String("_")));
m_parser = new Private::Parser(m_lastBuildDate);
m_parser->moveToThread(m_session->workingThread());
connect(this, &Feed::destroyed, m_parser, &Private::Parser::deleteLater);
connect(m_parser, &Private::Parser::finished, this, &Feed::handleParsingFinished);
connect(m_session, &Session::maxArticlesPerFeedChanged, this, &Feed::handleMaxArticlesPerFeedChanged);
if (m_session->isProcessingEnabled())
downloadIcon();
else
connect(m_session, &Session::processingStateChanged, this, &Feed::handleSessionProcessingEnabledChanged);
load();
}
Feed::~Feed()
{
emit aboutToBeDestroyed(this);
Utils::Fs::forceRemove(m_iconPath);
}
QList<Article *> Feed::articles() const
{
return m_articlesByDate;
}
void Feed::markAsRead()
{
auto oldUnreadCount = m_unreadCount;
foreach (Article *article, m_articles) {
if (!article->isRead()) {
article->disconnect(this);
article->markAsRead();
--m_unreadCount;
emit articleRead(article);
}
}
if (m_unreadCount != oldUnreadCount) {
m_dirty = true;
store();
emit unreadCountChanged(this);
}
}
void Feed::refresh()
{
if (isLoading()) return;
// NOTE: Should we allow manually refreshing for disabled session?
Net::DownloadHandler *handler = Net::DownloadManager::instance()->downloadUrl(m_url);
connect(handler
, static_cast<void (Net::DownloadHandler::*)(const QString &, const QByteArray &)>(&Net::DownloadHandler::downloadFinished)
, this, &Feed::handleDownloadFinished);
connect(handler, &Net::DownloadHandler::downloadFailed, this, &Feed::handleDownloadFailed);
m_isLoading = true;
emit stateChanged(this);
}
QString Feed::url() const
{
return m_url;
}
QString Feed::title() const
{
return m_title;
}
bool Feed::isLoading() const
{
return m_isLoading;
}
QString Feed::lastBuildDate() const
{
return m_lastBuildDate;
}
int Feed::unreadCount() const
{
return m_unreadCount;
}
Article *Feed::articleByGUID(const QString &guid) const
{
return m_articles.value(guid);
}
void Feed::handleMaxArticlesPerFeedChanged(int n)
{
while (m_articlesByDate.size() > n)
removeOldestArticle();
// We don't need store articles here
}
void Feed::handleIconDownloadFinished(const QString &url, const QString &filePath)
{
Q_UNUSED(url);
m_iconPath = Utils::Fs::fromNativePath(filePath);
emit iconLoaded(this);
}
bool Feed::hasError() const
{
return m_hasError;
}
void Feed::handleDownloadFinished(const QString &url, const QByteArray &data)
{
qDebug() << "Successfully downloaded RSS feed at" << url;
// Parse the download RSS
m_parser->parse(data);
}
void Feed::handleDownloadFailed(const QString &url, const QString &error)
{
m_isLoading = false;
m_hasError = true;
emit stateChanged(this);
qWarning() << "Failed to download RSS feed at" << url;
qWarning() << "Reason:" << error;
}
void Feed::handleParsingFinished(const RSS::Private::ParsingResult &result)
{
if (!result.error.isEmpty()) {
qWarning() << "Failed to parse RSS feed at" << m_url;
qWarning() << "Reason:" << result.error;
}
else {
if (title() != result.title) {
m_title = result.title;
emit titleChanged(this);
}
m_lastBuildDate = result.lastBuildDate;
foreach (const QVariantHash &varHash, result.articles) {
auto article = Article::fromVariantHash(this, varHash);
if (article) {
if (!addArticle(article))
delete article;
else
m_dirty = true;
}
}
store();
}
m_isLoading = false;
m_hasError = false;
emit stateChanged(this);
}
void Feed::load()
{
QFile file(m_session->dataFileStorage()->storageDir().absoluteFilePath(m_dataFileName));
if (!file.exists()) {
loadArticlesLegacy();
m_dirty = true;
store(); // convert to new format
}
else if (file.open(QFile::ReadOnly)) {
loadArticles(file.readAll());
file.close();
}
else {
Logger::instance()->addMessage(
QString("Couldn't read RSS AutoDownloader rules from %1. Error: %2")
.arg(m_dataFileName).arg(file.errorString()), Log::WARNING);
}
}
void Feed::loadArticles(const QByteArray &data)
{
QJsonParseError jsonError;
QJsonDocument jsonDoc = QJsonDocument::fromJson(data, &jsonError);
if (jsonError.error != QJsonParseError::NoError) {
Logger::instance()->addMessage(
QString("Couldn't parse RSS Session data. Error: %1")
.arg(jsonError.errorString()), Log::WARNING);
return;
}
if (!jsonDoc.isArray()) {
Logger::instance()->addMessage(
QString("Couldn't load RSS Session data. Invalid data format."), Log::WARNING);
return;
}
QJsonArray jsonArr = jsonDoc.array();
int i = -1;
foreach (const QJsonValue &jsonVal, jsonArr) {
++i;
if (!jsonVal.isObject()) {
Logger::instance()->addMessage(
QString("Couldn't load RSS article '%1#%2'. Invalid data format.").arg(m_url).arg(i)
, Log::WARNING);
continue;
}
auto article = Article::fromJsonObject(this, jsonVal.toObject());
if (article && !addArticle(article))
delete article;
}
}
void Feed::loadArticlesLegacy()
{
SettingsPtr qBTRSSFeeds = Profile::instance().applicationSettings(QStringLiteral("qBittorrent-rss-feeds"));
QVariantHash allOldItems = qBTRSSFeeds->value("old_items").toHash();
foreach (const QVariant &var, allOldItems.value(m_url).toList()) {
auto article = Article::fromVariantHash(this, var.toHash());
if (article && !addArticle(article))
delete article;
}
}
void Feed::store()
{
if (!m_dirty) return;
m_dirty = false;
m_savingTimer.stop();
QJsonArray jsonArr;
foreach (Article *article, m_articles)
jsonArr << article->toJsonObject();
m_session->dataFileStorage()->store(m_dataFileName, QJsonDocument(jsonArr).toJson());
}
void Feed::storeDeferred()
{
if (!m_savingTimer.isActive())
m_savingTimer.start(5 * 1000, this);
}
bool Feed::addArticle(Article *article)
{
Q_ASSERT(article);
if (m_articles.contains(article->guid()))
return false;
// Insertion sort
const int maxArticles = m_session->maxArticlesPerFeed();
auto lowerBound = std::lower_bound(m_articlesByDate.begin(), m_articlesByDate.end()
, article->date(), Article::articleDateRecentThan);
if ((lowerBound - m_articlesByDate.begin()) >= maxArticles)
return false; // we reach max articles
m_articles[article->guid()] = article;
m_articlesByDate.insert(lowerBound, article);
if (!article->isRead()) {
increaseUnreadCount();
connect(article, &Article::read, this, &Feed::handleArticleRead);
}
emit newArticle(article);
if (m_articlesByDate.size() > maxArticles)
removeOldestArticle();
return true;
}
void Feed::removeOldestArticle()
{
auto oldestArticle = m_articlesByDate.takeLast();
m_articles.remove(oldestArticle->guid());
emit articleAboutToBeRemoved(oldestArticle);
bool isRead = oldestArticle->isRead();
delete oldestArticle;
if (!isRead)
decreaseUnreadCount();
}
void Feed::increaseUnreadCount()
{
++m_unreadCount;
emit unreadCountChanged(this);
}
void Feed::decreaseUnreadCount()
{
Q_ASSERT(m_unreadCount > 0);
--m_unreadCount;
emit unreadCountChanged(this);
}
void Feed::downloadIcon()
{
// Download the RSS Feed icon
// XXX: This works for most sites but it is not perfect
const QUrl url(m_url);
auto iconUrl = QString("%1://%2/favicon.ico").arg(url.scheme()).arg(url.host());
Net::DownloadHandler *handler = Net::DownloadManager::instance()->downloadUrl(iconUrl, true);
connect(handler
, static_cast<void (Net::DownloadHandler::*)(const QString &, const QString &)>(&Net::DownloadHandler::downloadFinished)
, this, &Feed::handleIconDownloadFinished);
}
QString Feed::iconPath() const
{
return m_iconPath;
}
QJsonValue Feed::toJsonValue(bool withData) const
{
if (!withData) {
// if feed alias is empty we create "reduced" JSON
// value for it since its name is equal to its URL
return (name() == url() ? "" : url());
// if we'll need storing some more properties we should check
// for its default values and produce JSON object instead of (if it's required)
}
QJsonArray jsonArr;
foreach (Article *article, m_articles)
jsonArr << article->toJsonObject();
QJsonObject jsonObj;
jsonObj.insert(Str_Url, url());
jsonObj.insert(Str_Title, title());
jsonObj.insert(Str_LastBuildDate, lastBuildDate());
jsonObj.insert(Str_IsLoading, isLoading());
jsonObj.insert(Str_HasError, hasError());
jsonObj.insert(Str_Articles, jsonArr);
return jsonObj;
}
void Feed::handleSessionProcessingEnabledChanged(bool enabled)
{
if (enabled) {
downloadIcon();
disconnect(m_session, &Session::processingStateChanged
, this, &Feed::handleSessionProcessingEnabledChanged);
}
}
void Feed::handleArticleRead(Article *article)
{
article->disconnect(this);
decreaseUnreadCount();
emit articleRead(article);
// will be stored deferred
m_dirty = true;
storeDeferred();
}
void Feed::cleanup()
{
Utils::Fs::forceRemove(m_session->dataFileStorage()->storageDir().absoluteFilePath(m_dataFileName));
}
void Feed::timerEvent(QTimerEvent *event)
{
Q_UNUSED(event);
store();
}

121
src/base/rss/rss_feed.h Normal file
View File

@@ -0,0 +1,121 @@
/*
* Bittorrent Client using Qt and libtorrent.
* Copyright (C) 2015, 2017 Vladimir Golovnev <glassez@yandex.ru>
* Copyright (C) 2010 Christophe Dumez <chris@qbittorrent.org>
* Copyright (C) 2010 Arnaud Demaiziere <arnaud@qbittorrent.org>
*
* 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.
*/
#pragma once
#include <QBasicTimer>
#include <QHash>
#include <QList>
#include "rss_item.h"
class AsyncFileStorage;
namespace RSS
{
class Article;
class Session;
namespace Private
{
class Parser;
struct ParsingResult;
}
class Feed final: public Item
{
Q_OBJECT
Q_DISABLE_COPY(Feed)
friend class Session;
Feed(const QString &url, const QString &path, Session *session);
~Feed() override;
public:
QList<Article *> articles() const override;
int unreadCount() const override;
void markAsRead() override;
void refresh() override;
QString url() const;
QString title() const;
QString lastBuildDate() const;
bool hasError() const;
bool isLoading() const;
Article *articleByGUID(const QString &guid) const;
QString iconPath() const;
QJsonValue toJsonValue(bool withData = false) const override;
signals:
void iconLoaded(Feed *feed = nullptr);
void titleChanged(Feed *feed = nullptr);
void stateChanged(Feed *feed = nullptr);
private slots:
void handleSessionProcessingEnabledChanged(bool enabled);
void handleMaxArticlesPerFeedChanged(int n);
void handleIconDownloadFinished(const QString &url, const QString &filePath);
void handleDownloadFinished(const QString &url, const QByteArray &data);
void handleDownloadFailed(const QString &url, const QString &error);
void handleParsingFinished(const Private::ParsingResult &result);
void handleArticleRead(Article *article);
private:
void timerEvent(QTimerEvent *event) override;
void cleanup() override;
void load();
void loadArticles(const QByteArray &data);
void loadArticlesLegacy();
void store();
void storeDeferred();
bool addArticle(Article *article);
void removeOldestArticle();
void increaseUnreadCount();
void decreaseUnreadCount();
void downloadIcon();
Session *m_session;
Private::Parser *m_parser;
const QString m_url;
QString m_title;
QString m_lastBuildDate;
bool m_hasError = false;
bool m_isLoading = false;
QHash<QString, Article *> m_articles;
QList<Article *> m_articlesByDate;
int m_unreadCount = 0;
QString m_iconPath;
QString m_dataFileName;
QBasicTimer m_savingTimer;
bool m_dirty = false;
};
}

140
src/base/rss/rss_folder.cpp Normal file
View File

@@ -0,0 +1,140 @@
/*
* Bittorrent Client using Qt and libtorrent.
* Copyright (C) 2017 Vladimir Golovnev <glassez@yandex.ru>
* Copyright (C) 2010 Christophe Dumez <chris@qbittorrent.org>
* Copyright (C) 2010 Arnaud Demaiziere <arnaud@qbittorrent.org>
*
* 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_folder.h"
#include <QJsonObject>
#include <QJsonValue>
#include "rss_article.h"
using namespace RSS;
Folder::Folder(const QString &path)
: Item(path)
{
}
Folder::~Folder()
{
emit aboutToBeDestroyed(this);
foreach (auto item, items())
delete item;
}
QList<Article *> Folder::articles() const
{
QList<Article *> news;
foreach (Item *item, items()) {
int n = news.size();
news << item->articles();
std::inplace_merge(news.begin(), news.begin() + n, news.end()
, [](Article *a1, Article *a2)
{
return Article::articleDateRecentThan(a1, a2->date());
});
}
return news;
}
int Folder::unreadCount() const
{
int count = 0;
foreach (Item *item, items())
count += item->unreadCount();
return count;
}
void Folder::markAsRead()
{
foreach (Item *item, items())
item->markAsRead();
}
void Folder::refresh()
{
foreach (Item *item, items())
item->refresh();
}
QList<Item *> Folder::items() const
{
return m_items;
}
QJsonValue Folder::toJsonValue(bool withData) const
{
QJsonObject jsonObj;
foreach (Item *item, items())
jsonObj.insert(item->name(), item->toJsonValue(withData));
return jsonObj;
}
void Folder::handleItemUnreadCountChanged()
{
emit unreadCountChanged(this);
}
void Folder::handleItemAboutToBeDestroyed(Item *item)
{
if (item->unreadCount() > 0)
emit unreadCountChanged(this);
}
void Folder::cleanup()
{
foreach (Item *item, items())
item->cleanup();
}
void Folder::addItem(Item *item)
{
Q_ASSERT(item);
Q_ASSERT(!m_items.contains(item));
m_items.append(item);
connect(item, &Item::newArticle, this, &Item::newArticle);
connect(item, &Item::articleRead, this, &Item::articleRead);
connect(item, &Item::articleAboutToBeRemoved, this, &Item::articleAboutToBeRemoved);
connect(item, &Item::unreadCountChanged, this, &Folder::handleItemUnreadCountChanged);
connect(item, &Item::aboutToBeDestroyed, this, &Folder::handleItemAboutToBeDestroyed);
emit unreadCountChanged(this);
}
void Folder::removeItem(Item *item)
{
Q_ASSERT(m_items.contains(item));
item->disconnect(this);
m_items.removeOne(item);
emit unreadCountChanged(this);
}

View File

@@ -1,5 +1,6 @@
/*
* Bittorrent Client using Qt and libtorrent.
* Copyright (C) 2017 Vladimir Golovnev <glassez@yandex.ru>
* Copyright (C) 2010 Christophe Dumez <chris@qbittorrent.org>
* Copyright (C) 2010 Arnaud Demaiziere <arnaud@qbittorrent.org>
*
@@ -25,27 +26,46 @@
* 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.
*
* Contact: chris@qbittorrent.org, arnaud@qbittorrent.org
*/
#include "rssfolder.h"
#include "rssfile.h"
#pragma once
using namespace Rss;
#include <QList>
#include "rss_item.h"
File::~File() {}
Folder *File::parentFolder() const
namespace RSS
{
return m_parent;
}
class Session;
QStringList File::pathHierarchy() const
{
QStringList path;
if (m_parent)
path << m_parent->pathHierarchy();
path << id();
return path;
class Folder final: public Item
{
Q_OBJECT
Q_DISABLE_COPY(Folder)
friend class Session;
explicit Folder(const QString &path = "");
~Folder() override;
public:
QList<Article *> articles() const override;
int unreadCount() const override;
void markAsRead() override;
void refresh() override;
QList<Item *> items() const;
QJsonValue toJsonValue(bool withData = false) const override;
private slots:
void handleItemUnreadCountChanged();
void handleItemAboutToBeDestroyed(Item *item);
private:
void cleanup() override;
void addItem(Item *item);
void removeItem(Item *item);
QList<Item *> m_items;
};
}

115
src/base/rss/rss_item.cpp Normal file
View File

@@ -0,0 +1,115 @@
/*
* Bittorrent Client using Qt and libtorrent.
* Copyright (C) 2017 Vladimir Golovnev <glassez@yandex.ru>
* Copyright (C) 2010 Christophe Dumez <chris@qbittorrent.org>
* Copyright (C) 2010 Arnaud Demaiziere <arnaud@qbittorrent.org>
*
* 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_item.h"
#include <QDebug>
#include <QRegularExpression>
#include <QStringList>
using namespace RSS;
const QString Item::PathSeparator("\\");
Item::Item(const QString &path)
: m_path(path)
{
}
Item::~Item() {}
void Item::setPath(const QString &path)
{
if (path != m_path) {
m_path = path;
emit pathChanged(this);
}
}
QString Item::path() const
{
return m_path;
}
QString Item::name() const
{
return relativeName(path());
}
bool Item::isValidPath(const QString &path)
{
static const QRegularExpression re(
QString(R"(\A[^\%1]+(\%1[^\%1]+)*\z)").arg(Item::PathSeparator)
, QRegularExpression::DontCaptureOption | QRegularExpression::OptimizeOnFirstUsageOption);
if (path.isEmpty() || !re.match(path).hasMatch()) {
qDebug() << "Incorrect RSS Item path:" << path;
return false;
}
return true;
}
QString Item::joinPath(const QString &path1, const QString &path2)
{
if (path1.isEmpty())
return path2;
else
return path1 + Item::PathSeparator + path2;
}
QStringList Item::expandPath(const QString &path)
{
QStringList result;
if (path.isEmpty()) return result;
// if (!isValidRSSFolderName(folder))
// return result;
int index = 0;
while ((index = path.indexOf(Item::PathSeparator, index)) >= 0) {
result << path.left(index);
++index;
}
result << path;
return result;
}
QString Item::parentPath(const QString &path)
{
int pos;
return ((pos = path.lastIndexOf(Item::PathSeparator)) >= 0 ? path.left(pos) : "");
}
QString Item::relativeName(const QString &path)
{
int pos;
return ((pos = path.lastIndexOf(Item::PathSeparator)) >= 0 ? path.right(path.size() - (pos + 1)) : path);
}

View File

@@ -1,5 +1,6 @@
/*
* Bittorrent Client using Qt and libtorrent.
* Copyright (C) 2017 Vladimir Golovnev <glassez@yandex.ru>
* Copyright (C) 2010 Christophe Dumez <chris@qbittorrent.org>
* Copyright (C) 2010 Arnaud Demaiziere <arnaud@qbittorrent.org>
*
@@ -25,58 +26,63 @@
* 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.
*
* Contact: chris@qbittorrent.org, arnaud@qbittorrent.org
*/
#ifndef RSSFILE_H
#define RSSFILE_H
#pragma once
#include <QList>
#include <QStringList>
#include <QSharedPointer>
#include <QObject>
namespace Rss
namespace RSS
{
class Folder;
class File;
class Article;
class Folder;
class Session;
typedef QSharedPointer<File> FilePtr;
typedef QSharedPointer<Article> ArticlePtr;
typedef QList<ArticlePtr> ArticleList;
typedef QList<FilePtr> FileList;
/**
* Parent interface for Rss::Folder and Rss::Feed.
*/
class File
class Item: public QObject
{
Q_OBJECT
Q_DISABLE_COPY(Item)
friend class Folder;
friend class Session;
public:
virtual ~File();
virtual QString id() const = 0;
virtual QString displayName() const = 0;
virtual uint unreadCount() const = 0;
virtual QString iconPath() const = 0;
virtual ArticleList articleListByDateDesc() const = 0;
virtual ArticleList unreadArticleListByDateDesc() const = 0;
virtual void rename(const QString &newName) = 0;
virtual QList<Article *> articles() const = 0;
virtual int unreadCount() const = 0;
virtual void markAsRead() = 0;
virtual bool refresh() = 0;
virtual void removeAllSettings() = 0;
virtual void saveItemsToDisk() = 0;
virtual void recheckRssItemsForDownload() = 0;
virtual void refresh() = 0;
Folder *parentFolder() const;
QStringList pathHierarchy() const;
QString path() const;
QString name() const;
virtual QJsonValue toJsonValue(bool withData = false) const = 0;
static const QString PathSeparator;
static bool isValidPath(const QString &path);
static QString joinPath(const QString &path1, const QString &path2);
static QStringList expandPath(const QString &path);
static QString parentPath(const QString &path);
static QString relativeName(const QString &path);
signals:
void pathChanged(Item *item = nullptr);
void unreadCountChanged(Item *item = nullptr);
void aboutToBeDestroyed(Item *item = nullptr);
void newArticle(Article *article);
void articleRead(Article *article);
void articleAboutToBeRemoved(Article *article);
protected:
friend class Folder;
explicit Item(const QString &path);
~Item() override;
Folder *m_parent = nullptr;
virtual void cleanup() = 0;
private:
void setPath(const QString &path);
QString m_path;
};
}
#endif // RSSFILE_H

View File

@@ -0,0 +1,485 @@
/*
* Bittorrent Client using Qt and libtorrent.
* Copyright (C) 2017 Vladimir Golovnev <glassez@yandex.ru>
* Copyright (C) 2010 Christophe Dumez <chris@qbittorrent.org>
* Copyright (C) 2010 Arnaud Demaiziere <arnaud@qbittorrent.org>
*
* 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_session.h"
#include <QDebug>
#include <QDir>
#include <QJsonDocument>
#include <QJsonObject>
#include <QJsonValue>
#include <QSaveFile>
#include <QString>
#include <QThread>
#include <QVariantHash>
#include "../asyncfilestorage.h"
#include "../logger.h"
#include "../profile.h"
#include "../settingsstorage.h"
#include "../utils/fs.h"
#include "rss_article.h"
#include "rss_feed.h"
#include "rss_item.h"
#include "rss_folder.h"
const int MsecsPerMin = 60000;
const QString ConfFolderName(QStringLiteral("rss"));
const QString DataFolderName(QStringLiteral("rss/articles"));
const QString FeedsFileName(QStringLiteral("feeds.json"));
const QString SettingsKey_ProcessingEnabled(QStringLiteral("RSS/Session/EnableProcessing"));
const QString SettingsKey_RefreshInterval(QStringLiteral("RSS/Session/RefreshInterval"));
const QString SettingsKey_MaxArticlesPerFeed(QStringLiteral("RSS/Session/MaxArticlesPerFeed"));
using namespace RSS;
QPointer<Session> Session::m_instance = nullptr;
Session::Session()
: m_workingThread(new QThread(this))
, m_processingEnabled(SettingsStorage::instance()->loadValue(SettingsKey_ProcessingEnabled, false).toBool())
, m_refreshInterval(SettingsStorage::instance()->loadValue(SettingsKey_RefreshInterval, 30).toUInt())
, m_maxArticlesPerFeed(SettingsStorage::instance()->loadValue(SettingsKey_MaxArticlesPerFeed, 50).toInt())
{
Q_ASSERT(!m_instance); // only one instance is allowed
m_instance = this;
m_confFileStorage = new AsyncFileStorage(
Utils::Fs::expandPathAbs(specialFolderLocation(SpecialFolder::Config) + ConfFolderName));
m_confFileStorage->moveToThread(m_workingThread);
connect(m_workingThread, &QThread::finished, m_confFileStorage, &AsyncFileStorage::deleteLater);
connect(m_confFileStorage, &AsyncFileStorage::failed, [](const QString &fileName, const QString &errorString)
{
Logger::instance()->addMessage(QString("Couldn't save RSS Session configuration in %1. Error: %2")
.arg(fileName).arg(errorString), Log::WARNING);
});
m_dataFileStorage = new AsyncFileStorage(
Utils::Fs::expandPathAbs(specialFolderLocation(SpecialFolder::Data) + DataFolderName));
m_dataFileStorage->moveToThread(m_workingThread);
connect(m_workingThread, &QThread::finished, m_dataFileStorage, &AsyncFileStorage::deleteLater);
connect(m_dataFileStorage, &AsyncFileStorage::failed, [](const QString &fileName, const QString &errorString)
{
Logger::instance()->addMessage(QString("Couldn't save RSS Session data in %1. Error: %2")
.arg(fileName).arg(errorString), Log::WARNING);
});
m_itemsByPath.insert("", new Folder); // root folder
m_workingThread->start();
load();
connect(&m_refreshTimer, &QTimer::timeout, this, &Session::refresh);
if (m_processingEnabled) {
m_refreshTimer.start(m_refreshInterval * MsecsPerMin);
refresh();
}
}
Session::~Session()
{
qDebug() << "Deleting RSS Session...";
m_workingThread->quit();
m_workingThread->wait();
//store();
delete m_itemsByPath[""]; // deleting root folder
qDebug() << "RSS Session deleted.";
}
Session *Session::instance()
{
return m_instance;
}
bool Session::addFolder(const QString &path, QString *error)
{
Folder *destFolder = prepareItemDest(path, error);
if (!destFolder)
return false;
addItem(new Folder(path), destFolder);
store();
return true;
}
bool Session::addFeed(const QString &url, const QString &path, QString *error)
{
if (m_feedsByURL.contains(url)) {
if (error)
*error = tr("RSS feed with given URL already exists: %1.").arg(url);
return false;
}
Folder *destFolder = prepareItemDest(path, error);
if (!destFolder)
return false;
addItem(new Feed(url, path, this), destFolder);
store();
if (m_processingEnabled)
feedByURL(url)->refresh();
return true;
}
bool Session::moveItem(const QString &itemPath, const QString &destPath, QString *error)
{
if (itemPath.isEmpty()) {
if (error)
*error = tr("Cannot move root folder.");
return false;
}
auto item = m_itemsByPath.value(itemPath);
if (!item) {
if (error)
*error = tr("Item doesn't exists: %1.").arg(itemPath);
return false;
}
return moveItem(item, destPath, error);
}
bool Session::moveItem(Item *item, const QString &destPath, QString *error)
{
Q_ASSERT(item);
Q_ASSERT(item != rootFolder());
Folder *destFolder = prepareItemDest(destPath, error);
if (!destFolder)
return false;
auto srcFolder = static_cast<Folder *>(m_itemsByPath.value(Item::parentPath(item->path())));
if (srcFolder != destFolder) {
srcFolder->removeItem(item);
destFolder->addItem(item);
}
m_itemsByPath.insert(destPath, m_itemsByPath.take(item->path()));
item->setPath(destPath);
store();
return true;
}
bool Session::removeItem(const QString &itemPath, QString *error)
{
if (itemPath.isEmpty()) {
if (error)
*error = tr("Cannot delete root folder.");
return false;
}
auto item = m_itemsByPath.value(itemPath);
if (!item) {
if (error)
*error = tr("Item doesn't exists: %1.").arg(itemPath);
return false;
}
emit itemAboutToBeRemoved(item);
item->cleanup();
auto folder = static_cast<Folder *>(m_itemsByPath.value(Item::parentPath(item->path())));
folder->removeItem(item);
delete item;
store();
return true;
}
QList<Item *> Session::items() const
{
return m_itemsByPath.values();
}
Item *Session::itemByPath(const QString &path) const
{
return m_itemsByPath.value(path);
}
void Session::load()
{
QFile itemsFile(m_confFileStorage->storageDir().absoluteFilePath(FeedsFileName));
if (!itemsFile.exists()) {
loadLegacy();
return;
}
if (!itemsFile.open(QFile::ReadOnly)) {
Logger::instance()->addMessage(
QString("Couldn't read RSS Session data from %1. Error: %2")
.arg(itemsFile.fileName()).arg(itemsFile.errorString()), Log::WARNING);
return;
}
QJsonParseError jsonError;
QJsonDocument jsonDoc = QJsonDocument::fromJson(itemsFile.readAll(), &jsonError);
if (jsonError.error != QJsonParseError::NoError) {
Logger::instance()->addMessage(
QString("Couldn't parse RSS Session data from %1. Error: %2")
.arg(itemsFile.fileName()).arg(jsonError.errorString()), Log::WARNING);
return;
}
if (!jsonDoc.isObject()) {
Logger::instance()->addMessage(
QString("Couldn't load RSS Session data from %1. Invalid data format.")
.arg(itemsFile.fileName()), Log::WARNING);
return;
}
loadFolder(jsonDoc.object(), rootFolder());
}
void Session::loadFolder(const QJsonObject &jsonObj, Folder *folder)
{
foreach (const QString &key, jsonObj.keys()) {
QJsonValue val = jsonObj[key];
if (val.isString()) {
QString url = val.toString();
if (url.isEmpty())
url = key;
addFeedToFolder(url, key, folder);
}
else if (!val.isObject()) {
Logger::instance()->addMessage(
QString("Couldn't load RSS Item '%1'. Invalid data format.")
.arg(QString("%1\\%2").arg(folder->path()).arg(key)), Log::WARNING);
}
else {
QJsonObject valObj = val.toObject();
if (valObj.contains("url")) {
if (!valObj["url"].isString()) {
Logger::instance()->addMessage(
QString("Couldn't load RSS Feed '%1'. URL is required.")
.arg(QString("%1\\%2").arg(folder->path()).arg(key)), Log::WARNING);
continue;
}
addFeedToFolder(valObj["url"].toString(), key, folder);
}
else {
loadFolder(valObj, addSubfolder(key, folder));
}
}
}
}
void Session::loadLegacy()
{
const QStringList legacyFeedPaths = SettingsStorage::instance()->loadValue("Rss/streamList").toStringList();
const QStringList feedAliases = SettingsStorage::instance()->loadValue("Rss/streamAlias").toStringList();
if (legacyFeedPaths.size() != feedAliases.size()) {
Logger::instance()->addMessage("Corrupted RSS list, not loading it.", Log::WARNING);
return;
}
uint i = 0;
foreach (QString legacyPath, legacyFeedPaths) {
if (Item::PathSeparator == QString(legacyPath[0]))
legacyPath.remove(0, 1);
const QString parentFolderPath = Item::parentPath(legacyPath);
const QString feedUrl = Item::relativeName(legacyPath);
foreach (const QString &folderPath, Item::expandPath(parentFolderPath))
addFolder(folderPath);
const QString feedPath = feedAliases[i].isEmpty()
? legacyPath
: Item::joinPath(parentFolderPath, feedAliases[i]);
addFeed(feedUrl, feedPath);
++i;
}
store(); // convert to new format
}
void Session::store()
{
m_confFileStorage->store(FeedsFileName, QJsonDocument(rootFolder()->toJsonValue().toObject()).toJson());
}
Folder *Session::prepareItemDest(const QString &path, QString *error)
{
if (!Item::isValidPath(path)) {
if (error)
*error = tr("Incorrect RSS Item path: %1.").arg(path);
return nullptr;
}
if (m_itemsByPath.contains(path)) {
if (error)
*error = tr("RSS item with given path already exists: %1.").arg(path);
return nullptr;
}
const QString destFolderPath = Item::parentPath(path);
auto destFolder = qobject_cast<Folder *>(m_itemsByPath.value(destFolderPath));
if (!destFolder) {
if (error)
*error = tr("Parent folder doesn't exist: %1.").arg(destFolderPath);
return nullptr;
}
return destFolder;
}
Folder *Session::addSubfolder(const QString &name, Folder *parentFolder)
{
auto folder = new Folder(Item::joinPath(parentFolder->path(), name));
addItem(folder, parentFolder);
return folder;
}
Feed *Session::addFeedToFolder(const QString &url, const QString &name, Folder *parentFolder)
{
auto feed = new Feed(url, Item::joinPath(parentFolder->path(), name), this);
addItem(feed, parentFolder);
return feed;
}
void Session::addItem(Item *item, Folder *destFolder)
{
if (auto feed = qobject_cast<Feed *>(item)) {
connect(feed, &Feed::titleChanged, this, &Session::handleFeedTitleChanged);
connect(feed, &Feed::iconLoaded, this, &Session::feedIconLoaded);
connect(feed, &Feed::stateChanged, this, &Session::feedStateChanged);
m_feedsByURL[feed->url()] = feed;
}
connect(item, &Item::pathChanged, this, &Session::itemPathChanged);
connect(item, &Item::aboutToBeDestroyed, this, &Session::handleItemAboutToBeDestroyed);
m_itemsByPath[item->path()] = item;
destFolder->addItem(item);
emit itemAdded(item);
}
bool Session::isProcessingEnabled() const
{
return m_processingEnabled;
}
void Session::setProcessingEnabled(bool enabled)
{
if (m_processingEnabled != enabled) {
m_processingEnabled = enabled;
SettingsStorage::instance()->storeValue(SettingsKey_ProcessingEnabled, m_processingEnabled);
if (m_processingEnabled) {
m_refreshTimer.start(m_refreshInterval * MsecsPerMin);
refresh();
}
else {
m_refreshTimer.stop();
}
emit processingStateChanged(m_processingEnabled);
}
}
AsyncFileStorage *Session::confFileStorage() const
{
return m_confFileStorage;
}
AsyncFileStorage *Session::dataFileStorage() const
{
return m_dataFileStorage;
}
Folder *Session::rootFolder() const
{
return static_cast<Folder *>(m_itemsByPath.value(""));
}
QList<Feed *> Session::feeds() const
{
return m_feedsByURL.values();
}
Feed *Session::feedByURL(const QString &url) const
{
return m_feedsByURL.value(url);
}
uint Session::refreshInterval() const
{
return m_refreshInterval;
}
void Session::setRefreshInterval(uint refreshInterval)
{
if (m_refreshInterval != refreshInterval) {
SettingsStorage::instance()->storeValue(SettingsKey_RefreshInterval, refreshInterval);
m_refreshInterval = refreshInterval;
m_refreshTimer.start(m_refreshInterval * MsecsPerMin);
}
}
QThread *Session::workingThread() const
{
return m_workingThread;
}
void Session::handleItemAboutToBeDestroyed(Item *item)
{
m_itemsByPath.remove(item->path());
auto feed = qobject_cast<Feed *>(item);
if (feed)
m_feedsByURL.remove(feed->url());
}
void Session::handleFeedTitleChanged(Feed *feed)
{
if (feed->name() == feed->url())
// Now we have something better than a URL.
// Trying to rename feed...
moveItem(feed, Item::joinPath(Item::parentPath(feed->path()), feed->title()));
}
int Session::maxArticlesPerFeed() const
{
return m_maxArticlesPerFeed;
}
void Session::setMaxArticlesPerFeed(int n)
{
if (m_maxArticlesPerFeed != n) {
m_maxArticlesPerFeed = n;
SettingsStorage::instance()->storeValue(SettingsKey_MaxArticlesPerFeed, n);
emit maxArticlesPerFeedChanged(n);
}
}
void Session::refresh()
{
// NOTE: Should we allow manually refreshing for disabled session?
rootFolder()->refresh();
}

154
src/base/rss/rss_session.h Normal file
View File

@@ -0,0 +1,154 @@
/*
* Bittorrent Client using Qt and libtorrent.
* Copyright (C) 2017 Vladimir Golovnev <glassez@yandex.ru>
* Copyright (C) 2010 Christophe Dumez <chris@qbittorrent.org>
* Copyright (C) 2010 Arnaud Demaiziere <arnaud@qbittorrent.org>
*
* 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.
*/
#pragma once
/*
* RSS Session configuration file format (JSON):
*
* =============== BEGIN ===============
* {
* "folder1": {
* "subfolder1": {
* "Feed name (Alias)": "http://some-feed-url1",
* "http://some-feed-url2": ""
* },
* "subfolder2": {},
* "http://some-feed-url3": "",
* "Feed name (Alias)": {
* "url": "http://some-feed-url4",
* }
* },
* "folder2": {},
* "folder3": {}
* }
* ================ END ================
*
* 1. Document is JSON object (the same as Folder)
* 2. Folder is JSON object (keys are Item names, values are Items)
* 3.1. Feed is JSON object (keys are property names, values are property values; 'url' is required)
* 3.2. (Reduced format) Feed is JSON string (string is URL unless it's empty, otherwise we take Feed URL from name)
*/
#include <QHash>
#include <QObject>
#include <QPointer>
#include <QStringList>
#include <QTimer>
class QThread;
class Application;
class AsyncFileStorage;
namespace RSS
{
class Item;
class Feed;
class Folder;
class Session: public QObject
{
Q_OBJECT
Q_DISABLE_COPY(Session)
friend class ::Application;
Session();
~Session() override;
public:
static Session *instance();
bool isProcessingEnabled() const;
void setProcessingEnabled(bool enabled);
QThread *workingThread() const;
AsyncFileStorage *confFileStorage() const;
AsyncFileStorage *dataFileStorage() const;
int maxArticlesPerFeed() const;
void setMaxArticlesPerFeed(int n);
uint refreshInterval() const;
void setRefreshInterval(uint refreshInterval);
bool addFolder(const QString &path, QString *error = nullptr);
bool addFeed(const QString &url, const QString &path, QString *error = nullptr);
bool moveItem(const QString &itemPath, const QString &destPath
, QString *error = nullptr);
bool moveItem(Item *item, const QString &destPath, QString *error = nullptr);
bool removeItem(const QString &itemPath, QString *error = nullptr);
QList<Item *> items() const;
Item *itemByPath(const QString &path) const;
QList<Feed *> feeds() const;
Feed *feedByURL(const QString &url) const;
Folder *rootFolder() const;
public slots:
void refresh();
signals:
void processingStateChanged(bool enabled);
void maxArticlesPerFeedChanged(int n);
void itemAdded(Item *item);
void itemPathChanged(Item *item);
void itemAboutToBeRemoved(Item *item);
void feedIconLoaded(Feed *feed);
void feedStateChanged(Feed *feed);
private slots:
void handleItemAboutToBeDestroyed(Item *item);
void handleFeedTitleChanged(Feed *feed);
private:
void load();
void loadFolder(const QJsonObject &jsonObj, Folder *folder);
void loadLegacy();
void store();
Folder *prepareItemDest(const QString &path, QString *error);
Folder *addSubfolder(const QString &name, Folder *parentFolder);
Feed *addFeedToFolder(const QString &url, const QString &name, Folder *parentFolder);
void addItem(Item *item, Folder *destFolder);
static QPointer<Session> m_instance;
bool m_processingEnabled;
QThread *m_workingThread;
AsyncFileStorage *m_confFileStorage;
AsyncFileStorage *m_dataFileStorage;
QTimer m_refreshTimer;
uint m_refreshInterval;
int m_maxArticlesPerFeed;
QHash<QString, Item *> m_itemsByPath;
QHash<QString, Feed *> m_feedsByURL;
};
}

View File

@@ -1,143 +0,0 @@
/*
* Bittorrent Client using Qt and libtorrent.
* Copyright (C) 2010 Christophe Dumez <chris@qbittorrent.org>
* Copyright (C) 2010 Arnaud Demaiziere <arnaud@qbittorrent.org>
*
* 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.
*
* Contact: chris@qbittorrent.org, arnaud@qbittorrent.org
*/
#include <QVariant>
#include <QDebug>
#include <iostream>
#include "rssfeed.h"
#include "rssarticle.h"
using namespace Rss;
// public constructor
Article::Article(Feed *parent, const QString &guid)
: m_parent(parent)
, m_guid(guid)
, m_read(false)
{
}
bool Article::hasAttachment() const
{
return !m_torrentUrl.isEmpty();
}
QVariantHash Article::toHash() const
{
QVariantHash item;
item["title"] = m_title;
item["id"] = m_guid;
item["torrent_url"] = m_torrentUrl;
item["news_link"] = m_link;
item["description"] = m_description;
item["date"] = m_date;
item["author"] = m_author;
item["read"] = m_read;
return item;
}
ArticlePtr Article::fromHash(Feed *parent, const QVariantHash &h)
{
const QString guid = h.value("id").toString();
if (guid.isEmpty())
return ArticlePtr();
ArticlePtr art(new Article(parent, guid));
art->m_title = h.value("title", "").toString();
art->m_torrentUrl = h.value("torrent_url", "").toString();
art->m_link = h.value("news_link", "").toString();
art->m_description = h.value("description").toString();
art->m_date = h.value("date").toDateTime();
art->m_author = h.value("author").toString();
art->m_read = h.value("read", false).toBool();
return art;
}
Feed *Article::parent() const
{
return m_parent;
}
const QString &Article::author() const
{
return m_author;
}
const QString &Article::torrentUrl() const
{
return m_torrentUrl;
}
const QString &Article::link() const
{
return m_link;
}
QString Article::description() const
{
return m_description.isNull() ? "" : m_description;
}
const QDateTime &Article::date() const
{
return m_date;
}
bool Article::isRead() const
{
return m_read;
}
void Article::markAsRead()
{
if (!m_read) {
m_read = true;
emit articleWasRead();
}
}
const QString &Article::guid() const
{
return m_guid;
}
const QString &Article::title() const
{
return m_title;
}
void Article::handleTorrentDownloadSuccess(const QString &url)
{
if (url == m_torrentUrl)
markAsRead();
}

View File

@@ -1,440 +0,0 @@
/*
* Bittorrent Client using Qt and libtorrent.
* Copyright (C) 2010 Christophe Dumez
*
* 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.
*
* Contact : chris@qbittorrent.org
*/
#include <QDebug>
#include <QDir>
#include <QHash>
#include <QRegExp>
#include <QRegularExpression>
#include <QString>
#include <QStringList>
#include "base/preferences.h"
#include "base/utils/fs.h"
#include "base/utils/string.h"
#include "rssfeed.h"
#include "rssarticle.h"
#include "rssdownloadrule.h"
using namespace Rss;
DownloadRule::DownloadRule()
: m_enabled(false)
, m_useRegex(false)
, m_apstate(USE_GLOBAL)
, m_ignoreDays(0)
, m_cachedRegexes(new QHash<QString, QRegularExpression>)
{
}
DownloadRule::~DownloadRule()
{
delete m_cachedRegexes;
}
QRegularExpression DownloadRule::cachedRegex(const QString &expression, bool isRegex) const
{
// Use a cache of regexes so we don't have to continually recompile - big performance increase.
// The cache is cleared whenever the regex/wildcard, must or must not contain fields or
// episode filter are modified.
Q_ASSERT(!expression.isEmpty());
QRegularExpression regex((*m_cachedRegexes)[expression]);
if (!regex.pattern().isEmpty())
return regex;
return (*m_cachedRegexes)[expression] = QRegularExpression(isRegex ? expression : Utils::String::wildcardToRegex(expression), QRegularExpression::CaseInsensitiveOption);
}
bool DownloadRule::matches(const QString &articleTitle, const QString &expression) const
{
static QRegularExpression whitespace("\\s+");
if (expression.isEmpty()) {
// A regex of the form "expr|" will always match, so do the same for wildcards
return true;
}
else if (m_useRegex) {
QRegularExpression reg(cachedRegex(expression));
return reg.match(articleTitle).hasMatch();
}
else {
// Only match if every wildcard token (separated by spaces) is present in the article name.
// Order of wildcard tokens is unimportant (if order is important, they should have used *).
foreach (const QString &wildcard, expression.split(whitespace, QString::SplitBehavior::SkipEmptyParts)) {
QRegularExpression reg(cachedRegex(wildcard, false));
if (!reg.match(articleTitle).hasMatch())
return false;
}
}
return true;
}
bool DownloadRule::matches(const QString &articleTitle) const
{
if (!m_mustContain.empty()) {
bool logged = false;
bool foundMustContain = false;
// Each expression is either a regex, or a set of wildcards separated by whitespace.
// Accept if any complete expression matches.
foreach (const QString &expression, m_mustContain) {
if (!logged) {
qDebug() << "Checking matching" << (m_useRegex ? "regex:" : "wildcard expressions:") << m_mustContain.join("|");
logged = true;
}
// A regex of the form "expr|" will always match, so do the same for wildcards
foundMustContain = matches(articleTitle, expression);
if (foundMustContain) {
qDebug() << "Found matching" << (m_useRegex ? "regex:" : "wildcard expression:") << expression;
break;
}
}
if (!foundMustContain)
return false;
}
if (!m_mustNotContain.empty()) {
bool logged = false;
// Each expression is either a regex, or a set of wildcards separated by whitespace.
// Reject if any complete expression matches.
foreach (const QString &expression, m_mustNotContain) {
if (!logged) {
qDebug() << "Checking not matching" << (m_useRegex ? "regex:" : "wildcard expressions:") << m_mustNotContain.join("|");
logged = true;
}
// A regex of the form "expr|" will always match, so do the same for wildcards
if (matches(articleTitle, expression)) {
qDebug() << "Found not matching" << (m_useRegex ? "regex:" : "wildcard expression:") << expression;
return false;
}
}
}
if (!m_episodeFilter.isEmpty()) {
qDebug() << "Checking episode filter:" << m_episodeFilter;
QRegularExpression f(cachedRegex("(^\\d{1,4})x(.*;$)"));
QRegularExpressionMatch matcher = f.match(m_episodeFilter);
bool matched = matcher.hasMatch();
if (!matched)
return false;
QString s = matcher.captured(1);
QStringList eps = matcher.captured(2).split(";");
int sOurs = s.toInt();
foreach (QString ep, eps) {
if (ep.isEmpty())
continue;
// We need to trim leading zeroes, but if it's all zeros then we want episode zero.
while (ep.size() > 1 && ep.startsWith("0"))
ep = ep.right(ep.size() - 1);
if (ep.indexOf('-') != -1) { // Range detected
QString partialPattern1 = "\\bs0?(\\d{1,4})[ -_\\.]?e(0?\\d{1,4})(?:\\D|\\b)";
QString partialPattern2 = "\\b(\\d{1,4})x(0?\\d{1,4})(?:\\D|\\b)";
QRegularExpression reg(cachedRegex(partialPattern1));
if (ep.endsWith('-')) { // Infinite range
int epOurs = ep.left(ep.size() - 1).toInt();
// Extract partial match from article and compare as digits
matcher = reg.match(articleTitle);
matched = matcher.hasMatch();
if (!matched) {
reg = QRegularExpression(cachedRegex(partialPattern2));
matcher = reg.match(articleTitle);
matched = matcher.hasMatch();
}
if (matched) {
int sTheirs = matcher.captured(1).toInt();
int epTheirs = matcher.captured(2).toInt();
if (((sTheirs == sOurs) && (epTheirs >= epOurs)) || (sTheirs > sOurs)) {
qDebug() << "Matched episode:" << ep;
qDebug() << "Matched article:" << articleTitle;
return true;
}
}
}
else { // Normal range
QStringList range = ep.split('-');
Q_ASSERT(range.size() == 2);
if (range.first().toInt() > range.last().toInt())
continue; // Ignore this subrule completely
int epOursFirst = range.first().toInt();
int epOursLast = range.last().toInt();
// Extract partial match from article and compare as digits
matcher = reg.match(articleTitle);
matched = matcher.hasMatch();
if (!matched) {
reg = QRegularExpression(cachedRegex(partialPattern2));
matcher = reg.match(articleTitle);
matched = matcher.hasMatch();
}
if (matched) {
int sTheirs = matcher.captured(1).toInt();
int epTheirs = matcher.captured(2).toInt();
if ((sTheirs == sOurs) && ((epOursFirst <= epTheirs) && (epOursLast >= epTheirs))) {
qDebug() << "Matched episode:" << ep;
qDebug() << "Matched article:" << articleTitle;
return true;
}
}
}
}
else { // Single number
QString expStr("\\b(?:s0?" + s + "[ -_\\.]?" + "e0?" + ep + "|" + s + "x" + "0?" + ep + ")(?:\\D|\\b)");
QRegularExpression reg(cachedRegex(expStr));
if (reg.match(articleTitle).hasMatch()) {
qDebug() << "Matched episode:" << ep;
qDebug() << "Matched article:" << articleTitle;
return true;
}
}
}
return false;
}
qDebug() << "Matched article:" << articleTitle;
return true;
}
void DownloadRule::setMustContain(const QString &tokens)
{
m_cachedRegexes->clear();
if (m_useRegex)
m_mustContain = QStringList() << tokens;
else
m_mustContain = tokens.split("|");
// Check for single empty string - if so, no condition
if ((m_mustContain.size() == 1) && m_mustContain[0].isEmpty())
m_mustContain.clear();
}
void DownloadRule::setMustNotContain(const QString &tokens)
{
m_cachedRegexes->clear();
if (m_useRegex)
m_mustNotContain = QStringList() << tokens;
else
m_mustNotContain = tokens.split("|");
// Check for single empty string - if so, no condition
if ((m_mustNotContain.size() == 1) && m_mustNotContain[0].isEmpty())
m_mustNotContain.clear();
}
QStringList DownloadRule::rssFeeds() const
{
return m_rssFeeds;
}
void DownloadRule::setRssFeeds(const QStringList &rssFeeds)
{
m_rssFeeds = rssFeeds;
}
QString DownloadRule::name() const
{
return m_name;
}
void DownloadRule::setName(const QString &name)
{
m_name = name;
}
QString DownloadRule::savePath() const
{
return m_savePath;
}
DownloadRulePtr DownloadRule::fromVariantHash(const QVariantHash &ruleHash)
{
DownloadRulePtr rule(new DownloadRule);
rule->setName(ruleHash.value("name").toString());
rule->setUseRegex(ruleHash.value("use_regex", false).toBool());
rule->setMustContain(ruleHash.value("must_contain").toString());
rule->setMustNotContain(ruleHash.value("must_not_contain").toString());
rule->setEpisodeFilter(ruleHash.value("episode_filter").toString());
rule->setRssFeeds(ruleHash.value("affected_feeds").toStringList());
rule->setEnabled(ruleHash.value("enabled", false).toBool());
rule->setSavePath(ruleHash.value("save_path").toString());
rule->setCategory(ruleHash.value("category_assigned").toString());
rule->setAddPaused(AddPausedState(ruleHash.value("add_paused").toUInt()));
rule->setLastMatch(ruleHash.value("last_match").toDateTime());
rule->setIgnoreDays(ruleHash.value("ignore_days").toInt());
return rule;
}
QVariantHash DownloadRule::toVariantHash() const
{
QVariantHash hash;
hash["name"] = m_name;
hash["must_contain"] = m_mustContain.join("|");
hash["must_not_contain"] = m_mustNotContain.join("|");
hash["save_path"] = m_savePath;
hash["affected_feeds"] = m_rssFeeds;
hash["enabled"] = m_enabled;
hash["category_assigned"] = m_category;
hash["use_regex"] = m_useRegex;
hash["add_paused"] = m_apstate;
hash["episode_filter"] = m_episodeFilter;
hash["last_match"] = m_lastMatch;
hash["ignore_days"] = m_ignoreDays;
return hash;
}
bool DownloadRule::operator==(const DownloadRule &other) const
{
return m_name == other.name();
}
void DownloadRule::setSavePath(const QString &savePath)
{
m_savePath = Utils::Fs::fromNativePath(savePath);
}
DownloadRule::AddPausedState DownloadRule::addPaused() const
{
return m_apstate;
}
void DownloadRule::setAddPaused(const DownloadRule::AddPausedState &aps)
{
m_apstate = aps;
}
QString DownloadRule::category() const
{
return m_category;
}
void DownloadRule::setCategory(const QString &category)
{
m_category = category;
}
bool DownloadRule::isEnabled() const
{
return m_enabled;
}
void DownloadRule::setEnabled(bool enable)
{
m_enabled = enable;
}
void DownloadRule::setLastMatch(const QDateTime &d)
{
m_lastMatch = d;
}
QDateTime DownloadRule::lastMatch() const
{
return m_lastMatch;
}
void DownloadRule::setIgnoreDays(int d)
{
m_ignoreDays = d;
}
int DownloadRule::ignoreDays() const
{
return m_ignoreDays;
}
QString DownloadRule::mustContain() const
{
return m_mustContain.join("|");
}
QString DownloadRule::mustNotContain() const
{
return m_mustNotContain.join("|");
}
bool DownloadRule::useRegex() const
{
return m_useRegex;
}
void DownloadRule::setUseRegex(bool enabled)
{
m_useRegex = enabled;
m_cachedRegexes->clear();
}
QString DownloadRule::episodeFilter() const
{
return m_episodeFilter;
}
void DownloadRule::setEpisodeFilter(const QString &e)
{
m_episodeFilter = e;
m_cachedRegexes->clear();
}
QStringList DownloadRule::findMatchingArticles(const FeedPtr &feed) const
{
QStringList ret;
const ArticleHash &feedArticles = feed->articleHash();
ArticleHash::ConstIterator artIt = feedArticles.begin();
ArticleHash::ConstIterator artItend = feedArticles.end();
for (; artIt != artItend; ++artIt) {
const QString title = artIt.value()->title();
qDebug() << "Matching article:" << title;
if (matches(title))
ret << title;
}
return ret;
}

View File

@@ -1,187 +0,0 @@
/*
* Bittorrent Client using Qt and libtorrent.
* Copyright (C) 2010 Christophe Dumez
*
* 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.
*
* Contact : chris@qbittorrent.org
*/
#include "rssdownloadrulelist.h"
#include <QFile>
#include <QDataStream>
#include <QDebug>
#include "base/preferences.h"
#include "base/profile.h"
using namespace Rss;
DownloadRuleList::DownloadRuleList()
{
loadRulesFromStorage();
}
DownloadRulePtr DownloadRuleList::findMatchingRule(const QString &feedUrl, const QString &articleTitle) const
{
Q_ASSERT(Preferences::instance()->isRssDownloadingEnabled());
qDebug() << "Matching article:" << articleTitle;
QStringList ruleNames = m_feedRules.value(feedUrl);
foreach (const QString &rule_name, ruleNames) {
DownloadRulePtr rule = m_rules[rule_name];
if (rule->isEnabled() && rule->matches(articleTitle)) return rule;
}
return DownloadRulePtr();
}
void DownloadRuleList::replace(DownloadRuleList *other)
{
m_rules.clear();
m_feedRules.clear();
foreach (const QString &name, other->ruleNames()) {
saveRule(other->getRule(name));
}
}
void DownloadRuleList::saveRulesToStorage()
{
SettingsPtr qBTRSS = Profile::instance().applicationSettings(QLatin1String("qBittorrent-rss"));
qBTRSS->setValue("download_rules", toVariantHash());
}
void DownloadRuleList::loadRulesFromStorage()
{
SettingsPtr qBTRSS = Profile::instance().applicationSettings(QLatin1String("qBittorrent-rss"));
loadRulesFromVariantHash(qBTRSS->value("download_rules").toHash());
}
QVariantHash DownloadRuleList::toVariantHash() const
{
QVariantHash ret;
foreach (const DownloadRulePtr &rule, m_rules.values()) {
ret.insert(rule->name(), rule->toVariantHash());
}
return ret;
}
void DownloadRuleList::loadRulesFromVariantHash(const QVariantHash &h)
{
QVariantHash::ConstIterator it = h.begin();
QVariantHash::ConstIterator itend = h.end();
for ( ; it != itend; ++it) {
DownloadRulePtr rule = DownloadRule::fromVariantHash(it.value().toHash());
if (rule && !rule->name().isEmpty())
saveRule(rule);
}
}
void DownloadRuleList::saveRule(const DownloadRulePtr &rule)
{
qDebug() << Q_FUNC_INFO << rule->name();
Q_ASSERT(rule);
if (m_rules.contains(rule->name())) {
qDebug("This is an update, removing old rule first");
removeRule(rule->name());
}
m_rules.insert(rule->name(), rule);
// Update feedRules hashtable
foreach (const QString &feedUrl, rule->rssFeeds()) {
m_feedRules[feedUrl].append(rule->name());
}
qDebug() << Q_FUNC_INFO << "EXIT";
}
void DownloadRuleList::removeRule(const QString &name)
{
qDebug() << Q_FUNC_INFO << name;
if (!m_rules.contains(name)) return;
DownloadRulePtr rule = m_rules.take(name);
// Update feedRules hashtable
foreach (const QString &feedUrl, rule->rssFeeds()) {
m_feedRules[feedUrl].removeOne(rule->name());
}
}
void DownloadRuleList::renameRule(const QString &oldName, const QString &newName)
{
if (!m_rules.contains(oldName)) return;
DownloadRulePtr rule = m_rules.take(oldName);
rule->setName(newName);
m_rules.insert(newName, rule);
// Update feedRules hashtable
foreach (const QString &feedUrl, rule->rssFeeds()) {
m_feedRules[feedUrl].replace(m_feedRules[feedUrl].indexOf(oldName), newName);
}
}
DownloadRulePtr DownloadRuleList::getRule(const QString &name) const
{
return m_rules.value(name);
}
QStringList DownloadRuleList::ruleNames() const
{
return m_rules.keys();
}
bool DownloadRuleList::isEmpty() const
{
return m_rules.isEmpty();
}
bool DownloadRuleList::serialize(const QString &path)
{
QFile f(path);
if (f.open(QIODevice::WriteOnly)) {
QDataStream out(&f);
out.setVersion(QDataStream::Qt_4_5);
out << toVariantHash();
f.close();
return true;
}
return false;
}
bool DownloadRuleList::unserialize(const QString &path)
{
QFile f(path);
if (f.open(QIODevice::ReadOnly)) {
QDataStream in(&f);
in.setVersion(QDataStream::Qt_4_5);
QVariantHash tmp;
in >> tmp;
f.close();
if (tmp.isEmpty())
return false;
qDebug("Processing was successful!");
loadRulesFromVariantHash(tmp);
return true;
} else {
qDebug("Error: could not open file at %s", qPrintable(path));
return false;
}
}

View File

@@ -1,73 +0,0 @@
/*
* Bittorrent Client using Qt and libtorrent.
* Copyright (C) 2010 Christophe Dumez
*
* 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.
*
* Contact : chris@qbittorrent.org
*/
#ifndef RSSDOWNLOADRULELIST_H
#define RSSDOWNLOADRULELIST_H
#include <QList>
#include <QHash>
#include <QVariantHash>
#include "rssdownloadrule.h"
namespace Rss
{
class DownloadRuleList
{
Q_DISABLE_COPY(DownloadRuleList)
public:
DownloadRuleList();
DownloadRulePtr findMatchingRule(const QString &feedUrl, const QString &articleTitle) const;
// Operators
void saveRule(const DownloadRulePtr &rule);
void removeRule(const QString &name);
void renameRule(const QString &oldName, const QString &newName);
DownloadRulePtr getRule(const QString &name) const;
QStringList ruleNames() const;
bool isEmpty() const;
void saveRulesToStorage();
bool serialize(const QString &path);
bool unserialize(const QString &path);
void replace(DownloadRuleList *other);
private:
void loadRulesFromStorage();
void loadRulesFromVariantHash(const QVariantHash &l);
QVariantHash toVariantHash() const;
private:
QHash<QString, DownloadRulePtr> m_rules;
QHash<QString, QStringList> m_feedRules;
};
}
#endif // RSSDOWNLOADFILTERLIST_H

View File

@@ -1,458 +0,0 @@
/*
* Bittorrent Client using Qt and libtorrent.
* Copyright (C) 2015 Vladimir Golovnev <glassez@yandex.ru>
* Copyright (C) 2010 Christophe Dumez <chris@qbittorrent.org>
* Copyright (C) 2010 Arnaud Demaiziere <arnaud@qbittorrent.org>
*
* 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.
*
* Contact: chris@qbittorrent.org, arnaud@qbittorrent.org
*/
#include "rssfeed.h"
#include <QDebug>
#include "base/preferences.h"
#include "base/logger.h"
#include "base/profile.h"
#include "base/bittorrent/session.h"
#include "base/bittorrent/magneturi.h"
#include "base/utils/misc.h"
#include "base/utils/fs.h"
#include "base/net/downloadmanager.h"
#include "base/net/downloadhandler.h"
#include "private/rssparser.h"
#include "rssdownloadrulelist.h"
#include "rssarticle.h"
#include "rssfolder.h"
#include "rssmanager.h"
namespace Rss
{
bool articleDateRecentThan(const ArticlePtr &left, const ArticlePtr &right)
{
return left->date() > right->date();
}
}
using namespace Rss;
Feed::Feed(const QString &url, Manager *manager)
: m_manager(manager)
, m_url (QUrl::fromEncoded(url.toUtf8()).toString())
, m_icon(":/icons/qbt-theme/application-rss+xml.png")
, m_unreadCount(0)
, m_dirty(false)
, m_inErrorState(false)
, m_loading(false)
{
qDebug() << Q_FUNC_INFO << m_url;
m_parser = new Private::Parser;
m_parser->moveToThread(m_manager->workingThread());
connect(this, SIGNAL(destroyed()), m_parser, SLOT(deleteLater()));
// Listen for new RSS downloads
connect(m_parser, SIGNAL(feedTitle(QString)), SLOT(handleFeedTitle(QString)));
connect(m_parser, SIGNAL(newArticle(QVariantHash)), SLOT(handleNewArticle(QVariantHash)));
connect(m_parser, SIGNAL(finished(QString)), SLOT(handleParsingFinished(QString)));
// Download the RSS Feed icon
Net::DownloadHandler *handler = Net::DownloadManager::instance()->downloadUrl(iconUrl(), true);
connect(handler, SIGNAL(downloadFinished(QString,QString)), this, SLOT(handleIconDownloadFinished(QString,QString)));
// Load old RSS articles
loadItemsFromDisk();
refresh();
}
Feed::~Feed()
{
if (!m_icon.startsWith(":/") && QFile::exists(m_icon))
Utils::Fs::forceRemove(m_icon);
}
void Feed::saveItemsToDisk()
{
qDebug() << Q_FUNC_INFO << m_url;
if (!m_dirty) return;
m_dirty = false;
SettingsPtr qBTRSSFeeds = Profile::instance().applicationSettings(QLatin1String("qBittorrent-rss-feeds"));
QVariantList oldItems;
ArticleHash::ConstIterator it = m_articles.begin();
ArticleHash::ConstIterator itend = m_articles.end();
for (; it != itend; ++it)
oldItems << it.value()->toHash();
qDebug("Saving %d old items for feed %s", oldItems.size(), qPrintable(displayName()));
QHash<QString, QVariant> allOldItems = qBTRSSFeeds->value("old_items", QHash<QString, QVariant>()).toHash();
allOldItems[m_url] = oldItems;
qBTRSSFeeds->setValue("old_items", allOldItems);
}
void Feed::loadItemsFromDisk()
{
SettingsPtr qBTRSSFeeds = Profile::instance().applicationSettings(QLatin1String("qBittorrent-rss-feeds"));
QHash<QString, QVariant> allOldItems = qBTRSSFeeds->value("old_items", QHash<QString, QVariant>()).toHash();
const QVariantList oldItems = allOldItems.value(m_url, QVariantList()).toList();
qDebug("Loading %d old items for feed %s", oldItems.size(), qPrintable(displayName()));
foreach (const QVariant &var_it, oldItems) {
QVariantHash item = var_it.toHash();
ArticlePtr rssItem = Article::fromHash(this, item);
if (rssItem)
addArticle(rssItem);
}
}
void Feed::addArticle(const ArticlePtr &article)
{
int maxArticles = Preferences::instance()->getRSSMaxArticlesPerFeed();
if (!m_articles.contains(article->guid())) {
m_dirty = true;
// Update unreadCount
if (!article->isRead())
++m_unreadCount;
// Insert in hash table
m_articles[article->guid()] = article;
if (!article->isRead()) // Optimization
connect(article.data(), SIGNAL(articleWasRead()), SLOT(handleArticleRead()), Qt::UniqueConnection);
// Insertion sort
ArticleList::Iterator lowerBound = qLowerBound(m_articlesByDate.begin(), m_articlesByDate.end(), article, articleDateRecentThan);
m_articlesByDate.insert(lowerBound, article);
int lbIndex = m_articlesByDate.indexOf(article);
if (m_articlesByDate.size() > maxArticles) {
ArticlePtr oldestArticle = m_articlesByDate.takeLast();
m_articles.remove(oldestArticle->guid());
// Update unreadCount
if (!oldestArticle->isRead())
--m_unreadCount;
}
// Check if article was inserted at the end of the list and will break max_articles limit
if (Preferences::instance()->isRssDownloadingEnabled())
if ((lbIndex < maxArticles) && !article->isRead())
downloadArticleTorrentIfMatching(article);
}
else {
// m_articles.contains(article->guid())
// Try to download skipped articles
if (Preferences::instance()->isRssDownloadingEnabled()) {
ArticlePtr skipped = m_articles.value(article->guid(), ArticlePtr());
if (skipped)
if (!skipped->isRead())
downloadArticleTorrentIfMatching(skipped);
}
}
}
bool Feed::refresh()
{
if (m_loading) {
qWarning() << Q_FUNC_INFO << "Feed" << displayName() << "is already being refreshed, ignoring request";
return false;
}
m_loading = true;
// Download the RSS again
Net::DownloadHandler *handler = Net::DownloadManager::instance()->downloadUrl(m_url);
connect(handler, SIGNAL(downloadFinished(QString,QByteArray)), this, SLOT(handleRssDownloadFinished(QString,QByteArray)));
connect(handler, SIGNAL(downloadFailed(QString,QString)), this, SLOT(handleRssDownloadFailed(QString,QString)));
return true;
}
QString Feed::id() const
{
return m_url;
}
void Feed::removeAllSettings()
{
qDebug() << "Removing all settings / history for feed: " << m_url;
SettingsPtr qBTRSS = Profile::instance().applicationSettings(QLatin1String("qBittorrent-rss"));
QVariantHash feedsWDownloader = qBTRSS->value("downloader_on", QVariantHash()).toHash();
if (feedsWDownloader.contains(m_url)) {
feedsWDownloader.remove(m_url);
qBTRSS->setValue("downloader_on", feedsWDownloader);
}
QVariantHash allFeedsFilters = qBTRSS->value("feed_filters", QVariantHash()).toHash();
if (allFeedsFilters.contains(m_url)) {
allFeedsFilters.remove(m_url);
qBTRSS->setValue("feed_filters", allFeedsFilters);
}
SettingsPtr qBTRSSFeeds = Profile::instance().applicationSettings(QLatin1String("qBittorrent-rss-feeds"));
QVariantHash allOldItems = qBTRSSFeeds->value("old_items", QVariantHash()).toHash();
if (allOldItems.contains(m_url)) {
allOldItems.remove(m_url);
qBTRSSFeeds->setValue("old_items", allOldItems);
}
}
bool Feed::isLoading() const
{
return m_loading;
}
QString Feed::title() const
{
return m_title;
}
void Feed::rename(const QString &newName)
{
qDebug() << "Renaming stream to" << newName;
m_alias = newName;
}
// Return the alias if the stream has one, the url if it has no alias
QString Feed::displayName() const
{
if (!m_alias.isEmpty())
return m_alias;
if (!m_title.isEmpty())
return m_title;
return m_url;
}
QString Feed::url() const
{
return m_url;
}
QString Feed::iconPath() const
{
if (m_inErrorState)
return QLatin1String(":/icons/qbt-theme/unavailable.png");
return m_icon;
}
bool Feed::hasCustomIcon() const
{
return !m_icon.startsWith(":/");
}
void Feed::setIconPath(const QString &path)
{
QString nativePath = Utils::Fs::fromNativePath(path);
if ((nativePath == m_icon) || nativePath.isEmpty() || !QFile::exists(nativePath)) return;
if (!m_icon.startsWith(":/") && QFile::exists(m_icon))
Utils::Fs::forceRemove(m_icon);
m_icon = nativePath;
}
ArticlePtr Feed::getItem(const QString &guid) const
{
return m_articles.value(guid);
}
uint Feed::count() const
{
return m_articles.size();
}
void Feed::markAsRead()
{
ArticleHash::ConstIterator it = m_articles.begin();
ArticleHash::ConstIterator itend = m_articles.end();
for (; it != itend; ++it)
it.value()->markAsRead();
m_unreadCount = 0;
m_manager->forwardFeedInfosChanged(m_url, displayName(), 0);
}
uint Feed::unreadCount() const
{
return m_unreadCount;
}
ArticleList Feed::articleListByDateDesc() const
{
return m_articlesByDate;
}
const ArticleHash &Feed::articleHash() const
{
return m_articles;
}
ArticleList Feed::unreadArticleListByDateDesc() const
{
ArticleList unreadNews;
ArticleList::ConstIterator it = m_articlesByDate.begin();
ArticleList::ConstIterator itend = m_articlesByDate.end();
for (; it != itend; ++it)
if (!(*it)->isRead())
unreadNews << *it;
return unreadNews;
}
// download the icon from the address
QString Feed::iconUrl() const
{
// XXX: This works for most sites but it is not perfect
return QString("http://%1/favicon.ico").arg(QUrl(m_url).host());
}
void Feed::handleIconDownloadFinished(const QString &url, const QString &filePath)
{
Q_UNUSED(url);
setIconPath(filePath);
qDebug() << Q_FUNC_INFO << "icon path:" << m_icon;
m_manager->forwardFeedIconChanged(m_url, m_icon);
}
void Feed::handleRssDownloadFinished(const QString &url, const QByteArray &data)
{
Q_UNUSED(url);
qDebug() << Q_FUNC_INFO << "Successfully downloaded RSS feed at" << m_url;
// Parse the download RSS
QMetaObject::invokeMethod(m_parser, "parse", Qt::QueuedConnection, Q_ARG(QByteArray, data));
}
void Feed::handleRssDownloadFailed(const QString &url, const QString &error)
{
Q_UNUSED(url);
m_inErrorState = true;
m_loading = false;
m_manager->forwardFeedInfosChanged(m_url, displayName(), m_unreadCount);
qWarning() << "Failed to download RSS feed at" << m_url;
qWarning() << "Reason:" << error;
}
void Feed::handleFeedTitle(const QString &title)
{
if (m_title == title) return;
m_title = title;
// Notify that we now have something better than a URL to display
if (m_alias.isEmpty())
m_manager->forwardFeedInfosChanged(m_url, title, m_unreadCount);
}
void Feed::downloadArticleTorrentIfMatching(const ArticlePtr &article)
{
Q_ASSERT(Preferences::instance()->isRssDownloadingEnabled());
qDebug().nospace() << Q_FUNC_INFO << " Deferring matching of " << article->title() << " from " << displayName() << " RSS feed";
m_manager->downloadArticleTorrentIfMatching(m_url, article);
}
void Feed::deferredDownloadArticleTorrentIfMatching(const ArticlePtr &article)
{
qDebug().nospace() << Q_FUNC_INFO << " Matching of " << article->title() << " from " << displayName() << " RSS feed";
DownloadRuleList *rules = m_manager->downloadRules();
DownloadRulePtr matchingRule = rules->findMatchingRule(m_url, article->title());
if (!matchingRule) return;
if (matchingRule->ignoreDays() > 0) {
QDateTime lastMatch = matchingRule->lastMatch();
if (lastMatch.isValid()) {
if (QDateTime::currentDateTime() < lastMatch.addDays(matchingRule->ignoreDays())) {
article->markAsRead();
return;
}
}
}
matchingRule->setLastMatch(QDateTime::currentDateTime());
rules->saveRulesToStorage();
// Download the torrent
const QString &torrentUrl = article->torrentUrl();
if (torrentUrl.isEmpty()) {
Logger::instance()->addMessage(tr("Automatic download of '%1' from '%2' RSS feed failed because it doesn't contain a torrent or a magnet link...").arg(article->title()).arg(displayName()), Log::WARNING);
article->markAsRead();
return;
}
Logger::instance()->addMessage(tr("Automatically downloading '%1' torrent from '%2' RSS feed...").arg(article->title()).arg(displayName()));
if (BitTorrent::MagnetUri(torrentUrl).isValid())
article->markAsRead();
else
connect(BitTorrent::Session::instance(), SIGNAL(downloadFromUrlFinished(QString)), article.data(), SLOT(handleTorrentDownloadSuccess(const QString&)), Qt::UniqueConnection);
BitTorrent::AddTorrentParams params;
params.savePath = matchingRule->savePath();
params.category = matchingRule->category();
if (matchingRule->addPaused() == DownloadRule::ALWAYS_PAUSED)
params.addPaused = TriStateBool::True;
else if (matchingRule->addPaused() == DownloadRule::NEVER_PAUSED)
params.addPaused = TriStateBool::False;
BitTorrent::Session::instance()->addTorrent(torrentUrl, params);
}
void Feed::recheckRssItemsForDownload()
{
Q_ASSERT(Preferences::instance()->isRssDownloadingEnabled());
foreach (const ArticlePtr &article, m_articlesByDate)
if (!article->isRead())
downloadArticleTorrentIfMatching(article);
}
void Feed::handleNewArticle(const QVariantHash &articleData)
{
ArticlePtr article = Article::fromHash(this, articleData);
if (article.isNull()) {
qDebug() << "Article hash corrupted or guid is uncomputable; feed url: " << m_url;
return;
}
Q_ASSERT(article);
addArticle(article);
m_manager->forwardFeedInfosChanged(m_url, displayName(), m_unreadCount);
// FIXME: We should forward the information here but this would seriously decrease
// performance with current design.
// m_manager->forwardFeedContentChanged(m_url);
}
void Feed::handleParsingFinished(const QString &error)
{
if (!error.isEmpty()) {
qWarning() << "Failed to parse RSS feed at" << m_url;
qWarning() << "Reason:" << error;
}
m_loading = false;
m_inErrorState = !error.isEmpty();
m_manager->forwardFeedInfosChanged(m_url, displayName(), m_unreadCount);
// XXX: Would not be needed if we did this in handleNewArticle() instead
m_manager->forwardFeedContentChanged(m_url);
saveItemsToDisk();
}
void Feed::handleArticleRead()
{
--m_unreadCount;
m_dirty = true;
m_manager->forwardFeedInfosChanged(m_url, displayName(), m_unreadCount);
}

View File

@@ -1,125 +0,0 @@
/*
* Bittorrent Client using Qt and libtorrent.
* Copyright (C) 2015 Vladimir Golovnev <glassez@yandex.ru>
* Copyright (C) 2010 Christophe Dumez <chris@qbittorrent.org>
* Copyright (C) 2010 Arnaud Demaiziere <arnaud@qbittorrent.org>
*
* 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.
*
* Contact: chris@qbittorrent.org, arnaud@qbittorrent.org
*/
#ifndef RSSFEED_H
#define RSSFEED_H
#include <QHash>
#include <QSharedPointer>
#include <QVariantHash>
#include <QXmlStreamReader>
#include <QNetworkCookie>
#include "rssfile.h"
namespace Rss
{
class Folder;
class Feed;
class Manager;
class DownloadRuleList;
typedef QHash<QString, ArticlePtr> ArticleHash;
typedef QSharedPointer<Feed> FeedPtr;
typedef QList<FeedPtr> FeedList;
namespace Private
{
class Parser;
}
bool articleDateRecentThan(const ArticlePtr &left, const ArticlePtr &right);
class Feed: public QObject, public File
{
Q_OBJECT
public:
Feed(const QString &url, Manager * manager);
~Feed();
bool refresh();
QString id() const;
void removeAllSettings();
void saveItemsToDisk();
bool isLoading() const;
QString title() const;
void rename(const QString &newName);
QString displayName() const;
QString url() const;
QString iconPath() const;
bool hasCustomIcon() const;
void setIconPath(const QString &pathHierarchy);
ArticlePtr getItem(const QString &guid) const;
uint count() const;
void markAsRead();
uint unreadCount() const;
ArticleList articleListByDateDesc() const;
const ArticleHash &articleHash() const;
ArticleList unreadArticleListByDateDesc() const;
void recheckRssItemsForDownload();
private slots:
void handleIconDownloadFinished(const QString &url, const QString &filePath);
void handleRssDownloadFinished(const QString &url, const QByteArray &data);
void handleRssDownloadFailed(const QString &url, const QString &error);
void handleFeedTitle(const QString &title);
void handleNewArticle(const QVariantHash &article);
void handleParsingFinished(const QString &error);
void handleArticleRead();
private:
friend class Manager;
QString iconUrl() const;
void loadItemsFromDisk();
void addArticle(const ArticlePtr &article);
void downloadArticleTorrentIfMatching(const ArticlePtr &article);
void deferredDownloadArticleTorrentIfMatching(const ArticlePtr &article);
private:
Manager *m_manager;
Private::Parser *m_parser;
ArticleHash m_articles;
ArticleList m_articlesByDate; // Articles sorted by date (more recent first)
QString m_title;
QString m_url;
QString m_alias;
QString m_icon;
uint m_unreadCount;
bool m_dirty;
bool m_inErrorState;
bool m_loading;
};
}
#endif // RSSFEED_H

View File

@@ -1,253 +0,0 @@
/*
* Bittorrent Client using Qt and libtorrent.
* Copyright (C) 2010 Christophe Dumez <chris@qbittorrent.org>
* Copyright (C) 2010 Arnaud Demaiziere <arnaud@qbittorrent.org>
*
* 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.
*
* Contact: chris@qbittorrent.org, arnaud@qbittorrent.org
*/
#include <QDebug>
#include "base/iconprovider.h"
#include "base/bittorrent/session.h"
#include "rssmanager.h"
#include "rssfeed.h"
#include "rssarticle.h"
#include "rssfolder.h"
using namespace Rss;
Folder::Folder(const QString &name)
: m_name(name)
{
}
uint Folder::unreadCount() const
{
uint nbUnread = 0;
FileHash::ConstIterator it = m_children.begin();
FileHash::ConstIterator itend = m_children.end();
for ( ; it != itend; ++it)
nbUnread += it.value()->unreadCount();
return nbUnread;
}
void Folder::removeChild(const QString &childId)
{
if (m_children.contains(childId)) {
FilePtr child = m_children.take(childId);
child->removeAllSettings();
}
}
// Refresh All Children
bool Folder::refresh()
{
FileHash::ConstIterator it = m_children.begin();
FileHash::ConstIterator itend = m_children.end();
bool refreshed = false;
for ( ; it != itend; ++it) {
if (it.value()->refresh())
refreshed = true;
}
return refreshed;
}
ArticleList Folder::articleListByDateDesc() const
{
ArticleList news;
FileHash::ConstIterator it = m_children.begin();
FileHash::ConstIterator itend = m_children.end();
for ( ; it != itend; ++it) {
int n = news.size();
news << it.value()->articleListByDateDesc();
std::inplace_merge(news.begin(), news.begin() + n, news.end(), articleDateRecentThan);
}
return news;
}
ArticleList Folder::unreadArticleListByDateDesc() const
{
ArticleList unreadNews;
FileHash::ConstIterator it = m_children.begin();
FileHash::ConstIterator itend = m_children.end();
for ( ; it != itend; ++it) {
int n = unreadNews.size();
unreadNews << it.value()->unreadArticleListByDateDesc();
std::inplace_merge(unreadNews.begin(), unreadNews.begin() + n, unreadNews.end(), articleDateRecentThan);
}
return unreadNews;
}
FileList Folder::getContent() const
{
return m_children.values();
}
uint Folder::getNbFeeds() const
{
uint nbFeeds = 0;
FileHash::ConstIterator it = m_children.begin();
FileHash::ConstIterator itend = m_children.end();
for ( ; it != itend; ++it) {
if (FolderPtr folder = qSharedPointerDynamicCast<Folder>(it.value()))
nbFeeds += folder->getNbFeeds();
else
++nbFeeds; // Feed
}
return nbFeeds;
}
QString Folder::displayName() const
{
return m_name;
}
void Folder::rename(const QString &newName)
{
if (m_name == newName) return;
Q_ASSERT(!m_parent->hasChild(newName));
if (!m_parent->hasChild(newName)) {
// Update parent
FilePtr folder = m_parent->m_children.take(m_name);
m_parent->m_children[newName] = folder;
// Actually rename
m_name = newName;
}
}
void Folder::markAsRead()
{
FileHash::ConstIterator it = m_children.begin();
FileHash::ConstIterator itend = m_children.end();
for ( ; it != itend; ++it) {
it.value()->markAsRead();
}
}
FeedList Folder::getAllFeeds() const
{
FeedList streams;
FileHash::ConstIterator it = m_children.begin();
FileHash::ConstIterator itend = m_children.end();
for ( ; it != itend; ++it) {
if (FeedPtr feed = qSharedPointerDynamicCast<Feed>(it.value()))
streams << feed;
else if (FolderPtr folder = qSharedPointerDynamicCast<Folder>(it.value()))
streams << folder->getAllFeeds();
}
return streams;
}
QHash<QString, FeedPtr> Folder::getAllFeedsAsHash() const
{
QHash<QString, FeedPtr> ret;
FileHash::ConstIterator it = m_children.begin();
FileHash::ConstIterator itend = m_children.end();
for ( ; it != itend; ++it) {
if (FeedPtr feed = qSharedPointerDynamicCast<Feed>(it.value())) {
qDebug() << Q_FUNC_INFO << feed->url();
ret[feed->url()] = feed;
}
else if (FolderPtr folder = qSharedPointerDynamicCast<Folder>(it.value())) {
ret.unite(folder->getAllFeedsAsHash());
}
}
return ret;
}
bool Folder::addFile(const FilePtr &item)
{
Q_ASSERT(!m_children.contains(item->id()));
if (!m_children.contains(item->id())) {
m_children[item->id()] = item;
// Update parent
item->m_parent = this;
return true;
}
return false;
}
void Folder::removeAllItems()
{
m_children.clear();
}
FilePtr Folder::child(const QString &childId)
{
return m_children.value(childId);
}
void Folder::removeAllSettings()
{
FileHash::ConstIterator it = m_children.begin();
FileHash::ConstIterator itend = m_children.end();
for ( ; it != itend; ++it)
it.value()->removeAllSettings();
}
void Folder::saveItemsToDisk()
{
foreach (const FilePtr &child, m_children.values())
child->saveItemsToDisk();
}
QString Folder::id() const
{
return m_name;
}
QString Folder::iconPath() const
{
return IconProvider::instance()->getIconPath("inode-directory");
}
bool Folder::hasChild(const QString &childId)
{
return m_children.contains(childId);
}
FilePtr Folder::takeChild(const QString &childId)
{
return m_children.take(childId);
}
void Folder::recheckRssItemsForDownload()
{
FileHash::ConstIterator it = m_children.begin();
FileHash::ConstIterator itend = m_children.end();
for ( ; it != itend; ++it)
it.value()->recheckRssItemsForDownload();
}

View File

@@ -1,86 +0,0 @@
/*
* Bittorrent Client using Qt and libtorrent.
* Copyright (C) 2010 Christophe Dumez <chris@qbittorrent.org>
* Copyright (C) 2010 Arnaud Demaiziere <arnaud@qbittorrent.org>
*
* 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.
*
* Contact: chris@qbittorrent.org, arnaud@qbittorrent.org
*/
#ifndef RSSFOLDER_H
#define RSSFOLDER_H
#include <QHash>
#include <QSharedPointer>
#include "rssfile.h"
namespace Rss
{
class Folder;
class Feed;
class Manager;
typedef QHash<QString, FilePtr> FileHash;
typedef QSharedPointer<Feed> FeedPtr;
typedef QSharedPointer<Folder> FolderPtr;
typedef QList<FeedPtr> FeedList;
class Folder: public File
{
public:
explicit Folder(const QString &name = QString());
uint unreadCount() const;
uint getNbFeeds() const;
FileList getContent() const;
FeedList getAllFeeds() const;
QHash<QString, FeedPtr> getAllFeedsAsHash() const;
QString displayName() const;
QString id() const;
QString iconPath() const;
bool hasChild(const QString &childId);
ArticleList articleListByDateDesc() const;
ArticleList unreadArticleListByDateDesc() const;
void rename(const QString &newName);
void markAsRead();
bool refresh();
void removeAllSettings();
void saveItemsToDisk();
void recheckRssItemsForDownload();
void removeAllItems();
FilePtr child(const QString &childId);
FilePtr takeChild(const QString &childId);
bool addFile(const FilePtr &item);
void removeChild(const QString &childId);
private:
QString m_name;
FileHash m_children;
};
}
#endif // RSSFOLDER_H

View File

@@ -1,218 +0,0 @@
/*
* Bittorrent Client using Qt and libtorrent.
* Copyright (C) 2010 Christophe Dumez <chris@qbittorrent.org>
* Copyright (C) 2010 Arnaud Demaiziere <arnaud@qbittorrent.org>
*
* 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.
*
* Contact: chris@qbittorrent.org, arnaud@qbittorrent.org
*/
#include <QDebug>
#include "base/logger.h"
#include "base/preferences.h"
#include "rssfolder.h"
#include "rssfeed.h"
#include "rssarticle.h"
#include "rssdownloadrulelist.h"
#include "rssmanager.h"
static const int MSECS_PER_MIN = 60000;
using namespace Rss;
using namespace Rss::Private;
Manager::Manager(QObject *parent)
: QObject(parent)
, m_downloadRules(new DownloadRuleList)
, m_rootFolder(new Folder)
, m_workingThread(new QThread(this))
{
m_workingThread->start();
connect(&m_refreshTimer, SIGNAL(timeout()), SLOT(refresh()));
m_refreshInterval = Preferences::instance()->getRSSRefreshInterval();
m_refreshTimer.start(m_refreshInterval * MSECS_PER_MIN);
m_deferredDownloadTimer.setInterval(1);
m_deferredDownloadTimer.setSingleShot(true);
connect(&m_deferredDownloadTimer, SIGNAL(timeout()), SLOT(downloadNextArticleTorrentIfMatching()));
}
Manager::~Manager()
{
qDebug("Deleting RSSManager...");
m_workingThread->quit();
m_workingThread->wait();
delete m_downloadRules;
m_rootFolder->saveItemsToDisk();
saveStreamList();
m_rootFolder.clear();
qDebug("RSSManager deleted");
}
void Manager::updateRefreshInterval(uint val)
{
if (m_refreshInterval != val) {
m_refreshInterval = val;
m_refreshTimer.start(m_refreshInterval * 60000);
qDebug("New RSS refresh interval is now every %dmin", m_refreshInterval);
}
}
void Manager::loadStreamList()
{
const Preferences *const pref = Preferences::instance();
const QStringList streamsUrl = pref->getRssFeedsUrls();
const QStringList aliases = pref->getRssFeedsAliases();
if (streamsUrl.size() != aliases.size()) {
Logger::instance()->addMessage("Corrupted RSS list, not loading it.", Log::WARNING);
return;
}
uint i = 0;
qDebug() << Q_FUNC_INFO << streamsUrl;
foreach (QString s, streamsUrl) {
QStringList path = s.split("\\", QString::SkipEmptyParts);
if (path.empty()) continue;
const QString feedUrl = path.takeLast();
qDebug() << "Feed URL:" << feedUrl;
// Create feed path (if it does not exists)
FolderPtr feedParent = m_rootFolder;
foreach (const QString &folderName, path) {
if (!feedParent->hasChild(folderName)) {
qDebug() << "Adding parent folder:" << folderName;
FolderPtr folder(new Folder(folderName));
feedParent->addFile(folder);
feedParent = folder;
}
else {
feedParent = qSharedPointerDynamicCast<Folder>(feedParent->child(folderName));
}
}
// Create feed
qDebug() << "Adding feed to parent folder";
FeedPtr stream(new Feed(feedUrl, this));
feedParent->addFile(stream);
const QString &alias = aliases[i];
if (!alias.isEmpty())
stream->rename(alias);
++i;
}
qDebug("NB RSS streams loaded: %d", streamsUrl.size());
}
void Manager::forwardFeedContentChanged(const QString &url)
{
emit feedContentChanged(url);
}
void Manager::forwardFeedInfosChanged(const QString &url, const QString &displayName, uint unreadCount)
{
emit feedInfosChanged(url, displayName, unreadCount);
}
void Manager::forwardFeedIconChanged(const QString &url, const QString &iconPath)
{
emit feedIconChanged(url, iconPath);
}
void Manager::moveFile(const FilePtr &file, const FolderPtr &destinationFolder)
{
Folder *srcFolder = file->parentFolder();
if (destinationFolder != srcFolder) {
// Remove reference in old folder
srcFolder->takeChild(file->id());
// add to new Folder
destinationFolder->addFile(file);
}
else {
qDebug("Nothing to move, same destination folder");
}
}
void Manager::saveStreamList() const
{
QStringList streamsUrl;
QStringList aliases;
FeedList streams = m_rootFolder->getAllFeeds();
foreach (const FeedPtr &stream, streams) {
// This backslash has nothing to do with path handling
QString streamPath = stream->pathHierarchy().join("\\");
if (streamPath.isNull())
streamPath = "";
qDebug("Saving stream path: %s", qPrintable(streamPath));
streamsUrl << streamPath;
aliases << stream->displayName();
}
Preferences *const pref = Preferences::instance();
pref->setRssFeedsUrls(streamsUrl);
pref->setRssFeedsAliases(aliases);
}
DownloadRuleList *Manager::downloadRules() const
{
Q_ASSERT(m_downloadRules);
return m_downloadRules;
}
FolderPtr Manager::rootFolder() const
{
return m_rootFolder;
}
QThread *Manager::workingThread() const
{
return m_workingThread;
}
void Manager::refresh()
{
m_rootFolder->refresh();
}
void Manager::downloadArticleTorrentIfMatching(const QString &url, const ArticlePtr &article)
{
m_deferredDownloads.append(qMakePair(url, article));
m_deferredDownloadTimer.start();
}
void Manager::downloadNextArticleTorrentIfMatching()
{
if (m_deferredDownloads.empty())
return;
// Schedule to process the next article (if any)
m_deferredDownloadTimer.start();
QPair<QString, ArticlePtr> urlArticle(m_deferredDownloads.takeFirst());
FeedList streams = m_rootFolder->getAllFeeds();
foreach (const FeedPtr &stream, streams) {
if (stream->url() == urlArticle.first) {
stream->deferredDownloadArticleTorrentIfMatching(urlArticle.second);
break;
}
}
}

View File

@@ -1,100 +0,0 @@
/*
* Bittorrent Client using Qt and libtorrent.
* Copyright (C) 2010 Christophe Dumez <chris@qbittorrent.org>
* Copyright (C) 2010 Arnaud Demaiziere <arnaud@qbittorrent.org>
*
* 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.
*
* Contact: chris@qbittorrent.org, arnaud@qbittorrent.org
*/
#ifndef RSSMANAGER_H
#define RSSMANAGER_H
#include <QList>
#include <QObject>
#include <QPair>
#include <QTimer>
#include <QSharedPointer>
#include <QThread>
namespace Rss
{
class Article;
class DownloadRuleList;
class File;
class Folder;
class Feed;
class Manager;
typedef QSharedPointer<Article> ArticlePtr;
typedef QSharedPointer<File> FilePtr;
typedef QSharedPointer<Folder> FolderPtr;
typedef QSharedPointer<Feed> FeedPtr;
typedef QSharedPointer<Manager> ManagerPtr;
class Manager: public QObject
{
Q_OBJECT
public:
explicit Manager(QObject *parent = 0);
~Manager();
DownloadRuleList *downloadRules() const;
FolderPtr rootFolder() const;
QThread *workingThread() const;
void downloadArticleTorrentIfMatching(const QString &url, const ArticlePtr &article);
public slots:
void refresh();
void loadStreamList();
void saveStreamList() const;
void forwardFeedContentChanged(const QString &url);
void forwardFeedInfosChanged(const QString &url, const QString &displayName, uint unreadCount);
void forwardFeedIconChanged(const QString &url, const QString &iconPath);
void moveFile(const FilePtr &file, const FolderPtr &destinationFolder);
void updateRefreshInterval(uint val);
signals:
void feedContentChanged(const QString &url);
void feedInfosChanged(const QString &url, const QString &displayName, uint unreadCount);
void feedIconChanged(const QString &url, const QString &iconPath);
private slots:
void downloadNextArticleTorrentIfMatching();
private:
QTimer m_refreshTimer;
uint m_refreshInterval;
DownloadRuleList *m_downloadRules;
FolderPtr m_rootFolder;
QThread *m_workingThread;
QTimer m_deferredDownloadTimer;
QList<QPair<QString, ArticlePtr>> m_deferredDownloads;
};
}
#endif // RSSMANAGER_H