/** * SPDX-FileCopyrightText: 2021 Bart De Vries * * SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL */ #include #include #include #include #include #include #include #include #include "datamanager.h" #include "fetcher.h" #include "database.h" #include "settingsmanager.h" DataManager::DataManager() { // connect signals to lambda slots connect(&Fetcher::instance(), &Fetcher::feedDetailsUpdated, this, [this](const QString &url, const QString &name, const QString &image, const QString &link, const QString &description, const QDateTime &lastUpdated) { qDebug() << "Start updating feed details" << m_feeds; Feed* feed = getFeed(url); if (feed != nullptr) { feed->setName(name); feed->setImage(image); feed->setLink(link); feed->setDescription(description); feed->setLastUpdated(lastUpdated); //qDebug() << "Retrieving authors"; feed->retrieveAuthors(); // TODO: signal feedmodel: Q_EMIT dataChanged(createIndex(i, 0), createIndex(i, 0)); } }); connect(&Fetcher::instance(), &Fetcher::entryAdded, this, [this](const QString &feedurl, const QString &id) { // Only add the new entry to m_entries // we will repopulate m_entrymap once all new entries have been added, // such that m_entrymap will show all new entries in the correct order m_entries[id] = nullptr; Q_EMIT entryAdded(feedurl, id); }); connect(&Fetcher::instance(), &Fetcher::feedUpdated, this, [this](const QString &feedurl) { // Update m_entrymap for feedurl, such that the new and old entries show // up in the correct order // TODO: put this code into a separate method and re-use this in the constructor QSqlQuery query; m_entrymap[feedurl].clear(); query.prepare(QStringLiteral("SELECT id FROM Entries WHERE feed=:feed ORDER BY updated DESC;")); query.bindValue(QStringLiteral(":feed"), feedurl); Database::instance().execute(query); while (query.next()) { m_entrymap[feedurl] += query.value(QStringLiteral("id")).toString(); //qDebug() << m_entrymap[feedurl]; } // Check for "new" entries if (SettingsManager::self()->autoQueue()) { query.prepare(QStringLiteral("SELECT id FROM Entries WHERE feed=:feed AND new=:new;")); query.bindValue(QStringLiteral(":feed"), feedurl); query.bindValue(QStringLiteral(":new"), true); Database::instance().execute(query); while (query.next()) { QString const id = query.value(QStringLiteral("id")).toString(); addToQueue(feedurl, id); if (SettingsManager::self()->autoDownload()) { if (getEntry(id)->hasEnclosure()) { qDebug() << "Start downloading" << getEntry(id)->title(); getEntry(id)->enclosure()->download(); } } } } Q_EMIT feedEntriesUpdated(feedurl); }); // Only read unique feedurls and entry ids from the database. // The feed and entry datastructures will be loaded lazily. QSqlQuery query; query.prepare(QStringLiteral("SELECT url FROM Feeds;")); Database::instance().execute(query); while (query.next()) { m_feedmap += query.value(QStringLiteral("url")).toString(); m_feeds[query.value(QStringLiteral("url")).toString()] == nullptr; } //qDebug() << m_feedmap; for (auto &feedurl : m_feedmap) { query.prepare(QStringLiteral("SELECT id FROM Entries WHERE feed=:feed ORDER BY updated DESC;")); query.bindValue(QStringLiteral(":feed"), feedurl); Database::instance().execute(query); while (query.next()) { m_entrymap[feedurl] += query.value(QStringLiteral("id")).toString(); m_entries[query.value(QStringLiteral("id")).toString()] = nullptr; //qDebug() << m_entrymap[feedurl]; } } //qDebug() << m_entrymap; query.prepare(QStringLiteral("SELECT id FROM Queue ORDER BY listnr;")); Database::instance().execute(query); while (query.next()) { m_queuemap += query.value(QStringLiteral("id")).toString(); } //qDebug() << m_queuemap; } Feed* DataManager::getFeed(int const index) const { return getFeed(m_feedmap[index]); } Feed* DataManager::getFeed(QString const feedurl) const { if (m_feeds[feedurl] == nullptr) loadFeed(feedurl); return m_feeds[feedurl]; } Entry* DataManager::getEntry(int const feed_index, int const entry_index) const { return getEntry(m_entrymap[m_feedmap[feed_index]][entry_index]); } Entry* DataManager::getEntry(const Feed* feed, int const entry_index) const { return getEntry(m_entrymap[feed->url()][entry_index]); } Entry* DataManager::getEntry(QString id) const { if (m_entries[id] == nullptr) loadEntry(id); return m_entries[id]; } int DataManager::feedCount() const { return m_feedmap.count(); } int DataManager::entryCount(const int feed_index) const { return m_entrymap[m_feedmap[feed_index]].count(); } int DataManager::entryCount(const Feed* feed) const { return m_entrymap[feed->url()].count(); } int DataManager::unreadEntryCount(const Feed* feed) const { QSqlQuery query; query.prepare(QStringLiteral("SELECT COUNT (id) FROM Entries where feed=:feed AND read=0;")); query.bindValue(QStringLiteral(":feed"), feed->url()); Database::instance().execute(query); if (!query.next()) return -1; return query.value(0).toInt(); } int DataManager::newEntryCount(const Feed* feed) const { QSqlQuery query; query.prepare(QStringLiteral("SELECT COUNT (id) FROM Entries where feed=:feed AND new=1;")); query.bindValue(QStringLiteral(":feed"), feed->url()); Database::instance().execute(query); if (!query.next()) return -1; return query.value(0).toInt(); } void DataManager::removeFeed(const Feed* feed) { removeFeed(m_feedmap.indexOf(feed->url())); } void DataManager::removeFeed(const int &index) { // Get feed pointer Feed* feed = getFeed(m_feedmap[index]); const QString feedurl = feed->url(); // Delete the object instances and mappings // First delete entries in Queue qDebug() << "delete queueentries of" << feedurl; for (auto& id : m_queuemap) { if (getEntry(id)->feed()->url() == feedurl) { removeQueueItem(id); } } // Delete entries themselves qDebug() << "delete entries of" << feedurl; for (auto& id : m_entrymap[feedurl]) { if (getEntry(id)->hasEnclosure()) getEntry(id)->enclosure()->deleteFile(); // delete enclosure (if it exists) if (!getEntry(id)->image().isEmpty()) Fetcher::instance().removeImage(getEntry(id)->image()); // delete entry images delete m_entries[id]; // delete pointer m_entries.remove(id); // delete the hash key } m_entrymap.remove(feedurl); // remove all the entry mappings belonging to the feed qDebug() << "Remove feed image" << feed->image() << "for feed" << feedurl; if (!feed->image().isEmpty()) Fetcher::instance().removeImage(feed->image()); m_feeds.remove(m_feedmap[index]); // remove from m_feeds m_feedmap.removeAt(index); // remove from m_feedmap delete feed; // remove the pointer // Then delete everything from the database qDebug() << "delete database part of" << feedurl; // Delete Authors QSqlQuery query; query.prepare(QStringLiteral("DELETE FROM Authors WHERE feed=:feed;")); query.bindValue(QStringLiteral(":feed"), feedurl); Database::instance().execute(query); // Delete Entries query.prepare(QStringLiteral("DELETE FROM Entries WHERE feed=:feed;")); query.bindValue(QStringLiteral(":feed"), feedurl); Database::instance().execute(query); // Delete Enclosures query.prepare(QStringLiteral("DELETE FROM Enclosures WHERE feed=:feed;")); query.bindValue(QStringLiteral(":feed"), feedurl); Database::instance().execute(query); // Delete Feed query.prepare(QStringLiteral("DELETE FROM Feeds WHERE url=:url;")); query.bindValue(QStringLiteral(":url"), feedurl); Database::instance().execute(query); Q_EMIT feedRemoved(index); } void DataManager::addFeed(const QString &url) { // This method will add the relevant internal data structures, and then add // a preliminary entry into the database. Those details (as well as entries, // authors and enclosures) will be updated by calling Fetcher::fetch() which // will trigger a full update of the feed and all related items. qDebug() << "Adding feed"; if (feedExists(url)) { qDebug() << "Feed already exists"; return; } qDebug() << "Feed does not yet exist"; QUrl urlFromInput = QUrl::fromUserInput(url); QSqlQuery query; query.prepare(QStringLiteral("INSERT INTO Feeds VALUES (:name, :url, :image, :link, :description, :deleteAfterCount, :deleteAfterType, :subscribed, :lastUpdated, :new, :notify);")); query.bindValue(QStringLiteral(":name"), urlFromInput.toString()); query.bindValue(QStringLiteral(":url"), urlFromInput.toString()); query.bindValue(QStringLiteral(":image"), QLatin1String("")); query.bindValue(QStringLiteral(":link"), QLatin1String("")); query.bindValue(QStringLiteral(":description"), QLatin1String("")); query.bindValue(QStringLiteral(":deleteAfterCount"), 0); query.bindValue(QStringLiteral(":deleteAfterType"), 0); query.bindValue(QStringLiteral(":subscribed"), QDateTime::currentDateTime().toSecsSinceEpoch()); query.bindValue(QStringLiteral(":lastUpdated"), 0); query.bindValue(QStringLiteral(":new"), true); query.bindValue(QStringLiteral(":notify"), false); Database::instance().execute(query); m_feeds[urlFromInput.toString()] = nullptr; m_feedmap.append(urlFromInput.toString()); Q_EMIT feedAdded(urlFromInput.toString()); Fetcher::instance().fetch(urlFromInput.toString()); } Entry* DataManager::getQueueEntry(int const &index) const { return getEntry(m_queuemap[index]); } int DataManager::queueCount() const { return m_queuemap.count(); } QStringList DataManager::getQueue() const { return m_queuemap; } bool DataManager::entryInQueue(const QString &feedurl, const QString &id) const { Q_UNUSED(feedurl); return m_queuemap.contains(id); } void DataManager::addToQueue(const Entry* entry) { if (entry != nullptr) { return addToQueue(entry->feed()->url(), entry->id()); } } void DataManager::addToQueue(const QString &feedurl, const QString &id) { // If item is already in queue, then stop here if (m_queuemap.contains(id)) return; // Add to internal queuemap data structure m_queuemap += id; //qDebug() << m_queuemap; // Get index of this entry const int index = m_queuemap.indexOf(id); // add new entry to end of queue // Add to Queue database QSqlQuery query; query.prepare(QStringLiteral("INSERT INTO Queue VALUES (:index, :feedurl, :id);")); query.bindValue(QStringLiteral(":index"), index); query.bindValue(QStringLiteral(":feedurl"), feedurl); query.bindValue(QStringLiteral(":id"), id); Database::instance().execute(query); // Make sure that the QueueModel is aware of the changes Q_EMIT queueEntryAdded(index, id); Q_EMIT getEntry(id)->queueStatusChanged(true); } void DataManager::moveQueueItem(const int &from, const int &to) { // First move the items in the internal data structure m_queuemap.move(from, to); // Then make sure that the database Queue table reflects these changes updateQueueListnrs(); // Make sure that the QueueModel is aware of the changes so it can update Q_EMIT queueEntryMoved(from, to); } void DataManager::removeQueueItem(const int &index) { qDebug() << m_queuemap; // Unset "new" state getEntry(m_queuemap[index])->setNew(false); // TODO: Make sure to unset the pointer in the Audio class once it's been // ported to c++ // Remove the item from the internal data structure const QString id = m_queuemap[index]; m_queuemap.removeAt(index); // Then make sure that the database Queue table reflects these changes QSqlQuery query; query.prepare(QStringLiteral("DELETE FROM Queue WHERE listnr=:listnr;")); query.bindValue(QStringLiteral(":listnr"), index); Database::instance().execute(query); // ... and update all other listnrs in Queue table updateQueueListnrs(); // Make sure that the QueueModel is aware of the change so it can update Q_EMIT queueEntryRemoved(index, id); Q_EMIT getEntry(id)->queueStatusChanged(false); } void DataManager::removeQueueItem(const QString id) { removeQueueItem(m_queuemap.indexOf(id)); } void DataManager::removeQueueItem(Entry* entry) { removeQueueItem(m_queuemap.indexOf(entry->id())); } void DataManager::importFeeds(const QString &path) { QUrl url(path); QFile file(url.isLocalFile() ? url.toLocalFile() : url.toString()); file.open(QIODevice::ReadOnly); QXmlStreamReader xmlReader(&file); while(!xmlReader.atEnd()) { xmlReader.readNext(); if(xmlReader.tokenType() == 4 && xmlReader.attributes().hasAttribute(QStringLiteral("xmlUrl"))) { addFeed(xmlReader.attributes().value(QStringLiteral("xmlUrl")).toString()); } } } void DataManager::exportFeeds(const QString &path) { QUrl url(path); QFile file(url.isLocalFile() ? url.toLocalFile() : url.toString()); file.open(QIODevice::WriteOnly); QXmlStreamWriter xmlWriter(&file); xmlWriter.setAutoFormatting(true); xmlWriter.writeStartDocument(QStringLiteral("1.0")); xmlWriter.writeStartElement(QStringLiteral("opml")); xmlWriter.writeEmptyElement(QStringLiteral("head")); xmlWriter.writeStartElement(QStringLiteral("body")); xmlWriter.writeAttribute(QStringLiteral("version"), QStringLiteral("1.0")); QSqlQuery query; query.prepare(QStringLiteral("SELECT url, name FROM Feeds;")); Database::instance().execute(query); while(query.next()) { xmlWriter.writeEmptyElement(QStringLiteral("outline")); xmlWriter.writeAttribute(QStringLiteral("xmlUrl"), query.value(0).toString()); xmlWriter.writeAttribute(QStringLiteral("title"), query.value(1).toString()); } xmlWriter.writeEndElement(); xmlWriter.writeEndElement(); xmlWriter.writeEndDocument(); } void DataManager::loadFeed(const QString feedurl) const { m_feeds[feedurl] = new Feed(feedurl); } void DataManager::loadEntry(const QString id) const { // First find the feed that this entry belongs to Feed* feed = nullptr; QHashIterator i(m_entrymap); while (i.hasNext()) { i.next(); if (i.value().contains(id)) feed = getFeed(i.key()); } if (feed == nullptr) { qDebug() << "Failed to find feed belonging to entry" << id; return; } m_entries[id] = new Entry(feed, id); } bool DataManager::feedExists(const QString &url) { return m_feeds.contains(url); } void DataManager::updateQueueListnrs() const { QSqlQuery query; for (int i=0; i