File indexing completed on 2025-01-05 04:49:36

0001 /*
0002   This file is part of KOrganizer.
0003   SPDX-FileCopyrightText: 2001 Cornelius Schumacher <schumacher@kde.org>
0004   SPDX-FileCopyrightText: 2007 Loïc Corbasson <loic.corbasson@gmail.com>
0005   SPDX-FileCopyrightText: 2021 Friedrich W. H. Kossebau <kossebau@kde.org>
0006 
0007   SPDX-License-Identifier: GPL-2.0-or-later
0008 */
0009 
0010 #include "element.h"
0011 #include "picoftheday.h"
0012 
0013 #include "korganizer_picoftheday_plugin_debug.h"
0014 
0015 #include <KIO/StoredTransferJob>
0016 #include <KLocalizedString>
0017 
0018 #include <QJsonArray>
0019 #include <QJsonDocument>
0020 #include <QJsonObject>
0021 #include <QTimer>
0022 #include <QUrlQuery>
0023 
0024 #include <chrono>
0025 
0026 using namespace std::chrono_literals;
0027 
0028 constexpr auto updateDelay = 1s;
0029 
0030 void ElementData::updateFetchedThumbSize()
0031 {
0032     int thumbWidth = mThumbSize.width();
0033     int thumbHeight = static_cast<int>(thumbWidth * mPictureHWRatio);
0034     if (mThumbSize.height() < thumbHeight) {
0035         /* if the requested height is less than the requested width * ratio
0036            we would download too much, as the downloaded picture would be
0037            taller than requested, so we adjust the width of the picture to
0038            be downloaded in consequence */
0039         thumbWidth /= (thumbHeight / static_cast<float>(mThumbSize.height()));
0040         thumbHeight = static_cast<int>(thumbWidth * mPictureHWRatio);
0041     }
0042     mFetchedThumbSize = QSize(thumbWidth, thumbHeight);
0043 }
0044 
0045 POTDElement::POTDElement(const QString &id, QDate date, ElementData *data)
0046     : Element(id)
0047     , mDate(date)
0048     , mData(data)
0049     , mThumbImageGetDelayTimer(new QTimer(this))
0050 {
0051     mThumbImageGetDelayTimer->setSingleShot(true);
0052     mThumbImageGetDelayTimer->setInterval(updateDelay);
0053     connect(mThumbImageGetDelayTimer, &QTimer::timeout, this, &POTDElement::queryThumbImageInfoJson);
0054 
0055     // wait a bit to avoid data queries in case of quick paging through views
0056     QTimer::singleShot(updateDelay, this, &POTDElement::completeMissingData);
0057 }
0058 
0059 POTDElement::~POTDElement()
0060 {
0061     // reset thumb update state
0062     if (mData->mState > DataLoaded) {
0063         mData->mState = DataLoaded;
0064     }
0065     Picoftheday::cacheData(mDate, mData);
0066 }
0067 
0068 void POTDElement::completeMissingData()
0069 {
0070     if (mData->mState <= NeedingPageData) {
0071         queryImagesJson();
0072     } else if (mData->mState <= NeedingBasicImageInfo) {
0073         queryBasicImageInfoJson();
0074     } else if (mData->mState <= NeedingFirstThumbImage) {
0075         queryThumbImageInfoJson();
0076     }
0077 }
0078 
0079 KIO::SimpleJob *POTDElement::createJsonQueryJob(const QString &property, const QString &title, const QList<QueryItem> &otherQueryItems)
0080 {
0081     QUrl url(QStringLiteral("https://en.wikipedia.org/w/api.php"));
0082 
0083     QUrlQuery urlQuery{
0084         {QStringLiteral("action"), QStringLiteral("query")},
0085         {QStringLiteral("format"), QStringLiteral("json")},
0086         {QStringLiteral("prop"), property},
0087         {QStringLiteral("titles"), title},
0088     };
0089     for (const auto &item : otherQueryItems) {
0090         urlQuery.addQueryItem(item.key, item.value);
0091     }
0092     url.setQuery(urlQuery);
0093 
0094     return KIO::storedGet(url, KIO::NoReload, KIO::HideProgressInfo);
0095 }
0096 
0097 KIO::SimpleJob *POTDElement::createImagesJsonQueryJob(PageProtectionState state)
0098 {
0099     const char *const templatePagePrefix = (state == ProtectedPage) ? "Template:POTD_protected/" : "Template:POTD/";
0100     const QString templatePageName = QLatin1StringView(templatePagePrefix) + mDate.toString(Qt::ISODate);
0101     const QList<QueryItem> otherQueryItems{
0102         // TODO: unsure if formatversion is needed, used by https://www.mediawiki.org/wiki/API:Picture_of_the_day_viewer in October 2021
0103         {QStringLiteral("formatversion"), QStringLiteral("2")},
0104     };
0105 
0106     return createJsonQueryJob(QStringLiteral("images"), templatePageName, otherQueryItems);
0107 }
0108 
0109 void POTDElement::queryImagesJson()
0110 {
0111     auto queryImagesJob = createImagesJsonQueryJob(ProtectedPage);
0112 
0113     connect(queryImagesJob, &KIO::SimpleJob::result, this, &POTDElement::handleProtectedImagesJsonResponse);
0114 }
0115 
0116 void POTDElement::handleImagesJsonResponse(KJob *job, PageProtectionState pageProtectionState)
0117 {
0118     if (job->error()) {
0119         qCWarning(KORGANIZERPICOFTHEDAYPLUGIN_LOG) << mDate << ": could not get POTD file name:" << job->errorString();
0120         setLoadingFailed();
0121         return;
0122     }
0123 
0124     auto const transferJob = static_cast<KIO::StoredTransferJob *>(job);
0125 
0126     const auto json = QJsonDocument::fromJson(transferJob->data());
0127 
0128     const auto pageObject = json.object().value(QLatin1StringView("query")).toObject().value(QLatin1StringView("pages")).toArray().at(0).toObject();
0129 
0130     auto missingIt = pageObject.find(QLatin1StringView("missing"));
0131     if ((missingIt != pageObject.end()) && missingIt.value().toBool(false)) {
0132         // fallback to unprotected variant in case there is no protected variant
0133         if (pageProtectionState == ProtectedPage) {
0134             qCDebug(KORGANIZERPICOFTHEDAYPLUGIN_LOG) << mDate << ": protected page reported as missing, trying unprocteded now.";
0135             auto queryImagesJob = createImagesJsonQueryJob(UnprotectedPage);
0136 
0137             connect(queryImagesJob, &KIO::SimpleJob::result, this, &POTDElement::handleUnprotectedImagesJsonResponse);
0138             return;
0139         }
0140 
0141         // no POTD set
0142         qCDebug(KORGANIZERPICOFTHEDAYPLUGIN_LOG) << mDate << ": also unprotected page reported as missing, Seems no POTD is declared.";
0143         setLoadingFailed();
0144         return;
0145     }
0146 
0147     const auto imageObject = pageObject.value(QLatin1StringView("images")).toArray().at(0).toObject();
0148     const QString imageFile = imageObject.value(QLatin1StringView("title")).toString();
0149     if (imageFile.isEmpty()) {
0150         qCWarning(KORGANIZERPICOFTHEDAYPLUGIN_LOG) << mDate << ": missing images data in reply:" << json;
0151         setLoadingFailed();
0152         return;
0153     }
0154 
0155     // store data
0156     mData->mPictureName = imageFile;
0157     mData->mState = NeedingBasicImageInfo;
0158 
0159     queryBasicImageInfoJson();
0160 }
0161 
0162 void POTDElement::handleUnprotectedImagesJsonResponse(KJob *job)
0163 {
0164     handleImagesJsonResponse(job, UnprotectedPage);
0165 }
0166 
0167 void POTDElement::handleProtectedImagesJsonResponse(KJob *job)
0168 {
0169     handleImagesJsonResponse(job, ProtectedPage);
0170 }
0171 
0172 void POTDElement::queryBasicImageInfoJson()
0173 {
0174     const QList<QueryItem> otherQueryItems{
0175         {QStringLiteral("iiprop"), QStringLiteral("url|size|canonicaltitle")},
0176     };
0177     auto queryBasicImageInfoJob = createJsonQueryJob(QStringLiteral("imageinfo"), mData->mPictureName, otherQueryItems);
0178 
0179     connect(queryBasicImageInfoJob, &KIO::SimpleJob::result, this, &POTDElement::handleBasicImageInfoJsonResponse);
0180 }
0181 
0182 void POTDElement::handleBasicImageInfoJsonResponse(KJob *job)
0183 {
0184     if (job->error()) {
0185         qCWarning(KORGANIZERPICOFTHEDAYPLUGIN_LOG) << mDate << ": could not get POTD file name:" << job->errorString();
0186         setLoadingFailed();
0187         return;
0188     }
0189 
0190     auto const transferJob = static_cast<KIO::StoredTransferJob *>(job);
0191 
0192     const auto json = QJsonDocument::fromJson(transferJob->data());
0193 
0194     const auto pagesObject = json.object().value(QLatin1StringView("query")).toObject().value(QLatin1StringView("pages")).toObject();
0195     const auto pageObject = pagesObject.isEmpty() ? QJsonObject() : pagesObject.begin()->toObject();
0196     const auto imageInfo = pageObject.value(QLatin1StringView("imageinfo")).toArray().at(0).toObject();
0197 
0198     const QString url = imageInfo.value(QLatin1StringView("url")).toString();
0199     if (url.isEmpty()) {
0200         qCWarning(KORGANIZERPICOFTHEDAYPLUGIN_LOG) << mDate << ": missing imageinfo data in reply:" << json;
0201         setLoadingFailed();
0202         return;
0203     }
0204 
0205     const QString descriptionUrl = imageInfo.value(QLatin1StringView("descriptionurl")).toString();
0206     mData->mAboutPageUrl = QUrl(descriptionUrl);
0207 
0208     const QString description = imageInfo.value(QLatin1StringView("canonicaltitle")).toString();
0209     mData->mTitle = i18n("Wikipedia POTD: %1", description);
0210 
0211     const int width = imageInfo.value(QLatin1StringView("width")).toInt();
0212     const int height = imageInfo.value(QLatin1StringView("height")).toInt();
0213     mData->mPictureHWRatio = ((width != 0) && (height != 0)) ? height / static_cast<float>(width) : 1.0;
0214     qCDebug(KORGANIZERPICOFTHEDAYPLUGIN_LOG) << mDate << ": thumb width" << width << " thumb height" << height << "ratio" << mData->mPictureHWRatio;
0215     mData->updateFetchedThumbSize();
0216     mData->mState = NeedingFirstThumbImageInfo;
0217 
0218     queryThumbImageInfoJson();
0219 }
0220 
0221 void POTDElement::queryThumbImageInfoJson()
0222 {
0223     qCDebug(KORGANIZERPICOFTHEDAYPLUGIN_LOG) << mDate << ": thumb size" << mData->mThumbSize << " adapted size" << mData->mFetchedThumbSize;
0224 
0225     const QList<QueryItem> otherQueryItems{
0226         {QStringLiteral("iiprop"), QStringLiteral("url")},
0227         {QStringLiteral("iiurlwidth"), QString::number(mData->mFetchedThumbSize.width())},
0228         {QStringLiteral("iiurlheight"), QString::number(mData->mFetchedThumbSize.height())},
0229     };
0230     mQueryThumbImageInfoJob = createJsonQueryJob(QStringLiteral("imageinfo"), mData->mPictureName, otherQueryItems);
0231 
0232     connect(mQueryThumbImageInfoJob, &KIO::SimpleJob::result, this, &POTDElement::handleThumbImageInfoJsonResponse);
0233 }
0234 
0235 void POTDElement::handleThumbImageInfoJsonResponse(KJob *job)
0236 {
0237     mQueryThumbImageInfoJob = nullptr;
0238 
0239     if (job->error()) {
0240         qCWarning(KORGANIZERPICOFTHEDAYPLUGIN_LOG) << mDate << ": could not get thumb info:" << job->errorString();
0241         if (mData->mState == NeedingFirstThumbImageInfo) {
0242             setLoadingFailed();
0243         }
0244         return;
0245     }
0246 
0247     auto const transferJob = static_cast<KIO::StoredTransferJob *>(job);
0248 
0249     const auto json = QJsonDocument::fromJson(transferJob->data());
0250     auto pagesObject = json.object().value(QLatin1StringView("query")).toObject().value(QLatin1StringView("pages")).toObject();
0251     auto pageObject = pagesObject.isEmpty() ? QJsonObject() : pagesObject.begin()->toObject();
0252     auto imageInfo = pageObject.value(QLatin1StringView("imageinfo")).toArray().at(0).toObject();
0253 
0254     const QString thumbUrl = imageInfo.value(QStringLiteral("thumburl")).toString();
0255     if (thumbUrl.isEmpty()) {
0256         qCWarning(KORGANIZERPICOFTHEDAYPLUGIN_LOG) << mDate << ": missing imageinfo data in reply:" << json;
0257         return;
0258     }
0259 
0260     mData->mState = (mData->mState == NeedingFirstThumbImageInfo) ? NeedingFirstThumbImage : NeedingNextThumbImage;
0261 
0262     getThumbImage(QUrl(thumbUrl));
0263 }
0264 
0265 void POTDElement::getThumbImage(const QUrl &thumbUrl)
0266 {
0267     if (mGetThumbImageJob) {
0268         mGetThumbImageJob->kill();
0269     }
0270 
0271     qCDebug(KORGANIZERPICOFTHEDAYPLUGIN_LOG) << mDate << ": fetching POTD thumbnail:" << thumbUrl;
0272 
0273     mGetThumbImageJob = KIO::storedGet(thumbUrl, KIO::NoReload, KIO::HideProgressInfo);
0274 
0275     connect(mGetThumbImageJob, &KIO::SimpleJob::result, this, &POTDElement::handleGetThumbImageResponse);
0276 }
0277 
0278 void POTDElement::handleGetThumbImageResponse(KJob *job)
0279 {
0280     mGetThumbImageJob = nullptr;
0281 
0282     const bool isAboutFirstThumbImage = (mData->mState == NeedingFirstThumbImage);
0283 
0284     if (job->error()) {
0285         qCWarning(KORGANIZERPICOFTHEDAYPLUGIN_LOG) << mDate << ": could not get POTD thumb:" << job->errorString();
0286         if (isAboutFirstThumbImage) {
0287             setLoadingFailed();
0288         }
0289         return;
0290     }
0291 
0292     // Last step completed: we get the pixmap from the transfer job's data
0293     auto const transferJob = static_cast<KIO::StoredTransferJob *>(job);
0294     if (!mData->mThumbnail.loadFromData(transferJob->data())) {
0295         qCWarning(KORGANIZERPICOFTHEDAYPLUGIN_LOG) << mDate << ": could not load POTD thumb data.";
0296         if (isAboutFirstThumbImage) {
0297             setLoadingFailed();
0298         }
0299         return;
0300     }
0301 
0302     mData->mState = DataLoaded;
0303 
0304     if (isAboutFirstThumbImage) {
0305         // update other properties
0306         Q_EMIT gotNewShortText(shortText());
0307         Q_EMIT gotNewLongText(mData->mTitle);
0308         Q_EMIT gotNewUrl(mData->mAboutPageUrl);
0309     }
0310 
0311     if (!mRequestedThumbSize.isNull()) {
0312         Q_EMIT gotNewPixmap(mData->mThumbnail.scaled(mRequestedThumbSize, Qt::KeepAspectRatio, Qt::SmoothTransformation));
0313     }
0314 }
0315 
0316 void POTDElement::setLoadingFailed()
0317 {
0318     mData->mState = LoadingFailed;
0319 
0320     Q_EMIT gotNewShortText(QString());
0321     Q_EMIT gotNewLongText(QString());
0322 }
0323 
0324 QString POTDElement::shortText() const
0325 {
0326     return (mData->mState >= DataLoaded) ? i18n("Picture Page") : (mData->mState >= NeedingPageData) ? i18n("Loading...") : QString();
0327 }
0328 
0329 QString POTDElement::longText() const
0330 {
0331     return (mData->mState >= DataLoaded)     ? mData->mTitle
0332         : (mData->mState >= NeedingPageData) ? i18n("<qt>Loading <i>Picture of the Day</i>...</qt>")
0333                                              : QString();
0334 }
0335 
0336 QUrl POTDElement::url() const
0337 {
0338     return (mData->mState >= DataLoaded) ? mData->mAboutPageUrl : QUrl();
0339 }
0340 
0341 QPixmap POTDElement::newPixmap(const QSize &size)
0342 {
0343     mRequestedThumbSize = size;
0344 
0345     if ((mData->mThumbSize.width() < size.width()) || (mData->mThumbSize.height() < size.height())) {
0346         qCDebug(KORGANIZERPICOFTHEDAYPLUGIN_LOG) << mDate << ": called for a new pixmap size (" << size << "instead of" << mData->mThumbSize
0347                                                  << ", stored pixmap:" << mData->mThumbnail.size() << ")";
0348         mData->mThumbSize = size;
0349 
0350         if (mData->mState >= NeedingFirstThumbImageInfo) {
0351             mData->updateFetchedThumbSize();
0352 
0353             if ((mData->mFetchedThumbSize.width() < size.width()) || (mData->mFetchedThumbSize.height() < size.height())) {
0354                 // only if there is already an initial pixmap to show at least something,
0355                 // kill current update and trigger new delayed update
0356                 if (mData->mState >= DataLoaded) {
0357                     if (mQueryThumbImageInfoJob) {
0358                         mQueryThumbImageInfoJob->kill();
0359                         mQueryThumbImageInfoJob = nullptr;
0360                     }
0361                     if (mGetThumbImageJob) {
0362                         mGetThumbImageJob->kill();
0363                         mGetThumbImageJob = nullptr;
0364                     }
0365                     mData->mState = NeedingNextThumbImageInfo;
0366                 }
0367 
0368                 // We start a new thumbnail download a little later; the following code
0369                 // is to avoid too frequent transfers e.g. when resizing
0370                 mThumbImageGetDelayTimer->start();
0371             }
0372         }
0373     }
0374 
0375     /* else, either we already got a sufficiently big pixmap (stored in mData->mThumbnail),
0376        or we will get one anytime soon (we are downloading it already) and we will
0377        actualize what we return here later via gotNewPixmap */
0378     if (mData->mThumbnail.isNull()) {
0379         return {};
0380     }
0381     return mData->mThumbnail.scaled(mRequestedThumbSize, Qt::KeepAspectRatio, Qt::SmoothTransformation);
0382 }
0383 
0384 #include "moc_element.cpp"