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"