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 }