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; };