summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorNikita Kostovsky <nikita@kostovsky.me>2025-08-10 09:54:19 +0200
committerNikita Kostovsky <nikita@kostovsky.me>2025-08-10 09:54:19 +0200
commit99c0f738e1d67aa16875110bfb5621bd7ea04e41 (patch)
tree8ac12901d852b757544483204e795b12a029a78a
parent0915fc1494df1cd15fc9c09bbf622f137406c84c (diff)
changing parsers
-rw-r--r--CMakeLists.txt5
-rw-r--r--src/atomchannel.cpp107
-rw-r--r--src/atomchannel.h82
-rw-r--r--src/macros.h22
-rw-r--r--src/main.cpp14
-rw-r--r--src/playground.cpp72
-rw-r--r--src/rsshit_db.cpp5
7 files changed, 277 insertions, 30 deletions
diff --git a/CMakeLists.txt b/CMakeLists.txt
index 430e2d5..df07b07 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -9,8 +9,8 @@ set(CMAKE_AUTORCC ON)
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
-find_package(QT NAMES Qt6 Qt5 REQUIRED COMPONENTS Core Network Sql)
-find_package(Qt${QT_VERSION_MAJOR} REQUIRED COMPONENTS Core Network Sql)
+find_package(QT NAMES Qt6 Qt5 REQUIRED COMPONENTS Core Network Sql Xml)
+find_package(Qt${QT_VERSION_MAJOR} REQUIRED COMPONENTS Core Network Sql Xml)
file(GLOB_RECURSE SOURCES src/*.h src/*.cpp)
file(GLOB_RECURSE RESOURCES src/*.qrc)
@@ -21,6 +21,7 @@ target_link_libraries(${PROJECT_NAME}
Qt${QT_VERSION_MAJOR}::Core
Qt${QT_VERSION_MAJOR}::Network
Qt${QT_VERSION_MAJOR}::Sql
+ Qt${QT_VERSION_MAJOR}::Xml
)
include(GNUInstallDirs)
diff --git a/src/atomchannel.cpp b/src/atomchannel.cpp
index 95f4f0d..d89d4ef 100644
--- a/src/atomchannel.cpp
+++ b/src/atomchannel.cpp
@@ -79,9 +79,9 @@ AtomChannel::AtomChannel(QXmlStreamReader *xmlReader)
}
// TODO: move to a separate function, code is almost identical in all methods called `getDbId`
-int AtomChannel::getDbId()
+std::optional<int> AtomChannel::getDbId()
{
- if (m_id != rsshit::db::IdNotFound)
+ if (m_id)
return m_id;
const auto db = rsshit::db::open();
@@ -120,17 +120,23 @@ int AtomChannel::getDbId()
}
// TODO: move to a separate function, code is almost identical in all methods called `createInDb`
-int AtomChannel::createInDb()
+std::optional<int> AtomChannel::createInDb()
{
- if (m_id != rsshit::db::IdNotFound)
+ if (m_id)
return m_id;
const auto db = rsshit::db::open();
- if (!db)
+ if (!db) {
return rsshit::db::IdNotFound;
+ }
+
+ QSqlQuery insertQ;
+ if (!insertQ.prepare("insert into feeds(link, title, image_url) values(?, ?, ?)")) {
+ qCritical() << "cannot prepare query:" << insertQ.lastError().text();
- QSqlQuery insertQ{"insert into feeds(link, title, image_url) values(?, ?, ?)"};
+ return rsshit::db::IdNotFound;
+ }
insertQ.addBindValue(link);
insertQ.addBindValue(title);
insertQ.addBindValue(image.url);
@@ -148,7 +154,7 @@ int AtomChannel::createInDb()
}
// TODO: can be moved to IDbObject
-int AtomChannel::getOrInsertDbId()
+std::optional<int> AtomChannel::getOrInsertDbId()
{
const auto id = getDbId();
@@ -160,18 +166,19 @@ int AtomChannel::getOrInsertDbId()
QList<int> AtomChannel::syncDbItems()
{
- if (m_id == rsshit::db::IdNotFound)
+ if (!m_id)
m_id = getOrInsertDbId();
- if (m_id == rsshit::db::IdNotFound)
+ if (!m_id)
return {};
QList<int> result;
+ result.reserve(items.count());
for (auto &item : items) {
- auto id = item.getOrInsertDbId(this->m_id);
+ auto id = item.getOrInsertDbId(*this->m_id);
- if (id != rsshit::db::IdNotFound)
+ if (id)
result << id;
}
@@ -199,3 +206,81 @@ QDebug operator<<(QDebug debug, const AtomChannel &channel)
return debug;
}
+
+QDebug operator<<(QDebug debug, const Channel &channel)
+{
+ QDebugStateSaver saver{debug};
+
+ const auto &mo = Channel::staticMetaObject;
+
+ debug.nospace() << mo.className() << " {" << Qt::endl;
+
+ for (int i = 0; i < mo.propertyCount(); ++i) {
+ auto prop = mo.property(i);
+ debug.nospace() << "\t" << prop.name() << ": " << prop.readOnGadget(&channel) << Qt::endl;
+ }
+
+ debug.nospace() << "}";
+
+ return debug;
+}
+
+QDebug operator<<(QDebug debug, const Item &item)
+{
+ QDebugStateSaver saver{debug};
+
+ const auto &mo = Item::staticMetaObject;
+
+ debug.nospace() << mo.className() << " {" << Qt::endl;
+
+ for (int i = 0; i < mo.propertyCount(); ++i) {
+ auto prop = mo.property(i);
+ debug.nospace() << "\t" << prop.name() << ": " << prop.readOnGadget(&item) << Qt::endl;
+ }
+
+ debug.nospace() << "}";
+
+ return debug;
+}
+
+Channel::Channel(const QDomNode &node)
+ : DbItem{node}
+{
+ init(staticMetaObject, node);
+
+ // use 'atom:link href="LINK"' if any
+ const QString atomLinkTag = QStringLiteral("atom:link");
+ const QString hrefTag = QStringLiteral("href");
+ set_link(node.toElement()
+ .elementsByTagName(atomLinkTag)
+ .at(0)
+ .toElement()
+ .attribute(hrefTag, get_link().toString()));
+}
+
+bool Channel::isValid() const
+{
+ staticMetaObject;
+ return !m_title.isEmpty() && m_link.isValid();
+}
+
+DbItem::DbItem(const QDomNode &node)
+{
+ init(staticMetaObject, node);
+}
+
+void DbItem::init(const QMetaObject &mo, const QDomNode &node)
+{
+ for (int i = mo.propertyOffset(); i < mo.propertyCount(); ++i) {
+ auto prop = mo.property(i);
+ QVariant variant{node.toElement().elementsByTagName(prop.name()).at(0).toElement().text()};
+ const bool converted = variant.convert(QMetaType{prop.typeId()});
+ prop.writeOnGadget(this, converted ? std::move(variant) : QVariant{});
+ }
+}
+
+Item::Item(const QDomNode &node)
+ : DbItem{node}
+{
+ init(staticMetaObject, node);
+}
diff --git a/src/atomchannel.h b/src/atomchannel.h
index 7f7294a..87727d2 100644
--- a/src/atomchannel.h
+++ b/src/atomchannel.h
@@ -1,15 +1,87 @@
#pragma once
+#include <optional>
+
#include <QDateTime>
#include <QDebug>
+#include <QDomDocument>
#include <QList>
#include <QString>
+#include <QUrl>
#include "atomchannelimage.h"
#include "atomitem.h"
class QXmlStreamReader;
+// TODO: mark items as read in all user channels
+
+#define GADGET_PROPERTY(type, name) \
+protected: \
+ Q_PROPERTY(const type name READ get_##name WRITE set_##name) \
+public: \
+ const type &get_##name() const \
+ { \
+ return m_##name; \
+ } \
+ void set_##name(const type &name) \
+ { \
+ m_##name = name; \
+ } \
+\
+protected: \
+ type m_##name
+
+class DbItem
+{
+ Q_GADGET
+
+ GADGET_PROPERTY(std::optional<qlonglong>, id);
+
+ static constexpr auto dateFormat = Qt::DateFormat::RFC2822Date;
+
+public:
+ explicit DbItem(const QDomNode &node);
+
+public:
+ void init(const QMetaObject &mo, const QDomNode &node);
+};
+
+class Channel : public DbItem
+{
+ Q_GADGET
+
+ GADGET_PROPERTY(QString, title);
+ GADGET_PROPERTY(QUrl, link);
+
+public:
+ explicit Channel(const QDomNode &node);
+
+public:
+ bool isValid() const;
+};
+
+// TODO: <image>
+
+class Item : public DbItem
+{
+ Q_GADGET
+
+ GADGET_PROPERTY(QString, title);
+ GADGET_PROPERTY(QUrl, link);
+ GADGET_PROPERTY(QStringList, categories);
+ GADGET_PROPERTY(QString, guid);
+ GADGET_PROPERTY(QDateTime, pubDate);
+ GADGET_PROPERTY(QString, description);
+ GADGET_PROPERTY(QString, content);
+
+public:
+ explicit Item(const QDomNode &node);
+
+public:
+ bool isValid() const;
+};
+
class AtomChannel
{
public:
@@ -26,20 +98,20 @@ public:
* and return db id if any
* \return id on success, 0 otherwise
*/
- int getDbId();
+ std::optional<int> getDbId();
/*!
* \brief createInDb - create channel in db
* \return new channel id on success, 0 otherwise
*/
- int createInDb();
+ std::optional<int> createInDb();
/*!
* \brief getOrInsertDbId - get existing channel id or try to create a new
* channel and get its id
* \return existing or new channel id on success, 0 otherwise
*/
- int getOrInsertDbId();
+ std::optional<int> getOrInsertDbId();
/*!
* \brief syncDbItems - create items in db if not exist
@@ -51,7 +123,7 @@ public:
/*!
* \brief m_id - cache db m_id
*/
- int m_id{0};
+ std::optional<int> m_id{0};
QString title;
/*!
@@ -68,3 +140,5 @@ public:
};
QDebug operator<<(QDebug debug, const AtomChannel &channel);
+QDebug operator<<(QDebug debug, const Channel &channel);
+QDebug operator<<(QDebug debug, const Item &item);
diff --git a/src/macros.h b/src/macros.h
index 2459230..4556a09 100644
--- a/src/macros.h
+++ b/src/macros.h
@@ -2,3 +2,25 @@
#define PRINT_ATOM_FIELD(object, field) \
debug.nospace() << "\t" << #field << ": " << object.field << Qt::endl
+
+#define VERIFY_RETURN_VAL(cond, val) \
+ if (!cond) { \
+ return val; \
+ }
+
+#define VERIFY_CRIT_RETURN(cond) \
+ if (Q_UNLIKELY(!cond)) { \
+ qCritical() << #cond " == false"; \
+ return; \
+ }
+
+#define VERIFY_CRIT_RETURN_VAL(cond, val) \
+ if (Q_UNLIKELY(!cond)) { \
+ qCritical() << #cond " == false"; \
+ return val; \
+ }
+
+#define VERIFY_CONTINUE(cond) \
+ if (!cond) { \
+ continue; \
+ }
diff --git a/src/main.cpp b/src/main.cpp
index 2dab907..843d66a 100644
--- a/src/main.cpp
+++ b/src/main.cpp
@@ -5,6 +5,7 @@
#include <QStandardPaths>
#include <QTimer>
+#include "atomchannel.h"
#include "constants.h"
#include "playground.h"
#include "rsshit_db.h"
@@ -14,17 +15,6 @@ int main(int argc, char *argv[])
{
QCoreApplication a(argc, argv);
- // Set up code that uses the Qt event loop here.
- // Call a.quit() or a.exit() to quit the application.
- // A not very useful example would be including
- // #include <QTimer>
- // near the top of the file and calling
- // QTimer::singleShot(5000, &a, &QCoreApplication::quit);
- // which quits the application after 5 seconds.
-
- // If you do not need a running Qt event loop, remove the call
- // to a.exec() or use the Non-Qt Plain C++ Application template.
-
QFile dbSqliteResource{rsshit::db::dbSqliteResourceFilename};
const auto dataLocation = QStandardPaths::writableLocation(QStandardPaths::AppDataLocation);
qDebug() << "data location:" << dataLocation;
@@ -94,6 +84,8 @@ int main(int argc, char *argv[])
return;
}
+
+ qDebug() << "added channel to db:" << channel->link;
});
return a.exec();
diff --git a/src/playground.cpp b/src/playground.cpp
index 476d716..117fb31 100644
--- a/src/playground.cpp
+++ b/src/playground.cpp
@@ -1,6 +1,7 @@
#include "playground.h"
#include <QDebug>
+#include <QDomDocument>
#include <QLocale>
#include <QNetworkAccessManager>
#include <QNetworkReply>
@@ -34,6 +35,77 @@ void Playground::onReplyFinished(QNetworkReply *reply)
std::shared_ptr<AtomChannel> Playground::parseFeed(QIODevice *ioDevice)
{
+ if (!ioDevice) {
+ qCritical() << "ioDevice is nullptr";
+ return {};
+ }
+
+ QDomDocument doc;
+
+ if (!doc.setContent(ioDevice)) {
+ qCritical() << "cannot set io device to dom doc";
+
+ return {};
+ }
+
+ const auto rssTag = QStringLiteral("rss");
+ const auto channelTag = QStringLiteral("channel");
+ const auto titleTag = QStringLiteral("title");
+ const auto linkTag = QStringLiteral("link");
+ const auto itemTag = QStringLiteral("item");
+
+ const auto documentElement = doc.documentElement();
+
+ // const auto channelNode = documentElement.elementsByTagName(rssTag).at(0);
+ const auto channelNode = documentElement.firstChildElement();
+ VERIFY_CRIT_RETURN_VAL(!channelNode.isNull(), {});
+
+ Channel tmpChannel{channelNode};
+ qDebug() << tmpChannel;
+
+ const auto itemNodes = channelNode.elementsByTagName(itemTag);
+
+ for (const auto &itemNode : itemNodes) {
+ const Item item{itemNode};
+ qDebug() << item;
+ }
+
+ return {};
+
+ // const auto channelNode = rssNode.toElement().elementsByTagName(channelTag).at(0);
+ // VERIFY_CRIT_RETURN_VAL(!channelNode.isNull(), {});
+
+ const auto childNode = [](const QDomNode &parent, const QString &tag) {
+ return parent.toElement().elementsByTagName(tag).at(0);
+ };
+
+ const auto childNodeText = [childNode](const QDomNode &parent, const QString &tag) {
+ const auto node = childNode(parent, tag);
+ VERIFY_RETURN_VAL(!node.isNull(), QString{});
+ return node.toElement().text();
+ };
+
+ qDebug() << "AZAZA: title:" << childNodeText(channelNode, titleTag);
+ qDebug() << "AZAZA: link:" << childNodeText(channelNode, linkTag);
+
+ QDomNode node{channelNode.firstChildElement()};
+
+ while (!node.isNull()) {
+ const auto element = node.toElement();
+
+ if (element.isNull()) {
+ node = node.nextSibling();
+
+ continue;
+ }
+
+ // const auto title = childNodeText(element, titleTag);
+ node = node.nextSibling();
+ // qDebug() << "AZAZA: title:" << title;
+ }
+
+ return {};
+
// TODO: check for nullptr
// TODO: try `QNetworkReply::readyRead` instead of `QNetworkAccessManager::finished`
std::shared_ptr<AtomChannel> channel{};
diff --git a/src/rsshit_db.cpp b/src/rsshit_db.cpp
index b690eaa..2224c57 100644
--- a/src/rsshit_db.cpp
+++ b/src/rsshit_db.cpp
@@ -24,8 +24,9 @@ const QString image_url{"image_url"};
bool applyToDb(const std::shared_ptr<AtomChannel> channel)
{
- if (channel->getOrInsertDbId() == rsshit::db::IdNotFound)
+ if (channel->getOrInsertDbId() == rsshit::db::IdNotFound) {
return false;
+ }
if (channel->items.isEmpty())
return true;
@@ -50,7 +51,7 @@ std::optional<QSqlDatabase> rsshit::db::open()
db.setDatabaseName(dbFilepath);
if (!db.open()) {
- qWarning() << "cannot open db:" << db.lastError().text();
+ qCritical() << "cannot open db:" << db.lastError().text();
return {};
}