File indexing completed on 2024-05-12 16:21:29
0001 /** 0002 * SPDX-FileCopyrightText: 2020 Tobias Fella <tobias.fella@kde.org> 0003 * SPDX-FileCopyrightText: 2021-2023 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 "entry.h" 0009 #include "entrylogging.h" 0010 0011 #include <QRegularExpression> 0012 #include <QSqlQuery> 0013 #include <QUrl> 0014 0015 #include "database.h" 0016 #include "datamanager.h" 0017 #include "feed.h" 0018 #include "fetcher.h" 0019 #include "settingsmanager.h" 0020 #include "sync/sync.h" 0021 0022 Entry::Entry(Feed *feed, const QString &id) 0023 : QObject(&DataManager::instance()) 0024 , m_feed(feed) 0025 , m_id(id) 0026 { 0027 connect(&Fetcher::instance(), &Fetcher::downloadFinished, this, [this](QString url) { 0028 if (url == m_image) { 0029 Q_EMIT imageChanged(url); 0030 Q_EMIT cachedImageChanged(cachedImage()); 0031 } else if (m_image.isEmpty() && url == m_feed->image()) { 0032 Q_EMIT imageChanged(url); 0033 Q_EMIT cachedImageChanged(cachedImage()); 0034 } 0035 }); 0036 connect(&Fetcher::instance(), &Fetcher::entryUpdated, this, [this](const QString &url, const QString &id) { 0037 if ((m_feed->url() == url) && (m_id == id)) { 0038 updateFromDb(); 0039 } 0040 }); 0041 0042 updateFromDb(false); 0043 } 0044 0045 Entry::~Entry() 0046 { 0047 } 0048 0049 void Entry::updateFromDb(bool emitSignals) 0050 { 0051 QSqlQuery entryQuery; 0052 entryQuery.prepare(QStringLiteral("SELECT * FROM Entries WHERE feed=:feed AND id=:id;")); 0053 entryQuery.bindValue(QStringLiteral(":feed"), m_feed->url()); 0054 entryQuery.bindValue(QStringLiteral(":id"), m_id); 0055 Database::instance().execute(entryQuery); 0056 if (!entryQuery.next()) { 0057 qWarning() << "No element with index" << m_id << "found in feed" << m_feed->url(); 0058 return; 0059 } 0060 0061 setCreated(QDateTime::fromSecsSinceEpoch(entryQuery.value(QStringLiteral("created")).toInt()), emitSignals); 0062 setUpdated(QDateTime::fromSecsSinceEpoch(entryQuery.value(QStringLiteral("updated")).toInt()), emitSignals); 0063 setTitle(entryQuery.value(QStringLiteral("title")).toString(), emitSignals); 0064 setContent(entryQuery.value(QStringLiteral("content")).toString(), emitSignals); 0065 setLink(entryQuery.value(QStringLiteral("link")).toString(), emitSignals); 0066 0067 if (m_read != entryQuery.value(QStringLiteral("read")).toBool()) { 0068 m_read = entryQuery.value(QStringLiteral("read")).toBool(); 0069 Q_EMIT readChanged(m_read); 0070 } 0071 if (m_new != entryQuery.value(QStringLiteral("new")).toBool()) { 0072 m_new = entryQuery.value(QStringLiteral("new")).toBool(); 0073 Q_EMIT newChanged(m_new); 0074 } 0075 if (m_favorite != entryQuery.value(QStringLiteral("favorite")).toBool()) { 0076 m_favorite = entryQuery.value(QStringLiteral("favorite")).toBool(); 0077 Q_EMIT favoriteChanged(m_favorite); 0078 } 0079 0080 setHasEnclosure(entryQuery.value(QStringLiteral("hasEnclosure")).toBool(), emitSignals); 0081 setImage(entryQuery.value(QStringLiteral("image")).toString(), emitSignals); 0082 0083 updateAuthors(emitSignals); 0084 } 0085 0086 void Entry::updateAuthors(bool emitSignals) 0087 { 0088 QVector<Author *> newAuthors; 0089 bool haveAuthorsChanged = false; 0090 0091 QSqlQuery authorQuery; 0092 authorQuery.prepare(QStringLiteral("SELECT * FROM Authors WHERE id=:id AND feed=:feed;")); 0093 authorQuery.bindValue(QStringLiteral(":id"), m_id); 0094 authorQuery.bindValue(QStringLiteral(":feed"), m_feed->url()); 0095 Database::instance().execute(authorQuery); 0096 while (authorQuery.next()) { 0097 // check if author already exists, if so, then reuse 0098 bool existingAuthor = false; 0099 QString name = authorQuery.value(QStringLiteral("name")).toString(); 0100 QString email = authorQuery.value(QStringLiteral("email")).toString(); 0101 QString url = authorQuery.value(QStringLiteral("uri")).toString(); 0102 qCDebug(kastsEntry) << name << email << url; 0103 for (Author *author : m_authors) { 0104 if (author) 0105 qCDebug(kastsEntry) << "old authors" << author->name() << author->email() << author->url(); 0106 if (author && author->name() == name && author->email() == email && author->url() == url) { 0107 existingAuthor = true; 0108 newAuthors += author; 0109 } 0110 } 0111 if (!existingAuthor) { 0112 newAuthors += new Author(name, email, url, this); 0113 haveAuthorsChanged = true; 0114 } 0115 } 0116 0117 // Finally check whether m_authors and newAuthors are identical 0118 // if not, then delete the authors that were removed 0119 for (Author *author : m_authors) { 0120 if (!newAuthors.contains(author)) { 0121 delete author; 0122 haveAuthorsChanged = true; 0123 } 0124 } 0125 0126 m_authors = newAuthors; 0127 0128 if (haveAuthorsChanged && emitSignals) { 0129 Q_EMIT authorsChanged(m_authors); 0130 qCDebug(kastsEntry) << "entry" << m_id << "authors have changed?" << haveAuthorsChanged; 0131 } 0132 } 0133 0134 QString Entry::id() const 0135 { 0136 return m_id; 0137 } 0138 0139 QString Entry::title() const 0140 { 0141 return m_title; 0142 } 0143 0144 QString Entry::content() const 0145 { 0146 return m_content; 0147 } 0148 0149 QVector<Author *> Entry::authors() const 0150 { 0151 return m_authors; 0152 } 0153 0154 QDateTime Entry::created() const 0155 { 0156 return m_created; 0157 } 0158 0159 QDateTime Entry::updated() const 0160 { 0161 return m_updated; 0162 } 0163 0164 QString Entry::link() const 0165 { 0166 return m_link; 0167 } 0168 0169 bool Entry::read() const 0170 { 0171 return m_read; 0172 } 0173 0174 bool Entry::getNew() const 0175 { 0176 return m_new; 0177 } 0178 0179 bool Entry::favorite() const 0180 { 0181 return m_favorite; 0182 } 0183 0184 QString Entry::baseUrl() const 0185 { 0186 return QUrl(m_link).adjusted(QUrl::RemovePath).toString(); 0187 } 0188 0189 void Entry::setTitle(const QString &title, bool emitSignal) 0190 { 0191 if (m_title != title) { 0192 m_title = title; 0193 if (emitSignal) { 0194 Q_EMIT titleChanged(m_title); 0195 } 0196 } 0197 } 0198 0199 void Entry::setContent(const QString &content, bool emitSignal) 0200 { 0201 if (m_content != content) { 0202 m_content = content; 0203 if (emitSignal) { 0204 Q_EMIT contentChanged(m_content); 0205 } 0206 } 0207 } 0208 0209 void Entry::setCreated(const QDateTime &created, bool emitSignal) 0210 { 0211 if (m_created != created) { 0212 m_created = created; 0213 if (emitSignal) { 0214 Q_EMIT createdChanged(m_created); 0215 } 0216 } 0217 } 0218 0219 void Entry::setUpdated(const QDateTime &updated, bool emitSignal) 0220 { 0221 if (m_updated != updated) { 0222 m_updated = updated; 0223 if (emitSignal) { 0224 Q_EMIT updatedChanged(m_updated); 0225 } 0226 } 0227 } 0228 0229 void Entry::setLink(const QString &link, bool emitSignal) 0230 { 0231 if (m_link != link) { 0232 m_link = link; 0233 if (emitSignal) { 0234 Q_EMIT linkChanged(m_link); 0235 Q_EMIT baseUrlChanged(baseUrl()); 0236 } 0237 } 0238 } 0239 0240 void Entry::setHasEnclosure(bool hasEnclosure, bool emitSignal) 0241 { 0242 if (hasEnclosure) { 0243 // if there is already an enclosure, it will be updated through separate 0244 // signals if required 0245 if (!m_enclosure) { 0246 m_enclosure = new Enclosure(this); 0247 } 0248 } else { 0249 delete m_enclosure; 0250 m_enclosure = nullptr; 0251 } 0252 if (m_hasenclosure != hasEnclosure) { 0253 m_hasenclosure = hasEnclosure; 0254 if (emitSignal) { 0255 Q_EMIT hasEnclosureChanged(m_hasenclosure); 0256 } 0257 } 0258 } 0259 0260 void Entry::setImage(const QString &image, bool emitSignal) 0261 { 0262 if (m_image != image) { 0263 m_image = image; 0264 if (emitSignal) { 0265 Q_EMIT imageChanged(m_image); 0266 Q_EMIT cachedImageChanged(cachedImage()); 0267 } 0268 } 0269 } 0270 0271 void Entry::setRead(bool read) 0272 { 0273 if (read != m_read) { 0274 // Making a detour through DataManager to make bulk operations more 0275 // performant. DataManager will call setReadInternal on every item to 0276 // be marked read/unread. So implement features there. 0277 DataManager::instance().bulkMarkRead(read, QStringList(m_id)); 0278 } 0279 } 0280 0281 void Entry::setReadInternal(bool read) 0282 { 0283 if (read != m_read) { 0284 // Make sure that operations done here can be wrapped inside an sqlite 0285 // transaction. I.e. no calls that trigger a SELECT operation. 0286 m_read = read; 0287 Q_EMIT readChanged(m_read); 0288 0289 QSqlQuery query; 0290 query.prepare(QStringLiteral("UPDATE Entries SET read=:read WHERE id=:id AND feed=:feed")); 0291 query.bindValue(QStringLiteral(":id"), m_id); 0292 query.bindValue(QStringLiteral(":feed"), m_feed->url()); 0293 query.bindValue(QStringLiteral(":read"), m_read); 0294 Database::instance().execute(query); 0295 0296 m_feed->setUnreadEntryCount(m_feed->unreadEntryCount() + (read ? -1 : 1)); 0297 0298 // Follow up actions 0299 if (read) { 0300 // 1) Remove item from queue 0301 setQueueStatusInternal(false); 0302 0303 // 2) Remove "new" label 0304 setNewInternal(false); 0305 0306 if (hasEnclosure()) { 0307 // 3) Reset play position 0308 if (SettingsManager::self()->resetPositionOnPlayed()) { 0309 m_enclosure->setPlayPosition(0); 0310 } 0311 0312 // 4) Delete episode if that setting is set 0313 if (SettingsManager::self()->autoDeleteOnPlayed() == 1) { 0314 m_enclosure->deleteFile(); 0315 } 0316 } 0317 // 5) Log a sync action to sync this state with (gpodder) server 0318 Sync::instance().storePlayedEpisodeAction(m_id); 0319 } 0320 } 0321 } 0322 0323 void Entry::setNew(bool state) 0324 { 0325 if (state != m_new) { 0326 // Making a detour through DataManager to make bulk operations more 0327 // performant. DataManager will call setNewInternal on every item to 0328 // be marked new/not new. So implement features there. 0329 DataManager::instance().bulkMarkNew(state, QStringList(m_id)); 0330 } 0331 } 0332 0333 void Entry::setNewInternal(bool state) 0334 { 0335 if (state != m_new) { 0336 // Make sure that operations done here can be wrapped inside an sqlite 0337 // transaction. I.e. no calls that trigger a SELECT operation. 0338 m_new = state; 0339 Q_EMIT newChanged(m_new); 0340 0341 QSqlQuery query; 0342 query.prepare(QStringLiteral("UPDATE Entries SET new=:new WHERE id=:id;")); 0343 query.bindValue(QStringLiteral(":id"), m_id); 0344 query.bindValue(QStringLiteral(":new"), m_new); 0345 Database::instance().execute(query); 0346 0347 // Q_EMIT m_feed->newEntryCountChanged(); // TODO: signal and slots to be implemented 0348 Q_EMIT DataManager::instance().newEntryCountChanged(m_feed->url()); 0349 } 0350 } 0351 0352 void Entry::setFavorite(bool favorite) 0353 { 0354 if (favorite != m_favorite) { 0355 // Making a detour through DataManager to make bulk operations more 0356 // performant. DataManager will call setFavoriteInternal on every item to 0357 // be marked new/not new. So implement features there. 0358 DataManager::instance().bulkMarkFavorite(favorite, QStringList(m_id)); 0359 } 0360 } 0361 0362 void Entry::setFavoriteInternal(bool favorite) 0363 { 0364 if (favorite != m_favorite) { 0365 // Make sure that operations done here can be wrapped inside an sqlite 0366 // transaction. I.e. no calls that trigger a SELECT operation. 0367 m_favorite = favorite; 0368 Q_EMIT favoriteChanged(m_favorite); 0369 0370 QSqlQuery query; 0371 query.prepare(QStringLiteral("UPDATE Entries SET favorite=:favorite WHERE id=:id;")); 0372 query.bindValue(QStringLiteral(":id"), m_id); 0373 query.bindValue(QStringLiteral(":favorite"), m_favorite); 0374 Database::instance().execute(query); 0375 0376 Q_EMIT DataManager::instance().favoriteEntryCountChanged(m_feed->url()); 0377 } 0378 } 0379 0380 QString Entry::adjustedContent(int width, int fontSize) 0381 { 0382 QString ret(m_content); 0383 QRegularExpression imgRegex(QStringLiteral("<img ((?!width=\"[0-9]+(px)?\").)*(width=\"([0-9]+)(px)?\")?[^>]*>")); 0384 0385 QRegularExpressionMatchIterator i = imgRegex.globalMatch(ret); 0386 while (i.hasNext()) { 0387 QRegularExpressionMatch match = i.next(); 0388 0389 QString imgTag(match.captured()); 0390 if (imgTag.contains(QStringLiteral("wp-smiley"))) 0391 imgTag.insert(4, QStringLiteral(" width=\"%1\"").arg(fontSize)); 0392 0393 QString widthParameter = match.captured(4); 0394 0395 if (widthParameter.length() != 0) { 0396 if (widthParameter.toInt() > width) { 0397 imgTag.replace(match.captured(3), QStringLiteral("width=\"%1\"").arg(width)); 0398 imgTag.replace(QRegularExpression(QStringLiteral("height=\"([0-9]+)(px)?\"")), QString()); 0399 } 0400 } 0401 ret.replace(match.captured(), imgTag); 0402 } 0403 0404 ret.replace(QStringLiteral("<img"), QStringLiteral("<br /> <img")); 0405 0406 // Replace strings that look like timestamps into clickable links with scheme 0407 // "timestamp://". We will pick these up in the GUI to work like chapter marks 0408 0409 QRegularExpression imgRegexDate(QStringLiteral("\\d{1,2}(:\\d{2})+")); 0410 0411 i = imgRegexDate.globalMatch(ret); 0412 while (i.hasNext()) { 0413 QRegularExpressionMatch match = i.next(); 0414 QString timeStamp(match.captured()); 0415 QStringList timeFragments(timeStamp.split(QStringLiteral(":"))); 0416 int timeUnit = 1; 0417 qint64 time = 0; 0418 for (QList<QString>::const_reverse_iterator iter = timeFragments.crbegin(); iter != timeFragments.crend(); iter++) { 0419 time += (*iter).toInt() * 1000 * timeUnit; 0420 timeUnit *= 60; 0421 } 0422 timeStamp = QStringLiteral("<a href=\"timestamp://%1\">%2</a>").arg(time).arg(timeStamp); 0423 ret.replace(match.captured(), timeStamp); 0424 } 0425 0426 return ret; 0427 } 0428 0429 Enclosure *Entry::enclosure() const 0430 { 0431 return m_enclosure; 0432 } 0433 0434 bool Entry::hasEnclosure() const 0435 { 0436 return m_hasenclosure; 0437 } 0438 0439 QString Entry::image() const 0440 { 0441 if (m_hasenclosure && !m_enclosure->cachedEmbeddedImage().isEmpty()) { 0442 // use embedded image if available 0443 return m_enclosure->cachedEmbeddedImage(); 0444 } else if (!m_image.isEmpty()) { 0445 return m_image; 0446 } else { 0447 // else fall back to feed image 0448 return m_feed->image(); 0449 } 0450 } 0451 0452 QString Entry::cachedImage() const 0453 { 0454 // First check for an image in the downloaded file 0455 if (m_hasenclosure && !m_enclosure->cachedEmbeddedImage().isEmpty()) { 0456 // use embedded image if available 0457 return m_enclosure->cachedEmbeddedImage(); 0458 } 0459 0460 // Then check for the entry image, fall back if needed to feed image 0461 QString image = m_image; 0462 if (image.isEmpty()) { 0463 // else fall back to feed image 0464 image = m_feed->image(); 0465 } 0466 0467 return Fetcher::instance().image(image); 0468 } 0469 0470 bool Entry::queueStatus() const 0471 { 0472 return DataManager::instance().entryInQueue(this); 0473 } 0474 0475 void Entry::setQueueStatus(bool state) 0476 { 0477 if (state != DataManager::instance().entryInQueue(this)) { 0478 // Making a detour through DataManager to make bulk operations more 0479 // performant. DataManager will call setQueueStatusInternal on every 0480 // item to be processed. So implement features there. 0481 DataManager::instance().bulkQueueStatus(state, QStringList(m_id)); 0482 } 0483 } 0484 0485 void Entry::setQueueStatusInternal(bool state) 0486 { 0487 // Make sure that operations done here can be wrapped inside an sqlite 0488 // transaction. I.e. no calls that trigger a SELECT operation. 0489 if (state) { 0490 DataManager::instance().addToQueue(m_id); 0491 // Set status to unplayed/unread when adding item to the queue 0492 setReadInternal(false); 0493 } else { 0494 DataManager::instance().removeFromQueue(m_id); 0495 // Unset "new" state 0496 setNewInternal(false); 0497 } 0498 0499 Q_EMIT queueStatusChanged(state); 0500 } 0501 0502 Feed *Entry::feed() const 0503 { 0504 return m_feed; 0505 }