This implements support for streaming episodes rather than downloading them first. This introduces a new setting: prioritizeStreaming. If it's set to false (default) then a streaming play button is only added to the EntryPage. If it is set to true, then the streaming play button will also appear on the Entry delegates instead of the download button. There is a separate setting to decide if streaming is also allowed on metered connections. FEATURE: 438864
657 lines
30 KiB
C++
657 lines
30 KiB
C++
/**
|
|
* 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 "updatefeedjob.h"
|
|
|
|
#include <QDir>
|
|
#include <QDomElement>
|
|
#include <QMultiMap>
|
|
#include <QNetworkReply>
|
|
#include <QSqlQuery>
|
|
#include <QTextDocumentFragment>
|
|
#include <QTimer>
|
|
|
|
#include <KLocalizedString>
|
|
#include <ThreadWeaver/Thread>
|
|
|
|
#include "database.h"
|
|
#include "enclosure.h"
|
|
#include "fetcher.h"
|
|
#include "fetcherlogging.h"
|
|
#include "kasts-version.h"
|
|
#include "settingsmanager.h"
|
|
|
|
using namespace ThreadWeaver;
|
|
|
|
UpdateFeedJob::UpdateFeedJob(const QString &url, const QByteArray &data, QObject *parent)
|
|
: QObject(parent)
|
|
, m_url(url)
|
|
, m_data(data)
|
|
{
|
|
// connect to signals in Fetcher such that GUI can pick up the changes
|
|
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);
|
|
}
|
|
|
|
UpdateFeedJob::~UpdateFeedJob()
|
|
{
|
|
qCDebug(kastsFetcher) << "destroyed UpdateFeedJob for" << m_url;
|
|
}
|
|
|
|
void UpdateFeedJob::run(JobPointer, Thread *)
|
|
{
|
|
if (m_abort) {
|
|
Q_EMIT finished();
|
|
return;
|
|
}
|
|
|
|
Database::openDatabase(m_url);
|
|
|
|
Syndication::DocumentSource document(m_data, m_url);
|
|
Syndication::FeedPtr feed = Syndication::parserCollection()->parse(document, QStringLiteral("Atom"));
|
|
processFeed(feed);
|
|
|
|
Database::closeDatabase(m_url);
|
|
|
|
Q_EMIT finished();
|
|
}
|
|
|
|
void UpdateFeedJob::processFeed(Syndication::FeedPtr feed)
|
|
{
|
|
qCDebug(kastsFetcher) << "start process feed" << feed;
|
|
|
|
if (feed.isNull())
|
|
return;
|
|
|
|
// First check if this is a newly added feed
|
|
m_isNewFeed = false;
|
|
QSqlQuery query(QSqlDatabase::database(m_url));
|
|
query.prepare(QStringLiteral("SELECT new FROM Feeds WHERE url=:url;"));
|
|
query.bindValue(QStringLiteral(":url"), m_url);
|
|
Database::execute(query);
|
|
if (query.next()) {
|
|
m_isNewFeed = query.value(QStringLiteral("new")).toBool();
|
|
} else {
|
|
qCDebug(kastsFetcher) << "Feed not found in database" << m_url;
|
|
return;
|
|
}
|
|
if (m_isNewFeed)
|
|
qCDebug(kastsFetcher) << "New feed" << feed->title();
|
|
|
|
m_markUnreadOnNewFeed = !(SettingsManager::self()->markUnreadOnNewFeed() == 2);
|
|
|
|
// Retrieve "other" fields; this will include the "itunes" tags
|
|
QMultiMap<QString, QDomElement> otherItems = feed->additionalProperties();
|
|
|
|
query.prepare(QStringLiteral("UPDATE Feeds SET name=:name, image=:image, link=:link, description=:description, lastUpdated=:lastUpdated WHERE url=:url;"));
|
|
query.bindValue(QStringLiteral(":name"), feed->title());
|
|
query.bindValue(QStringLiteral(":url"), m_url);
|
|
query.bindValue(QStringLiteral(":link"), feed->link());
|
|
query.bindValue(QStringLiteral(":description"), feed->description());
|
|
|
|
QDateTime current = QDateTime::currentDateTime();
|
|
query.bindValue(QStringLiteral(":lastUpdated"), current.toSecsSinceEpoch());
|
|
|
|
QString image = feed->image()->url();
|
|
// If there is no regular image tag, then try the itunes tags
|
|
if (image.isEmpty()) {
|
|
if (otherItems.value(QStringLiteral("http://www.itunes.com/dtds/podcast-1.0.dtdimage")).hasAttribute(QStringLiteral("href"))) {
|
|
image = otherItems.value(QStringLiteral("http://www.itunes.com/dtds/podcast-1.0.dtdimage")).attribute(QStringLiteral("href"));
|
|
}
|
|
}
|
|
if (image.startsWith(QStringLiteral("/")))
|
|
image = QUrl(m_url).adjusted(QUrl::RemovePath).toString() + image;
|
|
query.bindValue(QStringLiteral(":image"), image);
|
|
|
|
// Do the actual database UPDATE of this feed
|
|
Database::execute(query);
|
|
|
|
// Now that we have the feed details, we make vectors of the data that's
|
|
// 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 * FROM Entries WHERE feed=:feed;"));
|
|
query.bindValue(QStringLiteral(":feed"), m_url);
|
|
Database::execute(query);
|
|
while (query.next()) {
|
|
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 * FROM Enclosures WHERE feed=:feed;"));
|
|
query.bindValue(QStringLiteral(":feed"), m_url);
|
|
Database::execute(query);
|
|
while (query.next()) {
|
|
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 * FROM Authors WHERE feed=:feed;"));
|
|
query.bindValue(QStringLiteral(":feed"), m_url);
|
|
Database::execute(query);
|
|
while (query.next()) {
|
|
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 * FROM Chapters WHERE feed=:feed;"));
|
|
query.bindValue(QStringLiteral(":feed"), m_url);
|
|
Database::execute(query);
|
|
while (query.next()) {
|
|
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
|
|
if (feed->authors().count() > 0) {
|
|
for (auto &author : feed->authors()) {
|
|
processAuthor(QLatin1String(""), author->name(), QLatin1String(""), QLatin1String(""));
|
|
}
|
|
} else {
|
|
// Try to find itunes fields if plain author doesn't exist
|
|
QString authorname, authoremail;
|
|
// First try the "itunes:owner" tag, if that doesn't succeed, then try the "itunes:author" tag
|
|
if (otherItems.value(QStringLiteral("http://www.itunes.com/dtds/podcast-1.0.dtdowner")).hasChildNodes()) {
|
|
QDomNodeList nodelist = otherItems.value(QStringLiteral("http://www.itunes.com/dtds/podcast-1.0.dtdowner")).childNodes();
|
|
for (int i = 0; i < nodelist.length(); i++) {
|
|
if (nodelist.item(i).nodeName() == QStringLiteral("itunes:name")) {
|
|
authorname = nodelist.item(i).toElement().text();
|
|
} else if (nodelist.item(i).nodeName() == QStringLiteral("itunes:email")) {
|
|
authoremail = nodelist.item(i).toElement().text();
|
|
}
|
|
}
|
|
} else {
|
|
authorname = otherItems.value(QStringLiteral("http://www.itunes.com/dtds/podcast-1.0.dtdauthor")).text();
|
|
qCDebug(kastsFetcher) << "authorname" << authorname;
|
|
}
|
|
if (!authorname.isEmpty()) {
|
|
processAuthor(QLatin1String(""), authorname, QLatin1String(""), authoremail);
|
|
}
|
|
}
|
|
|
|
qCDebug(kastsFetcher) << "Updated feed details:" << feed->title();
|
|
|
|
// TODO: Only emit signal if the details have really changed
|
|
Q_EMIT feedDetailsUpdated(m_url, feed->title(), image, feed->link(), feed->description(), current);
|
|
|
|
if (m_abort)
|
|
return;
|
|
|
|
// Now deal with the entries, enclosures, entry authors and chapter marks
|
|
bool updatedEntries = false;
|
|
for (const auto &entry : feed->items()) {
|
|
if (m_abort)
|
|
return;
|
|
|
|
bool isNewEntry = processEntry(entry);
|
|
updatedEntries = updatedEntries || isNewEntry;
|
|
}
|
|
|
|
writeToDatabase();
|
|
|
|
if (m_isNewFeed) {
|
|
// Finally, reset the new flag to false now that the new feed has been
|
|
// fully processed. If we would reset the flag sooner, then too many
|
|
// episodes will get flagged as new if the initial import gets
|
|
// interrupted somehow.
|
|
query.prepare(QStringLiteral("UPDATE Feeds SET new=:new WHERE url=:url;"));
|
|
query.bindValue(QStringLiteral(":url"), m_url);
|
|
query.bindValue(QStringLiteral(":new"), false);
|
|
Database::execute(query);
|
|
}
|
|
|
|
if (updatedEntries || m_isNewFeed)
|
|
Q_EMIT feedUpdated(m_url);
|
|
qCDebug(kastsFetcher) << "done processing feed" << 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 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;
|
|
}
|
|
|
|
// Retrieve "other" fields; this will include the "itunes" tags
|
|
QMultiMap<QString, QDomElement> otherItems = entry->additionalProperties();
|
|
|
|
for (const QString &key : otherItems.uniqueKeys()) {
|
|
qCDebug(kastsFetcher) << "other elements";
|
|
qCDebug(kastsFetcher) << key << otherItems.value(key).tagName();
|
|
}
|
|
|
|
EntryDetails entryDetails;
|
|
entryDetails.feed = m_url;
|
|
entryDetails.id = entry->id();
|
|
entryDetails.title = QTextDocumentFragment::fromHtml(entry->title()).toPlainText();
|
|
entryDetails.created = static_cast<int>(entry->datePublished());
|
|
entryDetails.updated = static_cast<int>(entry->dateUpdated());
|
|
entryDetails.link = entry->link();
|
|
entryDetails.hasEnclosure = (entry->enclosures().length() > 0);
|
|
entryDetails.read = m_isNewFeed ? m_markUnreadOnNewFeed : false; // if new feed, then check settings
|
|
entryDetails.isNew = !m_isNewFeed; // if new feed, then mark none as new
|
|
|
|
if (!entry->description().isEmpty())
|
|
entryDetails.content = entry->description();
|
|
else
|
|
entryDetails.content = entry->content();
|
|
|
|
// Look for image in itunes tags
|
|
if (otherItems.value(QStringLiteral("http://www.itunes.com/dtds/podcast-1.0.dtdimage")).hasAttribute(QStringLiteral("href"))) {
|
|
entryDetails.image = otherItems.value(QStringLiteral("http://www.itunes.com/dtds/podcast-1.0.dtdimage")).attribute(QStringLiteral("href"));
|
|
} else if (otherItems.contains(QStringLiteral("http://search.yahoo.com/mrss/thumbnail"))) {
|
|
entryDetails.image = otherItems.value(QStringLiteral("http://search.yahoo.com/mrss/thumbnail")).attribute(QStringLiteral("url"));
|
|
}
|
|
if (entryDetails.image.startsWith(QStringLiteral("/"))) {
|
|
entryDetails.image = QUrl(m_url).adjusted(QUrl::RemovePath).toString() + entryDetails.image;
|
|
}
|
|
qCDebug(kastsFetcher) << "Entry image found" << entryDetails.image;
|
|
|
|
// 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()) {
|
|
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())
|
|
isUpdateDependencies = isUpdateDependencies | processAuthor(entry->id(), authorName, QLatin1String(""), QLatin1String(""));
|
|
}
|
|
|
|
// Process chapters
|
|
if (otherItems.value(QStringLiteral("http://podlove.org/simple-chapterschapters")).hasChildNodes()) {
|
|
QDomNodeList nodelist = otherItems.value(QStringLiteral("http://podlove.org/simple-chapterschapters")).childNodes();
|
|
for (int i = 0; i < nodelist.length(); i++) {
|
|
if (nodelist.item(i).nodeName() == QStringLiteral("psc:chapter")) {
|
|
QDomElement element = nodelist.at(i).toElement();
|
|
QString title = element.attribute(QStringLiteral("title"));
|
|
QString start = element.attribute(QStringLiteral("start"));
|
|
QStringList startParts = start.split(QStringLiteral(":"));
|
|
// Some podcasts use colon for milliseconds as well
|
|
while (startParts.count() > 3) {
|
|
startParts.removeLast();
|
|
}
|
|
int startInt = 0;
|
|
for (QString part : startParts) {
|
|
// strip off decimal point if it's present
|
|
startInt = part.split(QStringLiteral("."))[0].toInt() + startInt * 60;
|
|
}
|
|
qCDebug(kastsFetcher) << "Found chapter mark:" << start << "; in seconds:" << startInt;
|
|
QString images = element.attribute(QStringLiteral("image"));
|
|
isUpdateDependencies = isUpdateDependencies | processChapter(entry->id(), startInt, title, entry->link(), images);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Process enclosures
|
|
// only process first enclosure if there are multiple (e.g. mp3 and ogg);
|
|
// the first one is probably the podcast author's preferred version
|
|
// TODO: handle more than one enclosure?
|
|
if (entry->enclosures().count() > 0) {
|
|
isUpdateDependencies = isUpdateDependencies | processEnclosure(entry->enclosures()[0], entry);
|
|
}
|
|
|
|
return isNewEntry | isUpdateEntry | isUpdateDependencies; // this is a new or updated entry, or an enclosure, chapter or author has been changed/added
|
|
}
|
|
|
|
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
|
|
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;
|
|
authorDetails.id = entryId;
|
|
authorDetails.name = authorName;
|
|
authorDetails.uri = authorUri;
|
|
authorDetails.email = authorEmail;
|
|
|
|
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;
|
|
}
|
|
|
|
bool UpdateFeedJob::processEnclosure(Syndication::EnclosurePtr enclosure, Syndication::ItemPtr entry)
|
|
{
|
|
bool isNewEnclosure = true;
|
|
bool isUpdateEnclosure = false;
|
|
EnclosureDetails currentEnclosure;
|
|
|
|
// check against existing enclosures already in database
|
|
for (const EnclosureDetails &enclosureDetails : (m_enclosures + m_newEnclosures)) {
|
|
if (enclosureDetails.id == entry->id()) {
|
|
isNewEnclosure = false;
|
|
currentEnclosure = enclosureDetails;
|
|
}
|
|
}
|
|
|
|
EnclosureDetails enclosureDetails;
|
|
enclosureDetails.feed = m_url;
|
|
enclosureDetails.id = entry->id();
|
|
enclosureDetails.duration = enclosure->duration();
|
|
enclosureDetails.size = enclosure->length();
|
|
enclosureDetails.title = enclosure->title();
|
|
enclosureDetails.type = enclosure->type();
|
|
enclosureDetails.url = enclosure->url();
|
|
enclosureDetails.playPosition = 0;
|
|
enclosureDetails.downloaded = Enclosure::Downloadable;
|
|
|
|
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;
|
|
}
|
|
|
|
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
|
|
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;
|
|
chapterDetails.id = entryId;
|
|
chapterDetails.start = start;
|
|
chapterDetails.title = chapterTitle;
|
|
chapterDetails.link = link;
|
|
chapterDetails.image = image;
|
|
|
|
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()
|
|
{
|
|
QSqlQuery writeQuery(QSqlDatabase::database(m_url));
|
|
|
|
Database::transaction(m_url);
|
|
|
|
// new entries
|
|
writeQuery.prepare(
|
|
QStringLiteral("INSERT INTO Entries VALUES (:feed, :id, :title, :content, :created, :updated, :link, :read, :new, :hasEnclosure, :image);"));
|
|
for (const EntryDetails &entryDetails : m_newEntries) {
|
|
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(":read"), entryDetails.read);
|
|
writeQuery.bindValue(QStringLiteral(":new"), entryDetails.isNew);
|
|
writeQuery.bindValue(QStringLiteral(":image"), entryDetails.image);
|
|
Database::execute(writeQuery);
|
|
}
|
|
|
|
// 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 (const AuthorDetails &authorDetails : m_newAuthors) {
|
|
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);
|
|
}
|
|
|
|
// 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 (const EnclosureDetails &enclosureDetails : m_newEnclosures) {
|
|
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);
|
|
writeQuery.bindValue(QStringLiteral(":playposition"), enclosureDetails.playPosition);
|
|
writeQuery.bindValue(QStringLiteral(":downloaded"), Enclosure::statusToDb(enclosureDetails.downloaded));
|
|
Database::execute(writeQuery);
|
|
}
|
|
|
|
// 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 (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);
|
|
writeQuery.bindValue(QStringLiteral(":title"), chapterDetails.title);
|
|
writeQuery.bindValue(QStringLiteral(":link"), chapterDetails.link);
|
|
writeQuery.bindValue(QStringLiteral(":image"), chapterDetails.image);
|
|
Database::execute(writeQuery);
|
|
}
|
|
|
|
// set custom amount of episodes to unread/new if required
|
|
if (m_isNewFeed && (SettingsManager::self()->markUnreadOnNewFeed() == 1) && (SettingsManager::self()->markUnreadOnNewFeedCustomAmount() > 0)) {
|
|
writeQuery.prepare(QStringLiteral(
|
|
"UPDATE Entries SET read=:read, new=:new WHERE id in (SELECT id FROM Entries WHERE feed =:feed ORDER BY updated DESC LIMIT :recentUnread);"));
|
|
writeQuery.bindValue(QStringLiteral(":feed"), m_url);
|
|
writeQuery.bindValue(QStringLiteral(":read"), false);
|
|
writeQuery.bindValue(QStringLiteral(":new"), true);
|
|
writeQuery.bindValue(QStringLiteral(":recentUnread"), SettingsManager::self()->markUnreadOnNewFeedCustomAmount());
|
|
Database::execute(writeQuery);
|
|
}
|
|
|
|
if (Database::commit(m_url)) {
|
|
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);
|
|
}
|
|
}
|
|
}
|
|
|
|
void UpdateFeedJob::abort()
|
|
{
|
|
m_abort = true;
|
|
Q_EMIT aborting();
|
|
}
|