From aac899a7f006678252a196471db475b81bd69afa Mon Sep 17 00:00:00 2001 From: Bart De Vries Date: Wed, 25 May 2022 16:01:38 +0200 Subject: [PATCH] Refactor feed update routine to allow for entry, enclosure, authors and chapter updates This commit adds a bunch of API extensions (public and private) to the entry, enclosure, etc classes to allow runtime updates of internal data. Additionally, the feed update routine has been adapted to find updates in entries, enclosures, etc and pass them on to the relevant objects. All of this functionality is put behind a new toggle exposed in the settings (default is on). This is useful since a full update takes quite a bit longer on underpowered hardware, so users should be able to switch off this potentially non-essential overhead. BUG: 446158 --- src/CMakeLists.txt | 7 + src/enclosure.cpp | 50 ++++ src/enclosure.h | 13 +- src/entry.cpp | 216 ++++++++++++---- src/entry.h | 39 ++- src/fetcher.cpp | 2 +- src/fetcher.h | 3 +- src/qml/Settings/GeneralSettingsPage.qml | 7 +- src/settingsmanager.kcfg | 4 + src/updatefeedjob.cpp | 301 +++++++++++++++++++---- src/updatefeedjob.h | 20 +- 11 files changed, 543 insertions(+), 119 deletions(-) diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 124906f3..49039cc2 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -71,6 +71,13 @@ ecm_qt_declare_logging_category(SRCS_base DEFAULT_SEVERITY Info ) +ecm_qt_declare_logging_category(SRCS_base + HEADER "entrylogging.h" + IDENTIFIER "kastsEntry" + CATEGORY_NAME "org.kde.kasts.entry" + DEFAULT_SEVERITY Info +) + ecm_qt_declare_logging_category(SRCS_base HEADER "feedlogging.h" IDENTIFIER "kastsFeed" diff --git a/src/enclosure.cpp b/src/enclosure.cpp index d1e1a5e1..f00e7587 100644 --- a/src/enclosure.cpp +++ b/src/enclosure.cpp @@ -41,6 +41,11 @@ Enclosure::Enclosure(Entry *entry) { connect(this, &Enclosure::statusChanged, &DownloadModel::instance(), &DownloadModel::monitorDownloadStatus); connect(this, &Enclosure::downloadError, &ErrorLogModel::instance(), &ErrorLogModel::monitorErrorMessages); + connect(&Fetcher::instance(), &Fetcher::entryUpdated, this, [this](const QString &url, const QString &id) { + if ((m_entry->feed()->url() == url) && (m_entry->id() == id)) { + updateFromDb(); + } + }); QSqlQuery query; query.prepare(QStringLiteral("SELECT * FROM Enclosures WHERE id=:id")); @@ -63,6 +68,51 @@ Enclosure::Enclosure(Entry *entry) checkSizeOnDisk(); } +void Enclosure::updateFromDb() +{ + // This method is used to update the most relevant fields from the RSS feed, + // most notably the download URL. It's deliberatly only updating the + // duration and size if the URL has changed, since these values are + // notably untrustworthy. We generally get them from the files themselves + // at the time they are downloaded. + QSqlQuery query; + query.prepare(QStringLiteral("SELECT * FROM Enclosures WHERE id=:id")); + query.bindValue(QStringLiteral(":id"), m_entry->id()); + Database::instance().execute(query); + + if (!query.next()) { + return; + } + + if (m_url != query.value(QStringLiteral("url")).toString() && m_status != Downloaded) { + // this means that the audio file has changed, or at least its location + // let's only do something if the file isn't downloaded. + // try to delete the file first (it actually shouldn't exist) + deleteFile(); + + m_url = query.value(QStringLiteral("url")).toString(); + Q_EMIT urlChanged(m_url); + Q_EMIT pathChanged(path()); + + if (m_duration != query.value(QStringLiteral("duration")).toInt()) { + m_duration = query.value(QStringLiteral("duration")).toInt(); + Q_EMIT durationChanged(); + } + if (m_size != query.value(QStringLiteral("size")).toInt()) { + m_size = query.value(QStringLiteral("size")).toInt(); + Q_EMIT sizeChanged(); + } + if (m_title != query.value(QStringLiteral("title")).toString()) { + m_title = query.value(QStringLiteral("title")).toString(); + Q_EMIT titleChanged(m_title); + } + if (m_type != query.value(QStringLiteral("type")).toString()) { + m_type = query.value(QStringLiteral("type")).toString(); + Q_EMIT typeChanged(m_type); + } + } +} + int Enclosure::statusToDb(Enclosure::Status status) { switch (status) { diff --git a/src/enclosure.h b/src/enclosure.h index d4f4fe2f..515e4d70 100644 --- a/src/enclosure.h +++ b/src/enclosure.h @@ -24,13 +24,13 @@ class Enclosure : public QObject Q_PROPERTY(qint64 size READ size WRITE setSize NOTIFY sizeChanged) Q_PROPERTY(QString formattedSize READ formattedSize NOTIFY sizeChanged) Q_PROPERTY(qint64 sizeOnDisk READ sizeOnDisk NOTIFY sizeOnDiskChanged) - Q_PROPERTY(QString title MEMBER m_title CONSTANT) - Q_PROPERTY(QString type MEMBER m_type CONSTANT) - Q_PROPERTY(QString url READ url CONSTANT) + Q_PROPERTY(QString title MEMBER m_title NOTIFY titleChanged) + Q_PROPERTY(QString type MEMBER m_type NOTIFY typeChanged) + Q_PROPERTY(QString url READ url NOTIFY urlChanged) Q_PROPERTY(Status status READ status WRITE setStatus NOTIFY statusChanged) Q_PROPERTY(double downloadProgress MEMBER m_downloadProgress NOTIFY downloadProgressChanged) Q_PROPERTY(QString formattedDownloadSize READ formattedDownloadSize NOTIFY downloadProgressChanged) - Q_PROPERTY(QString path READ path CONSTANT) + Q_PROPERTY(QString path READ path NOTIFY pathChanged) Q_PROPERTY(QString cachedEmbeddedImage READ cachedEmbeddedImage CONSTANT) Q_PROPERTY(qint64 playPosition READ playPosition WRITE setPlayPosition NOTIFY playPositionChanged) Q_PROPERTY(QString formattedLeftDuration READ formattedLeftDuration NOTIFY playPositionChanged) @@ -77,6 +77,10 @@ public: void checkSizeOnDisk(); Q_SIGNALS: + void titleChanged(const QString &title); + void typeChanged(const QString &type); + void urlChanged(const QString &url); + void pathChanged(const QString &path); void statusChanged(Entry *entry, Status status); void downloadProgressChanged(); void cancelDownload(); @@ -87,6 +91,7 @@ Q_SIGNALS: void downloadError(const Error::Type type, const QString &url, const QString &id, const int errorId, const QString &errorString, const QString &title); private: + void updateFromDb(); void processDownloadedFile(); Entry *m_entry; diff --git a/src/entry.cpp b/src/entry.cpp index e5a69221..c848da9f 100644 --- a/src/entry.cpp +++ b/src/entry.cpp @@ -1,11 +1,12 @@ /** * SPDX-FileCopyrightText: 2020 Tobias Fella - * SPDX-FileCopyrightText: 2021 Bart De Vries + * SPDX-FileCopyrightText: 2021-2022 Bart De Vries * * SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL */ #include "entry.h" +#include "entrylogging.h" #include #include @@ -21,6 +22,7 @@ Entry::Entry(Feed *feed, const QString &id) : QObject(&DataManager::instance()) , m_feed(feed) + , m_id(id) { connect(&Fetcher::instance(), &Fetcher::downloadFinished, this, [this](QString url) { if (url == m_image) { @@ -31,47 +33,98 @@ Entry::Entry(Feed *feed, const QString &id) Q_EMIT cachedImageChanged(cachedImage()); } }); + connect(&Fetcher::instance(), &Fetcher::entryUpdated, this, [this](const QString &url, const QString &id) { + if ((m_feed->url() == url) && (m_id == id)) { + updateFromDb(); + } + }); - QSqlQuery entryQuery; - entryQuery.prepare(QStringLiteral("SELECT * FROM Entries WHERE feed=:feed AND id=:id;")); - entryQuery.bindValue(QStringLiteral(":feed"), m_feed->url()); - entryQuery.bindValue(QStringLiteral(":id"), id); - Database::instance().execute(entryQuery); - if (!entryQuery.next()) - qWarning() << "No element with index" << id << "found in feed" << m_feed->url(); - - QSqlQuery authorQuery; - authorQuery.prepare(QStringLiteral("SELECT * FROM Authors WHERE id=:id")); - authorQuery.bindValue(QStringLiteral(":id"), entryQuery.value(QStringLiteral("id")).toString()); - Database::instance().execute(authorQuery); - - while (authorQuery.next()) { - m_authors += new Author(authorQuery.value(QStringLiteral("name")).toString(), - authorQuery.value(QStringLiteral("email")).toString(), - authorQuery.value(QStringLiteral("uri")).toString(), - nullptr); - } - - m_created.setSecsSinceEpoch(entryQuery.value(QStringLiteral("created")).toInt()); - m_updated.setSecsSinceEpoch(entryQuery.value(QStringLiteral("updated")).toInt()); - - m_id = entryQuery.value(QStringLiteral("id")).toString(); - m_title = entryQuery.value(QStringLiteral("title")).toString(); - m_content = entryQuery.value(QStringLiteral("content")).toString(); - m_link = entryQuery.value(QStringLiteral("link")).toString(); - m_read = entryQuery.value(QStringLiteral("read")).toBool(); - m_new = entryQuery.value(QStringLiteral("new")).toBool(); - - if (entryQuery.value(QStringLiteral("hasEnclosure")).toBool()) { - m_hasenclosure = true; - m_enclosure = new Enclosure(this); - } - m_image = entryQuery.value(QStringLiteral("image")).toString(); + updateFromDb(false); } Entry::~Entry() { - qDeleteAll(m_authors); +} + +void Entry::updateFromDb(bool emitSignals) +{ + QSqlQuery entryQuery; + entryQuery.prepare(QStringLiteral("SELECT * FROM Entries WHERE feed=:feed AND id=:id;")); + entryQuery.bindValue(QStringLiteral(":feed"), m_feed->url()); + entryQuery.bindValue(QStringLiteral(":id"), m_id); + Database::instance().execute(entryQuery); + if (!entryQuery.next()) { + qWarning() << "No element with index" << m_id << "found in feed" << m_feed->url(); + return; + } + + setCreated(QDateTime::fromSecsSinceEpoch(entryQuery.value(QStringLiteral("created")).toInt()), emitSignals); + setUpdated(QDateTime::fromSecsSinceEpoch(entryQuery.value(QStringLiteral("updated")).toInt()), emitSignals); + setTitle(entryQuery.value(QStringLiteral("title")).toString(), emitSignals); + setContent(entryQuery.value(QStringLiteral("content")).toString(), emitSignals); + setLink(entryQuery.value(QStringLiteral("link")).toString(), emitSignals); + + if (m_read != entryQuery.value(QStringLiteral("read")).toBool()) { + m_read = entryQuery.value(QStringLiteral("read")).toBool(); + Q_EMIT readChanged(m_read); + } + if (m_new != entryQuery.value(QStringLiteral("new")).toBool()) { + m_new = entryQuery.value(QStringLiteral("new")).toBool(); + Q_EMIT newChanged(m_new); + } + + setHasEnclosure(entryQuery.value(QStringLiteral("hasEnclosure")).toBool(), emitSignals); + setImage(entryQuery.value(QStringLiteral("image")).toString(), emitSignals); + + updateAuthors(emitSignals); +} + +void Entry::updateAuthors(bool emitSignals) +{ + QVector newAuthors; + bool haveAuthorsChanged = false; + + QSqlQuery authorQuery; + authorQuery.prepare(QStringLiteral("SELECT * FROM Authors WHERE id=:id AND feed=:feed;")); + authorQuery.bindValue(QStringLiteral(":id"), m_id); + authorQuery.bindValue(QStringLiteral(":feed"), m_feed->url()); + Database::instance().execute(authorQuery); + while (authorQuery.next()) { + // check if author already exists, if so, then reuse + bool existingAuthor = false; + QString name = authorQuery.value(QStringLiteral("name")).toString(); + QString email = authorQuery.value(QStringLiteral("email")).toString(); + QString url = authorQuery.value(QStringLiteral("uri")).toString(); + qCDebug(kastsEntry) << name << email << url; + for (Author *author : m_authors) { + if (author) + qCDebug(kastsEntry) << "old authors" << author->name() << author->email() << author->url(); + if (author && author->name() == name && author->email() == email && author->url() == url) { + existingAuthor = true; + newAuthors += author; + } + } + if (!existingAuthor) { + newAuthors += new Author(name, email, url, this); + haveAuthorsChanged = true; + } + } + + // Finally check whether m_authors and newAuthors are identical + // if not, then delete the authors that were removed + for (Author *author : m_authors) { + if (!newAuthors.contains(author)) { + delete author; + haveAuthorsChanged = true; + } + } + + m_authors = newAuthors; + + if (haveAuthorsChanged && emitSignals) { + Q_EMIT authorsChanged(m_authors); + qCDebug(kastsEntry) << "entry" << m_id << "authors have changed?" << haveAuthorsChanged; + } } QString Entry::id() const @@ -124,6 +177,88 @@ QString Entry::baseUrl() const return QUrl(m_link).adjusted(QUrl::RemovePath).toString(); } +void Entry::setTitle(const QString &title, bool emitSignal) +{ + if (m_title != title) { + m_title = title; + if (emitSignal) { + Q_EMIT titleChanged(m_title); + } + } +} + +void Entry::setContent(const QString &content, bool emitSignal) +{ + if (m_content != content) { + m_content = content; + if (emitSignal) { + Q_EMIT contentChanged(m_content); + } + } +} + +void Entry::setCreated(const QDateTime &created, bool emitSignal) +{ + if (m_created != created) { + m_created = created; + if (emitSignal) { + Q_EMIT createdChanged(m_created); + } + } +} + +void Entry::setUpdated(const QDateTime &updated, bool emitSignal) +{ + if (m_updated != updated) { + m_updated = updated; + if (emitSignal) { + Q_EMIT updatedChanged(m_updated); + } + } +} + +void Entry::setLink(const QString &link, bool emitSignal) +{ + if (m_link != link) { + m_link = link; + if (emitSignal) { + Q_EMIT linkChanged(m_link); + Q_EMIT baseUrlChanged(baseUrl()); + } + } +} + +void Entry::setHasEnclosure(bool hasEnclosure, bool emitSignal) +{ + if (hasEnclosure) { + // if there is already an enclosure, it will be updated through separate + // signals if required + if (!m_enclosure) { + m_enclosure = new Enclosure(this); + } + } else { + delete m_enclosure; + m_enclosure = nullptr; + } + if (m_hasenclosure != hasEnclosure) { + m_hasenclosure = hasEnclosure; + if (emitSignal) { + Q_EMIT hasEnclosureChanged(m_hasenclosure); + } + } +} + +void Entry::setImage(const QString &image, bool emitSignal) +{ + if (m_image != image) { + m_image = image; + if (emitSignal) { + Q_EMIT imageChanged(m_image); + Q_EMIT cachedImageChanged(cachedImage()); + } + } +} + void Entry::setRead(bool read) { if (read != m_read) { @@ -307,13 +442,6 @@ void Entry::setQueueStatusInternal(bool state) Q_EMIT queueStatusChanged(state); } -void Entry::setImage(const QString &image) -{ - m_image = image; - Q_EMIT imageChanged(m_image); - Q_EMIT cachedImageChanged(cachedImage()); -} - Feed *Entry::feed() const { return m_feed; diff --git a/src/entry.h b/src/entry.h index 4d4c0f7c..777cb4bd 100644 --- a/src/entry.h +++ b/src/entry.h @@ -1,6 +1,6 @@ /** * SPDX-FileCopyrightText: 2020 Tobias Fella - * SPDX-FileCopyrightText: 2021 Bart De Vries + * SPDX-FileCopyrightText: 2021-2022 Bart De Vries * * SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL */ @@ -24,18 +24,18 @@ class Entry : public QObject Q_PROPERTY(Feed *feed READ feed CONSTANT) Q_PROPERTY(QString id READ id CONSTANT) - Q_PROPERTY(QString title READ title CONSTANT) - Q_PROPERTY(QString content READ content CONSTANT) - Q_PROPERTY(QVector authors READ authors CONSTANT) - Q_PROPERTY(QDateTime created READ created CONSTANT) - Q_PROPERTY(QDateTime updated READ updated CONSTANT) - Q_PROPERTY(QString link READ link CONSTANT) - Q_PROPERTY(QString baseUrl READ baseUrl CONSTANT) + Q_PROPERTY(QString title READ title NOTIFY titleChanged) + Q_PROPERTY(QString content READ content NOTIFY contentChanged) + Q_PROPERTY(QVector authors READ authors NOTIFY authorsChanged) + Q_PROPERTY(QDateTime created READ created NOTIFY createdChanged) + Q_PROPERTY(QDateTime updated READ updated NOTIFY updatedChanged) + Q_PROPERTY(QString link READ link NOTIFY linkChanged) + Q_PROPERTY(QString baseUrl READ baseUrl NOTIFY baseUrlChanged) Q_PROPERTY(bool read READ read WRITE setRead NOTIFY readChanged) Q_PROPERTY(bool new READ getNew WRITE setNew NOTIFY newChanged) Q_PROPERTY(Enclosure *enclosure READ enclosure CONSTANT) - Q_PROPERTY(bool hasEnclosure READ hasEnclosure CONSTANT) - Q_PROPERTY(QString image READ image WRITE setImage NOTIFY imageChanged) + Q_PROPERTY(bool hasEnclosure READ hasEnclosure NOTIFY hasEnclosureChanged) + Q_PROPERTY(QString image READ image NOTIFY imageChanged) Q_PROPERTY(QString cachedImage READ cachedImage NOTIFY cachedImageChanged) Q_PROPERTY(bool queueStatus READ queueStatus WRITE setQueueStatus NOTIFY queueStatusChanged) @@ -63,7 +63,6 @@ public: void setRead(bool read); void setNew(bool state); - void setImage(const QString &url); void setQueueStatus(bool status); Q_INVOKABLE QString adjustedContent(int width, int fontSize); @@ -73,13 +72,31 @@ public: void setQueueStatusInternal(bool state); Q_SIGNALS: + void titleChanged(const QString &title); + void contentChanged(const QString &content); + void authorsChanged(const QVector &authors); + void createdChanged(const QDateTime &created); + void updatedChanged(const QDateTime &updated); + void linkChanged(const QString &link); + void baseUrlChanged(const QString &baseUrl); void readChanged(bool read); void newChanged(bool state); + void hasEnclosureChanged(bool hasEnclosure); void imageChanged(const QString &url); void cachedImageChanged(const QString &imagePath); void queueStatusChanged(bool queueStatus); private: + void updateFromDb(bool emitSignals = true); + void updateAuthors(bool emitSignals = true); + void setTitle(const QString &title, bool emitSignal = true); + void setContent(const QString &content, bool emitSignal = true); + void setCreated(const QDateTime &created, bool emitSignal = true); + void setUpdated(const QDateTime &updated, bool emitSignal = true); + void setLink(const QString &link, bool emitSignal = true); + void setHasEnclosure(bool hasEnclosure, bool emitSignal = true); + void setImage(const QString &url, bool emitSignal = true); + Feed *m_feed; QString m_id; QString m_title; diff --git a/src/fetcher.cpp b/src/fetcher.cpp index 37c87b62..4bb2b497 100644 --- a/src/fetcher.cpp +++ b/src/fetcher.cpp @@ -1,6 +1,6 @@ /** * SPDX-FileCopyrightText: 2020 Tobias Fella - * SPDX-FileCopyrightText: 2021 Bart De Vries + * SPDX-FileCopyrightText: 2021-2022 Bart De Vries * * SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL */ diff --git a/src/fetcher.h b/src/fetcher.h index 62fa8f97..ec789e49 100644 --- a/src/fetcher.h +++ b/src/fetcher.h @@ -1,6 +1,6 @@ /** * SPDX-FileCopyrightText: 2020 Tobias Fella - * SPDX-FileCopyrightText: 2021 Bart De Vries + * SPDX-FileCopyrightText: 2021-2022 Bart De Vries * * SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL */ @@ -43,6 +43,7 @@ public: Q_SIGNALS: void entryAdded(const QString &feedurl, const QString &id); + void entryUpdated(const QString &feedurl, const QString &id); void feedUpdated(const QString &url); void feedDetailsUpdated(const QString &url, const QString &name, diff --git a/src/qml/Settings/GeneralSettingsPage.qml b/src/qml/Settings/GeneralSettingsPage.qml index d7fbb2d0..60792682 100644 --- a/src/qml/Settings/GeneralSettingsPage.qml +++ b/src/qml/Settings/GeneralSettingsPage.qml @@ -53,7 +53,12 @@ Kirigami.ScrollablePage { text: i18n("Automatically fetch podcast updates on startup") onToggled: SettingsManager.refreshOnStartup = checked } - + Controls.CheckBox { + id: doFullUpdate + checked: SettingsManager.doFullUpdate + text: i18n("Update existing episode data on refresh (slower)") + onToggled: SettingsManager.doFullUpdate = checked + } Controls.CheckBox { id: autoQueue Kirigami.FormData.label: i18n("New Episodes:") diff --git a/src/settingsmanager.kcfg b/src/settingsmanager.kcfg index b53c25aa..7dfc47d4 100644 --- a/src/settingsmanager.kcfg +++ b/src/settingsmanager.kcfg @@ -17,6 +17,10 @@ false + + + true + true diff --git a/src/updatefeedjob.cpp b/src/updatefeedjob.cpp index d644afe8..e5c666db 100644 --- a/src/updatefeedjob.cpp +++ b/src/updatefeedjob.cpp @@ -35,6 +35,7 @@ UpdateFeedJob::UpdateFeedJob(const QString &url, const QByteArray &data, QObject connect(this, &UpdateFeedJob::feedDetailsUpdated, &Fetcher::instance(), &Fetcher::feedDetailsUpdated); connect(this, &UpdateFeedJob::feedUpdated, &Fetcher::instance(), &Fetcher::feedUpdated); connect(this, &UpdateFeedJob::entryAdded, &Fetcher::instance(), &Fetcher::entryAdded); + connect(this, &UpdateFeedJob::entryUpdated, &Fetcher::instance(), &Fetcher::entryUpdated); connect(this, &UpdateFeedJob::feedUpdateStatusChanged, &Fetcher::instance(), &Fetcher::feedUpdateStatusChanged); } @@ -113,36 +114,70 @@ void UpdateFeedJob::processFeed(Syndication::FeedPtr feed) // already in the database relating to this feed // NOTE: We will do the feed authors after this step, because otherwise // we can't check for duplicates and we'll keep adding more of the same! - query.prepare(QStringLiteral("SELECT id FROM Entries WHERE feed=:feed;")); + query.prepare(QStringLiteral("SELECT * FROM Entries WHERE feed=:feed;")); query.bindValue(QStringLiteral(":feed"), m_url); Database::execute(query); while (query.next()) { - m_existingEntryIds += query.value(QStringLiteral("id")).toString(); + EntryDetails entryDetails; + entryDetails.feed = m_url; + entryDetails.id = query.value(QStringLiteral("id")).toString(); + entryDetails.title = query.value(QStringLiteral("title")).toString(); + entryDetails.content = query.value(QStringLiteral("content")).toString(); + entryDetails.created = query.value(QStringLiteral("created")).toInt(); + entryDetails.updated = query.value(QStringLiteral("updated")).toInt(); + entryDetails.read = query.value(QStringLiteral("read")).toBool(); + entryDetails.isNew = query.value(QStringLiteral("new")).toBool(); + entryDetails.link = query.value(QStringLiteral("link")).toString(); + entryDetails.hasEnclosure = query.value(QStringLiteral("hasEnclosure")).toBool(); + entryDetails.image = query.value(QStringLiteral("image")).toString(); + m_entries += entryDetails; } - query.prepare(QStringLiteral("SELECT id, url FROM Enclosures WHERE feed=:feed;")); + query.prepare(QStringLiteral("SELECT * FROM Enclosures WHERE feed=:feed;")); query.bindValue(QStringLiteral(":feed"), m_url); Database::execute(query); while (query.next()) { - m_existingEnclosures += qMakePair(query.value(QStringLiteral("id")).toString(), query.value(QStringLiteral("url")).toString()); + EnclosureDetails enclosureDetails; + enclosureDetails.feed = m_url; + enclosureDetails.id = query.value(QStringLiteral("id")).toString(); + enclosureDetails.duration = query.value(QStringLiteral("duration")).toInt(); + enclosureDetails.size = query.value(QStringLiteral("size")).toInt(); + enclosureDetails.title = query.value(QStringLiteral("title")).toString(); + enclosureDetails.type = query.value(QStringLiteral("type")).toString(); + enclosureDetails.url = query.value(QStringLiteral("url")).toString(); + enclosureDetails.playPosition = query.value(QStringLiteral("id")).toInt(); + enclosureDetails.downloaded = Enclosure::dbToStatus(query.value(QStringLiteral("downloaded")).toInt()); + m_enclosures += enclosureDetails; } - query.prepare(QStringLiteral("SELECT id, name FROM Authors WHERE feed=:feed;")); + query.prepare(QStringLiteral("SELECT * FROM Authors WHERE feed=:feed;")); query.bindValue(QStringLiteral(":feed"), m_url); Database::execute(query); while (query.next()) { - m_existingAuthors += qMakePair(query.value(QStringLiteral("id")).toString(), query.value(QStringLiteral("name")).toString()); + AuthorDetails authorDetails; + authorDetails.feed = m_url; + authorDetails.id = query.value(QStringLiteral("id")).toString(); + authorDetails.name = query.value(QStringLiteral("name")).toString(); + authorDetails.uri = query.value(QStringLiteral("uri")).toString(); + authorDetails.email = query.value(QStringLiteral("email")).toString(); + m_authors += authorDetails; } - query.prepare(QStringLiteral("SELECT id, start FROM Chapters WHERE feed=:feed;")); + query.prepare(QStringLiteral("SELECT * FROM Chapters WHERE feed=:feed;")); query.bindValue(QStringLiteral(":feed"), m_url); Database::execute(query); while (query.next()) { - m_existingChapters += qMakePair(query.value(QStringLiteral("id")).toString(), query.value(QStringLiteral("start")).toInt()); + ChapterDetails chapterDetails; + chapterDetails.feed = m_url; + chapterDetails.id = query.value(QStringLiteral("id")).toString(); + chapterDetails.start = query.value(QStringLiteral("start")).toInt(); + chapterDetails.title = query.value(QStringLiteral("title")).toString(); + chapterDetails.link = query.value(QStringLiteral("link")).toString(); + chapterDetails.image = query.value(QStringLiteral("image")).toString(); + m_chapters += chapterDetails; } // Process feed authors - QString authorname, authoremail; if (feed->authors().count() > 0) { for (auto &author : feed->authors()) { processAuthor(QLatin1String(""), author->name(), QLatin1String(""), QLatin1String("")); @@ -208,21 +243,28 @@ void UpdateFeedJob::processFeed(Syndication::FeedPtr feed) bool UpdateFeedJob::processEntry(Syndication::ItemPtr entry) { qCDebug(kastsFetcher) << "Processing" << entry->title(); + bool isNewEntry = true; + bool isUpdateEntry = false; + bool isUpdateDependencies = false; + EntryDetails currentEntry; - // check against existing entries in database - if (m_existingEntryIds.contains(entry->id())) + // check against existing entries and the list of new entries + for (const EntryDetails &entryDetails : (m_entries + m_newEntries)) { + if (entryDetails.id == entry->id()) { + isNewEntry = false; + currentEntry = entryDetails; + } + } + + // stop here if doFullUpdate is set to false and this is an existing entry + if (!isNewEntry && !SettingsManager::self()->doFullUpdate()) { return false; - - // also check against the list of new entries - for (EntryDetails entryDetails : m_entries) { - if (entryDetails.id == entry->id()) - return false; // entry already exists } // Retrieve "other" fields; this will include the "itunes" tags QMultiMap otherItems = entry->additionalProperties(); - for (QString key : otherItems.uniqueKeys()) { + for (const QString &key : otherItems.uniqueKeys()) { qCDebug(kastsFetcher) << "other elements"; qCDebug(kastsFetcher) << key << otherItems.value(key).tagName(); } @@ -254,18 +296,32 @@ bool UpdateFeedJob::processEntry(Syndication::ItemPtr entry) } qCDebug(kastsFetcher) << "Entry image found" << entryDetails.image; - m_entries += entryDetails; + // if this is an existing episode, check if it needs updating + if (!isNewEntry) { + if ((currentEntry.title != entryDetails.title) || (currentEntry.content != entryDetails.content) || (currentEntry.created != entryDetails.created) + || (currentEntry.updated != entryDetails.updated) || (currentEntry.link != entryDetails.link) + || (currentEntry.hasEnclosure != entryDetails.hasEnclosure) || (currentEntry.image != entryDetails.image)) { + qCDebug(kastsFetcher) << "episode details have been updated:" << entry->id(); + isUpdateEntry = true; + m_updateEntries += entryDetails; + } else { + qCDebug(kastsFetcher) << "episode details are unchanged:" << entry->id(); + } + } else { + qCDebug(kastsFetcher) << "this is a new episode:" << entry->id(); + m_newEntries += entryDetails; + } // Process authors if (entry->authors().count() > 0) { for (const auto &author : entry->authors()) { - processAuthor(entry->id(), author->name(), author->uri(), author->email()); + isUpdateDependencies = isUpdateDependencies | processAuthor(entry->id(), author->name(), author->uri(), author->email()); } } else { // As fallback, check if there is itunes "author" information QString authorName = otherItems.value(QStringLiteral("http://www.itunes.com/dtds/podcast-1.0.dtdauthor")).text(); if (!authorName.isEmpty()) - processAuthor(entry->id(), authorName, QLatin1String(""), QLatin1String("")); + isUpdateDependencies = isUpdateDependencies | processAuthor(entry->id(), authorName, QLatin1String(""), QLatin1String("")); } // Process chapters @@ -288,7 +344,7 @@ bool UpdateFeedJob::processEntry(Syndication::ItemPtr entry) } qCDebug(kastsFetcher) << "Found chapter mark:" << start << "; in seconds:" << startInt; QString images = element.attribute(QStringLiteral("image")); - processChapter(entry->id(), startInt, title, entry->link(), images); + isUpdateDependencies = isUpdateDependencies | processChapter(entry->id(), startInt, title, entry->link(), images); } } } @@ -298,17 +354,25 @@ bool UpdateFeedJob::processEntry(Syndication::ItemPtr entry) // the first one is probably the podcast author's preferred version // TODO: handle more than one enclosure? if (entry->enclosures().count() > 0) { - processEnclosure(entry->enclosures()[0], entry); + isUpdateDependencies = isUpdateDependencies | processEnclosure(entry->enclosures()[0], entry); } - return true; // this is a new entry + return isNewEntry | isUpdateEntry | isUpdateDependencies; // this is a new or updated entry, or an enclosure, chapter or author has been changed/added } -void UpdateFeedJob::processAuthor(const QString &entryId, const QString &authorName, const QString &authorUri, const QString &authorEmail) +bool UpdateFeedJob::processAuthor(const QString &entryId, const QString &authorName, const QString &authorUri, const QString &authorEmail) { + bool isNewAuthor = true; + bool isUpdateAuthor = false; + AuthorDetails currentAuthor; + // check against existing authors already in database - if (m_existingAuthors.contains(qMakePair(entryId, authorName))) - return; + for (const AuthorDetails &authorDetails : (m_authors + m_newAuthors)) { + if ((authorDetails.id == entryId) && (authorDetails.name == authorName)) { + isNewAuthor = false; + currentAuthor = authorDetails; + } + } AuthorDetails authorDetails; authorDetails.feed = m_url; @@ -316,14 +380,36 @@ void UpdateFeedJob::processAuthor(const QString &entryId, const QString &authorN authorDetails.name = authorName; authorDetails.uri = authorUri; authorDetails.email = authorEmail; - m_authors += authorDetails; + + if (!isNewAuthor) { + if ((currentAuthor.uri != authorDetails.uri) || (currentAuthor.email != authorDetails.email)) { + qCDebug(kastsFetcher) << "author details have been updated for:" << entryId << authorName; + isUpdateAuthor = true; + m_updateAuthors += authorDetails; + } else { + qCDebug(kastsFetcher) << "author details are unchanged:" << entryId << authorName; + } + } else { + qCDebug(kastsFetcher) << "this is a new author:" << entryId << authorName; + m_newAuthors += authorDetails; + } + + return isNewAuthor | isUpdateAuthor; } -void UpdateFeedJob::processEnclosure(Syndication::EnclosurePtr enclosure, Syndication::ItemPtr entry) +bool UpdateFeedJob::processEnclosure(Syndication::EnclosurePtr enclosure, Syndication::ItemPtr entry) { + bool isNewEnclosure = true; + bool isUpdateEnclosure = false; + EnclosureDetails currentEnclosure; + // check against existing enclosures already in database - if (m_existingEnclosures.contains(qMakePair(entry->id(), enclosure->url()))) - return; + for (const EnclosureDetails &enclosureDetails : (m_enclosures + m_newEnclosures)) { + if (enclosureDetails.id == entry->id()) { + isNewEnclosure = false; + currentEnclosure = enclosureDetails; + } + } EnclosureDetails enclosureDetails; enclosureDetails.feed = m_url; @@ -336,14 +422,36 @@ void UpdateFeedJob::processEnclosure(Syndication::EnclosurePtr enclosure, Syndic enclosureDetails.playPosition = 0; enclosureDetails.downloaded = Enclosure::Downloadable; - m_enclosures += enclosureDetails; + if (!isNewEnclosure) { + if ((currentEnclosure.url != enclosureDetails.url) || (currentEnclosure.title != enclosureDetails.title) + || (currentEnclosure.type != enclosureDetails.type)) { + qCDebug(kastsFetcher) << "enclosure details have been updated for:" << entry->id(); + isUpdateEnclosure = true; + m_updateEnclosures += enclosureDetails; + } else { + qCDebug(kastsFetcher) << "enclosure details are unchanged:" << entry->id(); + } + } else { + qCDebug(kastsFetcher) << "this is a new enclosure:" << entry->id(); + m_newEnclosures += enclosureDetails; + } + + return isNewEnclosure | isUpdateEnclosure; } -void UpdateFeedJob::processChapter(const QString &entryId, const int &start, const QString &chapterTitle, const QString &link, const QString &image) +bool UpdateFeedJob::processChapter(const QString &entryId, const int &start, const QString &chapterTitle, const QString &link, const QString &image) { + bool isNewChapter = true; + bool isUpdateChapter = false; + ChapterDetails currentChapter; + // check against existing enclosures already in database - if (m_existingChapters.contains(qMakePair(entryId, start))) - return; + for (const ChapterDetails &chapterDetails : (m_chapters + m_newChapters)) { + if ((chapterDetails.id == entryId) && (chapterDetails.start == start)) { + isNewChapter = false; + currentChapter = chapterDetails; + } + } ChapterDetails chapterDetails; chapterDetails.feed = m_url; @@ -353,7 +461,20 @@ void UpdateFeedJob::processChapter(const QString &entryId, const int &start, con chapterDetails.link = link; chapterDetails.image = image; - m_chapters += chapterDetails; + if (!isNewChapter) { + if ((currentChapter.title != chapterDetails.title) || (currentChapter.link != chapterDetails.link) || (currentChapter.image != chapterDetails.image)) { + qCDebug(kastsFetcher) << "chapter details have been updated for:" << entryId << start; + isUpdateChapter = true; + m_updateChapters += chapterDetails; + } else { + qCDebug(kastsFetcher) << "chapter details are unchanged:" << entryId << start; + } + } else { + qCDebug(kastsFetcher) << "this is a new chapter:" << entryId << start; + m_newChapters += chapterDetails; + } + + return isNewChapter | isUpdateChapter; } void UpdateFeedJob::writeToDatabase() @@ -362,10 +483,10 @@ void UpdateFeedJob::writeToDatabase() Database::transaction(m_url); - // Entries + // new entries writeQuery.prepare( QStringLiteral("INSERT INTO Entries VALUES (:feed, :id, :title, :content, :created, :updated, :link, :read, :new, :hasEnclosure, :image);")); - for (EntryDetails entryDetails : m_entries) { + for (const EntryDetails &entryDetails : m_newEntries) { writeQuery.bindValue(QStringLiteral(":feed"), entryDetails.feed); writeQuery.bindValue(QStringLiteral(":id"), entryDetails.id); writeQuery.bindValue(QStringLiteral(":title"), entryDetails.title); @@ -380,9 +501,26 @@ void UpdateFeedJob::writeToDatabase() Database::execute(writeQuery); } - // Authors + // update entries + writeQuery.prepare( + QStringLiteral("UPDATE Entries SET title=:title, content=:content, created=:created, updated=:updated, link=:link, hasEnclosure=:hasEnclosure, " + "image=:image WHERE id=:id AND feed=:feed;")); + for (const EntryDetails &entryDetails : m_updateEntries) { + writeQuery.bindValue(QStringLiteral(":feed"), entryDetails.feed); + writeQuery.bindValue(QStringLiteral(":id"), entryDetails.id); + writeQuery.bindValue(QStringLiteral(":title"), entryDetails.title); + writeQuery.bindValue(QStringLiteral(":content"), entryDetails.content); + writeQuery.bindValue(QStringLiteral(":created"), entryDetails.created); + writeQuery.bindValue(QStringLiteral(":updated"), entryDetails.updated); + writeQuery.bindValue(QStringLiteral(":link"), entryDetails.link); + writeQuery.bindValue(QStringLiteral(":hasEnclosure"), entryDetails.hasEnclosure); + writeQuery.bindValue(QStringLiteral(":image"), entryDetails.image); + Database::execute(writeQuery); + } + + // new authors writeQuery.prepare(QStringLiteral("INSERT INTO Authors VALUES(:feed, :id, :name, :uri, :email);")); - for (AuthorDetails authorDetails : m_authors) { + for (const AuthorDetails &authorDetails : m_newAuthors) { writeQuery.bindValue(QStringLiteral(":feed"), authorDetails.feed); writeQuery.bindValue(QStringLiteral(":id"), authorDetails.id); writeQuery.bindValue(QStringLiteral(":name"), authorDetails.name); @@ -391,9 +529,20 @@ void UpdateFeedJob::writeToDatabase() Database::execute(writeQuery); } - // Enclosures + // update authors + writeQuery.prepare(QStringLiteral("UPDATE Authors SET uri=:uri, email=:email WHERE feed=:feed AND id=:id AND name=:name;")); + for (const AuthorDetails &authorDetails : m_updateAuthors) { + writeQuery.bindValue(QStringLiteral(":feed"), authorDetails.feed); + writeQuery.bindValue(QStringLiteral(":id"), authorDetails.id); + writeQuery.bindValue(QStringLiteral(":name"), authorDetails.name); + writeQuery.bindValue(QStringLiteral(":uri"), authorDetails.uri); + writeQuery.bindValue(QStringLiteral(":email"), authorDetails.email); + Database::execute(writeQuery); + } + + // new enclosures writeQuery.prepare(QStringLiteral("INSERT INTO Enclosures VALUES (:feed, :id, :duration, :size, :title, :type, :url, :playposition, :downloaded);")); - for (EnclosureDetails enclosureDetails : m_enclosures) { + for (const EnclosureDetails &enclosureDetails : m_newEnclosures) { writeQuery.bindValue(QStringLiteral(":feed"), enclosureDetails.feed); writeQuery.bindValue(QStringLiteral(":id"), enclosureDetails.id); writeQuery.bindValue(QStringLiteral(":duration"), enclosureDetails.duration); @@ -406,9 +555,34 @@ void UpdateFeedJob::writeToDatabase() Database::execute(writeQuery); } - // Chapters + // update enclosures + writeQuery.prepare(QStringLiteral("UPDATE Enclosures SET duration=:duration, size=:size, title=:title, type=:type, url=:url WHERE feed=:feed AND id=:id;")); + for (const EnclosureDetails &enclosureDetails : m_updateEnclosures) { + writeQuery.bindValue(QStringLiteral(":feed"), enclosureDetails.feed); + writeQuery.bindValue(QStringLiteral(":id"), enclosureDetails.id); + writeQuery.bindValue(QStringLiteral(":duration"), enclosureDetails.duration); + writeQuery.bindValue(QStringLiteral(":size"), enclosureDetails.size); + writeQuery.bindValue(QStringLiteral(":title"), enclosureDetails.title); + writeQuery.bindValue(QStringLiteral(":type"), enclosureDetails.type); + writeQuery.bindValue(QStringLiteral(":url"), enclosureDetails.url); + Database::execute(writeQuery); + } + + // new chapters writeQuery.prepare(QStringLiteral("INSERT INTO Chapters VALUES(:feed, :id, :start, :title, :link, :image);")); - for (ChapterDetails chapterDetails : m_chapters) { + for (const ChapterDetails &chapterDetails : m_newChapters) { + writeQuery.bindValue(QStringLiteral(":feed"), chapterDetails.feed); + writeQuery.bindValue(QStringLiteral(":id"), chapterDetails.id); + writeQuery.bindValue(QStringLiteral(":start"), chapterDetails.start); + writeQuery.bindValue(QStringLiteral(":title"), chapterDetails.title); + writeQuery.bindValue(QStringLiteral(":link"), chapterDetails.link); + writeQuery.bindValue(QStringLiteral(":image"), chapterDetails.image); + Database::execute(writeQuery); + } + + // update chapters + writeQuery.prepare(QStringLiteral("UPDATE Chapters SET title=:title, link=:link, image=:image WHERE feed=:feed AND id=:id AND start=:start;")); + for (const ChapterDetails &chapterDetails : m_updateChapters) { writeQuery.bindValue(QStringLiteral(":feed"), chapterDetails.feed); writeQuery.bindValue(QStringLiteral(":id"), chapterDetails.id); writeQuery.bindValue(QStringLiteral(":start"), chapterDetails.start); @@ -419,8 +593,45 @@ void UpdateFeedJob::writeToDatabase() } if (Database::commit(m_url)) { - for (EntryDetails entryDetails : m_entries) { - Q_EMIT entryAdded(m_url, entryDetails.id); + QStringList newIds, updateIds; + + // emit signals for new entries + for (const EntryDetails &entryDetails : m_newEntries) { + if (!newIds.contains(entryDetails.id)) { + newIds += entryDetails.id; + } + } + + for (const QString &id : newIds) { + Q_EMIT entryAdded(m_url, id); + } + + // emit signals for updated entries or entries with new/updated authors, + // enclosures or chapters + for (const EntryDetails &entryDetails : m_updateEntries) { + if (!updateIds.contains(entryDetails.id) && !newIds.contains(entryDetails.id)) { + updateIds += entryDetails.id; + } + } + for (const EnclosureDetails &enclosureDetails : (m_newEnclosures + m_updateEnclosures)) { + if (!updateIds.contains(enclosureDetails.id) && !newIds.contains(enclosureDetails.id)) { + updateIds += enclosureDetails.id; + } + } + for (const AuthorDetails &authorDetails : (m_newAuthors + m_updateAuthors)) { + if (!updateIds.contains(authorDetails.id) && !newIds.contains(authorDetails.id)) { + updateIds += authorDetails.id; + } + } + for (const ChapterDetails &chapterDetails : (m_newChapters + m_updateChapters)) { + if (!updateIds.contains(chapterDetails.id) && !newIds.contains(chapterDetails.id)) { + updateIds += chapterDetails.id; + } + } + + for (const QString &id : updateIds) { + qCDebug(kastsFetcher) << "updated episode" << id; + Q_EMIT entryUpdated(m_url, id); } } } diff --git a/src/updatefeedjob.h b/src/updatefeedjob.h index 8397f540..e7d134dd 100644 --- a/src/updatefeedjob.h +++ b/src/updatefeedjob.h @@ -79,6 +79,7 @@ Q_SIGNALS: const QDateTime &lastUpdated); void feedUpdated(const QString &url); void entryAdded(const QString &feedurl, const QString &id); + void entryUpdated(const QString &feedurl, const QString &id); void feedUpdateStatusChanged(const QString &url, bool status); void aborting(); void finished(); @@ -86,9 +87,9 @@ Q_SIGNALS: private: void processFeed(Syndication::FeedPtr feed); bool processEntry(Syndication::ItemPtr entry); - void processAuthor(const QString &entryId, const QString &authorName, const QString &authorUri, const QString &authorEmail); - void processEnclosure(Syndication::EnclosurePtr enclosure, Syndication::ItemPtr entry); - void processChapter(const QString &entryId, const int &start, const QString &chapterTitle, const QString &link, const QString &image); + bool processAuthor(const QString &entryId, const QString &authorName, const QString &authorUri, const QString &authorEmail); + bool processEnclosure(Syndication::EnclosurePtr enclosure, Syndication::ItemPtr entry); + bool processChapter(const QString &entryId, const int &start, const QString &chapterTitle, const QString &link, const QString &image); void writeToDatabase(); bool m_abort = false; @@ -97,13 +98,8 @@ private: QByteArray m_data; bool m_isNewFeed; - QVector m_entries; - QVector m_authors; - QVector m_enclosures; - QVector m_chapters; - - QStringList m_existingEntryIds; - QVector> m_existingEnclosures; - QVector> m_existingAuthors; - QVector> m_existingChapters; + QVector m_entries, m_newEntries, m_updateEntries; + QVector m_authors, m_newAuthors, m_updateAuthors; + QVector m_enclosures, m_newEnclosures, m_updateEnclosures; + QVector m_chapters, m_newChapters, m_updateChapters; };