File indexing completed on 2024-05-12 16:21:32

0001 /**
0002  * SPDX-FileCopyrightText: 2021-2022 Bart De Vries <bart@mogwai.be>
0003  *
0004  * SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
0005  */
0006 
0007 #include "updatefeedjob.h"
0008 
0009 #include <QDir>
0010 #include <QDomElement>
0011 #include <QMultiMap>
0012 #include <QNetworkReply>
0013 #include <QSqlQuery>
0014 #include <QTextDocumentFragment>
0015 #include <QTimer>
0016 
0017 #include <KLocalizedString>
0018 #include <ThreadWeaver/Thread>
0019 
0020 #include "database.h"
0021 #include "enclosure.h"
0022 #include "fetcher.h"
0023 #include "fetcherlogging.h"
0024 #include "kasts-version.h"
0025 #include "settingsmanager.h"
0026 #include "storagemanager.h"
0027 
0028 using namespace ThreadWeaver;
0029 
0030 UpdateFeedJob::UpdateFeedJob(const QString &url, const QByteArray &data, QObject *parent)
0031     : QObject(parent)
0032     , m_url(url)
0033     , m_data(data)
0034 {
0035     // connect to signals in Fetcher such that GUI can pick up the changes
0036     connect(this, &UpdateFeedJob::feedDetailsUpdated, &Fetcher::instance(), &Fetcher::feedDetailsUpdated);
0037     connect(this, &UpdateFeedJob::feedUpdated, &Fetcher::instance(), &Fetcher::feedUpdated);
0038     connect(this, &UpdateFeedJob::entryAdded, &Fetcher::instance(), &Fetcher::entryAdded);
0039     connect(this, &UpdateFeedJob::entryUpdated, &Fetcher::instance(), &Fetcher::entryUpdated);
0040     connect(this, &UpdateFeedJob::feedUpdateStatusChanged, &Fetcher::instance(), &Fetcher::feedUpdateStatusChanged);
0041 }
0042 
0043 UpdateFeedJob::~UpdateFeedJob()
0044 {
0045     qCDebug(kastsFetcher) << "destroyed UpdateFeedJob for" << m_url;
0046 }
0047 
0048 void UpdateFeedJob::run(JobPointer, Thread *)
0049 {
0050     if (m_abort) {
0051         Q_EMIT finished();
0052         return;
0053     }
0054 
0055     Database::openDatabase(m_url);
0056 
0057     Syndication::DocumentSource document(m_data, m_url);
0058     Syndication::FeedPtr feed = Syndication::parserCollection()->parse(document, QStringLiteral("Atom"));
0059     processFeed(feed);
0060 
0061     Database::closeDatabase(m_url);
0062 
0063     Q_EMIT finished();
0064 }
0065 
0066 void UpdateFeedJob::processFeed(Syndication::FeedPtr feed)
0067 {
0068     qCDebug(kastsFetcher) << "start process feed" << feed;
0069 
0070     if (feed.isNull())
0071         return;
0072 
0073     // First check if this is a newly added feed and get current name and dirname
0074     m_isNewFeed = false;
0075     QString oldName, oldDirname;
0076     QSqlQuery query(QSqlDatabase::database(m_url));
0077     query.prepare(QStringLiteral("SELECT new, name, dirname FROM Feeds WHERE url=:url;"));
0078     query.bindValue(QStringLiteral(":url"), m_url);
0079     Database::execute(query);
0080     if (query.next()) {
0081         m_isNewFeed = query.value(QStringLiteral("new")).toBool();
0082         oldName = query.value(QStringLiteral("name")).toString();
0083         oldDirname = query.value(QStringLiteral("dirname")).toString();
0084     } else {
0085         qCDebug(kastsFetcher) << "Feed not found in database" << m_url;
0086         return;
0087     }
0088     if (m_isNewFeed) {
0089         qCDebug(kastsFetcher) << "New feed" << feed->title();
0090     }
0091 
0092     m_markUnreadOnNewFeed = !(SettingsManager::self()->markUnreadOnNewFeed() == 2);
0093 
0094     // Retrieve "other" fields; this will include the "itunes" tags
0095     QMultiMap<QString, QDomElement> otherItems = feed->additionalProperties();
0096 
0097     query.prepare(QStringLiteral(
0098         "UPDATE Feeds SET name=:name, image=:image, link=:link, description=:description, lastUpdated=:lastUpdated, dirname=:dirname WHERE url=:url;"));
0099     query.bindValue(QStringLiteral(":name"), feed->title());
0100     query.bindValue(QStringLiteral(":url"), m_url);
0101     query.bindValue(QStringLiteral(":link"), feed->link());
0102     query.bindValue(QStringLiteral(":description"), feed->description());
0103 
0104     QDateTime current = QDateTime::currentDateTime();
0105     query.bindValue(QStringLiteral(":lastUpdated"), current.toSecsSinceEpoch());
0106 
0107     QString image;
0108     // First try the itunes tags, if not, fall back to regular image tag
0109     if (otherItems.value(QStringLiteral("http://www.itunes.com/dtds/podcast-1.0.dtdimage")).hasAttribute(QStringLiteral("href"))) {
0110         image = otherItems.value(QStringLiteral("http://www.itunes.com/dtds/podcast-1.0.dtdimage")).attribute(QStringLiteral("href"));
0111     } else {
0112         image = feed->image()->url();
0113     }
0114 
0115     if (image.startsWith(QStringLiteral("/")))
0116         image = QUrl(m_url).adjusted(QUrl::RemovePath).toString() + image;
0117     query.bindValue(QStringLiteral(":image"), image);
0118 
0119     // if the title has changed, we need to rename the corresponding enclosure
0120     // download directory name and move the files
0121     m_dirname = oldDirname;
0122     if (oldName != feed->title() || oldDirname.isEmpty() || m_isNewFeed) {
0123         QString generatedDirname = generateFeedDirname(feed->title());
0124         if (generatedDirname != oldDirname) {
0125             m_dirname = generatedDirname;
0126             QString enclosurePath = StorageManager::instance().enclosureDirPath();
0127             if (QDir(enclosurePath + oldDirname).exists()) {
0128                 QDir().rename(enclosurePath + oldDirname, enclosurePath + m_dirname);
0129             } else {
0130                 QDir().mkpath(enclosurePath + m_dirname);
0131             }
0132         }
0133     }
0134     query.bindValue(QStringLiteral(":dirname"), m_dirname);
0135 
0136     // Do the actual database UPDATE of this feed
0137     Database::execute(query);
0138 
0139     // Now that we have the feed details, we make vectors of the data that's
0140     // already in the database relating to this feed
0141     // NOTE: We will do the feed authors after this step, because otherwise
0142     // we can't check for duplicates and we'll keep adding more of the same!
0143     query.prepare(QStringLiteral("SELECT * FROM Entries WHERE feed=:feed;"));
0144     query.bindValue(QStringLiteral(":feed"), m_url);
0145     Database::execute(query);
0146     while (query.next()) {
0147         EntryDetails entryDetails;
0148         entryDetails.feed = m_url;
0149         entryDetails.id = query.value(QStringLiteral("id")).toString();
0150         entryDetails.title = query.value(QStringLiteral("title")).toString();
0151         entryDetails.content = query.value(QStringLiteral("content")).toString();
0152         entryDetails.created = query.value(QStringLiteral("created")).toInt();
0153         entryDetails.updated = query.value(QStringLiteral("updated")).toInt();
0154         entryDetails.read = query.value(QStringLiteral("read")).toBool();
0155         entryDetails.isNew = query.value(QStringLiteral("new")).toBool();
0156         entryDetails.link = query.value(QStringLiteral("link")).toString();
0157         entryDetails.hasEnclosure = query.value(QStringLiteral("hasEnclosure")).toBool();
0158         entryDetails.image = query.value(QStringLiteral("image")).toString();
0159         m_entries += entryDetails;
0160     }
0161 
0162     query.prepare(QStringLiteral("SELECT * FROM Enclosures WHERE feed=:feed;"));
0163     query.bindValue(QStringLiteral(":feed"), m_url);
0164     Database::execute(query);
0165     while (query.next()) {
0166         EnclosureDetails enclosureDetails;
0167         enclosureDetails.feed = m_url;
0168         enclosureDetails.id = query.value(QStringLiteral("id")).toString();
0169         enclosureDetails.duration = query.value(QStringLiteral("duration")).toInt();
0170         enclosureDetails.size = query.value(QStringLiteral("size")).toInt();
0171         enclosureDetails.title = query.value(QStringLiteral("title")).toString();
0172         enclosureDetails.type = query.value(QStringLiteral("type")).toString();
0173         enclosureDetails.url = query.value(QStringLiteral("url")).toString();
0174         enclosureDetails.playPosition = query.value(QStringLiteral("id")).toInt();
0175         enclosureDetails.downloaded = Enclosure::dbToStatus(query.value(QStringLiteral("downloaded")).toInt());
0176         m_enclosures += enclosureDetails;
0177     }
0178 
0179     query.prepare(QStringLiteral("SELECT * FROM Authors WHERE feed=:feed;"));
0180     query.bindValue(QStringLiteral(":feed"), m_url);
0181     Database::execute(query);
0182     while (query.next()) {
0183         AuthorDetails authorDetails;
0184         authorDetails.feed = m_url;
0185         authorDetails.id = query.value(QStringLiteral("id")).toString();
0186         authorDetails.name = query.value(QStringLiteral("name")).toString();
0187         authorDetails.uri = query.value(QStringLiteral("uri")).toString();
0188         authorDetails.email = query.value(QStringLiteral("email")).toString();
0189         m_authors += authorDetails;
0190     }
0191 
0192     query.prepare(QStringLiteral("SELECT * FROM Chapters WHERE feed=:feed;"));
0193     query.bindValue(QStringLiteral(":feed"), m_url);
0194     Database::execute(query);
0195     while (query.next()) {
0196         ChapterDetails chapterDetails;
0197         chapterDetails.feed = m_url;
0198         chapterDetails.id = query.value(QStringLiteral("id")).toString();
0199         chapterDetails.start = query.value(QStringLiteral("start")).toInt();
0200         chapterDetails.title = query.value(QStringLiteral("title")).toString();
0201         chapterDetails.link = query.value(QStringLiteral("link")).toString();
0202         chapterDetails.image = query.value(QStringLiteral("image")).toString();
0203         m_chapters += chapterDetails;
0204     }
0205 
0206     // Process feed authors
0207     if (feed->authors().count() > 0) {
0208         for (auto &author : feed->authors()) {
0209             processAuthor(QLatin1String(""), author->name(), QLatin1String(""), QLatin1String(""));
0210         }
0211     } else {
0212         // Try to find itunes fields if plain author doesn't exist
0213         QString authorname, authoremail;
0214         // First try the "itunes:owner" tag, if that doesn't succeed, then try the "itunes:author" tag
0215         if (otherItems.value(QStringLiteral("http://www.itunes.com/dtds/podcast-1.0.dtdowner")).hasChildNodes()) {
0216             QDomNodeList nodelist = otherItems.value(QStringLiteral("http://www.itunes.com/dtds/podcast-1.0.dtdowner")).childNodes();
0217             for (int i = 0; i < nodelist.length(); i++) {
0218                 if (nodelist.item(i).nodeName() == QStringLiteral("itunes:name")) {
0219                     authorname = nodelist.item(i).toElement().text();
0220                 } else if (nodelist.item(i).nodeName() == QStringLiteral("itunes:email")) {
0221                     authoremail = nodelist.item(i).toElement().text();
0222                 }
0223             }
0224         } else {
0225             authorname = otherItems.value(QStringLiteral("http://www.itunes.com/dtds/podcast-1.0.dtdauthor")).text();
0226             qCDebug(kastsFetcher) << "authorname" << authorname;
0227         }
0228         if (!authorname.isEmpty()) {
0229             processAuthor(QLatin1String(""), authorname, QLatin1String(""), authoremail);
0230         }
0231     }
0232 
0233     qCDebug(kastsFetcher) << "Updated feed details:" << feed->title();
0234 
0235     // TODO: Only emit signal if the details have really changed
0236     Q_EMIT feedDetailsUpdated(m_url, feed->title(), image, feed->link(), feed->description(), current, m_dirname);
0237 
0238     if (m_abort)
0239         return;
0240 
0241     // Now deal with the entries, enclosures, entry authors and chapter marks
0242     bool updatedEntries = false;
0243     for (const auto &entry : feed->items()) {
0244         if (m_abort)
0245             return;
0246 
0247         bool isNewEntry = processEntry(entry);
0248         updatedEntries = updatedEntries || isNewEntry;
0249     }
0250 
0251     writeToDatabase();
0252 
0253     if (m_isNewFeed) {
0254         // Finally, reset the new flag to false now that the new feed has been
0255         // fully processed.  If we would reset the flag sooner, then too many
0256         // episodes will get flagged as new if the initial import gets
0257         // interrupted somehow.
0258         query.prepare(QStringLiteral("UPDATE Feeds SET new=:new WHERE url=:url;"));
0259         query.bindValue(QStringLiteral(":url"), m_url);
0260         query.bindValue(QStringLiteral(":new"), false);
0261         Database::execute(query);
0262     }
0263 
0264     if (updatedEntries || m_isNewFeed)
0265         Q_EMIT feedUpdated(m_url);
0266     qCDebug(kastsFetcher) << "done processing feed" << feed;
0267 }
0268 
0269 bool UpdateFeedJob::processEntry(Syndication::ItemPtr entry)
0270 {
0271     qCDebug(kastsFetcher) << "Processing" << entry->title();
0272     bool isNewEntry = true;
0273     bool isUpdateEntry = false;
0274     bool isUpdateDependencies = false;
0275     EntryDetails currentEntry;
0276 
0277     // check against existing entries and the list of new entries
0278     for (const EntryDetails &entryDetails : (m_entries + m_newEntries)) {
0279         if (entryDetails.id == entry->id()) {
0280             isNewEntry = false;
0281             currentEntry = entryDetails;
0282         }
0283     }
0284 
0285     // stop here if doFullUpdate is set to false and this is an existing entry
0286     if (!isNewEntry && !SettingsManager::self()->doFullUpdate()) {
0287         return false;
0288     }
0289 
0290     // Retrieve "other" fields; this will include the "itunes" tags
0291     QMultiMap<QString, QDomElement> otherItems = entry->additionalProperties();
0292 
0293     for (const QString &key : otherItems.uniqueKeys()) {
0294         qCDebug(kastsFetcher) << "other elements";
0295         qCDebug(kastsFetcher) << key << otherItems.value(key).tagName();
0296     }
0297 
0298     EntryDetails entryDetails;
0299     entryDetails.feed = m_url;
0300     entryDetails.id = entry->id();
0301     entryDetails.title = QTextDocumentFragment::fromHtml(entry->title()).toPlainText();
0302     entryDetails.created = static_cast<int>(entry->datePublished());
0303     entryDetails.updated = static_cast<int>(entry->dateUpdated());
0304     entryDetails.link = entry->link();
0305     entryDetails.hasEnclosure = (entry->enclosures().length() > 0);
0306     entryDetails.read = m_isNewFeed ? m_markUnreadOnNewFeed : false; // if new feed, then check settings
0307     entryDetails.isNew = !m_isNewFeed; // if new feed, then mark none as new
0308 
0309     // Take the longest text, either content or description
0310     if (entry->content().length() > entry->description().length()) {
0311         entryDetails.content = entry->content();
0312     } else {
0313         entryDetails.content = entry->description();
0314     }
0315 
0316     // Look for image in itunes tags
0317     if (otherItems.value(QStringLiteral("http://www.itunes.com/dtds/podcast-1.0.dtdimage")).hasAttribute(QStringLiteral("href"))) {
0318         entryDetails.image = otherItems.value(QStringLiteral("http://www.itunes.com/dtds/podcast-1.0.dtdimage")).attribute(QStringLiteral("href"));
0319     } else if (otherItems.contains(QStringLiteral("http://search.yahoo.com/mrss/thumbnail"))) {
0320         entryDetails.image = otherItems.value(QStringLiteral("http://search.yahoo.com/mrss/thumbnail")).attribute(QStringLiteral("url"));
0321     }
0322     if (entryDetails.image.startsWith(QStringLiteral("/"))) {
0323         entryDetails.image = QUrl(m_url).adjusted(QUrl::RemovePath).toString() + entryDetails.image;
0324     }
0325     qCDebug(kastsFetcher) << "Entry image found" << entryDetails.image;
0326 
0327     // if this is an existing episode, check if it needs updating
0328     if (!isNewEntry) {
0329         if ((currentEntry.title != entryDetails.title) || (currentEntry.content != entryDetails.content) || (currentEntry.created != entryDetails.created)
0330             || (currentEntry.updated != entryDetails.updated) || (currentEntry.link != entryDetails.link)
0331             || (currentEntry.hasEnclosure != entryDetails.hasEnclosure) || (currentEntry.image != entryDetails.image)) {
0332             qCDebug(kastsFetcher) << "episode details have been updated:" << entry->id();
0333             isUpdateEntry = true;
0334             m_updateEntries += entryDetails;
0335         } else {
0336             qCDebug(kastsFetcher) << "episode details are unchanged:" << entry->id();
0337         }
0338     } else {
0339         qCDebug(kastsFetcher) << "this is a new episode:" << entry->id();
0340         m_newEntries += entryDetails;
0341     }
0342 
0343     // Process authors
0344     if (entry->authors().count() > 0) {
0345         for (const auto &author : entry->authors()) {
0346             isUpdateDependencies = isUpdateDependencies | processAuthor(entry->id(), author->name(), author->uri(), author->email());
0347         }
0348     } else {
0349         // As fallback, check if there is itunes "author" information
0350         QString authorName = otherItems.value(QStringLiteral("http://www.itunes.com/dtds/podcast-1.0.dtdauthor")).text();
0351         if (!authorName.isEmpty())
0352             isUpdateDependencies = isUpdateDependencies | processAuthor(entry->id(), authorName, QLatin1String(""), QLatin1String(""));
0353     }
0354 
0355     // Process chapters
0356     if (otherItems.value(QStringLiteral("http://podlove.org/simple-chapterschapters")).hasChildNodes()) {
0357         QDomNodeList nodelist = otherItems.value(QStringLiteral("http://podlove.org/simple-chapterschapters")).childNodes();
0358         for (int i = 0; i < nodelist.length(); i++) {
0359             if (nodelist.item(i).nodeName() == QStringLiteral("psc:chapter")) {
0360                 QDomElement element = nodelist.at(i).toElement();
0361                 QString title = element.attribute(QStringLiteral("title"));
0362                 QString start = element.attribute(QStringLiteral("start"));
0363                 QStringList startParts = start.split(QStringLiteral(":"));
0364                 // Some podcasts use colon for milliseconds as well
0365                 while (startParts.count() > 3) {
0366                     startParts.removeLast();
0367                 }
0368                 int startInt = 0;
0369                 for (QString part : startParts) {
0370                     // strip off decimal point if it's present
0371                     startInt = part.split(QStringLiteral("."))[0].toInt() + startInt * 60;
0372                 }
0373                 qCDebug(kastsFetcher) << "Found chapter mark:" << start << "; in seconds:" << startInt;
0374                 QString images = element.attribute(QStringLiteral("image"));
0375                 isUpdateDependencies = isUpdateDependencies | processChapter(entry->id(), startInt, title, entry->link(), images);
0376             }
0377         }
0378     }
0379 
0380     // Process enclosures
0381     // only process first enclosure if there are multiple (e.g. mp3 and ogg);
0382     // the first one is probably the podcast author's preferred version
0383     // TODO: handle more than one enclosure?
0384     if (entry->enclosures().count() > 0) {
0385         isUpdateDependencies = isUpdateDependencies | processEnclosure(entry->enclosures()[0], entryDetails, currentEntry);
0386     }
0387 
0388     return isNewEntry | isUpdateEntry | isUpdateDependencies; // this is a new or updated entry, or an enclosure, chapter or author has been changed/added
0389 }
0390 
0391 bool UpdateFeedJob::processAuthor(const QString &entryId, const QString &authorName, const QString &authorUri, const QString &authorEmail)
0392 {
0393     bool isNewAuthor = true;
0394     bool isUpdateAuthor = false;
0395     AuthorDetails currentAuthor;
0396 
0397     // check against existing authors already in database
0398     for (const AuthorDetails &authorDetails : (m_authors + m_newAuthors)) {
0399         if ((authorDetails.id == entryId) && (authorDetails.name == authorName)) {
0400             isNewAuthor = false;
0401             currentAuthor = authorDetails;
0402         }
0403     }
0404 
0405     AuthorDetails authorDetails;
0406     authorDetails.feed = m_url;
0407     authorDetails.id = entryId;
0408     authorDetails.name = authorName;
0409     authorDetails.uri = authorUri;
0410     authorDetails.email = authorEmail;
0411 
0412     if (!isNewAuthor) {
0413         if ((currentAuthor.uri != authorDetails.uri) || (currentAuthor.email != authorDetails.email)) {
0414             qCDebug(kastsFetcher) << "author details have been updated for:" << entryId << authorName;
0415             isUpdateAuthor = true;
0416             m_updateAuthors += authorDetails;
0417         } else {
0418             qCDebug(kastsFetcher) << "author details are unchanged:" << entryId << authorName;
0419         }
0420     } else {
0421         qCDebug(kastsFetcher) << "this is a new author:" << entryId << authorName;
0422         m_newAuthors += authorDetails;
0423     }
0424 
0425     return isNewAuthor | isUpdateAuthor;
0426 }
0427 
0428 bool UpdateFeedJob::processEnclosure(Syndication::EnclosurePtr enclosure, const EntryDetails &newEntry, const EntryDetails &oldEntry)
0429 {
0430     bool isNewEnclosure = true;
0431     bool isUpdateEnclosure = false;
0432     EnclosureDetails currentEnclosure;
0433 
0434     // check against existing enclosures already in database
0435     for (const EnclosureDetails &enclosureDetails : (m_enclosures + m_newEnclosures)) {
0436         if (enclosureDetails.id == newEntry.id) {
0437             isNewEnclosure = false;
0438             currentEnclosure = enclosureDetails;
0439         }
0440     }
0441 
0442     EnclosureDetails enclosureDetails;
0443     enclosureDetails.feed = m_url;
0444     enclosureDetails.id = newEntry.id;
0445     enclosureDetails.duration = enclosure->duration();
0446     enclosureDetails.size = enclosure->length();
0447     enclosureDetails.title = enclosure->title();
0448     enclosureDetails.type = enclosure->type();
0449     enclosureDetails.url = enclosure->url();
0450     enclosureDetails.playPosition = 0;
0451     enclosureDetails.downloaded = Enclosure::Downloadable;
0452 
0453     if (!isNewEnclosure) {
0454         if ((currentEnclosure.url != enclosureDetails.url) || (currentEnclosure.title != enclosureDetails.title)
0455             || (currentEnclosure.type != enclosureDetails.type)) {
0456             qCDebug(kastsFetcher) << "enclosure details have been updated for:" << newEntry.id;
0457             isUpdateEnclosure = true;
0458             m_updateEnclosures += enclosureDetails;
0459         } else {
0460             qCDebug(kastsFetcher) << "enclosure details are unchanged:" << newEntry.id;
0461         }
0462 
0463         // Check if entry title or enclosure URL has changed
0464         if (newEntry.title != oldEntry.title) {
0465             QString oldFilename = StorageManager::instance().enclosurePath(oldEntry.title, currentEnclosure.url, m_dirname);
0466             QString newFilename = StorageManager::instance().enclosurePath(newEntry.title, enclosureDetails.url, m_dirname);
0467 
0468             if (oldFilename != newFilename) {
0469                 if (currentEnclosure.url == enclosureDetails.url) {
0470                     // If entry title has changed but URL is still the same, the existing enclosure needs to be renamed
0471                     QFile::rename(oldFilename, newFilename);
0472                 } else {
0473                     // If enclosure URL has changed, the old enclosure needs to be deleted
0474                     if (QFile(oldFilename).exists()) {
0475                         QFile(oldFilename).remove();
0476                     }
0477                 }
0478             }
0479         }
0480     } else {
0481         qCDebug(kastsFetcher) << "this is a new enclosure:" << newEntry.id;
0482         m_newEnclosures += enclosureDetails;
0483     }
0484 
0485     return isNewEnclosure | isUpdateEnclosure;
0486 }
0487 
0488 bool UpdateFeedJob::processChapter(const QString &entryId, const int &start, const QString &chapterTitle, const QString &link, const QString &image)
0489 {
0490     bool isNewChapter = true;
0491     bool isUpdateChapter = false;
0492     ChapterDetails currentChapter;
0493 
0494     // check against existing enclosures already in database
0495     for (const ChapterDetails &chapterDetails : (m_chapters + m_newChapters)) {
0496         if ((chapterDetails.id == entryId) && (chapterDetails.start == start)) {
0497             isNewChapter = false;
0498             currentChapter = chapterDetails;
0499         }
0500     }
0501 
0502     ChapterDetails chapterDetails;
0503     chapterDetails.feed = m_url;
0504     chapterDetails.id = entryId;
0505     chapterDetails.start = start;
0506     chapterDetails.title = chapterTitle;
0507     chapterDetails.link = link;
0508     chapterDetails.image = image;
0509 
0510     if (!isNewChapter) {
0511         if ((currentChapter.title != chapterDetails.title) || (currentChapter.link != chapterDetails.link) || (currentChapter.image != chapterDetails.image)) {
0512             qCDebug(kastsFetcher) << "chapter details have been updated for:" << entryId << start;
0513             isUpdateChapter = true;
0514             m_updateChapters += chapterDetails;
0515         } else {
0516             qCDebug(kastsFetcher) << "chapter details are unchanged:" << entryId << start;
0517         }
0518     } else {
0519         qCDebug(kastsFetcher) << "this is a new chapter:" << entryId << start;
0520         m_newChapters += chapterDetails;
0521     }
0522 
0523     return isNewChapter | isUpdateChapter;
0524 }
0525 
0526 void UpdateFeedJob::writeToDatabase()
0527 {
0528     QSqlQuery writeQuery(QSqlDatabase::database(m_url));
0529 
0530     Database::transaction(m_url);
0531 
0532     // new entries
0533     writeQuery.prepare(
0534         QStringLiteral("INSERT INTO Entries VALUES (:feed, :id, :title, :content, :created, :updated, :link, :read, :new, :hasEnclosure, :image, :favorite);"));
0535     for (const EntryDetails &entryDetails : m_newEntries) {
0536         writeQuery.bindValue(QStringLiteral(":feed"), entryDetails.feed);
0537         writeQuery.bindValue(QStringLiteral(":id"), entryDetails.id);
0538         writeQuery.bindValue(QStringLiteral(":title"), entryDetails.title);
0539         writeQuery.bindValue(QStringLiteral(":content"), entryDetails.content);
0540         writeQuery.bindValue(QStringLiteral(":created"), entryDetails.created);
0541         writeQuery.bindValue(QStringLiteral(":updated"), entryDetails.updated);
0542         writeQuery.bindValue(QStringLiteral(":link"), entryDetails.link);
0543         writeQuery.bindValue(QStringLiteral(":hasEnclosure"), entryDetails.hasEnclosure);
0544         writeQuery.bindValue(QStringLiteral(":read"), entryDetails.read);
0545         writeQuery.bindValue(QStringLiteral(":new"), entryDetails.isNew);
0546         writeQuery.bindValue(QStringLiteral(":image"), entryDetails.image);
0547         writeQuery.bindValue(QStringLiteral(":favorite"), false);
0548         Database::execute(writeQuery);
0549     }
0550 
0551     // update entries
0552     writeQuery.prepare(
0553         QStringLiteral("UPDATE Entries SET title=:title, content=:content, created=:created, updated=:updated, link=:link, hasEnclosure=:hasEnclosure, "
0554                        "image=:image WHERE id=:id AND feed=:feed;"));
0555     for (const EntryDetails &entryDetails : m_updateEntries) {
0556         writeQuery.bindValue(QStringLiteral(":feed"), entryDetails.feed);
0557         writeQuery.bindValue(QStringLiteral(":id"), entryDetails.id);
0558         writeQuery.bindValue(QStringLiteral(":title"), entryDetails.title);
0559         writeQuery.bindValue(QStringLiteral(":content"), entryDetails.content);
0560         writeQuery.bindValue(QStringLiteral(":created"), entryDetails.created);
0561         writeQuery.bindValue(QStringLiteral(":updated"), entryDetails.updated);
0562         writeQuery.bindValue(QStringLiteral(":link"), entryDetails.link);
0563         writeQuery.bindValue(QStringLiteral(":hasEnclosure"), entryDetails.hasEnclosure);
0564         writeQuery.bindValue(QStringLiteral(":image"), entryDetails.image);
0565         Database::execute(writeQuery);
0566     }
0567 
0568     // new authors
0569     writeQuery.prepare(QStringLiteral("INSERT INTO Authors VALUES(:feed, :id, :name, :uri, :email);"));
0570     for (const AuthorDetails &authorDetails : m_newAuthors) {
0571         writeQuery.bindValue(QStringLiteral(":feed"), authorDetails.feed);
0572         writeQuery.bindValue(QStringLiteral(":id"), authorDetails.id);
0573         writeQuery.bindValue(QStringLiteral(":name"), authorDetails.name);
0574         writeQuery.bindValue(QStringLiteral(":uri"), authorDetails.uri);
0575         writeQuery.bindValue(QStringLiteral(":email"), authorDetails.email);
0576         Database::execute(writeQuery);
0577     }
0578 
0579     // update authors
0580     writeQuery.prepare(QStringLiteral("UPDATE Authors SET uri=:uri, email=:email WHERE feed=:feed AND id=:id AND name=:name;"));
0581     for (const AuthorDetails &authorDetails : m_updateAuthors) {
0582         writeQuery.bindValue(QStringLiteral(":feed"), authorDetails.feed);
0583         writeQuery.bindValue(QStringLiteral(":id"), authorDetails.id);
0584         writeQuery.bindValue(QStringLiteral(":name"), authorDetails.name);
0585         writeQuery.bindValue(QStringLiteral(":uri"), authorDetails.uri);
0586         writeQuery.bindValue(QStringLiteral(":email"), authorDetails.email);
0587         Database::execute(writeQuery);
0588     }
0589 
0590     // new enclosures
0591     writeQuery.prepare(QStringLiteral("INSERT INTO Enclosures VALUES (:feed, :id, :duration, :size, :title, :type, :url, :playposition, :downloaded);"));
0592     for (const EnclosureDetails &enclosureDetails : m_newEnclosures) {
0593         writeQuery.bindValue(QStringLiteral(":feed"), enclosureDetails.feed);
0594         writeQuery.bindValue(QStringLiteral(":id"), enclosureDetails.id);
0595         writeQuery.bindValue(QStringLiteral(":duration"), enclosureDetails.duration);
0596         writeQuery.bindValue(QStringLiteral(":size"), enclosureDetails.size);
0597         writeQuery.bindValue(QStringLiteral(":title"), enclosureDetails.title);
0598         writeQuery.bindValue(QStringLiteral(":type"), enclosureDetails.type);
0599         writeQuery.bindValue(QStringLiteral(":url"), enclosureDetails.url);
0600         writeQuery.bindValue(QStringLiteral(":playposition"), enclosureDetails.playPosition);
0601         writeQuery.bindValue(QStringLiteral(":downloaded"), Enclosure::statusToDb(enclosureDetails.downloaded));
0602         Database::execute(writeQuery);
0603     }
0604 
0605     // update enclosures
0606     writeQuery.prepare(QStringLiteral("UPDATE Enclosures SET duration=:duration, size=:size, title=:title, type=:type, url=:url WHERE feed=:feed AND id=:id;"));
0607     for (const EnclosureDetails &enclosureDetails : m_updateEnclosures) {
0608         writeQuery.bindValue(QStringLiteral(":feed"), enclosureDetails.feed);
0609         writeQuery.bindValue(QStringLiteral(":id"), enclosureDetails.id);
0610         writeQuery.bindValue(QStringLiteral(":duration"), enclosureDetails.duration);
0611         writeQuery.bindValue(QStringLiteral(":size"), enclosureDetails.size);
0612         writeQuery.bindValue(QStringLiteral(":title"), enclosureDetails.title);
0613         writeQuery.bindValue(QStringLiteral(":type"), enclosureDetails.type);
0614         writeQuery.bindValue(QStringLiteral(":url"), enclosureDetails.url);
0615         Database::execute(writeQuery);
0616     }
0617 
0618     // new chapters
0619     writeQuery.prepare(QStringLiteral("INSERT INTO Chapters VALUES(:feed, :id, :start, :title, :link, :image);"));
0620     for (const ChapterDetails &chapterDetails : m_newChapters) {
0621         writeQuery.bindValue(QStringLiteral(":feed"), chapterDetails.feed);
0622         writeQuery.bindValue(QStringLiteral(":id"), chapterDetails.id);
0623         writeQuery.bindValue(QStringLiteral(":start"), chapterDetails.start);
0624         writeQuery.bindValue(QStringLiteral(":title"), chapterDetails.title);
0625         writeQuery.bindValue(QStringLiteral(":link"), chapterDetails.link);
0626         writeQuery.bindValue(QStringLiteral(":image"), chapterDetails.image);
0627         Database::execute(writeQuery);
0628     }
0629 
0630     // update chapters
0631     writeQuery.prepare(QStringLiteral("UPDATE Chapters SET title=:title, link=:link, image=:image WHERE feed=:feed AND id=:id AND start=:start;"));
0632     for (const ChapterDetails &chapterDetails : m_updateChapters) {
0633         writeQuery.bindValue(QStringLiteral(":feed"), chapterDetails.feed);
0634         writeQuery.bindValue(QStringLiteral(":id"), chapterDetails.id);
0635         writeQuery.bindValue(QStringLiteral(":start"), chapterDetails.start);
0636         writeQuery.bindValue(QStringLiteral(":title"), chapterDetails.title);
0637         writeQuery.bindValue(QStringLiteral(":link"), chapterDetails.link);
0638         writeQuery.bindValue(QStringLiteral(":image"), chapterDetails.image);
0639         Database::execute(writeQuery);
0640     }
0641 
0642     // set custom amount of episodes to unread/new if required
0643     if (m_isNewFeed && (SettingsManager::self()->markUnreadOnNewFeed() == 1) && (SettingsManager::self()->markUnreadOnNewFeedCustomAmount() > 0)) {
0644         writeQuery.prepare(QStringLiteral(
0645             "UPDATE Entries SET read=:read, new=:new WHERE id in (SELECT id FROM Entries WHERE feed =:feed ORDER BY updated DESC LIMIT :recentUnread);"));
0646         writeQuery.bindValue(QStringLiteral(":feed"), m_url);
0647         writeQuery.bindValue(QStringLiteral(":read"), false);
0648         writeQuery.bindValue(QStringLiteral(":new"), true);
0649         writeQuery.bindValue(QStringLiteral(":recentUnread"), SettingsManager::self()->markUnreadOnNewFeedCustomAmount());
0650         Database::execute(writeQuery);
0651     }
0652 
0653     if (Database::commit(m_url)) {
0654         QStringList newIds, updateIds;
0655 
0656         // emit signals for new entries
0657         for (const EntryDetails &entryDetails : m_newEntries) {
0658             if (!newIds.contains(entryDetails.id)) {
0659                 newIds += entryDetails.id;
0660             }
0661         }
0662 
0663         for (const QString &id : newIds) {
0664             Q_EMIT entryAdded(m_url, id);
0665         }
0666 
0667         // emit signals for updated entries or entries with new/updated authors,
0668         // enclosures or chapters
0669         for (const EntryDetails &entryDetails : m_updateEntries) {
0670             if (!updateIds.contains(entryDetails.id) && !newIds.contains(entryDetails.id)) {
0671                 updateIds += entryDetails.id;
0672             }
0673         }
0674         for (const EnclosureDetails &enclosureDetails : (m_newEnclosures + m_updateEnclosures)) {
0675             if (!updateIds.contains(enclosureDetails.id) && !newIds.contains(enclosureDetails.id)) {
0676                 updateIds += enclosureDetails.id;
0677             }
0678         }
0679         for (const AuthorDetails &authorDetails : (m_newAuthors + m_updateAuthors)) {
0680             if (!updateIds.contains(authorDetails.id) && !newIds.contains(authorDetails.id)) {
0681                 updateIds += authorDetails.id;
0682             }
0683         }
0684         for (const ChapterDetails &chapterDetails : (m_newChapters + m_updateChapters)) {
0685             if (!updateIds.contains(chapterDetails.id) && !newIds.contains(chapterDetails.id)) {
0686                 updateIds += chapterDetails.id;
0687             }
0688         }
0689 
0690         for (const QString &id : updateIds) {
0691             qCDebug(kastsFetcher) << "updated episode" << id;
0692             Q_EMIT entryUpdated(m_url, id);
0693         }
0694     }
0695 }
0696 
0697 QString UpdateFeedJob::generateFeedDirname(const QString &name) const
0698 {
0699     // Generate directory name for enclosures based on feed name
0700     // NOTE: Any changes here require a database migration!
0701     QString dirBaseName = StorageManager::instance().sanitizedFilePath(name);
0702     QString dirName = dirBaseName;
0703 
0704     QStringList dirNameList;
0705     QSqlQuery query(QSqlDatabase::database(m_url));
0706     query.prepare(QStringLiteral("SELECT name FROM Feeds;"));
0707     while (query.next()) {
0708         dirNameList << query.value(QStringLiteral("name")).toString();
0709     }
0710 
0711     // Check for duplicate names in database and on filesystem
0712     int numDups = 1; // Minimum to append is " (1)" if file already exists
0713     while (dirNameList.contains(dirName) || QDir(StorageManager::instance().enclosureDirPath() + dirName).exists()) {
0714         dirName = QStringLiteral("%1 (%2)").arg(dirBaseName, QString::number(numDups));
0715         numDups++;
0716     }
0717     return dirName;
0718 }
0719 
0720 void UpdateFeedJob::abort()
0721 {
0722     m_abort = true;
0723     Q_EMIT aborting();
0724 }