diff --git a/src/article.cpp b/src/article.cpp index 7eef0d07..5919fb63 100644 --- a/src/article.cpp +++ b/src/article.cpp @@ -1,561 +1,561 @@ /* This file is part of Akregator. Copyright (C) 2004 Stanislav Karchebny 2005 Frank Osterfeld This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. As a special exception, permission is given to link this program with any edition of Qt, and distribute the resulting executable, without including the source code for Qt in the source distribution. */ #include "article.h" #include "feed.h" #include "feedstorage.h" #include "shared.h" #include "storage.h" #include "utils.h" #include #include #include #include #include #include "akregator_debug.h" #include #include using namespace Syndication; namespace { QString buildTitle(const QString &description) { QString s = description; if (description.trimmed().isEmpty()) { return QString(); } int i = s.indexOf(QLatin1Char('>'), 500); /*avoid processing too much */ if (i != -1) { s = s.left(i + 1); } QRegExp rx(QStringLiteral("(<([^\\s>]*)(?:[^>]*)>)[^<]*"), Qt::CaseInsensitive); QString tagName, toReplace, replaceWith; while (rx.indexIn(s) != -1) { tagName = rx.cap(2); if (tagName == QLatin1String("SCRIPT") || tagName == QLatin1String("script")) { toReplace = rx.cap(0); // strip tag AND tag contents } else if (tagName.startsWith(QStringLiteral("br")) || tagName.startsWith(QStringLiteral("BR"))) { toReplace = rx.cap(1); replaceWith = QLatin1Char(' '); } else { toReplace = rx.cap(1); // strip just tag } s = s.replace(s.indexOf(toReplace), toReplace.length(), replaceWith); // do the deed } if (s.length() > 90) { s = s.left(90) + QLatin1String("..."); } return s.simplified(); } } namespace Akregator { struct Article::Private : public Shared { Private(); Private(const QString &guid, Feed *feed, Backend::FeedStorage *archive); Private(const ItemPtr &article, Feed *feed, Backend::FeedStorage *archive); /** The status of the article is stored in an int, the bits having the following meaning: 0000 0001 Deleted 0000 0010 Trash 0000 0100 New 0000 1000 Read 0001 0000 Keep */ enum Status { Deleted = 0x01, Trash = 0x02, New = 0x04, Read = 0x08, Keep = 0x10 }; Feed *feed = nullptr; QString guid; Backend::FeedStorage *archive = nullptr; int status; uint hash; QDateTime pubDate; mutable QSharedPointer enclosure; }; namespace { class EnclosureImpl : public Enclosure { public: EnclosureImpl(const QString &url, const QString &type, uint length) : m_url(url) , m_type(type) , m_length(length) { } QString url() const override { return m_url; } QString type() const override { return m_type; } QString title() const override { return m_title; } uint length() const override { return m_length; } uint duration() const override { return 0; } bool isNull() const override { return m_url.isNull(); } private: QString m_url; QString m_type; QString m_title; uint m_length; }; } Article::Private::Private() : feed(0) , archive(0) , status(0) , hash(0) , pubDate(QDateTime::fromTime_t(1)) { } Article::Private::Private(const QString &guid_, Feed *feed_, Backend::FeedStorage *archive_) : feed(feed_) , guid(guid_) , archive(archive_) , status(archive->status(guid)) , hash(archive->hash(guid)) , pubDate(QDateTime::fromTime_t(archive->pubDate(guid))) { } Article::Private::Private(const ItemPtr &article, Feed *feed_, Backend::FeedStorage *archive_) : feed(feed_) , archive(archive_) , status(New) , hash(0) { Q_ASSERT(archive); const QList authorList = article->authors(); QString author; const PersonPtr firstAuthor = !authorList.isEmpty() ? authorList.first() : PersonPtr(); hash = Utils::calcHash(article->title() + article->description() + article->content() + article->link() + author); guid = article->id(); if (!archive->contains(guid)) { archive->addEntry(guid); archive->setHash(guid, hash); QString title = article->title(); if (title.isEmpty()) { title = buildTitle(article->description()); } archive->setTitle(guid, title); archive->setContent(guid, article->content()); archive->setDescription(guid, article->description()); archive->setLink(guid, article->link()); //archive->setComments(guid, article.comments()); //archive->setCommentsLink(guid, article.commentsLink().url()); archive->setGuidIsPermaLink(guid, false); archive->setGuidIsHash(guid, guid.startsWith(QStringLiteral("hash:"))); const time_t datePublished = article->datePublished(); if (datePublished > 0) { pubDate.setTime_t(datePublished); } else { pubDate = QDateTime::currentDateTime(); } archive->setPubDate(guid, pubDate.toTime_t()); if (firstAuthor) { archive->setAuthorName(guid, firstAuthor->name()); archive->setAuthorUri(guid, firstAuthor->uri()); archive->setAuthorEMail(guid, firstAuthor->email()); } const QList encs = article->enclosures(); if (!encs.isEmpty()) { archive->setEnclosure(guid, encs[0]->url(), encs[0]->type(), encs[0]->length()); } } else { // always update comments count, as it's not used for hash calculation //archive->setComments(guid, article.comments()); if (hash != archive->hash(guid)) { //article is in archive, was it modified? // if yes, update pubDate.setTime_t(archive->pubDate(guid)); archive->setHash(guid, hash); QString title = article->title(); if (title.isEmpty()) { title = buildTitle(article->description()); } archive->setTitle(guid, title); archive->setDescription(guid, article->description()); archive->setContent(guid, article->content()); archive->setLink(guid, article->link()); if (firstAuthor) { archive->setAuthorName(guid, firstAuthor->name()); archive->setAuthorUri(guid, firstAuthor->uri()); archive->setAuthorEMail(guid, firstAuthor->email()); } //archive->setCommentsLink(guid, article.commentsLink()); } } const QList encs = article->enclosures(); if (!encs.isEmpty()) { archive->setEnclosure(guid, encs[0]->url(), encs[0]->type(), encs[0]->length()); } } Article::Article() : d(new Private) { } Article::Article(const QString &guid, Feed *feed) : d(new Private(guid, feed, feed->storage()->archiveFor(feed->xmlUrl()))) { } Article::Article(const ItemPtr &article, Feed *feed) : d(new Private(article, feed, feed->storage()->archiveFor(feed->xmlUrl()))) { } Article::Article(const ItemPtr &article, Backend::FeedStorage *archive) : d(new Private(article, 0, archive)) { } bool Article::isNull() const { return d->archive == 0; // TODO: use proper null state } void Article::offsetPubDate(int secs) { d->pubDate = d->pubDate.addSecs(secs); d->archive->setPubDate(d->guid, d->pubDate.toTime_t()); } void Article::setDeleted() { if (isDeleted()) { return; } setStatus(Read); d->status = Private::Deleted | Private::Read; d->archive->setStatus(d->guid, d->status); d->archive->setDeleted(d->guid); if (d->feed) { d->feed->setArticleDeleted(*this); } } bool Article::isDeleted() const { return (d->status & Private::Deleted) != 0; } Article::Article(const Article &other) : d(other.d) { d->ref(); } Article::~Article() { if (d->deref()) { delete d; d = 0; } } Article &Article::operator=(const Article &other) { Article copy(other); swap(copy); return *this; } bool Article::operator<(const Article &other) const { return pubDate() > other.pubDate() || (pubDate() == other.pubDate() && guid() < other.guid()); } bool Article::operator<=(const Article &other) const { return pubDate() > other.pubDate() || *this == other; } bool Article::operator>(const Article &other) const { return pubDate() < other.pubDate() || (pubDate() == other.pubDate() && guid() > other.guid()); } bool Article::operator>=(const Article &other) const { return pubDate() > other.pubDate() || *this == other; } bool Article::operator==(const Article &other) const { return d->guid == other.guid(); } bool Article::operator!=(const Article &other) const { return d->guid != other.guid(); } int Article::status() const { if ((d->status & Private::Read) != 0) { return Read; } if ((d->status & Private::New) != 0) { return New; } return Unread; } void Article::setStatus(int stat) { int oldStatus = status(); if (oldStatus != stat) { switch (stat) { case Read: d->status = (d->status | Private::Read) & ~Private::New; break; case Unread: d->status = (d->status & ~Private::Read) & ~Private::New; break; case New: d->status = (d->status | Private::New) & ~Private::Read; break; } if (d->archive) { d->archive->setStatus(d->guid, d->status); } if (d->feed) { - d->feed->setArticleChanged(*this, oldStatus, stat != Read); + d->feed->setArticleChanged(*this, oldStatus); } } } QString Article::title() const { QString str; if (d->archive) { str = d->archive->title(d->guid); } return str; } QString Article::authorName() const { QString str; if (d->archive) { str = d->archive->authorName(d->guid); } return str; } QString Article::authorEMail() const { QString str; if (d->archive) { str = d->archive->authorEMail(d->guid); } return str; } QString Article::authorUri() const { QString str; if (d->archive) { str = d->archive->authorUri(d->guid); } return str; } QString Article::authorShort() const { const QString name = authorName(); if (!name.isEmpty()) { return name; } const QString email = authorEMail(); if (!email.isEmpty()) { return email; } const QString uri = authorUri(); if (!uri.isEmpty()) { return uri; } return QString(); } QString Article::authorAsHtml() const { const QString name = authorName(); const QString email = authorEMail(); if (!email.isEmpty()) { if (!name.isEmpty()) { return QStringLiteral("%2").arg(email, name); } else { return QStringLiteral("%1").arg(email); } } const QString uri = authorUri(); if (!name.isEmpty()) { if (!uri.isEmpty()) { return QStringLiteral("%2").arg(uri, name); } else { return name; } } if (!uri.isEmpty()) { return QStringLiteral("%1").arg(uri); } return QString(); } QUrl Article::link() const { return QUrl(d->archive->link(d->guid)); } QString Article::description() const { return d->archive->description(d->guid); } QString Article::content(ContentOption opt) const { const QString cnt = d->archive->content(d->guid); return opt == ContentAndOnlyContent ? cnt : (!cnt.isEmpty() ? cnt : description()); } QString Article::guid() const { return d->guid; } QUrl Article::commentsLink() const { return QUrl(d->archive->commentsLink(d->guid)); } int Article::comments() const { return d->archive->comments(d->guid); } bool Article::guidIsPermaLink() const { return d->archive->guidIsPermaLink(d->guid); } bool Article::guidIsHash() const { return d->archive->guidIsHash(d->guid); } uint Article::hash() const { return d->hash; } bool Article::keep() const { return (d->status & Private::Keep) != 0; } void Article::setKeep(bool keep) { d->status = keep ? (d->status | Private::Keep) : (d->status & ~Private::Keep); d->archive->setStatus(d->guid, d->status); if (d->feed) { d->feed->setArticleChanged(*this); } } Feed *Article::feed() const { return d->feed; } QDateTime Article::pubDate() const { return d->pubDate; } QSharedPointer Article::enclosure() const { if (!d->enclosure) { QString url; QString type; int length; bool hasEnc; d->archive->enclosure(d->guid, hasEnc, url, type, length); if (hasEnc) { d->enclosure.reset(new EnclosureImpl(url, type, static_cast(length))); } else { d->enclosure.reset(new EnclosureImpl(QString(), QString(), 0)); } } return d->enclosure; } } // namespace Akregator diff --git a/src/feed/feed.cpp b/src/feed/feed.cpp index 310a66dd..90557b63 100644 --- a/src/feed/feed.cpp +++ b/src/feed/feed.cpp @@ -1,945 +1,943 @@ /* This file is part of Akregator. Copyright (C) 2004 Stanislav Karchebny 2005 Frank Osterfeld This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. As a special exception, permission is given to link this program with any edition of Qt, and distribute the resulting executable, without including the source code for Qt in the source distribution. */ #include "feed.h" #include "akregatorconfig.h" #include "article.h" #include "articlejobs.h" #include "feedstorage.h" #include "fetchqueue.h" #include "folder.h" #include "notificationmanager.h" #include "storage.h" #include "treenodevisitor.h" #include "types.h" #include "utils.h" #include #include "akregator_debug.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include using Syndication::ItemPtr; using namespace Akregator; template class Container> QVector valuesToVector(const Container &container) { QVector values; values.reserve(container.size()); for (const Value &value : container) { values << value; } return values; } class Q_DECL_HIDDEN Akregator::Feed::Private { Akregator::Feed *const q; public: explicit Private(Backend::Storage *storage, Akregator::Feed *qq); Backend::Storage *storage = nullptr; bool autoFetch = false; int fetchInterval; ArchiveMode archiveMode; int maxArticleAge; int maxArticleNumber; bool markImmediatelyAsRead = false; bool useNotification = false; bool loadLinkedWebsite = false; int lastFetched; Syndication::ErrorCode fetchErrorCode; int fetchTries; bool followDiscovery = false; Syndication::Loader *loader = nullptr; bool articlesLoaded = false; Backend::FeedStorage *archive = nullptr; QString xmlUrl; QString htmlUrl; QString description; /** list of feed articles */ QHash articles; /** list of deleted articles. This contains **/ QVector
deletedArticles; /** caches guids of deleted articles for notification */ QVector
addedArticlesNotify; QVector
removedArticlesNotify; QVector
updatedArticlesNotify; QPixmap imagePixmap; Syndication::ImagePtr image; QIcon favicon; mutable int totalCount; void setTotalCountDirty() const { totalCount = -1; } }; QString Akregator::Feed::archiveModeToString(ArchiveMode mode) { switch (mode) { case keepAllArticles: return QStringLiteral("keepAllArticles"); case disableArchiving: return QStringLiteral("disableArchiving"); case limitArticleNumber: return QStringLiteral("limitArticleNumber"); case limitArticleAge: return QStringLiteral("limitArticleAge"); default: break; } return QStringLiteral("globalDefault"); } Akregator::Feed *Akregator::Feed::fromOPML(QDomElement e, Backend::Storage *storage) { if (!e.hasAttribute(QStringLiteral("xmlUrl")) && !e.hasAttribute(QStringLiteral("xmlurl")) && !e.hasAttribute(QStringLiteral("xmlURL"))) { return nullptr; } QString title = e.hasAttribute(QStringLiteral("text")) ? e.attribute(QStringLiteral("text")) : e.attribute(QStringLiteral("title")); QString xmlUrl = e.hasAttribute(QStringLiteral("xmlUrl")) ? e.attribute(QStringLiteral("xmlUrl")) : e.attribute(QStringLiteral("xmlurl")); if (xmlUrl.isEmpty()) { xmlUrl = e.attribute(QStringLiteral("xmlURL")); } bool useCustomFetchInterval = e.attribute(QStringLiteral("useCustomFetchInterval")) == QLatin1String("true"); QString htmlUrl = e.attribute(QStringLiteral("htmlUrl")); QString description = e.attribute(QStringLiteral("description")); int fetchInterval = e.attribute(QStringLiteral("fetchInterval")).toInt(); ArchiveMode archiveMode = stringToArchiveMode(e.attribute(QStringLiteral("archiveMode"))); int maxArticleAge = e.attribute(QStringLiteral("maxArticleAge")).toUInt(); int maxArticleNumber = e.attribute(QStringLiteral("maxArticleNumber")).toUInt(); bool markImmediatelyAsRead = e.attribute(QStringLiteral("markImmediatelyAsRead")) == QLatin1String("true"); bool useNotification = e.attribute(QStringLiteral("useNotification")) == QLatin1String("true"); bool loadLinkedWebsite = e.attribute(QStringLiteral("loadLinkedWebsite")) == QLatin1String("true"); uint id = e.attribute(QStringLiteral("id")).toUInt(); Feed *const feed = new Feed(storage); feed->setTitle(title); feed->setXmlUrl(xmlUrl); feed->setCustomFetchIntervalEnabled(useCustomFetchInterval); feed->setHtmlUrl(htmlUrl); feed->setId(id); feed->setDescription(description); feed->setArchiveMode(archiveMode); feed->setUseNotification(useNotification); feed->setFetchInterval(fetchInterval); feed->setMaxArticleAge(maxArticleAge); feed->setMaxArticleNumber(maxArticleNumber); feed->setMarkImmediatelyAsRead(markImmediatelyAsRead); feed->setLoadLinkedWebsite(loadLinkedWebsite); feed->loadArticles(); // TODO: make me fly: make this delayed return feed; } bool Akregator::Feed::accept(TreeNodeVisitor *visitor) { if (visitor->visitFeed(this)) { return true; } else { return visitor->visitTreeNode(this); } } QVector Akregator::Feed::folders() const { return QVector(); } QVector Akregator::Feed::folders() { return QVector(); } QVector Akregator::Feed::feeds() const { QVector list; list.append(this); return list; } QVector Akregator::Feed::feeds() { QVector list; list.append(this); return list; } Article Akregator::Feed::findArticle(const QString &guid) const { return d->articles.value(guid); } QVector
Akregator::Feed::articles() { if (!d->articlesLoaded) { loadArticles(); } return valuesToVector(d->articles); } Backend::Storage *Akregator::Feed::storage() { return d->storage; } void Akregator::Feed::loadArticles() { if (d->articlesLoaded) { return; } if (!d->archive && d->storage) { d->archive = d->storage->archiveFor(xmlUrl()); } QStringList list = d->archive->articles(); for (QStringList::ConstIterator it = list.constBegin(); it != list.constEnd(); ++it) { Article mya(*it, this); d->articles[mya.guid()] = mya; if (mya.isDeleted()) { d->deletedArticles.append(mya); } } d->articlesLoaded = true; enforceLimitArticleNumber(); recalcUnreadCount(); } void Akregator::Feed::recalcUnreadCount() { QVector
tarticles = articles(); QVector
::ConstIterator it; QVector
::ConstIterator en = tarticles.constEnd(); int oldUnread = d->archive->unread(); int unread = 0; for (it = tarticles.constBegin(); it != en; ++it) { if (!(*it).isDeleted() && (*it).status() != Read) { ++unread; } } if (unread != oldUnread) { d->archive->setUnread(unread); nodeModified(); } } Akregator::Feed::ArchiveMode Akregator::Feed::stringToArchiveMode(const QString &str) { if (str == QLatin1String("globalDefault")) { return globalDefault; } else if (str == QLatin1String("keepAllArticles")) { return keepAllArticles; } else if (str == QLatin1String("disableArchiving")) { return disableArchiving; } else if (str == QLatin1String("limitArticleNumber")) { return limitArticleNumber; } else if (str == QLatin1String("limitArticleAge")) { return limitArticleAge; } return globalDefault; } Akregator::Feed::Private::Private(Backend::Storage *storage_, Akregator::Feed *qq) : q(qq) , storage(storage_) , autoFetch(false) , fetchInterval(30) , archiveMode(globalDefault) , maxArticleAge(60) , maxArticleNumber(1000) , markImmediatelyAsRead(false) , useNotification(false) , loadLinkedWebsite(false) , lastFetched(0) , fetchErrorCode(Syndication::Success) , fetchTries(0) , followDiscovery(false) , loader(0) , articlesLoaded(false) , archive(0) , totalCount(-1) { Q_ASSERT(q); Q_ASSERT(storage); } Akregator::Feed::Feed(Backend::Storage *storage) : TreeNode() , d(new Private(storage, this)) { } Akregator::Feed::~Feed() { slotAbortFetch(); emitSignalDestroyed(); delete d; d = 0; } void Akregator::Feed::loadFavicon(const QUrl &url) { KIO::FavIconRequestJob *job = new KIO::FavIconRequestJob(url); connect(job, &KIO::FavIconRequestJob::result, this, [job, this](KJob *) { if (!job->error()) { setFavicon(QIcon(job->iconFile())); } }); } bool Akregator::Feed::useCustomFetchInterval() const { return d->autoFetch; } void Akregator::Feed::setCustomFetchIntervalEnabled(bool enabled) { d->autoFetch = enabled; } int Akregator::Feed::fetchInterval() const { return d->fetchInterval; } void Akregator::Feed::setFetchInterval(int interval) { d->fetchInterval = interval; } int Akregator::Feed::maxArticleAge() const { return d->maxArticleAge; } void Akregator::Feed::setMaxArticleAge(int maxArticleAge) { d->maxArticleAge = maxArticleAge; } int Akregator::Feed::maxArticleNumber() const { return d->maxArticleNumber; } void Akregator::Feed::setMaxArticleNumber(int maxArticleNumber) { d->maxArticleNumber = maxArticleNumber; } bool Akregator::Feed::markImmediatelyAsRead() const { return d->markImmediatelyAsRead; } bool Akregator::Feed::isFetching() const { return d->loader != 0; } void Akregator::Feed::setMarkImmediatelyAsRead(bool enabled) { d->markImmediatelyAsRead = enabled; if (enabled) { createMarkAsReadJob()->start(); } } void Akregator::Feed::setUseNotification(bool enabled) { d->useNotification = enabled; } bool Akregator::Feed::useNotification() const { return d->useNotification; } void Akregator::Feed::setLoadLinkedWebsite(bool enabled) { d->loadLinkedWebsite = enabled; } bool Akregator::Feed::loadLinkedWebsite() const { return d->loadLinkedWebsite; } QPixmap Akregator::Feed::image() const { return d->imagePixmap; } QString Akregator::Feed::xmlUrl() const { return d->xmlUrl; } void Akregator::Feed::setXmlUrl(const QString &s) { d->xmlUrl = s; if (!Settings::fetchOnStartup()) { QTimer::singleShot(KRandom::random() % 4000, this, &Feed::slotAddFeedIconListener); // TODO: let's give a gui some time to show up before starting the fetch when no fetch on startup is used. replace this with something proper later... } } QString Akregator::Feed::htmlUrl() const { return d->htmlUrl; } void Akregator::Feed::setHtmlUrl(const QString &s) { d->htmlUrl = s; } QString Akregator::Feed::description() const { return d->description; } void Akregator::Feed::setDescription(const QString &s) { d->description = s; } bool Akregator::Feed::fetchErrorOccurred() const { return d->fetchErrorCode != Syndication::Success; } Syndication::ErrorCode Akregator::Feed::fetchErrorCode() const { return d->fetchErrorCode; } bool Akregator::Feed::isArticlesLoaded() const { return d->articlesLoaded; } QDomElement Akregator::Feed::toOPML(QDomElement parent, QDomDocument document) const { QDomElement el = document.createElement(QStringLiteral("outline")); el.setAttribute(QStringLiteral("text"), title()); el.setAttribute(QStringLiteral("title"), title()); el.setAttribute(QStringLiteral("xmlUrl"), d->xmlUrl); el.setAttribute(QStringLiteral("htmlUrl"), d->htmlUrl); el.setAttribute(QStringLiteral("id"), QString::number(id())); el.setAttribute(QStringLiteral("description"), d->description); el.setAttribute(QStringLiteral("useCustomFetchInterval"), (useCustomFetchInterval() ? QStringLiteral("true") : QStringLiteral("false"))); el.setAttribute(QStringLiteral("fetchInterval"), QString::number(fetchInterval())); el.setAttribute(QStringLiteral("archiveMode"), archiveModeToString(d->archiveMode)); el.setAttribute(QStringLiteral("maxArticleAge"), d->maxArticleAge); el.setAttribute(QStringLiteral("maxArticleNumber"), d->maxArticleNumber); if (d->markImmediatelyAsRead) { el.setAttribute(QStringLiteral("markImmediatelyAsRead"), QStringLiteral("true")); } if (d->useNotification) { el.setAttribute(QStringLiteral("useNotification"), QStringLiteral("true")); } if (d->loadLinkedWebsite) { el.setAttribute(QStringLiteral("loadLinkedWebsite"), QStringLiteral("true")); } el.setAttribute(QStringLiteral("maxArticleNumber"), d->maxArticleNumber); el.setAttribute(QStringLiteral("type"), QStringLiteral("rss")); // despite some additional fields, it is still "rss" OPML el.setAttribute(QStringLiteral("version"), QStringLiteral("RSS")); parent.appendChild(el); return el; } KJob *Akregator::Feed::createMarkAsReadJob() { ArticleModifyJob *job = new ArticleModifyJob; Q_FOREACH (const Article &i, articles()) { const ArticleId aid = { xmlUrl(), i.guid() }; job->setStatus(aid, Read); } return job; } void Akregator::Feed::slotAddToFetchQueue(FetchQueue *queue, bool intervalFetchOnly) { if (!intervalFetchOnly) { queue->addFeed(this); } else { int interval = -1; if (useCustomFetchInterval()) { interval = fetchInterval() * 60; } else if (Settings::useIntervalFetch()) { interval = Settings::autoFetchInterval() * 60; } uint lastFetch = d->archive->lastFetch(); uint now = QDateTime::currentDateTimeUtc().toTime_t(); if (interval > 0 && now - lastFetch >= (uint)interval) { queue->addFeed(this); } } } void Akregator::Feed::slotAddFeedIconListener() { loadFavicon(QUrl(d->xmlUrl)); } void Akregator::Feed::appendArticles(const Syndication::FeedPtr &feed) { d->setTotalCountDirty(); bool changed = false; const bool notify = useNotification() || Settings::useNotifications(); QList items = feed->items(); QList::ConstIterator it = items.constBegin(); QList::ConstIterator en = items.constEnd(); int nudge = 0; QVector
deletedArticles = d->deletedArticles; for (; it != en; ++it) { if (!d->articles.contains((*it)->id())) { // article not in list Article mya(*it, this); mya.offsetPubDate(nudge); nudge--; appendArticle(mya); d->addedArticlesNotify.append(mya); if (!mya.isDeleted() && !markImmediatelyAsRead()) { mya.setStatus(New); } else { mya.setStatus(Read); } if (notify) { NotificationManager::self()->slotNotifyArticle(mya); } changed = true; } else { // article is in list // 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. Article old = d->articles[(*it)->id()]; Article mya(*it, this); if (!mya.guidIsHash() && mya.hash() != old.hash() && !old.isDeleted()) { mya.setKeep(old.keep()); int oldstatus = old.status(); old.setStatus(Read); d->articles.remove(old.guid()); appendArticle(mya); mya.setStatus(oldstatus); d->updatedArticlesNotify.append(mya); changed = true; } else if (old.isDeleted()) { deletedArticles.removeAll(mya); } } } QVector
::ConstIterator dit = deletedArticles.constBegin(); QVector
::ConstIterator dtmp; QVector
::ConstIterator den = deletedArticles.constEnd(); // delete articles with delete flag set completely from archive, which aren't in the current feed source anymore while (dit != den) { dtmp = dit; ++dit; d->articles.remove((*dtmp).guid()); d->archive->deleteArticle((*dtmp).guid()); d->removedArticlesNotify.append(*dtmp); changed = true; d->deletedArticles.removeAll(*dtmp); } if (changed) { articlesModified(); } } bool Akregator::Feed::usesExpiryByAge() const { return (d->archiveMode == globalDefault && Settings::archiveMode() == Settings::EnumArchiveMode::limitArticleAge) || d->archiveMode == limitArticleAge; } bool Akregator::Feed::isExpired(const Article &a) const { QDateTime now = QDateTime::currentDateTime(); int expiryAge = -1; // check whether the feed uses the global default and the default is limitArticleAge if (d->archiveMode == globalDefault && Settings::archiveMode() == Settings::EnumArchiveMode::limitArticleAge) { expiryAge = Settings::maxArticleAge() * 24 * 3600; } else // otherwise check if this feed has limitArticleAge set if (d->archiveMode == limitArticleAge) { expiryAge = d->maxArticleAge * 24 * 3600; } return expiryAge != -1 && a.pubDate().secsTo(now) > expiryAge; } void Akregator::Feed::appendArticle(const Article &a) { if ((a.keep() && Settings::doNotExpireImportantArticles()) || (!usesExpiryByAge() || !isExpired(a))) { // if not expired if (!d->articles.contains(a.guid())) { d->articles[a.guid()] = a; if (!a.isDeleted() && a.status() != Read) { setUnread(unread() + 1); } } } } void Akregator::Feed::fetch(bool followDiscovery) { d->followDiscovery = followDiscovery; d->fetchTries = 0; // mark all new as unread for (auto it = d->articles.begin(), end = d->articles.end(); it != end; ++it) { if ((*it).status() == New) { (*it).setStatus(Unread); } } Q_EMIT fetchStarted(this); tryFetch(); } void Akregator::Feed::slotAbortFetch() { if (d->loader) { d->loader->abort(); } } void Akregator::Feed::tryFetch() { d->fetchErrorCode = Syndication::Success; d->loader = Syndication::Loader::create(this, SLOT(fetchCompleted(Syndication::Loader *, Syndication::FeedPtr, Syndication::ErrorCode))); d->loader->loadFrom(QUrl(d->xmlUrl)); } void Akregator::Feed::slotImageFetched(const QPixmap &image) { setImage(image); } void Akregator::Feed::fetchCompleted(Syndication::Loader *l, Syndication::FeedPtr doc, Syndication::ErrorCode status) { // Note that loader instances delete themselves d->loader = 0; // fetching wasn't successful: if (status != Syndication::Success) { if (status == Syndication::Aborted) { d->fetchErrorCode = Syndication::Success; Q_EMIT fetchAborted(this); } else if (d->followDiscovery && (status == Syndication::InvalidXml) && (d->fetchTries < 3) && (l->discoveredFeedURL().isValid())) { d->fetchTries++; d->xmlUrl = l->discoveredFeedURL().url(); Q_EMIT fetchDiscovery(this); tryFetch(); } else { d->fetchErrorCode = status; Q_EMIT fetchError(this); } markAsFetchedNow(); return; } loadArticles(); // TODO: make me fly: make this delayed loadFavicon(QUrl(xmlUrl())); d->fetchErrorCode = Syndication::Success; if (d->imagePixmap.isNull()) { const QString imageFileName = QStandardPaths::writableLocation(QStandardPaths::CacheLocation) + QLatin1String("/akregator/Media/") + Utils::fileNameForUrl(d->xmlUrl) + QLatin1String(".png"); d->imagePixmap = QPixmap(imageFileName, "PNG"); } if (title().isEmpty()) { setTitle(Syndication::htmlToPlainText(doc->title())); } d->description = doc->description(); d->htmlUrl = doc->link(); appendArticles(doc); markAsFetchedNow(); Q_EMIT fetched(this); } void Akregator::Feed::markAsFetchedNow() { if (d->archive) { d->archive->setLastFetch(QDateTime::currentDateTimeUtc().toTime_t()); } } QIcon Akregator::Feed::icon() const { if (fetchErrorOccurred()) { return QIcon::fromTheme(QStringLiteral("dialog-error")); } return !d->favicon.isNull() ? d->favicon : QIcon::fromTheme(QStringLiteral("text-html")); } void Akregator::Feed::deleteExpiredArticles(ArticleDeleteJob *deleteJob) { if (!usesExpiryByAge()) { return; } setNotificationMode(false); QList toDelete; const QString feedUrl = xmlUrl(); const bool useKeep = Settings::doNotExpireImportantArticles(); for (const Article &i : qAsConst(d->articles)) { if ((!useKeep || !i.keep()) && isExpired(i)) { const ArticleId aid = { feedUrl, i.guid() }; toDelete.append(aid); } } deleteJob->appendArticleIds(toDelete); setNotificationMode(true); } void Akregator::Feed::setFavicon(const QIcon &icon) { d->favicon = icon; nodeModified(); } void Akregator::Feed::setImage(const QPixmap &p) { if (p.isNull()) { return; } d->imagePixmap = p; const QString filename = QStandardPaths::writableLocation(QStandardPaths::CacheLocation) + QLatin1String("/akregator/Media/") + Utils::fileNameForUrl(d->xmlUrl) + QLatin1String(".png"); QFileInfo fileInfo(filename); QDir().mkpath(fileInfo.absolutePath()); d->imagePixmap.save(filename, "PNG"); nodeModified(); } Akregator::Feed::ArchiveMode Akregator::Feed::archiveMode() const { return d->archiveMode; } void Akregator::Feed::setArchiveMode(ArchiveMode archiveMode) { d->archiveMode = archiveMode; } int Akregator::Feed::unread() const { return d->archive ? d->archive->unread() : 0; } void Akregator::Feed::setUnread(int unread) { if (d->archive && unread != d->archive->unread()) { d->archive->setUnread(unread); nodeModified(); } } void Akregator::Feed::setArticleDeleted(Article &a) { d->setTotalCountDirty(); if (!d->deletedArticles.contains(a)) { d->deletedArticles.append(a); } d->updatedArticlesNotify.append(a); articlesModified(); } -void Akregator::Feed::setArticleChanged(Article &a, int oldStatus, bool process) +void Akregator::Feed::setArticleChanged(Article &a, int oldStatus) { - int newStatus = a.status(); if (oldStatus != -1) { + int newStatus = a.status(); if (oldStatus == Read && newStatus != Read) { setUnread(unread() + 1); } else if (oldStatus != Read && newStatus == Read) { setUnread(unread() - 1); } } d->updatedArticlesNotify.append(a); - if (process) { - articlesModified(); - } + articlesModified(); } int Akregator::Feed::totalCount() const { if (d->totalCount == -1) { d->totalCount = std::count_if(d->articles.constBegin(), d->articles.constEnd(), [](const Article &art) -> bool { return !art.isDeleted(); }); } return d->totalCount; } TreeNode *Akregator::Feed::next() { if (nextSibling()) { return nextSibling(); } Folder *p = parent(); while (p) { if (p->nextSibling()) { return p->nextSibling(); } else { p = p->parent(); } } return nullptr; } const TreeNode *Akregator::Feed::next() const { if (nextSibling()) { return nextSibling(); } const Folder *p = parent(); while (p) { if (p->nextSibling()) { return p->nextSibling(); } else { p = p->parent(); } } return nullptr; } void Akregator::Feed::doArticleNotification() { if (!d->addedArticlesNotify.isEmpty()) { // copy list, otherwise the refcounting in Article::Private breaks for // some reason (causing segfaults) QVector
l = d->addedArticlesNotify; Q_EMIT signalArticlesAdded(this, l); d->addedArticlesNotify.clear(); } if (!d->updatedArticlesNotify.isEmpty()) { // copy list, otherwise the refcounting in Article::Private breaks for // some reason (causing segfaults) QVector
l = d->updatedArticlesNotify; Q_EMIT signalArticlesUpdated(this, l); d->updatedArticlesNotify.clear(); } if (!d->removedArticlesNotify.isEmpty()) { // copy list, otherwise the refcounting in Article::Private breaks for // some reason (causing segfaults) QVector
l = d->removedArticlesNotify; Q_EMIT signalArticlesRemoved(this, l); d->removedArticlesNotify.clear(); } TreeNode::doArticleNotification(); } void Akregator::Feed::enforceLimitArticleNumber() { int limit = -1; if (d->archiveMode == globalDefault && Settings::archiveMode() == Settings::EnumArchiveMode::limitArticleNumber) { limit = Settings::maxArticleNumber(); } else if (d->archiveMode == limitArticleNumber) { limit = maxArticleNumber(); } if (limit == -1 || limit >= d->articles.count() - d->deletedArticles.count()) { return; } QVector
articles = valuesToVector(d->articles); std::sort(articles.begin(), articles.end()); int c = 0; const bool useKeep = Settings::doNotExpireImportantArticles(); for (Article i : qAsConst(articles)) { if (c < limit) { if (!i.isDeleted() && (!useKeep || !i.keep())) { ++c; } } else if (!useKeep || !i.keep()) { i.setDeleted(); } } } diff --git a/src/feed/feed.h b/src/feed/feed.h index 9c957298..540b3aae 100644 --- a/src/feed/feed.h +++ b/src/feed/feed.h @@ -1,304 +1,301 @@ /* This file is part of Akregator. Copyright (C) 2004 Stanislav Karchebny 2005 Frank Osterfeld This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. As a special exception, permission is given to link this program with any edition of Qt, and distribute the resulting executable, without including the source code for Qt in the source distribution. */ #ifndef AKREGATOR_FEED_H #define AKREGATOR_FEED_H #include "akregator_export.h" #include "treenode.h" #include #include class QDomElement; class QString; namespace Akregator { class Article; class FetchQueue; class TreeNodeVisitor; class ArticleDeleteJob; namespace Backend { class Storage; } /** represents a feed */ class AKREGATOR_EXPORT Feed : public TreeNode { friend class ::Akregator::Article; friend class ::Akregator::Folder; Q_OBJECT public: /** the archiving modes */ enum ArchiveMode { globalDefault, /**< use default from Settings (default) */ keepAllArticles, /**< Don't delete any articles */ disableArchiving, /**< Don't save any articles except articles with keep flag set (equal to maxArticleNumber() == 0) */ limitArticleNumber, /**< Save maxArticleNumber() articles, plus the ones with keep flag set */ limitArticleAge /**< Save articles not older than maxArticleAge() (or keep flag set) */ }; // class methods /** converts strings to ArchiveMode value if parsing fails, it returns ArchiveMode::globalDefault */ static ArchiveMode stringToArchiveMode(const QString &str); /** converts ArchiveMode values to corresponding strings */ static QString archiveModeToString(ArchiveMode mode); /** creates a Feed object from a description in OPML format */ static Feed *fromOPML(QDomElement e, Akregator::Backend::Storage *storage); /** default constructor */ explicit Feed(Akregator::Backend::Storage *storage); ~Feed(); bool accept(TreeNodeVisitor *visitor) override; /** exports the feed settings to OPML */ QDomElement toOPML(QDomElement parent, QDomDocument document) const override; /** returns whether this feed uses its own fetch interval or the global setting @return @c true iff this feed has a custom fetch interval */ bool useCustomFetchInterval() const; /** set if the feed has its custom fetch interval or uses the global setting @param enabled @c true: use custom interval, @c false: use global default */ void setCustomFetchIntervalEnabled(bool enabled); // FIXME is it -1 or 0 to disable interval fetching? /** Returns custom auto fetch interval of this feed. @return custom fetch interval in minutes, 0 if disabled */ int fetchInterval() const; /** Sets custom auto fetch interval. @param interval interval in minutes, -1 for disabling auto fetching */ void setFetchInterval(int interval); /** returns the archiving mode which is used for this feed */ ArchiveMode archiveMode() const; /** sets the archiving mode for this feed */ void setArchiveMode(ArchiveMode archiveMode); /** returns the maximum age of articles used for expiration by age (used in @c limitArticleAge archive mode) @return expiry age in days */ int maxArticleAge() const; /** sets the maximum age of articles used for expiration by age (used in @c limitArticleAge archive mode) @param maxArticleAge expiry age in days */ void setMaxArticleAge(int maxArticleAge); /** returns the article count limit used in @c limitArticleNumber archive mode **/ int maxArticleNumber() const; /** sets the article count limit used in @c limitArticleNumber archive mode **/ void setMaxArticleNumber(int maxArticleNumber); /** if @c true, new articles are marked immediately as read instead of new/unread. Useful for high-traffic feeds. */ bool markImmediatelyAsRead() const; void setMarkImmediatelyAsRead(bool enabled); void setUseNotification(bool enabled); bool useNotification() const; /** if true, the linked URL is loaded directly in the article viewer instead of showing the description */ void setLoadLinkedWebsite(bool enabled); bool loadLinkedWebsite() const; /** returns the feed image */ QPixmap image() const; /** sets the feed image */ void setImage(const QPixmap &p); /** returns the url of the actual feed source (rss/rdf/atom file) */ QString xmlUrl() const; /** sets the url of the actual feed source (rss/rdf/atom file) */ void setXmlUrl(const QString &s); /** returns the URL of the HTML page of this feed */ QString htmlUrl() const; /** sets the URL of the HTML page of this feed */ void setHtmlUrl(const QString &s); /** returns the description of this feed */ QString description() const; /** sets the description of this feed */ void setDescription(const QString &s); /** returns article by guid * @param guid the guid of the article to be returned * @return the article object with the given guid, or a * null article if non-existent */ Article findArticle(const QString &guid) const; /** returns whether a fetch error has occurred */ bool fetchErrorOccurred() const; Syndication::ErrorCode fetchErrorCode() const; /** returns the unread count for this feed */ int unread() const override; /** returns the number of total articles in this feed @return number of articles */ int totalCount() const override; /** returns if the article archive of this feed is loaded */ bool isArticlesLoaded() const; /** returns if this node is a feed group (@c false here) */ bool isGroup() const override { return false; } //impl bool isAggregation() const override { return false; } /** returns the next node in the tree. Calling next() unless it returns 0 iterates through the tree in pre-order */ const TreeNode *next() const override; TreeNode *next() override; //impl QIcon icon() const override; /** deletes expired articles */ void deleteExpiredArticles(Akregator::ArticleDeleteJob *job); bool isFetching() const; QVector feeds() const override; QVector feeds() override; QVector folders() const override; QVector folders() override; KJob *createMarkAsReadJob() override; public Q_SLOTS: /** starts fetching */ void fetch(bool followDiscovery = false); void slotAbortFetch(); /** add this feed to the fetch queue @c queue */ void slotAddToFetchQueue(Akregator::FetchQueue *queue, bool intervalFetchOnly = false) override; void slotAddFeedIconListener(); Q_SIGNALS: /** emitted when fetching started */ void fetchStarted(Akregator::Feed *); /** emitted when feed finished fetching */ void fetched(Akregator::Feed *); /** emitted when a fetch error occurred */ void fetchError(Akregator::Feed *); /** emitted when a feed URL was found by auto discovery */ void fetchDiscovery(Akregator::Feed *); /** emitted when a fetch is aborted */ void fetchAborted(Akregator::Feed *); private: Akregator::Backend::Storage *storage(); private: void setFavicon(const QIcon &icon); void loadFavicon(const QUrl &url); QVector
articles() override; /** loads articles from archive **/ void loadArticles(); void enforceLimitArticleNumber(); void recalcUnreadCount(); void doArticleNotification() override; /** sets the unread count for this feed */ void setUnread(int unread); /** notifies that article @c mya was set to "deleted". To be called by @ref Article */ void setArticleDeleted(Article &a); - /** Notifies that article @p a was changed. - @param oldStatus The old status if the status was changed, - or -1 if the status was not changed - @param process Set to @c false to disable processing the change - (updating article list and updating on-screen unread count) + /** notifies that article @c mya was changed + @param oldStatus if the status was changed, it contains the old status, -1 otherwise To be called by @ref Article */ - void setArticleChanged(Article &a, int oldStatus = -1, bool process = true); + void setArticleChanged(Article &a, int oldStatus = -1); void appendArticles(const Syndication::FeedPtr &feed); /** appends article @c a to the article list */ void appendArticle(const Article &a); /** checks whether article @c a is expired (considering custom and global archive mode settings) */ bool isExpired(const Article &a) const; /** returns @c true if either this article uses @c limitArticleAge as custom setting or uses the global default, which is @c limitArticleAge */ bool usesExpiryByAge() const; /** executes the actual fetch action */ void tryFetch(); void markAsFetchedNow(); private Q_SLOTS: void fetchCompleted(Syndication::Loader *loader, Syndication::FeedPtr doc, Syndication::ErrorCode errorCode); void slotImageFetched(const QPixmap &image); private: class Private; Private *d; }; } // namespace Akregator #endif // AKREGATOR_FEED_H