1 /*
2  *   SPDX-FileCopyrightText: 2007 Tobias Koenig <tokoe@kde.org>
3  *
4  *   SPDX-License-Identifier: GPL-2.0-or-later
5  */
6 
7 #include "potd.h"
8 
9 #include <QDate>
10 #include <QDebug>
11 #include <QFile>
12 #include <QFileInfo>
13 #include <QRegularExpression>
14 #include <QThreadPool>
15 #include <QTimer>
16 
17 #include <KPluginLoader>
18 #include <KPluginMetaData>
19 #include <Plasma/DataContainer>
20 
21 #include "cachedprovider.h"
22 
23 namespace
24 {
25 namespace DataKeys
26 {
image()27 inline QString image()
28 {
29     return QStringLiteral("Image");
30 }
url()31 inline QString url()
32 {
33     return QStringLiteral("Url");
34 }
35 }
36 }
37 
PotdEngine(QObject * parent,const QVariantList & args)38 PotdEngine::PotdEngine(QObject *parent, const QVariantList &args)
39     : Plasma::DataEngine(parent, args)
40 {
41     // set polling to every 5 minutes
42     setMinimumPollingInterval(5 * 60 * 1000);
43     m_checkDatesTimer = new QTimer(this); // change picture after 24 hours
44     connect(m_checkDatesTimer, &QTimer::timeout, this, &PotdEngine::checkDayChanged);
45     // FIXME: would be nice to stop and start this timer ONLY as needed, e.g. only when there are
46     // time insensitive sources to serve; still, this is better than how i found it, checking
47     // every 2 seconds (!)
48     m_checkDatesTimer->setInterval(10 * 60 * 1000); // check every 10 minutes
49     m_checkDatesTimer->start();
50 
51     const QVector<KPluginMetaData> plugins = KPluginLoader::findPlugins(QStringLiteral("potd"));
52 
53     for (const auto &metadata : plugins) {
54         QString provider = metadata.value(QLatin1String("X-KDE-PlasmaPoTDProvider-Identifier"));
55         if (provider.isEmpty()) {
56             continue;
57         }
58         mFactories.insert(provider, metadata);
59         setData(QLatin1String("Providers"), provider, metadata.name());
60     }
61 }
62 
~PotdEngine()63 PotdEngine::~PotdEngine()
64 {
65 }
66 
updateSourceEvent(const QString & identifier)67 bool PotdEngine::updateSourceEvent(const QString &identifier)
68 {
69     return updateSource(identifier, false);
70 }
71 
updateSource(const QString & identifier,bool loadCachedAlways)72 bool PotdEngine::updateSource(const QString &identifier, bool loadCachedAlways)
73 {
74     // check whether it is cached already...
75     if (CachedProvider::isCached(identifier, loadCachedAlways)) {
76         QVariantList args;
77         args << QLatin1String("String") << identifier;
78 
79         CachedProvider *provider = new CachedProvider(identifier, this);
80         connect(provider, &PotdProvider::finished, this, &PotdEngine::finished);
81         connect(provider, &PotdProvider::error, this, &PotdEngine::error);
82 
83         m_canDiscardCache = loadCachedAlways;
84         if (!loadCachedAlways) {
85             return true;
86         }
87     }
88 
89     const QStringList parts = identifier.split(QLatin1Char(':'), Qt::SkipEmptyParts);
90     if (parts.empty()) {
91         qDebug() << "invalid identifier";
92         return false;
93     }
94     const QString providerName = parts[0];
95     if (!mFactories.contains(providerName)) {
96         qDebug() << "invalid provider: " << parts[0];
97         return false;
98     }
99 
100     QVariantList args;
101 
102     for (int i = 0; i < parts.count(); i++) {
103         args << parts[i];
104     }
105 
106     auto factory = KPluginLoader(mFactories[providerName].fileName()).factory();
107     PotdProvider *provider = nullptr;
108     if (factory) {
109         provider = factory->create<PotdProvider>(this, args);
110     }
111     if (provider) {
112         connect(provider, &PotdProvider::finished, this, &PotdEngine::finished);
113         connect(provider, &PotdProvider::error, this, &PotdEngine::error);
114         return true;
115     }
116 
117     return false;
118 }
119 
sourceRequestEvent(const QString & identifier)120 bool PotdEngine::sourceRequestEvent(const QString &identifier)
121 {
122     if (updateSource(identifier, true)) {
123         setData(identifier, DataKeys::image(), QImage());
124         return true;
125     }
126 
127     return false;
128 }
129 
finished(PotdProvider * provider)130 void PotdEngine::finished(PotdProvider *provider)
131 {
132     if (m_canDiscardCache && qobject_cast<CachedProvider *>(provider)) {
133         Plasma::DataContainer *source = containerForSource(provider->identifier());
134         if (source && !source->data().value(DataKeys::image()).value<QImage>().isNull()) {
135             provider->deleteLater();
136             return;
137         }
138     }
139 
140     QImage img(provider->image());
141     // store in cache if it's not the response of a CachedProvider
142     if (qobject_cast<CachedProvider *>(provider) == nullptr && !img.isNull()) {
143         SaveImageThread *thread = new SaveImageThread(provider->identifier(), img);
144         connect(thread, &SaveImageThread::done, this, &PotdEngine::cachingFinished);
145         QThreadPool::globalInstance()->start(thread);
146     } else {
147         setData(provider->identifier(), DataKeys::image(), img);
148         setData(provider->identifier(), DataKeys::url(), CachedProvider::identifierToPath(provider->identifier()));
149     }
150 
151     provider->deleteLater();
152 }
153 
cachingFinished(const QString & source,const QString & path,const QImage & img)154 void PotdEngine::cachingFinished(const QString &source, const QString &path, const QImage &img)
155 {
156     setData(source, DataKeys::image(), img);
157     setData(source, DataKeys::url(), path);
158 }
159 
error(PotdProvider * provider)160 void PotdEngine::error(PotdProvider *provider)
161 {
162     provider->disconnect(this);
163     provider->deleteLater();
164 }
165 
checkDayChanged()166 void PotdEngine::checkDayChanged()
167 {
168     SourceDict dict = containerDict();
169     QHashIterator<QString, Plasma::DataContainer *> it(dict);
170     QRegularExpression re(QLatin1String(":\\d{4}-\\d{2}-\\d{2}"));
171 
172     while (it.hasNext()) {
173         it.next();
174 
175         if (it.key() == QLatin1String("Providers")) {
176             continue;
177         }
178 
179         // Check if the identifier contains ISO date string, like 2019-01-09.
180         // If so, don't update the picture. Otherwise, update the picture.
181         if (!re.match(it.key()).hasMatch()) {
182             const QString path = CachedProvider::identifierToPath(it.key());
183             if (!QFile::exists(path)) {
184                 updateSourceEvent(it.key());
185             } else {
186                 QFileInfo info(path);
187                 if (info.lastModified().daysTo(QDateTime::currentDateTime()) >= 1) {
188                     updateSourceEvent(it.key());
189                 }
190             }
191         }
192     }
193 }
194 
195 K_PLUGIN_CLASS_WITH_JSON(PotdEngine, "plasma-dataengine-potd.json")
196 
197 #include "potd.moc"
198