2
* Copyright (C) 2007 Aaron Seigo <aseigo@kde.org>
3
* Copyright (C) 2007 Petri Damsten <damu@iki.fi>
4
* Copyright (C) 2008 Rob Scheepmaker <r.scheepmaker@student.utwente.nl>
6
* This program is free software; you can redistribute it and/or modify
7
* it under the terms of the GNU Library General Public License version 2 as
8
* published by the Free Software Foundation
10
* This program is distributed in the hope that it will be useful,
11
* but WITHOUT ANY WARRANTY; without even the implied warranty of
12
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13
* GNU General Public License for more details
15
* You should have received a copy of the GNU Library General Public
16
* License along with this program; if not, write to the
17
* Free Software Foundation, Inc.,
18
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
27
#include <KStandardDirs>
28
#include <Solid/Networking>
29
#include <syndication/item.h>
30
#include <syndication/loader.h>
31
#include <syndication/image.h>
32
#include <syndication/person.h>
35
#include <Plasma/DataEngine>
40
#include <QDBusInterface>
42
#include <QSignalMapper>
44
#define TIMEOUT 15000 //timeout before updating the source if not all feeds
46
#define CACHE_TIMEOUT 60 //time in seconds before the cached feeds are marked
48
#define MINIMUM_INTERVAL 60000
49
#define FAVICONINTERFACE "org.kde.FavIcon"
51
RssEngine::RssEngine(QObject* parent, const QVariantList& args)
52
: Plasma::DataEngine(parent, args),
56
setMinimumPollingInterval(MINIMUM_INTERVAL);
57
m_favIconsModule = new QDBusInterface("org.kde.kded", "/modules/favicons",
59
m_signalMapper = new QSignalMapper(this);
60
connect(m_favIconsModule, SIGNAL(iconChanged(bool,QString,QString)),
61
this, SLOT(slotIconChanged(bool,QString,QString)));
62
connect(m_signalMapper, SIGNAL(mapped(const QString &)),
63
this, SLOT(timeout(const QString &)));
64
connect(Solid::Networking::notifier(), SIGNAL(statusChanged(Solid::Networking::Status)),
65
SLOT(networkStatusChanged(Solid::Networking::Status)));
69
RssEngine::~RssEngine()
71
delete m_favIconsModule;
74
void RssEngine::networkStatusChanged(Solid::Networking::Status status)
76
if (status == Solid::Networking::Connected || status == Solid::Networking::Unknown) {
77
kDebug() << "network connected, force refreshing feeds in 3 seconds";
78
// The forced update needs to happen after the new feeds are in,
79
// so remember to force the update in processRss()
81
// start updating the feeds
82
foreach(const QString &feedUrl, sources()) {
83
updateSourceEvent(feedUrl);
88
bool RssEngine::updateSourceEvent(const QString &name)
90
/* Plasmoids using this engine should be able to retrieve
91
* multiple feeds at the same time, so we allow a comma
92
* separated list of url's
94
// NOTE: A comma separated list of feeds is not url compliant. Urls
95
// may and do contain commas see http://www.spiegel.de/schlagzeilen/rss/0,5291,,00.xml
96
// I have changed it to something more not url compliant " " three dots
97
// Otherwise take a list instead
98
const QStringList sources = name.split(' ', QString::SkipEmptyParts);
100
foreach (const QString& source, sources) {
101
setStorageEnabled(source, true);
102
// Let's first see if we've got a recent cached version of
103
// the feed. This avoids 'large' amounts of unnecessary network
105
if (QDateTime::currentDateTime() >
106
m_feedTimes[source.toLower()].addSecs(CACHE_TIMEOUT)){
107
kDebug() << "Cache from " << source <<
108
" older than 60 seconds, refreshing...";
110
Syndication::Loader * loader = Syndication::Loader::create();
111
connect(loader, SIGNAL(loadingComplete(Syndication::Loader*,
112
Syndication::FeedPtr,
113
Syndication::ErrorCode)),
114
this, SLOT(processRss(Syndication::Loader*,
115
Syndication::FeedPtr,
116
Syndication::ErrorCode)));
118
m_feedMap.insert(loader, source);
119
m_sourceMap.insert(loader, name);
120
loader->loadFrom(source);
122
kDebug() << "Recent cached version of " << source <<
123
" found. Skipping...";
125
// We might want to update the source:
126
if (cachesUpToDate(name)) {
127
updateFeeds(name, m_feedTitles[ source ] );
132
QTimer *timer = new QTimer(this);
133
m_timerMap[name] = timer;
134
timer->setSingleShot(true);
135
m_signalMapper->setMapping(timer, name);
137
connect(timer, SIGNAL(timeout()), m_signalMapper, SLOT(map()));
139
timer->start(TIMEOUT);
143
void RssEngine::slotIconChanged(bool isHost, const QString& hostOrURL,
144
const QString& iconName)
147
const QString iconFile = KGlobal::dirs()->findResource("cache",
149
const QString url = hostOrURL.toLower();
151
m_feedIcons[url] = iconFile;
152
QMap<QString, QVariant> map;
154
for (int i = 0; i < m_feedItems[url].size(); i++) {
155
map = m_feedItems[url].at(i).toMap();
156
map["icon"] = iconFile;
157
m_feedItems[url].replace(i, map);
160
//Are there sources ready to get updated now?
161
foreach (const QString& source, m_sourceMap) {
162
if (source.contains(url, Qt::CaseInsensitive) &&
163
cachesUpToDate(source)) {
164
kDebug() << "all caches from source " << source <<
165
" up to date, updating...";
166
updateFeeds(source, m_feedTitles[ source ] );
171
void RssEngine::timeout(const QString & source)
173
kDebug() << "timout fired, updating source";
174
updateFeeds(source, m_feedTitles[ source ] );
175
m_signalMapper->removeMappings(m_timerMap[source]);
178
bool RssEngine::sourceRequestEvent(const QString &name)
180
setData(name, DataEngine::Data());
181
updateSourceEvent(name);
185
void RssEngine::processRss(Syndication::Loader* loader,
186
Syndication::FeedPtr feed,
187
Syndication::ErrorCode error)
189
const QString url = m_feedMap.take(loader);
190
const QString source = m_sourceMap.take(loader);
192
bool iconRequested = false;
195
if (error != Syndication::Success) {
196
kDebug() << "Syndication did not work out... url = " << url;
197
title = i18n("Syndication did not work out");
198
setData(source, "title", i18n("Fetching feed failed."));
199
setData(source, "link", url);
201
title = feed->title();
205
foreach (const Syndication::ItemPtr& item, feed->items()) {
206
QMap<QString, QVariant> dataItem;
208
//some malformed rss feeds can have empty entries
209
if (item->title().isNull() && item->description().isNull() && dataItem["content"].isNull()) {
213
dataItem["title"] = item->title();
214
dataItem["feed_title"] = feed->title();
215
dataItem["link"] = item->link();
216
dataItem["feed_url"] = url;
217
dataItem["description"] = item->description();
218
dataItem["content"] = item->content();
219
dataItem["time"] = (uint)item->datePublished();
220
if (!m_feedIcons.contains(url.toLower()) && !iconRequested) {
221
//lets request an icon, and only do this once per feed.
222
location = iconLocation(u);
223
if (location.isEmpty()) {
224
m_favIconsModule->call( "downloadHostIcon", u.url() );
226
//the icon is already in cache, so call this slot manually.
227
slotIconChanged(false, u.url(), iconLocation(u));
229
iconRequested = true;
231
dataItem["icon"] = m_feedIcons[url.toLower()];
233
foreach (const boost::shared_ptr<Syndication::Person> a, item->authors()) {
234
authors << a->name();
236
dataItem["author"] = authors;
238
items.append(dataItem);
241
if (!m_rssSourceNames.contains(url)) {
242
QMap<QString, QVariant> sourceItem;
244
sourceItem["feed_title"] = feed->title();
245
sourceItem["feed_url"] = url;
246
sourceItem["icon"] = m_feedIcons[url.toLower()];
248
m_rssSources.append(sourceItem);
249
m_rssSourceNames.insert(url);
252
m_feedItems[url.toLower()] = items;
253
m_feedTimes[url.toLower()] = QDateTime::currentDateTime();
254
m_feedTitles[url.toLower()] = title;
256
// If we update the feeds every time a feed is fetched,
257
// only the first update will actually update a connected
258
// applet, which is actually sane, since plasma updates
259
// only one time each interval. This means, however, that
260
// we maybe want to delay updating the feeds untill either
261
// timeout, or all feeds are up to date.
262
if (cachesUpToDate(source)) {
263
kDebug() << "all caches from source " << source
264
<< " up to date, updating...";
265
updateFeeds(source, title);
267
// Should be used with care ...
268
forceImmediateUpdateOfAllVisualizations();
269
m_forceUpdate = false;
270
// and skip scheduleSourcesUpdated(), since we
271
// force a repaint anyway already
275
kDebug() << "not all caches from source " << source
276
<< ", delaying update.";
278
scheduleSourcesUpdated();
282
void RssEngine::updateFeeds(const QString & source, const QString & title)
285
* TODO: can this be improved? I'm calling mergeFeeds way too
288
const QVariantList list = mergeFeeds(source);
289
setData(source, "items", list);
291
setData(source, "sources", m_rssSources);
292
const QStringList sourceNames = source.split(' ', QString::SkipEmptyParts);
293
if (sourceNames.size() > 1) {
294
setData(source, "title", i18np("1 RSS feed fetched",
295
"%1 RSS feeds fetched", sourceNames.size()));
297
setData(source, "title", title);
301
bool RssEngine::cachesUpToDate(const QString & source) const
303
const QStringList sources = source.split(' ', QString::SkipEmptyParts);
304
bool outOfDate = false;
305
foreach (const QString &url, sources) {
306
if (QDateTime::currentDateTime() >
307
m_feedTimes[url.toLower()].addSecs(CACHE_TIMEOUT)){
310
if (!m_feedIcons.contains(url.toLower())) {
317
bool compare(const QVariant &v1, const QVariant &v2)
319
return v1.toMap()["time"].toUInt() > v2.toMap()["time"].toUInt();
322
QVariantList RssEngine::mergeFeeds(QString source) const
325
const QStringList sources = source.split(' ', QString::SkipEmptyParts);
327
foreach (const QString& feed, sources) {
328
result += m_feedItems[feed.toLower()];
331
qSort(result.begin(), result.end(), compare);
335
QString RssEngine::iconLocation(const KUrl & url) const
337
QDBusReply<QString> reply = m_favIconsModule->call( "iconForUrl", url.url() );
338
if (reply.isValid()) {
339
QString result = reply;