1 /*
2 This file is part of KOrganizer.
3 SPDX-FileCopyrightText: 2001 Cornelius Schumacher <schumacher@kde.org>
4 SPDX-FileCopyrightText: 2007 Loïc Corbasson <loic.corbasson@gmail.com>
5 SPDX-FileCopyrightText: 2021 Friedrich W. H. Kossebau <kossebau@kde.org>
6
7 SPDX-License-Identifier: GPL-2.0-or-later
8 */
9
10 #include "element.h"
11 #include "picoftheday.h"
12
13 #include "korganizer_picoftheday_plugin_debug.h"
14
15 #include <KIO/Scheduler>
16 #include <KIO/StoredTransferJob>
17 #include <KLocalizedString>
18
19 #include <QJsonArray>
20 #include <QJsonDocument>
21 #include <QUrlQuery>
22
23 #include <chrono>
24
25 using namespace std::chrono_literals;
26
27 constexpr auto updateDelay = 1s;
28
updateFetchedThumbSize()29 void ElementData::updateFetchedThumbSize()
30 {
31 int thumbWidth = mThumbSize.width();
32 int thumbHeight = static_cast<int>(thumbWidth * mPictureHWRatio);
33 if (mThumbSize.height() < thumbHeight) {
34 /* if the requested height is less than the requested width * ratio
35 we would download too much, as the downloaded picture would be
36 taller than requested, so we adjust the width of the picture to
37 be downloaded in consequence */
38 thumbWidth /= (thumbHeight / static_cast<float>(mThumbSize.height()));
39 thumbHeight = static_cast<int>(thumbWidth * mPictureHWRatio);
40 }
41 mFetchedThumbSize = QSize(thumbWidth, thumbHeight);
42 }
43
POTDElement(const QString & id,QDate date,ElementData * data)44 POTDElement::POTDElement(const QString &id, QDate date, ElementData *data)
45 : Element(id)
46 , mDate(date)
47 , mData(data)
48 , mThumbImageGetDelayTimer(new QTimer(this))
49 {
50 mThumbImageGetDelayTimer->setSingleShot(true);
51 mThumbImageGetDelayTimer->setInterval(updateDelay);
52 connect(mThumbImageGetDelayTimer, &QTimer::timeout, this, &POTDElement::queryThumbImageInfoJson);
53
54 // wait a bit to avoid data queries in case of quick paging through views
55 QTimer::singleShot(updateDelay, this, &POTDElement::completeMissingData);
56 }
57
~POTDElement()58 POTDElement::~POTDElement()
59 {
60 // reset thumb update state
61 if (mData->mState > DataLoaded) {
62 mData->mState = DataLoaded;
63 }
64 Picoftheday::cacheData(mDate, mData);
65 }
66
completeMissingData()67 void POTDElement::completeMissingData()
68 {
69 if (mData->mState <= NeedingPageData) {
70 queryImagesJson();
71 } else if (mData->mState <= NeedingBasicImageInfo) {
72 queryBasicImageInfoJson();
73 } else if (mData->mState <= NeedingFirstThumbImage) {
74 queryThumbImageInfoJson();
75 }
76 }
77
createJsonQueryJob(const QString & property,const QString & title,const QList<QueryItem> & otherQueryItems)78 KIO::SimpleJob *POTDElement::createJsonQueryJob(const QString &property, const QString &title, const QList<QueryItem> &otherQueryItems)
79 {
80 QUrl url(QStringLiteral("https://en.wikipedia.org/w/api.php"));
81
82 QUrlQuery urlQuery{
83 {QStringLiteral("action"), QStringLiteral("query")},
84 {QStringLiteral("format"), QStringLiteral("json")},
85 {QStringLiteral("prop"), property},
86 {QStringLiteral("titles"), title},
87 };
88 for (const auto &item : otherQueryItems) {
89 urlQuery.addQueryItem(item.key, item.value);
90 }
91 url.setQuery(urlQuery);
92
93 auto job = KIO::storedGet(url, KIO::NoReload, KIO::HideProgressInfo);
94 KIO::Scheduler::setJobPriority(job, 1);
95
96 return job;
97 }
98
createImagesJsonQueryJob(PageProtectionState state)99 KIO::SimpleJob *POTDElement::createImagesJsonQueryJob(PageProtectionState state)
100 {
101 const char *const templatePagePrefix = (state == ProtectedPage) ? "Template:POTD_protected/" : "Template:POTD/";
102 const QString templatePageName = QLatin1String(templatePagePrefix) + mDate.toString(Qt::ISODate);
103 const QList<QueryItem> otherQueryItems{
104 // TODO: unsure if formatversion is needed, used by https://www.mediawiki.org/wiki/API:Picture_of_the_day_viewer in October 2021
105 {QStringLiteral("formatversion"), QStringLiteral("2")},
106 };
107
108 return createJsonQueryJob(QStringLiteral("images"), templatePageName, otherQueryItems);
109 }
110
queryImagesJson()111 void POTDElement::queryImagesJson()
112 {
113 auto queryImagesJob = createImagesJsonQueryJob(ProtectedPage);
114
115 connect(queryImagesJob, &KIO::SimpleJob::result, this, &POTDElement::handleProtectedImagesJsonResponse);
116 }
117
handleImagesJsonResponse(KJob * job,PageProtectionState pageProtectionState)118 void POTDElement::handleImagesJsonResponse(KJob *job, PageProtectionState pageProtectionState)
119 {
120 if (job->error()) {
121 qCWarning(KORGANIZERPICOFTHEDAYPLUGIN_LOG) << mDate << ": could not get POTD file name:" << job->errorString();
122 setLoadingFailed();
123 return;
124 }
125
126 auto const transferJob = static_cast<KIO::StoredTransferJob *>(job);
127
128 const auto json = QJsonDocument::fromJson(transferJob->data());
129
130 const auto pageObject = json.object().value(QLatin1String("query")).toObject().value(QLatin1String("pages")).toArray().at(0).toObject();
131
132 auto missingIt = pageObject.find(QLatin1String("missing"));
133 if ((missingIt != pageObject.end()) && missingIt.value().toBool(false)) {
134 // fallback to unprotected variant in case there is no protected variant
135 if (pageProtectionState == ProtectedPage) {
136 qCDebug(KORGANIZERPICOFTHEDAYPLUGIN_LOG) << mDate << ": protected page reported as missing, trying unprocteded now.";
137 auto queryImagesJob = createImagesJsonQueryJob(UnprotectedPage);
138
139 connect(queryImagesJob, &KIO::SimpleJob::result, this, &POTDElement::handleUnprotectedImagesJsonResponse);
140 return;
141 }
142
143 // no POTD set
144 qCDebug(KORGANIZERPICOFTHEDAYPLUGIN_LOG) << mDate << ": also unprotected page reported as missing, Seems no POTD is declared.";
145 setLoadingFailed();
146 return;
147 }
148
149 const auto imageObject = pageObject.value(QLatin1String("images")).toArray().at(0).toObject();
150 const QString imageFile = imageObject.value(QLatin1String("title")).toString();
151 if (imageFile.isEmpty()) {
152 qCWarning(KORGANIZERPICOFTHEDAYPLUGIN_LOG) << mDate << ": missing images data in reply:" << json;
153 setLoadingFailed();
154 return;
155 }
156
157 // store data
158 mData->mPictureName = imageFile;
159 mData->mState = NeedingBasicImageInfo;
160
161 queryBasicImageInfoJson();
162 }
163
handleUnprotectedImagesJsonResponse(KJob * job)164 void POTDElement::handleUnprotectedImagesJsonResponse(KJob *job)
165 {
166 handleImagesJsonResponse(job, UnprotectedPage);
167 }
168
handleProtectedImagesJsonResponse(KJob * job)169 void POTDElement::handleProtectedImagesJsonResponse(KJob *job)
170 {
171 handleImagesJsonResponse(job, ProtectedPage);
172 }
173
queryBasicImageInfoJson()174 void POTDElement::queryBasicImageInfoJson()
175 {
176 const QList<QueryItem> otherQueryItems{
177 {QStringLiteral("iiprop"), QStringLiteral("url|size|canonicaltitle")},
178 };
179 auto queryBasicImageInfoJob = createJsonQueryJob(QStringLiteral("imageinfo"), mData->mPictureName, otherQueryItems);
180
181 connect(queryBasicImageInfoJob, &KIO::SimpleJob::result, this, &POTDElement::handleBasicImageInfoJsonResponse);
182 }
183
handleBasicImageInfoJsonResponse(KJob * job)184 void POTDElement::handleBasicImageInfoJsonResponse(KJob *job)
185 {
186 if (job->error()) {
187 qCWarning(KORGANIZERPICOFTHEDAYPLUGIN_LOG) << mDate << ": could not get POTD file name:" << job->errorString();
188 setLoadingFailed();
189 return;
190 }
191
192 auto const transferJob = static_cast<KIO::StoredTransferJob *>(job);
193
194 const auto json = QJsonDocument::fromJson(transferJob->data());
195
196 const auto pagesObject = json.object().value(QLatin1String("query")).toObject().value(QLatin1String("pages")).toObject();
197 const auto pageObject = pagesObject.isEmpty() ? QJsonObject() : pagesObject.begin()->toObject();
198 const auto imageInfo = pageObject.value(QLatin1String("imageinfo")).toArray().at(0).toObject();
199
200 const QString url = imageInfo.value(QLatin1String("url")).toString();
201 if (url.isEmpty()) {
202 qCWarning(KORGANIZERPICOFTHEDAYPLUGIN_LOG) << mDate << ": missing imageinfo data in reply:" << json;
203 setLoadingFailed();
204 return;
205 }
206
207 const QString descriptionUrl = imageInfo.value(QLatin1String("descriptionurl")).toString();
208 mData->mAboutPageUrl = QUrl(descriptionUrl);
209
210 const QString description = imageInfo.value(QLatin1String("canonicaltitle")).toString();
211 mData->mTitle = i18n("Wikipedia POTD: %1", description);
212
213 const int width = imageInfo.value(QLatin1String("width")).toInt();
214 const int height = imageInfo.value(QLatin1String("height")).toInt();
215 mData->mPictureHWRatio = ((width != 0) && (height != 0)) ? height / static_cast<float>(width) : 1.0;
216 qCDebug(KORGANIZERPICOFTHEDAYPLUGIN_LOG) << mDate << ": thumb width" << width << " thumb height" << height << "ratio" << mData->mPictureHWRatio;
217 mData->updateFetchedThumbSize();
218 mData->mState = NeedingFirstThumbImageInfo;
219
220 queryThumbImageInfoJson();
221 }
222
queryThumbImageInfoJson()223 void POTDElement::queryThumbImageInfoJson()
224 {
225 qCDebug(KORGANIZERPICOFTHEDAYPLUGIN_LOG) << mDate << ": thumb size" << mData->mThumbSize << " adapted size" << mData->mFetchedThumbSize;
226
227 const QList<QueryItem> otherQueryItems{
228 {QStringLiteral("iiprop"), QStringLiteral("url")},
229 {QStringLiteral("iiurlwidth"), QString::number(mData->mFetchedThumbSize.width())},
230 {QStringLiteral("iiurlheight"), QString::number(mData->mFetchedThumbSize.height())},
231 };
232 mQueryThumbImageInfoJob = createJsonQueryJob(QStringLiteral("imageinfo"), mData->mPictureName, otherQueryItems);
233
234 connect(mQueryThumbImageInfoJob, &KIO::SimpleJob::result, this, &POTDElement::handleThumbImageInfoJsonResponse);
235 }
236
handleThumbImageInfoJsonResponse(KJob * job)237 void POTDElement::handleThumbImageInfoJsonResponse(KJob *job)
238 {
239 mQueryThumbImageInfoJob = nullptr;
240
241 if (job->error()) {
242 qCWarning(KORGANIZERPICOFTHEDAYPLUGIN_LOG) << mDate << ": could not get thumb info:" << job->errorString();
243 if (mData->mState == NeedingFirstThumbImageInfo) {
244 setLoadingFailed();
245 }
246 return;
247 }
248
249 auto const transferJob = static_cast<KIO::StoredTransferJob *>(job);
250
251 const auto json = QJsonDocument::fromJson(transferJob->data());
252 auto pagesObject = json.object().value(QLatin1String("query")).toObject().value(QLatin1String("pages")).toObject();
253 auto pageObject = pagesObject.isEmpty() ? QJsonObject() : pagesObject.begin()->toObject();
254 auto imageInfo = pageObject.value(QLatin1String("imageinfo")).toArray().at(0).toObject();
255
256 const QString thumbUrl = imageInfo.value(QStringLiteral("thumburl")).toString();
257 if (thumbUrl.isEmpty()) {
258 qCWarning(KORGANIZERPICOFTHEDAYPLUGIN_LOG) << mDate << ": missing imageinfo data in reply:" << json;
259 return;
260 }
261
262 mData->mState = (mData->mState == NeedingFirstThumbImageInfo) ? NeedingFirstThumbImage : NeedingNextThumbImage;
263
264 getThumbImage(QUrl(thumbUrl));
265 }
266
getThumbImage(const QUrl & thumbUrl)267 void POTDElement::getThumbImage(const QUrl &thumbUrl)
268 {
269 if (mGetThumbImageJob) {
270 mGetThumbImageJob->kill();
271 }
272
273 qCDebug(KORGANIZERPICOFTHEDAYPLUGIN_LOG) << mDate << ": fetching POTD thumbnail:" << thumbUrl;
274
275 mGetThumbImageJob = KIO::storedGet(thumbUrl, KIO::NoReload, KIO::HideProgressInfo);
276 KIO::Scheduler::setJobPriority(mGetThumbImageJob, 1);
277
278 connect(mGetThumbImageJob, &KIO::SimpleJob::result, this, &POTDElement::handleGetThumbImageResponse);
279 }
280
handleGetThumbImageResponse(KJob * job)281 void POTDElement::handleGetThumbImageResponse(KJob *job)
282 {
283 mGetThumbImageJob = nullptr;
284
285 const bool isAboutFirstThumbImage = (mData->mState == NeedingFirstThumbImage);
286
287 if (job->error()) {
288 qCWarning(KORGANIZERPICOFTHEDAYPLUGIN_LOG) << mDate << ": could not get POTD thumb:" << job->errorString();
289 if (isAboutFirstThumbImage) {
290 setLoadingFailed();
291 }
292 return;
293 }
294
295 // Last step completed: we get the pixmap from the transfer job's data
296 auto const transferJob = static_cast<KIO::StoredTransferJob *>(job);
297 if (!mData->mThumbnail.loadFromData(transferJob->data())) {
298 qCWarning(KORGANIZERPICOFTHEDAYPLUGIN_LOG) << mDate << ": could not load POTD thumb data.";
299 if (isAboutFirstThumbImage) {
300 setLoadingFailed();
301 }
302 return;
303 }
304
305 mData->mState = DataLoaded;
306
307 if (isAboutFirstThumbImage) {
308 // update other properties
309 Q_EMIT gotNewShortText(shortText());
310 Q_EMIT gotNewLongText(mData->mTitle);
311 Q_EMIT gotNewUrl(mData->mAboutPageUrl);
312 }
313
314 if (!mRequestedThumbSize.isNull()) {
315 Q_EMIT gotNewPixmap(mData->mThumbnail.scaled(mRequestedThumbSize, Qt::KeepAspectRatio, Qt::SmoothTransformation));
316 }
317 }
318
setLoadingFailed()319 void POTDElement::setLoadingFailed()
320 {
321 mData->mState = LoadingFailed;
322
323 Q_EMIT gotNewShortText(QString());
324 Q_EMIT gotNewLongText(QString());
325 }
326
shortText() const327 QString POTDElement::shortText() const
328 {
329 return (mData->mState >= DataLoaded) ? i18n("Picture Page") : (mData->mState >= NeedingPageData) ? i18n("Loading...") : QString();
330 }
331
longText() const332 QString POTDElement::longText() const
333 {
334 return (mData->mState >= DataLoaded) ? mData->mTitle
335 : (mData->mState >= NeedingPageData) ? i18n("<qt>Loading <i>Picture of the Day</i>...</qt>")
336 : QString();
337 }
338
url() const339 QUrl POTDElement::url() const
340 {
341 return (mData->mState >= DataLoaded) ? mData->mAboutPageUrl : QUrl();
342 }
343
newPixmap(const QSize & size)344 QPixmap POTDElement::newPixmap(const QSize &size)
345 {
346 mRequestedThumbSize = size;
347
348 if ((mData->mThumbSize.width() < size.width()) || (mData->mThumbSize.height() < size.height())) {
349 qCDebug(KORGANIZERPICOFTHEDAYPLUGIN_LOG) << mDate << ": called for a new pixmap size (" << size << "instead of" << mData->mThumbSize
350 << ", stored pixmap:" << mData->mThumbnail.size() << ")";
351 mData->mThumbSize = size;
352
353 if (mData->mState >= NeedingFirstThumbImageInfo) {
354 mData->updateFetchedThumbSize();
355
356 if ((mData->mFetchedThumbSize.width() < size.width()) || (mData->mFetchedThumbSize.height() < size.height())) {
357 // only if there is already an initial pixmap to show at least something,
358 // kill current update and trigger new delayed update
359 if (mData->mState >= DataLoaded) {
360 if (mQueryThumbImageInfoJob) {
361 mQueryThumbImageInfoJob->kill();
362 mQueryThumbImageInfoJob = nullptr;
363 }
364 if (mGetThumbImageJob) {
365 mGetThumbImageJob->kill();
366 mGetThumbImageJob = nullptr;
367 }
368 mData->mState = NeedingNextThumbImageInfo;
369 }
370
371 // We start a new thumbnail download a little later; the following code
372 // is to avoid too frequent transfers e.g. when resizing
373 mThumbImageGetDelayTimer->start();
374 }
375 }
376 }
377
378 /* else, either we already got a sufficiently big pixmap (stored in mData->mThumbnail),
379 or we will get one anytime soon (we are downloading it already) and we will
380 actualize what we return here later via gotNewPixmap */
381 if (mData->mThumbnail.isNull()) {
382 return QPixmap();
383 }
384 return mData->mThumbnail.scaled(mRequestedThumbSize, Qt::KeepAspectRatio, Qt::SmoothTransformation);
385 }
386