diff options
| author | Nikita Kostovsky <nikita@kostovsky.me> | 2025-08-10 09:54:19 +0200 |
|---|---|---|
| committer | Nikita Kostovsky <nikita@kostovsky.me> | 2025-08-10 09:54:19 +0200 |
| commit | 99c0f738e1d67aa16875110bfb5621bd7ea04e41 (patch) | |
| tree | 8ac12901d852b757544483204e795b12a029a78a | |
| parent | 0915fc1494df1cd15fc9c09bbf622f137406c84c (diff) | |
changing parsers
| -rw-r--r-- | CMakeLists.txt | 5 | ||||
| -rw-r--r-- | src/atomchannel.cpp | 107 | ||||
| -rw-r--r-- | src/atomchannel.h | 82 | ||||
| -rw-r--r-- | src/macros.h | 22 | ||||
| -rw-r--r-- | src/main.cpp | 14 | ||||
| -rw-r--r-- | src/playground.cpp | 72 | ||||
| -rw-r--r-- | src/rsshit_db.cpp | 5 |
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 {}; } |
