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 }