2 This file is part of Akregator.
4 Copyright (C) 2004 Stanislav Karchebny <Stanislav.Karchebny@kdemail.net>
5 2005 Frank Osterfeld <osterfeld@kde.org>
7 This program is free software; you can redistribute it and/or modify
8 it under the terms of the GNU General Public License as published by
9 the Free Software Foundation; either version 2 of the License, or
10 (at your option) any later version.
12 This program is distributed in the hope that it will be useful,
13 but WITHOUT ANY WARRANTY; without even the implied warranty of
14 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 GNU General Public License for more details.
17 You should have received a copy of the GNU General Public License
18 along with this program; if not, write to the Free Software
19 Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
21 As a special exception, permission is given to link this program
22 with any edition of Qt, and distribute the resulting executable,
23 without including the source code for Qt in the source distribution.
26 #include "akregatorconfig.h"
29 #include "articlejobs.h"
30 #include "feediconmanager.h"
31 #include "feedstorage.h"
32 #include "fetchqueue.h"
34 #include "notificationmanager.h"
36 #include "treenodevisitor.h"
39 #include <syndication/syndication.h>
44 #include <kstandarddirs.h>
51 #include <QDomDocument>
52 #include <QDomElement>
59 #include <boost/bind.hpp>
63 using Syndication::ItemPtr
;
64 using namespace Akregator
;
65 using namespace boost
;
71 explicit Private( Backend::Storage
* storage
, Feed
* qq
);
73 Backend::Storage
* storage
;
76 ArchiveMode archiveMode
;
79 bool markImmediatelyAsRead
;
81 bool loadLinkedWebsite
;
84 Syndication::ErrorCode fetchErrorCode
;
87 Syndication::Loader
* loader
;
89 Backend::FeedStorage
* archive
;
95 /** list of feed articles */
96 QHash
<QString
, Article
> articles
;
98 /** list of deleted articles. This contains **/
99 QList
<Article
> deletedArticles
;
101 /** caches guids of deleted articles for notification */
103 QList
<Article
> addedArticlesNotify
;
104 QList
<Article
> removedArticlesNotify
;
105 QList
<Article
> updatedArticlesNotify
;
108 Syndication::ImagePtr image
;
110 mutable int totalCount
;
111 void setTotalCountDirty() const { totalCount
= -1; }
114 QString
Feed::archiveModeToString(ArchiveMode mode
)
118 case keepAllArticles
:
119 return "keepAllArticles";
120 case disableArchiving
:
121 return "disableArchiving";
122 case limitArticleNumber
:
123 return "limitArticleNumber";
124 case limitArticleAge
:
125 return "limitArticleAge";
127 return "globalDefault";
130 // in a perfect world, this is never reached
132 return "globalDefault";
135 Feed
* Feed::fromOPML(QDomElement e
, Backend::Storage
* storage
)
138 if( !e
.hasAttribute("xmlUrl") && !e
.hasAttribute("xmlurl") && !e
.hasAttribute("xmlURL") )
141 QString title
= e
.hasAttribute("text") ? e
.attribute("text") : e
.attribute("title");
143 QString xmlUrl
= e
.hasAttribute("xmlUrl") ? e
.attribute("xmlUrl") : e
.attribute("xmlurl");
144 if (xmlUrl
.isEmpty())
145 xmlUrl
= e
.attribute("xmlURL");
147 bool useCustomFetchInterval
= e
.attribute("useCustomFetchInterval") == "true";
149 QString htmlUrl
= e
.attribute("htmlUrl");
150 QString description
= e
.attribute("description");
151 int fetchInterval
= e
.attribute("fetchInterval").toInt();
152 ArchiveMode archiveMode
= stringToArchiveMode(e
.attribute("archiveMode"));
153 int maxArticleAge
= e
.attribute("maxArticleAge").toUInt();
154 int maxArticleNumber
= e
.attribute("maxArticleNumber").toUInt();
155 bool markImmediatelyAsRead
= e
.attribute("markImmediatelyAsRead") == "true";
156 bool useNotification
= e
.attribute("useNotification") == "true";
157 bool loadLinkedWebsite
= e
.attribute("loadLinkedWebsite") == "true";
158 uint id
= e
.attribute("id").toUInt();
160 Feed
* const feed
= new Feed( storage
);
161 feed
->setTitle(title
);
162 feed
->setXmlUrl(xmlUrl
);
163 feed
->setCustomFetchIntervalEnabled(useCustomFetchInterval
);
164 feed
->setHtmlUrl(htmlUrl
);
166 feed
->setDescription(description
);
167 feed
->setArchiveMode(archiveMode
);
168 feed
->setUseNotification(useNotification
);
169 feed
->setFetchInterval(fetchInterval
);
170 feed
->setMaxArticleAge(maxArticleAge
);
171 feed
->setMaxArticleNumber(maxArticleNumber
);
172 feed
->setMarkImmediatelyAsRead(markImmediatelyAsRead
);
173 feed
->setLoadLinkedWebsite(loadLinkedWebsite
);
174 feed
->loadArticles(); // TODO: make me fly: make this delayed
179 bool Feed::accept(TreeNodeVisitor
* visitor
)
181 if (visitor
->visitFeed(this))
184 return visitor
->visitTreeNode(this);
187 QVector
<const Folder
*> Feed::folders() const
189 return QVector
<const Folder
*>();
192 QVector
<Folder
*> Feed::folders()
194 return QVector
<Folder
*>();
197 QVector
<const Feed
*> Feed::feeds() const
199 QVector
<const Feed
*> list
;
204 QVector
<Feed
*> Feed::feeds()
211 Article
Feed::findArticle(const QString
& guid
) const
213 return d
->articles
[guid
];
216 QList
<Article
> Feed::articles()
218 if (!d
->articlesLoaded
)
220 return d
->articles
.values();
223 Backend::Storage
* Feed::storage()
228 void Feed::loadArticles()
230 if (d
->articlesLoaded
)
234 d
->archive
= d
->storage
->archiveFor(xmlUrl());
236 QStringList list
= d
->archive
->articles();
237 for ( QStringList::ConstIterator it
= list
.constBegin(); it
!= list
.constEnd(); ++it
)
239 Article
mya(*it
, this);
240 d
->articles
[mya
.guid()] = mya
;
242 d
->deletedArticles
.append(mya
);
245 d
->articlesLoaded
= true;
246 enforceLimitArticleNumber();
250 void Feed::recalcUnreadCount()
252 QList
<Article
> tarticles
= articles();
253 QList
<Article
>::ConstIterator it
;
254 QList
<Article
>::ConstIterator en
= tarticles
.constEnd();
256 int oldUnread
= d
->archive
->unread();
260 for (it
= tarticles
.constBegin(); it
!= en
; ++it
)
261 if (!(*it
).isDeleted() && (*it
).status() != Read
)
264 if (unread
!= oldUnread
)
266 d
->archive
->setUnread(unread
);
271 Feed::ArchiveMode
Feed::stringToArchiveMode(const QString
& str
)
273 if (str
== "globalDefault")
274 return globalDefault
;
275 if (str
== "keepAllArticles")
276 return keepAllArticles
;
277 if (str
== "disableArchiving")
278 return disableArchiving
;
279 if (str
== "limitArticleNumber")
280 return limitArticleNumber
;
281 if (str
== "limitArticleAge")
282 return limitArticleAge
;
284 return globalDefault
;
287 Feed::Private::Private( Backend::Storage
* storage_
, Feed
* qq
)
292 archiveMode( globalDefault
),
294 maxArticleNumber( 1000 ),
295 markImmediatelyAsRead( false ),
296 useNotification( false ),
297 loadLinkedWebsite( false ),
299 fetchErrorCode( Syndication::Success
),
301 followDiscovery( false ),
303 articlesLoaded( false ),
311 Feed::Feed( Backend::Storage
* storage
) : TreeNode(), d( new Private( storage
, this ) )
317 FeedIconManager::self()->removeListener( this );
319 emitSignalDestroyed();
324 bool Feed::useCustomFetchInterval() const { return d
->autoFetch
; }
326 void Feed::setCustomFetchIntervalEnabled(bool enabled
) { d
->autoFetch
= enabled
; }
328 int Feed::fetchInterval() const { return d
->fetchInterval
; }
330 void Feed::setFetchInterval(int interval
) { d
->fetchInterval
= interval
; }
332 int Feed::maxArticleAge() const { return d
->maxArticleAge
; }
334 void Feed::setMaxArticleAge(int maxArticleAge
) { d
->maxArticleAge
= maxArticleAge
; }
336 int Feed::maxArticleNumber() const { return d
->maxArticleNumber
; }
338 void Feed::setMaxArticleNumber(int maxArticleNumber
) { d
->maxArticleNumber
= maxArticleNumber
; }
340 bool Feed::markImmediatelyAsRead() const { return d
->markImmediatelyAsRead
; }
342 bool Feed::isFetching() const { return d
->loader
!= 0; }
344 void Feed::setMarkImmediatelyAsRead(bool enabled
)
346 d
->markImmediatelyAsRead
= enabled
;
348 createMarkAsReadJob()->start();
351 void Feed::setUseNotification(bool enabled
)
353 d
->useNotification
= enabled
;
356 bool Feed::useNotification() const
358 return d
->useNotification
;
361 void Feed::setLoadLinkedWebsite(bool enabled
)
363 d
->loadLinkedWebsite
= enabled
;
366 bool Feed::loadLinkedWebsite() const
368 return d
->loadLinkedWebsite
;
371 QPixmap
Feed::image() const { return d
->imagePixmap
; }
373 QString
Feed::xmlUrl() const { return d
->xmlUrl
; }
375 void Feed::setXmlUrl(const QString
& s
)
378 if( ! Settings::fetchOnStartup() )
379 QTimer::singleShot(KRandom::random() % 4000, this, SLOT(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...
382 QString
Feed::htmlUrl() const { return d
->htmlUrl
; }
384 void Feed::setHtmlUrl(const QString
& s
) { d
->htmlUrl
= s
; }
386 QString
Feed::description() const { return d
->description
; }
388 void Feed::setDescription(const QString
& s
) { d
->description
= s
; }
390 bool Feed::fetchErrorOccurred() const { return d
->fetchErrorCode
!= Syndication::Success
; }
392 Syndication::ErrorCode
Feed::fetchErrorCode() const { return d
->fetchErrorCode
; }
394 bool Feed::isArticlesLoaded() const { return d
->articlesLoaded
; }
396 QDomElement
Feed::toOPML( QDomElement parent
, QDomDocument document
) const
398 QDomElement el
= document
.createElement( "outline" );
399 el
.setAttribute( "text", title() );
400 el
.setAttribute( "title", title() );
401 el
.setAttribute( "xmlUrl", d
->xmlUrl
);
402 el
.setAttribute( "htmlUrl", d
->htmlUrl
);
403 el
.setAttribute( "id", QString::number(id()) );
404 el
.setAttribute( "description", d
->description
);
405 el
.setAttribute( "useCustomFetchInterval", (useCustomFetchInterval() ? "true" : "false") );
406 el
.setAttribute( "fetchInterval", QString::number(fetchInterval()) );
407 el
.setAttribute( "archiveMode", archiveModeToString(d
->archiveMode
) );
408 el
.setAttribute( "maxArticleAge", d
->maxArticleAge
);
409 el
.setAttribute( "maxArticleNumber", d
->maxArticleNumber
);
410 if (d
->markImmediatelyAsRead
)
411 el
.setAttribute( "markImmediatelyAsRead", "true" );
412 if (d
->useNotification
)
413 el
.setAttribute( "useNotification", "true" );
414 if (d
->loadLinkedWebsite
)
415 el
.setAttribute( "loadLinkedWebsite", "true" );
416 el
.setAttribute( "maxArticleNumber", d
->maxArticleNumber
);
417 el
.setAttribute( "type", "rss" ); // despite some additional fields, it is still "rss" OPML
418 el
.setAttribute( "version", "RSS" );
419 parent
.appendChild( el
);
423 KJob
* Feed::createMarkAsReadJob()
425 std::auto_ptr
<ArticleModifyJob
> job( new ArticleModifyJob
);
426 Q_FOREACH ( const Article
& i
, articles() )
428 const ArticleId aid
= { xmlUrl(), i
.guid() };
429 job
->setStatus( aid
, Read
);
431 return job
.release();
434 void Feed::slotAddToFetchQueue(FetchQueue
* queue
, bool intervalFetchOnly
)
436 if (!intervalFetchOnly
)
437 queue
->addFeed(this);
442 if (useCustomFetchInterval() )
443 interval
= fetchInterval() * 60;
445 if ( Settings::useIntervalFetch() )
446 interval
= Settings::autoFetchInterval() * 60;
448 uint lastFetch
= d
->archive
->lastFetch();
450 uint now
= QDateTime::currentDateTime().toTime_t();
452 if ( interval
> 0 && now
- lastFetch
>= (uint
)interval
)
453 queue
->addFeed(this);
457 void Feed::slotAddFeedIconListener()
459 FeedIconManager::self()->addListener( KUrl( d
->xmlUrl
), this );
462 void Feed::appendArticles(const Syndication::FeedPtr feed
)
464 d
->setTotalCountDirty();
465 bool changed
= false;
466 const bool notify
= useNotification() || Settings::useNotifications();
468 QList
<ItemPtr
> items
= feed
->items();
469 QList
<ItemPtr
>::ConstIterator it
= items
.constBegin();
470 QList
<ItemPtr
>::ConstIterator en
= items
.constEnd();
475 QList
<Article
> deletedArticles
= d
->deletedArticles
;
477 for ( ; it
!= en
; ++it
)
479 if ( !d
->articles
.contains((*it
)->id()) ) // article not in list
481 Article
mya(*it
, this);
482 mya
.offsetPubDate(nudge
);
485 d
->addedArticlesNotify
.append(mya
);
487 if (!mya
.isDeleted() && !markImmediatelyAsRead())
492 NotificationManager::self()->slotNotifyArticle( mya
);
495 else // article is in list
497 // 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.
498 Article old
= d
->articles
[(*it
)->id()];
499 Article
mya(*it
, this);
500 if (!mya
.guidIsHash() && mya
.hash() != old
.hash() && !old
.isDeleted())
502 mya
.setKeep(old
.keep());
503 int oldstatus
= old
.status();
506 d
->articles
.remove(old
.guid());
509 mya
.setStatus(oldstatus
);
511 d
->updatedArticlesNotify
.append(mya
);
514 else if (old
.isDeleted())
515 deletedArticles
.removeAll(mya
);
520 QList
<Article
>::ConstIterator dit
= deletedArticles
.constBegin();
521 QList
<Article
>::ConstIterator dtmp
;
522 QList
<Article
>::ConstIterator den
= deletedArticles
.constEnd();
524 // delete articles with delete flag set completely from archive, which aren't in the current feed source anymore
529 d
->articles
.remove((*dtmp
).guid());
530 d
->archive
->deleteArticle((*dtmp
).guid());
531 d
->removedArticlesNotify
.append( *dtmp
);
533 d
->deletedArticles
.removeAll(*dtmp
);
540 bool Feed::usesExpiryByAge() const
542 return ( d
->archiveMode
== globalDefault
&& Settings::archiveMode() == Settings::EnumArchiveMode::limitArticleAge
) || d
->archiveMode
== limitArticleAge
;
545 bool Feed::isExpired(const Article
& a
) const
547 QDateTime now
= QDateTime::currentDateTime();
549 // check whether the feed uses the global default and the default is limitArticleAge
550 if ( d
->archiveMode
== globalDefault
&& Settings::archiveMode() == Settings::EnumArchiveMode::limitArticleAge
)
551 expiryAge
= Settings::maxArticleAge() *24*3600;
552 else // otherwise check if this feed has limitArticleAge set
553 if ( d
->archiveMode
== limitArticleAge
)
554 expiryAge
= d
->maxArticleAge
*24*3600;
556 return ( expiryAge
!= -1 && a
.pubDate().secsTo(now
) > expiryAge
);
559 void Feed::appendArticle(const Article
& a
)
561 if ( (a
.keep() && Settings::doNotExpireImportantArticles()) || ( !usesExpiryByAge() || !isExpired(a
) ) ) // if not expired
563 if (!d
->articles
.contains(a
.guid()))
565 d
->articles
[a
.guid()] = a
;
566 if (!a
.isDeleted() && a
.status() != Read
)
567 setUnread(unread()+1);
573 void Feed::fetch(bool followDiscovery
)
575 d
->followDiscovery
= followDiscovery
;
578 // mark all new as unread
579 QList
<Article
> articles
= d
->articles
.values();
580 QList
<Article
>::Iterator it
;
581 QList
<Article
>::Iterator en
= articles
.end();
582 for (it
= articles
.begin(); it
!= en
; ++it
)
584 if ((*it
).status() == New
)
586 (*it
).setStatus(Unread
);
590 emit
fetchStarted(this);
595 void Feed::slotAbortFetch()
603 void Feed::tryFetch()
605 d
->fetchErrorCode
= Syndication::Success
;
607 d
->loader
= Syndication::Loader::create( this, SLOT(fetchCompleted(Syndication::Loader
*,
608 Syndication::FeedPtr
,
609 Syndication::ErrorCode
)) );
610 //connect(d->loader, SIGNAL(progress(unsigned long)), this, SLOT(slotSetProgress(unsigned long)));
611 d
->loader
->loadFrom( d
->xmlUrl
);
614 void Feed::slotImageFetched(const QPixmap
& image
)
619 void Feed::fetchCompleted(Syndication::Loader
*l
, Syndication::FeedPtr doc
, Syndication::ErrorCode status
)
621 // Note that loader instances delete themselves
624 // fetching wasn't successful:
625 if (status
!= Syndication::Success
)
627 if (status
== Syndication::Aborted
)
629 d
->fetchErrorCode
= Syndication::Success
;
630 emit
fetchAborted(this);
632 else if (d
->followDiscovery
&& (status
== Syndication::InvalidXml
) && (d
->fetchTries
< 3) && (l
->discoveredFeedURL().isValid()))
635 d
->xmlUrl
= l
->discoveredFeedURL().url();
636 emit
fetchDiscovery(this);
641 d
->fetchErrorCode
= status
;
642 emit
fetchError(this);
648 loadArticles(); // TODO: make me fly: make this delayed
650 FeedIconManager::self()->addListener( KUrl( xmlUrl() ), this );
652 d
->fetchErrorCode
= Syndication::Success
;
654 if (d
->imagePixmap
.isNull())
656 QString u
= d
->xmlUrl
;
657 QString imageFileName
= KGlobal::dirs()->saveLocation("cache", "akregator/Media/")
658 + Utils::fileNameForUrl(d
->xmlUrl
) + ".png";
659 d
->imagePixmap
=QPixmap(imageFileName
, "PNG");
661 // if we ain't got the image and the feed provides one, get it....
662 // TODO: reenable image fetching!
663 if (false) // d->imagePixmap.isNull() && doc.image())
665 //d->image = *doc.image();
666 //connect(&d->image, SIGNAL(gotPixmap(const QPixmap&)), this, SLOT(slotImageFetched(const QPixmap&)));
667 //d->image.getPixmap();
671 if (title().isEmpty())
672 setTitle( Syndication::htmlToPlainText( doc
->title() ) );
674 d
->description
= doc
->description();
675 d
->htmlUrl
= doc
->link();
683 void Feed::markAsFetchedNow()
686 d
->archive
->setLastFetch( QDateTime::currentDateTime().toTime_t());
689 QIcon
Feed::icon() const
691 if ( fetchErrorOccurred() )
692 return KIcon("dialog-error");
694 return !d
->favicon
.isNull() ? d
->favicon
: KIcon("text-html");
697 void Feed::deleteExpiredArticles( ArticleDeleteJob
* deleteJob
)
699 if ( !usesExpiryByAge() )
702 setNotificationMode(false);
704 const QList
<Article
> articles
= d
->articles
.values();
705 QList
<ArticleId
> toDelete
;
706 const QString feedUrl
= xmlUrl();
707 const bool useKeep
= Settings::doNotExpireImportantArticles();
709 Q_FOREACH ( const Article
& i
, articles
)
711 if ( ( !useKeep
|| !i
.keep() ) && isExpired( i
) )
713 const ArticleId aid
= { feedUrl
, i
.guid() };
714 toDelete
.append( aid
);
718 deleteJob
->appendArticleIds( toDelete
);
719 setNotificationMode(true);
722 void Feed::setFavicon( const QIcon
& icon
)
728 void Feed::setImage(const QPixmap
&p
)
733 d
->imagePixmap
.save(KGlobal::dirs()->saveLocation("cache", "akregator/Media/")+ Utils::fileNameForUrl(d
->xmlUrl
) + ".png","PNG");
737 Feed::ArchiveMode
Feed::archiveMode() const
739 return d
->archiveMode
;
742 void Feed::setArchiveMode(ArchiveMode archiveMode
)
744 d
->archiveMode
= archiveMode
;
747 int Feed::unread() const
749 return d
->archive
? d
->archive
->unread() : 0;
752 void Feed::setUnread(int unread
)
754 if (d
->archive
&& unread
!= d
->archive
->unread())
756 d
->archive
->setUnread(unread
);
762 void Feed::setArticleDeleted(Article
& a
)
764 d
->setTotalCountDirty();
765 if (!d
->deletedArticles
.contains(a
))
766 d
->deletedArticles
.append(a
);
768 d
->updatedArticlesNotify
.append(a
);
772 void Feed::setArticleChanged(Article
& a
, int oldStatus
)
776 int newStatus
= a
.status();
777 if (oldStatus
== Read
&& newStatus
!= Read
)
778 setUnread(unread()+1);
779 else if (oldStatus
!= Read
&& newStatus
== Read
)
780 setUnread(unread()-1);
782 d
->updatedArticlesNotify
.append(a
);
786 int Feed::totalCount() const
788 if ( d
->totalCount
== -1 )
789 d
->totalCount
= std::count_if( d
->articles
.constBegin(), d
->articles
.constEnd(), !bind( &Article::isDeleted
, _1
) );
790 return d
->totalCount
;
793 TreeNode
* Feed::next()
796 return nextSibling();
798 Folder
* p
= parent();
801 if ( p
->nextSibling() )
802 return p
->nextSibling();
810 const TreeNode
* Feed::next() const
813 return nextSibling();
815 const Folder
* p
= parent();
818 if ( p
->nextSibling() )
819 return p
->nextSibling();
826 void Feed::doArticleNotification()
828 if (!d
->addedArticlesNotify
.isEmpty())
830 // copy list, otherwise the refcounting in Article::Private breaks for
831 // some reason (causing segfaults)
832 QList
<Article
> l
= d
->addedArticlesNotify
;
833 emit
signalArticlesAdded(this, l
);
834 d
->addedArticlesNotify
.clear();
836 if (!d
->updatedArticlesNotify
.isEmpty())
838 // copy list, otherwise the refcounting in Article::Private breaks for
839 // some reason (causing segfaults)
840 QList
<Article
> l
= d
->updatedArticlesNotify
;
841 emit
signalArticlesUpdated(this, l
);
842 d
->updatedArticlesNotify
.clear();
844 if (!d
->removedArticlesNotify
.isEmpty())
846 // copy list, otherwise the refcounting in Article::Private breaks for
847 // some reason (causing segfaults)
848 QList
<Article
> l
= d
->removedArticlesNotify
;
849 emit
signalArticlesRemoved(this, l
);
850 d
->removedArticlesNotify
.clear();
852 TreeNode::doArticleNotification();
855 void Feed::enforceLimitArticleNumber()
858 if (d
->archiveMode
== globalDefault
&& Settings::archiveMode() == Settings::EnumArchiveMode::limitArticleNumber
)
859 limit
= Settings::maxArticleNumber();
860 else if (d
->archiveMode
== limitArticleNumber
)
861 limit
= maxArticleNumber();
863 if (limit
== -1 || limit
>= d
->articles
.count() - d
->deletedArticles
.count())
866 QList
<Article
> articles
= d
->articles
.values();
870 const bool useKeep
= Settings::doNotExpireImportantArticles();
872 Q_FOREACH ( Article i
, articles
)
876 if ( !i
.isDeleted() && ( !useKeep
|| !i
.keep() ) )
879 else if ( !useKeep
|| !i
.keep() )