diff --git a/src/base/CMakeLists.txt b/src/base/CMakeLists.txt index 4c1fafd85..44c7c3210 100644 --- a/src/base/CMakeLists.txt +++ b/src/base/CMakeLists.txt @@ -70,6 +70,7 @@ add_library(qbt_base STATIC preferences.h profile.h profile_p.h + rss/feed_serializer.h rss/rss_article.h rss/rss_autodownloader.h rss/rss_autodownloadrule.h @@ -152,6 +153,7 @@ add_library(qbt_base STATIC preferences.cpp profile.cpp profile_p.cpp + rss/feed_serializer.cpp rss/rss_article.cpp rss/rss_autodownloader.cpp rss/rss_autodownloadrule.cpp diff --git a/src/base/base.pri b/src/base/base.pri index f6b838536..4e1f3b77c 100644 --- a/src/base/base.pri +++ b/src/base/base.pri @@ -69,6 +69,7 @@ HEADERS += \ $$PWD/preferences.h \ $$PWD/profile.h \ $$PWD/profile_p.h \ + $$PWD/rss/feed_serializer.h \ $$PWD/rss/rss_article.h \ $$PWD/rss/rss_autodownloader.h \ $$PWD/rss/rss_autodownloadrule.h \ @@ -152,6 +153,7 @@ SOURCES += \ $$PWD/preferences.cpp \ $$PWD/profile.cpp \ $$PWD/profile_p.cpp \ + $$PWD/rss/feed_serializer.cpp \ $$PWD/rss/rss_article.cpp \ $$PWD/rss/rss_autodownloader.cpp \ $$PWD/rss/rss_autodownloadrule.cpp \ diff --git a/src/base/rss/feed_serializer.cpp b/src/base/rss/feed_serializer.cpp new file mode 100644 index 000000000..bd875e3ad --- /dev/null +++ b/src/base/rss/feed_serializer.cpp @@ -0,0 +1,126 @@ +/* + * Bittorrent Client using Qt and libtorrent. + * Copyright (C) 2022 Prince Gupta + * Copyright (C) 2015-2022 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 "feed_serializer.h" + +#include +#include +#include +#include +#include + +#include "base/logger.h" +#include "base/path.h" +#include "base/utils/io.h" +#include "rss_article.h" + +void RSS::Private::FeedSerializer::load(const Path &dataFileName, const QString &url) +{ + QFile file {dataFileName.data()}; + + if (!file.exists()) + { + emit loadingFinished({}); + } + else if (file.open(QFile::ReadOnly)) + { + emit loadingFinished(loadArticles(file.readAll(), url)); + file.close(); + } + else + { + LogMsg(tr("Couldn't read RSS Session data from %1. Error: %2") + .arg(dataFileName.toString(), file.errorString()) + , Log::WARNING); + } +} + +void RSS::Private::FeedSerializer::store(const Path &dataFileName, const QVector &articlesData) +{ + QJsonArray arr; + for (const QVariantHash &data : articlesData) + { + auto jsonObj = QJsonObject::fromVariantHash(data); + // JSON object doesn't support DateTime so we need to convert it + jsonObj[Article::KeyDate] = data[Article::KeyDate].toDateTime().toString(Qt::RFC2822Date); + + arr << jsonObj; + } + + const nonstd::expected result = Utils::IO::saveToFile(dataFileName, QJsonDocument(arr).toJson()); + if (!result) + { + LogMsg(tr("Failed to save RSS feed in '%1', Reason: %2").arg(dataFileName.toString(), result.error()) + , Log::WARNING); + } +} + +QVector RSS::Private::FeedSerializer::loadArticles(const QByteArray &data, const QString &url) +{ + QJsonParseError jsonError; + const 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 {}; + } + + QVector result; + const QJsonArray jsonArr = jsonDoc.array(); + result.reserve(jsonArr.size()); + for (int i = 0; i < jsonArr.size(); ++i) + { + const QJsonValue jsonVal = jsonArr[i]; + if (!jsonVal.isObject()) + { + LogMsg(tr("Couldn't load RSS article '%1#%2'. Invalid data format.") + .arg(url, QString::number(i)), Log::WARNING); + continue; + } + + const auto jsonObj = jsonVal.toObject(); + auto varHash = jsonObj.toVariantHash(); + // JSON object store DateTime as string so we need to convert it + varHash[Article::KeyDate] = + QDateTime::fromString(jsonObj.value(Article::KeyDate).toString(), Qt::RFC2822Date); + + result.push_back(varHash); + } + + return result; +} diff --git a/src/base/rss/feed_serializer.h b/src/base/rss/feed_serializer.h new file mode 100644 index 000000000..db593b0cc --- /dev/null +++ b/src/base/rss/feed_serializer.h @@ -0,0 +1,60 @@ +/* + * Bittorrent Client using Qt and libtorrent. + * Copyright (C) 2022 Prince Gupta + * Copyright (C) 2015-2022 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 + +#include "base/pathfwd.h" + +namespace RSS::Private +{ + class FeedSerializer final : public QObject + { + Q_OBJECT + Q_DISABLE_COPY_MOVE(FeedSerializer) + + public: + using QObject::QObject; + + void load(const Path &dataFileName, const QString &url); + void store(const Path &dataFileName, const QVector &articlesData); + + signals: + void loadingFinished(const QVector &articles); + + private: + QVector loadArticles(const QByteArray &data, const QString &url); + }; +} diff --git a/src/base/rss/rss_feed.cpp b/src/base/rss/rss_feed.cpp index 5253016a8..14d419343 100644 --- a/src/base/rss/rss_feed.cpp +++ b/src/base/rss/rss_feed.cpp @@ -1,6 +1,6 @@ /* * Bittorrent Client using Qt and libtorrent. - * Copyright (C) 2015, 2017 Vladimir Golovnev + * Copyright (C) 2015-2022 Vladimir Golovnev * Copyright (C) 2010 Christophe Dumez * Copyright (C) 2010 Arnaud Demaiziere * @@ -34,12 +34,12 @@ #include #include -#include #include #include #include #include #include +#include #include "base/asyncfilestorage.h" #include "base/global.h" @@ -47,6 +47,7 @@ #include "base/net/downloadmanager.h" #include "base/profile.h" #include "base/utils/fs.h" +#include "feed_serializer.h" #include "rss_article.h" #include "rss_parser.h" #include "rss_session.h" @@ -79,6 +80,11 @@ Feed::Feed(const QUuid &uid, const QString &url, const QString &path, Session *s m_iconPath = storageDir / Path(uidHex + u".ico"); + m_serializer = new Private::FeedSerializer; + m_serializer->moveToThread(m_session->workingThread()); + connect(this, &Feed::destroyed, m_serializer, &Private::FeedSerializer::deleteLater); + connect(m_serializer, &Private::FeedSerializer::loadingFinished, this, &Feed::handleArticleLoadFinished); + m_parser = new Private::Parser(m_lastBuildDate); m_parser->moveToThread(m_session->workingThread()); connect(this, &Feed::destroyed, m_parser, &Private::Parser::deleteLater); @@ -98,6 +104,7 @@ Feed::Feed(const QUuid &uid, const QString &url, const QString &path, Session *s Feed::~Feed() { + store(); emit aboutToBeDestroyed(this); } @@ -130,6 +137,12 @@ void Feed::markAsRead() void Feed::refresh() { + if (!m_isInitialized) + { + m_pendingRefresh = true; + return; + } + if (m_downloadHandler) m_downloadHandler->cancel(); @@ -162,7 +175,7 @@ QString Feed::title() const bool Feed::isLoading() const { - return m_isLoading; + return m_isLoading || !m_isInitialized; } QString Feed::lastBuildDate() const @@ -261,100 +274,30 @@ void Feed::handleParsingFinished(const RSS::Private::ParsingResult &result) void Feed::load() { - QFile file {(m_session->dataFileStorage()->storageDir() / m_dataFileName).data()}; - - 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.toString(), file.errorString()) - , Log::WARNING); - } -} - -void Feed::loadArticles(const QByteArray &data) -{ - QJsonParseError jsonError; - const 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; - } - - const QJsonArray jsonArr = jsonDoc.array(); - int i = -1; - for (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 RuntimeError &) {} - } -} - -void Feed::loadArticlesLegacy() -{ - const SettingsPtr qBTRSSFeeds = Profile::instance()->applicationSettings(u"qBittorrent-rss-feeds"_qs); - const QVariantHash allOldItems = qBTRSSFeeds->value(u"old_items"_qs).toHash(); - - for (const QVariant &var : asConst(allOldItems.value(m_url).toList())) + QMetaObject::invokeMethod(m_serializer, [this]() { - auto hash = var.toHash(); - // update legacy keys - hash[Article::KeyLink] = hash.take(u"news_link"_qs); - hash[Article::KeyTorrentURL] = hash.take(u"torrent_url"_qs); - hash[Article::KeyIsRead] = hash.take(u"read"_qs); - try - { - auto article = new Article(this, hash); - if (!addArticle(article)) - delete article; - } - catch (const RuntimeError &) {} - } + m_serializer->load((m_session->dataFileStorage()->storageDir() / m_dataFileName), m_url); + }); } void Feed::store() { - if (!m_dirty) return; + if (!m_dirty) + return; m_dirty = false; m_savingTimer.stop(); - QJsonArray jsonArr; + QVector articlesData; + articlesData.reserve(m_articles.size()); + for (Article *article :asConst(m_articles)) - jsonArr << article->toJsonObject(); + articlesData.push_back(article->data()); - m_session->dataFileStorage()->store(m_dataFileName, QJsonDocument(jsonArr).toJson()); + QMetaObject::invokeMethod(m_serializer, [this, articlesData]() + { + m_serializer->store((m_session->dataFileStorage()->storageDir() / m_dataFileName), articlesData); + }); } void Feed::storeDeferred() @@ -546,8 +489,33 @@ void Feed::handleArticleRead(Article *article) storeDeferred(); } +void Feed::handleArticleLoadFinished(const QVector &articles) +{ + for (const QVariantHash &data : articles) + { + try + { + auto *article = new Article(this, data); + if (!addArticle(article)) + delete article; + } + catch (const RuntimeError &) {} + } + + m_isInitialized = true; + emit stateChanged(this); + + if (m_pendingRefresh) + { + m_pendingRefresh = false; + refresh(); + } +} + void Feed::cleanup() { + m_dirty = false; + m_savingTimer.stop(); Utils::Fs::removeFile(m_session->dataFileStorage()->storageDir() / m_dataFileName); Utils::Fs::removeFile(m_iconPath); } diff --git a/src/base/rss/rss_feed.h b/src/base/rss/rss_feed.h index 584c6b2d6..dfeab9e2f 100644 --- a/src/base/rss/rss_feed.h +++ b/src/base/rss/rss_feed.h @@ -1,6 +1,6 @@ /* * Bittorrent Client using Qt and libtorrent. - * Copyright (C) 2015, 2017 Vladimir Golovnev + * Copyright (C) 2015-2022 Vladimir Golovnev * Copyright (C) 2010 Christophe Dumez * Copyright (C) 2010 Arnaud Demaiziere * @@ -30,10 +30,12 @@ #pragma once +#include #include #include #include #include +#include #include "base/path.h" #include "rss_item.h" @@ -53,6 +55,7 @@ namespace RSS namespace Private { + class FeedSerializer; class Parser; struct ParsingResult; } @@ -96,13 +99,12 @@ namespace RSS void handleDownloadFinished(const Net::DownloadResult &result); void handleParsingFinished(const Private::ParsingResult &result); void handleArticleRead(Article *article); + void handleArticleLoadFinished(const QVector &articles); 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); @@ -112,14 +114,17 @@ namespace RSS void downloadIcon(); int updateArticles(const QList &loadedArticles); - Session *m_session; - Private::Parser *m_parser; + Session *m_session = nullptr; + Private::Parser *m_parser = nullptr; + Private::FeedSerializer *m_serializer = nullptr; const QUuid m_uid; const QString m_url; QString m_title; QString m_lastBuildDate; bool m_hasError = false; bool m_isLoading = false; + bool m_isInitialized = false; + bool m_pendingRefresh = false; QHash m_articles; QList
m_articlesByDate; int m_unreadCount = 0; diff --git a/src/base/rss/rss_session.cpp b/src/base/rss/rss_session.cpp index 85d79f660..b6808a087 100644 --- a/src/base/rss/rss_session.cpp +++ b/src/base/rss/rss_session.cpp @@ -122,12 +122,14 @@ Session::~Session() { qDebug() << "Deleting RSS Session..."; - m_workingThread->quit(); - m_workingThread->wait(); - //store(); delete m_itemsByPath[u""_qs]; // deleting root folder + // some items may add I/O operation at destruction + // stop working thread after deleting root folder + m_workingThread->quit(); + m_workingThread->wait(); + qDebug() << "RSS Session deleted."; }