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 ©right) 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"