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
This commit is contained in:
Bart De Vries 2022-05-25 16:01:38 +02:00
parent 6fb7118e34
commit aac899a7f0
11 changed files with 543 additions and 119 deletions

View File

@ -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"

View File

@ -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) {

View File

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

View File

@ -1,11 +1,12 @@
/**
* SPDX-FileCopyrightText: 2020 Tobias Fella <fella@posteo.de>
* SPDX-FileCopyrightText: 2021 Bart De Vries <bart@mogwai.be>
* SPDX-FileCopyrightText: 2021-2022 Bart De Vries <bart@mogwai.be>
*
* SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
*/
#include "entry.h"
#include "entrylogging.h"
#include <QRegularExpression>
#include <QSqlQuery>
@ -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<Author *> 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;

View File

@ -1,6 +1,6 @@
/**
* SPDX-FileCopyrightText: 2020 Tobias Fella <fella@posteo.de>
* SPDX-FileCopyrightText: 2021 Bart De Vries <bart@mogwai.be>
* SPDX-FileCopyrightText: 2021-2022 Bart De Vries <bart@mogwai.be>
*
* 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<Author *> 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<Author *> 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<Author *> &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;

View File

@ -1,6 +1,6 @@
/**
* SPDX-FileCopyrightText: 2020 Tobias Fella <fella@posteo.de>
* SPDX-FileCopyrightText: 2021 Bart De Vries <bart@mogwai.be>
* SPDX-FileCopyrightText: 2021-2022 Bart De Vries <bart@mogwai.be>
*
* SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
*/

View File

@ -1,6 +1,6 @@
/**
* SPDX-FileCopyrightText: 2020 Tobias Fella <fella@posteo.de>
* SPDX-FileCopyrightText: 2021 Bart De Vries <bart@mogwai.be>
* SPDX-FileCopyrightText: 2021-2022 Bart De Vries <bart@mogwai.be>
*
* 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,

View File

@ -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:")

View File

@ -17,6 +17,10 @@
<label>Automatically fetch feed updates on startup</label>
<default>false</default>
</entry>
<entry name="doFullUpdate" type="Bool">
<label>Update existing entries on feed update</label>
<default>true</default>
</entry>
<entry name="autoQueue" type="Bool">
<label>Automatically add new episodes to queue</label>
<default>true</default>

View File

@ -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<QString, QDomElement> 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);
}
}
}

View File

@ -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<EntryDetails> m_entries;
QVector<AuthorDetails> m_authors;
QVector<EnclosureDetails> m_enclosures;
QVector<ChapterDetails> m_chapters;
QStringList m_existingEntryIds;
QVector<QPair<QString, QString>> m_existingEnclosures;
QVector<QPair<QString, QString>> m_existingAuthors;
QVector<QPair<QString, int>> m_existingChapters;
QVector<EntryDetails> m_entries, m_newEntries, m_updateEntries;
QVector<AuthorDetails> m_authors, m_newAuthors, m_updateAuthors;
QVector<EnclosureDetails> m_enclosures, m_newEnclosures, m_updateEnclosures;
QVector<ChapterDetails> m_chapters, m_newChapters, m_updateChapters;
};