File indexing completed on 2025-01-05 04:29:56

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