diff options
| author | Nikita Kostovsky <nikita@kostovsky.me> | 2025-06-22 16:54:02 +0200 |
|---|---|---|
| committer | Nikita Kostovsky <nikita@kostovsky.me> | 2025-06-22 16:54:02 +0200 |
| commit | f674e179d602d3ccb9818d28fe06f371059449dc (patch) | |
| tree | 996fb624986512de91581a18332f004d34220ba2 | |
parse and insert feeds and items
| -rw-r--r-- | .gitignore | 82 | ||||
| -rw-r--r-- | CMakeLists.txt | 28 | ||||
| -rw-r--r-- | src/atomchannel.cpp | 197 | ||||
| -rw-r--r-- | src/atomchannel.h | 69 | ||||
| -rw-r--r-- | src/atomchannelimage.cpp | 69 | ||||
| -rw-r--r-- | src/atomchannelimage.h | 32 | ||||
| -rw-r--r-- | src/atomitem.cpp | 180 | ||||
| -rw-r--r-- | src/atomitem.h | 66 | ||||
| -rw-r--r-- | src/constants.h | 23 | ||||
| -rw-r--r-- | src/iatomobject.h | 9 | ||||
| -rw-r--r-- | src/idbobject.h | 24 | ||||
| -rw-r--r-- | src/macros.h | 4 | ||||
| -rw-r--r-- | src/main.cpp | 90 | ||||
| -rw-r--r-- | src/playground.cpp | 108 | ||||
| -rw-r--r-- | src/playground.h | 27 | ||||
| -rw-r--r-- | src/rsshit.qrc | 5 | ||||
| -rw-r--r-- | src/rsshit_db.cpp | 60 | ||||
| -rw-r--r-- | src/rsshit_db.h | 17 | ||||
| -rw-r--r-- | src/sql/db.sqlite | bin | 0 -> 36864 bytes |
19 files changed, 1090 insertions, 0 deletions
diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..aa3808c --- /dev/null +++ b/.gitignore @@ -0,0 +1,82 @@ +# This file is used to ignore files which are generated +# ---------------------------------------------------------------------------- + +*~ +*.autosave +*.a +*.core +*.moc +*.o +*.obj +*.orig +*.rej +*.so +*.so.* +*_pch.h.cpp +*_resource.rc +*.qm +.#* +*.*# +core +!core/ +tags +.DS_Store +.directory +*.debug +Makefile* +*.prl +*.app +moc_*.cpp +ui_*.h +qrc_*.cpp +Thumbs.db +*.res +*.rc +/.qmake.cache +/.qmake.stash + +# qtcreator generated files +*.pro.user* +*.qbs.user* +CMakeLists.txt.user* + +# xemacs temporary files +*.flc + +# Vim temporary files +.*.swp + +# Visual Studio generated files +*.ib_pdb_index +*.idb +*.ilk +*.pdb +*.sln +*.suo +*.vcproj +*vcproj.*.*.user +*.ncb +*.sdf +*.opensdf +*.vcxproj +*vcxproj.* + +# MinGW generated files +*.Debug +*.Release + +# Python byte code +*.pyc + +# Binaries +# -------- +*.dll +*.exe + +# Directories with generated files +.moc/ +.obj/ +.pch/ +.rcc/ +.uic/ +/build*/ diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 0000000..f5ca22e --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,28 @@ +cmake_minimum_required(VERSION 3.16) + +project(rsshit LANGUAGES CXX) + +set(CMAKE_AUTOUIC ON) +set(CMAKE_AUTOMOC ON) +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) + +file(GLOB_RECURSE SOURCES src/*.h src/*.cpp src/*.qrc) +add_executable(${PROJECT_NAME} ${SOURCES}) + +target_link_libraries(${PROJECT_NAME} + Qt${QT_VERSION_MAJOR}::Core + Qt${QT_VERSION_MAJOR}::Network + Qt${QT_VERSION_MAJOR}::Sql +) + +include(GNUInstallDirs) +install(TARGETS ${PROJECT_NAME} + LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR} + RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR} +) diff --git a/src/atomchannel.cpp b/src/atomchannel.cpp new file mode 100644 index 0000000..6982e8b --- /dev/null +++ b/src/atomchannel.cpp @@ -0,0 +1,197 @@ +#include "atomchannel.h" + +#include <QSqlError> +#include <QSqlQuery> +#include <QXmlStreamReader> + +#include "constants.h" +#include "macros.h" +#include "rsshit_db.h" + +AtomChannel::AtomChannel(QXmlStreamReader *xmlReader) +{ + Q_ASSERT(xmlReader != nullptr); + + const QString titleTag{"title"}; + const QString linkTag{"link"}; + const QString descriptionTag{"description"}; + const QString lastBuildDateTag{"lastBuildDate"}; + const QString languageTag{"language"}; + + while (!xmlReader->atEnd() && !xmlReader->hasError()) { + const auto itemNext = xmlReader->readNext(); + + switch (itemNext) { + case QXmlStreamReader::TokenType::StartElement: { + const auto name = xmlReader->name(); + // qDebug() << __func__ << ": StartElement" << name; + // qDebug() << __func__ << "namespaceUri:" << xmlReader->namespaceUri(); + // qDebug() << __func__ << "prefix:" << xmlReader->prefix(); + // qDebug() << __func__ << "qualifiedName:" << xmlReader->qualifiedName(); + // const auto elementText = xmlReader->readElementText(); + + if (name == titleTag) + title = xmlReader->readElementText(); + else if (name == linkTag) + link = xmlReader->readElementText(); + else if (name == descriptionTag) + description = xmlReader->readElementText(); + else if (name == lastBuildDateTag) + lastBuildDate = QDateTime::fromString(xmlReader->readElementText(), + Qt::DateFormat::RFC2822Date); + else if (name == languageTag) + language = QLocale{xmlReader->readElementText()}.language(); + else if (name == AtomChannelImage::tag) { + // qDebug() << "got image tag"; + image = AtomChannelImage{xmlReader}; + // qDebug() << image; + } else if (name == AtomItem::tag) { + items << AtomItem{xmlReader}; + // qDebug() << items.constLast(); + // qDebug() << __func__ << "got feed item"; + } else { + // qDebug() << "exit" << __func__; + // qDebug() << __func__ << "unknown tag:" << name; + continue; + } + + break; + } + case QXmlStreamReader::TokenType::EndElement: { + // qDebug() << "EndElement: " << xmlReader->name(); + + if (xmlReader->name() == AtomChannel::xmlTag) + return; + } + case QXmlStreamReader::TokenType::Characters: { + const auto characters = xmlReader->text().toString().simplified(); + + if (characters.isEmpty()) + break; + + qDebug() << "channel: characters: " << characters; + break; + } + } + } + + qDebug() << "exit " << __func__; +} + +int AtomChannel::getDbId() +{ + if (dbId != rsshit::db::IdNotFound) + return dbId; + + const auto db = rsshit::db::open(); + + if (!db) + return rsshit::db::IdNotFound; + + QSqlQuery selectQ{"select id from feeds where link=?"}; + selectQ.addBindValue(link); + + if (!selectQ.exec()) { + qWarning() << "cannot exec query" << selectQ.lastQuery() << ":" + << selectQ.lastError().text() << ":" << selectQ.executedQuery(); + + return rsshit::db::IdNotFound; + } + + if (!selectQ.next()) + return rsshit::db::IdNotFound; + + const auto idVariant = selectQ.value(rsshit::db::idTag); + + if (!idVariant.isValid() || !idVariant.canConvert<int>()) + return rsshit::db::IdNotFound; + + bool ok{false}; + const auto result = idVariant.toInt(&ok); + + if (!ok) { + qWarning() << "got invalid id from db:" << idVariant; + + return rsshit::db::IdNotFound; + } + + return result; +} + +int AtomChannel::createInDb() +{ + if (dbId != rsshit::db::IdNotFound) + return dbId; + + const auto db = rsshit::db::open(); + + if (!db) + return rsshit::db::IdNotFound; + + QSqlQuery insertQ{"insert into feeds(link, title, image_url) values(?, ?, ?)"}; + insertQ.addBindValue(link); + insertQ.addBindValue(title); + insertQ.addBindValue(image.url); + + if (!insertQ.exec()) { + qWarning() << "cannot exec query" << insertQ.lastQuery() << ":" + << insertQ.lastError().text() << ":" << insertQ.executedQuery(); + + return rsshit::db::IdNotFound; + } + + return insertQ.lastInsertId().toInt(); +} + +// TODO: can be moved to IDbObject +int AtomChannel::getOrInsertDbId() +{ + const auto id = getDbId(); + + if (id != rsshit::db::IdNotFound) + return id; + + return createInDb(); +} + +QList<int> AtomChannel::syncDbItems() +{ + if (dbId == rsshit::db::IdNotFound) + dbId = getOrInsertDbId(); + + if (dbId == rsshit::db::IdNotFound) + return {}; + + QList<int> result; + + for (auto &item : items) { + auto id = item.getOrInsertDbId(this->dbId); + + if (id != rsshit::db::IdNotFound) + result << id; + } + + return result; +} + +QDebug operator<<(QDebug debug, const AtomChannel &channel) +{ + QDebugStateSaver saver{debug}; + + debug.nospace() << typeid(AtomChannel).name() << " {" << Qt::endl; + + PRINT_ATOM_FIELD(channel, title); + PRINT_ATOM_FIELD(channel, link); + PRINT_ATOM_FIELD(channel, description); + PRINT_ATOM_FIELD(channel, lastBuildDate); + PRINT_ATOM_FIELD(channel, language); + debug << "\timage:\n" << channel.image << Qt::endl; + debug << "\titems count:" << channel.items.size() << Qt::endl; + + for (const auto &item : channel.items) + debug << item << Qt::endl; + + debug.nospace() << "}"; + + return debug; +} diff --git a/src/atomchannel.h b/src/atomchannel.h new file mode 100644 index 0000000..0498a64 --- /dev/null +++ b/src/atomchannel.h @@ -0,0 +1,69 @@ +#pragma once + +#include <QDateTime> +#include <QDebug> +#include <QList> +#include <QString> + +#include "atomchannelimage.h" +#include "atomitem.h" + +class QXmlStreamReader; + +class AtomChannel +{ +public: + // TODO: move to interface + static inline const QString xmlTag{"channel"}; + +public: + explicit AtomChannel(QXmlStreamReader *xmlReader); + +public: + /*! + * \brief getDbId - check if channel with corresponding `link` exists in db + * and return db id if any + * \return id on success, 0 otherwise + */ + int getDbId(); + + /*! + * \brief createInDb - create channel in db + * \return new channel id on success, 0 otherwise + */ + 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(); + + /*! + * \brief syncDbItems - create items in db if not exist + * \return list of new items ids + */ + QList<int> syncDbItems(); + +public: + /*! + * \brief dbId - cache db id + */ + int dbId{0}; + + QString title; + /*! + * \brief link - field called `link` in atom xml. Points to feed itself + */ + QUrl link; + QString description; + QDateTime lastBuildDate; + QLocale::Language language; + // ... + // TODO: shared_ptr + AtomChannelImage image; + QList<AtomItem> items; +}; + +QDebug operator<<(QDebug debug, const AtomChannel &channel); diff --git a/src/atomchannelimage.cpp b/src/atomchannelimage.cpp new file mode 100644 index 0000000..4b9210d --- /dev/null +++ b/src/atomchannelimage.cpp @@ -0,0 +1,69 @@ +#include "atomchannelimage.h" + +#include <QXmlStreamReader> + +#include "macros.h" + +AtomChannelImage::AtomChannelImage(QXmlStreamReader *xmlReader) +{ + Q_ASSERT(xmlReader != nullptr); + + const QString urlTag{"url"}; + const QString titleTag{"title"}; + const QString linkTag{"link"}; + const QString widthTag{"width"}; + const QString heightTag{"height"}; + + while (!xmlReader->atEnd() && !xmlReader->hasError()) { + const auto itemNext = xmlReader->readNext(); + + switch (itemNext) { + case QXmlStreamReader::TokenType::StartElement: { + const auto name = xmlReader->name(); + // qDebug() << __func__ << ":" << name; + const auto elementText = xmlReader->readElementText(); + + if (name == urlTag) + url = elementText; + else if (name == titleTag) + title = elementText; + else if (name == linkTag) + link = elementText; + else if (name == widthTag) + width = elementText.toInt(); + else if (name == heightTag) + height = elementText.toInt(); + + break; + } + case QXmlStreamReader::TokenType::EndElement: + // qDebug() << "EndElement: " << xmlReader->name(); + return; + case QXmlStreamReader::TokenType::Characters: + const auto characters = xmlReader->text().toString().simplified(); + + if (characters.isEmpty()) + break; + + qDebug() << "image: characters: " << characters; + break; + } + } +} + +QDebug operator<<(QDebug debug, const AtomChannelImage &image) +{ + QDebugStateSaver saver{debug}; + + debug.nospace() << typeid(AtomChannelImage).name() << " {" << Qt::endl; + + PRINT_ATOM_FIELD(image, url); + PRINT_ATOM_FIELD(image, title); + PRINT_ATOM_FIELD(image, link); + PRINT_ATOM_FIELD(image, width); + PRINT_ATOM_FIELD(image, height); + + debug.nospace() << "}"; + + return debug; +} diff --git a/src/atomchannelimage.h b/src/atomchannelimage.h new file mode 100644 index 0000000..8c43034 --- /dev/null +++ b/src/atomchannelimage.h @@ -0,0 +1,32 @@ +#pragma once + +#include <QDebug> +#include <QString> +#include <QUrl> + +class QXmlStreamReader; + +struct AtomChannelImage +{ + // TODO: move to interface + static inline const QString tag{"image"}; + + // TODO: remove, use shared_ptr in AtomChannel + AtomChannelImage() = default; + explicit AtomChannelImage(QXmlStreamReader *xmlReader); + + /*! + * \brief url - url of image file + */ + QUrl url; + QString title; + + /*! + * \brief link - link to website + */ + QUrl link; + size_t width{0}; + size_t height{0}; +}; + +QDebug operator<<(QDebug debug, const AtomChannelImage &image); diff --git a/src/atomitem.cpp b/src/atomitem.cpp new file mode 100644 index 0000000..5f099f6 --- /dev/null +++ b/src/atomitem.cpp @@ -0,0 +1,180 @@ +#include "atomitem.h" + +#include <QSqlError> +#include <QSqlQuery> +#include <QXmlStreamReader> + +#include "atomchannel.h" +#include "constants.h" +#include "macros.h" +#include "rsshit_db.h" + +AtomItem::AtomItem(QXmlStreamReader *xmlReader) +{ + Q_ASSERT(xmlReader != nullptr); + + const QString titleTag{"title"}; + const QString linkTag{"link"}; + const QString categoryTag{"category"}; + const QString guidTag{"guid"}; + const QString pubDateTag{"pubDate"}; + const QString descriptionTag{"description"}; + const QString encodedTag{"encoded"}; + + while (!xmlReader->atEnd() && !xmlReader->hasError()) { + const auto itemNext = xmlReader->readNext(); + + switch (itemNext) { + case QXmlStreamReader::TokenType::StartElement: { + const auto name = xmlReader->name(); + const auto elementText = xmlReader->readElementText(); + + if (name == titleTag) + title = elementText; + else if (name == linkTag) + link = elementText; + else if (name == categoryTag) + categories.append(elementText); + else if (name == guidTag) + guid = AtomGuid{elementText}; + else if (name == pubDateTag) + pubDate = QDateTime::fromString(elementText, Qt::DateFormat::RFC2822Date); + else if (name == descriptionTag) + description = elementText; + else if (name == encodedTag) + encoded = elementText; + + break; + } + case QXmlStreamReader::TokenType::EndElement: + // qDebug() << "EndElement: " << xmlReader->name(); + return; + case QXmlStreamReader::TokenType::Characters: + const auto characters = xmlReader->text().toString().simplified(); + + if (characters.isEmpty()) + break; + + qDebug() << "item: characters: " << characters; + break; + } + } +} + +int AtomItem::getDbId() +{ + if (dbId != rsshit::db::IdNotFound) + return dbId; + + const auto db = rsshit::db::open(); + + if (!db) + return rsshit::db::IdNotFound; + + QSqlQuery selectQ{"select id from items where link=?"}; + selectQ.addBindValue(link); + + if (!selectQ.exec()) { + qWarning() << "cannot exec query" << selectQ.lastQuery() << ":" + << selectQ.lastError().text() << ":" << selectQ.executedQuery(); + + return rsshit::db::IdNotFound; + } + + if (!selectQ.next()) + return rsshit::db::IdNotFound; + + const auto idVariant = selectQ.value(rsshit::db::idTag); + + if (!idVariant.isValid() || !idVariant.canConvert<int>()) + return rsshit::db::IdNotFound; + + bool ok{false}; + const auto result = idVariant.toInt(&ok); + + if (!ok) { + qWarning() << "got invalid id from db:" << idVariant; + + return rsshit::db::IdNotFound; + } + + return result; +} + +int AtomItem::createInDb(const int feedId) +{ + if (dbId != rsshit::db::IdNotFound) + return dbId; + + const auto db = rsshit::db::open(); + + if (!db) + return rsshit::db::IdNotFound; + + QSqlQuery insertQ{ + "insert into items(feed_fk, pub_datetime_unix, title, link, author, description)" + "values(?, ?, ?, ?, ?, ?)"}; + + insertQ.addBindValue(feedId); + insertQ.addBindValue(pubDate.toSecsSinceEpoch()); + insertQ.addBindValue(title); + insertQ.addBindValue(link); + insertQ.addBindValue(author); + insertQ.addBindValue(description); + + if (!insertQ.exec()) { + qWarning() << "cannot exec query" << insertQ.lastQuery() << ":" + << insertQ.lastError().text() << ":" << insertQ.executedQuery(); + + return rsshit::db::IdNotFound; + } + + return insertQ.lastInsertId().toInt(); +} + +int AtomItem::getOrInsertDbId(const int feedId) +{ + const auto id = getDbId(); + + if (id != rsshit::db::IdNotFound) + return id; + + return createInDb(feedId); +} + +QDebug operator<<(QDebug debug, const AtomItem &item) +{ + QDebugStateSaver saver{debug}; + + debug.nospace() << typeid(AtomItem).name() << " {" << Qt::endl; + + PRINT_ATOM_FIELD(item, title); + PRINT_ATOM_FIELD(item, link); + PRINT_ATOM_FIELD(item, categories); + PRINT_ATOM_FIELD(item, guid); + PRINT_ATOM_FIELD(item, pubDate); + // PRINT_ATOM_ITEM_FIELD(description); + // PRINT_ATOM_ITEM_FIELD(encoded); + + auto halfSize = item.description.size() / 2; + constexpr decltype(halfSize) maxLeft{70}; + constexpr decltype(halfSize) maxRight{20}; + + auto left = std::min(maxLeft, halfSize); + auto right = std::min(maxRight, halfSize); + + debug.nospace().noquote() << "\tdescription: \"" << item.description.left(left) << "..." + << item.description.right(right) << '"' << Qt::endl; + + halfSize = item.encoded.size() / 2; + + left = std::min(maxLeft, halfSize); + right = std::min(maxRight, halfSize); + + debug.nospace().noquote() << "\tencoded: \"" << item.encoded.left(left) << "..." + << item.encoded.right(right) << '"' << Qt::endl; + + debug.nospace() << "}"; + + return debug; +} diff --git a/src/atomitem.h b/src/atomitem.h new file mode 100644 index 0000000..39917b5 --- /dev/null +++ b/src/atomitem.h @@ -0,0 +1,66 @@ +#pragma once + +#include <QDateTime> +#include <QDebug> +#include <QString> +#include <QUrl> + +class QXmlStreamReader; + +class AtomChannel; + +struct AtomGuid : public QString +{ + using QString::QString; + + // NOTE: unused + bool isPermalink{false}; +}; + +class AtomItem +{ +public: + // TODO: move to interface + static inline const QString tag{"item"}; + +public: + explicit AtomItem(QXmlStreamReader *xmlReader); + +public: + /*! + * \brief getDbId - check if item with corresponding `link` exists in db + * and return db id if any + * \return id on success, 0 otherwise + */ + int getDbId(); + + /*! + * \brief createInDb - create item in db + * \return new item id on success, 0 otherwise + */ + int createInDb(const int feedId); + + /*! + * \brief getOrInsertDbId - get existing item id or try to create a new + * item and get its id + * \return existing or new item id on success, 0 otherwise + */ + int getOrInsertDbId(const int feedId); + +public: + /*! + * \brief dbId - cache db id + */ + int dbId{0}; + + QString title; + QUrl link; + QString author; + QStringList categories; + AtomGuid guid; + QDateTime pubDate; + QString description; + QString encoded; +}; + +QDebug operator<<(QDebug debug, const AtomItem &item); diff --git a/src/constants.h b/src/constants.h new file mode 100644 index 0000000..c27b777 --- /dev/null +++ b/src/constants.h @@ -0,0 +1,23 @@ +#pragma once + +namespace rsshit { +namespace db { +// TODO: read from config +const QString dbSqliteResourceFilename{":/sql/db.sqlite"}; +const QString driver{"QSQLITE"}; + +const QString feedsTableName{"feeds"}; + +const QString idTag{"id"}; +const QString linkTag{"link"}; + +constexpr int IdNotFound{0}; + +namespace feeds_field_names { +const QString link{"link"}; +const QString title{"title"}; +const QString image_url{"image_url"}; +} // namespace feeds_field_names + +} // namespace db +} // namespace rsshit diff --git a/src/iatomobject.h b/src/iatomobject.h new file mode 100644 index 0000000..f6e28b1 --- /dev/null +++ b/src/iatomobject.h @@ -0,0 +1,9 @@ +#pragma once + +// #include <QString> + +// class IAtomObject +// { +// public: +// virtual QString tag() const = 0; +// }; diff --git a/src/idbobject.h b/src/idbobject.h new file mode 100644 index 0000000..8b04551 --- /dev/null +++ b/src/idbobject.h @@ -0,0 +1,24 @@ +#pragma once + +class IDbObject +{ +public: + /*! + * \brief getDbId - check if object exists in db and return its id if any + * \return id on success, 0 otherwise + */ + virtual int getDbId() = 0; + + /*! + * \brief createInDb - create object in db + * \return new object id on success, 0 otherwise + */ + virtual int createInDb() = 0; + + /*! + * \brief getOrInsertDbId - get existing object id or try to create a new + * object and get its id + * \return existing or new object id on success, 0 otherwise + */ + virtual int getOrInsertDbId() = 0; +}; diff --git a/src/macros.h b/src/macros.h new file mode 100644 index 0000000..2459230 --- /dev/null +++ b/src/macros.h @@ -0,0 +1,4 @@ +#pragma once + +#define PRINT_ATOM_FIELD(object, field) \ + debug.nospace() << "\t" << #field << ": " << object.field << Qt::endl diff --git a/src/main.cpp b/src/main.cpp new file mode 100644 index 0000000..f9fab22 --- /dev/null +++ b/src/main.cpp @@ -0,0 +1,90 @@ +#include <QCoreApplication> +#include <QDir> +#include <QFile> +#include <QFileInfo> +#include <QStandardPaths> +#include <QTimer> + +#include "constants.h" +#include "playground.h" +#include "rsshit_db.h" + +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; + + if (!QDir{}.mkpath(dataLocation)) { + qWarning() << "cannot create " << dataLocation; + return EXIT_FAILURE; + } + + const QString newDbFilepath{dataLocation + '/' + QFileInfo{dbSqliteResource}.fileName()}; + + // #define RECREATE_DB_FILE + +#ifdef RECREATE_DB_FILE + if (!QFile::remove(newDbFilepath)) { + qWarning() << "cannot remove" << newDbFilepath; + } + + if (!dbSqliteResource.copy(newDbFilepath)) { + qWarning() << "cannot copy db file (" << dbSqliteResource.fileName() + << ") from resources to data dir (" << newDbFilepath + << "):" << dbSqliteResource.errorString(); + return EXIT_FAILURE; + } +#endif + + QFile newDbFile{newDbFilepath}; + + if (!newDbFile.setPermissions(QFileDevice::Permission::ReadUser + | QFileDevice::Permission::WriteUser)) { + qWarning() << "cannot set file permissions:" << newDbFile.fileName() << Qt::endl + << newDbFile.errorString(); + + return EXIT_FAILURE; + } + + Playground playground; + + QTimer::singleShot(0, [&]() { + // const QString url{"https://jenngott.com/feed/"}; + // const QUrl url{"https://liliputing.com/feed"}; + // playground.fetchUrl(url); + // return; + // QFile f{"/tmp/rss/feed"}; + QFile f{"/home/nikita/tmp/rss/feed"}; + // QFile f{"/tmp/rss/feed.1"}; + // QFile f{"/tmp/rss/rss"}; + + if (!f.open(QFile::ReadOnly)) { + qWarning() << "cannot open file" << f.fileName() << ":" << f.errorString(); + return; + } + + const auto channel = playground.parseFeed(&f); + + if (!applyToDb(channel)) { + qWarning() << "cannot apply channel to db"; + + return; + } + }); + + return a.exec(); +} diff --git a/src/playground.cpp b/src/playground.cpp new file mode 100644 index 0000000..476d716 --- /dev/null +++ b/src/playground.cpp @@ -0,0 +1,108 @@ +#include "playground.h" + +#include <QDebug> +#include <QLocale> +#include <QNetworkAccessManager> +#include <QNetworkReply> +#include <QTimer> +#include <QXmlStreamReader> + +#include "atomchannel.h" +#include "macros.h" + +Playground::Playground() + : m_networkManager{new QNetworkAccessManager} +{ + connect(m_networkManager, &QNetworkAccessManager::finished, this, &Playground::onReplyFinished); +} + +void Playground::fetchUrl(const QUrl &url) +{ + m_networkManager->get(QNetworkRequest{url}); +} + +void Playground::onReplyFinished(QNetworkReply *reply) +{ + // TODO: check for nullptr + + reply->deleteLater(); + + qDebug() << "reply size:" << reply->size(); + + parseFeed(reply); +} + +std::shared_ptr<AtomChannel> Playground::parseFeed(QIODevice *ioDevice) +{ + // TODO: check for nullptr + // TODO: try `QNetworkReply::readyRead` instead of `QNetworkAccessManager::finished` + std::shared_ptr<AtomChannel> channel{}; + QXmlStreamReader xmlReader{ioDevice}; + + while (!xmlReader.atEnd() && !xmlReader.hasError()) { + const auto next = xmlReader.readNext(); + + // qDebug() << "\tnext:" << next; + + switch (next) { + case QXmlStreamReader::TokenType::NoToken: + qDebug() << "NoToken"; + break; + case QXmlStreamReader::TokenType::Invalid: + qDebug() << "Invalid"; + break; + case QXmlStreamReader::TokenType::StartDocument: + // qDebug() << "StartDocument"; + break; + case QXmlStreamReader::TokenType::EndDocument: + // qDebug() << "EndDocument"; + break; + case QXmlStreamReader::TokenType::StartElement: { + const auto startElementName = xmlReader.name(); + // qDebug() << "StartElement: " << startElementName; + + if (startElementName == AtomChannel::xmlTag) { + channel = std::make_shared<AtomChannel>(&xmlReader); + // qDebug() << *channel; + } /* else if (startElementName == itemTag) { + const AtomItem item{&xmlReader}; + qDebug() << item; + }*/ + + break; + } + case QXmlStreamReader::TokenType::EndElement: + // qDebug() << "EndElement: " << xmlReader.name(); + break; + case QXmlStreamReader::TokenType::Characters: { + const auto characters = xmlReader.text().toString().simplified(); + + if (characters.isEmpty()) + break; + + qDebug() << "Characters"; + break; + } + case QXmlStreamReader::TokenType::Comment: + qDebug() << "Comment"; + break; + case QXmlStreamReader::TokenType::DTD: + qDebug() << "DTD"; + break; + case QXmlStreamReader::TokenType::EntityReference: + qDebug() << "EntityReference"; + break; + case QXmlStreamReader::TokenType::ProcessingInstruction: + qDebug() << "ProcessingInstruction"; + break; + } + + if (xmlReader.hasError()) { + qWarning() << "xml parsing error:" << xmlReader.errorString(); + + return {}; + } + } + + return channel; +} diff --git a/src/playground.h b/src/playground.h new file mode 100644 index 0000000..784b06b --- /dev/null +++ b/src/playground.h @@ -0,0 +1,27 @@ +#pragma once + +#include <QDateTime> +#include <QObject> +#include <QSize> +#include <QUrl> + +class QNetworkAccessManager; +class QNetworkReply; + +class AtomChannel; + +class Playground : public QObject +{ + Q_OBJECT + +public: + explicit Playground(); + +public slots: + void fetchUrl(const QUrl &url); + void onReplyFinished(QNetworkReply *reply); + std::shared_ptr<AtomChannel> parseFeed(QIODevice *ioDevice); + +private: + QNetworkAccessManager *m_networkManager{nullptr}; +}; diff --git a/src/rsshit.qrc b/src/rsshit.qrc new file mode 100644 index 0000000..ccc5399 --- /dev/null +++ b/src/rsshit.qrc @@ -0,0 +1,5 @@ +<RCC> + <qresource prefix="/"> + <file>sql/db.sqlite</file> + </qresource> +</RCC> diff --git a/src/rsshit_db.cpp b/src/rsshit_db.cpp new file mode 100644 index 0000000..d5d2a2a --- /dev/null +++ b/src/rsshit_db.cpp @@ -0,0 +1,60 @@ +#include "rsshit_db.h" + +#include <QDebug> +#include <QFileInfo> +#include <QSqlDatabase> +#include <QSqlError> +#include <QSqlQuery> +#include <QSqlRecord> +#include <QSqlResult> +#include <QSqlTableModel> +#include <QStandardPaths> + +#include "atomchannel.h" +#include "constants.h" +#include "playground.h" + +namespace { +namespace feeds_field_names { +const QString link{"link"}; +const QString title{"title"}; +const QString image_url{"image_url"}; +} // namespace feeds_field_names +} // namespace + +bool applyToDb(const std::shared_ptr<AtomChannel> channel) +{ + if (channel->getOrInsertDbId() == rsshit::db::IdNotFound) + return false; + + if (channel->items.isEmpty()) + return true; + + channel->syncDbItems(); + + return true; +} + +std::optional<QSqlDatabase> rsshit::db::open() +{ + auto db = QSqlDatabase::database(); + + if (db.isValid() && db.isOpen()) + return db; + + db = QSqlDatabase::addDatabase(rsshit::db::driver); + const auto dataLocation = QStandardPaths::writableLocation(QStandardPaths::AppDataLocation); + const QString dbFilepath{dataLocation + '/' + + QFileInfo{rsshit::db::dbSqliteResourceFilename}.fileName()}; + + db.setDatabaseName(dbFilepath); + qDebug() << "open" << dbFilepath; + + if (!db.open()) { + qWarning() << "cannot open db:" << db.lastError().text(); + + return {}; + } + + return db; +} diff --git a/src/rsshit_db.h b/src/rsshit_db.h new file mode 100644 index 0000000..f4dbeee --- /dev/null +++ b/src/rsshit_db.h @@ -0,0 +1,17 @@ +#pragma once + +#include <memory> +#include <optional> + +#include <QSqlDatabase> +#include <QString> + +struct AtomChannel; + +namespace rsshit { +namespace db { +std::optional<QSqlDatabase> open(); +} // namespace db +} // namespace rsshit + +bool applyToDb(const std::shared_ptr<AtomChannel> channel); diff --git a/src/sql/db.sqlite b/src/sql/db.sqlite Binary files differnew file mode 100644 index 0000000..c982736 --- /dev/null +++ b/src/sql/db.sqlite |
