/* * 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 #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; LogMsg(tr("Failed to download RSS feed at '%1'. Reason: %2").arg(url).arg(error) , Log::WARNING); emit stateChanged(this); } void Feed::handleParsingFinished(const RSS::Private::ParsingResult &result) { if (!result.error.isEmpty()) { m_hasError = true; LogMsg(tr("Failed to parse RSS feed at '%1'. Reason: %2").arg(m_url).arg(result.error) , Log::WARNING); } else { if (title() != result.title) { m_title = result.title; emit titleChanged(this); } m_lastBuildDate = result.lastBuildDate; int newArticlesCount = 0; foreach (const QVariantHash &varHash, result.articles) { try { auto article = new Article(this, varHash); if (addArticle(article)) ++newArticlesCount; else delete article; } catch (const std::runtime_error&) {} } m_dirty = (newArticlesCount > 0); store(); m_hasError = false; LogMsg(tr("RSS feed at '%1' successfully updated. Added %2 new articles.") .arg(m_url).arg(newArticlesCount)); } m_isLoading = 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 { LogMsg(tr("Couldn't read RSS Session data 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) { LogMsg(tr("Couldn't parse RSS Session data. Error: %1").arg(jsonError.errorString()) , Log::WARNING); return; } if (!jsonDoc.isArray()) { LogMsg(tr("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()) { LogMsg(tr("Couldn't load RSS article '%1#%2'. Invalid data format.").arg(m_url).arg(i) , Log::WARNING); continue; } try { auto article = new Article(this, jsonVal.toObject()); if (!addArticle(article)) delete article; } catch (const std::runtime_error&) {} } } 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 hash = var.toHash(); // update legacy keys hash[Article::KeyLink] = hash.take(QLatin1String("news_link")); hash[Article::KeyTorrentURL] = hash.take(QLatin1String("torrent_url")); hash[Article::KeyIsRead] = hash.take(QLatin1String("read")); try { auto article = new Article(this, hash); if (!addArticle(article)) delete article; } catch (const std::runtime_error&) {} } } 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.last(); emit articleAboutToBeRemoved(oldestArticle); m_articles.remove(oldestArticle->guid()); m_articlesByDate.removeLast(); 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(); }