diff --git a/interfaces/article.h b/interfaces/article.h index 31c5ddcf..6874321f 100644 --- a/interfaces/article.h +++ b/interfaces/article.h @@ -1,152 +1,153 @@ /* 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_ARTICLE_H #define AKREGATOR_ARTICLE_H #include "akregatorinterfaces_export.h" #include "types.h" #include #include class QDateTime; class QString; template class QList; typedef unsigned int uint; class QUrl; namespace Syndication { class Enclosure; class Item; typedef QSharedPointer ItemPtr; } namespace Akregator { namespace Backend { class FeedStorage; } class Feed; /** A proxy class for Syndication::ItemPtr with some additional methods to assist sorting. */ class AKREGATORINTERFACES_EXPORT Article { friend class ArticleDeleteJob; friend class ArticleModifyJob; friend class Feed; public: enum ContentOption { ContentAndOnlyContent, /*< returns the content field even if empty */ DescriptionAsFallback /*< uses the description field as fallback if the content field is empty */ }; Article(); /** creates am article object for an existing article. The constructor accesses the archive to load it's data */ - Article(const QString &guid, Feed *feed); + Article(const QString &guid, Feed *feed, Backend::FeedStorage *archive = nullptr); + /** creates an article object from a parsed librss Article the article is added to the archive if not yet stored, or updated if stored but modified */ Article(const Syndication::ItemPtr &article, Feed *feed); Article(const Syndication::ItemPtr &article, Backend::FeedStorage *archive); Article(const Article &other); ~Article(); void swap(Article &other) { std::swap(d, other.d); } Article &operator=(const Article &other); bool operator==(const Article &other) const; bool operator!=(const Article &other) const; bool isNull() const; int status() const; QString title() const; QUrl link() const; QString description() const; QString content(ContentOption opt = ContentAndOnlyContent) const; QString guid() const; /** if true, the article should be kept even when expired **/ bool keep() const; bool isDeleted() const; void offsetPubDate(int secs); Feed *feed() const; /** returns a hash value used to detect changes in articles with non-hash GUIDs. If the guid is a hash itself, it returns @c 0 */ uint hash() const; /** returns if the guid is a hash or an ID taken from the source */ bool guidIsHash() const; bool guidIsPermaLink() const; QDateTime pubDate() const; QUrl commentsLink() const; int comments() const; QString authorName() const; QString authorUri() const; QString authorEMail() const; QString authorAsHtml() const; QString authorShort() const; QSharedPointer enclosure() const; bool operator<(const Article &other) const; bool operator<=(const Article &other) const; bool operator>(const Article &other) const; bool operator>=(const Article &other) const; private: //only for our friends void setStatus(int s); void setDeleted(); void setKeep(bool keep); private: struct Private; Private *d; }; } // namespace Akregator Q_DECLARE_TYPEINFO(Akregator::Article, Q_MOVABLE_TYPE); #endif // AKREGATOR_ARTICLE_H diff --git a/interfaces/feedstorage.h b/interfaces/feedstorage.h index 170fcf70..7367f775 100644 --- a/interfaces/feedstorage.h +++ b/interfaces/feedstorage.h @@ -1,97 +1,98 @@ /* This file is part of Akregator. Copyright (C) 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_BACKEND_FEEDSTORAGE_H #define AKREGATOR_BACKEND_FEEDSTORAGE_H #include #include class QString; class QStringList; class QDateTime; namespace Akregator { namespace Backend { class Storage; class FeedStorage : public QObject //krazy:exclude=qobject { public: virtual int unread() const = 0; virtual void setUnread(int unread) = 0; virtual int totalCount() const = 0; virtual QDateTime lastFetch() const = 0; virtual void setLastFetch(const QDateTime &lastFetch) = 0; /** returns the guids of all articles in this storage. */ virtual QStringList articles() const = 0; /** deletes all articles from the archive */ virtual void clear() = 0; + virtual void article(const QString &guid, uint &hash, QString &title, int &status, QDateTime &pubDate) const = 0; virtual bool contains(const QString &guid) const = 0; virtual void addEntry(const QString &guid) = 0; virtual void deleteArticle(const QString &guid) = 0; virtual bool guidIsHash(const QString &guid) const = 0; virtual void setGuidIsHash(const QString &guid, bool isHash) = 0; virtual bool guidIsPermaLink(const QString &guid) const = 0; virtual void setGuidIsPermaLink(const QString &guid, bool isPermaLink) = 0; virtual uint hash(const QString &guid) const = 0; virtual void setHash(const QString &guid, uint hash) = 0; virtual void setDeleted(const QString &guid) = 0; virtual QString link(const QString &guid) const = 0; virtual void setLink(const QString &guid, const QString &link) = 0; virtual QDateTime pubDate(const QString &guid) const = 0; virtual void setPubDate(const QString &guid, const QDateTime &pubdate) = 0; virtual int status(const QString &guid) const = 0; virtual void setStatus(const QString &guid, int status) = 0; virtual QString title(const QString &guid) const = 0; virtual void setTitle(const QString &guid, const QString &title) = 0; virtual QString description(const QString &guid) const = 0; virtual void setDescription(const QString &guid, const QString &description) = 0; virtual QString content(const QString &guid) const = 0; virtual void setContent(const QString &guid, const QString &content) = 0; virtual void setEnclosure(const QString &guid, const QString &url, const QString &type, int length) = 0; virtual void removeEnclosure(const QString &guid) = 0; virtual void setAuthorName(const QString & /*guid*/, const QString &name) = 0; virtual void setAuthorUri(const QString & /*guid*/, const QString &uri) = 0; virtual void setAuthorEMail(const QString & /*guid*/, const QString &email) = 0; virtual QString authorName(const QString &guid) const = 0; virtual QString authorUri(const QString &guid) const = 0; virtual QString authorEMail(const QString &guid) const = 0; virtual void enclosure(const QString &guid, bool &hasEnclosure, QString &url, QString &type, int &length) const = 0; virtual void close() = 0; virtual void commit() = 0; virtual void rollback() = 0; }; } // namespace Backend } // namespace Akregator #endif // AKREGATOR_BACKEND_FEEDSTORAGE_H diff --git a/plugins/mk4storage/feedstoragemk4impl.cpp b/plugins/mk4storage/feedstoragemk4impl.cpp index 38cf35fc..bad1c81d 100644 --- a/plugins/mk4storage/feedstoragemk4impl.cpp +++ b/plugins/mk4storage/feedstoragemk4impl.cpp @@ -1,545 +1,557 @@ /* This file is part of Akregator. Copyright (C) 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 "feedstoragemk4impl.h" #include "storagemk4impl.h" #include #include #include #include #include #include #include #include #include #include namespace { static uint calcHash(const QString &str) { if (str.isNull()) { // handle null string as "", prevents crash return calcHash(QLatin1String("")); } const char *s = str.toLatin1(); uint hash = 5381; int c; while ((c = *s++)) { hash = ((hash << 5) + hash) + c; // hash*33 + c } return hash; } } namespace Akregator { namespace Backend { class FeedStorageMK4Impl::FeedStorageMK4ImplPrivate { public: FeedStorageMK4ImplPrivate() : modified(false) , pguid("guid") , ptitle("title") , pdescription("description") , pcontent("content") , plink("link") , pcommentsLink("commentsLink") , ptag("tag") , pEnclosureType("enclosureType") , pEnclosureUrl("enclosureUrl") , pcatTerm("catTerm") , pcatScheme("catScheme") , pcatName("catName") , pauthorName("authorName") , pauthorUri("authorUri") , pauthorEMail("authorEMail") , phash("hash") , pguidIsHash("guidIsHash") , pguidIsPermaLink("guidIsPermaLink") , pcomments("comments") , pstatus("status") , ppubDate("pubDate") , pHasEnclosure("hasEnclosure") , pEnclosureLength("enclosureLength") { } QString url; c4_Storage *storage; StorageMK4Impl *mainStorage; c4_View archiveView; bool autoCommit; bool modified; c4_StringProp pguid, ptitle, pdescription, pcontent, plink, pcommentsLink, ptag, pEnclosureType, pEnclosureUrl, pcatTerm, pcatScheme, pcatName, pauthorName, pauthorUri, pauthorEMail; c4_IntProp phash, pguidIsHash, pguidIsPermaLink, pcomments, pstatus, ppubDate, pHasEnclosure, pEnclosureLength; }; FeedStorageMK4Impl::FeedStorageMK4Impl(const QString &url, StorageMK4Impl *main) { d = new FeedStorageMK4ImplPrivate; d->autoCommit = main->autoCommit(); d->url = url; d->mainStorage = main; QString url2 = url; if (url.length() > 255) { url2 = url.left(200) + QString::number(::calcHash(url), 16); } qDebug() << url2; QString t = url2; QString t2 = url2; QString filePath = main->archivePath() + QLatin1Char('/') + t.replace(QLatin1Char('/'), QLatin1Char('_')).replace(QLatin1Char(':'), QLatin1Char('_')); d->storage = new c4_Storage(QString(filePath + QLatin1String(".mk4")).toLocal8Bit(), true); d->archiveView = d->storage->GetAs( "articles[guid:S,title:S,hash:I,guidIsHash:I,guidIsPermaLink:I,description:S,link:S,comments:I,commentsLink:S,status:I,pubDate:I,tags[tag:S],hasEnclosure:I,enclosureUrl:S,enclosureType:S,enclosureLength:I,categories[catTerm:S,catScheme:S,catName:S],authorName:S,content:S,authorUri:S,authorEMail:S]"); c4_View hash = d->storage->GetAs("archiveHash[_H:I,_R:I]"); d->archiveView = d->archiveView.Hash(hash, 1); // hash on guid } FeedStorageMK4Impl::~FeedStorageMK4Impl() { delete d->storage; delete d; d = 0; } void FeedStorageMK4Impl::markDirty() { if (!d->modified) { d->modified = true; // Tell this to mainStorage d->mainStorage->markDirty(); } } void FeedStorageMK4Impl::commit() { if (d->modified) { d->storage->Commit(); } d->modified = false; } void FeedStorageMK4Impl::rollback() { d->storage->Rollback(); } void FeedStorageMK4Impl::close() { if (d->autoCommit) { commit(); } } int FeedStorageMK4Impl::unread() const { return d->mainStorage->unreadFor(d->url); } void FeedStorageMK4Impl::setUnread(int unread) { d->mainStorage->setUnreadFor(d->url, unread); } int FeedStorageMK4Impl::totalCount() const { return d->mainStorage->totalCountFor(d->url); } void FeedStorageMK4Impl::setTotalCount(int total) { d->mainStorage->setTotalCountFor(d->url, total); } QDateTime FeedStorageMK4Impl::lastFetch() const { return d->mainStorage->lastFetchFor(d->url); } void FeedStorageMK4Impl::setLastFetch(const QDateTime &lastFetch) { d->mainStorage->setLastFetchFor(d->url, lastFetch); } QStringList FeedStorageMK4Impl::articles() const { QStringList list; int size = d->archiveView.GetSize(); list.reserve(size); for (int i = 0; i < size; ++i) { // fill with guids list += QString::fromLatin1(d->pguid(d->archiveView.GetAt(i))); } return list; } void FeedStorageMK4Impl::addEntry(const QString &guid) { c4_Row row; d->pguid(row) = guid.toLatin1(); if (!contains(guid)) { d->archiveView.Add(row); markDirty(); setTotalCount(totalCount() + 1); } } bool FeedStorageMK4Impl::contains(const QString &guid) const { return findArticle(guid) != -1; } int FeedStorageMK4Impl::findArticle(const QString &guid) const { c4_Row findrow; d->pguid(findrow) = guid.toLatin1(); return d->archiveView.Find(findrow); } void FeedStorageMK4Impl::deleteArticle(const QString &guid) { int findidx = findArticle(guid); if (findidx != -1) { setTotalCount(totalCount() - 1); d->archiveView.RemoveAt(findidx); markDirty(); } } bool FeedStorageMK4Impl::guidIsHash(const QString &guid) const { int findidx = findArticle(guid); return findidx != -1 ? d->pguidIsHash(d->archiveView.GetAt(findidx)) : false; } bool FeedStorageMK4Impl::guidIsPermaLink(const QString &guid) const { int findidx = findArticle(guid); return findidx != -1 ? d->pguidIsPermaLink(d->archiveView.GetAt(findidx)) : false; } uint FeedStorageMK4Impl::hash(const QString &guid) const { int findidx = findArticle(guid); return findidx != -1 ? d->phash(d->archiveView.GetAt(findidx)) : 0; } void FeedStorageMK4Impl::setDeleted(const QString &guid) { int findidx = findArticle(guid); if (findidx == -1) { return; } c4_Row row; row = d->archiveView.GetAt(findidx); d->pdescription(row) = ""; d->pcontent(row) = ""; d->ptitle(row) = ""; d->plink(row) = ""; d->pauthorName(row) = ""; d->pauthorUri(row) = ""; d->pauthorEMail(row) = ""; d->pcommentsLink(row) = ""; d->archiveView.SetAt(findidx, row); markDirty(); } QString FeedStorageMK4Impl::link(const QString &guid) const { int findidx = findArticle(guid); return findidx != -1 ? QString::fromLatin1(d->plink(d->archiveView.GetAt(findidx))) : QLatin1String(""); } QDateTime FeedStorageMK4Impl::pubDate(const QString &guid) const { int findidx = findArticle(guid); return findidx != -1 ? QDateTime::fromTime_t(d->ppubDate(d->archiveView.GetAt(findidx))) : QDateTime(); } int FeedStorageMK4Impl::status(const QString &guid) const { int findidx = findArticle(guid); return findidx != -1 ? d->pstatus(d->archiveView.GetAt(findidx)) : 0; } void FeedStorageMK4Impl::setStatus(const QString &guid, int status) { int findidx = findArticle(guid); if (findidx == -1) { return; } c4_Row row; row = d->archiveView.GetAt(findidx); d->pstatus(row) = status; d->archiveView.SetAt(findidx, row); markDirty(); } +void FeedStorageMK4Impl::article(const QString &guid, uint &hash, QString &title, int &status, QDateTime &pubDate) const +{ + int idx = findArticle(guid); + if (idx != -1) { + auto view = d->archiveView.GetAt(idx); + hash = d->phash(view); + title = QString::fromUtf8(d->ptitle(view)); + status = d->pstatus(view); + pubDate = QDateTime::fromTime_t(d->ppubDate(view)); + } +} + QString FeedStorageMK4Impl::title(const QString &guid) const { int findidx = findArticle(guid); return findidx != -1 ? QString::fromUtf8(d->ptitle(d->archiveView.GetAt(findidx))) : QLatin1String(""); } QString FeedStorageMK4Impl::description(const QString &guid) const { int findidx = findArticle(guid); return findidx != -1 ? QString::fromUtf8(d->pdescription(d->archiveView.GetAt(findidx))) : QLatin1String(""); } QString FeedStorageMK4Impl::content(const QString &guid) const { int findidx = findArticle(guid); return findidx != -1 ? QString::fromUtf8(d->pcontent(d->archiveView.GetAt(findidx))) : QLatin1String(""); } void FeedStorageMK4Impl::setPubDate(const QString &guid, const QDateTime &pubdate) { int findidx = findArticle(guid); if (findidx == -1) { return; } c4_Row row; row = d->archiveView.GetAt(findidx); d->ppubDate(row) = pubdate.toTime_t(); d->archiveView.SetAt(findidx, row); markDirty(); } void FeedStorageMK4Impl::setGuidIsHash(const QString &guid, bool isHash) { int findidx = findArticle(guid); if (findidx == -1) { return; } c4_Row row; row = d->archiveView.GetAt(findidx); d->pguidIsHash(row) = isHash; d->archiveView.SetAt(findidx, row); markDirty(); } void FeedStorageMK4Impl::setLink(const QString &guid, const QString &link) { int findidx = findArticle(guid); if (findidx == -1) { return; } c4_Row row; row = d->archiveView.GetAt(findidx); d->plink(row) = !link.isEmpty() ? link.toLatin1() : ""; d->archiveView.SetAt(findidx, row); markDirty(); } void FeedStorageMK4Impl::setHash(const QString &guid, uint hash) { int findidx = findArticle(guid); if (findidx == -1) { return; } c4_Row row; row = d->archiveView.GetAt(findidx); d->phash(row) = hash; d->archiveView.SetAt(findidx, row); markDirty(); } void FeedStorageMK4Impl::setTitle(const QString &guid, const QString &title) { int findidx = findArticle(guid); if (findidx == -1) { return; } c4_Row row; row = d->archiveView.GetAt(findidx); d->ptitle(row) = !title.isEmpty() ? title.toUtf8().data() : ""; d->archiveView.SetAt(findidx, row); markDirty(); } void FeedStorageMK4Impl::setDescription(const QString &guid, const QString &description) { int findidx = findArticle(guid); if (findidx == -1) { return; } c4_Row row; row = d->archiveView.GetAt(findidx); d->pdescription(row) = !description.isEmpty() ? description.toUtf8().data() : ""; d->archiveView.SetAt(findidx, row); markDirty(); } void FeedStorageMK4Impl::setContent(const QString &guid, const QString &content) { int findidx = findArticle(guid); if (findidx == -1) { return; } c4_Row row; row = d->archiveView.GetAt(findidx); d->pcontent(row) = !content.isEmpty() ? content.toUtf8().data() : ""; d->archiveView.SetAt(findidx, row); markDirty(); } void FeedStorageMK4Impl::setAuthorName(const QString &guid, const QString &author) { int findidx = findArticle(guid); if (findidx == -1) { return; } c4_Row row; row = d->archiveView.GetAt(findidx); d->pauthorName(row) = !author.isEmpty() ? author.toUtf8().data() : ""; d->archiveView.SetAt(findidx, row); markDirty(); } void FeedStorageMK4Impl::setAuthorUri(const QString &guid, const QString &author) { int findidx = findArticle(guid); if (findidx == -1) { return; } c4_Row row; row = d->archiveView.GetAt(findidx); d->pauthorUri(row) = !author.isEmpty() ? author.toUtf8().data() : ""; d->archiveView.SetAt(findidx, row); markDirty(); } void FeedStorageMK4Impl::setAuthorEMail(const QString &guid, const QString &author) { int findidx = findArticle(guid); if (findidx == -1) { return; } c4_Row row; row = d->archiveView.GetAt(findidx); d->pauthorEMail(row) = !author.isEmpty() ? author.toUtf8().data() : ""; d->archiveView.SetAt(findidx, row); markDirty(); } QString FeedStorageMK4Impl::authorName(const QString &guid) const { int findidx = findArticle(guid); return findidx != -1 ? QString::fromUtf8(d->pauthorName(d->archiveView.GetAt(findidx))) : QString(); } QString FeedStorageMK4Impl::authorUri(const QString &guid) const { int findidx = findArticle(guid); return findidx != -1 ? QString::fromUtf8(d->pauthorUri(d->archiveView.GetAt(findidx))) : QString(); } QString FeedStorageMK4Impl::authorEMail(const QString &guid) const { int findidx = findArticle(guid); return findidx != -1 ? QString::fromUtf8(d->pauthorEMail(d->archiveView.GetAt(findidx))) : QString(); } void FeedStorageMK4Impl::setGuidIsPermaLink(const QString &guid, bool isPermaLink) { int findidx = findArticle(guid); if (findidx == -1) { return; } c4_Row row; row = d->archiveView.GetAt(findidx); d->pguidIsPermaLink(row) = isPermaLink; d->archiveView.SetAt(findidx, row); markDirty(); } void FeedStorageMK4Impl::setEnclosure(const QString &guid, const QString &url, const QString &type, int length) { int findidx = findArticle(guid); if (findidx == -1) { return; } c4_Row row; row = d->archiveView.GetAt(findidx); d->pHasEnclosure(row) = true; d->pEnclosureUrl(row) = !url.isEmpty() ? url.toUtf8().data() : ""; d->pEnclosureType(row) = !type.isEmpty() ? type.toUtf8().data() : ""; d->pEnclosureLength(row) = length; d->archiveView.SetAt(findidx, row); markDirty(); } void FeedStorageMK4Impl::removeEnclosure(const QString &guid) { int findidx = findArticle(guid); if (findidx == -1) { return; } c4_Row row; row = d->archiveView.GetAt(findidx); d->pHasEnclosure(row) = false; d->pEnclosureUrl(row) = ""; d->pEnclosureType(row) = ""; d->pEnclosureLength(row) = -1; d->archiveView.SetAt(findidx, row); markDirty(); } void FeedStorageMK4Impl::enclosure(const QString &guid, bool &hasEnclosure, QString &url, QString &type, int &length) const { int findidx = findArticle(guid); if (findidx == -1) { hasEnclosure = false; url.clear(); type.clear(); length = -1; return; } c4_Row row = d->archiveView.GetAt(findidx); hasEnclosure = d->pHasEnclosure(row); url = QLatin1String(d->pEnclosureUrl(row)); type = QLatin1String(d->pEnclosureType(row)); length = d->pEnclosureLength(row); } void FeedStorageMK4Impl::clear() { d->storage->RemoveAll(); setUnread(0); markDirty(); } } // namespace Backend } // namespace Akregator diff --git a/plugins/mk4storage/feedstoragemk4impl.h b/plugins/mk4storage/feedstoragemk4impl.h index 9fe5c5aa..be16d1aa 100644 --- a/plugins/mk4storage/feedstoragemk4impl.h +++ b/plugins/mk4storage/feedstoragemk4impl.h @@ -1,96 +1,97 @@ /* This file is part of Akregator. Copyright (C) 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_BACKEND_FEEDSTORAGEMK4IMPL_H #define AKREGATOR_BACKEND_FEEDSTORAGEMK4IMPL_H #include "feedstorage.h" namespace Akregator { namespace Backend { class StorageMK4Impl; class FeedStorageMK4Impl : public FeedStorage { public: FeedStorageMK4Impl(const QString &url, StorageMK4Impl *main); ~FeedStorageMK4Impl(); void clear() override; int unread() const override; void setUnread(int unread) override; int totalCount() const override; QDateTime lastFetch() const override; void setLastFetch(const QDateTime &lastFetch) override; QStringList articles() const override; + void article(const QString &guid, uint &hash, QString &title, int &status, QDateTime &pubDate) const override; bool contains(const QString &guid) const override; void addEntry(const QString &guid) override; void deleteArticle(const QString &guid) override; bool guidIsHash(const QString &guid) const override; void setGuidIsHash(const QString &guid, bool isHash) override; bool guidIsPermaLink(const QString &guid) const override; void setGuidIsPermaLink(const QString &guid, bool isPermaLink) override; uint hash(const QString &guid) const override; void setHash(const QString &guid, uint hash) override; void setDeleted(const QString &guid) override; QString link(const QString &guid) const override; void setLink(const QString &guid, const QString &link) override; QDateTime pubDate(const QString &guid) const override; void setPubDate(const QString &guid, const QDateTime &pubdate) override; int status(const QString &guid) const override; void setStatus(const QString &guid, int status) override; QString title(const QString &guid) const override; void setTitle(const QString &guid, const QString &title) override; QString description(const QString &guid) const override; void setDescription(const QString &guid, const QString &description) override; QString content(const QString &guid) const override; void setContent(const QString &guid, const QString &content) override; void setEnclosure(const QString &guid, const QString &url, const QString &type, int length) override; void removeEnclosure(const QString &guid) override; void enclosure(const QString &guid, bool &hasEnclosure, QString &url, QString &type, int &length) const override; void setAuthorName(const QString &guid, const QString &name) override; void setAuthorUri(const QString &guid, const QString &uri) override; void setAuthorEMail(const QString &guid, const QString &email) override; QString authorName(const QString &guid) const override; QString authorUri(const QString &guid) const override; QString authorEMail(const QString &guid) const override; void close() override; void commit() override; void rollback() override; private: void markDirty(); /** finds article by guid, returns -1 if not in archive **/ int findArticle(const QString &guid) const; void setTotalCount(int total); class FeedStorageMK4ImplPrivate; FeedStorageMK4ImplPrivate *d; }; } // namespace Backend } // namespace Akregator #endif // AKREGATOR_BACKEND_FEEDSTORAGEMK4IMPL_H diff --git a/src/article.cpp b/src/article.cpp index 03d38b8f..3af74895 100644 --- a/src/article.cpp +++ b/src/article.cpp @@ -1,547 +1,545 @@ /* 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(QLatin1String("br")) || tagName.startsWith(QLatin1String("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; + QString title; // Cache the title, for performance 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(nullptr) , archive(nullptr) , 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(archive->pubDate(guid)) { + archive->article(guid, hash, title, status, pubDate); } 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(); + 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->setGuidIsPermaLink(guid, false); archive->setGuidIsHash(guid, guid.startsWith(QLatin1String("hash:"))); const time_t datePublished = article->datePublished(); if (datePublished > 0) { pubDate.setTime_t(datePublished); } else { pubDate = QDateTime::currentDateTime(); } archive->setPubDate(guid, pubDate); 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 if (hash != archive->hash(guid)) { //article is in archive, was it modified? // if yes, update pubDate = archive->pubDate(guid); archive->setHash(guid, hash); - QString title = article->title(); + 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()); } } } 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 QString &guid, Feed *feed, Backend::FeedStorage *archive) : d() { + if (!archive) + archive = feed->storage()->archiveFor(feed->xmlUrl()); + d = new Private(guid, feed, archive); } 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, nullptr, archive)) { } bool Article::isNull() const { return d->archive == nullptr; // TODO: use proper null state } void Article::offsetPubDate(int secs) { d->pubDate = d->pubDate.addSecs(secs); d->archive->setPubDate(d->guid, d->pubDate); } 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 = nullptr; } } 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); } } } QString Article::title() const { - QString str; - if (d->archive) { - str = d->archive->title(d->guid); - } - return str; + return d->title; } 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; } 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/articlemodel.cpp b/src/articlemodel.cpp index cbf0cb98..9a128315 100644 --- a/src/articlemodel.cpp +++ b/src/articlemodel.cpp @@ -1,300 +1,296 @@ /* This file is part of Akregator. Copyright (C) 2007 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 "articlemodel.h" #include "article.h" #include "articlematcher.h" #include "akregatorconfig.h" #include "feed.h" #include "utils.h" #include #include #include #include #include #include #include #include #include #include using namespace Akregator; class Q_DECL_HIDDEN ArticleModel::Private { private: ArticleModel *const q; public: Private(const QVector
&articles, ArticleModel *qq); QVector
articles; QVector titleCache; void articlesAdded(const QVector
&); void articlesRemoved(const QVector
&); void articlesUpdated(const QVector
&); }; //like Syndication::htmlToPlainText, but without linebreaks static QString stripHtml(const QString &html) { QString str(html); //TODO: preserve some formatting, such as line breaks str = Akregator::Utils::stripTags(str); // remove tags str = Syndication::resolveEntities(str); return str.simplified(); } ArticleModel::Private::Private(const QVector
&articles_, ArticleModel *qq) : q(qq) , articles(articles_) { const int articlesCount(articles.count()); titleCache.resize(articlesCount); for (int i = 0; i < articlesCount; ++i) { titleCache[i] = stripHtml(articles[i].title()); } } ArticleModel::ArticleModel(const QVector
&articles, QObject *parent) : QAbstractTableModel(parent) , d(new Private(articles, this)) { } ArticleModel::~ArticleModel() { delete d; } int ArticleModel::columnCount(const QModelIndex &parent) const { return parent.isValid() ? 0 : ColumnCount; } int ArticleModel::rowCount(const QModelIndex &parent) const { return parent.isValid() ? 0 : d->articles.count(); } QVariant ArticleModel::headerData(int section, Qt::Orientation, int role) const { if (role != Qt::DisplayRole) { return QVariant(); } switch (section) { case ItemTitleColumn: return i18nc("Articlelist's column header", "Title"); case FeedTitleColumn: return i18nc("Articlelist's column header", "Feed"); case DateColumn: return i18nc("Articlelist's column header", "Date"); case AuthorColumn: return i18nc("Articlelist's column header", "Author"); case DescriptionColumn: return i18nc("Articlelist's column header", "Description"); case ContentColumn: return i18nc("Articlelist's column header", "Content"); } return QVariant(); } QVariant ArticleModel::data(const QModelIndex &index, int role) const { if (!index.isValid() || index.row() < 0 || index.row() >= d->articles.count()) { return QVariant(); } const int row = index.row(); const Article &article(d->articles[row]); - if (article.isNull()) { - return QVariant(); - } - switch (role) { case SortRole: if (index.column() == DateColumn) { return article.pubDate(); } Q_FALLTHROUGH(); // no break case Qt::DisplayRole: switch (index.column()) { case FeedTitleColumn: return article.feed() ? article.feed()->title() : QVariant(); case DateColumn: return QLocale().toString(article.pubDate(), QLocale::ShortFormat); case ItemTitleColumn: return d->titleCache[row]; case AuthorColumn: return article.authorShort(); case DescriptionColumn: case ContentColumn: return article.description(); } case LinkRole: return article.link(); case ItemIdRole: case GuidRole: return article.guid(); case FeedIdRole: return article.feed() ? article.feed()->xmlUrl() : QVariant(); case StatusRole: return article.status(); case IsImportantRole: return article.keep(); case IsDeletedRole: return article.isDeleted(); } return QVariant(); } void ArticleModel::clear() { beginResetModel(); d->articles.clear(); d->titleCache.clear(); endResetModel(); } void ArticleModel::articlesAdded(TreeNode *, const QVector
&l) { d->articlesAdded(l); } void ArticleModel::articlesRemoved(TreeNode *, const QVector
&l) { d->articlesRemoved(l); } void ArticleModel::articlesUpdated(TreeNode *, const QVector
&l) { d->articlesUpdated(l); } void ArticleModel::Private::articlesAdded(const QVector
&list) { if (list.isEmpty()) { //assert? return; } const int first = articles.count(); q->beginInsertRows(QModelIndex(), first, first + list.size() - 1); const int oldSize = articles.size(); articles << list; const int newArticlesCount(articles.count()); titleCache.resize(newArticlesCount); for (int i = oldSize; i < newArticlesCount; ++i) { titleCache[i] = stripHtml(articles[i].title()); } q->endInsertRows(); } void ArticleModel::Private::articlesRemoved(const QVector
&list) { //might want to avoid indexOf() in case of performance problems for (const Article &i : list) { const int row = articles.indexOf(i); Q_ASSERT(row != -1); q->removeRow(row, QModelIndex()); } } void ArticleModel::Private::articlesUpdated(const QVector
&list) { int rmin = 0; int rmax = 0; const int numberOfArticles(articles.count()); if (numberOfArticles > 0) { rmin = numberOfArticles - 1; //might want to avoid indexOf() in case of performance problems for (const Article &i : list) { const int row = articles.indexOf(i); //TODO: figure out how why the Article might not be found in //TODO: the articles list because we should need this conditional. if (row >= 0) { titleCache[row] = stripHtml(articles[row].title()); rmin = std::min(row, rmin); rmax = std::max(row, rmax); } } } Q_EMIT q->dataChanged(q->index(rmin, 0), q->index(rmax, ColumnCount - 1)); } bool ArticleModel::rowMatches(int row, const QSharedPointer &matcher) const { Q_ASSERT(matcher); return matcher->matches(article(row)); } Article ArticleModel::article(int row) const { if (row < 0 || row >= d->articles.count()) { return Article(); } return d->articles[row]; } QStringList ArticleModel::mimeTypes() const { return QStringList() << QStringLiteral("text/uri-list"); } QMimeData *ArticleModel::mimeData(const QModelIndexList &indexes) const { QScopedPointer md(new QMimeData); QList urls; for (const QModelIndex &i : indexes) { const QUrl url = i.data(ArticleModel::LinkRole).toUrl(); if (url.isValid()) { urls.push_back(url); } else { const QUrl guid(i.data(ArticleModel::GuidRole).toString()); if (guid.isValid()) { urls.push_back(guid); } } } md->setUrls(urls); return md.take(); } Qt::ItemFlags ArticleModel::flags(const QModelIndex &idx) const { const Qt::ItemFlags f = QAbstractTableModel::flags(idx); if (!idx.isValid()) { return f; } return f | Qt::ItemIsDragEnabled; } diff --git a/src/dummystorage/feedstoragedummyimpl.cpp b/src/dummystorage/feedstoragedummyimpl.cpp index 9e45b013..8bef5dc9 100644 --- a/src/dummystorage/feedstoragedummyimpl.cpp +++ b/src/dummystorage/feedstoragedummyimpl.cpp @@ -1,365 +1,376 @@ /* This file is part of Akregator. Copyright (C) 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 "feedstoragedummyimpl.h" #include "storagedummyimpl.h" #include #include #include #include #include #include #include namespace Akregator { namespace Backend { class FeedStorageDummyImpl::FeedStorageDummyImplPrivate { public: class Entry { public: Entry() : status(0) , pubDate() , hash(0) , guidIsHash(false) , guidIsPermaLink(false) { } StorageDummyImpl *mainStorage = nullptr; QString enclosureUrl; QString enclosureType; QString title; QString description; QString content; QString link; QString authorName; QString authorUri; QString authorEMail; int status; int enclosureLength; QDateTime pubDate; uint hash; bool guidIsHash = false; bool guidIsPermaLink = false; bool hasEnclosure = false; }; QHash entries; StorageDummyImpl *mainStorage; QString url; }; FeedStorageDummyImpl::FeedStorageDummyImpl(const QString &url, StorageDummyImpl *main) : d(new FeedStorageDummyImplPrivate) { d->url = url; d->mainStorage = main; } FeedStorageDummyImpl::~FeedStorageDummyImpl() { delete d; d = nullptr; } void FeedStorageDummyImpl::commit() { } void FeedStorageDummyImpl::rollback() { } void FeedStorageDummyImpl::close() { } int FeedStorageDummyImpl::unread() const { return d->mainStorage->unreadFor(d->url); } void FeedStorageDummyImpl::setUnread(int unread) { d->mainStorage->setUnreadFor(d->url, unread); } int FeedStorageDummyImpl::totalCount() const { return d->mainStorage->totalCountFor(d->url); } void FeedStorageDummyImpl::setTotalCount(int total) { d->mainStorage->setTotalCountFor(d->url, total); } QDateTime FeedStorageDummyImpl::lastFetch() const { return d->mainStorage->lastFetchFor(d->url); } void FeedStorageDummyImpl::setLastFetch(const QDateTime &lastFetch) { d->mainStorage->setLastFetchFor(d->url, lastFetch); } QStringList FeedStorageDummyImpl::articles() const { return QStringList(d->entries.keys()); } void FeedStorageDummyImpl::addEntry(const QString &guid) { if (!d->entries.contains(guid)) { d->entries[guid] = FeedStorageDummyImplPrivate::Entry(); setTotalCount(totalCount() + 1); } } bool FeedStorageDummyImpl::contains(const QString &guid) const { return d->entries.contains(guid); } void FeedStorageDummyImpl::deleteArticle(const QString &guid) { if (!d->entries.contains(guid)) { return; } setDeleted(guid); d->entries.remove(guid); } bool FeedStorageDummyImpl::guidIsHash(const QString &guid) const { return contains(guid) ? d->entries[guid].guidIsHash : false; } bool FeedStorageDummyImpl::guidIsPermaLink(const QString &guid) const { return contains(guid) ? d->entries[guid].guidIsPermaLink : false; } uint FeedStorageDummyImpl::hash(const QString &guid) const { return contains(guid) ? d->entries[guid].hash : 0; } void FeedStorageDummyImpl::setDeleted(const QString &guid) { if (!contains(guid)) { return; } FeedStorageDummyImplPrivate::Entry entry = d->entries[guid]; entry.description.clear(); entry.content.clear(); entry.title.clear(); entry.link.clear(); } QString FeedStorageDummyImpl::link(const QString &guid) const { return contains(guid) ? d->entries[guid].link : QString(); } QDateTime FeedStorageDummyImpl::pubDate(const QString &guid) const { return contains(guid) ? d->entries[guid].pubDate : QDateTime(); } int FeedStorageDummyImpl::status(const QString &guid) const { return contains(guid) ? d->entries[guid].status : 0; } void FeedStorageDummyImpl::setStatus(const QString &guid, int status) { if (contains(guid)) { d->entries[guid].status = status; } } +void FeedStorageDummyImpl::article(const QString &guid, uint &hash, QString &title, int &status, QDateTime &pubDate) const +{ + if (contains(guid)) { + auto &entry = d->entries[guid]; + hash = entry.hash; + title = entry.title; + status = entry.status; + pubDate = entry.pubDate; + } +} + QString FeedStorageDummyImpl::title(const QString &guid) const { return contains(guid) ? d->entries[guid].title : QString(); } QString FeedStorageDummyImpl::description(const QString &guid) const { return contains(guid) ? d->entries[guid].description : QString(); } QString FeedStorageDummyImpl::content(const QString &guid) const { return contains(guid) ? d->entries[guid].content : QString(); } QString FeedStorageDummyImpl::authorName(const QString &guid) const { return contains(guid) ? d->entries[guid].authorName : QString(); } QString FeedStorageDummyImpl::authorUri(const QString &guid) const { return contains(guid) ? d->entries[guid].authorUri : QString(); } QString FeedStorageDummyImpl::authorEMail(const QString &guid) const { return contains(guid) ? d->entries[guid].authorEMail : QString(); } void FeedStorageDummyImpl::setPubDate(const QString &guid, const QDateTime &pubdate) { if (contains(guid)) { d->entries[guid].pubDate = pubdate; } } void FeedStorageDummyImpl::setGuidIsHash(const QString &guid, bool isHash) { if (contains(guid)) { d->entries[guid].guidIsHash = isHash; } } void FeedStorageDummyImpl::setLink(const QString &guid, const QString &link) { if (contains(guid)) { d->entries[guid].link = link; } } void FeedStorageDummyImpl::setHash(const QString &guid, uint hash) { if (contains(guid)) { d->entries[guid].hash = hash; } } void FeedStorageDummyImpl::setTitle(const QString &guid, const QString &title) { if (contains(guid)) { d->entries[guid].title = title; } } void FeedStorageDummyImpl::setDescription(const QString &guid, const QString &description) { if (contains(guid)) { d->entries[guid].description = description; } } void FeedStorageDummyImpl::setContent(const QString &guid, const QString &content) { if (contains(guid)) { d->entries[guid].content = content; } } void FeedStorageDummyImpl::setAuthorName(const QString &guid, const QString &author) { if (contains(guid)) { d->entries[guid].authorName = author; } } void FeedStorageDummyImpl::setAuthorUri(const QString &guid, const QString &author) { if (contains(guid)) { d->entries[guid].authorUri = author; } } void FeedStorageDummyImpl::setAuthorEMail(const QString &guid, const QString &author) { if (contains(guid)) { d->entries[guid].authorEMail = author; } } void FeedStorageDummyImpl::setGuidIsPermaLink(const QString &guid, bool isPermaLink) { if (contains(guid)) { d->entries[guid].guidIsPermaLink = isPermaLink; } } void FeedStorageDummyImpl::clear() { d->entries.clear(); setUnread(0); setTotalCount(0); } void FeedStorageDummyImpl::setEnclosure(const QString &guid, const QString &url, const QString &type, int length) { if (contains(guid)) { FeedStorageDummyImplPrivate::Entry entry = d->entries[guid]; entry.hasEnclosure = true; entry.enclosureUrl = url; entry.enclosureType = type; entry.enclosureLength = length; } } void FeedStorageDummyImpl::removeEnclosure(const QString &guid) { if (contains(guid)) { FeedStorageDummyImplPrivate::Entry entry = d->entries[guid]; entry.hasEnclosure = false; entry.enclosureUrl.clear(); entry.enclosureType.clear(); entry.enclosureLength = -1; } } void FeedStorageDummyImpl::enclosure(const QString &guid, bool &hasEnclosure, QString &url, QString &type, int &length) const { if (contains(guid)) { FeedStorageDummyImplPrivate::Entry entry = d->entries[guid]; hasEnclosure = entry.hasEnclosure; url = entry.enclosureUrl; type = entry.enclosureType; length = entry.enclosureLength; } else { hasEnclosure = false; url.clear(); type.clear(); length = -1; } } } // namespace Backend } // namespace Akregator diff --git a/src/dummystorage/feedstoragedummyimpl.h b/src/dummystorage/feedstoragedummyimpl.h index f1469869..56a10690 100644 --- a/src/dummystorage/feedstoragedummyimpl.h +++ b/src/dummystorage/feedstoragedummyimpl.h @@ -1,95 +1,96 @@ /* This file is part of Akregator. Copyright (C) 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_BACKEND_FEEDSTORAGEDUMMYIMPL_H #define AKREGATOR_BACKEND_FEEDSTORAGEDUMMYIMPL_H #include "feedstorage.h" #include namespace Akregator { namespace Backend { class StorageDummyImpl; class FeedStorageDummyImpl : public FeedStorage { public: FeedStorageDummyImpl(const QString &url, StorageDummyImpl *main); ~FeedStorageDummyImpl() override; void clear() override; int unread() const override; void setUnread(int unread) override; int totalCount() const override; QDateTime lastFetch() const override; void setLastFetch(const QDateTime &lastFetch) override; QStringList articles() const override; + void article(const QString &guid, uint &hash, QString &title, int &status, QDateTime &pubDate) const override; bool contains(const QString &guid) const override; void addEntry(const QString &guid) override; void deleteArticle(const QString &guid) override; bool guidIsHash(const QString &guid) const override; void setGuidIsHash(const QString &guid, bool isHash) override; bool guidIsPermaLink(const QString &guid) const override; void setGuidIsPermaLink(const QString &guid, bool isPermaLink) override; uint hash(const QString &guid) const override; void setHash(const QString &guid, uint hash) override; void setDeleted(const QString &guid) override; QString link(const QString &guid) const override; void setLink(const QString &guid, const QString &link) override; QDateTime pubDate(const QString &guid) const override; void setPubDate(const QString &guid, const QDateTime & pubdate) override; int status(const QString &guid) const override; void setStatus(const QString &guid, int status) override; QString title(const QString &guid) const override; void setTitle(const QString &guid, const QString &title) override; QString description(const QString &guid) const override; void setDescription(const QString &guid, const QString &description) override; QString content(const QString &guid) const override; void setContent(const QString &guid, const QString &content) override; void setEnclosure(const QString &guid, const QString &url, const QString &type, int length) override; void removeEnclosure(const QString &guid) override; void enclosure(const QString &guid, bool &hasEnclosure, QString &url, QString &type, int &length) const override; void setAuthorName(const QString &guid, const QString &authorName) override; void setAuthorUri(const QString &guid, const QString &authorUri) override; void setAuthorEMail(const QString &guid, const QString &authorEMail) override; QString authorName(const QString &guid) const override; QString authorUri(const QString &guid) const override; QString authorEMail(const QString &guid) const override; void close() override; void commit() override; void rollback() override; private: /** finds article by guid, returns -1 if not in archive **/ int findArticle(const QString &guid) const; void setTotalCount(int total); class FeedStorageDummyImplPrivate; FeedStorageDummyImplPrivate *d; }; } // namespace Backend } // namespace Akregator #endif // AKREGATOR_FEEDSTORAGEDUMMYIMPL_H diff --git a/src/feed/feed.cpp b/src/feed/feed.cpp index 4a0851ad..4388cf93 100644 --- a/src/feed/feed.cpp +++ b/src/feed/feed.cpp @@ -1,946 +1,947 @@ /* 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 "feedretriever.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 + if (!feed->d->archive && storage) { + // Instead of loading the articles, we use the cache from storage + feed->d->archive = storage->archiveFor(xmlUrl); + feed->d->totalCount = feed->d->archive->totalCount(); + } 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); + Article mya(*it, this, d->archive); 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(nullptr) , articlesLoaded(false) , archive(nullptr) , 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 = nullptr; } 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 != nullptr; } 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().toTime_t(); uint now = QDateTime::currentDateTimeUtc().toTime_t(); if (interval > 0 && (now - lastFetch) >= static_cast(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), new FeedRetriever()); } 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 = nullptr; // 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()); } } 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) { int newStatus = a.status(); if (oldStatus != -1) { if (oldStatus == Read && newStatus != Read) { setUnread(unread() + 1); } else if (oldStatus != Read && newStatus == Read) { setUnread(unread() - 1); } } d->updatedArticlesNotify.append(a); if (process) { 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/folder.cpp b/src/folder.cpp index 5dd57ef4..784357aa 100644 --- a/src/folder.cpp +++ b/src/folder.cpp @@ -1,508 +1,499 @@ /* This file is part of Akregator. Copyright (C) 2004 Stanislav Karchebny 2004-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 "folder.h" #include "article.h" #include "articlejobs.h" #include "feed.h" #include "fetchqueue.h" #include "treenodevisitor.h" #include #include #include #include "akregator_debug.h" #include using namespace Akregator; // efficient alternative so we don't convert first to a temporary QList then to QVector template static QVector hashValuesToVector(const QHash &hash) { QVector result; result.reserve(hash.count()); for (auto it = hash.cbegin(), end = hash.cend(); it != end; ++it) { result.append(it.value()); } return result; } class Folder::FolderPrivate { Folder *const q; public: explicit FolderPrivate(Folder *qq); ~FolderPrivate(); /** List of children */ QList children; /** caching unread count of children */ mutable int unread; /** whether or not the folder is expanded */ bool open; - - /** caches guids for notifying added articles */ - QVector
addedArticlesNotify; - /** caches guids for notifying removed articles */ - QVector
removedArticlesNotify; }; Folder::FolderPrivate::FolderPrivate(Folder *qq) : q(qq) , unread(0) , open(false) { } Folder::FolderPrivate::~FolderPrivate() { while (!children.isEmpty()) { // child removes itself from list in its destructor delete children.first(); } Q_EMIT q->emitSignalDestroyed(); } bool Folder::accept(TreeNodeVisitor *visitor) { if (visitor->visitFolder(this)) { return true; } else { return visitor->visitTreeNode(this); } } Folder *Folder::fromOPML(const QDomElement &e) { Folder *fg = new Folder(e.hasAttribute(QStringLiteral("text")) ? e.attribute(QStringLiteral("text")) : e.attribute(QStringLiteral("title"))); fg->setOpen(e.attribute(QStringLiteral("isOpen")) == QLatin1String("true")); fg->setId(e.attribute(QStringLiteral("id")).toUInt()); return fg; } Folder::Folder(const QString &title) : TreeNode() , d(new FolderPrivate(this)) { setTitle(title); } Folder::~Folder() { delete d; d = nullptr; } QVector
Folder::articles() { QVector
seq; Q_FOREACH (Feed *const i, feeds()) { seq += i->articles(); } return seq; } QDomElement Folder::toOPML(QDomElement parent, QDomDocument document) const { QDomElement el = document.createElement(QStringLiteral("outline")); el.setAttribute(QStringLiteral("text"), title()); parent.appendChild(el); el.setAttribute(QStringLiteral("isOpen"), d->open ? QStringLiteral("true") : QStringLiteral("false")); el.setAttribute(QStringLiteral("id"), QString::number(id())); Q_FOREACH (const Akregator::TreeNode *i, d->children) { el.appendChild(i->toOPML(el, document)); } return el; } QList Folder::children() const { QList children; children.reserve(d->children.size()); for (const TreeNode *i : qAsConst(d->children)) { children.append(i); } return children; } QList Folder::children() { return d->children; } QVector Folder::feeds() const { QHash feedsById; for (const TreeNode *i : qAsConst(d->children)) { Q_FOREACH (const Akregator::Feed *j, i->feeds()) { feedsById.insert(j->id(), j); } } return hashValuesToVector(feedsById); } QVector Folder::feeds() { QHash feedsById; for (TreeNode *i : qAsConst(d->children)) { Q_FOREACH (Akregator::Feed *j, i->feeds()) { feedsById.insert(j->id(), j); } } return hashValuesToVector(feedsById); } QVector Folder::folders() const { QHash foldersById; foldersById.insert(id(), this); for (const TreeNode *i : qAsConst(d->children)) { Q_FOREACH (const Folder *j, i->folders()) { foldersById.insert(j->id(), j); } } return hashValuesToVector(foldersById); } QVector Folder::folders() { QHash foldersById; foldersById.insert(id(), this); for (TreeNode *i : qAsConst(d->children)) { Q_FOREACH (Folder *j, i->folders()) { foldersById.insert(j->id(), j); } } return hashValuesToVector(foldersById); } int Folder::indexOf(const TreeNode *node) const { return children().indexOf(node); } void Folder::insertChild(TreeNode *node, TreeNode *after) { int pos = d->children.indexOf(after); if (pos < 0) { prependChild(node); } else { insertChild(pos, node); } } QIcon Folder::icon() const { return QIcon::fromTheme(QStringLiteral("folder")); } void Folder::insertChild(int index, TreeNode *node) { // qCDebug(AKREGATOR_LOG) <<"enter Folder::insertChild(int, node)" << node->title(); if (node) { if (index >= d->children.size()) { d->children.append(node); } else { d->children.insert(index, node); } node->setParent(this); connectToNode(node); updateUnreadCount(); Q_EMIT signalChildAdded(node); - d->addedArticlesNotify += node->articles(); articlesModified(); nodeModified(); } // qCDebug(AKREGATOR_LOG) <<"leave Folder::insertChild(int, node)" << node->title(); } void Folder::appendChild(TreeNode *node) { // qCDebug(AKREGATOR_LOG) <<"enter Folder::appendChild()" << node->title(); if (node) { d->children.append(node); node->setParent(this); connectToNode(node); updateUnreadCount(); Q_EMIT signalChildAdded(node); - d->addedArticlesNotify += node->articles(); articlesModified(); nodeModified(); } // qCDebug(AKREGATOR_LOG) <<"leave Folder::appendChild()" << node->title(); } void Folder::prependChild(TreeNode *node) { // qCDebug(AKREGATOR_LOG) <<"enter Folder::prependChild()" << node->title(); if (node) { d->children.prepend(node); node->setParent(this); connectToNode(node); updateUnreadCount(); Q_EMIT signalChildAdded(node); - d->addedArticlesNotify += node->articles(); articlesModified(); nodeModified(); } // qCDebug(AKREGATOR_LOG) <<"leave Folder::prependChild()" << node->title(); } void Folder::removeChild(TreeNode *node) { if (!node || !d->children.contains(node)) { return; } Q_EMIT signalAboutToRemoveChild(node); node->setParent(nullptr); d->children.removeOne(node); disconnectFromNode(node); updateUnreadCount(); Q_EMIT signalChildRemoved(this, node); - d->removedArticlesNotify += node->articles(); articlesModified(); // articles were removed, TODO: add guids to a list nodeModified(); } TreeNode *Folder::firstChild() { return d->children.isEmpty() ? nullptr : children().first(); } const TreeNode *Folder::firstChild() const { return d->children.isEmpty() ? nullptr : children().first(); } TreeNode *Folder::lastChild() { return d->children.isEmpty() ? nullptr : children().last(); } const TreeNode *Folder::lastChild() const { return d->children.isEmpty() ? nullptr : children().last(); } bool Folder::isOpen() const { return d->open; } void Folder::setOpen(bool open) { d->open = open; } int Folder::unread() const { return d->unread; } int Folder::totalCount() const { int total = 0; Q_FOREACH (const Feed *const i, feeds()) { total += i->totalCount(); } return total; } void Folder::updateUnreadCount() const { int unread = 0; Q_FOREACH (const Feed *const i, feeds()) { unread += i->unread(); } d->unread = unread; } KJob *Folder::createMarkAsReadJob() { CompositeJob *job = new CompositeJob; Q_FOREACH (Feed *const i, feeds()) { job->addSubjob(i->createMarkAsReadJob()); } return job; } void Folder::slotChildChanged(TreeNode * /*node*/) { updateUnreadCount(); nodeModified(); } void Folder::slotChildDestroyed(TreeNode *node) { d->children.removeAll(node); updateUnreadCount(); nodeModified(); } bool Folder::subtreeContains(const TreeNode *node) const { if (node == this) { return false; } const Folder *parent = node ? node->parent() : nullptr; while (parent) { if (parent == this) { return true; } parent = parent->parent(); } return false; } void Folder::slotAddToFetchQueue(FetchQueue *queue, bool intervalFetchOnly) { Q_FOREACH (Feed *const i, feeds()) { if (i->useCustomFetchInterval()) { if (i->fetchInterval() != -1) { i->slotAddToFetchQueue(queue, intervalFetchOnly); } else { qCDebug(AKREGATOR_LOG) << " excluded feeds: " << i->description(); } } else { i->slotAddToFetchQueue(queue, intervalFetchOnly); } } } void Folder::doArticleNotification() { } void Folder::connectToNode(TreeNode *child) { connect(child, &TreeNode::signalChanged, this, &Folder::slotChildChanged); connect(child, &TreeNode::signalDestroyed, this, &Folder::slotChildDestroyed); connect(child, &TreeNode::signalArticlesAdded, this, &TreeNode::signalArticlesAdded); connect(child, &TreeNode::signalArticlesRemoved, this, &TreeNode::signalArticlesRemoved); connect(child, &TreeNode::signalArticlesUpdated, this, &TreeNode::signalArticlesUpdated); } void Folder::disconnectFromNode(TreeNode *child) { Q_ASSERT(child); child->disconnect(this); } TreeNode *Folder::childAt(int pos) { if (pos < 0 || pos >= d->children.count()) { return nullptr; } return d->children.at(pos); } const TreeNode *Folder::childAt(int pos) const { if (pos < 0 || pos >= d->children.count()) { return nullptr; } return d->children.at(pos); } TreeNode *Folder::next() { if (firstChild()) { return firstChild(); } if (nextSibling()) { return nextSibling(); } Folder *p = parent(); while (p) { if (p->nextSibling()) { return p->nextSibling(); } else { p = p->parent(); } } return nullptr; } const TreeNode *Folder::next() const { if (firstChild()) { return firstChild(); } if (nextSibling()) { return nextSibling(); } const Folder *p = parent(); while (p) { if (p->nextSibling()) { return p->nextSibling(); } else { p = p->parent(); } } return nullptr; } QList Folder::namedChildren(const QString &title) const { QList nodeList; foreach (const TreeNode *child, children()) { if (child->title() == title) { nodeList.append(child); } const Folder *fld = dynamic_cast(child); if (fld) { nodeList += fld->namedChildren(title); } } return nodeList; } QList Folder::namedChildren(const QString &title) { QList nodeList; foreach (TreeNode *const child, children()) { if (child->title() == title) { nodeList.append(child); } Folder *const fld = qobject_cast(child); if (fld) { nodeList += fld->namedChildren(title); } } return nodeList; }