diff --git a/src/app/application.cpp b/src/app/application.cpp index 56a0906f9..d534eac80 100644 --- a/src/app/application.cpp +++ b/src/app/application.cpp @@ -74,6 +74,8 @@ #include "base/net/proxyconfigurationmanager.h" #include "base/bittorrent/session.h" #include "base/bittorrent/torrenthandle.h" +#include "base/rss/rss_autodownloader.h" +#include "base/rss/rss_session.h" namespace { @@ -438,6 +440,9 @@ int Application::exec(const QStringList ¶ms) m_webui = new WebUI; #endif + new RSS::Session; // create RSS::Session singleton + new RSS::AutoDownloader; // create RSS::AutoDownloader singleton + #ifdef DISABLE_GUI #ifndef DISABLE_WEBUI Preferences* const pref = Preferences::instance(); @@ -629,6 +634,9 @@ void Application::cleanup() delete m_webui; #endif + delete RSS::AutoDownloader::instance(); + delete RSS::Session::instance(); + ScanFoldersModel::freeInstance(); BitTorrent::Session::freeInstance(); #ifndef DISABLE_COUNTRIES_RESOLUTION diff --git a/src/app/application.h b/src/app/application.h index 7d7fdbaa8..313bcfa4a 100644 --- a/src/app/application.h +++ b/src/app/application.h @@ -64,6 +64,12 @@ namespace BitTorrent class TorrentHandle; } +namespace RSS +{ + class Session; + class AutoDownloader; +} + class Application : public BaseApplication { Q_OBJECT diff --git a/src/base/asyncfilestorage.cpp b/src/base/asyncfilestorage.cpp new file mode 100644 index 000000000..94402b924 --- /dev/null +++ b/src/base/asyncfilestorage.cpp @@ -0,0 +1,88 @@ +/* + * Bittorrent Client using Qt and libtorrent. + * Copyright (C) 2017 Vladimir Golovnev + * + * 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 "asyncfilestorage.h" + +#include +#include +#include + +AsyncFileStorage::AsyncFileStorage(const QString &storageFolderPath, QObject *parent) + : QObject(parent) + , m_storageDir(storageFolderPath) + , m_lockFile(m_storageDir.absoluteFilePath(QStringLiteral("storage.lock"))) +{ + if (!m_storageDir.mkpath(m_storageDir.absolutePath())) + throw AsyncFileStorageError( + QString("Could not create directory '%1'.").arg(m_storageDir.absolutePath())); + + // TODO: This folder locking approach does not work for UNIX systems. Implement it. + if (!m_lockFile.open(QFile::WriteOnly)) + throw AsyncFileStorageError(m_lockFile.errorString()); +} + +AsyncFileStorage::~AsyncFileStorage() +{ + m_lockFile.close(); + m_lockFile.remove(); +} + +void AsyncFileStorage::store(const QString &fileName, const QByteArray &data) +{ + QMetaObject::invokeMethod(this, "store_impl", Qt::QueuedConnection + , Q_ARG(QString, fileName), Q_ARG(QByteArray, data)); +} + +QDir AsyncFileStorage::storageDir() const +{ + return m_storageDir; +} + +void AsyncFileStorage::store_impl(const QString &fileName, const QByteArray &data) +{ + const QString filePath = m_storageDir.absoluteFilePath(fileName); + QSaveFile file(filePath); + qDebug() << "AsyncFileStorage: Saving data to" << filePath; + if (file.open(QIODevice::WriteOnly)) { + file.write(data); + if (!file.commit()) { + qDebug() << "AsyncFileStorage: Failed to save data"; + emit failed(filePath, file.errorString()); + } + } +} + +AsyncFileStorageError::AsyncFileStorageError(const QString &message) + : std::runtime_error(message.toUtf8().data()) +{ +} + +QString AsyncFileStorageError::message() const +{ + return what(); +} diff --git a/src/base/rss/rssfile.cpp b/src/base/asyncfilestorage.h similarity index 63% rename from src/base/rss/rssfile.cpp rename to src/base/asyncfilestorage.h index 394bd56f0..79e53faf7 100644 --- a/src/base/rss/rssfile.cpp +++ b/src/base/asyncfilestorage.h @@ -1,7 +1,6 @@ /* * Bittorrent Client using Qt and libtorrent. - * Copyright (C) 2010 Christophe Dumez - * Copyright (C) 2010 Arnaud Demaiziere + * Copyright (C) 2017 Vladimir Golovnev * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License @@ -25,27 +24,41 @@ * 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 -File::~File() {} +#include +#include +#include -Folder *File::parentFolder() const +class AsyncFileStorageError: public std::runtime_error { - return m_parent; -} +public: + explicit AsyncFileStorageError(const QString &message); + QString message() const; +}; -QStringList File::pathHierarchy() const +class AsyncFileStorage: public QObject { - QStringList path; - if (m_parent) - path << m_parent->pathHierarchy(); - path << id(); - return path; -} + Q_OBJECT + +public: + explicit AsyncFileStorage(const QString &storageFolderPath, QObject *parent = nullptr); + ~AsyncFileStorage() override; + + void store(const QString &fileName, const QByteArray &data); + + QDir storageDir() const; + +signals: + void failed(const QString &fileName, const QString &errorString); + +private: + Q_INVOKABLE void store_impl(const QString &fileName, const QByteArray &data); + + QDir m_storageDir; + QFile m_lockFile; +}; diff --git a/src/base/base.pri b/src/base/base.pri index ef9906d14..6289676cc 100644 --- a/src/base/base.pri +++ b/src/base/base.pri @@ -1,4 +1,5 @@ HEADERS += \ + $$PWD/asyncfilestorage.h \ $$PWD/types.h \ $$PWD/tristatebool.h \ $$PWD/filesystemwatcher.h \ @@ -40,14 +41,14 @@ HEADERS += \ $$PWD/bittorrent/private/filterparserthread.h \ $$PWD/bittorrent/private/statistics.h \ $$PWD/bittorrent/private/resumedatasavingmanager.h \ - $$PWD/rss/rssmanager.h \ - $$PWD/rss/rssfeed.h \ - $$PWD/rss/rssfolder.h \ - $$PWD/rss/rssfile.h \ - $$PWD/rss/rssarticle.h \ - $$PWD/rss/rssdownloadrule.h \ - $$PWD/rss/rssdownloadrulelist.h \ - $$PWD/rss/private/rssparser.h \ + $$PWD/rss/rss_article.h \ + $$PWD/rss/rss_item.h \ + $$PWD/rss/rss_feed.h \ + $$PWD/rss/rss_folder.h \ + $$PWD/rss/rss_session.h \ + $$PWD/rss/rss_autodownloader.h \ + $$PWD/rss/rss_autodownloadrule.h \ + $$PWD/rss/private/rss_parser.h \ $$PWD/utils/fs.h \ $$PWD/utils/gzip.h \ $$PWD/utils/misc.h \ @@ -64,6 +65,7 @@ HEADERS += \ $$PWD/searchengine.h SOURCES += \ + $$PWD/asyncfilestorage.cpp \ $$PWD/tristatebool.cpp \ $$PWD/filesystemwatcher.cpp \ $$PWD/logger.cpp \ @@ -100,14 +102,14 @@ SOURCES += \ $$PWD/bittorrent/private/filterparserthread.cpp \ $$PWD/bittorrent/private/statistics.cpp \ $$PWD/bittorrent/private/resumedatasavingmanager.cpp \ - $$PWD/rss/rssmanager.cpp \ - $$PWD/rss/rssfeed.cpp \ - $$PWD/rss/rssfolder.cpp \ - $$PWD/rss/rssarticle.cpp \ - $$PWD/rss/rssdownloadrule.cpp \ - $$PWD/rss/rssdownloadrulelist.cpp \ - $$PWD/rss/rssfile.cpp \ - $$PWD/rss/private/rssparser.cpp \ + $$PWD/rss/rss_article.cpp \ + $$PWD/rss/rss_item.cpp \ + $$PWD/rss/rss_feed.cpp \ + $$PWD/rss/rss_folder.cpp \ + $$PWD/rss/rss_session.cpp \ + $$PWD/rss/rss_autodownloader.cpp \ + $$PWD/rss/rss_autodownloadrule.cpp \ + $$PWD/rss/private/rss_parser.cpp \ $$PWD/utils/fs.cpp \ $$PWD/utils/gzip.cpp \ $$PWD/utils/misc.cpp \ diff --git a/src/base/bittorrent/session.cpp b/src/base/bittorrent/session.cpp index 405fa3c4b..3f8e52239 100644 --- a/src/base/bittorrent/session.cpp +++ b/src/base/bittorrent/session.cpp @@ -3372,8 +3372,8 @@ void Session::createTorrentHandle(const libt::torrent_handle &nativeHandle) bool fromMagnetUri = !torrent->hasMetadata(); if (data.resumed) { - if (fromMagnetUri && !data.addPaused) - torrent->resume(data.addForced); + if (fromMagnetUri && (data.addPaused != TriStateBool::True)) + torrent->resume(data.addForced == TriStateBool::True); logger->addMessage(tr("'%1' resumed. (fast resume)", "'torrent name' was resumed. (fast resume)") .arg(torrent->name())); @@ -3399,7 +3399,7 @@ void Session::createTorrentHandle(const libt::torrent_handle &nativeHandle) if (isAddTrackersEnabled() && !torrent->isPrivate()) torrent->addTrackers(m_additionalTrackerList); - bool addPaused = data.addPaused; + bool addPaused = (data.addPaused == TriStateBool::True); if (data.addPaused == TriStateBool::Undefined) addPaused = isAddTorrentPaused(); @@ -3664,8 +3664,8 @@ namespace torrentData.hasRootFolder = fast.dict_find_int_value("qBt-hasRootFolder"); magnetUri = MagnetUri(QString::fromStdString(fast.dict_find_string_value("qBt-magnetUri"))); - torrentData.addPaused = fast.dict_find_int_value("qBt-paused"); - torrentData.addForced = fast.dict_find_int_value("qBt-forced"); + torrentData.addPaused = TriStateBool(fast.dict_find_int_value("qBt-paused")); + torrentData.addForced = TriStateBool(fast.dict_find_int_value("qBt-forced")); prio = fast.dict_find_int_value("qBt-queuePosition"); diff --git a/src/base/net/downloadmanager.cpp b/src/base/net/downloadmanager.cpp index 02b5929d9..547c30807 100644 --- a/src/base/net/downloadmanager.cpp +++ b/src/base/net/downloadmanager.cpp @@ -144,7 +144,7 @@ DownloadHandler *DownloadManager::downloadUrl(const QString &url, bool saveToFil // Process download request qDebug("url is %s", qPrintable(url)); - const QUrl qurl = QUrl::fromEncoded(url.toUtf8()); + const QUrl qurl = QUrl(url); QNetworkRequest request(qurl); if (userAgent.isEmpty()) diff --git a/src/base/preferences.cpp b/src/base/preferences.cpp index 630dbe4f8..0c55373d6 100644 --- a/src/base/preferences.cpp +++ b/src/base/preferences.cpp @@ -1241,32 +1241,32 @@ void Preferences::setRssHSplitterSizes(const QByteArray &sizes) QStringList Preferences::getRssOpenFolders() const { - return value("Rss/open_folders").toStringList(); + return value("GUI/RSSWidget/OpenedFolders").toStringList(); } void Preferences::setRssOpenFolders(const QStringList &folders) { - setValue("Rss/open_folders", folders); + setValue("GUI/RSSWidget/OpenedFolders", folders); } QByteArray Preferences::getRssSideSplitterState() const { - return value("Rss/qt5/splitter_h").toByteArray(); + return value("GUI/RSSWidget/qt5/splitter_h").toByteArray(); } void Preferences::setRssSideSplitterState(const QByteArray &state) { - setValue("Rss/qt5/splitter_h", state); + setValue("GUI/RSSWidget/qt5/splitter_h", state); } QByteArray Preferences::getRssMainSplitterState() const { - return value("Rss/qt5/splitterMain").toByteArray(); + return value("GUI/RSSWidget/qt5/splitterMain").toByteArray(); } void Preferences::setRssMainSplitterState(const QByteArray &state) { - setValue("Rss/qt5/splitterMain", state); + setValue("GUI/RSSWidget/qt5/splitterMain", state); } QByteArray Preferences::getSearchTabHeaderState() const @@ -1410,64 +1410,14 @@ void Preferences::setTransHeaderState(const QByteArray &state) } //From old RssSettings class -bool Preferences::isRSSEnabled() const +bool Preferences::isRSSWidgetEnabled() const { - return value("Preferences/RSS/RSSEnabled", false).toBool(); + return value("GUI/RSSWidget/Enabled", false).toBool(); } -void Preferences::setRSSEnabled(const bool enabled) +void Preferences::setRSSWidgetVisible(const bool enabled) { - setValue("Preferences/RSS/RSSEnabled", enabled); -} - -uint Preferences::getRSSRefreshInterval() const -{ - return value("Preferences/RSS/RSSRefresh", 30).toUInt(); -} - -void Preferences::setRSSRefreshInterval(const uint &interval) -{ - setValue("Preferences/RSS/RSSRefresh", interval); -} - -int Preferences::getRSSMaxArticlesPerFeed() const -{ - return value("Preferences/RSS/RSSMaxArticlesPerFeed", 50).toInt(); -} - -void Preferences::setRSSMaxArticlesPerFeed(const int &nb) -{ - setValue("Preferences/RSS/RSSMaxArticlesPerFeed", nb); -} - -bool Preferences::isRssDownloadingEnabled() const -{ - return value("Preferences/RSS/RssDownloading", true).toBool(); -} - -void Preferences::setRssDownloadingEnabled(const bool b) -{ - setValue("Preferences/RSS/RssDownloading", b); -} - -QStringList Preferences::getRssFeedsUrls() const -{ - return value("Rss/streamList").toStringList(); -} - -void Preferences::setRssFeedsUrls(const QStringList &rssFeeds) -{ - setValue("Rss/streamList", rssFeeds); -} - -QStringList Preferences::getRssFeedsAliases() const -{ - return value("Rss/streamAlias").toStringList(); -} - -void Preferences::setRssFeedsAliases(const QStringList &rssAliases) -{ - setValue("Rss/streamAlias", rssAliases); + setValue("GUI/RSSWidget/Enabled", enabled); } int Preferences::getToolbarTextPosition() const @@ -1522,24 +1472,6 @@ void Preferences::setSpeedWidgetGraphEnable(int id, const bool enable) void Preferences::upgrade() { - // Move RSS cookies to global storage - QList cookies = getNetworkCookies(); - QVariantMap hostsTable = value("Rss/hosts_cookies").toMap(); - foreach (const QString &key, hostsTable.keys()) { - QVariant value = hostsTable[key]; - QList rawCookies = value.toByteArray().split(':'); - foreach (const QByteArray &rawCookie, rawCookies) { - foreach (QNetworkCookie cookie, QNetworkCookie::parseCookies(rawCookie)) { - cookie.setDomain(key); - cookie.setPath("/"); - cookie.setExpirationDate(QDateTime::currentDateTime().addYears(10)); - cookies << cookie; - } - } - } - - setNetworkCookies(cookies); - QStringList labels = value("TransferListFilters/customLabels").toStringList(); if (!labels.isEmpty()) { QVariantMap categories = value("BitTorrent/Session/Categories").toMap(); @@ -1551,7 +1483,6 @@ void Preferences::upgrade() SettingsStorage::instance()->removeValue("TransferListFilters/customLabels"); } - SettingsStorage::instance()->removeValue("Rss/hosts_cookies"); SettingsStorage::instance()->removeValue("Preferences/Downloads/AppendLabel"); } diff --git a/src/base/preferences.h b/src/base/preferences.h index ec328f264..14eef4a38 100644 --- a/src/base/preferences.h +++ b/src/base/preferences.h @@ -333,18 +333,8 @@ public: void setToolbarTextPosition(const int position); //From old RssSettings class - bool isRSSEnabled() const; - void setRSSEnabled(const bool enabled); - uint getRSSRefreshInterval() const; - void setRSSRefreshInterval(const uint &interval); - int getRSSMaxArticlesPerFeed() const; - void setRSSMaxArticlesPerFeed(const int &nb); - bool isRssDownloadingEnabled() const; - void setRssDownloadingEnabled(const bool b); - QStringList getRssFeedsUrls() const; - void setRssFeedsUrls(const QStringList &rssFeeds); - QStringList getRssFeedsAliases() const; - void setRssFeedsAliases(const QStringList &rssAliases); + bool isRSSWidgetEnabled() const; + void setRSSWidgetVisible(const bool enabled); // Network QList getNetworkCookies() const; diff --git a/src/base/rss/private/rssparser.cpp b/src/base/rss/private/rss_parser.cpp similarity index 84% rename from src/base/rss/private/rssparser.cpp rename to src/base/rss/private/rss_parser.cpp index 8dc1b0849..f88b79336 100644 --- a/src/base/rss/private/rssparser.cpp +++ b/src/base/rss/private/rss_parser.cpp @@ -29,15 +29,16 @@ * Contact : chris@qbittorrent.org */ +#include "rss_parser.h" + #include #include +#include #include #include #include #include -#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(); + +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") { diff --git a/src/base/rss/private/rssparser.h b/src/base/rss/private/rss_parser.h similarity index 80% rename from src/base/rss/private/rssparser.h rename to src/base/rss/private/rss_parser.h index afbf1df03..8d2c705e2 100644 --- a/src/base/rss/private/rssparser.h +++ b/src/base/rss/private/rss_parser.h @@ -29,41 +29,49 @@ * Contact : chris@qbittorrent.org */ -#ifndef RSSPARSER_H -#define RSSPARSER_H +#pragma once +#include #include #include #include class QXmlStreamReader; -namespace Rss +namespace RSS { namespace Private { + struct ParsingResult + { + QString error; + QString lastBuildDate; + QString title; + QList 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) diff --git a/src/base/rss/rss_article.cpp b/src/base/rss/rss_article.cpp new file mode 100644 index 000000000..2da933666 --- /dev/null +++ b/src/base/rss/rss_article.cpp @@ -0,0 +1,178 @@ +/* + * Bittorrent Client using Qt and libtorrent. + * Copyright (C) 2017 Vladimir Golovnev + * Copyright (C) 2010 Christophe Dumez + * Copyright (C) 2010 Arnaud Demaiziere + * + * 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 +#include + +#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; +} diff --git a/src/base/rss/rssarticle.h b/src/base/rss/rss_article.h similarity index 65% rename from src/base/rss/rssarticle.h rename to src/base/rss/rss_article.h index e81a10f6a..d4fc0bbdf 100644 --- a/src/base/rss/rssarticle.h +++ b/src/base/rss/rss_article.h @@ -1,5 +1,6 @@ /* * Bittorrent Client using Qt and libtorrent. + * Copyright (C) 2017 Vladimir Golovnev * Copyright (C) 2010 Christophe Dumez * Copyright (C) 2010 Arnaud Demaiziere * @@ -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 -#include -#include +#include +#include -namespace Rss +namespace RSS { class Feed; - class Article; - typedef QSharedPointer
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 diff --git a/src/base/rss/rss_autodownloader.cpp b/src/base/rss/rss_autodownloader.cpp new file mode 100644 index 000000000..0279df675 --- /dev/null +++ b/src/base/rss/rss_autodownloader.cpp @@ -0,0 +1,390 @@ +/* + * Bittorrent Client using Qt and libtorrent. + * Copyright (C) 2017 Vladimir Golovnev + * + * 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 +#include +#include +#include +#include +#include +#include +#include + +#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::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 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 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 &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(); +} diff --git a/src/base/rss/rss_autodownloader.h b/src/base/rss/rss_autodownloader.h new file mode 100644 index 000000000..50919f978 --- /dev/null +++ b/src/base/rss/rss_autodownloader.h @@ -0,0 +1,114 @@ +/* + * Bittorrent Client using Qt and libtorrent. + * Copyright (C) 2017 Vladimir Golovnev + * + * 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 +#include +#include +#include +#include +#include + +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 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 &job); + void load(); + void loadRules(const QByteArray &data); + void loadRulesLegacy(); + void store(); + void storeDeferred(); + + static QPointer m_instance; + + bool m_processingEnabled; + QTimer *m_processingTimer; + QThread *m_ioThread; + AsyncFileStorage *m_fileStorage; + QHash m_rules; + QList> m_processingQueue; + QHash> m_waitingJobs; + bool m_dirty = false; + QBasicTimer m_savingTimer; + }; +} diff --git a/src/base/rss/rss_autodownloadrule.cpp b/src/base/rss/rss_autodownloadrule.cpp new file mode 100644 index 000000000..e32bfa584 --- /dev/null +++ b/src/base/rss/rss_autodownloadrule.cpp @@ -0,0 +1,538 @@ +/* + * Bittorrent Client using Qt and libtorrent. + * Copyright (C) 2017 Vladimir Golovnev + * 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. + */ + +#include "rss_autodownloadrule.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#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(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 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(); +} diff --git a/src/base/rss/rssdownloadrule.h b/src/base/rss/rss_autodownloadrule.h similarity index 59% rename from src/base/rss/rssdownloadrule.h rename to src/base/rss/rss_autodownloadrule.h index 280dfdb96..d02d67b89 100644 --- a/src/base/rss/rssdownloadrule.h +++ b/src/base/rss/rss_autodownloadrule.h @@ -1,6 +1,7 @@ /* * Bittorrent Client using Qt and libtorrent. - * Copyright (C) 2010 Christophe Dumez + * Copyright (C) 2017 Vladimir Golovnev + * 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 @@ -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 -#include -#include -#include +#include +#include -template class QHash; +class QJsonObject; class QRegularExpression; +class TriStateBool; -namespace Rss +namespace RSS { - class Feed; - typedef QSharedPointer FeedPtr; + struct AutoDownloadRuleData; - class DownloadRule; - typedef QSharedPointer 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 *m_cachedRegexes; + QSharedDataPointer m_dataPtr; }; } - -#endif // RSSDOWNLOADRULE_H diff --git a/src/base/rss/rss_feed.cpp b/src/base/rss/rss_feed.cpp new file mode 100644 index 000000000..77fc8ac93 --- /dev/null +++ b/src/base/rss/rss_feed.cpp @@ -0,0 +1,437 @@ +/* + * Bittorrent Client using Qt and libtorrent. + * Copyright (C) 2015, 2017 Vladimir Golovnev + * Copyright (C) 2010 Christophe Dumez + * Copyright (C) 2010 Arnaud Demaiziere + * + * 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 +#include +#include +#include +#include +#include +#include +#include + +#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
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(&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(&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(); +} diff --git a/src/base/rss/rss_feed.h b/src/base/rss/rss_feed.h new file mode 100644 index 000000000..248095d9d --- /dev/null +++ b/src/base/rss/rss_feed.h @@ -0,0 +1,121 @@ +/* + * Bittorrent Client using Qt and libtorrent. + * Copyright (C) 2015, 2017 Vladimir Golovnev + * Copyright (C) 2010 Christophe Dumez + * Copyright (C) 2010 Arnaud Demaiziere + * + * 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 +#include +#include + +#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
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 m_articles; + QList
m_articlesByDate; + int m_unreadCount = 0; + QString m_iconPath; + QString m_dataFileName; + QBasicTimer m_savingTimer; + bool m_dirty = false; + }; +} diff --git a/src/base/rss/rss_folder.cpp b/src/base/rss/rss_folder.cpp new file mode 100644 index 000000000..3d5c0db7c --- /dev/null +++ b/src/base/rss/rss_folder.cpp @@ -0,0 +1,140 @@ +/* + * Bittorrent Client using Qt and libtorrent. + * Copyright (C) 2017 Vladimir Golovnev + * Copyright (C) 2010 Christophe Dumez + * Copyright (C) 2010 Arnaud Demaiziere + * + * 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 +#include + +#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
Folder::articles() const +{ + QList
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 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); +} diff --git a/src/base/rss/rss_folder.h b/src/base/rss/rss_folder.h new file mode 100644 index 000000000..3e2f5469e --- /dev/null +++ b/src/base/rss/rss_folder.h @@ -0,0 +1,71 @@ +/* + * Bittorrent Client using Qt and libtorrent. + * Copyright (C) 2017 Vladimir Golovnev + * Copyright (C) 2010 Christophe Dumez + * Copyright (C) 2010 Arnaud Demaiziere + * + * 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 +#include "rss_item.h" + +namespace RSS +{ + class Session; + + class Folder final: public Item + { + Q_OBJECT + Q_DISABLE_COPY(Folder) + + friend class Session; + + explicit Folder(const QString &path = ""); + ~Folder() override; + + public: + QList
articles() const override; + int unreadCount() const override; + void markAsRead() override; + void refresh() override; + + QList 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 m_items; + }; +} diff --git a/src/base/rss/rss_item.cpp b/src/base/rss/rss_item.cpp new file mode 100644 index 000000000..7c0917438 --- /dev/null +++ b/src/base/rss/rss_item.cpp @@ -0,0 +1,115 @@ +/* + * Bittorrent Client using Qt and libtorrent. + * Copyright (C) 2017 Vladimir Golovnev + * Copyright (C) 2010 Christophe Dumez + * Copyright (C) 2010 Arnaud Demaiziere + * + * 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 +#include +#include + +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); +} diff --git a/src/base/rss/rssfile.h b/src/base/rss/rss_item.h similarity index 55% rename from src/base/rss/rssfile.h rename to src/base/rss/rss_item.h index 285da93f6..9ba1db692 100644 --- a/src/base/rss/rssfile.h +++ b/src/base/rss/rss_item.h @@ -1,5 +1,6 @@ /* * Bittorrent Client using Qt and libtorrent. + * Copyright (C) 2017 Vladimir Golovnev * Copyright (C) 2010 Christophe Dumez * Copyright (C) 2010 Arnaud Demaiziere * @@ -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 -#include -#include +#include -namespace Rss +namespace RSS { - class Folder; - class File; class Article; + class Folder; + class Session; - typedef QSharedPointer FilePtr; - typedef QSharedPointer
ArticlePtr; - typedef QList ArticleList; - typedef QList 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
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 diff --git a/src/base/rss/rss_session.cpp b/src/base/rss/rss_session.cpp new file mode 100644 index 000000000..ced8e91af --- /dev/null +++ b/src/base/rss/rss_session.cpp @@ -0,0 +1,485 @@ +/* + * Bittorrent Client using Qt and libtorrent. + * Copyright (C) 2017 Vladimir Golovnev + * Copyright (C) 2010 Christophe Dumez + * Copyright (C) 2010 Arnaud Demaiziere + * + * 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 +#include +#include +#include +#include +#include +#include +#include +#include + +#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::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(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(m_itemsByPath.value(Item::parentPath(item->path()))); + folder->removeItem(item); + delete item; + store(); + return true; +} + +QList 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(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(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(m_itemsByPath.value("")); +} + +QList 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(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(); +} diff --git a/src/base/rss/rss_session.h b/src/base/rss/rss_session.h new file mode 100644 index 000000000..648e167bf --- /dev/null +++ b/src/base/rss/rss_session.h @@ -0,0 +1,154 @@ +/* + * Bittorrent Client using Qt and libtorrent. + * Copyright (C) 2017 Vladimir Golovnev + * Copyright (C) 2010 Christophe Dumez + * Copyright (C) 2010 Arnaud Demaiziere + * + * 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 +#include +#include +#include +#include + +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 items() const; + Item *itemByPath(const QString &path) const; + QList 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 m_instance; + + bool m_processingEnabled; + QThread *m_workingThread; + AsyncFileStorage *m_confFileStorage; + AsyncFileStorage *m_dataFileStorage; + QTimer m_refreshTimer; + uint m_refreshInterval; + int m_maxArticlesPerFeed; + QHash m_itemsByPath; + QHash m_feedsByURL; + }; +} diff --git a/src/base/rss/rssarticle.cpp b/src/base/rss/rssarticle.cpp deleted file mode 100644 index 1357d09c9..000000000 --- a/src/base/rss/rssarticle.cpp +++ /dev/null @@ -1,143 +0,0 @@ -/* - * Bittorrent Client using Qt and libtorrent. - * Copyright (C) 2010 Christophe Dumez - * Copyright (C) 2010 Arnaud Demaiziere - * - * 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 -#include -#include - -#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(); -} diff --git a/src/base/rss/rssdownloadrule.cpp b/src/base/rss/rssdownloadrule.cpp deleted file mode 100644 index 3dee5d4ba..000000000 --- a/src/base/rss/rssdownloadrule.cpp +++ /dev/null @@ -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 -#include -#include -#include -#include -#include -#include - -#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) -{ -} - -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; -} diff --git a/src/base/rss/rssdownloadrulelist.cpp b/src/base/rss/rssdownloadrulelist.cpp deleted file mode 100644 index 865ce4f02..000000000 --- a/src/base/rss/rssdownloadrulelist.cpp +++ /dev/null @@ -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 -#include -#include - -#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; - } -} diff --git a/src/base/rss/rssdownloadrulelist.h b/src/base/rss/rssdownloadrulelist.h deleted file mode 100644 index 2dc8c36f6..000000000 --- a/src/base/rss/rssdownloadrulelist.h +++ /dev/null @@ -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 -#include -#include - -#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 m_rules; - QHash m_feedRules; - }; -} - -#endif // RSSDOWNLOADFILTERLIST_H diff --git a/src/base/rss/rssfeed.cpp b/src/base/rss/rssfeed.cpp deleted file mode 100644 index 30038be38..000000000 --- a/src/base/rss/rssfeed.cpp +++ /dev/null @@ -1,458 +0,0 @@ -/* - * Bittorrent Client using Qt and libtorrent. - * Copyright (C) 2015 Vladimir Golovnev - * Copyright (C) 2010 Christophe Dumez - * Copyright (C) 2010 Arnaud Demaiziere - * - * 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 - -#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 allOldItems = qBTRSSFeeds->value("old_items", QHash()).toHash(); - allOldItems[m_url] = oldItems; - qBTRSSFeeds->setValue("old_items", allOldItems); -} - -void Feed::loadItemsFromDisk() -{ - SettingsPtr qBTRSSFeeds = Profile::instance().applicationSettings(QLatin1String("qBittorrent-rss-feeds")); - QHash allOldItems = qBTRSSFeeds->value("old_items", QHash()).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); -} diff --git a/src/base/rss/rssfeed.h b/src/base/rss/rssfeed.h deleted file mode 100644 index 7fcbcb361..000000000 --- a/src/base/rss/rssfeed.h +++ /dev/null @@ -1,125 +0,0 @@ -/* - * Bittorrent Client using Qt and libtorrent. - * Copyright (C) 2015 Vladimir Golovnev - * Copyright (C) 2010 Christophe Dumez - * Copyright (C) 2010 Arnaud Demaiziere - * - * 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 -#include -#include -#include -#include - -#include "rssfile.h" - -namespace Rss -{ - class Folder; - class Feed; - class Manager; - class DownloadRuleList; - - typedef QHash ArticleHash; - typedef QSharedPointer FeedPtr; - typedef QList 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 diff --git a/src/base/rss/rssfolder.cpp b/src/base/rss/rssfolder.cpp deleted file mode 100644 index e17afecb9..000000000 --- a/src/base/rss/rssfolder.cpp +++ /dev/null @@ -1,253 +0,0 @@ -/* - * Bittorrent Client using Qt and libtorrent. - * Copyright (C) 2010 Christophe Dumez - * Copyright (C) 2010 Arnaud Demaiziere - * - * 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 - -#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(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(it.value())) - streams << feed; - else if (FolderPtr folder = qSharedPointerDynamicCast(it.value())) - streams << folder->getAllFeeds(); - } - return streams; -} - -QHash Folder::getAllFeedsAsHash() const -{ - QHash ret; - - FileHash::ConstIterator it = m_children.begin(); - FileHash::ConstIterator itend = m_children.end(); - for ( ; it != itend; ++it) { - if (FeedPtr feed = qSharedPointerDynamicCast(it.value())) { - qDebug() << Q_FUNC_INFO << feed->url(); - ret[feed->url()] = feed; - } - else if (FolderPtr folder = qSharedPointerDynamicCast(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(); -} diff --git a/src/base/rss/rssfolder.h b/src/base/rss/rssfolder.h deleted file mode 100644 index 7adefb423..000000000 --- a/src/base/rss/rssfolder.h +++ /dev/null @@ -1,86 +0,0 @@ -/* - * Bittorrent Client using Qt and libtorrent. - * Copyright (C) 2010 Christophe Dumez - * Copyright (C) 2010 Arnaud Demaiziere - * - * 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 -#include - -#include "rssfile.h" - -namespace Rss -{ - class Folder; - class Feed; - class Manager; - - typedef QHash FileHash; - typedef QSharedPointer FeedPtr; - typedef QSharedPointer FolderPtr; - typedef QList 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 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 diff --git a/src/base/rss/rssmanager.cpp b/src/base/rss/rssmanager.cpp deleted file mode 100644 index cce0cd828..000000000 --- a/src/base/rss/rssmanager.cpp +++ /dev/null @@ -1,218 +0,0 @@ -/* - * Bittorrent Client using Qt and libtorrent. - * Copyright (C) 2010 Christophe Dumez - * Copyright (C) 2010 Arnaud Demaiziere - * - * 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 - -#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(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 urlArticle(m_deferredDownloads.takeFirst()); - FeedList streams = m_rootFolder->getAllFeeds(); - foreach (const FeedPtr &stream, streams) { - if (stream->url() == urlArticle.first) { - stream->deferredDownloadArticleTorrentIfMatching(urlArticle.second); - break; - } - } -} diff --git a/src/base/rss/rssmanager.h b/src/base/rss/rssmanager.h deleted file mode 100644 index 37254aeb2..000000000 --- a/src/base/rss/rssmanager.h +++ /dev/null @@ -1,100 +0,0 @@ -/* - * Bittorrent Client using Qt and libtorrent. - * Copyright (C) 2010 Christophe Dumez - * Copyright (C) 2010 Arnaud Demaiziere - * - * 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 -#include -#include -#include -#include -#include - -namespace Rss -{ - class Article; - class DownloadRuleList; - class File; - class Folder; - class Feed; - class Manager; - - typedef QSharedPointer
ArticlePtr; - typedef QSharedPointer FilePtr; - typedef QSharedPointer FolderPtr; - typedef QSharedPointer FeedPtr; - - typedef QSharedPointer 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> m_deferredDownloads; - }; -} - -#endif // RSSMANAGER_H diff --git a/src/base/tristatebool.cpp b/src/base/tristatebool.cpp index 1e5a5eed9..1de7efc7f 100644 --- a/src/base/tristatebool.cpp +++ b/src/base/tristatebool.cpp @@ -28,33 +28,31 @@ #include "tristatebool.h" -TriStateBool::TriStateBool() - : m_value(Undefined) -{ -} +const TriStateBool TriStateBool::Undefined(-1); +const TriStateBool TriStateBool::False(0); +const TriStateBool TriStateBool::True(1); -TriStateBool::TriStateBool(bool b) -{ - m_value = (b ? True : False); -} - -TriStateBool::TriStateBool(TriStateBool::ValueType value) - : m_value(Undefined) -{ - switch (value) { - case Undefined: - case True: - case False: - m_value = value; - } -} - -TriStateBool::operator bool() const -{ - return (m_value == True); -} - -TriStateBool::operator ValueType() const +TriStateBool::operator int() const { return m_value; } + +TriStateBool::TriStateBool(int value) +{ + if (value < 0) + m_value = -1; + else if (value > 0) + m_value = 1; + else + m_value = 0; +} + +bool TriStateBool::operator==(const TriStateBool &other) const +{ + return (m_value == other.m_value); +} + +bool TriStateBool::operator!=(const TriStateBool &other) const +{ + return !operator==(other); +} diff --git a/src/base/tristatebool.h b/src/base/tristatebool.h index b57f3873c..a75d1d09c 100644 --- a/src/base/tristatebool.h +++ b/src/base/tristatebool.h @@ -32,22 +32,22 @@ class TriStateBool { public: - enum ValueType - { - Undefined = -1, - False = 0, - True = 1 - }; + static const TriStateBool Undefined; + static const TriStateBool False; + static const TriStateBool True; - TriStateBool(); - TriStateBool(bool b); - TriStateBool(ValueType value); + TriStateBool() = default; + TriStateBool(const TriStateBool &other) = default; - operator ValueType() const; - operator bool() const; + explicit TriStateBool(int value); + explicit operator int() const; + + TriStateBool &operator=(const TriStateBool &other) = default; + bool operator==(const TriStateBool &other) const; + bool operator!=(const TriStateBool &other) const; private: - ValueType m_value; + int m_value = -1; // Undefined by default }; #endif // TRISTATEBOOL_H diff --git a/src/base/utils/fs.cpp b/src/base/utils/fs.cpp index eca53876d..a47ba0fe1 100644 --- a/src/base/utils/fs.cpp +++ b/src/base/utils/fs.cpp @@ -214,12 +214,12 @@ bool Utils::Fs::sameFiles(const QString& path1, const QString& path2) return same; } -QString Utils::Fs::toValidFileSystemName(const QString &name, bool allowSeparators) +QString Utils::Fs::toValidFileSystemName(const QString &name, bool allowSeparators, const QString &pad) { QRegExp regex(allowSeparators ? "[:?\"*<>|]+" : "[\\\\/:?\"*<>|]+"); QString validName = name.trimmed(); - validName.replace(regex, " "); + validName.replace(regex, pad); qDebug() << "toValidFileSystemName:" << name << "=>" << validName; return validName; diff --git a/src/base/utils/fs.h b/src/base/utils/fs.h index 56320c1dc..d3305afb8 100644 --- a/src/base/utils/fs.h +++ b/src/base/utils/fs.h @@ -48,7 +48,8 @@ namespace Utils QString folderName(const QString& file_path); qint64 computePathSize(const QString& path); bool sameFiles(const QString& path1, const QString& path2); - QString toValidFileSystemName(const QString &name, bool allowSeparators = false); + QString toValidFileSystemName(const QString &name, bool allowSeparators = false + , const QString &pad = QLatin1String(" ")); bool isValidFileSystemName(const QString& name, bool allowSeparators = false); qulonglong freeDiskSpaceOnPath(const QString &path); QString branchPath(const QString& file_path, QString* removed = 0); diff --git a/src/base/utils/misc.cpp b/src/base/utils/misc.cpp index 981892f0c..800b22c30 100644 --- a/src/base/utils/misc.cpp +++ b/src/base/utils/misc.cpp @@ -35,6 +35,7 @@ #include #include #include +#include #include #include #include @@ -503,9 +504,10 @@ QList Utils::Misc::boolListfromStringList(const QStringList &l) bool Utils::Misc::isUrl(const QString &s) { - const QString scheme = QUrl(s).scheme(); - QRegExp is_url("http[s]?|ftp", Qt::CaseInsensitive); - return is_url.exactMatch(scheme); + static const QRegularExpression reURLScheme( + "http[s]?|ftp", QRegularExpression::CaseInsensitiveOption); + + return reURLScheme.match(QUrl(s).scheme()).hasMatch(); } QString Utils::Misc::parseHtmlLinks(const QString &raw_text) diff --git a/src/gui/addnewtorrentdialog.cpp b/src/gui/addnewtorrentdialog.cpp index c0b540f44..7e7014c3a 100644 --- a/src/gui/addnewtorrentdialog.cpp +++ b/src/gui/addnewtorrentdialog.cpp @@ -641,8 +641,8 @@ void AddNewTorrentDialog::accept() if (m_contentModel) params.filePriorities = m_contentModel->model()->getFilePriorities(); - params.addPaused = !ui->startTorrentCheckBox->isChecked(); - params.createSubfolder = ui->createSubfolderCheckBox->isChecked(); + params.addPaused = TriStateBool(!ui->startTorrentCheckBox->isChecked()); + params.createSubfolder = !ui->startTorrentCheckBox->isChecked(); QString savePath = ui->savePathComboBox->itemData(ui->savePathComboBox->currentIndex()).toString(); if (ui->comboTTM->currentIndex() != 1) { // 0 is Manual mode and 1 is Automatic mode. Handle all non 1 values as manual mode. diff --git a/src/gui/gui.pri b/src/gui/gui.pri index b41408df6..5cb464ed7 100644 --- a/src/gui/gui.pri +++ b/src/gui/gui.pri @@ -2,7 +2,6 @@ INCLUDEPATH += $$PWD include(lineedit/lineedit.pri) include(properties/properties.pri) -include(rss/rss.pri) include(powermanagement/powermanagement.pri) unix:!macx:dbus: include(qtnotify/qtnotify.pri) @@ -52,7 +51,12 @@ HEADERS += \ $$PWD/cookiesdialog.h \ $$PWD/categoryfiltermodel.h \ $$PWD/categoryfilterwidget.h \ - $$PWD/banlistoptions.h + $$PWD/banlistoptions.h \ + $$PWD/rss/rsswidget.h \ + $$PWD/rss/articlelistwidget.h \ + $$PWD/rss/feedlistwidget.h \ + $$PWD/rss/automatedrssdownloader.h \ + $$PWD/rss/htmlbrowser.h SOURCES += \ $$PWD/mainwindow.cpp \ @@ -95,7 +99,12 @@ SOURCES += \ $$PWD/cookiesdialog.cpp \ $$PWD/categoryfiltermodel.cpp \ $$PWD/categoryfilterwidget.cpp \ - $$PWD/banlistoptions.cpp + $$PWD/banlistoptions.cpp \ + $$PWD/rss/rsswidget.cpp \ + $$PWD/rss/articlelistwidget.cpp \ + $$PWD/rss/feedlistwidget.cpp \ + $$PWD/rss/automatedrssdownloader.cpp \ + $$PWD/rss/htmlbrowser.cpp win32|macx { HEADERS += $$PWD/programupdater.h @@ -123,6 +132,8 @@ FORMS += \ $$PWD/search/pluginsourcedlg.ui \ $$PWD/search/searchtab.ui \ $$PWD/cookiesdialog.ui \ - $$PWD/banlistoptions.ui + $$PWD/banlistoptions.ui \ + $$PWD/rss/rsswidget.ui \ + $$PWD/rss/automatedrssdownloader.ui RESOURCES += $$PWD/about.qrc diff --git a/src/gui/mainwindow.cpp b/src/gui/mainwindow.cpp index cdafde5c6..a3bf7159a 100644 --- a/src/gui/mainwindow.cpp +++ b/src/gui/mainwindow.cpp @@ -64,6 +64,8 @@ #include "base/bittorrent/session.h" #include "base/bittorrent/sessionstatus.h" #include "base/bittorrent/torrenthandle.h" +#include "base/rss/rss_folder.h" +#include "base/rss/rss_session.h" #include "application.h" #if defined(Q_OS_WIN) || defined(Q_OS_MAC) @@ -86,7 +88,7 @@ #include "transferlistfilterswidget.h" #include "propertieswidget.h" #include "statusbar.h" -#include "rss_imp.h" +#include "rss/rsswidget.h" #include "about_imp.h" #include "optionsdlg.h" #include "trackerlogin.h" @@ -296,7 +298,7 @@ MainWindow::MainWindow(QWidget *parent) // View settings m_ui->actionTopToolBar->setChecked(pref->isToolbarDisplayed()); m_ui->actionSpeedInTitleBar->setChecked(pref->speedInTitleBar()); - m_ui->actionRSSReader->setChecked(pref->isRSSEnabled()); + m_ui->actionRSSReader->setChecked(pref->isRSSWidgetEnabled()); m_ui->actionSearchWidget->setChecked(pref->isSearchEnabled()); m_ui->actionExecutionLogs->setChecked(isExecutionLogEnabled()); @@ -600,14 +602,19 @@ void MainWindow::on_actionLock_triggered() hide(); } +void MainWindow::handleRSSUnreadCountUpdated(int count) +{ + m_tabs->setTabText(m_tabs->indexOf(m_rssWidget), tr("RSS (%1)").arg(count)); +} + void MainWindow::displayRSSTab(bool enable) { if (enable) { // RSS tab if (!m_rssWidget) { - m_rssWidget = new RSSImp(m_tabs); - connect(m_rssWidget, SIGNAL(updateRSSCount(int)), this, SLOT(updateRSSTabLabel(int))); - int indexTab = m_tabs->addTab(m_rssWidget, tr("RSS (%1)").arg(0)); + m_rssWidget = new RSSWidget(m_tabs); + connect(m_rssWidget.data(), &RSSWidget::unreadCountUpdated, this, &MainWindow::handleRSSUnreadCountUpdated); + int indexTab = m_tabs->addTab(m_rssWidget, tr("RSS (%1)").arg(RSS::Session::instance()->rootFolder()->unreadCount())); m_tabs->setTabIcon(indexTab, GuiIconProvider::instance()->getIcon("application-rss+xml")); } } @@ -616,11 +623,6 @@ void MainWindow::displayRSSTab(bool enable) } } -void MainWindow::updateRSSTabLabel(int count) -{ - m_tabs->setTabText(m_tabs->indexOf(m_rssWidget), tr("RSS (%1)").arg(count)); -} - void MainWindow::displaySearchTab(bool enable) { Preferences::instance()->setSearchEnabled(enable); @@ -685,6 +687,10 @@ void MainWindow::cleanup() { writeSettings(); + // delete RSSWidget explicitly to avoid crash in + // handleRSSUnreadCountUpdated() at application shutdown + delete m_rssWidget; + delete m_executableWatcher; if (m_systrayCreator) m_systrayCreator->stop(); @@ -1502,7 +1508,7 @@ void MainWindow::on_actionSpeedInTitleBar_triggered() void MainWindow::on_actionRSSReader_triggered() { - Preferences::instance()->setRSSEnabled(m_ui->actionRSSReader->isChecked()); + Preferences::instance()->setRSSWidgetVisible(m_ui->actionRSSReader->isChecked()); displayRSSTab(m_ui->actionRSSReader->isChecked()); } diff --git a/src/gui/mainwindow.h b/src/gui/mainwindow.h index 04de52d06..2c2ab35d8 100644 --- a/src/gui/mainwindow.h +++ b/src/gui/mainwindow.h @@ -44,7 +44,7 @@ class QTimer; class downloadFromURL; class SearchWidget; -class RSSImp; +class RSSWidget; class about; class OptionsDialog; class TransferListWidget; @@ -138,7 +138,6 @@ private slots: #if defined(Q_OS_WIN) || defined(Q_OS_MAC) void handleUpdateCheckFinished(bool updateAvailable, QString newVersion, bool invokedByUser); #endif - void updateRSSTabLabel(int count); #ifdef Q_OS_WIN void pythonDownloadSuccess(const QString &url, const QString &filePath); @@ -151,6 +150,7 @@ private slots: void downloadFromURLList(const QStringList &urlList); void updateAltSpeedsBtn(bool alternative); void updateNbTorrents(); + void handleRSSUnreadCountUpdated(int count); void on_actionSearchWidget_triggered(); void on_actionRSSReader_triggered(); @@ -211,7 +211,7 @@ private: QList> m_unauthenticatedTrackers; // Still needed? // GUI related bool m_posInitialized; - QTabWidget *m_tabs; + QPointer m_tabs; StatusBar *m_statusBar; QPointer m_options; QPointer m_aboutDlg; @@ -235,7 +235,7 @@ private: QAction *m_prioSeparatorMenu; QSplitter *m_splitter; QPointer m_searchWidget; - QPointer m_rssWidget; + QPointer m_rssWidget; QPointer m_executionLog; // Power Management PowerManagement *m_pwr; diff --git a/src/gui/optionsdlg.cpp b/src/gui/optionsdlg.cpp index 03abb20be..d184d4736 100644 --- a/src/gui/optionsdlg.cpp +++ b/src/gui/optionsdlg.cpp @@ -54,6 +54,8 @@ #include "base/net/portforwarder.h" #include "base/net/proxyconfigurationmanager.h" #include "base/preferences.h" +#include "base/rss/rss_autodownloader.h" +#include "base/rss/rss_session.h" #include "base/scanfoldersmodel.h" #include "base/torrentfileguard.h" #include "base/unicodestrings.h" @@ -61,6 +63,7 @@ #include "base/utils/random.h" #include "addnewtorrentdialog.h" #include "advancedsettings.h" +#include "rss/automatedrssdownloader.h" #include "banlistoptions.h" #include "guiiconprovider.h" #include "scanfoldersdelegate.h" @@ -84,6 +87,7 @@ OptionsDialog::OptionsDialog(QWidget *parent) m_ui->tabSelection->item(TAB_CONNECTION)->setIcon(GuiIconProvider::instance()->getIcon("network-wired")); m_ui->tabSelection->item(TAB_DOWNLOADS)->setIcon(GuiIconProvider::instance()->getIcon("folder-download")); m_ui->tabSelection->item(TAB_SPEED)->setIcon(GuiIconProvider::instance()->getIcon("speedometer", "chronometer")); + m_ui->tabSelection->item(TAB_RSS)->setIcon(GuiIconProvider::instance()->getIcon("rss-config", "application-rss+xml")); #ifndef DISABLE_WEBUI m_ui->tabSelection->item(TAB_WEBUI)->setIcon(GuiIconProvider::instance()->getIcon("network-server")); #else @@ -335,6 +339,15 @@ OptionsDialog::OptionsDialog(QWidget *parent) connect(m_ui->DNSUsernameTxt, SIGNAL(textChanged(QString)), SLOT(enableApplyButton())); connect(m_ui->DNSPasswordTxt, SIGNAL(textChanged(QString)), SLOT(enableApplyButton())); #endif + + // RSS tab + connect(m_ui->checkRSSEnable, &QCheckBox::toggled, this, &OptionsDialog::enableApplyButton); + connect(m_ui->checkRSSAutoDownloaderEnable, &QCheckBox::toggled, this, &OptionsDialog::enableApplyButton); + connect(m_ui->spinRSSRefreshInterval, static_cast(&QSpinBox::valueChanged) + , this, &OptionsDialog::enableApplyButton); + connect(m_ui->spinRSSMaxArticlesPerFeed, static_cast(&QSpinBox::valueChanged), this, &OptionsDialog::enableApplyButton); + connect(m_ui->btnEditRules, &QPushButton::clicked, [this]() { AutomatedRssDownloader(this).exec(); }); + // Disable apply Button applyButton->setEnabled(false); // Tab selection mechanism @@ -500,6 +513,11 @@ void OptionsDialog::saveOptions() app->setFileLoggerEnabled(m_ui->checkFileLog->isChecked()); // End General preferences + RSS::Session::instance()->setRefreshInterval(m_ui->spinRSSRefreshInterval->value()); + RSS::Session::instance()->setMaxArticlesPerFeed(m_ui->spinRSSMaxArticlesPerFeed->value()); + RSS::Session::instance()->setProcessingEnabled(m_ui->checkRSSEnable->isChecked()); + RSS::AutoDownloader::instance()->setProcessingEnabled(m_ui->checkRSSAutoDownloaderEnable->isChecked()); + auto session = BitTorrent::Session::instance(); // Downloads preferences @@ -712,6 +730,11 @@ void OptionsDialog::loadOptions() m_ui->comboFileLogAgeType->setCurrentIndex(app->fileLoggerAgeType()); // End General preferences + m_ui->checkRSSEnable->setChecked(RSS::Session::instance()->isProcessingEnabled()); + m_ui->checkRSSAutoDownloaderEnable->setChecked(RSS::AutoDownloader::instance()->isProcessingEnabled()); + m_ui->spinRSSRefreshInterval->setValue(RSS::Session::instance()->refreshInterval()); + m_ui->spinRSSMaxArticlesPerFeed->setValue(RSS::Session::instance()->maxArticlesPerFeed()); + auto session = BitTorrent::Session::instance(); // Downloads preferences diff --git a/src/gui/optionsdlg.h b/src/gui/optionsdlg.h index 744aa9896..7cf995496 100644 --- a/src/gui/optionsdlg.h +++ b/src/gui/optionsdlg.h @@ -68,6 +68,7 @@ private: TAB_CONNECTION, TAB_SPEED, TAB_BITTORRENT, + TAB_RSS, TAB_WEBUI, TAB_ADVANCED }; diff --git a/src/gui/optionsdlg.ui b/src/gui/optionsdlg.ui index 82854e831..80801c0e6 100644 --- a/src/gui/optionsdlg.ui +++ b/src/gui/optionsdlg.ui @@ -45,7 +45,7 @@ QListView::IconMode - 0 + -1 @@ -72,6 +72,11 @@ BitTorrent + + + RSS + + Web UI @@ -117,8 +122,8 @@ 0 0 - 497 - 880 + 470 + 783 @@ -673,8 +678,8 @@ 0 0 - 586 - 1118 + 470 + 994 @@ -1358,8 +1363,8 @@ 0 0 - 457 - 672 + 470 + 619 @@ -1838,8 +1843,8 @@ 0 0 - 388 - 452 + 487 + 542 @@ -2225,8 +2230,8 @@ 0 0 - 574 - 534 + 487 + 542 @@ -2598,6 +2603,125 @@ + + + + + + RSS Reader + + + + + + Enable fetching RSS feeds + + + + + + + + + Feeds refresh interval: + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + + + + Maximum number of articles per feed: + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + + + + min + + + 1 + + + 999999 + + + 5 + + + + + + + 9999 + + + 100 + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + + + + + RSS Torrent Auto Downloader + + + + + + Enable auto downloading of RSS torrents + + + + + + + Edit auto downloading rules... + + + + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + @@ -2622,8 +2746,8 @@ 0 0 - 438 - 543 + 487 + 542 diff --git a/src/gui/rss/articlelistwidget.cpp b/src/gui/rss/articlelistwidget.cpp new file mode 100644 index 000000000..f102f322a --- /dev/null +++ b/src/gui/rss/articlelistwidget.cpp @@ -0,0 +1,117 @@ +/* + * Bittorrent Client using Qt and libtorrent. + * Copyright (C) 2017 Vladimir Golovnev + * + * 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 "articlelistwidget.h" + +#include + +#include "base/rss/rss_article.h" +#include "base/rss/rss_item.h" + +ArticleListWidget::ArticleListWidget(QWidget *parent) + : QListWidget(parent) +{ + setContextMenuPolicy(Qt::CustomContextMenu); + setSelectionMode(QAbstractItemView::ExtendedSelection); +} + +RSS::Article *ArticleListWidget::getRSSArticle(QListWidgetItem *item) const +{ + Q_ASSERT(item); + return reinterpret_cast(item->data(Qt::UserRole).value()); +} + +QListWidgetItem *ArticleListWidget::mapRSSArticle(RSS::Article *rssArticle) const +{ + return m_rssArticleToListItemMapping.value(rssArticle); +} + +void ArticleListWidget::setRSSItem(RSS::Item *rssItem, bool unreadOnly) +{ + // Clear the list first + clear(); + if (m_rssItem) + m_rssItem->disconnect(this); + + m_unreadOnly = unreadOnly; + m_rssItem = rssItem; + if (!m_rssItem) return; + + m_rssItem = rssItem; + connect(m_rssItem, &RSS::Item::newArticle, this, &ArticleListWidget::handleArticleAdded); + connect(m_rssItem, &RSS::Item::articleRead, this, &ArticleListWidget::handleArticleRead); + connect(m_rssItem, &RSS::Item::articleAboutToBeRemoved, this, &ArticleListWidget::handleArticleAboutToBeRemoved); + + foreach (auto article, rssItem->articles()) { + if (!(m_unreadOnly && article->isRead())) + addItem(createItem(article)); + } +} + +void ArticleListWidget::handleArticleAdded(RSS::Article *rssArticle) +{ + if (!(m_unreadOnly && rssArticle->isRead())) + addItem(createItem(rssArticle)); +} + +void ArticleListWidget::handleArticleRead(RSS::Article *rssArticle) +{ + if (m_unreadOnly) { + delete m_rssArticleToListItemMapping.take(rssArticle); + } + else { + auto item = mapRSSArticle(rssArticle); + item->setData(Qt::ForegroundRole, QColor("grey")); + item->setData(Qt::DecorationRole, QIcon(":/icons/sphere.png")); + } +} + +void ArticleListWidget::handleArticleAboutToBeRemoved(RSS::Article *rssArticle) +{ + delete m_rssArticleToListItemMapping.take(rssArticle); +} + +QListWidgetItem *ArticleListWidget::createItem(RSS::Article *article) +{ + Q_ASSERT(article); + QListWidgetItem *item = new QListWidgetItem; + + item->setData(Qt::DisplayRole, article->title()); + item->setData(Qt::UserRole, reinterpret_cast(article)); + if (article->isRead()) { + item->setData(Qt::ForegroundRole, QColor("grey")); + item->setData(Qt::DecorationRole, QIcon(":/icons/sphere.png")); + } + else { + item->setData(Qt::ForegroundRole, QColor("blue")); + item->setData(Qt::DecorationRole, QIcon(":/icons/sphere2.png")); + } + + m_rssArticleToListItemMapping.insert(article, item); + return item; +} diff --git a/src/gui/rss/rsssettingsdlg.h b/src/gui/rss/articlelistwidget.h similarity index 58% rename from src/gui/rss/rsssettingsdlg.h rename to src/gui/rss/articlelistwidget.h index a4d88d177..dec2fb90f 100644 --- a/src/gui/rss/rsssettingsdlg.h +++ b/src/gui/rss/articlelistwidget.h @@ -1,6 +1,6 @@ /* - * Bittorrent Client using Qt4 and libtorrent. - * Copyright (C) 2010 Christophe Dumez + * Bittorrent Client using Qt and libtorrent. + * Copyright (C) 2017 Vladimir Golovnev * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License @@ -24,35 +24,43 @@ * 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 RSSSETTINGSDLG_H -#define RSSSETTINGSDLG_H +#ifndef ARTICLELISTWIDGET_H +#define ARTICLELISTWIDGET_H -#include +#include +#include -QT_BEGIN_NAMESPACE -namespace Ui { - class RssSettingsDlg; +namespace RSS +{ + class Article; + class Item; } -QT_END_NAMESPACE -class RssSettingsDlg : public QDialog +class ArticleListWidget: public QListWidget { Q_OBJECT public: - explicit RssSettingsDlg(QWidget *parent = 0); - ~RssSettingsDlg(); + explicit ArticleListWidget(QWidget *parent); -protected slots: - void on_buttonBox_accepted(); + RSS::Article *getRSSArticle(QListWidgetItem *item) const; + QListWidgetItem *mapRSSArticle(RSS::Article *rssArticle) const; + + void setRSSItem(RSS::Item *rssItem, bool unreadOnly = false); + +private slots: + void handleArticleAdded(RSS::Article *rssArticle); + void handleArticleRead(RSS::Article *rssArticle); + void handleArticleAboutToBeRemoved(RSS::Article *rssArticle); private: - Ui::RssSettingsDlg *ui; + QListWidgetItem *createItem(RSS::Article *article); + RSS::Item *m_rssItem = nullptr; + bool m_unreadOnly = false; + QHash m_rssArticleToListItemMapping; }; -#endif // RSSSETTINGS_H +#endif // ARTICLELISTWIDGET_H diff --git a/src/gui/rss/automatedrssdownloader.cpp b/src/gui/rss/automatedrssdownloader.cpp index 30d13091e..31db14a84 100644 --- a/src/gui/rss/automatedrssdownloader.cpp +++ b/src/gui/rss/automatedrssdownloader.cpp @@ -1,6 +1,7 @@ /* - * Bittorrent Client using Qt4 and libtorrent. - * Copyright (C) 2010 Christophe Dumez + * Bittorrent Client using Qt and libtorrent. + * Copyright (C) 2017 Vladimir Golovnev + * 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 @@ -24,223 +25,192 @@ * 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 "automatedrssdownloader.h" + #include #include #include -#include #include +#include +#include #include +#include +#include +#include +#include #include "base/bittorrent/session.h" #include "base/preferences.h" -#include "base/rss/rssdownloadrulelist.h" -#include "base/rss/rssmanager.h" -#include "base/rss/rssfolder.h" -#include "base/rss/rssfeed.h" +#include "base/rss/rss_article.h" +#include "base/rss/rss_autodownloader.h" +#include "base/rss/rss_feed.h" +#include "base/rss/rss_folder.h" +#include "base/rss/rss_session.h" #include "base/utils/fs.h" #include "base/utils/string.h" #include "guiiconprovider.h" #include "autoexpandabledialog.h" #include "ui_automatedrssdownloader.h" -#include "automatedrssdownloader.h" -AutomatedRssDownloader::AutomatedRssDownloader(const QWeakPointer &manager, QWidget *parent) +AutomatedRssDownloader::AutomatedRssDownloader(QWidget *parent) : QDialog(parent) - , ui(new Ui::AutomatedRssDownloader) - , m_manager(manager) - , m_editedRule(0) + , m_ui(new Ui::AutomatedRssDownloader) + , m_currentRuleItem(nullptr) { - ui->setupUi(this); + m_ui->setupUi(this); // Icons - ui->removeRuleBtn->setIcon(GuiIconProvider::instance()->getIcon("list-remove")); - ui->addRuleBtn->setIcon(GuiIconProvider::instance()->getIcon("list-add")); + m_ui->removeRuleBtn->setIcon(GuiIconProvider::instance()->getIcon("list-remove")); + m_ui->addRuleBtn->setIcon(GuiIconProvider::instance()->getIcon("list-add")); // Ui Settings - ui->listRules->setSortingEnabled(true); - ui->listRules->setSelectionMode(QAbstractItemView::ExtendedSelection); - ui->treeMatchingArticles->setSortingEnabled(true); - ui->treeMatchingArticles->sortByColumn(0, Qt::AscendingOrder); - ui->hsplitter->setCollapsible(0, false); - ui->hsplitter->setCollapsible(1, false); - ui->hsplitter->setCollapsible(2, true); // Only the preview list is collapsible - bool ok; Q_UNUSED(ok); - ok = connect(ui->checkRegex, SIGNAL(toggled(bool)), SLOT(updateFieldsToolTips(bool))); - Q_ASSERT(ok); - ok = connect(ui->listRules, SIGNAL(customContextMenuRequested(const QPoint&)), this, SLOT(displayRulesListMenu(const QPoint&))); - Q_ASSERT(ok); - m_ruleList = manager.toStrongRef()->downloadRules(); - m_editableRuleList = new Rss::DownloadRuleList; // Read rule list from disk - m_episodeRegex = new QRegularExpression("^(^\\d{1,4}x(\\d{1,4}(-(\\d{1,4})?)?;){1,}){1,1}", - QRegularExpression::CaseInsensitiveOption); + m_ui->listRules->setSortingEnabled(true); + m_ui->listRules->setSelectionMode(QAbstractItemView::ExtendedSelection); + m_ui->treeMatchingArticles->setSortingEnabled(true); + m_ui->treeMatchingArticles->sortByColumn(0, Qt::AscendingOrder); + m_ui->hsplitter->setCollapsible(0, false); + m_ui->hsplitter->setCollapsible(1, false); + m_ui->hsplitter->setCollapsible(2, true); // Only the preview list is collapsible + + connect(m_ui->checkRegex, &QAbstractButton::toggled, this, &AutomatedRssDownloader::updateFieldsToolTips); + connect(m_ui->listRules, &QWidget::customContextMenuRequested, this, &AutomatedRssDownloader::displayRulesListMenu); + + m_episodeRegex = new QRegularExpression("^(^\\d{1,4}x(\\d{1,4}(-(\\d{1,4})?)?;){1,}){1,1}" + , QRegularExpression::CaseInsensitiveOption); QString tip = "

" + tr("Matches articles based on episode filter.") + "

" + tr("Example: ") + "1x2;8-15;5;30-;" + tr(" will match 2, 5, 8 through 15, 30 and onward episodes of season one", "example X will match") + "

"; tip += "

" + tr("Episode filter rules: ") + "

  • " + tr("Season number is a mandatory non-zero value") + "
  • " + "
  • " + tr("Episode number is a mandatory positive value") + "
  • " + "
  • " + tr("Filter must end with semicolon") + "
  • " + "
  • " + tr("Three range types for episodes are supported: ") + "
  • " + "
    • " - "
    • " + tr("Single number: 1x25; matches episode 25 of season one") + "
    • " + + "
    • " + tr("Single number: 1x25; matches episode 25 of season one") + "
    • " + "
    • " + tr("Normal range: 1x25-40; matches episodes 25 through 40 of season one") + "
    • " + "
    • " + tr("Infinite range: 1x25-; matches episodes 25 and upward of season one, and all episodes of later seasons") + "
    • " + "
"; - ui->lineEFilter->setToolTip(tip); + m_ui->lineEFilter->setToolTip(tip); + initCategoryCombobox(); loadSettings(); + + connect(RSS::AutoDownloader::instance(), &RSS::AutoDownloader::ruleAdded, this, &AutomatedRssDownloader::handleRuleAdded); + connect(RSS::AutoDownloader::instance(), &RSS::AutoDownloader::ruleRenamed, this, &AutomatedRssDownloader::handleRuleRenamed); + connect(RSS::AutoDownloader::instance(), &RSS::AutoDownloader::ruleChanged, this, &AutomatedRssDownloader::handleRuleChanged); + connect(RSS::AutoDownloader::instance(), &RSS::AutoDownloader::ruleAboutToBeRemoved, this, &AutomatedRssDownloader::handleRuleAboutToBeRemoved); + // Update matching articles when necessary - ok = connect(ui->lineContains, SIGNAL(textEdited(QString)), SLOT(updateMatchingArticles())); - Q_ASSERT(ok); - ok = connect(ui->lineContains, SIGNAL(textEdited(QString)), SLOT(updateMustLineValidity())); - Q_ASSERT(ok); - ok = connect(ui->lineNotContains, SIGNAL(textEdited(QString)), SLOT(updateMatchingArticles())); - Q_ASSERT(ok); - ok = connect(ui->lineNotContains, SIGNAL(textEdited(QString)), SLOT(updateMustNotLineValidity())); - Q_ASSERT(ok); - ok = connect(ui->lineEFilter, SIGNAL(textEdited(QString)), SLOT(updateEpisodeFilterValidity())); - Q_ASSERT(ok); - ok = connect(ui->checkRegex, SIGNAL(stateChanged(int)), SLOT(updateMatchingArticles())); - Q_ASSERT(ok); - ok = connect(ui->checkRegex, SIGNAL(stateChanged(int)), SLOT(updateMustLineValidity())); - Q_ASSERT(ok); - ok = connect(ui->checkRegex, SIGNAL(stateChanged(int)), SLOT(updateMustNotLineValidity())); - Q_ASSERT(ok); - ok = connect(this, SIGNAL(finished(int)), SLOT(onFinished(int))); - Q_ASSERT(ok); - ok = connect(ui->lineEFilter, SIGNAL(textEdited(QString)), SLOT(updateMatchingArticles())); - Q_ASSERT(ok); - editHotkey = new QShortcut(Qt::Key_F2, ui->listRules, 0, 0, Qt::WidgetShortcut); - ok = connect(editHotkey, SIGNAL(activated()), SLOT(renameSelectedRule())); - Q_ASSERT(ok); - ok = connect(ui->listRules, SIGNAL(doubleClicked(QModelIndex)), SLOT(renameSelectedRule())); - Q_ASSERT(ok); - deleteHotkey = new QShortcut(QKeySequence::Delete, ui->listRules, 0, 0, Qt::WidgetShortcut); - ok = connect(deleteHotkey, SIGNAL(activated()), SLOT(on_removeRuleBtn_clicked())); - Q_ASSERT(ok); + connect(m_ui->lineContains, &QLineEdit::textEdited, this, &AutomatedRssDownloader::handleRuleDefinitionChanged); + connect(m_ui->lineContains, &QLineEdit::textEdited, this, &AutomatedRssDownloader::updateMustLineValidity); + connect(m_ui->lineNotContains, &QLineEdit::textEdited, this, &AutomatedRssDownloader::handleRuleDefinitionChanged); + connect(m_ui->lineNotContains, &QLineEdit::textEdited, this, &AutomatedRssDownloader::updateMustNotLineValidity); + connect(m_ui->lineEFilter, &QLineEdit::textEdited, this, &AutomatedRssDownloader::handleRuleDefinitionChanged); + connect(m_ui->lineEFilter, &QLineEdit::textEdited, this, &AutomatedRssDownloader::updateEpisodeFilterValidity); + connect(m_ui->checkRegex, &QCheckBox::stateChanged, this, &AutomatedRssDownloader::handleRuleDefinitionChanged); + connect(m_ui->checkRegex, &QCheckBox::stateChanged, this, &AutomatedRssDownloader::updateMustLineValidity); + connect(m_ui->checkRegex, &QCheckBox::stateChanged, this, &AutomatedRssDownloader::updateMustNotLineValidity); + + connect(m_ui->listFeeds, &QListWidget::itemChanged, this, &AutomatedRssDownloader::handleFeedCheckStateChange); + + connect(m_ui->listRules, &QListWidget::itemSelectionChanged, this, &AutomatedRssDownloader::updateRuleDefinitionBox); + connect(m_ui->listRules, &QListWidget::itemChanged, this, &AutomatedRssDownloader::handleRuleCheckStateChange); + + m_editHotkey = new QShortcut(Qt::Key_F2, m_ui->listRules, 0, 0, Qt::WidgetShortcut); + connect(m_editHotkey, &QShortcut::activated, this, &AutomatedRssDownloader::renameSelectedRule); + connect(m_ui->listRules, &QAbstractItemView::doubleClicked, this, &AutomatedRssDownloader::renameSelectedRule); + + m_deleteHotkey = new QShortcut(QKeySequence::Delete, m_ui->listRules, 0, 0, Qt::WidgetShortcut); + connect(m_deleteHotkey, &QShortcut::activated, this, &AutomatedRssDownloader::on_removeRuleBtn_clicked); + + loadFeedList(); + + m_ui->listRules->blockSignals(true); + foreach (const RSS::AutoDownloadRule &rule, RSS::AutoDownloader::instance()->rules()) + createRuleItem(rule); + m_ui->listRules->blockSignals(false); updateRuleDefinitionBox(); + + if (RSS::AutoDownloader::instance()->isProcessingEnabled()) + m_ui->labelWarn->hide(); + connect(RSS::AutoDownloader::instance(), &RSS::AutoDownloader::processingStateChanged + , this, &AutomatedRssDownloader::handleProcessingStateChanged); } AutomatedRssDownloader::~AutomatedRssDownloader() { - qDebug() << Q_FUNC_INFO; - delete editHotkey; - delete deleteHotkey; - delete ui; - delete m_editableRuleList; + // Save current item on exit + saveEditedRule(); + saveSettings(); + + delete m_editHotkey; + delete m_deleteHotkey; + delete m_ui; delete m_episodeRegex; } -void AutomatedRssDownloader::connectRuleFeedSlots() -{ - qDebug() << Q_FUNC_INFO << "Connecting rule and feed slots"; - connect(ui->listRules, SIGNAL(itemSelectionChanged()), this, SLOT(updateRuleDefinitionBox()), Qt::UniqueConnection); - connect(ui->listRules, SIGNAL(itemChanged(QListWidgetItem *)), this, SLOT(handleRuleCheckStateChange(QListWidgetItem *)), Qt::UniqueConnection); - connect(ui->listFeeds, SIGNAL(itemChanged(QListWidgetItem *)), this, SLOT(handleFeedCheckStateChange(QListWidgetItem *)), Qt::UniqueConnection); -} - -void AutomatedRssDownloader::disconnectRuleFeedSlots() -{ - qDebug() << Q_FUNC_INFO << "Disconnecting rule and feed slots"; - disconnect(ui->listRules, SIGNAL(itemSelectionChanged()), this, SLOT(updateRuleDefinitionBox())); - disconnect(ui->listRules, SIGNAL(itemChanged(QListWidgetItem *)), this, SLOT(handleRuleCheckStateChange(QListWidgetItem *))); - disconnect(ui->listFeeds, SIGNAL(itemChanged(QListWidgetItem *)), this, SLOT(handleFeedCheckStateChange(QListWidgetItem *))); -} - void AutomatedRssDownloader::loadSettings() { // load dialog geometry const Preferences *const pref = Preferences::instance(); restoreGeometry(pref->getRssGeometry()); - ui->checkEnableDownloader->setChecked(pref->isRssDownloadingEnabled()); - ui->hsplitter->restoreState(pref->getRssHSplitterSizes()); - // Display download rules - loadRulesList(); + m_ui->hsplitter->restoreState(pref->getRssHSplitterSizes()); } void AutomatedRssDownloader::saveSettings() { - Preferences::instance()->setRssDownloadingEnabled(ui->checkEnableDownloader->isChecked()); // Save dialog geometry Preferences *const pref = Preferences::instance(); pref->setRssGeometry(saveGeometry()); - pref->setRssHSplitterSizes(ui->hsplitter->saveState()); + pref->setRssHSplitterSizes(m_ui->hsplitter->saveState()); } -void AutomatedRssDownloader::loadRulesList() +void AutomatedRssDownloader::createRuleItem(const RSS::AutoDownloadRule &rule) { - // Make sure we save the current item before clearing - saveEditedRule(); - ui->listRules->clear(); - - foreach (const QString &rule_name, m_editableRuleList->ruleNames()) { - QListWidgetItem *item = new QListWidgetItem(rule_name, ui->listRules); - item->setFlags(item->flags() | Qt::ItemIsUserCheckable); - if (m_editableRuleList->getRule(rule_name)->isEnabled()) - item->setCheckState(Qt::Checked); - else - item->setCheckState(Qt::Unchecked); - } + QListWidgetItem *item = new QListWidgetItem(rule.name(), m_ui->listRules); + m_itemsByRuleName.insert(rule.name(), item); + item->setFlags(item->flags() | Qt::ItemIsUserCheckable); + item->setCheckState(rule.isEnabled() ? Qt::Checked : Qt::Unchecked); } void AutomatedRssDownloader::loadFeedList() { - disconnectRuleFeedSlots(); + const QSignalBlocker feedListSignalBlocker(m_ui->listFeeds); - const Preferences *const pref = Preferences::instance(); - const QStringList feed_aliases = pref->getRssFeedsAliases(); - const QStringList feed_urls = pref->getRssFeedsUrls(); - ui->listFeeds->clear(); - - for (int i = 0; i < feed_aliases.size(); ++i) { - QString feed_url = feed_urls.at(i); - feed_url = feed_url.split("\\").last(); - qDebug() << Q_FUNC_INFO << feed_url; - - QListWidgetItem *item = new QListWidgetItem(feed_aliases.at(i), ui->listFeeds); - item->setData(Qt::UserRole, feed_url); + foreach (auto feed, RSS::Session::instance()->feeds()) { + QListWidgetItem *item = new QListWidgetItem(feed->name(), m_ui->listFeeds); + item->setData(Qt::UserRole, feed->url()); item->setFlags(item->flags() | Qt::ItemIsUserCheckable | Qt::ItemIsTristate); } - // Reconnects slots updateFeedList(); } -void AutomatedRssDownloader::updateFeedList(QListWidgetItem *selected) +void AutomatedRssDownloader::updateFeedList() { - disconnectRuleFeedSlots(); + const QSignalBlocker feedListSignalBlocker(m_ui->listFeeds); QList selection; - if (selected) - selection << selected; + if (m_currentRuleItem) + selection << m_currentRuleItem; else - selection = ui->listRules->selectedItems(); + selection = m_ui->listRules->selectedItems(); bool enable = !selection.isEmpty(); - for (int i = 0; ilistFeeds->count(); ++i) { - QListWidgetItem *item = ui->listFeeds->item(i); - const QString feed_url = item->data(Qt::UserRole).toString(); + for (int i = 0; i < m_ui->listFeeds->count(); ++i) { + QListWidgetItem *item = m_ui->listFeeds->item(i); + const QString feedURL = item->data(Qt::UserRole).toString(); item->setHidden(!enable); bool allEnabled = true; bool anyEnabled = false; foreach (const QListWidgetItem *ruleItem, selection) { - Rss::DownloadRulePtr rule = m_editableRuleList->getRule(ruleItem->text()); - if (!rule) continue; - qDebug() << "Rule" << rule->name() << "affects" << rule->rssFeeds().size() << "feeds."; - foreach (QString test, rule->rssFeeds()) - qDebug() << "Feed is " << test; - if (rule->rssFeeds().contains(feed_url)) { - qDebug() << "Rule " << rule->name() << " affects feed " << feed_url; + auto rule = RSS::AutoDownloader::instance()->ruleByName(ruleItem->text()); + if (rule.feedURLs().contains(feedURL)) anyEnabled = true; - } - else { - qDebug() << "Rule " << rule->name() << " does NOT affect feed " << feed_url; + else allEnabled = false; - } } if (anyEnabled && allEnabled) @@ -251,270 +221,205 @@ void AutomatedRssDownloader::updateFeedList(QListWidgetItem *selected) item->setCheckState(Qt::Unchecked); } - ui->listFeeds->sortItems(); - ui->lblListFeeds->setEnabled(enable); - ui->listFeeds->setEnabled(enable); + m_ui->listFeeds->sortItems(); + m_ui->lblListFeeds->setEnabled(enable); + m_ui->listFeeds->setEnabled(enable); +} - if (selected) { - m_editedRule = selected; - ui->listRules->clearSelection(); - ui->listRules->setCurrentItem(selected); +void AutomatedRssDownloader::updateRuleDefinitionBox() +{ + const QList selection = m_ui->listRules->selectedItems(); + QListWidgetItem *currentRuleItem = ((selection.count() == 1) ? selection.first() : nullptr); + if (m_currentRuleItem != currentRuleItem) { + saveEditedRule(); // Save previous rule first + m_currentRuleItem = currentRuleItem; + //m_ui->listRules->setCurrentItem(m_currentRuleItem); } - connectRuleFeedSlots(); - updateMatchingArticles(); -} - -bool AutomatedRssDownloader::isRssDownloaderEnabled() const -{ - return ui->checkEnableDownloader->isChecked(); -} - -void AutomatedRssDownloader::updateRuleDefinitionBox(QListWidgetItem *selected) -{ - disconnectRuleFeedSlots(); - - qDebug() << Q_FUNC_INFO; - // Save previous rule first - saveEditedRule(); // Update rule definition box - const QList selection = ui->listRules->selectedItems(); + if (m_currentRuleItem) { + m_currentRule = RSS::AutoDownloader::instance()->ruleByName(m_currentRuleItem->text()); - if (!selected && (selection.count() == 1)) - selected = selection.first(); + m_ui->lineContains->setText(m_currentRule.mustContain()); + m_ui->lineNotContains->setText(m_currentRule.mustNotContain()); + if (!m_currentRule.episodeFilter().isEmpty()) + m_ui->lineEFilter->setText(m_currentRule.episodeFilter()); + else + m_ui->lineEFilter->clear(); + m_ui->saveDiffDir_check->setChecked(!m_currentRule.savePath().isEmpty()); + m_ui->lineSavePath->setText(Utils::Fs::toNativePath(m_currentRule.savePath())); + m_ui->checkRegex->blockSignals(true); + m_ui->checkRegex->setChecked(m_currentRule.useRegex()); + m_ui->checkRegex->blockSignals(false); + m_ui->comboCategory->setCurrentIndex(m_ui->comboCategory->findText(m_currentRule.assignedCategory())); + if (m_currentRule.assignedCategory().isEmpty()) + m_ui->comboCategory->clearEditText(); + int index = 0; + if (m_currentRule.addPaused() == TriStateBool::True) + index = 1; + else if (m_currentRule.addPaused() == TriStateBool::False) + index = 2; + m_ui->comboAddPaused->setCurrentIndex(index); + m_ui->spinIgnorePeriod->setValue(m_currentRule.ignoreDays()); + QDateTime dateTime = m_currentRule.lastMatch(); + QString lMatch; + if (dateTime.isValid()) + lMatch = tr("Last Match: %1 days ago").arg(dateTime.daysTo(QDateTime::currentDateTime())); + else + lMatch = tr("Last Match: Unknown"); + m_ui->lblLastMatch->setText(lMatch); + updateMustLineValidity(); + updateMustNotLineValidity(); + updateEpisodeFilterValidity(); - if (selected) { - m_editedRule = selected; - - // Cannot call getCurrentRule() here as the current item hasn't been updated yet - // and we could get the details from the wrong rule. - // Also can't set the current item here or the selected items gets messed up. - Rss::DownloadRulePtr rule = m_editableRuleList->getRule(m_editedRule->text()); - - if (rule) { - ui->lineContains->setText(rule->mustContain()); - ui->lineNotContains->setText(rule->mustNotContain()); - QString ep = rule->episodeFilter(); - if (!ep.isEmpty()) - ui->lineEFilter->setText(ep); - else - ui->lineEFilter->clear(); - ui->saveDiffDir_check->setChecked(!rule->savePath().isEmpty()); - ui->lineSavePath->setText(Utils::Fs::toNativePath(rule->savePath())); - ui->checkRegex->blockSignals(true); - ui->checkRegex->setChecked(rule->useRegex()); - ui->checkRegex->blockSignals(false); - ui->comboCategory->setCurrentIndex(ui->comboCategory->findText(rule->category())); - if (rule->category().isEmpty()) - ui->comboCategory->clearEditText(); - ui->comboAddPaused->setCurrentIndex(rule->addPaused()); - ui->spinIgnorePeriod->setValue(rule->ignoreDays()); - QDateTime dateTime = rule->lastMatch(); - QString lMatch; - if (dateTime.isValid()) - lMatch = tr("Last Match: %1 days ago").arg(dateTime.daysTo(QDateTime::currentDateTime())); - else - lMatch = tr("Last Match: Unknown"); - ui->lblLastMatch->setText(lMatch); - updateMustLineValidity(); - updateMustNotLineValidity(); - updateEpisodeFilterValidity(); - } - else { - // New rule - clearRuleDefinitionBox(); - ui->comboAddPaused->setCurrentIndex(0); - ui->comboCategory->setCurrentIndex(0); - ui->spinIgnorePeriod->setValue(0); - } - - updateFieldsToolTips(ui->checkRegex->isChecked()); - ui->ruleDefBox->setEnabled(true); + updateFieldsToolTips(m_ui->checkRegex->isChecked()); + m_ui->ruleDefBox->setEnabled(true); } else { - m_editedRule = 0; + m_currentRule = RSS::AutoDownloadRule(); clearRuleDefinitionBox(); - ui->ruleDefBox->setEnabled(false); + m_ui->ruleDefBox->setEnabled(false); } - // Reconnects slots - updateFeedList(selected); + updateFeedList(); + updateMatchingArticles(); } void AutomatedRssDownloader::clearRuleDefinitionBox() { - ui->lineContains->clear(); - ui->lineNotContains->clear(); - ui->lineEFilter->clear(); - ui->saveDiffDir_check->setChecked(false); - ui->lineSavePath->clear(); - ui->comboCategory->clearEditText(); - ui->comboCategory->setCurrentIndex(-1); - ui->checkRegex->setChecked(false); - ui->spinIgnorePeriod->setValue(0); - ui->comboAddPaused->clearEditText(); - ui->comboAddPaused->setCurrentIndex(-1); - updateFieldsToolTips(ui->checkRegex->isChecked()); + m_ui->lineContains->clear(); + m_ui->lineNotContains->clear(); + m_ui->lineEFilter->clear(); + m_ui->saveDiffDir_check->setChecked(false); + m_ui->lineSavePath->clear(); + m_ui->comboCategory->clearEditText(); + m_ui->comboCategory->setCurrentIndex(-1); + m_ui->checkRegex->setChecked(false); + m_ui->spinIgnorePeriod->setValue(0); + m_ui->comboAddPaused->clearEditText(); + m_ui->comboAddPaused->setCurrentIndex(-1); + updateFieldsToolTips(m_ui->checkRegex->isChecked()); updateMustLineValidity(); updateMustNotLineValidity(); updateEpisodeFilterValidity(); } -Rss::DownloadRulePtr AutomatedRssDownloader::getCurrentRule() const -{ - QListWidgetItem *current_item = ui->listRules->currentItem(); - if (current_item) - return m_editableRuleList->getRule(current_item->text()); - return Rss::DownloadRulePtr(); -} - void AutomatedRssDownloader::initCategoryCombobox() { // Load torrent categories QStringList categories = BitTorrent::Session::instance()->categories(); std::sort(categories.begin(), categories.end(), Utils::String::naturalCompareCaseInsensitive); - ui->comboCategory->addItem(QString("")); - ui->comboCategory->addItems(categories); + m_ui->comboCategory->addItem(""); + m_ui->comboCategory->addItems(categories); +} + +void AutomatedRssDownloader::updateEditedRule() +{ + if (!m_currentRuleItem || !m_ui->ruleDefBox->isEnabled()) return; + + m_currentRule.setEnabled(m_currentRuleItem->checkState() != Qt::Unchecked); + m_currentRule.setUseRegex(m_ui->checkRegex->isChecked()); + m_currentRule.setMustContain(m_ui->lineContains->text()); + m_currentRule.setMustNotContain(m_ui->lineNotContains->text()); + m_currentRule.setEpisodeFilter(m_ui->lineEFilter->text()); + m_currentRule.setSavePath(m_ui->saveDiffDir_check->isChecked() ? m_ui->lineSavePath->text() : ""); + m_currentRule.setCategory(m_ui->comboCategory->currentText()); + TriStateBool addPaused; // Undefined by default + if (m_ui->comboAddPaused->currentIndex() == 1) + addPaused = TriStateBool::True; + else if (m_ui->comboAddPaused->currentIndex() == 2) + addPaused = TriStateBool::False; + m_currentRule.setAddPaused(addPaused); + m_currentRule.setIgnoreDays(m_ui->spinIgnorePeriod->value()); } void AutomatedRssDownloader::saveEditedRule() { - if (!m_editedRule || !ui->ruleDefBox->isEnabled()) return; + if (!m_currentRuleItem || !m_ui->ruleDefBox->isEnabled()) return; - qDebug() << Q_FUNC_INFO << m_editedRule; - if (ui->listRules->findItems(m_editedRule->text(), Qt::MatchExactly).isEmpty()) { - qDebug() << "Could not find rule" << m_editedRule->text() << "in the UI list"; - qDebug() << "Probably removed the item, no need to save it"; - return; - } - Rss::DownloadRulePtr rule = m_editableRuleList->getRule(m_editedRule->text()); - if (!rule) { - rule = Rss::DownloadRulePtr(new Rss::DownloadRule); - rule->setName(m_editedRule->text()); - } - if (m_editedRule->checkState() == Qt::Unchecked) - rule->setEnabled(false); - else - rule->setEnabled(true); - rule->setUseRegex(ui->checkRegex->isChecked()); - rule->setMustContain(ui->lineContains->text()); - rule->setMustNotContain(ui->lineNotContains->text()); - rule->setEpisodeFilter(ui->lineEFilter->text()); - if (ui->saveDiffDir_check->isChecked()) - rule->setSavePath(ui->lineSavePath->text()); - else - rule->setSavePath(""); - rule->setCategory(ui->comboCategory->currentText()); - - rule->setAddPaused(Rss::DownloadRule::AddPausedState(ui->comboAddPaused->currentIndex())); - rule->setIgnoreDays(ui->spinIgnorePeriod->value()); - // rule->setRssFeeds(getSelectedFeeds()); - // Save it - m_editableRuleList->saveRule(rule); + updateEditedRule(); + RSS::AutoDownloader::instance()->insertRule(m_currentRule); } void AutomatedRssDownloader::on_addRuleBtn_clicked() { - saveEditedRule(); +// saveEditedRule(); // Ask for a rule name - const QString rule_name = AutoExpandableDialog::getText(this, tr("New rule name"), tr("Please type the name of the new download rule.")); - if (rule_name.isEmpty()) return; + const QString ruleName = AutoExpandableDialog::getText( + this, tr("New rule name"), tr("Please type the name of the new download rule.")); + if (ruleName.isEmpty()) return; + // Check if this rule name already exists - if (m_editableRuleList->getRule(rule_name)) { - QMessageBox::warning(this, tr("Rule name conflict"), tr("A rule with this name already exists, please choose another name.")); + if (RSS::AutoDownloader::instance()->hasRule(ruleName)) { + QMessageBox::warning(this, tr("Rule name conflict") + , tr("A rule with this name already exists, please choose another name.")); return; } - disconnectRuleFeedSlots(); - - // Add the new rule to the list - QListWidgetItem *item = new QListWidgetItem(rule_name, ui->listRules); - item->setFlags(item->flags() | Qt::ItemIsUserCheckable | Qt::ItemIsTristate); - item->setCheckState(Qt::Checked); // Enable as a default - m_editedRule = 0; - - // Reconnects slots - updateRuleDefinitionBox(item); + RSS::AutoDownloader::instance()->insertRule(RSS::AutoDownloadRule(ruleName)); } void AutomatedRssDownloader::on_removeRuleBtn_clicked() { - const QList selection = ui->listRules->selectedItems(); + const QList selection = m_ui->listRules->selectedItems(); if (selection.isEmpty()) return; + // Ask for confirmation - QString confirm_text; - if (selection.count() == 1) - confirm_text = tr("Are you sure you want to remove the download rule named '%1'?").arg(selection.first()->text()); - else - confirm_text = tr("Are you sure you want to remove the selected download rules?"); - if (QMessageBox::question(this, tr("Rule deletion confirmation"), confirm_text, QMessageBox::Yes, QMessageBox::No) != QMessageBox::Yes) + const QString confirmText = ((selection.count() == 1) + ? tr("Are you sure you want to remove the download rule named '%1'?") + .arg(selection.first()->text()) + : tr("Are you sure you want to remove the selected download rules?")); + if (QMessageBox::question(this, tr("Rule deletion confirmation"), confirmText, QMessageBox::Yes, QMessageBox::No) != QMessageBox::Yes) return; - disconnectRuleFeedSlots(); - - foreach (QListWidgetItem *item, selection) { - // Actually remove the item - ui->listRules->removeItemWidget(item); - const QString rule_name = item->text(); - // Clean up memory - delete item; - qDebug("Removed item for the UI list"); - // Remove it from the m_editableRuleList - m_editableRuleList->removeRule(rule_name); - } - - m_editedRule = 0; - // Reconnects slots - updateRuleDefinitionBox(); + foreach (QListWidgetItem *item, selection) + RSS::AutoDownloader::instance()->removeRule(item->text()); } void AutomatedRssDownloader::on_browseSP_clicked() { - QString save_path = QFileDialog::getExistingDirectory(this, tr("Destination directory"), QDir::homePath()); - if (!save_path.isEmpty()) - ui->lineSavePath->setText(Utils::Fs::toNativePath(save_path)); + QString savePath = QFileDialog::getExistingDirectory(this, tr("Destination directory"), QDir::homePath()); + if (!savePath.isEmpty()) + m_ui->lineSavePath->setText(Utils::Fs::toNativePath(savePath)); } void AutomatedRssDownloader::on_exportBtn_clicked() { - if (m_editableRuleList->isEmpty()) { - QMessageBox::warning(this, tr("Invalid action"), tr("The list is empty, there is nothing to export.")); - return; - } - // Ask for a save path - QString save_path = QFileDialog::getSaveFileName(this, tr("Where would you like to save the list?"), QDir::homePath(), tr("Rules list (*.rssrules)")); - if (save_path.isEmpty()) return; - if (!save_path.endsWith(".rssrules", Qt::CaseInsensitive)) - save_path += ".rssrules"; - if (!m_editableRuleList->serialize(save_path)) { - QMessageBox::warning(this, tr("I/O Error"), tr("Failed to create the destination file")); - return; - } +// if (m_editableRuleList->isEmpty()) { +// QMessageBox::warning(this, tr("Invalid action"), tr("The list is empty, there is nothing to export.")); +// return; +// } +// // Ask for a save path +// QString save_path = QFileDialog::getSaveFileName(this, tr("Where would you like to save the list?"), QDir::homePath(), tr("Rules list (*.rssrules)")); +// if (save_path.isEmpty()) return; +// if (!save_path.endsWith(".rssrules", Qt::CaseInsensitive)) +// save_path += ".rssrules"; +// if (!m_editableRuleList->serialize(save_path)) { +// QMessageBox::warning(this, tr("I/O Error"), tr("Failed to create the destination file")); +// return; +// } } void AutomatedRssDownloader::on_importBtn_clicked() { - // Ask for filter path - QString load_path = QFileDialog::getOpenFileName(this, tr("Please point to the RSS download rules file"), QDir::homePath(), tr("Rules list") + QString(" (*.rssrules *.filters)")); - if (load_path.isEmpty() || !QFile::exists(load_path)) return; - // Load it - if (!m_editableRuleList->unserialize(load_path)) { - QMessageBox::warning(this, tr("Import Error"), tr("Failed to import the selected rules file")); - return; - } - // Reload the rule list - loadRulesList(); +// // Ask for filter path +// QString load_path = QFileDialog::getOpenFileName(this, tr("Please point to the RSS download rules file"), QDir::homePath(), tr("Rules list") + QString(" (*.rssrules *.filters)")); +// if (load_path.isEmpty() || !QFile::exists(load_path)) return; +// // Load it +// if (!m_editableRuleList->unserialize(load_path)) { +// QMessageBox::warning(this, tr("Import Error"), tr("Failed to import the selected rules file")); +// return; +// } } -void AutomatedRssDownloader::displayRulesListMenu(const QPoint &pos) +void AutomatedRssDownloader::displayRulesListMenu() { - Q_UNUSED(pos); QMenu menu; QAction *addAct = menu.addAction(GuiIconProvider::instance()->getIcon("list-add"), tr("Add new rule...")); - QAction *delAct = 0; - QAction *renameAct = 0; - const QList selection = ui->listRules->selectedItems(); + QAction *delAct = nullptr; + QAction *renameAct = nullptr; + const QList selection = m_ui->listRules->selectedItems(); if (!selection.isEmpty()) { if (selection.count() == 1) { delAct = menu.addAction(GuiIconProvider::instance()->getIcon("list-remove"), tr("Delete rule")); @@ -525,115 +430,104 @@ void AutomatedRssDownloader::displayRulesListMenu(const QPoint &pos) delAct = menu.addAction(GuiIconProvider::instance()->getIcon("list-remove"), tr("Delete selected rules")); } } + QAction *act = menu.exec(QCursor::pos()); if (!act) return; - if (act == addAct) { + + if (act == addAct) on_addRuleBtn_clicked(); - return; - } - if (act == delAct) { + else if (act == delAct) on_removeRuleBtn_clicked(); - return; - } - if (act == renameAct) { + else if (act == renameAct) renameSelectedRule(); - return; - } } void AutomatedRssDownloader::renameSelectedRule() { - const QList selection = ui->listRules->selectedItems(); - if (selection.isEmpty()) - return; + const QList selection = m_ui->listRules->selectedItems(); + if (selection.isEmpty()) return; QListWidgetItem *item = selection.first(); forever { - QString new_name = AutoExpandableDialog::getText(this, tr("Rule renaming"), tr("Please type the new rule name"), QLineEdit::Normal, item->text()); - new_name = new_name.trimmed(); - if (new_name.isEmpty()) return; - if (m_editableRuleList->ruleNames().contains(new_name, Qt::CaseInsensitive)) { - QMessageBox::warning(this, tr("Rule name conflict"), tr("A rule with this name already exists, please choose another name.")); + QString newName = AutoExpandableDialog::getText( + this, tr("Rule renaming"), tr("Please type the new rule name") + , QLineEdit::Normal, item->text()); + newName = newName.trimmed(); + if (newName.isEmpty()) return; + + if (RSS::AutoDownloader::instance()->hasRule(newName)) { + QMessageBox::warning(this, tr("Rule name conflict") + , tr("A rule with this name already exists, please choose another name.")); } else { // Rename the rule - m_editableRuleList->renameRule(item->text(), new_name); - item->setText(new_name); + RSS::AutoDownloader::instance()->renameRule(item->text(), newName); return; } } } -void AutomatedRssDownloader::handleRuleCheckStateChange(QListWidgetItem *rule_item) +void AutomatedRssDownloader::handleRuleCheckStateChange(QListWidgetItem *ruleItem) { - // Make sure the current rule is saved - saveEditedRule(); - - // Make sure we save the rule that was enabled or disabled - it might not be the current selection. - m_editedRule = rule_item; - saveEditedRule(); - updateRuleDefinitionBox(); + m_ui->listRules->setCurrentItem(ruleItem); } -void AutomatedRssDownloader::handleFeedCheckStateChange(QListWidgetItem *feed_item) +void AutomatedRssDownloader::handleFeedCheckStateChange(QListWidgetItem *feedItem) { - // Make sure the current rule is saved - saveEditedRule(); - const QString feed_url = feed_item->data(Qt::UserRole).toString(); - foreach (QListWidgetItem *rule_item, ui->listRules->selectedItems()) { - Rss::DownloadRulePtr rule = m_editableRuleList->getRule(rule_item->text()); - Q_ASSERT(rule); - QStringList affected_feeds = rule->rssFeeds(); - if ((feed_item->checkState() == Qt::Checked) && !affected_feeds.contains(feed_url)) - affected_feeds << feed_url; - else if ((feed_item->checkState() == Qt::Unchecked) && affected_feeds.contains(feed_url)) - affected_feeds.removeOne(feed_url); - // Save the updated rule - if (affected_feeds.size() != rule->rssFeeds().size()) { - rule->setRssFeeds(affected_feeds); - m_editableRuleList->saveRule(rule); - } + const QString feedURL = feedItem->data(Qt::UserRole).toString(); + foreach (QListWidgetItem *ruleItem, m_ui->listRules->selectedItems()) { + RSS::AutoDownloadRule rule = (ruleItem == m_currentRuleItem + ? m_currentRule + : RSS::AutoDownloader::instance()->ruleByName(ruleItem->text())); + QStringList affectedFeeds = rule.feedURLs(); + if ((feedItem->checkState() == Qt::Checked) && !affectedFeeds.contains(feedURL)) + affectedFeeds << feedURL; + else if ((feedItem->checkState() == Qt::Unchecked) && affectedFeeds.contains(feedURL)) + affectedFeeds.removeOne(feedURL); + + rule.setFeedURLs(affectedFeeds); + if (ruleItem != m_currentRuleItem) + RSS::AutoDownloader::instance()->insertRule(rule); + else + m_currentRule = rule; } - // Update Matching articles - updateMatchingArticles(); + + handleRuleDefinitionChanged(); } void AutomatedRssDownloader::updateMatchingArticles() { - ui->treeMatchingArticles->clear(); - Rss::ManagerPtr manager = m_manager.toStrongRef(); - if (!manager) - return; - const QHash all_feeds = manager->rootFolder()->getAllFeedsAsHash(); + m_ui->treeMatchingArticles->clear(); - saveEditedRule(); - foreach (const QListWidgetItem *rule_item, ui->listRules->selectedItems()) { - Rss::DownloadRulePtr rule = m_editableRuleList->getRule(rule_item->text()); - if (!rule) continue; - foreach (const QString &feed_url, rule->rssFeeds()) { - qDebug() << Q_FUNC_INFO << feed_url; - if (!all_feeds.contains(feed_url)) continue; // Feed was removed - Rss::FeedPtr feed = all_feeds.value(feed_url); - Q_ASSERT(feed); - if (!feed) continue; - const QStringList matching_articles = rule->findMatchingArticles(feed); - if (!matching_articles.isEmpty()) - addFeedArticlesToTree(feed, matching_articles); + foreach (const QListWidgetItem *ruleItem, m_ui->listRules->selectedItems()) { + RSS::AutoDownloadRule rule = (ruleItem == m_currentRuleItem + ? m_currentRule + : RSS::AutoDownloader::instance()->ruleByName(ruleItem->text())); + foreach (const QString &feedURL, rule.feedURLs()) { + auto feed = RSS::Session::instance()->feedByURL(feedURL); + if (!feed) continue; // feed doesn't exists + + QStringList matchingArticles; + foreach (auto article, feed->articles()) + if (rule.matches(article->title())) + matchingArticles << article->title(); + if (!matchingArticles.isEmpty()) + addFeedArticlesToTree(feed, matchingArticles); } } m_treeListEntries.clear(); } -void AutomatedRssDownloader::addFeedArticlesToTree(const Rss::FeedPtr &feed, const QStringList &articles) +void AutomatedRssDownloader::addFeedArticlesToTree(RSS::Feed *feed, const QStringList &articles) { // Turn off sorting while inserting - ui->treeMatchingArticles->setSortingEnabled(false); + m_ui->treeMatchingArticles->setSortingEnabled(false); // Check if this feed is already in the tree - QTreeWidgetItem *treeFeedItem = 0; - for (int i = 0; itreeMatchingArticles->topLevelItemCount(); ++i) { - QTreeWidgetItem *item = ui->treeMatchingArticles->topLevelItem(i); + QTreeWidgetItem *treeFeedItem = nullptr; + for (int i = 0; i < m_ui->treeMatchingArticles->topLevelItemCount(); ++i) { + QTreeWidgetItem *item = m_ui->treeMatchingArticles->topLevelItem(i); if (item->data(0, Qt::UserRole).toString() == feed->url()) { treeFeedItem = item; break; @@ -642,31 +536,31 @@ void AutomatedRssDownloader::addFeedArticlesToTree(const Rss::FeedPtr &feed, con // If there is none, create it if (!treeFeedItem) { - treeFeedItem = new QTreeWidgetItem(QStringList() << feed->displayName()); - treeFeedItem->setToolTip(0, feed->displayName()); + treeFeedItem = new QTreeWidgetItem(QStringList() << feed->name()); + treeFeedItem->setToolTip(0, feed->name()); QFont f = treeFeedItem->font(0); f.setBold(true); treeFeedItem->setFont(0, f); treeFeedItem->setData(0, Qt::DecorationRole, GuiIconProvider::instance()->getIcon("inode-directory")); treeFeedItem->setData(0, Qt::UserRole, feed->url()); - ui->treeMatchingArticles->addTopLevelItem(treeFeedItem); + m_ui->treeMatchingArticles->addTopLevelItem(treeFeedItem); } // Insert the articles - foreach (const QString &art, articles) { - QPair key(feed->displayName(), art); + foreach (const QString &article, articles) { + QPair key(feed->name(), article); if (!m_treeListEntries.contains(key)) { m_treeListEntries << key; - QTreeWidgetItem *item = new QTreeWidgetItem(QStringList() << art); - item->setToolTip(0, art); + QTreeWidgetItem *item = new QTreeWidgetItem(QStringList() << article); + item->setToolTip(0, article); treeFeedItem->addChild(item); } } - ui->treeMatchingArticles->expandItem(treeFeedItem); - ui->treeMatchingArticles->sortItems(0, Qt::AscendingOrder); - ui->treeMatchingArticles->setSortingEnabled(true); + m_ui->treeMatchingArticles->expandItem(treeFeedItem); + m_ui->treeMatchingArticles->sortItems(0, Qt::AscendingOrder); + m_ui->treeMatchingArticles->setSortingEnabled(true); } void AutomatedRssDownloader::updateFieldsToolTips(bool regex) @@ -692,14 +586,14 @@ void AutomatedRssDownloader::updateFieldsToolTips(bool regex) "We talk about regex/wildcards in the RSS filters section here." " So a valid sentence would be: An expression with an empty | clause (e.g. expr|)" ).arg("|").arg("expr|"); - ui->lineContains->setToolTip(tip + tr(" will match all articles.") + "

"); - ui->lineNotContains->setToolTip(tip + tr(" will exclude all articles.") + "

"); + m_ui->lineContains->setToolTip(tip + tr(" will match all articles.") + "

"); + m_ui->lineNotContains->setToolTip(tip + tr(" will exclude all articles.") + "

"); } void AutomatedRssDownloader::updateMustLineValidity() { - const QString text = ui->lineContains->text(); - bool isRegex = ui->checkRegex->isChecked(); + const QString text = m_ui->lineContains->text(); + bool isRegex = m_ui->checkRegex->isChecked(); bool valid = true; QString error; @@ -723,21 +617,21 @@ void AutomatedRssDownloader::updateMustLineValidity() } if (valid) { - ui->lineContains->setStyleSheet(""); - ui->lbl_must_stat->setPixmap(QPixmap()); - ui->lbl_must_stat->setToolTip(QLatin1String("")); + m_ui->lineContains->setStyleSheet(""); + m_ui->lbl_must_stat->setPixmap(QPixmap()); + m_ui->lbl_must_stat->setToolTip(""); } else { - ui->lineContains->setStyleSheet("QLineEdit { color: #ff0000; }"); - ui->lbl_must_stat->setPixmap(GuiIconProvider::instance()->getIcon("task-attention").pixmap(16, 16)); - ui->lbl_must_stat->setToolTip(error); + m_ui->lineContains->setStyleSheet("QLineEdit { color: #ff0000; }"); + m_ui->lbl_must_stat->setPixmap(GuiIconProvider::instance()->getIcon("task-attention").pixmap(16, 16)); + m_ui->lbl_must_stat->setToolTip(error); } } void AutomatedRssDownloader::updateMustNotLineValidity() { - const QString text = ui->lineNotContains->text(); - bool isRegex = ui->checkRegex->isChecked(); + const QString text = m_ui->lineNotContains->text(); + bool isRegex = m_ui->checkRegex->isChecked(); bool valid = true; QString error; @@ -761,57 +655,63 @@ void AutomatedRssDownloader::updateMustNotLineValidity() } if (valid) { - ui->lineNotContains->setStyleSheet(""); - ui->lbl_mustnot_stat->setPixmap(QPixmap()); - ui->lbl_mustnot_stat->setToolTip(QLatin1String("")); + m_ui->lineNotContains->setStyleSheet(""); + m_ui->lbl_mustnot_stat->setPixmap(QPixmap()); + m_ui->lbl_mustnot_stat->setToolTip(""); } else { - ui->lineNotContains->setStyleSheet("QLineEdit { color: #ff0000; }"); - ui->lbl_mustnot_stat->setPixmap(GuiIconProvider::instance()->getIcon("task-attention").pixmap(16, 16)); - ui->lbl_mustnot_stat->setToolTip(error); + m_ui->lineNotContains->setStyleSheet("QLineEdit { color: #ff0000; }"); + m_ui->lbl_mustnot_stat->setPixmap(GuiIconProvider::instance()->getIcon("task-attention").pixmap(16, 16)); + m_ui->lbl_mustnot_stat->setToolTip(error); } } void AutomatedRssDownloader::updateEpisodeFilterValidity() { - const QString text = ui->lineEFilter->text(); + const QString text = m_ui->lineEFilter->text(); bool valid = text.isEmpty() || m_episodeRegex->match(text).hasMatch(); if (valid) { - ui->lineEFilter->setStyleSheet(""); - ui->lbl_epfilter_stat->setPixmap(QPixmap()); + m_ui->lineEFilter->setStyleSheet(""); + m_ui->lbl_epfilter_stat->setPixmap(QPixmap()); } else { - ui->lineEFilter->setStyleSheet("QLineEdit { color: #ff0000; }"); - ui->lbl_epfilter_stat->setPixmap(GuiIconProvider::instance()->getIcon("task-attention").pixmap(16, 16)); + m_ui->lineEFilter->setStyleSheet("QLineEdit { color: #ff0000; }"); + m_ui->lbl_epfilter_stat->setPixmap(GuiIconProvider::instance()->getIcon("task-attention").pixmap(16, 16)); } } -void AutomatedRssDownloader::showEvent(QShowEvent *event) +void AutomatedRssDownloader::handleRuleDefinitionChanged() { - // Connects the signals and slots - loadFeedList(); - QDialog::showEvent(event); + updateEditedRule(); + updateMatchingArticles(); } -void AutomatedRssDownloader::hideEvent(QHideEvent *event) +void AutomatedRssDownloader::handleRuleAdded(const QString &ruleName) { - disconnectRuleFeedSlots(); - QDialog::hideEvent(event); + createRuleItem(RSS::AutoDownloadRule(ruleName)); } -void AutomatedRssDownloader::onFinished(int result) +void AutomatedRssDownloader::handleRuleRenamed(const QString &ruleName, const QString &oldRuleName) { - Q_UNUSED(result); - disconnectRuleFeedSlots(); - - // Save current item on exit - saveEditedRule(); - ui->listRules->clearSelection(); - m_ruleList->replace(m_editableRuleList); - m_ruleList->saveRulesToStorage(); - saveSettings(); - - m_treeListEntries.clear(); - ui->treeMatchingArticles->clear(); + auto item = m_itemsByRuleName.value(oldRuleName); + m_itemsByRuleName.insert(ruleName, item); + item->setText(ruleName); +} + +void AutomatedRssDownloader::handleRuleChanged(const QString &ruleName) +{ + auto item = m_itemsByRuleName.value(ruleName); + if (item != m_currentRuleItem) + item->setCheckState(RSS::AutoDownloader::instance()->ruleByName(ruleName).isEnabled() ? Qt::Checked : Qt::Unchecked); +} + +void AutomatedRssDownloader::handleRuleAboutToBeRemoved(const QString &ruleName) +{ + delete m_itemsByRuleName.take(ruleName); +} + +void AutomatedRssDownloader::handleProcessingStateChanged(bool enabled) +{ + m_ui->labelWarn->setVisible(!enabled); } diff --git a/src/gui/rss/automatedrssdownloader.h b/src/gui/rss/automatedrssdownloader.h index 6c124bad0..acbd9b955 100644 --- a/src/gui/rss/automatedrssdownloader.h +++ b/src/gui/rss/automatedrssdownloader.h @@ -1,6 +1,7 @@ /* - * Bittorrent Client using Qt4 and libtorrent. - * Copyright (C) 2010 Christophe Dumez + * Bittorrent Client using Qt and libtorrent. + * Copyright (C) 2017 Vladimir Golovnev + * 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 @@ -24,99 +25,85 @@ * 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 AUTOMATEDRSSDOWNLOADER_H #define AUTOMATEDRSSDOWNLOADER_H #include -#include +#include #include #include -#include -#include -#include -#include -#include "base/rss/rssdownloadrule.h" +#include "base/rss/rss_autodownloadrule.h" -QT_BEGIN_NAMESPACE namespace Ui { class AutomatedRssDownloader; } -QT_END_NAMESPACE -namespace Rss -{ - class DownloadRuleList; - class Manager; -} - -QT_BEGIN_NAMESPACE class QListWidgetItem; class QRegularExpression; -QT_END_NAMESPACE +class QShortcut; + +namespace RSS +{ + class Feed; +} class AutomatedRssDownloader: public QDialog { Q_OBJECT public: - explicit AutomatedRssDownloader(const QWeakPointer &manager, QWidget *parent = 0); - ~AutomatedRssDownloader(); - bool isRssDownloaderEnabled() const; - -protected: - virtual void showEvent(QShowEvent *event) override; - virtual void hideEvent(QHideEvent *event) override; - -protected slots: - void loadSettings(); - void saveSettings(); - void loadRulesList(); - void handleRuleCheckStateChange(QListWidgetItem *rule_item); - void handleFeedCheckStateChange(QListWidgetItem *feed_item); - void updateRuleDefinitionBox(QListWidgetItem *selected = 0); - void clearRuleDefinitionBox(); - void saveEditedRule(); - void loadFeedList(); - void updateFeedList(QListWidgetItem *selected = 0); + explicit AutomatedRssDownloader(QWidget *parent = nullptr); + ~AutomatedRssDownloader() override; private slots: - void displayRulesListMenu(const QPoint &pos); void on_addRuleBtn_clicked(); void on_removeRuleBtn_clicked(); void on_browseSP_clicked(); void on_exportBtn_clicked(); void on_importBtn_clicked(); + + void handleRuleCheckStateChange(QListWidgetItem *ruleItem); + void handleFeedCheckStateChange(QListWidgetItem *feedItem); + void displayRulesListMenu(); void renameSelectedRule(); - void updateMatchingArticles(); + void updateRuleDefinitionBox(); void updateFieldsToolTips(bool regex); void updateMustLineValidity(); void updateMustNotLineValidity(); void updateEpisodeFilterValidity(); - void onFinished(int result); + void handleRuleDefinitionChanged(); + void handleRuleAdded(const QString &ruleName); + void handleRuleRenamed(const QString &ruleName, const QString &oldRuleName); + void handleRuleChanged(const QString &ruleName); + void handleRuleAboutToBeRemoved(const QString &ruleName); + + void handleProcessingStateChanged(bool enabled); private: - Rss::DownloadRulePtr getCurrentRule() const; + void loadSettings(); + void saveSettings(); + void createRuleItem(const RSS::AutoDownloadRule &rule); void initCategoryCombobox(); - void addFeedArticlesToTree(const Rss::FeedPtr &feed, const QStringList &articles); - void disconnectRuleFeedSlots(); - void connectRuleFeedSlots(); + void clearRuleDefinitionBox(); + void updateEditedRule(); + void updateMatchingArticles(); + void saveEditedRule(); + void loadFeedList(); + void updateFeedList(); + void addFeedArticlesToTree(RSS::Feed *feed, const QStringList &articles); -private: - Ui::AutomatedRssDownloader *ui; - QWeakPointer m_manager; - QListWidgetItem *m_editedRule; - Rss::DownloadRuleList *m_ruleList; - Rss::DownloadRuleList *m_editableRuleList; + Ui::AutomatedRssDownloader *m_ui; + QListWidgetItem *m_currentRuleItem; + QShortcut *m_editHotkey; + QShortcut *m_deleteHotkey; + QSet> m_treeListEntries; + RSS::AutoDownloadRule m_currentRule; + QHash m_itemsByRuleName; QRegularExpression *m_episodeRegex; - QShortcut *editHotkey; - QShortcut *deleteHotkey; - QSet> m_treeListEntries; }; #endif // AUTOMATEDRSSDOWNLOADER_H diff --git a/src/gui/rss/automatedrssdownloader.ui b/src/gui/rss/automatedrssdownloader.ui index c99e950fd..6c9db0cdf 100644 --- a/src/gui/rss/automatedrssdownloader.ui +++ b/src/gui/rss/automatedrssdownloader.ui @@ -7,7 +7,7 @@ 0 0 816 - 537 + 523 @@ -15,20 +15,31 @@ - + - 75 - true + true + + color: red; + - Enable Automated RSS Downloader + Auto downloading of RSS torrents is disabled now! You can enable it in application settings. + + + true + + + 0 + 0 + + Qt::Horizontal @@ -249,12 +260,12 @@ - - Disabled - true + + Disabled + days @@ -374,6 +385,9 @@ + + false + &Import... @@ -381,6 +395,9 @@ + + false + &Export... diff --git a/src/gui/rss/feedlistwidget.cpp b/src/gui/rss/feedlistwidget.cpp index 54251ac02..bf67f3c88 100644 --- a/src/gui/rss/feedlistwidget.cpp +++ b/src/gui/rss/feedlistwidget.cpp @@ -1,6 +1,7 @@ /* - * Bittorrent Client using Qt4 and libtorrent. - * Copyright (C) 2010 Christophe Dumez + * Bittorrent Client using Qt and libtorrent. + * Copyright (C) 2017 Vladimir Golovnev + * 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 @@ -24,213 +25,241 @@ * 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 "base/rss/rssmanager.h" -#include "base/rss/rssfolder.h" -#include "base/rss/rssfeed.h" -#include "guiiconprovider.h" #include "feedlistwidget.h" -FeedListWidget::FeedListWidget(QWidget *parent, const Rss::ManagerPtr& rssmanager) +#include +#include +#include + +#include "base/rss/rss_article.h" +#include "base/rss/rss_feed.h" +#include "base/rss/rss_folder.h" +#include "base/rss/rss_session.h" +#include "guiiconprovider.h" + +FeedListWidget::FeedListWidget(QWidget *parent) : QTreeWidget(parent) - , m_rssManager(rssmanager) - , m_currentFeed(nullptr) { - setContextMenuPolicy(Qt::CustomContextMenu); - setDragDropMode(QAbstractItemView::InternalMove); - setSelectionMode(QAbstractItemView::ExtendedSelection); - setColumnCount(1); - headerItem()->setText(0, tr("RSS feeds")); - m_unreadStickyItem = new QTreeWidgetItem(this); - m_unreadStickyItem->setText(0, tr("Unread") + QString::fromUtf8(" (") + QString::number(rssmanager->rootFolder()->unreadCount()) + QString(")")); - m_unreadStickyItem->setData(0,Qt::DecorationRole, GuiIconProvider::instance()->getIcon("mail-folder-inbox")); - itemAdded(m_unreadStickyItem, rssmanager->rootFolder()); - connect(this, SIGNAL(currentItemChanged(QTreeWidgetItem*,QTreeWidgetItem*)), SLOT(updateCurrentFeed(QTreeWidgetItem*))); - setCurrentItem(m_unreadStickyItem); + setContextMenuPolicy(Qt::CustomContextMenu); + setDragDropMode(QAbstractItemView::InternalMove); + setSelectionMode(QAbstractItemView::ExtendedSelection); + setColumnCount(1); + headerItem()->setText(0, tr("RSS feeds")); + + connect(RSS::Session::instance(), &RSS::Session::itemAdded, this, &FeedListWidget::handleItemAdded); + connect(RSS::Session::instance(), &RSS::Session::feedStateChanged, this, &FeedListWidget::handleFeedStateChanged); + connect(RSS::Session::instance(), &RSS::Session::feedIconLoaded, this, &FeedListWidget::handleFeedIconLoaded); + connect(RSS::Session::instance(), &RSS::Session::itemPathChanged, this, &FeedListWidget::handleItemPathChanged); + connect(RSS::Session::instance(), &RSS::Session::itemAboutToBeRemoved, this, &FeedListWidget::handleItemAboutToBeRemoved); + + m_rssToTreeItemMapping[RSS::Session::instance()->rootFolder()] = invisibleRootItem(); + + m_unreadStickyItem = new QTreeWidgetItem(this); + m_unreadStickyItem->setData(0, Qt::UserRole, reinterpret_cast(RSS::Session::instance()->rootFolder())); + m_unreadStickyItem->setText(0, tr("Unread (%1)").arg(RSS::Session::instance()->rootFolder()->unreadCount())); + m_unreadStickyItem->setData(0, Qt::DecorationRole, GuiIconProvider::instance()->getIcon("mail-folder-inbox")); + + connect(RSS::Session::instance()->rootFolder(), &RSS::Item::unreadCountChanged, this, &FeedListWidget::handleItemUnreadCountChanged); + + setSortingEnabled(false); + fill(nullptr, RSS::Session::instance()->rootFolder()); + setSortingEnabled(true); + +// setCurrentItem(m_unreadStickyItem); } -FeedListWidget::~FeedListWidget() { - delete m_unreadStickyItem; +FeedListWidget::~FeedListWidget() +{ + delete m_unreadStickyItem; } -void FeedListWidget::itemAdded(QTreeWidgetItem *item, const Rss::FilePtr& file) { - m_rssMapping[item] = file; - if (Rss::FeedPtr feed = qSharedPointerDynamicCast(file)) { - m_feedsItems[feed->id()] = item; - } +void FeedListWidget::handleItemAdded(RSS::Item *rssItem) +{ + auto parentItem = m_rssToTreeItemMapping.value( + RSS::Session::instance()->itemByPath(RSS::Item::parentPath(rssItem->path()))); + createItem(rssItem, parentItem); } -void FeedListWidget::itemAboutToBeRemoved(QTreeWidgetItem *item) { - Rss::FilePtr file = m_rssMapping.take(item); - if (Rss::FeedPtr feed = qSharedPointerDynamicCast(file)) { - m_feedsItems.remove(feed->id()); - } else if (Rss::FolderPtr folder = qSharedPointerDynamicCast(file)) { - Rss::FeedList feeds = folder->getAllFeeds(); - foreach (const Rss::FeedPtr& feed, feeds) { - m_feedsItems.remove(feed->id()); - } - } -} +void FeedListWidget::handleFeedStateChanged(RSS::Feed *feed) +{ + QTreeWidgetItem *item = m_rssToTreeItemMapping.value(feed); + Q_ASSERT(item); -bool FeedListWidget::hasFeed(const QString &url) const { - return m_feedsItems.contains(QUrl(url).toString()); -} - -QList FeedListWidget::getAllFeedItems() const { - return m_feedsItems.values(); -} - -QTreeWidgetItem* FeedListWidget::stickyUnreadItem() const { - return m_unreadStickyItem; -} - -QStringList FeedListWidget::getItemPath(QTreeWidgetItem* item) const { - QStringList path; - if (item) { - if (item->parent()) - path << getItemPath(item->parent()); - path.append(getRSSItem(item)->id()); - } - return path; -} - -QList FeedListWidget::getAllOpenFolders(QTreeWidgetItem *parent) const { - QList open_folders; - int nbChildren; - if (parent) - nbChildren = parent->childCount(); - else - nbChildren = topLevelItemCount(); - for (int i=0; ichild(i); + QIcon icon; + if (feed->isLoading()) + icon = QIcon(QStringLiteral(":/icons/loading.png")); + else if (feed->hasError()) + icon = GuiIconProvider::instance()->getIcon(QStringLiteral("unavailable")); + else if (!feed->iconPath().isEmpty()) + icon = QIcon(feed->iconPath()); else - item = topLevelItem(i); - if (isFolder(item) && item->isExpanded()) { - QList open_subfolders = getAllOpenFolders(item); - if (!open_subfolders.empty()) { - open_folders << open_subfolders; - } else { - open_folders << item; - } - } - } - return open_folders; + icon = GuiIconProvider::instance()->getIcon(QStringLiteral("application-rss+xml")); + item->setData(0, Qt::DecorationRole, icon); } -QList FeedListWidget::getAllFeedItems(QTreeWidgetItem* folder) { - QList feeds; - const int nbChildren = folder->childCount(); - for (int i=0; ichild(i); - if (isFeed(item)) { - feeds << item; - } else { - feeds << getAllFeedItems(item); +void FeedListWidget::handleFeedIconLoaded(RSS::Feed *feed) +{ + if (!feed->isLoading() && !feed->hasError()) { + QTreeWidgetItem *item = m_rssToTreeItemMapping.value(feed); + Q_ASSERT(item); + + item->setData(0, Qt::DecorationRole, QIcon(feed->iconPath())); } - } - return feeds; } -Rss::FilePtr FeedListWidget::getRSSItem(QTreeWidgetItem *item) const { - return m_rssMapping.value(item, Rss::FilePtr()); +void FeedListWidget::handleItemUnreadCountChanged(RSS::Item *rssItem) +{ + if (rssItem == RSS::Session::instance()->rootFolder()) { + m_unreadStickyItem->setText(0, tr("Unread (%1)").arg(RSS::Session::instance()->rootFolder()->unreadCount())); + } + else { + QTreeWidgetItem *item = mapRSSItem(rssItem); + Q_ASSERT(item); + item->setData(0, Qt::DisplayRole, QString("%1 (%2)").arg(rssItem->name()).arg(rssItem->unreadCount())); + } +} + +void FeedListWidget::handleItemPathChanged(RSS::Item *rssItem) +{ + QTreeWidgetItem *item = mapRSSItem(rssItem); + Q_ASSERT(item); + + item->setData(0, Qt::DisplayRole, QString("%1 (%2)").arg(rssItem->name()).arg(rssItem->unreadCount())); + + RSS::Item *parentRssItem = RSS::Session::instance()->itemByPath(RSS::Item::parentPath(rssItem->path())); + QTreeWidgetItem *parentItem = mapRSSItem(parentRssItem); + Q_ASSERT(parentItem); + + parentItem->addChild(item); +} + +void FeedListWidget::handleItemAboutToBeRemoved(RSS::Item *rssItem) +{ + delete m_rssToTreeItemMapping.take(rssItem); +} + +QTreeWidgetItem *FeedListWidget::stickyUnreadItem() const +{ + return m_unreadStickyItem; +} + +QList FeedListWidget::getAllOpenedFolders(QTreeWidgetItem *parent) const +{ + QList openedFolders; + int nbChildren = (parent ? parent->childCount() : topLevelItemCount()); + for (int i = 0; i < nbChildren; ++i) { + QTreeWidgetItem *item (parent ? parent->child(i) : topLevelItem(i)); + if (isFolder(item) && item->isExpanded()) { + QList openedSubfolders = getAllOpenedFolders(item); + if (!openedSubfolders.empty()) + openedFolders << openedSubfolders; + else + openedFolders << item; + } + } + return openedFolders; +} + +RSS::Item *FeedListWidget::getRSSItem(QTreeWidgetItem *item) const +{ + return reinterpret_cast(item->data(0, Qt::UserRole).value()); +} + +QTreeWidgetItem *FeedListWidget::mapRSSItem(RSS::Item *rssItem) const +{ + return m_rssToTreeItemMapping.value(rssItem); +} + +QString FeedListWidget::itemPath(QTreeWidgetItem *item) const +{ + return getRSSItem(item)->path(); } bool FeedListWidget::isFeed(QTreeWidgetItem *item) const { - return (qSharedPointerDynamicCast(m_rssMapping.value(item)) != NULL); + return qobject_cast(getRSSItem(item)); } bool FeedListWidget::isFolder(QTreeWidgetItem *item) const { - return (qSharedPointerDynamicCast(m_rssMapping.value(item)) != NULL); + return qobject_cast(getRSSItem(item)); } -QString FeedListWidget::getItemID(QTreeWidgetItem *item) const { - return m_rssMapping.value(item)->id(); +void FeedListWidget::dragMoveEvent(QDragMoveEvent *event) +{ + QTreeWidget::dragMoveEvent(event); + + QTreeWidgetItem *item = itemAt(event->pos()); + // Prohibit dropping onto global unread counter + if (item == m_unreadStickyItem) + event->ignore(); + // Prohibit dragging of global unread counter + else if (selectedItems().contains(m_unreadStickyItem)) + event->ignore(); + // Prohibit dropping onto feeds + else if (item && isFeed(item)) + event->ignore(); } -QTreeWidgetItem* FeedListWidget::getTreeItemFromUrl(const QString &url) const { - return m_feedsItems.value(url, 0); -} +void FeedListWidget::dropEvent(QDropEvent *event) +{ + QTreeWidgetItem *destFolderItem = itemAt(event->pos()); + RSS::Folder *destFolder = (destFolderItem + ? static_cast(getRSSItem(destFolderItem)) + : RSS::Session::instance()->rootFolder()); -Rss::FeedPtr FeedListWidget::getRSSItemFromUrl(const QString &url) const { - return qSharedPointerDynamicCast(getRSSItem(getTreeItemFromUrl(url))); -} - -QTreeWidgetItem* FeedListWidget::currentItem() const { - return m_currentFeed; -} - -QTreeWidgetItem* FeedListWidget::currentFeed() const { - return m_currentFeed; -} - -void FeedListWidget::updateCurrentFeed(QTreeWidgetItem* new_item) { - if (!new_item) return; - if (!m_rssMapping.contains(new_item)) return; - if (isFeed(new_item) || new_item == m_unreadStickyItem) - m_currentFeed = new_item; -} - -void FeedListWidget::dragMoveEvent(QDragMoveEvent * event) { - QTreeWidget::dragMoveEvent(event); - - QTreeWidgetItem *item = itemAt(event->pos()); - // Prohibit dropping onto global unread counter - if (item == m_unreadStickyItem) { - event->ignore(); - return; - } - // Prohibit dragging of global unread counter - if (selectedItems().contains(m_unreadStickyItem)) { - event->ignore(); - return; - } - // Prohibit dropping onto feeds - if (item && isFeed(item)) { - event->ignore(); - return; - } -} - -void FeedListWidget::dropEvent(QDropEvent *event) { - qDebug("dropEvent"); - QList folders_altered; - QTreeWidgetItem *dest_folder_item = itemAt(event->pos()); - Rss::FolderPtr dest_folder; - if (dest_folder_item) { - dest_folder = qSharedPointerCast(getRSSItem(dest_folder_item)); - folders_altered << dest_folder_item; - } else { - dest_folder = m_rssManager->rootFolder(); - } - QList src_items = selectedItems(); - // Check if there is not going to overwrite another file - foreach (QTreeWidgetItem *src_item, src_items) { - Rss::FilePtr file = getRSSItem(src_item); - if (dest_folder->hasChild(file->id())) { - QTreeWidget::dropEvent(event); - return; + // move as much items as possible + foreach (QTreeWidgetItem *srcItem, selectedItems()) { + auto rssItem = getRSSItem(srcItem); + RSS::Session::instance()->moveItem(rssItem, RSS::Item::joinPath(destFolder->path(), rssItem->name())); + } + + QTreeWidget::dropEvent(event); + if (destFolderItem) + destFolderItem->setExpanded(true); +} + +QTreeWidgetItem *FeedListWidget::createItem(RSS::Item *rssItem, QTreeWidgetItem *parentItem) +{ + QTreeWidgetItem *item = new QTreeWidgetItem; + item->setData(0, Qt::DisplayRole, QString("%1 (%2)").arg(rssItem->name()).arg(rssItem->unreadCount())); + item->setData(0, Qt::UserRole, reinterpret_cast(rssItem)); + m_rssToTreeItemMapping[rssItem] = item; + + QIcon icon; + if (auto feed = qobject_cast(rssItem)) { + if (feed->isLoading()) + icon = QIcon(QStringLiteral(":/icons/loading.png")); + else if (feed->hasError()) + icon = GuiIconProvider::instance()->getIcon(QStringLiteral("unavailable")); + else if (!feed->iconPath().isEmpty()) + icon = QIcon(feed->iconPath()); + else + icon = GuiIconProvider::instance()->getIcon(QStringLiteral("application-rss+xml")); + } + else { + icon = GuiIconProvider::instance()->getIcon("inode-directory"); + } + item->setData(0, Qt::DecorationRole, icon); + + connect(rssItem, &RSS::Item::unreadCountChanged, this, &FeedListWidget::handleItemUnreadCountChanged); + + if (!parentItem || (parentItem == m_unreadStickyItem)) + addTopLevelItem(item); + else + parentItem->addChild(item); + + return item; +} + +void FeedListWidget::fill(QTreeWidgetItem *parent, RSS::Folder *rssParent) +{ + foreach (auto rssItem, rssParent->items()) { + QTreeWidgetItem *item = createItem(rssItem, parent); + // Recursive call if this is a folder. + if (auto folder = qobject_cast(rssItem)) + fill(item, folder); } - } - // Proceed with the move - foreach (QTreeWidgetItem *src_item, src_items) { - QTreeWidgetItem *parent_folder = src_item->parent(); - if (parent_folder && !folders_altered.contains(parent_folder)) - folders_altered << parent_folder; - // Actually move the file - Rss::FilePtr file = getRSSItem(src_item); - m_rssManager->moveFile(file, dest_folder); - } - QTreeWidget::dropEvent(event); - if (dest_folder_item) - dest_folder_item->setExpanded(true); - // Emit signal for update - if (!folders_altered.empty()) - emit foldersAltered(folders_altered); } diff --git a/src/gui/rss/feedlistwidget.h b/src/gui/rss/feedlistwidget.h index edc108cc3..f7667a275 100644 --- a/src/gui/rss/feedlistwidget.h +++ b/src/gui/rss/feedlistwidget.h @@ -1,6 +1,7 @@ /* - * Bittorrent Client using Qt4 and libtorrent. - * Copyright (C) 2010 Christophe Dumez + * Bittorrent Client using Qt and libtorrent. + * Copyright (C) 2017 Vladimir Golovnev + * 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 @@ -24,67 +25,54 @@ * 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 FEEDLIST_H -#define FEEDLIST_H +#ifndef FEEDLISTWIDGET_H +#define FEEDLISTWIDGET_H -#include -#include -#include -#include -#include #include -#include +#include -#include "base/rss/rssfile.h" -#include "base/rss/rssfeed.h" -#include "base/rss/rssmanager.h" +namespace RSS +{ + class Article; + class Feed; + class Folder; + class Item; +} -class FeedListWidget: public QTreeWidget { - Q_OBJECT +class FeedListWidget: public QTreeWidget +{ + Q_OBJECT public: - FeedListWidget(QWidget *parent, const Rss::ManagerPtr& rssManager); - ~FeedListWidget(); + explicit FeedListWidget(QWidget *parent); + ~FeedListWidget(); - bool hasFeed(const QString &url) const; - QList getAllFeedItems() const; - QTreeWidgetItem* stickyUnreadItem() const; - QStringList getItemPath(QTreeWidgetItem* item) const; - QList getAllOpenFolders(QTreeWidgetItem *parent=0) const; - QList getAllFeedItems(QTreeWidgetItem* folder); - Rss::FilePtr getRSSItem(QTreeWidgetItem *item) const; - bool isFeed(QTreeWidgetItem *item) const; - bool isFolder(QTreeWidgetItem *item) const; - QString getItemID(QTreeWidgetItem *item) const; - QTreeWidgetItem* getTreeItemFromUrl(const QString &url) const; - Rss::FeedPtr getRSSItemFromUrl(const QString &url) const; - QTreeWidgetItem* currentItem() const; - QTreeWidgetItem* currentFeed() const; - -public slots: - void itemAdded(QTreeWidgetItem *item, const Rss::FilePtr& file); - void itemAboutToBeRemoved(QTreeWidgetItem *item); - -signals: - void foldersAltered(const QList &folders); + QTreeWidgetItem *stickyUnreadItem() const; + QList getAllOpenedFolders(QTreeWidgetItem *parent = nullptr) const; + RSS::Item *getRSSItem(QTreeWidgetItem *item) const; + QTreeWidgetItem *mapRSSItem(RSS::Item *rssItem) const; + QString itemPath(QTreeWidgetItem *item) const; + bool isFeed(QTreeWidgetItem *item) const; + bool isFolder(QTreeWidgetItem *item) const; private slots: - void updateCurrentFeed(QTreeWidgetItem* new_item); - -protected: - void dragMoveEvent(QDragMoveEvent * event); - void dropEvent(QDropEvent *event); + void handleItemAdded(RSS::Item *rssItem); + void handleFeedStateChanged(RSS::Feed *feed); + void handleFeedIconLoaded(RSS::Feed *feed); + void handleItemUnreadCountChanged(RSS::Item *rssItem); + void handleItemPathChanged(RSS::Item *rssItem); + void handleItemAboutToBeRemoved(RSS::Item *rssItem); private: - Rss::ManagerPtr m_rssManager; - QHash m_rssMapping; - QHash m_feedsItems; - QTreeWidgetItem* m_currentFeed; - QTreeWidgetItem *m_unreadStickyItem; + void dragMoveEvent(QDragMoveEvent *event); + void dropEvent(QDropEvent *event); + QTreeWidgetItem *createItem(RSS::Item *rssItem, QTreeWidgetItem *parentItem = nullptr); + void fill(QTreeWidgetItem *parent, RSS::Folder *rssParent); + + QHash m_rssToTreeItemMapping; + QTreeWidgetItem *m_unreadStickyItem; }; -#endif // FEEDLIST_H +#endif // FEEDLISTWIDGET_H diff --git a/src/gui/rss/rss.pri b/src/gui/rss/rss.pri deleted file mode 100644 index 76975d4c0..000000000 --- a/src/gui/rss/rss.pri +++ /dev/null @@ -1,17 +0,0 @@ -INCLUDEPATH += $$PWD - -HEADERS += $$PWD/rss_imp.h \ - $$PWD/rsssettingsdlg.h \ - $$PWD/feedlistwidget.h \ - $$PWD/automatedrssdownloader.h \ - $$PWD/htmlbrowser.h - -SOURCES += $$PWD/rss_imp.cpp \ - $$PWD/rsssettingsdlg.cpp \ - $$PWD/feedlistwidget.cpp \ - $$PWD/automatedrssdownloader.cpp \ - $$PWD/htmlbrowser.cpp - -FORMS += $$PWD/rss.ui \ - $$PWD/rsssettingsdlg.ui \ - $$PWD/automatedrssdownloader.ui diff --git a/src/gui/rss/rss_imp.cpp b/src/gui/rss/rss_imp.cpp deleted file mode 100644 index 911a17a76..000000000 --- a/src/gui/rss/rss_imp.cpp +++ /dev/null @@ -1,806 +0,0 @@ -/* - * Bittorrent Client using Qt4 and libtorrent. - * Copyright (C) 2006 Christophe Dumez, Arnaud Demaiziere - * - * 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 "rss_imp.h" - -#include -#include -#include -#include -#include -#include -#include -#include - -#include "feedlistwidget.h" -#include "base/bittorrent/session.h" -#include "base/net/downloadmanager.h" -#include "base/preferences.h" -#include "rsssettingsdlg.h" -#include "base/rss/rssmanager.h" -#include "base/rss/rssfolder.h" -#include "base/rss/rssarticle.h" -#include "base/rss/rssfeed.h" -#include "automatedrssdownloader.h" -#include "guiiconprovider.h" -#include "autoexpandabledialog.h" -#include "addnewtorrentdialog.h" - -#include "ui_rss.h" - -namespace Article -{ - enum ArticleRoles - { - TitleRole = Qt::DisplayRole, - IconRole = Qt::DecorationRole, - ColorRole = Qt::ForegroundRole, - IdRole = Qt::UserRole + 1, - FeedUrlRole = Qt::UserRole + 2 - }; -} - -// display a right-click menu -void RSSImp::displayRSSListMenu(const QPoint &pos) -{ - if (!m_feedList->indexAt(pos).isValid()) - // No item under the mouse, clear selection - m_feedList->clearSelection(); - QMenu myRSSListMenu(this); - QList selectedItems = m_feedList->selectedItems(); - if (selectedItems.size() > 0) { - myRSSListMenu.addAction(m_ui->actionUpdate); - myRSSListMenu.addAction(m_ui->actionMark_items_read); - myRSSListMenu.addSeparator(); - if (selectedItems.size() == 1) { - if (m_feedList->getRSSItem(selectedItems.first()) != m_rssManager->rootFolder()) { - myRSSListMenu.addAction(m_ui->actionRename); - myRSSListMenu.addAction(m_ui->actionDelete); - myRSSListMenu.addSeparator(); - if (m_feedList->isFolder(selectedItems.first())) - myRSSListMenu.addAction(m_ui->actionNew_folder); - } - } - else { - myRSSListMenu.addAction(m_ui->actionDelete); - myRSSListMenu.addSeparator(); - } - myRSSListMenu.addAction(m_ui->actionNew_subscription); - if (m_feedList->isFeed(selectedItems.first())) { - myRSSListMenu.addSeparator(); - myRSSListMenu.addAction(m_ui->actionCopy_feed_URL); - } - } - else { - myRSSListMenu.addAction(m_ui->actionNew_subscription); - myRSSListMenu.addAction(m_ui->actionNew_folder); - myRSSListMenu.addSeparator(); - myRSSListMenu.addAction(m_ui->actionUpdate_all_feeds); - } - myRSSListMenu.exec(QCursor::pos()); -} - -void RSSImp::displayItemsListMenu(const QPoint &) -{ - QMenu myItemListMenu(this); - QList selectedItems = m_ui->listArticles->selectedItems(); - if (selectedItems.size() <= 0) - return; - - bool hasTorrent = false; - bool hasLink = false; - foreach (const QListWidgetItem *item, selectedItems) { - if (!item) continue; - Rss::FeedPtr feed = m_feedList->getRSSItemFromUrl(item->data(Article::FeedUrlRole).toString()); - if (!feed) continue; - Rss::ArticlePtr article = feed->getItem(item->data(Article::IdRole).toString()); - if (!article) continue; - - if (!article->torrentUrl().isEmpty()) - hasTorrent = true; - if (!article->link().isEmpty()) - hasLink = true; - if (hasTorrent && hasLink) - break; - } - if (hasTorrent) - myItemListMenu.addAction(m_ui->actionDownload_torrent); - if (hasLink) - myItemListMenu.addAction(m_ui->actionOpen_news_URL); - if (hasTorrent || hasLink) - myItemListMenu.exec(QCursor::pos()); -} - -void RSSImp::askNewFolder() -{ - QTreeWidgetItem *parent_item = 0; - Rss::FolderPtr rss_parent; - if (m_feedList->selectedItems().size() > 0) { - parent_item = m_feedList->selectedItems().at(0); - rss_parent = qSharedPointerDynamicCast(m_feedList->getRSSItem(parent_item)); - Q_ASSERT(rss_parent); - } - else { - rss_parent = m_rssManager->rootFolder(); - } - bool ok; - QString new_name = AutoExpandableDialog::getText(this, tr("Please choose a folder name"), tr("Folder name:"), QLineEdit::Normal, tr("New folder"), &ok); - if (!ok || rss_parent->hasChild(new_name)) - return; - - Rss::FolderPtr newFolder(new Rss::Folder(new_name)); - rss_parent->addFile(newFolder); - QTreeWidgetItem *folderItem = createFolderListItem(newFolder); - if (parent_item) - parent_item->addChild(folderItem); - else - m_feedList->addTopLevelItem(folderItem); - // Notify TreeWidget - m_feedList->itemAdded(folderItem, newFolder); - // Expand parent folder to display new folder - if (parent_item) - parent_item->setExpanded(true); - m_feedList->setCurrentItem(folderItem); - m_rssManager->saveStreamList(); -} - -// add a stream by a button -void RSSImp::on_newFeedButton_clicked() -{ - // Determine parent folder for new feed - QTreeWidgetItem *parent_item = 0; - QList selected_items = m_feedList->selectedItems(); - if (!selected_items.empty()) { - parent_item = selected_items.first(); - // Consider the case where the user clicked on Unread item - if (parent_item == m_feedList->stickyUnreadItem()) - parent_item = 0; - else - if (!m_feedList->isFolder(parent_item)) - parent_item = parent_item->parent(); - } - Rss::FolderPtr rss_parent; - if (parent_item) - rss_parent = qSharedPointerCast(m_feedList->getRSSItem(parent_item)); - else - rss_parent = m_rssManager->rootFolder(); - // Ask for feed URL - bool ok; - QString clip_txt = qApp->clipboard()->text(); - QString default_url = "http://"; - if (clip_txt.startsWith("http://", Qt::CaseInsensitive) || clip_txt.startsWith("https://", Qt::CaseInsensitive) || clip_txt.startsWith("ftp://", Qt::CaseInsensitive)) - default_url = clip_txt; - - QString newUrl = AutoExpandableDialog::getText(this, tr("Please type a RSS stream URL"), tr("Stream URL:"), QLineEdit::Normal, default_url, &ok); - if (!ok) - return; - - newUrl = newUrl.trimmed(); - if (newUrl.isEmpty()) - return; - - if (m_feedList->hasFeed(newUrl)) { - QMessageBox::warning(this, "qBittorrent", - tr("This RSS feed is already in the list."), - QMessageBox::Ok); - return; - } - - Rss::FeedPtr stream(new Rss::Feed(newUrl, m_rssManager.data())); - rss_parent->addFile(stream); - // Create TreeWidget item - QTreeWidgetItem *item = createFolderListItem(stream); - if (parent_item) - parent_item->addChild(item); - else - m_feedList->addTopLevelItem(item); - // Notify TreeWidget - m_feedList->itemAdded(item, stream); - // Expand parent folder to display new feed - if (parent_item) - parent_item->setExpanded(true); - m_feedList->setCurrentItem(item); - m_rssManager->saveStreamList(); -} - -// delete a stream by a button -void RSSImp::deleteSelectedItems() -{ - QList selectedItems = m_feedList->selectedItems(); - if (selectedItems.isEmpty()) - return; - if ((selectedItems.size() == 1) && (selectedItems.first() == m_feedList->stickyUnreadItem())) - return; - - QMessageBox::StandardButton answer = QMessageBox::question(this, tr("Deletion confirmation"), - tr("Are you sure you want to delete the selected RSS feeds?"), - QMessageBox::Yes | QMessageBox::No, QMessageBox::No); - if (answer == QMessageBox::No) - return; - - QList deleted; - - foreach (QTreeWidgetItem *item, selectedItems) { - if (item == m_feedList->stickyUnreadItem()) - continue; - Rss::FilePtr rss_item = m_feedList->getRSSItem(item); - QTreeWidgetItem *parent = item->parent(); - // Notify TreeWidget - m_feedList->itemAboutToBeRemoved(item); - // Actually delete the item - rss_item->parentFolder()->removeChild(rss_item->id()); - deleted << rss_item->id(); - delete item; - // Update parents count - while (parent && (parent != m_feedList->invisibleRootItem())) { - updateItemInfos(parent); - parent = parent->parent(); - } - } - m_rssManager->saveStreamList(); - - foreach (const QString &feed_id, deleted) - m_rssManager->forwardFeedInfosChanged(feed_id, "", 0); - - // Update Unread items - updateItemInfos(m_feedList->stickyUnreadItem()); - if (m_feedList->currentItem() == m_feedList->stickyUnreadItem()) - populateArticleList(m_feedList->stickyUnreadItem()); -} - -void RSSImp::loadFoldersOpenState() -{ - QStringList open_folders = Preferences::instance()->getRssOpenFolders(); - foreach (const QString &var_path, open_folders) { - QStringList path = var_path.split("\\"); - QTreeWidgetItem *parent = 0; - foreach (const QString &name, path) { - int nbChildren = 0; - if (parent) - nbChildren = parent->childCount(); - else - nbChildren = m_feedList->topLevelItemCount(); - for (int i = 0; i < nbChildren; ++i) { - QTreeWidgetItem *child; - if (parent) - child = parent->child(i); - else - child = m_feedList->topLevelItem(i); - if (m_feedList->getRSSItem(child)->id() == name) { - parent = child; - parent->setExpanded(true); - qDebug("expanding folder %s", qPrintable(name)); - break; - } - } - } - } -} - -void RSSImp::saveFoldersOpenState() -{ - QStringList open_folders; - QList items = m_feedList->getAllOpenFolders(); - foreach (QTreeWidgetItem *item, items) { - QString path = m_feedList->getItemPath(item).join("\\"); - qDebug("saving open folder: %s", qPrintable(path)); - open_folders << path; - } - Preferences::instance()->setRssOpenFolders(open_folders); -} - -// refresh all streams by a button -void RSSImp::refreshAllFeeds() -{ - foreach (QTreeWidgetItem *item, m_feedList->getAllFeedItems()) - item->setData(0, Qt::DecorationRole, QVariant(QIcon(":/icons/loading.png"))); - m_rssManager->refresh(); -} - -void RSSImp::downloadSelectedTorrents() -{ - QList selected_items = m_ui->listArticles->selectedItems(); - if (selected_items.size() <= 0) - return; - foreach (QListWidgetItem *item, selected_items) { - if (!item) continue; - Rss::FeedPtr feed = m_feedList->getRSSItemFromUrl(item->data(Article::FeedUrlRole).toString()); - if (!feed) continue; - Rss::ArticlePtr article = feed->getItem(item->data(Article::IdRole).toString()); - if (!article) continue; - - // Mark as read - article->markAsRead(); - item->setData(Article::ColorRole, QVariant(QColor("grey"))); - item->setData(Article::IconRole, QVariant(QIcon(":/icons/sphere.png"))); - - if (article->torrentUrl().isEmpty()) - continue; - if (AddNewTorrentDialog::isEnabled()) - AddNewTorrentDialog::show(article->torrentUrl()); - else - BitTorrent::Session::instance()->addTorrent(article->torrentUrl()); - } - // Decrement feed nb unread news - updateItemInfos(m_feedList->stickyUnreadItem()); - updateItemInfos(m_feedList->getTreeItemFromUrl(selected_items.first()->data(Article::FeedUrlRole).toString())); -} - -// open the url of the selected RSS articles in the Web browser -void RSSImp::openSelectedArticlesUrls() -{ - QList selected_items = m_ui->listArticles->selectedItems(); - if (selected_items.size() <= 0) - return; - foreach (QListWidgetItem *item, selected_items) { - if (!item) continue; - Rss::FeedPtr feed = m_feedList->getRSSItemFromUrl(item->data(Article::FeedUrlRole).toString()); - if (!feed) continue; - Rss::ArticlePtr article = feed->getItem(item->data(Article::IdRole).toString()); - if (!article) continue; - - // Mark as read - article->markAsRead(); - item->setData(Article::ColorRole, QVariant(QColor("grey"))); - item->setData(Article::IconRole, QVariant(QIcon(":/icons/sphere.png"))); - - const QString link = article->link(); - if (!link.isEmpty()) - QDesktopServices::openUrl(QUrl(link)); - } - // Decrement feed nb unread news - updateItemInfos(m_feedList->stickyUnreadItem()); - updateItemInfos(m_feedList->getTreeItemFromUrl(selected_items.first()->data(Article::FeedUrlRole).toString())); -} - -// right-click on stream : give it an alias -void RSSImp::renameSelectedRssFile() -{ - QList selectedItems = m_feedList->selectedItems(); - if (selectedItems.size() != 1) - return; - QTreeWidgetItem *item = selectedItems.first(); - if (item == m_feedList->stickyUnreadItem()) - return; - Rss::FilePtr rss_item = m_feedList->getRSSItem(item); - bool ok; - QString newName; - do { - newName = AutoExpandableDialog::getText(this, tr("Please choose a new name for this RSS feed"), tr("New feed name:"), QLineEdit::Normal, m_feedList->getRSSItem(item)->displayName(), &ok); - // Check if name is already taken - if (ok) { - if (rss_item->parentFolder()->hasChild(newName)) { - QMessageBox::warning(0, tr("Name already in use"), tr("This name is already used by another item, please choose another one.")); - ok = false; - } - } - else { - return; - } - } while (!ok); - // Rename item - rss_item->rename(newName); - m_rssManager->saveStreamList(); - // Update TreeWidget - updateItemInfos(item); -} - -// right-click on stream : refresh it -void RSSImp::refreshSelectedItems() -{ - QList selectedItems = m_feedList->selectedItems(); - foreach (QTreeWidgetItem *item, selectedItems) { - Rss::FilePtr file = m_feedList->getRSSItem(item); - // Update icons - if (item == m_feedList->stickyUnreadItem()) { - refreshAllFeeds(); - return; - } - else { - if (!file->refresh()) - continue; - // Update UI - if (qSharedPointerDynamicCast(file)) { - item->setData(0, Qt::DecorationRole, QVariant(QIcon(":/icons/loading.png"))); - } - else if (qSharedPointerDynamicCast(file)) { - // Update feeds in the folder - foreach (QTreeWidgetItem *feed, m_feedList->getAllFeedItems(item)) - feed->setData(0, Qt::DecorationRole, QVariant(QIcon(":/icons/loading.png"))); - } - } - } -} - -void RSSImp::copySelectedFeedsURL() -{ - QStringList URLs; - QList selectedItems = m_feedList->selectedItems(); - QTreeWidgetItem *item; - foreach (item, selectedItems) - if (m_feedList->isFeed(item)) - URLs << m_feedList->getItemID(item); - qApp->clipboard()->setText(URLs.join("\n")); -} - -void RSSImp::on_markReadButton_clicked() -{ - QList selectedItems = m_feedList->selectedItems(); - foreach (QTreeWidgetItem *item, selectedItems) { - Rss::FilePtr rss_item = m_feedList->getRSSItem(item); - Q_ASSERT(rss_item); - rss_item->markAsRead(); - updateItemInfos(item); - } - // Update article list - if (!selectedItems.isEmpty()) - populateArticleList(m_feedList->currentItem()); -} - -QTreeWidgetItem *RSSImp::createFolderListItem(const Rss::FilePtr &rssFile) -{ - Q_ASSERT(rssFile); - QTreeWidgetItem *item = new QTreeWidgetItem; - item->setData(0, Qt::DisplayRole, QVariant(rssFile->displayName() + QString::fromUtf8(" (") + QString::number(rssFile->unreadCount()) + QString(")"))); - item->setData(0, Qt::DecorationRole, QIcon(rssFile->iconPath())); - - return item; -} - -void RSSImp::fillFeedsList(QTreeWidgetItem *parent, const Rss::FolderPtr &rss_parent) -{ - QList children; - if (parent) - children = rss_parent->getContent(); - else - children = m_rssManager->rootFolder()->getContent(); - foreach (const Rss::FilePtr &rssFile, children) { - QTreeWidgetItem *item = createFolderListItem(rssFile); - Q_ASSERT(item); - if (parent) - parent->addChild(item); - else - m_feedList->addTopLevelItem(item); - - // Notify TreeWidget of item addition - m_feedList->itemAdded(item, rssFile); - - // Recursive call if this is a folder. - if (Rss::FolderPtr folder = qSharedPointerDynamicCast(rssFile)) - fillFeedsList(item, folder); - } -} - -QListWidgetItem *RSSImp::createArticleListItem(const Rss::ArticlePtr &article) -{ - Q_ASSERT(article); - QListWidgetItem *item = new QListWidgetItem; - - item->setData(Article::TitleRole, article->title()); - item->setData(Article::FeedUrlRole, article->parent()->url()); - item->setData(Article::IdRole, article->guid()); - if (article->isRead()) { - item->setData(Article::ColorRole, QVariant(QColor("grey"))); - item->setData(Article::IconRole, QVariant(QIcon(":/icons/sphere.png"))); - } - else { - item->setData(Article::ColorRole, QVariant(QColor("blue"))); - item->setData(Article::IconRole, QVariant(QIcon(":/icons/sphere2.png"))); - } - - return item; -} - -// fills the newsList -void RSSImp::populateArticleList(QTreeWidgetItem *item) -{ - if (!item) { - m_ui->listArticles->clear(); - return; - } - - Rss::FilePtr rss_item = m_feedList->getRSSItem(item); - if (!rss_item) - return; - - // Clear the list first - m_ui->textBrowser->clear(); - m_currentArticle = 0; - m_ui->listArticles->clear(); - - qDebug("Getting the list of news"); - Rss::ArticleList articles; - if (rss_item == m_rssManager->rootFolder()) - articles = rss_item->unreadArticleListByDateDesc(); - else - articles = rss_item->articleListByDateDesc(); - - qDebug("Got the list of news"); - foreach (const Rss::ArticlePtr &article, articles) { - QListWidgetItem *articleItem = createArticleListItem(article); - m_ui->listArticles->addItem(articleItem); - } - qDebug("Added all news to the GUI"); -} - -// display a news -void RSSImp::refreshTextBrowser() -{ - QList selection = m_ui->listArticles->selectedItems(); - if (selection.empty()) return; - QListWidgetItem *item = selection.first(); - Q_ASSERT(item); - if (item == m_currentArticle) return; - m_currentArticle = item; - - Rss::FeedPtr feed = m_feedList->getRSSItemFromUrl(item->data(Article::FeedUrlRole).toString()); - if (!feed) return; - Rss::ArticlePtr article = feed->getItem(item->data(Article::IdRole).toString()); - if (!article) return; - QString html; - html += "
"; - html += "
" + article->title() + "
"; - if (article->date().isValid()) - html += "
" + tr("Date: ") + "" + article->date().toLocalTime().toString(Qt::SystemLocaleLongDate) + "
"; - if (!article->author().isEmpty()) - html += "
" + tr("Author: ") + "" + article->author() + "
"; - html += "
"; - html += "
"; - if (Qt::mightBeRichText(article->description())) { - html += article->description(); - } - else { - QString description = article->description(); - QRegExp rx; - // If description is plain text, replace BBCode tags with HTML and wrap everything in
 so it looks nice
-        rx.setMinimal(true);
-        rx.setCaseSensitivity(Qt::CaseInsensitive);
-
-        rx.setPattern("\\[img\\](.+)\\[/img\\]");
-        description = description.replace(rx, "");
-
-        rx.setPattern("\\[url=(\")?(.+)\\1\\]");
-        description = description.replace(rx, "");
-        description = description.replace("[/url]", "", Qt::CaseInsensitive);
-
-        rx.setPattern("\\[(/)?([bius])\\]");
-        description = description.replace(rx, "<\\1\\2>");
-
-        rx.setPattern("\\[color=(\")?(.+)\\1\\]");
-        description = description.replace(rx, "");
-        description = description.replace("[/color]", "", Qt::CaseInsensitive);
-
-        rx.setPattern("\\[size=(\")?(.+)\\d\\1\\]");
-        description = description.replace(rx, "");
-        description = description.replace("[/size]", "", Qt::CaseInsensitive);
-
-        html += "
" + description + "
"; - } - html += "
"; - m_ui->textBrowser->setHtml(html); - article->markAsRead(); - item->setData(Article::ColorRole, QVariant(QColor("grey"))); - item->setData(Article::IconRole, QVariant(QIcon(":/icons/sphere.png"))); - // Decrement feed nb unread news - updateItemInfos(m_feedList->stickyUnreadItem()); - updateItemInfos(m_feedList->getTreeItemFromUrl(item->data(Article::FeedUrlRole).toString())); -} - -void RSSImp::saveSlidersPosition() -{ - // Remember sliders positions - Preferences *const pref = Preferences::instance(); - pref->setRssSideSplitterState(m_ui->splitterSide->saveState()); - pref->setRssMainSplitterState(m_ui->splitterMain->saveState()); - qDebug("Splitters position saved"); -} - -void RSSImp::restoreSlidersPosition() -{ - const Preferences *const pref = Preferences::instance(); - const QByteArray stateSide = pref->getRssSideSplitterState(); - if (!stateSide.isEmpty()) - m_ui->splitterSide->restoreState(stateSide); - const QByteArray stateMain = pref->getRssMainSplitterState(); - if (!stateMain.isEmpty()) - m_ui->splitterMain->restoreState(stateMain); -} - -void RSSImp::updateItemsInfos(const QList &items) -{ - foreach (QTreeWidgetItem *item, items) - updateItemInfos(item); -} - -void RSSImp::updateItemInfos(QTreeWidgetItem *item) -{ - Rss::FilePtr rss_item = m_feedList->getRSSItem(item); - if (!rss_item) - return; - - QString name; - if (rss_item == m_rssManager->rootFolder()) { - name = tr("Unread"); - emit updateRSSCount(rss_item->unreadCount()); - } - else { - name = rss_item->displayName(); - } - item->setText(0, name + QString::fromUtf8(" (") + QString::number(rss_item->unreadCount()) + QString(")")); - // If item has a parent, update it too - if (item->parent()) - updateItemInfos(item->parent()); -} - -void RSSImp::updateFeedIcon(const QString &url, const QString &iconPath) -{ - QTreeWidgetItem *item = m_feedList->getTreeItemFromUrl(url); - - if (item) - item->setData(0, Qt::DecorationRole, QVariant(QIcon(iconPath))); -} - -void RSSImp::updateFeedInfos(const QString &url, const QString &display_name, uint nbUnread) -{ - qDebug() << Q_FUNC_INFO << display_name; - QTreeWidgetItem *item = m_feedList->getTreeItemFromUrl(url); - - if (item) { - Rss::FeedPtr stream = qSharedPointerCast(m_feedList->getRSSItem(item)); - item->setText(0, display_name + QString::fromUtf8(" (") + QString::number(nbUnread) + QString(")")); - if (!stream->isLoading()) - item->setData(0, Qt::DecorationRole, QIcon(stream->iconPath())); - // Update parent - if (item->parent()) - updateItemInfos(item->parent()); - } - - // Update Unread item - updateItemInfos(m_feedList->stickyUnreadItem()); -} - -void RSSImp::onFeedContentChanged(const QString &url) -{ - qDebug() << Q_FUNC_INFO << url; - QTreeWidgetItem *item = m_feedList->getTreeItemFromUrl(url); - // If the feed is selected, update the displayed news - if (m_feedList->currentItem() == item) - populateArticleList(item); - // Update unread items - else if (m_feedList->currentItem() == m_feedList->stickyUnreadItem()) - populateArticleList(m_feedList->stickyUnreadItem()); -} - -void RSSImp::updateRefreshInterval(uint val) -{ - m_rssManager->updateRefreshInterval(val); -} - -RSSImp::RSSImp(QWidget *parent) - : QWidget(parent) - , m_ui(new Ui::RSS()) - , m_rssManager(new Rss::Manager) -{ - m_ui->setupUi(this); - // Icons - m_ui->actionCopy_feed_URL->setIcon(GuiIconProvider::instance()->getIcon("edit-copy")); - m_ui->actionDelete->setIcon(GuiIconProvider::instance()->getIcon("edit-delete")); - m_ui->actionDownload_torrent->setIcon(GuiIconProvider::instance()->getIcon("download")); - m_ui->actionMark_items_read->setIcon(GuiIconProvider::instance()->getIcon("mail-mark-read")); - m_ui->actionNew_folder->setIcon(GuiIconProvider::instance()->getIcon("folder-new")); - m_ui->actionNew_subscription->setIcon(GuiIconProvider::instance()->getIcon("list-add")); - m_ui->actionOpen_news_URL->setIcon(GuiIconProvider::instance()->getIcon("application-x-mswinurl")); - m_ui->actionRename->setIcon(GuiIconProvider::instance()->getIcon("edit-rename")); - m_ui->actionUpdate->setIcon(GuiIconProvider::instance()->getIcon("view-refresh")); - m_ui->actionUpdate_all_feeds->setIcon(GuiIconProvider::instance()->getIcon("view-refresh")); - m_ui->newFeedButton->setIcon(GuiIconProvider::instance()->getIcon("list-add")); - m_ui->markReadButton->setIcon(GuiIconProvider::instance()->getIcon("mail-mark-read")); - m_ui->updateAllButton->setIcon(GuiIconProvider::instance()->getIcon("view-refresh")); - m_ui->rssDownloaderBtn->setIcon(GuiIconProvider::instance()->getIcon("download")); - m_ui->settingsButton->setIcon(GuiIconProvider::instance()->getIcon("configure", "preferences-system")); - - m_feedList = new FeedListWidget(m_ui->splitterSide, m_rssManager); - m_ui->splitterSide->insertWidget(0, m_feedList); - editHotkey = new QShortcut(Qt::Key_F2, m_feedList, 0, 0, Qt::WidgetShortcut); - connect(editHotkey, SIGNAL(activated()), SLOT(renameSelectedRssFile())); - connect(m_feedList, SIGNAL(doubleClicked(QModelIndex)), SLOT(renameSelectedRssFile())); - deleteHotkey = new QShortcut(QKeySequence::Delete, m_feedList, 0, 0, Qt::WidgetShortcut); - connect(deleteHotkey, SIGNAL(activated()), SLOT(deleteSelectedItems())); - - m_rssManager->loadStreamList(); - m_feedList->setSortingEnabled(false); - fillFeedsList(); - m_feedList->setSortingEnabled(true); - populateArticleList(m_feedList->currentItem()); - - loadFoldersOpenState(); - connect(m_rssManager.data(), SIGNAL(feedInfosChanged(QString,QString,unsigned int)), SLOT(updateFeedInfos(QString,QString,unsigned int))); - connect(m_rssManager.data(), SIGNAL(feedContentChanged(QString)), SLOT(onFeedContentChanged(QString))); - connect(m_rssManager.data(), SIGNAL(feedIconChanged(QString,QString)), SLOT(updateFeedIcon(QString,QString))); - - connect(m_feedList, SIGNAL(customContextMenuRequested(const QPoint&)), SLOT(displayRSSListMenu(const QPoint&))); - connect(m_ui->listArticles, SIGNAL(customContextMenuRequested(const QPoint&)), SLOT(displayItemsListMenu(const QPoint&))); - - // Feeds list actions - connect(m_ui->actionDelete, SIGNAL(triggered()), this, SLOT(deleteSelectedItems())); - connect(m_ui->actionRename, SIGNAL(triggered()), this, SLOT(renameSelectedRssFile())); - connect(m_ui->actionUpdate, SIGNAL(triggered()), this, SLOT(refreshSelectedItems())); - connect(m_ui->actionNew_folder, SIGNAL(triggered()), this, SLOT(askNewFolder())); - connect(m_ui->actionNew_subscription, SIGNAL(triggered()), this, SLOT(on_newFeedButton_clicked())); - connect(m_ui->actionUpdate_all_feeds, SIGNAL(triggered()), this, SLOT(refreshAllFeeds())); - connect(m_ui->updateAllButton, SIGNAL(clicked()), SLOT(refreshAllFeeds())); - connect(m_ui->actionCopy_feed_URL, SIGNAL(triggered()), this, SLOT(copySelectedFeedsURL())); - connect(m_ui->actionMark_items_read, SIGNAL(triggered()), this, SLOT(on_markReadButton_clicked())); - // News list actions - connect(m_ui->actionOpen_news_URL, SIGNAL(triggered()), this, SLOT(openSelectedArticlesUrls())); - connect(m_ui->actionDownload_torrent, SIGNAL(triggered()), this, SLOT(downloadSelectedTorrents())); - - connect(m_feedList, SIGNAL(currentItemChanged(QTreeWidgetItem *,QTreeWidgetItem *)), this, SLOT(populateArticleList(QTreeWidgetItem *))); - connect(m_feedList, SIGNAL(foldersAltered(QList)), this, SLOT(updateItemsInfos(QList))); - - connect(m_ui->listArticles, SIGNAL(itemSelectionChanged()), this, SLOT(refreshTextBrowser())); - connect(m_ui->listArticles, SIGNAL(itemDoubleClicked(QListWidgetItem *)), this, SLOT(downloadSelectedTorrents())); - - // Restore sliders position - restoreSlidersPosition(); - // Bind saveSliders slots - connect(m_ui->splitterMain, SIGNAL(splitterMoved(int,int)), this, SLOT(saveSlidersPosition())); - connect(m_ui->splitterSide, SIGNAL(splitterMoved(int,int)), this, SLOT(saveSlidersPosition())); - - qDebug("RSSImp constructed"); -} - -RSSImp::~RSSImp() -{ - qDebug("Deleting RSSImp..."); - saveFoldersOpenState(); - delete editHotkey; - delete deleteHotkey; - delete m_feedList; - delete m_ui; - qDebug("RSSImp deleted"); -} - -void RSSImp::on_settingsButton_clicked() -{ - RssSettingsDlg dlg(this); - if (dlg.exec()) - updateRefreshInterval(Preferences::instance()->getRSSRefreshInterval()); -} - -void RSSImp::on_rssDownloaderBtn_clicked() -{ - AutomatedRssDownloader dlg(m_rssManager, this); - dlg.exec(); - if (dlg.isRssDownloaderEnabled()) { - m_rssManager->rootFolder()->recheckRssItemsForDownload(); - refreshAllFeeds(); - } -} diff --git a/src/gui/rss/rsssettingsdlg.cpp b/src/gui/rss/rsssettingsdlg.cpp deleted file mode 100644 index 44ad9a27f..000000000 --- a/src/gui/rss/rsssettingsdlg.cpp +++ /dev/null @@ -1,61 +0,0 @@ -/* - * Bittorrent Client using Qt4 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 "rsssettingsdlg.h" -#include "ui_rsssettingsdlg.h" - -#include "base/preferences.h" -#include "base/utils/misc.h" -#include "guiiconprovider.h" - -RssSettingsDlg::RssSettingsDlg(QWidget *parent) : - QDialog(parent), - ui(new Ui::RssSettingsDlg) -{ - ui->setupUi(this); - ui->rssIcon->setPixmap(GuiIconProvider::instance()->getIcon("application-rss+xml").pixmap(Utils::Misc::largeIconSize())); - // Load settings - const Preferences* const pref = Preferences::instance(); - ui->spinRSSRefresh->setValue(pref->getRSSRefreshInterval()); - ui->spinRSSMaxArticlesPerFeed->setValue(pref->getRSSMaxArticlesPerFeed()); -} - -RssSettingsDlg::~RssSettingsDlg() -{ - qDebug("Deleting the RSS settings dialog"); - delete ui; -} - -void RssSettingsDlg::on_buttonBox_accepted() { - // Save settings - Preferences* const pref = Preferences::instance(); - pref->setRSSRefreshInterval(ui->spinRSSRefresh->value()); - pref->setRSSMaxArticlesPerFeed(ui->spinRSSMaxArticlesPerFeed->value()); -} diff --git a/src/gui/rss/rsssettingsdlg.ui b/src/gui/rss/rsssettingsdlg.ui deleted file mode 100644 index b2581f471..000000000 --- a/src/gui/rss/rsssettingsdlg.ui +++ /dev/null @@ -1,126 +0,0 @@ - - - RssSettingsDlg - - - - 0 - 0 - 415 - 123 - - - - RSS Reader Settings - - - - - - - - RSS feeds refresh interval: - - - - - - - min - - - 1 - - - 999999 - - - 5 - - - - - - - Maximum number of articles per feed: - - - - - - - 9999 - - - 100 - - - - - - - - - - - - Qt::Vertical - - - - 0 - 0 - - - - - - - - Qt::Horizontal - - - QDialogButtonBox::Cancel|QDialogButtonBox::Ok - - - - - - - - - - - buttonBox - accepted() - RssSettingsDlg - accept() - - - 248 - 254 - - - 157 - 274 - - - - - buttonBox - rejected() - RssSettingsDlg - reject() - - - 316 - 260 - - - 286 - 274 - - - - - diff --git a/src/gui/rss/rsswidget.cpp b/src/gui/rss/rsswidget.cpp new file mode 100644 index 000000000..6ffdb33fd --- /dev/null +++ b/src/gui/rss/rsswidget.cpp @@ -0,0 +1,533 @@ +/* + * Bittorrent Client using Qt and libtorrent. + * Copyright (C) 2017 Vladimir Golovnev + * Copyright (C) 2006 Christophe Dumez + * Copyright (C) 2006 Arnaud Demaiziere + * + * 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 "rsswidget.h" + +#include +#include +#include +#include +#include +#include +#include +#include + +#include "base/bittorrent/session.h" +#include "base/net/downloadmanager.h" +#include "base/preferences.h" +#include "base/rss/rss_article.h" +#include "base/rss/rss_feed.h" +#include "base/rss/rss_folder.h" +#include "base/rss/rss_session.h" +#include "base/utils/misc.h" +#include "addnewtorrentdialog.h" +#include "articlelistwidget.h" +#include "autoexpandabledialog.h" +#include "automatedrssdownloader.h" +#include "feedlistwidget.h" +#include "guiiconprovider.h" +#include "ui_rsswidget.h" + +RSSWidget::RSSWidget(QWidget *parent) + : QWidget(parent) + , m_ui(new Ui::RSSWidget) +{ + m_ui->setupUi(this); + + // Icons + m_ui->actionCopyFeedURL->setIcon(GuiIconProvider::instance()->getIcon("edit-copy")); + m_ui->actionDelete->setIcon(GuiIconProvider::instance()->getIcon("edit-delete")); + m_ui->actionDownloadTorrent->setIcon(GuiIconProvider::instance()->getIcon("download")); + m_ui->actionMarkItemsRead->setIcon(GuiIconProvider::instance()->getIcon("mail-mark-read")); + m_ui->actionNewFolder->setIcon(GuiIconProvider::instance()->getIcon("folder-new")); + m_ui->actionNewSubscription->setIcon(GuiIconProvider::instance()->getIcon("list-add")); + m_ui->actionOpenNewsURL->setIcon(GuiIconProvider::instance()->getIcon("application-x-mswinurl")); + m_ui->actionRename->setIcon(GuiIconProvider::instance()->getIcon("edit-rename")); + m_ui->actionUpdate->setIcon(GuiIconProvider::instance()->getIcon("view-refresh")); + m_ui->actionUpdateAllFeeds->setIcon(GuiIconProvider::instance()->getIcon("view-refresh")); + m_ui->newFeedButton->setIcon(GuiIconProvider::instance()->getIcon("list-add")); + m_ui->markReadButton->setIcon(GuiIconProvider::instance()->getIcon("mail-mark-read")); + m_ui->updateAllButton->setIcon(GuiIconProvider::instance()->getIcon("view-refresh")); + m_ui->rssDownloaderBtn->setIcon(GuiIconProvider::instance()->getIcon("download")); + + m_articleListWidget = new ArticleListWidget(m_ui->splitterMain); + m_ui->splitterMain->insertWidget(0, m_articleListWidget); + connect(m_articleListWidget, &ArticleListWidget::customContextMenuRequested, this, &RSSWidget::displayItemsListMenu); + connect(m_articleListWidget, &ArticleListWidget::currentItemChanged, this, &RSSWidget::handleCurrentArticleItemChanged); + connect(m_articleListWidget, &ArticleListWidget::itemDoubleClicked, this, &RSSWidget::downloadSelectedTorrents); + + m_feedListWidget = new FeedListWidget(m_ui->splitterSide); + m_ui->splitterSide->insertWidget(0, m_feedListWidget); + connect(m_feedListWidget, &QAbstractItemView::doubleClicked, this, &RSSWidget::renameSelectedRSSItem); + connect(m_feedListWidget, &QTreeWidget::currentItemChanged, this, &RSSWidget::handleCurrentFeedItemChanged); + connect(m_feedListWidget, &QWidget::customContextMenuRequested, this, &RSSWidget::displayRSSListMenu); + loadFoldersOpenState(); + m_feedListWidget->setCurrentItem(m_feedListWidget->stickyUnreadItem()); + + m_editHotkey = new QShortcut(Qt::Key_F2, m_feedListWidget, 0, 0, Qt::WidgetShortcut); + connect(m_editHotkey, &QShortcut::activated, this, &RSSWidget::renameSelectedRSSItem); + m_deleteHotkey = new QShortcut(QKeySequence::Delete, m_feedListWidget, 0, 0, Qt::WidgetShortcut); + connect(m_deleteHotkey, &QShortcut::activated, this, &RSSWidget::deleteSelectedItems); + + // Feeds list actions + connect(m_ui->actionDelete, &QAction::triggered, this, &RSSWidget::deleteSelectedItems); + connect(m_ui->actionRename, &QAction::triggered, this, &RSSWidget::renameSelectedRSSItem); + connect(m_ui->actionUpdate, &QAction::triggered, this, &RSSWidget::refreshSelectedItems); + connect(m_ui->actionNewFolder, &QAction::triggered, this, &RSSWidget::askNewFolder); + connect(m_ui->actionNewSubscription, &QAction::triggered, this, &RSSWidget::on_newFeedButton_clicked); + connect(m_ui->actionUpdateAllFeeds, &QAction::triggered, this, &RSSWidget::refreshAllFeeds); + connect(m_ui->updateAllButton, &QAbstractButton::clicked, this, &RSSWidget::refreshAllFeeds); + connect(m_ui->actionCopyFeedURL, &QAction::triggered, this, &RSSWidget::copySelectedFeedsURL); + connect(m_ui->actionMarkItemsRead, &QAction::triggered, this, &RSSWidget::on_markReadButton_clicked); + + // News list actions + connect(m_ui->actionOpenNewsURL, &QAction::triggered, this, &RSSWidget::openSelectedArticlesUrls); + connect(m_ui->actionDownloadTorrent, &QAction::triggered, this, &RSSWidget::downloadSelectedTorrents); + + // Restore sliders position + restoreSlidersPosition(); + // Bind saveSliders slots + connect(m_ui->splitterMain, &QSplitter::splitterMoved, this, &RSSWidget::saveSlidersPosition); + connect(m_ui->splitterSide, &QSplitter::splitterMoved, this, &RSSWidget::saveSlidersPosition); + + if (RSS::Session::instance()->isProcessingEnabled()) + m_ui->labelWarn->hide(); + connect(RSS::Session::instance(), &RSS::Session::processingStateChanged + , this, &RSSWidget::handleSessionProcessingStateChanged); + connect(RSS::Session::instance()->rootFolder(), &RSS::Folder::unreadCountChanged + , this, &RSSWidget::handleUnreadCountChanged); +} + +RSSWidget::~RSSWidget() +{ + // we need it here to properly mark latest article + // as read without having additional code + m_articleListWidget->clear(); + + saveFoldersOpenState(); + + delete m_editHotkey; + delete m_deleteHotkey; + delete m_feedListWidget; + delete m_ui; +} + +// display a right-click menu +void RSSWidget::displayRSSListMenu(const QPoint &pos) +{ + if (!m_feedListWidget->indexAt(pos).isValid()) + // No item under the mouse, clear selection + m_feedListWidget->clearSelection(); + QMenu myRSSListMenu(this); + QList selectedItems = m_feedListWidget->selectedItems(); + if (selectedItems.size() > 0) { + myRSSListMenu.addAction(m_ui->actionUpdate); + myRSSListMenu.addAction(m_ui->actionMarkItemsRead); + myRSSListMenu.addSeparator(); + if (selectedItems.size() == 1) { + if (selectedItems.first() != m_feedListWidget->stickyUnreadItem()) { + myRSSListMenu.addAction(m_ui->actionRename); + myRSSListMenu.addAction(m_ui->actionDelete); + myRSSListMenu.addSeparator(); + if (m_feedListWidget->isFolder(selectedItems.first())) + myRSSListMenu.addAction(m_ui->actionNewFolder); + } + } + else { + myRSSListMenu.addAction(m_ui->actionDelete); + myRSSListMenu.addSeparator(); + } + myRSSListMenu.addAction(m_ui->actionNewSubscription); + if (m_feedListWidget->isFeed(selectedItems.first())) { + myRSSListMenu.addSeparator(); + myRSSListMenu.addAction(m_ui->actionCopyFeedURL); + } + } + else { + myRSSListMenu.addAction(m_ui->actionNewSubscription); + myRSSListMenu.addAction(m_ui->actionNewFolder); + myRSSListMenu.addSeparator(); + myRSSListMenu.addAction(m_ui->actionUpdateAllFeeds); + } + myRSSListMenu.exec(QCursor::pos()); +} + +void RSSWidget::displayItemsListMenu(const QPoint &) +{ + bool hasTorrent = false; + bool hasLink = false; + foreach (const QListWidgetItem *item, m_articleListWidget->selectedItems()) { + auto article = reinterpret_cast(item->data(Qt::UserRole).value()); + Q_ASSERT(article); + + if (!article->torrentUrl().isEmpty()) + hasTorrent = true; + if (!article->link().isEmpty()) + hasLink = true; + if (hasTorrent && hasLink) + break; + } + + QMenu myItemListMenu(this); + if (hasTorrent) + myItemListMenu.addAction(m_ui->actionDownloadTorrent); + if (hasLink) + myItemListMenu.addAction(m_ui->actionOpenNewsURL); + if (hasTorrent || hasLink) + myItemListMenu.exec(QCursor::pos()); +} + +void RSSWidget::askNewFolder() +{ + bool ok; + QString newName = AutoExpandableDialog::getText( + this, tr("Please choose a folder name"), tr("Folder name:"), QLineEdit::Normal + , tr("New folder"), &ok); + if (!ok) return; + + newName = newName.trimmed(); + if (newName.isEmpty()) return; + + // Determine destination folder for new item + QTreeWidgetItem *destItem = nullptr; + QList selectedItems = m_feedListWidget->selectedItems(); + if (!selectedItems.empty()) { + destItem = selectedItems.first(); + if (!m_feedListWidget->isFolder(destItem)) + destItem = destItem->parent(); + } + // Consider the case where the user clicked on Unread item + RSS::Folder *rssDestFolder = ((destItem == m_feedListWidget->stickyUnreadItem()) + ? RSS::Session::instance()->rootFolder() + : qobject_cast(m_feedListWidget->getRSSItem(destItem))); + + QString error; + const QString newFolderPath = RSS::Item::joinPath(rssDestFolder->path(), newName); + if (!RSS::Session::instance()->addFolder(newFolderPath, &error)) + QMessageBox::warning(this, "qBittorrent", error, QMessageBox::Ok); + + // Expand destination folder to display new feed + if (destItem != m_feedListWidget->stickyUnreadItem()) + destItem->setExpanded(true); + // As new RSS items are added synchronously, we can do the following here. + m_feedListWidget->setCurrentItem(m_feedListWidget->mapRSSItem(RSS::Session::instance()->itemByPath(newFolderPath))); +} + +// add a stream by a button +void RSSWidget::on_newFeedButton_clicked() +{ + // Ask for feed URL + const QString clipText = qApp->clipboard()->text(); + const QString defaultURL = (Utils::Misc::isUrl(clipText) ? clipText : "http://"); + + bool ok; + QString newURL = AutoExpandableDialog::getText( + this, tr("Please type a RSS feed URL"), tr("Feed URL:"), QLineEdit::Normal, defaultURL, &ok); + if (!ok) return; + + newURL = newURL.trimmed(); + if (newURL.isEmpty()) return; + + // Determine destination folder for new item + QTreeWidgetItem *destItem = nullptr; + QList selectedItems = m_feedListWidget->selectedItems(); + if (!selectedItems.empty()) { + destItem = selectedItems.first(); + if (!m_feedListWidget->isFolder(destItem)) + destItem = destItem->parent(); + } + // Consider the case where the user clicked on Unread item + RSS::Folder *rssDestFolder = ((!destItem || (destItem == m_feedListWidget->stickyUnreadItem())) + ? RSS::Session::instance()->rootFolder() + : qobject_cast(m_feedListWidget->getRSSItem(destItem))); + + QString error; + // NOTE: We still add feed using legacy way (with URL as feed name) + const QString newFeedPath = RSS::Item::joinPath(rssDestFolder->path(), newURL); + if (!RSS::Session::instance()->addFeed(newURL, newFeedPath, &error)) + QMessageBox::warning(this, "qBittorrent", error, QMessageBox::Ok); + + // Expand destination folder to display new feed + if (destItem && (destItem != m_feedListWidget->stickyUnreadItem())) + destItem->setExpanded(true); + // As new RSS items are added synchronously, we can do the following here. + m_feedListWidget->setCurrentItem(m_feedListWidget->mapRSSItem(RSS::Session::instance()->itemByPath(newFeedPath))); +} + +void RSSWidget::deleteSelectedItems() +{ + QList selectedItems = m_feedListWidget->selectedItems(); + if (selectedItems.isEmpty()) + return; + if ((selectedItems.size() == 1) && (selectedItems.first() == m_feedListWidget->stickyUnreadItem())) + return; + + QMessageBox::StandardButton answer = QMessageBox::question( + this, tr("Deletion confirmation"), tr("Are you sure you want to delete the selected RSS feeds?") + , QMessageBox::Yes | QMessageBox::No, QMessageBox::No); + if (answer == QMessageBox::No) + return; + + foreach (QTreeWidgetItem *item, selectedItems) + if (item != m_feedListWidget->stickyUnreadItem()) + RSS::Session::instance()->removeItem(m_feedListWidget->itemPath(item)); +} + +void RSSWidget::loadFoldersOpenState() +{ + const QStringList openedFolders = Preferences::instance()->getRssOpenFolders(); + foreach (const QString &varPath, openedFolders) { + QTreeWidgetItem *parent = nullptr; + foreach (const QString &name, varPath.split("\\")) { + int nbChildren = (parent ? parent->childCount() : m_feedListWidget->topLevelItemCount()); + for (int i = 0; i < nbChildren; ++i) { + QTreeWidgetItem *child = (parent ? parent->child(i) : m_feedListWidget->topLevelItem(i)); + if (m_feedListWidget->getRSSItem(child)->name() == name) { + parent = child; + parent->setExpanded(true); + break; + } + } + } + } +} + +void RSSWidget::saveFoldersOpenState() +{ + QStringList openedFolders; + foreach (QTreeWidgetItem *item, m_feedListWidget->getAllOpenedFolders()) + openedFolders << m_feedListWidget->itemPath(item); + Preferences::instance()->setRssOpenFolders(openedFolders); +} + +void RSSWidget::refreshAllFeeds() +{ + RSS::Session::instance()->refresh(); +} + +void RSSWidget::downloadSelectedTorrents() +{ + foreach (QListWidgetItem *item, m_articleListWidget->selectedItems()) { + auto article = reinterpret_cast(item->data(Qt::UserRole).value()); + Q_ASSERT(article); + + // Mark as read + article->markAsRead(); + + if (!article->torrentUrl().isEmpty()) { + if (AddNewTorrentDialog::isEnabled()) + AddNewTorrentDialog::show(article->torrentUrl()); + else + BitTorrent::Session::instance()->addTorrent(article->torrentUrl()); + } + } +} + +// open the url of the selected RSS articles in the Web browser +void RSSWidget::openSelectedArticlesUrls() +{ + foreach (QListWidgetItem *item, m_articleListWidget->selectedItems()) { + auto article = reinterpret_cast(item->data(Qt::UserRole).value()); + Q_ASSERT(article); + + // Mark as read + article->markAsRead(); + + if (!article->link().isEmpty()) + QDesktopServices::openUrl(QUrl(article->link())); + } +} + +void RSSWidget::renameSelectedRSSItem() +{ + QList selectedItems = m_feedListWidget->selectedItems(); + if (selectedItems.size() != 1) return; + + QTreeWidgetItem *item = selectedItems.first(); + if (item == m_feedListWidget->stickyUnreadItem()) + return; + + RSS::Item *rssItem = m_feedListWidget->getRSSItem(item); + const QString parentPath = RSS::Item::parentPath(rssItem->path()); + bool ok; + do { + QString newName = AutoExpandableDialog::getText( + this, tr("Please choose a new name for this RSS feed"), tr("New feed name:") + , QLineEdit::Normal, rssItem->name(), &ok); + // Check if name is already taken + if (!ok) return; + + QString error; + if (!RSS::Session::instance()->moveItem(rssItem, RSS::Item::joinPath(parentPath, newName), &error)) { + QMessageBox::warning(0, tr("Rename failed"), error); + ok = false; + } + } while (!ok); +} + +void RSSWidget::refreshSelectedItems() +{ + foreach (QTreeWidgetItem *item, m_feedListWidget->selectedItems()) { + if (item == m_feedListWidget->stickyUnreadItem()) { + refreshAllFeeds(); + return; + } + + m_feedListWidget->getRSSItem(item)->refresh(); + } +} + +void RSSWidget::copySelectedFeedsURL() +{ + QStringList URLs; + foreach (QTreeWidgetItem *item, m_feedListWidget->selectedItems()) { + if (auto feed = qobject_cast(m_feedListWidget->getRSSItem(item))) + URLs << feed->url(); + } + qApp->clipboard()->setText(URLs.join("\n")); +} + +void RSSWidget::handleCurrentFeedItemChanged(QTreeWidgetItem *currentItem) +{ + if (!currentItem) { + m_articleListWidget->clear(); + return; + } + + m_articleListWidget->setRSSItem(m_feedListWidget->getRSSItem(currentItem) + , (currentItem == m_feedListWidget->stickyUnreadItem())); +} + +void RSSWidget::on_markReadButton_clicked() +{ + foreach (QTreeWidgetItem *item, m_feedListWidget->selectedItems()) { + m_feedListWidget->getRSSItem(item)->markAsRead(); + if (item == m_feedListWidget->stickyUnreadItem()) + break; // all items was read + } +} + +// display a news +void RSSWidget::handleCurrentArticleItemChanged(QListWidgetItem *currentItem, QListWidgetItem *previousItem) +{ + m_ui->textBrowser->clear(); + + if (previousItem) { + auto article = m_articleListWidget->getRSSArticle(previousItem); + Q_ASSERT(article); + article->markAsRead(); + } + + if (!currentItem) return; + + auto article = m_articleListWidget->getRSSArticle(currentItem); + Q_ASSERT(article); + + QString html; + html += "
"; + html += "
" + article->title() + "
"; + if (article->date().isValid()) + html += "
" + tr("Date: ") + "" + article->date().toLocalTime().toString(Qt::SystemLocaleLongDate) + "
"; + if (!article->author().isEmpty()) + html += "
" + tr("Author: ") + "" + article->author() + "
"; + html += "
"; + html += "
"; + if (Qt::mightBeRichText(article->description())) { + html += article->description(); + } + else { + QString description = article->description(); + QRegExp rx; + // If description is plain text, replace BBCode tags with HTML and wrap everything in
 so it looks nice
+        rx.setMinimal(true);
+        rx.setCaseSensitivity(Qt::CaseInsensitive);
+
+        rx.setPattern("\\[img\\](.+)\\[/img\\]");
+        description = description.replace(rx, "");
+
+        rx.setPattern("\\[url=(\")?(.+)\\1\\]");
+        description = description.replace(rx, "");
+        description = description.replace("[/url]", "", Qt::CaseInsensitive);
+
+        rx.setPattern("\\[(/)?([bius])\\]");
+        description = description.replace(rx, "<\\1\\2>");
+
+        rx.setPattern("\\[color=(\")?(.+)\\1\\]");
+        description = description.replace(rx, "");
+        description = description.replace("[/color]", "", Qt::CaseInsensitive);
+
+        rx.setPattern("\\[size=(\")?(.+)\\d\\1\\]");
+        description = description.replace(rx, "");
+        description = description.replace("[/size]", "", Qt::CaseInsensitive);
+
+        html += "
" + description + "
"; + } + html += "
"; + m_ui->textBrowser->setHtml(html); +} + +void RSSWidget::saveSlidersPosition() +{ + // Remember sliders positions + Preferences *const pref = Preferences::instance(); + pref->setRssSideSplitterState(m_ui->splitterSide->saveState()); + pref->setRssMainSplitterState(m_ui->splitterMain->saveState()); +} + +void RSSWidget::restoreSlidersPosition() +{ + const Preferences *const pref = Preferences::instance(); + const QByteArray stateSide = pref->getRssSideSplitterState(); + if (!stateSide.isEmpty()) + m_ui->splitterSide->restoreState(stateSide); + const QByteArray stateMain = pref->getRssMainSplitterState(); + if (!stateMain.isEmpty()) + m_ui->splitterMain->restoreState(stateMain); +} + +void RSSWidget::updateRefreshInterval(uint val) +{ + RSS::Session::instance()->setRefreshInterval(val); +} + +void RSSWidget::on_rssDownloaderBtn_clicked() +{ + AutomatedRssDownloader(this).exec(); +} + +void RSSWidget::handleSessionProcessingStateChanged(bool enabled) +{ + m_ui->labelWarn->setVisible(!enabled); +} + +void RSSWidget::handleUnreadCountChanged() +{ + emit unreadCountUpdated(RSS::Session::instance()->rootFolder()->unreadCount()); +} diff --git a/src/gui/rss/rss_imp.h b/src/gui/rss/rsswidget.h similarity index 60% rename from src/gui/rss/rss_imp.h rename to src/gui/rss/rsswidget.h index fbb3bb2d7..6c88e1cc3 100644 --- a/src/gui/rss/rss_imp.h +++ b/src/gui/rss/rsswidget.h @@ -1,6 +1,8 @@ /* - * Bittorrent Client using Qt4 and libtorrent. - * Copyright (C) 2006 Christophe Dumez, Arnaud Demaiziere + * Bittorrent Client using Qt and libtorrent. + * Copyright (C) 2017 Vladimir Golovnev + * Copyright (C) 2006 Christophe Dumez + * Copyright (C) 2006 Arnaud Demaiziere * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License @@ -24,46 +26,38 @@ * 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 __RSS_IMP_H__ -#define __RSS_IMP_H__ -#define REFRESH_MAX_LATENCY 600000 +#ifndef RSSWIDGET_H +#define RSSWIDGET_H #include #include -#include "base/rss/rssfolder.h" -#include "base/rss/rssmanager.h" - +class ArticleListWidget; class FeedListWidget; - -QT_BEGIN_NAMESPACE class QListWidgetItem; class QTreeWidgetItem; -QT_END_NAMESPACE namespace Ui { - class RSS; + class RSSWidget; } -class RSSImp: public QWidget +class RSSWidget: public QWidget { Q_OBJECT public: - RSSImp(QWidget * parent); - ~RSSImp(); + RSSWidget(QWidget *parent); + ~RSSWidget(); public slots: void deleteSelectedItems(); void updateRefreshInterval(uint val); signals: - void updateRSSCount(int); + void unreadCountUpdated(int count); private slots: void on_newFeedButton_clicked(); @@ -71,38 +65,28 @@ private slots: void on_markReadButton_clicked(); void displayRSSListMenu(const QPoint &); void displayItemsListMenu(const QPoint &); - void renameSelectedRssFile(); + void renameSelectedRSSItem(); void refreshSelectedItems(); void copySelectedFeedsURL(); - void populateArticleList(QTreeWidgetItem *item); - void refreshTextBrowser(); - void updateFeedIcon(const QString &url, const QString &icon_path); - void updateFeedInfos(const QString &url, const QString &display_name, uint nbUnread); - void onFeedContentChanged(const QString &url); - void updateItemsInfos(const QList &items); - void updateItemInfos(QTreeWidgetItem *item); + void handleCurrentFeedItemChanged(QTreeWidgetItem *currentItem); + void handleCurrentArticleItemChanged(QListWidgetItem *currentItem, QListWidgetItem *previousItem); void openSelectedArticlesUrls(); void downloadSelectedTorrents(); - void fillFeedsList(QTreeWidgetItem *parent = 0, const Rss::FolderPtr &rss_parent = Rss::FolderPtr()); void saveSlidersPosition(); void restoreSlidersPosition(); void askNewFolder(); void saveFoldersOpenState(); void loadFoldersOpenState(); - void on_settingsButton_clicked(); void on_rssDownloaderBtn_clicked(); + void handleSessionProcessingStateChanged(bool enabled); + void handleUnreadCountChanged(); private: - static QListWidgetItem *createArticleListItem(const Rss::ArticlePtr &article); - static QTreeWidgetItem *createFolderListItem(const Rss::FilePtr &rssFile); - -private: - Ui::RSS *m_ui; - Rss::ManagerPtr m_rssManager; - FeedListWidget *m_feedList; - QListWidgetItem *m_currentArticle; - QShortcut *editHotkey; - QShortcut *deleteHotkey; + Ui::RSSWidget *m_ui; + ArticleListWidget *m_articleListWidget; + FeedListWidget *m_feedListWidget; + QShortcut *m_editHotkey; + QShortcut *m_deleteHotkey; }; -#endif +#endif // RSSWIDGET_H diff --git a/src/gui/rss/rss.ui b/src/gui/rss/rsswidget.ui similarity index 84% rename from src/gui/rss/rss.ui rename to src/gui/rss/rsswidget.ui index 649a8586a..74fb81130 100644 --- a/src/gui/rss/rss.ui +++ b/src/gui/rss/rsswidget.ui @@ -1,7 +1,7 @@ - RSS - + RSSWidget + 0 @@ -17,6 +17,24 @@ Search + + + + + true + + + + color: red; + + + Fetching of RSS feeds is disabled now! You can enable it in application settings. + + + true + + + @@ -63,13 +81,6 @@
- - - - Settings... - - -
@@ -109,14 +120,6 @@ Qt::Horizontal - - - Qt::CustomContextMenu - - - QAbstractItemView::ExtendedSelection - - true @@ -153,12 +156,12 @@ Update - + New subscription... - + Update all feeds @@ -166,7 +169,7 @@ Update all feeds - + Mark items read @@ -174,22 +177,22 @@ Mark items read - + Download torrent - + Open news URL - + Copy feed URL - + New folder... @@ -199,7 +202,7 @@ HtmlBrowser QTextBrowser -
htmlbrowser.h
+
gui/rss/htmlbrowser.h
diff --git a/src/icons.qrc b/src/icons.qrc index 8a7441ef6..47d182380 100644 --- a/src/icons.qrc +++ b/src/icons.qrc @@ -1,380 +1,381 @@ - - icons/qbittorrent.png - icons/3-state-checkbox.gif - icons/L.gif - icons/loading.png - icons/slow.png - icons/slow_off.png - icons/sphere.png - icons/sphere2.png - icons/url.png - icons/flags/ad.png - icons/flags/ae.png - icons/flags/af.png - icons/flags/ag.png - icons/flags/ai.png - icons/flags/al.png - icons/flags/am.png - icons/flags/an.png - icons/flags/ao.png - icons/flags/ar.png - icons/flags/as.png - icons/flags/at.png - icons/flags/au.png - icons/flags/aw.png - icons/flags/ax.png - icons/flags/az.png - icons/flags/ba.png - icons/flags/bb.png - icons/flags/bd.png - icons/flags/be.png - icons/flags/bf.png - icons/flags/bg.png - icons/flags/bh.png - icons/flags/bi.png - icons/flags/bj.png - icons/flags/bm.png - icons/flags/bn.png - icons/flags/bo.png - icons/flags/br.png - icons/flags/bs.png - icons/flags/bt.png - icons/flags/bv.png - icons/flags/bw.png - icons/flags/by.png - icons/flags/bz.png - icons/flags/ca.png - icons/flags/cc.png - icons/flags/cd.png - icons/flags/cf.png - icons/flags/cg.png - icons/flags/ch.png - icons/flags/ci.png - icons/flags/ck.png - icons/flags/cl.png - icons/flags/cm.png - icons/flags/cn.png - icons/flags/co.png - icons/flags/cr.png - icons/flags/cs.png - icons/flags/cu.png - icons/flags/cv.png - icons/flags/cx.png - icons/flags/cy.png - icons/flags/cz.png - icons/flags/de.png - icons/flags/dj.png - icons/flags/dk.png - icons/flags/dm.png - icons/flags/do.png - icons/flags/dz.png - icons/flags/ec.png - icons/flags/ee.png - icons/flags/eg.png - icons/flags/eh.png - icons/flags/er.png - icons/flags/es.png - icons/flags/et.png - icons/flags/fi.png - icons/flags/fj.png - icons/flags/fk.png - icons/flags/fm.png - icons/flags/fo.png - icons/flags/fr.png - icons/flags/ga.png - icons/flags/gb.png - icons/flags/gd.png - icons/flags/ge.png - icons/flags/gf.png - icons/flags/gh.png - icons/flags/gi.png - icons/flags/gl.png - icons/flags/gm.png - icons/flags/gn.png - icons/flags/gp.png - icons/flags/gq.png - icons/flags/gr.png - icons/flags/gs.png - icons/flags/gt.png - icons/flags/gu.png - icons/flags/gw.png - icons/flags/gy.png - icons/flags/hk.png - icons/flags/hm.png - icons/flags/hn.png - icons/flags/hr.png - icons/flags/ht.png - icons/flags/hu.png - icons/flags/id.png - icons/flags/ie.png - icons/flags/il.png - icons/flags/in.png - icons/flags/io.png - icons/flags/iq.png - icons/flags/ir.png - icons/flags/is.png - icons/flags/it.png - icons/flags/jm.png - icons/flags/jo.png - icons/flags/jp.png - icons/flags/ke.png - icons/flags/kg.png - icons/flags/kh.png - icons/flags/ki.png - icons/flags/km.png - icons/flags/kn.png - icons/flags/kp.png - icons/flags/kr.png - icons/flags/kw.png - icons/flags/ky.png - icons/flags/kz.png - icons/flags/la.png - icons/flags/lb.png - icons/flags/lc.png - icons/flags/li.png - icons/flags/lk.png - icons/flags/lr.png - icons/flags/ls.png - icons/flags/lt.png - icons/flags/lu.png - icons/flags/lv.png - icons/flags/ly.png - icons/flags/ma.png - icons/flags/mc.png - icons/flags/md.png - icons/flags/me.png - icons/flags/mg.png - icons/flags/mh.png - icons/flags/mk.png - icons/flags/ml.png - icons/flags/mm.png - icons/flags/mn.png - icons/flags/mo.png - icons/flags/mp.png - icons/flags/mq.png - icons/flags/mr.png - icons/flags/ms.png - icons/flags/mt.png - icons/flags/mu.png - icons/flags/mv.png - icons/flags/mw.png - icons/flags/mx.png - icons/flags/my.png - icons/flags/mz.png - icons/flags/na.png - icons/flags/nc.png - icons/flags/ne.png - icons/flags/nf.png - icons/flags/ng.png - icons/flags/ni.png - icons/flags/nl.png - icons/flags/no.png - icons/flags/np.png - icons/flags/nr.png - icons/flags/nu.png - icons/flags/nz.png - icons/flags/om.png - icons/flags/pa.png - icons/flags/pe.png - icons/flags/pf.png - icons/flags/pg.png - icons/flags/ph.png - icons/flags/pk.png - icons/flags/pl.png - icons/flags/pm.png - icons/flags/pn.png - icons/flags/pr.png - icons/flags/ps.png - icons/flags/pt.png - icons/flags/pw.png - icons/flags/py.png - icons/flags/qa.png - icons/flags/re.png - icons/flags/ro.png - icons/flags/rs.png - icons/flags/ru.png - icons/flags/rw.png - icons/flags/sa.png - icons/flags/sb.png - icons/flags/sc.png - icons/flags/sd.png - icons/flags/se.png - icons/flags/sg.png - icons/flags/sh.png - icons/flags/si.png - icons/flags/sj.png - icons/flags/sk.png - icons/flags/sl.png - icons/flags/sm.png - icons/flags/sn.png - icons/flags/so.png - icons/flags/sr.png - icons/flags/st.png - icons/flags/sv.png - icons/flags/sy.png - icons/flags/sz.png - icons/flags/tc.png - icons/flags/td.png - icons/flags/tf.png - icons/flags/tg.png - icons/flags/th.png - icons/flags/tj.png - icons/flags/tk.png - icons/flags/tl.png - icons/flags/tm.png - icons/flags/tn.png - icons/flags/to.png - icons/flags/tr.png - icons/flags/tt.png - icons/flags/tv.png - icons/flags/tw.png - icons/flags/tz.png - icons/flags/ua.png - icons/flags/ug.png - icons/flags/um.png - icons/flags/us.png - icons/flags/uy.png - icons/flags/uz.png - icons/flags/va.png - icons/flags/vc.png - icons/flags/ve.png - icons/flags/vg.png - icons/flags/vi.png - icons/flags/vn.png - icons/flags/vu.png - icons/flags/wf.png - icons/flags/ws.png - icons/flags/ye.png - icons/flags/yt.png - icons/flags/za.png - icons/flags/zm.png - icons/flags/zw.png - icons/qbt-theme/application-exit.png - icons/qbt-theme/application-rss+xml.png - icons/qbt-theme/application-x-mswinurl.png - icons/qbt-theme/configure.png - icons/qbt-theme/dialog-cancel.png - icons/qbt-theme/dialog-information.png - icons/qbt-theme/dialog-warning.png - icons/qbt-theme/document-edit-verify.png - icons/qbt-theme/document-edit.png - icons/qbt-theme/document-encrypt.png - icons/qbt-theme/document-import.png - icons/qbt-theme/document-new.png - icons/qbt-theme/document-properties.png - icons/qbt-theme/document-save.png - icons/qbt-theme/download.png - icons/qbt-theme/edit-clear-history.png - icons/qbt-theme/edit-clear.png - icons/qbt-theme/edit-copy.png - icons/qbt-theme/edit-cut.png - icons/qbt-theme/edit-delete.png - icons/qbt-theme/edit-find-user.png - icons/qbt-theme/edit-find.png - icons/qbt-theme/edit-paste.png - icons/qbt-theme/edit-rename.png - icons/qbt-theme/folder-documents.png - icons/qbt-theme/folder-download.png - icons/qbt-theme/folder-new.png - icons/qbt-theme/folder-remote.png - icons/qbt-theme/gear.png - icons/qbt-theme/gear32.png - icons/qbt-theme/go-down.png - icons/qbt-theme/go-up.png - icons/qbt-theme/help-about.png - icons/qbt-theme/help-contents.png - icons/qbt-theme/inode-directory.png - icons/qbt-theme/insert-link.png - icons/qbt-theme/kt-magnet.png - icons/qbt-theme/kt-set-max-download-speed.png - icons/qbt-theme/kt-set-max-upload-speed.png - icons/qbt-theme/list-add.png - icons/qbt-theme/list-remove.png - icons/qbt-theme/mail-folder-inbox.png - icons/qbt-theme/mail-mark-read.png - icons/qbt-theme/media-playback-pause.png - icons/qbt-theme/media-playback-start.png - icons/qbt-theme/media-seek-forward.png - icons/qbt-theme/network-server.png - icons/qbt-theme/network-wired.png - icons/qbt-theme/object-locked.png - icons/qbt-theme/preferences-desktop.png - icons/qbt-theme/preferences-other.png - icons/qbt-theme/preferences-system-network.png - icons/qbt-theme/preferences-web-browser-cookies.png - icons/qbt-theme/security-high.png - icons/qbt-theme/security-low.png - icons/qbt-theme/services.png - icons/qbt-theme/speedometer.png - icons/qbt-theme/tab-close.png - icons/qbt-theme/task-attention.png - icons/qbt-theme/task-complete.png - icons/qbt-theme/task-ongoing.png - icons/qbt-theme/task-reject.png - icons/qbt-theme/text-plain.png - icons/qbt-theme/tools-report-bug.png - icons/qbt-theme/unavailable.png - icons/qbt-theme/user-group-delete.png - icons/qbt-theme/user-group-new.png - icons/qbt-theme/view-calendar-journal.png - icons/qbt-theme/view-categories.png - icons/qbt-theme/view-filter.png - icons/qbt-theme/view-preview.png - icons/qbt-theme/view-refresh.png - icons/qbt-theme/view-statistics.png - icons/qbt-theme/wallet-open.png - icons/qbt-theme/webui.png - icons/skin/arrow-right.gif - icons/skin/bg-dropdown.gif - icons/skin/bg-handle-horizontal.gif - icons/skin/bg-header.gif - icons/skin/bg-panel-header.gif - icons/skin/checking.png - icons/skin/collapse-expand.gif - icons/skin/connected.png - icons/skin/disconnected.png - icons/skin/dock-tabs.gif - icons/skin/download.png - icons/skin/downloading.png - icons/skin/error.png - icons/skin/filteractive.png - icons/skin/filterall.png - icons/skin/filterinactive.png - icons/skin/firewalled.png - icons/skin/handle-icon-horizontal.gif - icons/skin/handle-icon.gif - icons/skin/knob.gif - icons/skin/logo-blank.gif - icons/skin/logo.gif - icons/skin/logo2.gif - icons/skin/mascot.png - icons/skin/paused.png - icons/skin/qbittorrent16.png - icons/skin/qbittorrent22.png - icons/skin/qbittorrent32.png - icons/skin/qbittorrent_mono_dark.png - icons/skin/qbittorrent_mono_light.png - icons/skin/queued.png - icons/skin/ratio.png - icons/skin/seeding.png - icons/skin/slider-area.gif - icons/skin/spacer.gif - icons/skin/spinner-placeholder.gif - icons/skin/spinner.gif - icons/skin/splash.png - icons/skin/stalledDL.png - icons/skin/stalledUP.png - icons/skin/tabs.gif - icons/skin/toolbox-divider.gif - icons/skin/toolbox-divider2.gif - icons/skin/resumed.png - icons/skin/uploading.png - icons/skin/completed.png - icons/qbt-theme/system-log-out.png - icons/qbt-theme/go-bottom.png - icons/qbt-theme/go-top.png - icons/qbt-theme/checked.png - icons/qbt-theme/office-chart-line.png - + + icons/qbittorrent.png + icons/3-state-checkbox.gif + icons/L.gif + icons/loading.png + icons/slow.png + icons/slow_off.png + icons/sphere.png + icons/sphere2.png + icons/url.png + icons/flags/ad.png + icons/flags/ae.png + icons/flags/af.png + icons/flags/ag.png + icons/flags/ai.png + icons/flags/al.png + icons/flags/am.png + icons/flags/an.png + icons/flags/ao.png + icons/flags/ar.png + icons/flags/as.png + icons/flags/at.png + icons/flags/au.png + icons/flags/aw.png + icons/flags/ax.png + icons/flags/az.png + icons/flags/ba.png + icons/flags/bb.png + icons/flags/bd.png + icons/flags/be.png + icons/flags/bf.png + icons/flags/bg.png + icons/flags/bh.png + icons/flags/bi.png + icons/flags/bj.png + icons/flags/bm.png + icons/flags/bn.png + icons/flags/bo.png + icons/flags/br.png + icons/flags/bs.png + icons/flags/bt.png + icons/flags/bv.png + icons/flags/bw.png + icons/flags/by.png + icons/flags/bz.png + icons/flags/ca.png + icons/flags/cc.png + icons/flags/cd.png + icons/flags/cf.png + icons/flags/cg.png + icons/flags/ch.png + icons/flags/ci.png + icons/flags/ck.png + icons/flags/cl.png + icons/flags/cm.png + icons/flags/cn.png + icons/flags/co.png + icons/flags/cr.png + icons/flags/cs.png + icons/flags/cu.png + icons/flags/cv.png + icons/flags/cx.png + icons/flags/cy.png + icons/flags/cz.png + icons/flags/de.png + icons/flags/dj.png + icons/flags/dk.png + icons/flags/dm.png + icons/flags/do.png + icons/flags/dz.png + icons/flags/ec.png + icons/flags/ee.png + icons/flags/eg.png + icons/flags/eh.png + icons/flags/er.png + icons/flags/es.png + icons/flags/et.png + icons/flags/fi.png + icons/flags/fj.png + icons/flags/fk.png + icons/flags/fm.png + icons/flags/fo.png + icons/flags/fr.png + icons/flags/ga.png + icons/flags/gb.png + icons/flags/gd.png + icons/flags/ge.png + icons/flags/gf.png + icons/flags/gh.png + icons/flags/gi.png + icons/flags/gl.png + icons/flags/gm.png + icons/flags/gn.png + icons/flags/gp.png + icons/flags/gq.png + icons/flags/gr.png + icons/flags/gs.png + icons/flags/gt.png + icons/flags/gu.png + icons/flags/gw.png + icons/flags/gy.png + icons/flags/hk.png + icons/flags/hm.png + icons/flags/hn.png + icons/flags/hr.png + icons/flags/ht.png + icons/flags/hu.png + icons/flags/id.png + icons/flags/ie.png + icons/flags/il.png + icons/flags/in.png + icons/flags/io.png + icons/flags/iq.png + icons/flags/ir.png + icons/flags/is.png + icons/flags/it.png + icons/flags/jm.png + icons/flags/jo.png + icons/flags/jp.png + icons/flags/ke.png + icons/flags/kg.png + icons/flags/kh.png + icons/flags/ki.png + icons/flags/km.png + icons/flags/kn.png + icons/flags/kp.png + icons/flags/kr.png + icons/flags/kw.png + icons/flags/ky.png + icons/flags/kz.png + icons/flags/la.png + icons/flags/lb.png + icons/flags/lc.png + icons/flags/li.png + icons/flags/lk.png + icons/flags/lr.png + icons/flags/ls.png + icons/flags/lt.png + icons/flags/lu.png + icons/flags/lv.png + icons/flags/ly.png + icons/flags/ma.png + icons/flags/mc.png + icons/flags/md.png + icons/flags/me.png + icons/flags/mg.png + icons/flags/mh.png + icons/flags/mk.png + icons/flags/ml.png + icons/flags/mm.png + icons/flags/mn.png + icons/flags/mo.png + icons/flags/mp.png + icons/flags/mq.png + icons/flags/mr.png + icons/flags/ms.png + icons/flags/mt.png + icons/flags/mu.png + icons/flags/mv.png + icons/flags/mw.png + icons/flags/mx.png + icons/flags/my.png + icons/flags/mz.png + icons/flags/na.png + icons/flags/nc.png + icons/flags/ne.png + icons/flags/nf.png + icons/flags/ng.png + icons/flags/ni.png + icons/flags/nl.png + icons/flags/no.png + icons/flags/np.png + icons/flags/nr.png + icons/flags/nu.png + icons/flags/nz.png + icons/flags/om.png + icons/flags/pa.png + icons/flags/pe.png + icons/flags/pf.png + icons/flags/pg.png + icons/flags/ph.png + icons/flags/pk.png + icons/flags/pl.png + icons/flags/pm.png + icons/flags/pn.png + icons/flags/pr.png + icons/flags/ps.png + icons/flags/pt.png + icons/flags/pw.png + icons/flags/py.png + icons/flags/qa.png + icons/flags/re.png + icons/flags/ro.png + icons/flags/rs.png + icons/flags/ru.png + icons/flags/rw.png + icons/flags/sa.png + icons/flags/sb.png + icons/flags/sc.png + icons/flags/sd.png + icons/flags/se.png + icons/flags/sg.png + icons/flags/sh.png + icons/flags/si.png + icons/flags/sj.png + icons/flags/sk.png + icons/flags/sl.png + icons/flags/sm.png + icons/flags/sn.png + icons/flags/so.png + icons/flags/sr.png + icons/flags/st.png + icons/flags/sv.png + icons/flags/sy.png + icons/flags/sz.png + icons/flags/tc.png + icons/flags/td.png + icons/flags/tf.png + icons/flags/tg.png + icons/flags/th.png + icons/flags/tj.png + icons/flags/tk.png + icons/flags/tl.png + icons/flags/tm.png + icons/flags/tn.png + icons/flags/to.png + icons/flags/tr.png + icons/flags/tt.png + icons/flags/tv.png + icons/flags/tw.png + icons/flags/tz.png + icons/flags/ua.png + icons/flags/ug.png + icons/flags/um.png + icons/flags/us.png + icons/flags/uy.png + icons/flags/uz.png + icons/flags/va.png + icons/flags/vc.png + icons/flags/ve.png + icons/flags/vg.png + icons/flags/vi.png + icons/flags/vn.png + icons/flags/vu.png + icons/flags/wf.png + icons/flags/ws.png + icons/flags/ye.png + icons/flags/yt.png + icons/flags/za.png + icons/flags/zm.png + icons/flags/zw.png + icons/qbt-theme/application-exit.png + icons/qbt-theme/application-rss+xml.png + icons/qbt-theme/application-x-mswinurl.png + icons/qbt-theme/configure.png + icons/qbt-theme/dialog-cancel.png + icons/qbt-theme/dialog-information.png + icons/qbt-theme/dialog-warning.png + icons/qbt-theme/document-edit-verify.png + icons/qbt-theme/document-edit.png + icons/qbt-theme/document-encrypt.png + icons/qbt-theme/document-import.png + icons/qbt-theme/document-new.png + icons/qbt-theme/document-properties.png + icons/qbt-theme/document-save.png + icons/qbt-theme/download.png + icons/qbt-theme/edit-clear-history.png + icons/qbt-theme/edit-clear.png + icons/qbt-theme/edit-copy.png + icons/qbt-theme/edit-cut.png + icons/qbt-theme/edit-delete.png + icons/qbt-theme/edit-find-user.png + icons/qbt-theme/edit-find.png + icons/qbt-theme/edit-paste.png + icons/qbt-theme/edit-rename.png + icons/qbt-theme/folder-documents.png + icons/qbt-theme/folder-download.png + icons/qbt-theme/folder-new.png + icons/qbt-theme/folder-remote.png + icons/qbt-theme/gear.png + icons/qbt-theme/gear32.png + icons/qbt-theme/go-down.png + icons/qbt-theme/go-up.png + icons/qbt-theme/help-about.png + icons/qbt-theme/help-contents.png + icons/qbt-theme/inode-directory.png + icons/qbt-theme/insert-link.png + icons/qbt-theme/kt-magnet.png + icons/qbt-theme/kt-set-max-download-speed.png + icons/qbt-theme/kt-set-max-upload-speed.png + icons/qbt-theme/list-add.png + icons/qbt-theme/list-remove.png + icons/qbt-theme/mail-folder-inbox.png + icons/qbt-theme/mail-mark-read.png + icons/qbt-theme/media-playback-pause.png + icons/qbt-theme/media-playback-start.png + icons/qbt-theme/media-seek-forward.png + icons/qbt-theme/network-server.png + icons/qbt-theme/network-wired.png + icons/qbt-theme/object-locked.png + icons/qbt-theme/preferences-desktop.png + icons/qbt-theme/preferences-other.png + icons/qbt-theme/preferences-system-network.png + icons/qbt-theme/preferences-web-browser-cookies.png + icons/qbt-theme/security-high.png + icons/qbt-theme/security-low.png + icons/qbt-theme/services.png + icons/qbt-theme/speedometer.png + icons/qbt-theme/tab-close.png + icons/qbt-theme/task-attention.png + icons/qbt-theme/task-complete.png + icons/qbt-theme/task-ongoing.png + icons/qbt-theme/task-reject.png + icons/qbt-theme/text-plain.png + icons/qbt-theme/tools-report-bug.png + icons/qbt-theme/unavailable.png + icons/qbt-theme/user-group-delete.png + icons/qbt-theme/user-group-new.png + icons/qbt-theme/view-calendar-journal.png + icons/qbt-theme/view-categories.png + icons/qbt-theme/view-filter.png + icons/qbt-theme/view-preview.png + icons/qbt-theme/view-refresh.png + icons/qbt-theme/view-statistics.png + icons/qbt-theme/wallet-open.png + icons/qbt-theme/webui.png + icons/skin/arrow-right.gif + icons/skin/bg-dropdown.gif + icons/skin/bg-handle-horizontal.gif + icons/skin/bg-header.gif + icons/skin/bg-panel-header.gif + icons/skin/checking.png + icons/skin/collapse-expand.gif + icons/skin/connected.png + icons/skin/disconnected.png + icons/skin/dock-tabs.gif + icons/skin/download.png + icons/skin/downloading.png + icons/skin/error.png + icons/skin/filteractive.png + icons/skin/filterall.png + icons/skin/filterinactive.png + icons/skin/firewalled.png + icons/skin/handle-icon-horizontal.gif + icons/skin/handle-icon.gif + icons/skin/knob.gif + icons/skin/logo-blank.gif + icons/skin/logo.gif + icons/skin/logo2.gif + icons/skin/mascot.png + icons/skin/paused.png + icons/skin/qbittorrent16.png + icons/skin/qbittorrent22.png + icons/skin/qbittorrent32.png + icons/skin/qbittorrent_mono_dark.png + icons/skin/qbittorrent_mono_light.png + icons/skin/queued.png + icons/skin/ratio.png + icons/skin/seeding.png + icons/skin/slider-area.gif + icons/skin/spacer.gif + icons/skin/spinner-placeholder.gif + icons/skin/spinner.gif + icons/skin/splash.png + icons/skin/stalledDL.png + icons/skin/stalledUP.png + icons/skin/tabs.gif + icons/skin/toolbox-divider.gif + icons/skin/toolbox-divider2.gif + icons/skin/resumed.png + icons/skin/uploading.png + icons/skin/completed.png + icons/qbt-theme/system-log-out.png + icons/qbt-theme/go-bottom.png + icons/qbt-theme/go-top.png + icons/qbt-theme/checked.png + icons/qbt-theme/office-chart-line.png + icons/qbt-theme/rss-config.png + diff --git a/src/icons/qbt-theme/rss-config.png b/src/icons/qbt-theme/rss-config.png new file mode 100644 index 000000000..2d0117adc Binary files /dev/null and b/src/icons/qbt-theme/rss-config.png differ diff --git a/src/webui/webapplication.cpp b/src/webui/webapplication.cpp index ad896413a..9cba1b079 100644 --- a/src/webui/webapplication.cpp +++ b/src/webui/webapplication.cpp @@ -396,7 +396,7 @@ void WebApplication::action_command_download() // TODO: Check if destination actually exists params.skipChecking = skipChecking; - params.addPaused = addPaused; + params.addPaused = TriStateBool(addPaused); params.savePath = savepath; params.category = category; @@ -436,7 +436,7 @@ void WebApplication::action_command_upload() // TODO: Check if destination actually exists params.skipChecking = skipChecking; - params.addPaused = addPaused; + params.addPaused = TriStateBool(addPaused); params.savePath = savepath; params.category = category; if (!BitTorrent::Session::instance()->addTorrent(torrentInfo, params)) {