File indexing completed on 2024-05-05 05:13:01

0001 /*
0002     This file is part of Akregator.
0003 
0004     SPDX-FileCopyrightText: 2004 Stanislav Karchebny <Stanislav.Karchebny@kdemail.net>
0005     SPDX-FileCopyrightText: 2005 Frank Osterfeld <osterfeld@kde.org>
0006 
0007     SPDX-License-Identifier: GPL-2.0-or-later WITH Qt-Commercial-exception-1.0
0008 */
0009 
0010 #include "feed.h"
0011 
0012 #include "akregatorconfig.h"
0013 #include "article.h"
0014 #include "articlejobs.h"
0015 #include "feedretriever.h"
0016 #include "fetchqueue.h"
0017 #include "folder.h"
0018 #include "job/downloadfeediconjob.h"
0019 #include "notificationmanager.h"
0020 #include "storage/feedstorage.h"
0021 #include "storage/storage.h"
0022 #include "treenodevisitor.h"
0023 #include "types.h"
0024 #include "utils.h"
0025 
0026 #include "akregator_debug.h"
0027 
0028 #include <QUrl>
0029 
0030 #include <QDateTime>
0031 #include <QDomDocument>
0032 #include <QDomElement>
0033 #include <QHash>
0034 #include <QList>
0035 #include <QRandomGenerator>
0036 #include <QTimer>
0037 
0038 #include <QStandardPaths>
0039 
0040 using Syndication::ItemPtr;
0041 using namespace Akregator;
0042 
0043 template<typename Key, typename Value, template<typename, typename> class Container>
0044 QList<Value> valuesToVector(const Container<Key, Value> &container)
0045 {
0046     QList<Value> values;
0047     values.reserve(container.size());
0048     for (const Value &value : container) {
0049         values << value;
0050     }
0051     return values;
0052 }
0053 
0054 class Akregator::FeedPrivate
0055 {
0056     Akregator::Feed *const q;
0057 
0058 public:
0059     explicit FeedPrivate(Backend::Storage *storage, Akregator::Feed *qq);
0060 
0061     Backend::Storage *m_storage = nullptr;
0062     bool m_autoFetch = false;
0063     int m_fetchInterval;
0064     Feed::ArchiveMode m_archiveMode;
0065     int m_maxArticleAge;
0066     int m_maxArticleNumber;
0067     bool m_markImmediatelyAsRead = false;
0068     bool m_useNotification = false;
0069     bool m_loadLinkedWebsite = false;
0070 
0071     Syndication::ErrorCode m_fetchErrorCode;
0072     int m_fetchTries;
0073     bool m_followDiscovery = false;
0074     Syndication::Loader *m_loader = nullptr;
0075     bool m_articlesLoaded = false;
0076     Backend::FeedStorage *m_archive = nullptr;
0077 
0078     QString m_xmlUrl;
0079     QString m_htmlUrl;
0080     QString m_description;
0081     QString m_comment;
0082     QString m_copyright;
0083 
0084     /** list of feed articles */
0085     QHash<QString, Article> articles;
0086 
0087     /** list of deleted articles. This contains **/
0088     QList<Article> m_deletedArticles;
0089 
0090     /** caches guids of deleted articles for notification */
0091 
0092     QList<Article> m_addedArticlesNotify;
0093     QList<Article> m_removedArticlesNotify;
0094     QList<Article> m_updatedArticlesNotify;
0095 
0096     Feed::ImageInfo m_logoInfo;
0097     Feed::ImageInfo m_faviconInfo;
0098 
0099     QIcon m_favicon;
0100     mutable int m_totalCount;
0101     void setTotalCountDirty() const;
0102 };
0103 
0104 QString Akregator::Feed::archiveModeToString(ArchiveMode mode)
0105 {
0106     switch (mode) {
0107     case keepAllArticles:
0108         return QStringLiteral("keepAllArticles");
0109     case disableArchiving:
0110         return QStringLiteral("disableArchiving");
0111     case limitArticleNumber:
0112         return QStringLiteral("limitArticleNumber");
0113     case limitArticleAge:
0114         return QStringLiteral("limitArticleAge");
0115     default:
0116         break;
0117     }
0118     return QStringLiteral("globalDefault");
0119 }
0120 
0121 Akregator::Feed *Akregator::Feed::fromOPML(const QDomElement &e, Backend::Storage *storage)
0122 {
0123     if (!e.hasAttribute(QStringLiteral("xmlUrl")) && !e.hasAttribute(QStringLiteral("xmlurl")) && !e.hasAttribute(QStringLiteral("xmlURL"))) {
0124         return nullptr;
0125     }
0126 
0127     const QString title = e.hasAttribute(QStringLiteral("text")) ? e.attribute(QStringLiteral("text")) : e.attribute(QStringLiteral("title"));
0128 
0129     QString xmlUrl = e.hasAttribute(QStringLiteral("xmlUrl")) ? e.attribute(QStringLiteral("xmlUrl")) : e.attribute(QStringLiteral("xmlurl"));
0130     if (xmlUrl.isEmpty()) {
0131         xmlUrl = e.attribute(QStringLiteral("xmlURL"));
0132     }
0133 
0134     bool useCustomFetchInterval = e.attribute(QStringLiteral("useCustomFetchInterval")) == QLatin1StringView("true");
0135 
0136     const QString htmlUrl = e.attribute(QStringLiteral("htmlUrl"));
0137     const QString description = e.attribute(QStringLiteral("description"));
0138     const QString copyright = e.attribute(QStringLiteral("copyright"));
0139     const int fetchInterval = e.attribute(QStringLiteral("fetchInterval")).toInt();
0140     const Akregator::Feed::ArchiveMode archiveMode = stringToArchiveMode(e.attribute(QStringLiteral("archiveMode")));
0141     const int maxArticleAge = e.attribute(QStringLiteral("maxArticleAge")).toUInt();
0142     const int maxArticleNumber = e.attribute(QStringLiteral("maxArticleNumber")).toUInt();
0143     const bool markImmediatelyAsRead = e.attribute(QStringLiteral("markImmediatelyAsRead")) == QLatin1StringView("true");
0144     const bool useNotification = e.attribute(QStringLiteral("useNotification")) == QLatin1StringView("true");
0145     const bool loadLinkedWebsite = e.attribute(QStringLiteral("loadLinkedWebsite")) == QLatin1StringView("true");
0146     const QString comment = e.attribute(QStringLiteral("comment"));
0147     const QString faviconUrl = e.attribute(QStringLiteral("faviconUrl"));
0148     Feed::ImageInfo faviconInfo;
0149     faviconInfo.imageUrl = faviconUrl;
0150     if (e.hasAttribute(QStringLiteral("faviconWidth"))) {
0151         faviconInfo.width = e.attribute(QStringLiteral("faviconWidth")).toInt();
0152     }
0153     if (e.hasAttribute(QStringLiteral("faviconHeight"))) {
0154         faviconInfo.height = e.attribute(QStringLiteral("faviconHeight")).toInt();
0155     }
0156 
0157     Feed::ImageInfo logoInfo;
0158     const QString logoUrl = e.attribute(QStringLiteral("logoUrl"));
0159     logoInfo.imageUrl = logoUrl;
0160     if (e.hasAttribute(QStringLiteral("logoWidth"))) {
0161         logoInfo.width = e.attribute(QStringLiteral("logoWidth")).toInt();
0162     }
0163     if (e.hasAttribute(QStringLiteral("logoHeight"))) {
0164         logoInfo.height = e.attribute(QStringLiteral("logoHeight")).toInt();
0165     }
0166 
0167     const uint id = e.attribute(QStringLiteral("id")).toUInt();
0168 
0169     Feed *const feed = new Feed(storage);
0170     feed->setTitle(title);
0171     feed->setFaviconInfo(faviconInfo);
0172     feed->setLogoInfo(logoInfo);
0173     feed->setCopyright(copyright);
0174 
0175     feed->setXmlUrl(xmlUrl);
0176     feed->setCustomFetchIntervalEnabled(useCustomFetchInterval);
0177     feed->setHtmlUrl(htmlUrl);
0178     feed->setId(id);
0179     feed->setDescription(description);
0180     feed->setArchiveMode(archiveMode);
0181     feed->setUseNotification(useNotification);
0182     feed->setFetchInterval(fetchInterval);
0183     feed->setMaxArticleAge(maxArticleAge);
0184     feed->setMaxArticleNumber(maxArticleNumber);
0185     feed->setMarkImmediatelyAsRead(markImmediatelyAsRead);
0186     feed->setLoadLinkedWebsite(loadLinkedWebsite);
0187     feed->setComment(comment);
0188     if (!feed->d->m_archive && storage) {
0189         // Instead of loading the articles, we use the cache from storage
0190         feed->d->m_archive = storage->archiveFor(xmlUrl);
0191         feed->d->m_totalCount = feed->d->m_archive->totalCount();
0192     }
0193     return feed;
0194 }
0195 
0196 bool Akregator::Feed::accept(TreeNodeVisitor *visitor)
0197 {
0198     if (visitor->visitFeed(this)) {
0199         return true;
0200     } else {
0201         return visitor->visitTreeNode(this);
0202     }
0203 }
0204 
0205 QList<const Akregator::Folder *> Akregator::Feed::folders() const
0206 {
0207     return {};
0208 }
0209 
0210 QList<Folder *> Akregator::Feed::folders()
0211 {
0212     return {};
0213 }
0214 
0215 QList<const Akregator::Feed *> Akregator::Feed::feeds() const
0216 {
0217     QList<const Akregator::Feed *> list;
0218     list.append(this);
0219     return list;
0220 }
0221 
0222 QList<Akregator::Feed *> Akregator::Feed::feeds()
0223 {
0224     QList<Feed *> list;
0225     list.append(this);
0226     return list;
0227 }
0228 
0229 Article Akregator::Feed::findArticle(const QString &guid) const
0230 {
0231     return d->articles.value(guid);
0232 }
0233 
0234 QList<Article> Akregator::Feed::articles()
0235 {
0236     if (!d->m_articlesLoaded) {
0237         loadArticles();
0238     }
0239     return valuesToVector(d->articles);
0240 }
0241 
0242 Backend::Storage *Akregator::Feed::storage()
0243 {
0244     return d->m_storage;
0245 }
0246 
0247 void Akregator::Feed::loadArticles()
0248 {
0249     if (d->m_articlesLoaded) {
0250         return;
0251     }
0252 
0253     if (!d->m_archive && d->m_storage) {
0254         d->m_archive = d->m_storage->archiveFor(xmlUrl());
0255     }
0256 
0257     const QStringList list = d->m_archive->articles();
0258     for (QStringList::ConstIterator it = list.constBegin(); it != list.constEnd(); ++it) {
0259         Article mya(*it, this, d->m_archive);
0260         d->articles[mya.guid()] = mya;
0261         if (mya.isDeleted()) {
0262             d->m_deletedArticles.append(mya);
0263         }
0264     }
0265 
0266     d->m_articlesLoaded = true;
0267     enforceLimitArticleNumber();
0268     recalcUnreadCount();
0269 }
0270 
0271 void Akregator::Feed::recalcUnreadCount()
0272 {
0273     QList<Article> tarticles = articles();
0274     QList<Article>::ConstIterator it;
0275     QList<Article>::ConstIterator en = tarticles.constEnd();
0276 
0277     int oldUnread = d->m_archive->unread();
0278 
0279     int unread = 0;
0280 
0281     for (it = tarticles.constBegin(); it != en; ++it) {
0282         if (!(*it).isDeleted() && (*it).status() != Read) {
0283             ++unread;
0284         }
0285     }
0286 
0287     if (unread != oldUnread) {
0288         d->m_archive->setUnread(unread);
0289         nodeModified();
0290     }
0291 }
0292 
0293 Akregator::Feed::ArchiveMode Akregator::Feed::stringToArchiveMode(const QString &str)
0294 {
0295     if (str == QLatin1StringView("globalDefault")) {
0296         return globalDefault;
0297     } else if (str == QLatin1StringView("keepAllArticles")) {
0298         return keepAllArticles;
0299     } else if (str == QLatin1StringView("disableArchiving")) {
0300         return disableArchiving;
0301     } else if (str == QLatin1StringView("limitArticleNumber")) {
0302         return limitArticleNumber;
0303     } else if (str == QLatin1StringView("limitArticleAge")) {
0304         return limitArticleAge;
0305     }
0306 
0307     return globalDefault;
0308 }
0309 
0310 Akregator::FeedPrivate::FeedPrivate(Backend::Storage *storage_, Akregator::Feed *qq)
0311     : q(qq)
0312     , m_storage(storage_)
0313     , m_autoFetch(false)
0314     , m_fetchInterval(30)
0315     , m_archiveMode(Feed::globalDefault)
0316     , m_maxArticleAge(60)
0317     , m_maxArticleNumber(1000)
0318     , m_markImmediatelyAsRead(false)
0319     , m_useNotification(false)
0320     , m_loadLinkedWebsite(false)
0321     , m_fetchErrorCode(Syndication::Success)
0322     , m_fetchTries(0)
0323     , m_followDiscovery(false)
0324     , m_loader(nullptr)
0325     , m_articlesLoaded(false)
0326     , m_archive(nullptr)
0327     , m_totalCount(-1)
0328 {
0329     Q_ASSERT(q);
0330     Q_ASSERT(m_storage);
0331 }
0332 
0333 void Akregator::FeedPrivate::setTotalCountDirty() const
0334 {
0335     m_totalCount = -1;
0336 }
0337 
0338 Akregator::Feed::Feed(Backend::Storage *storage)
0339     : TreeNode()
0340     , d(new FeedPrivate(storage, this))
0341 {
0342 }
0343 
0344 Akregator::Feed::~Feed()
0345 {
0346     slotAbortFetch();
0347     emitSignalDestroyed();
0348 }
0349 
0350 void Akregator::Feed::loadFavicon(const QString &url, bool downloadFavicon)
0351 {
0352     QUrl u(url);
0353     if (u.scheme().isEmpty()) {
0354         qCWarning(AKREGATOR_LOG) << "Invalid url" << url;
0355     }
0356     if (u.isLocalFile()) {
0357         setFaviconLocalPath(u.toLocalFile());
0358     } else {
0359         auto job = new Akregator::DownloadFeedIconJob(this);
0360         job->setFeedIconUrl(u);
0361         job->setDownloadFavicon(downloadFavicon);
0362         connect(job, &DownloadFeedIconJob::result, this, [this](const QString &fileName) {
0363             setFaviconLocalPath(fileName);
0364         });
0365         if (!job->start()) {
0366             qCWarning(AKREGATOR_LOG) << "Impossible to start DownloadFeedIconJob for url: " << url;
0367         }
0368     }
0369 }
0370 
0371 bool Akregator::Feed::useCustomFetchInterval() const
0372 {
0373     return d->m_autoFetch;
0374 }
0375 
0376 void Akregator::Feed::setCustomFetchIntervalEnabled(bool enabled)
0377 {
0378     d->m_autoFetch = enabled;
0379 }
0380 
0381 int Akregator::Feed::fetchInterval() const
0382 {
0383     return d->m_fetchInterval;
0384 }
0385 
0386 void Akregator::Feed::setFetchInterval(int interval)
0387 {
0388     d->m_fetchInterval = interval;
0389 }
0390 
0391 int Akregator::Feed::maxArticleAge() const
0392 {
0393     return d->m_maxArticleAge;
0394 }
0395 
0396 void Akregator::Feed::setMaxArticleAge(int maxArticleAge)
0397 {
0398     d->m_maxArticleAge = maxArticleAge;
0399 }
0400 
0401 int Akregator::Feed::maxArticleNumber() const
0402 {
0403     return d->m_maxArticleNumber;
0404 }
0405 
0406 void Akregator::Feed::setMaxArticleNumber(int maxArticleNumber)
0407 {
0408     d->m_maxArticleNumber = maxArticleNumber;
0409 }
0410 
0411 bool Akregator::Feed::markImmediatelyAsRead() const
0412 {
0413     return d->m_markImmediatelyAsRead;
0414 }
0415 
0416 bool Akregator::Feed::isFetching() const
0417 {
0418     return d->m_loader != nullptr;
0419 }
0420 
0421 void Akregator::Feed::setMarkImmediatelyAsRead(bool enabled)
0422 {
0423     d->m_markImmediatelyAsRead = enabled;
0424 }
0425 
0426 void Akregator::Feed::setComment(const QString &comment)
0427 {
0428     d->m_comment = comment;
0429 }
0430 
0431 QString Akregator::Feed::comment() const
0432 {
0433     return d->m_comment;
0434 }
0435 
0436 void Akregator::Feed::setUseNotification(bool enabled)
0437 {
0438     d->m_useNotification = enabled;
0439 }
0440 
0441 bool Akregator::Feed::useNotification() const
0442 {
0443     return d->m_useNotification;
0444 }
0445 
0446 void Akregator::Feed::setLoadLinkedWebsite(bool enabled)
0447 {
0448     d->m_loadLinkedWebsite = enabled;
0449 }
0450 
0451 bool Akregator::Feed::loadLinkedWebsite() const
0452 {
0453     return d->m_loadLinkedWebsite;
0454 }
0455 
0456 Akregator::Feed::ImageInfo Akregator::Feed::logoInfo() const
0457 {
0458     return d->m_logoInfo;
0459 }
0460 
0461 QString Akregator::Feed::xmlUrl() const
0462 {
0463     return d->m_xmlUrl;
0464 }
0465 
0466 void Akregator::Feed::setXmlUrl(const QString &s)
0467 {
0468     d->m_xmlUrl = s;
0469     if (!Settings::fetchOnStartup()) {
0470         QTimer::singleShot(QRandomGenerator::global()->bounded(4000),
0471                            this,
0472                            &Feed::slotAddFeedIconListener); // TODO: let's give a gui some time to show up before starting the fetch when no fetch on startup is
0473                                                             // used. replace this with something proper later...
0474     }
0475 }
0476 
0477 QString Akregator::Feed::htmlUrl() const
0478 {
0479     return d->m_htmlUrl;
0480 }
0481 
0482 void Akregator::Feed::setHtmlUrl(const QString &s)
0483 {
0484     d->m_htmlUrl = s;
0485 }
0486 
0487 Akregator::Feed::ImageInfo Akregator::Feed::faviconInfo() const
0488 {
0489     return d->m_faviconInfo;
0490 }
0491 
0492 void Akregator::Feed::setFaviconLocalPath(const QString &localPath)
0493 {
0494     d->m_faviconInfo.imageUrl = QUrl::fromLocalFile(localPath).toString();
0495     setFavicon(QIcon(localPath));
0496 }
0497 
0498 void Akregator::Feed::setFaviconInfo(const Akregator::Feed::ImageInfo &info)
0499 {
0500     d->m_faviconInfo = info;
0501     QUrl u(info.imageUrl);
0502     if (u.isLocalFile()) {
0503         setFavicon(QIcon(u.toLocalFile()));
0504     }
0505 }
0506 
0507 QString Akregator::Feed::description() const
0508 {
0509     return d->m_description;
0510 }
0511 
0512 void Akregator::Feed::setDescription(const QString &s)
0513 {
0514     d->m_description = s;
0515 }
0516 
0517 bool Akregator::Feed::fetchErrorOccurred() const
0518 {
0519     return d->m_fetchErrorCode != Syndication::Success;
0520 }
0521 
0522 Syndication::ErrorCode Akregator::Feed::fetchErrorCode() const
0523 {
0524     return d->m_fetchErrorCode;
0525 }
0526 
0527 bool Akregator::Feed::isArticlesLoaded() const
0528 {
0529     return d->m_articlesLoaded;
0530 }
0531 
0532 QDomElement Akregator::Feed::toOPML(QDomElement parent, QDomDocument document) const
0533 {
0534     QDomElement el = document.createElement(QStringLiteral("outline"));
0535     el.setAttribute(QStringLiteral("text"), title());
0536     el.setAttribute(QStringLiteral("title"), title());
0537     el.setAttribute(QStringLiteral("xmlUrl"), d->m_xmlUrl);
0538     el.setAttribute(QStringLiteral("htmlUrl"), d->m_htmlUrl);
0539     el.setAttribute(QStringLiteral("id"), QString::number(id()));
0540     el.setAttribute(QStringLiteral("description"), d->m_description);
0541     el.setAttribute(QStringLiteral("useCustomFetchInterval"), (useCustomFetchInterval() ? QStringLiteral("true") : QStringLiteral("false")));
0542     el.setAttribute(QStringLiteral("fetchInterval"), QString::number(fetchInterval()));
0543     el.setAttribute(QStringLiteral("archiveMode"), archiveModeToString(d->m_archiveMode));
0544     el.setAttribute(QStringLiteral("maxArticleAge"), d->m_maxArticleAge);
0545     el.setAttribute(QStringLiteral("comment"), d->m_comment);
0546     el.setAttribute(QStringLiteral("maxArticleNumber"), d->m_maxArticleNumber);
0547     el.setAttribute(QStringLiteral("copyright"), d->m_copyright);
0548 
0549     if (d->m_markImmediatelyAsRead) {
0550         el.setAttribute(QStringLiteral("markImmediatelyAsRead"), QStringLiteral("true"));
0551     }
0552     if (d->m_useNotification) {
0553         el.setAttribute(QStringLiteral("useNotification"), QStringLiteral("true"));
0554     }
0555     if (d->m_loadLinkedWebsite) {
0556         el.setAttribute(QStringLiteral("loadLinkedWebsite"), QStringLiteral("true"));
0557     }
0558     if (!d->m_faviconInfo.imageUrl.isEmpty()) {
0559         el.setAttribute(QStringLiteral("faviconUrl"), d->m_faviconInfo.imageUrl);
0560         if (d->m_faviconInfo.width != -1) {
0561             el.setAttribute(QStringLiteral("faviconWidth"), d->m_faviconInfo.width);
0562         }
0563         if (d->m_faviconInfo.height != -1) {
0564             el.setAttribute(QStringLiteral("faviconHeight"), d->m_faviconInfo.height);
0565         }
0566     }
0567     if (!d->m_logoInfo.imageUrl.isEmpty()) {
0568         el.setAttribute(QStringLiteral("logoUrl"), d->m_logoInfo.imageUrl);
0569         if (d->m_logoInfo.width != -1) {
0570             el.setAttribute(QStringLiteral("logoWidth"), d->m_logoInfo.width);
0571         }
0572         if (d->m_logoInfo.height != -1) {
0573             el.setAttribute(QStringLiteral("logoHeight"), d->m_logoInfo.height);
0574         }
0575     }
0576     el.setAttribute(QStringLiteral("maxArticleNumber"), d->m_maxArticleNumber);
0577     el.setAttribute(QStringLiteral("type"), QStringLiteral("rss")); // despite some additional fields, it is still "rss" OPML
0578     el.setAttribute(QStringLiteral("version"), QStringLiteral("RSS"));
0579     parent.appendChild(el);
0580     return el;
0581 }
0582 
0583 KJob *Akregator::Feed::createMarkAsReadJob()
0584 {
0585     auto job = new ArticleModifyJob;
0586     const auto arts = articles();
0587     for (const Article &i : arts) {
0588         const ArticleId aid = {xmlUrl(), i.guid()};
0589         job->setStatus(aid, Read);
0590     }
0591     return job;
0592 }
0593 
0594 void Akregator::Feed::slotAddToFetchQueue(FetchQueue *queue, bool intervalFetchOnly)
0595 {
0596     if (!intervalFetchOnly) {
0597         queue->addFeed(this);
0598     } else {
0599         int interval = -1;
0600 
0601         if (useCustomFetchInterval()) {
0602             interval = fetchInterval() * 60;
0603         } else if (Settings::useIntervalFetch()) {
0604             interval = Settings::autoFetchInterval() * 60;
0605         }
0606 
0607         const uint lastFetch = d->m_archive->lastFetch().toSecsSinceEpoch();
0608 
0609         const uint now = QDateTime::currentDateTimeUtc().toSecsSinceEpoch();
0610 
0611         if (interval > 0 && (now - lastFetch) >= static_cast<uint>(interval)) {
0612             queue->addFeed(this);
0613         }
0614     }
0615 }
0616 
0617 void Akregator::Feed::slotAddFeedIconListener()
0618 {
0619     if (d->m_faviconInfo.imageUrl.isEmpty()) {
0620         loadFavicon(d->m_xmlUrl, true);
0621     } else {
0622         loadFavicon(d->m_faviconInfo.imageUrl, false);
0623     }
0624 }
0625 
0626 void Akregator::Feed::appendArticles(const Syndication::FeedPtr &feed)
0627 {
0628     d->setTotalCountDirty();
0629     bool changed = false;
0630     const bool notify = useNotification() || Settings::useNotifications();
0631 
0632     QList<ItemPtr> items = feed->items();
0633     QList<ItemPtr>::ConstIterator it = items.constBegin();
0634     QList<ItemPtr>::ConstIterator en = items.constEnd();
0635 
0636     int nudge = 0;
0637 
0638     QList<Article> deletedArticles = d->m_deletedArticles;
0639 
0640     for (; it != en; ++it) {
0641         if (!d->articles.contains((*it)->id())) { // article not in list
0642             Article mya(*it, this);
0643             mya.offsetPubDate(nudge);
0644             nudge--;
0645             appendArticle(mya);
0646             d->m_addedArticlesNotify.append(mya);
0647 
0648             if (!mya.isDeleted() && !markImmediatelyAsRead()) {
0649                 mya.setStatus(New);
0650             } else {
0651                 mya.setStatus(Read);
0652             }
0653             if (notify) {
0654                 NotificationManager::self()->slotNotifyArticle(mya);
0655             }
0656             changed = true;
0657         } else { // article is in list
0658             // if the article's guid is no hash but an ID, we have to check if the article was updated. That's done by comparing the hash values.
0659             Article old = d->articles[(*it)->id()];
0660             Article mya(*it, this);
0661             if (!mya.guidIsHash() && mya.hash() != old.hash() && !old.isDeleted()) {
0662                 mya.setKeep(old.keep());
0663                 int oldstatus = old.status();
0664                 old.setStatus(Read);
0665 
0666                 d->articles.remove(old.guid());
0667                 appendArticle(mya);
0668 
0669                 mya.setStatus(oldstatus);
0670 
0671                 d->m_updatedArticlesNotify.append(mya);
0672                 changed = true;
0673             } else if (old.isDeleted()) {
0674                 deletedArticles.removeAll(mya);
0675             }
0676         }
0677     }
0678 
0679     QList<Article>::ConstIterator dit = deletedArticles.constBegin();
0680     QList<Article>::ConstIterator dtmp;
0681     QList<Article>::ConstIterator den = deletedArticles.constEnd();
0682 
0683     // delete articles with delete flag set completely from archive, which aren't in the current feed source anymore
0684     while (dit != den) {
0685         dtmp = dit;
0686         ++dit;
0687         d->articles.remove((*dtmp).guid());
0688         d->m_archive->deleteArticle((*dtmp).guid());
0689         d->m_removedArticlesNotify.append(*dtmp);
0690         changed = true;
0691         d->m_deletedArticles.removeAll(*dtmp);
0692     }
0693 
0694     if (changed) {
0695         articlesModified();
0696     }
0697 }
0698 
0699 bool Akregator::Feed::usesExpiryByAge() const
0700 {
0701     return (d->m_archiveMode == globalDefault && Settings::archiveMode() == Settings::EnumArchiveMode::limitArticleAge) || d->m_archiveMode == limitArticleAge;
0702 }
0703 
0704 bool Akregator::Feed::isExpired(const Article &a) const
0705 {
0706     const QDateTime now = QDateTime::currentDateTime();
0707     int expiryAge = -1;
0708     // check whether the feed uses the global default and the default is limitArticleAge
0709     if (d->m_archiveMode == globalDefault && Settings::archiveMode() == Settings::EnumArchiveMode::limitArticleAge) {
0710         expiryAge = Settings::maxArticleAge() * 24 * 3600;
0711     } else // otherwise check if this feed has limitArticleAge set
0712         if (d->m_archiveMode == limitArticleAge) {
0713             expiryAge = d->m_maxArticleAge * 24 * 3600;
0714         }
0715 
0716     return expiryAge != -1 && a.pubDate().secsTo(now) > expiryAge;
0717 }
0718 
0719 void Akregator::Feed::appendArticle(const Article &a)
0720 {
0721     if ((a.keep() && Settings::doNotExpireImportantArticles()) || (!usesExpiryByAge() || !isExpired(a))) { // if not expired
0722         if (!d->articles.contains(a.guid())) {
0723             d->articles[a.guid()] = a;
0724             if (!a.isDeleted() && a.status() != Read) {
0725                 setUnread(unread() + 1);
0726             }
0727         }
0728     }
0729 }
0730 
0731 void Akregator::Feed::fetch(bool followDiscovery)
0732 {
0733     d->m_followDiscovery = followDiscovery;
0734     d->m_fetchTries = 0;
0735 
0736     // mark all new as unread
0737     for (auto it = d->articles.begin(), end = d->articles.end(); it != end; ++it) {
0738         if ((*it).status() == New) {
0739             (*it).setStatus(Unread);
0740         }
0741     }
0742 
0743     Q_EMIT fetchStarted(this);
0744 
0745     tryFetch();
0746 }
0747 
0748 void Akregator::Feed::slotAbortFetch()
0749 {
0750     if (d->m_loader) {
0751         d->m_loader->abort();
0752     }
0753 }
0754 
0755 void Akregator::Feed::tryFetch()
0756 {
0757     d->m_fetchErrorCode = Syndication::Success;
0758 
0759     d->m_loader = Syndication::Loader::create(this, SLOT(fetchCompleted(Syndication::Loader *, Syndication::FeedPtr, Syndication::ErrorCode)));
0760     d->m_loader->loadFrom(QUrl(d->m_xmlUrl), new FeedRetriever());
0761 }
0762 
0763 void Akregator::Feed::fetchCompleted(Syndication::Loader *l, Syndication::FeedPtr doc, Syndication::ErrorCode status)
0764 {
0765     // Note that loader instances delete themselves
0766     d->m_loader = nullptr;
0767 
0768     // fetching wasn't successful:
0769     if (status != Syndication::Success) {
0770         if (status == Syndication::Aborted) {
0771             d->m_fetchErrorCode = Syndication::Success;
0772             Q_EMIT fetchAborted(this);
0773         } else if (d->m_followDiscovery && (status == Syndication::InvalidXml) && (d->m_fetchTries < 3) && (l->discoveredFeedURL().isValid())) {
0774             d->m_fetchTries++;
0775             d->m_xmlUrl = l->discoveredFeedURL().url();
0776             Q_EMIT fetchDiscovery(this);
0777             tryFetch();
0778         } else {
0779             d->m_fetchErrorCode = status;
0780             Q_EMIT fetchError(this);
0781         }
0782         markAsFetchedNow();
0783         return;
0784     }
0785 
0786     loadArticles(); // TODO: make me fly: make this delayed
0787 
0788     if (!doc->icon().isNull() && !doc->icon()->url().isEmpty()) {
0789         loadFavicon(doc->icon()->url(), false);
0790         d->m_faviconInfo.width = doc->icon()->width();
0791         d->m_faviconInfo.height = doc->icon()->height();
0792     } else {
0793         loadFavicon(xmlUrl(), true);
0794     }
0795     d->m_fetchErrorCode = Syndication::Success;
0796 
0797     if (!doc->image().isNull()) {
0798         d->m_logoInfo.imageUrl = doc->image()->url();
0799         d->m_logoInfo.width = doc->image()->width();
0800         d->m_logoInfo.height = doc->image()->height();
0801     }
0802 
0803     if (title().isEmpty()) {
0804         setTitle(Syndication::htmlToPlainText(doc->title()));
0805     }
0806 
0807     d->m_description = doc->description();
0808     d->m_copyright = doc->copyright();
0809     d->m_htmlUrl = doc->link();
0810 
0811     appendArticles(doc);
0812 
0813     markAsFetchedNow();
0814     Q_EMIT fetched(this);
0815 }
0816 
0817 void Akregator::Feed::markAsFetchedNow()
0818 {
0819     if (d->m_archive) {
0820         d->m_archive->setLastFetch(QDateTime::currentDateTimeUtc());
0821     }
0822 }
0823 
0824 QIcon Akregator::Feed::icon() const
0825 {
0826     if (fetchErrorOccurred()) {
0827         return QIcon::fromTheme(QStringLiteral("dialog-error"));
0828     }
0829 
0830     return !d->m_favicon.isNull() ? d->m_favicon : QIcon::fromTheme(QStringLiteral("text-html"));
0831 }
0832 
0833 void Akregator::Feed::deleteExpiredArticles(ArticleDeleteJob *deleteJob)
0834 {
0835     if (!usesExpiryByAge()) {
0836         return;
0837     }
0838 
0839     setNotificationMode(false);
0840 
0841     Akregator::ArticleIdList toDelete;
0842     const QString feedUrl = xmlUrl();
0843     const bool useKeep = Settings::doNotExpireImportantArticles();
0844 
0845     for (const Article &i : std::as_const(d->articles)) {
0846         if ((!useKeep || !i.keep()) && isExpired(i)) {
0847             const ArticleId aid = {feedUrl, i.guid()};
0848             toDelete.append(aid);
0849         }
0850     }
0851 
0852     deleteJob->appendArticleIds(toDelete);
0853     setNotificationMode(true);
0854 }
0855 
0856 QString Akregator::Feed::copyright() const
0857 {
0858     return d->m_copyright;
0859 }
0860 
0861 void Akregator::Feed::setCopyright(const QString &copyright)
0862 {
0863     d->m_copyright = copyright;
0864 }
0865 
0866 void Akregator::Feed::setFavicon(const QIcon &icon)
0867 {
0868     d->m_favicon = icon;
0869     nodeModified();
0870 }
0871 
0872 void Akregator::Feed::setLogoInfo(const ImageInfo &image)
0873 {
0874     if (d->m_logoInfo != image) {
0875         d->m_logoInfo = image;
0876         nodeModified();
0877     }
0878 }
0879 
0880 Akregator::Feed::ArchiveMode Akregator::Feed::archiveMode() const
0881 {
0882     return d->m_archiveMode;
0883 }
0884 
0885 void Akregator::Feed::setArchiveMode(ArchiveMode archiveMode)
0886 {
0887     d->m_archiveMode = archiveMode;
0888 }
0889 
0890 int Akregator::Feed::unread() const
0891 {
0892     return d->m_archive ? d->m_archive->unread() : 0;
0893 }
0894 
0895 void Akregator::Feed::setUnread(int unread)
0896 {
0897     if (d->m_archive && unread != d->m_archive->unread()) {
0898         d->m_archive->setUnread(unread);
0899         nodeModified();
0900     }
0901 }
0902 
0903 void Akregator::Feed::setArticleDeleted(Article &a)
0904 {
0905     d->setTotalCountDirty();
0906     if (!d->m_deletedArticles.contains(a)) {
0907         d->m_deletedArticles.append(a);
0908     }
0909 
0910     d->m_updatedArticlesNotify.append(a);
0911     articlesModified();
0912 }
0913 
0914 void Akregator::Feed::setArticleChanged(Article &a, int oldStatus, bool process)
0915 {
0916     int newStatus = a.status();
0917     if (oldStatus != -1) {
0918         if (oldStatus == Read && newStatus != Read) {
0919             setUnread(unread() + 1);
0920         } else if (oldStatus != Read && newStatus == Read) {
0921             setUnread(unread() - 1);
0922         }
0923     }
0924     d->m_updatedArticlesNotify.append(a);
0925     if (process) {
0926         articlesModified();
0927     }
0928 }
0929 
0930 int Akregator::Feed::totalCount() const
0931 {
0932     if (d->m_totalCount == -1) {
0933         d->m_totalCount = std::count_if(d->articles.constBegin(), d->articles.constEnd(), [](const Article &art) -> bool {
0934             return !art.isDeleted();
0935         });
0936     }
0937     return d->m_totalCount;
0938 }
0939 
0940 TreeNode *Akregator::Feed::next()
0941 {
0942     if (nextSibling()) {
0943         return nextSibling();
0944     }
0945 
0946     Folder *p = parent();
0947     while (p) {
0948         if (p->nextSibling()) {
0949             return p->nextSibling();
0950         } else {
0951             p = p->parent();
0952         }
0953     }
0954     return nullptr;
0955 }
0956 
0957 const TreeNode *Akregator::Feed::next() const
0958 {
0959     if (nextSibling()) {
0960         return nextSibling();
0961     }
0962 
0963     const Folder *p = parent();
0964     while (p) {
0965         if (p->nextSibling()) {
0966             return p->nextSibling();
0967         } else {
0968             p = p->parent();
0969         }
0970     }
0971     return nullptr;
0972 }
0973 
0974 void Akregator::Feed::doArticleNotification()
0975 {
0976     if (!d->m_addedArticlesNotify.isEmpty()) {
0977         // copy list, otherwise the refcounting in Article::Private breaks for
0978         // some reason (causing segfaults)
0979         const QList<Article> l = d->m_addedArticlesNotify;
0980         Q_EMIT signalArticlesAdded(this, l);
0981         d->m_addedArticlesNotify.clear();
0982     }
0983     if (!d->m_updatedArticlesNotify.isEmpty()) {
0984         // copy list, otherwise the refcounting in Article::Private breaks for
0985         // some reason (causing segfaults)
0986         const QList<Article> l = d->m_updatedArticlesNotify;
0987         Q_EMIT signalArticlesUpdated(this, l);
0988         d->m_updatedArticlesNotify.clear();
0989     }
0990     if (!d->m_removedArticlesNotify.isEmpty()) {
0991         // copy list, otherwise the refcounting in Article::Private breaks for
0992         // some reason (causing segfaults)
0993         const QList<Article> l = d->m_removedArticlesNotify;
0994         Q_EMIT signalArticlesRemoved(this, l);
0995         d->m_removedArticlesNotify.clear();
0996     }
0997     TreeNode::doArticleNotification();
0998 }
0999 
1000 void Akregator::Feed::enforceLimitArticleNumber()
1001 {
1002     int limit = -1;
1003     if (d->m_archiveMode == globalDefault && Settings::archiveMode() == Settings::EnumArchiveMode::limitArticleNumber) {
1004         limit = Settings::maxArticleNumber();
1005     } else if (d->m_archiveMode == limitArticleNumber) {
1006         limit = maxArticleNumber();
1007     }
1008 
1009     if (limit == -1 || limit >= d->articles.count() - d->m_deletedArticles.count()) {
1010         return;
1011     }
1012 
1013     QList<Article> articles = valuesToVector(d->articles);
1014     std::sort(articles.begin(), articles.end());
1015 
1016     int c = 0;
1017     const bool useKeep = Settings::doNotExpireImportantArticles();
1018 
1019     for (Article i : std::as_const(articles)) {
1020         if (c < limit) {
1021             if (!i.isDeleted() && (!useKeep || !i.keep())) {
1022                 ++c;
1023             }
1024         } else if (!useKeep || !i.keep()) {
1025             i.setDeleted();
1026         }
1027     }
1028 }
1029 
1030 bool Akregator::Feed::ImageInfo::operator==(const Akregator::Feed::ImageInfo &other) const
1031 {
1032     return other.width == width && other.height == height && other.imageUrl == imageUrl;
1033 }
1034 
1035 bool Akregator::Feed::ImageInfo::operator!=(const Akregator::Feed::ImageInfo &other) const
1036 {
1037     return !ImageInfo::operator==(other);
1038 }
1039 
1040 #include "moc_feed.cpp"