Commit 12074996 authored by Friedrich W. H. Kossebau's avatar Friedrich W. H. Kossebau
Browse files

Update KOrganizer POTD plugin for current Wikipedia API

BUG: 369486
parent 5c4b134e
Pipeline #89892 passed with stage
in 51 minutes and 1 second
......@@ -4,6 +4,7 @@ add_library(korg_picoftheday MODULE)
target_sources(korg_picoftheday PRIVATE
configdialog.cpp
picoftheday.cpp
element.cpp
)
ecm_qt_declare_logging_category(korg_picoftheday HEADER korganizer_picoftheday_plugin_debug.h IDENTIFIER KORGANIZERPICOFTHEDAYPLUGIN_LOG CATEGORY_NAME org.kde.pim.korganizer_picoftheday_plugins
DESCRIPTION "kdepim-addons (korganizer picoftheday plugins)"
......
/*
This file is part of KOrganizer.
SPDX-FileCopyrightText: 2001 Cornelius Schumacher <schumacher@kde.org>
SPDX-FileCopyrightText: 2007 Loïc Corbasson <loic.corbasson@gmail.com>
SPDX-FileCopyrightText: 2021 Friedrich W. H. Kossebau <kossebau@kde.org>
SPDX-License-Identifier: GPL-2.0-or-later
*/
#include "element.h"
#include "picoftheday.h"
#include "korganizer_picoftheday_plugin_debug.h"
#include <KIO/Scheduler>
#include <KIO/StoredTransferJob>
#include <KLocalizedString>
#include <QJsonArray>
#include <QJsonDocument>
#include <QUrlQuery>
#include <chrono>
using namespace std::chrono_literals;
constexpr auto updateDelay = 1s;
void ElementData::updateFetchedThumbSize()
{
int thumbWidth = mThumbSize.width();
int thumbHeight = static_cast<int>(thumbWidth * mPictureHWRatio);
if (mThumbSize.height() < thumbHeight) {
/* if the requested height is less than the requested width * ratio
we would download too much, as the downloaded picture would be
taller than requested, so we adjust the width of the picture to
be downloaded in consequence */
thumbWidth /= (thumbHeight / static_cast<float>(mThumbSize.height()));
thumbHeight = static_cast<int>(thumbWidth * mPictureHWRatio);
}
mFetchedThumbSize = QSize(thumbWidth, thumbHeight);
}
POTDElement::POTDElement(const QString &id, QDate date, ElementData *data)
: Element(id)
, mDate(date)
, mData(data)
, mThumbImageGetDelayTimer(new QTimer(this))
{
mThumbImageGetDelayTimer->setSingleShot(true);
mThumbImageGetDelayTimer->setInterval(updateDelay);
connect(mThumbImageGetDelayTimer, &QTimer::timeout, this, &POTDElement::queryThumbImageInfoJson);
// wait a bit to avoid data queries in case of quick paging through views
QTimer::singleShot(updateDelay, this, &POTDElement::completeMissingData);
}
POTDElement::~POTDElement()
{
// reset thumb update state
if (mData->mState > DataLoaded) {
mData->mState = DataLoaded;
}
Picoftheday::cacheData(mDate, mData);
}
void POTDElement::completeMissingData()
{
if (mData->mState <= NeedingPageData) {
queryImagesJson();
} else if (mData->mState <= NeedingBasicImageInfo) {
queryBasicImageInfoJson();
} else if (mData->mState <= NeedingFirstThumbImage) {
queryThumbImageInfoJson();
}
}
KIO::SimpleJob *POTDElement::createJsonQueryJob(const QString &property, const QString &title, const QList<QueryItem> &otherQueryItems)
{
QUrl url(QStringLiteral("https://en.wikipedia.org/w/api.php"));
QUrlQuery urlQuery{
{QStringLiteral("action"), QStringLiteral("query")},
{QStringLiteral("format"), QStringLiteral("json")},
{QStringLiteral("prop"), property},
{QStringLiteral("titles"), title},
};
for (const auto &item : otherQueryItems) {
urlQuery.addQueryItem(item.key, item.value);
}
url.setQuery(urlQuery);
auto job = KIO::storedGet(url, KIO::NoReload, KIO::HideProgressInfo);
KIO::Scheduler::setJobPriority(job, 1);
return job;
}
KIO::SimpleJob *POTDElement::createImagesJsonQueryJob(PageProtectionState state)
{
const char *const templatePagePrefix = (state == ProtectedPage) ? "Template:POTD_protected/" : "Template:POTD/";
const QString templatePageName = QLatin1String(templatePagePrefix) + mDate.toString(Qt::ISODate);
const QList<QueryItem> otherQueryItems{
// TODO: unsure if formatversion is needed, used by https://www.mediawiki.org/wiki/API:Picture_of_the_day_viewer in October 2021
{QStringLiteral("formatversion"), QStringLiteral("2")},
};
return createJsonQueryJob(QStringLiteral("images"), templatePageName, otherQueryItems);
}
void POTDElement::queryImagesJson()
{
auto queryImagesJob = createImagesJsonQueryJob(ProtectedPage);
connect(queryImagesJob, &KIO::SimpleJob::result, this, &POTDElement::handleProtectedImagesJsonResponse);
}
void POTDElement::handleImagesJsonResponse(KJob *job, PageProtectionState pageProtectionState)
{
if (job->error()) {
qCWarning(KORGANIZERPICOFTHEDAYPLUGIN_LOG) << mDate << ": could not get POTD file name:" << job->errorString();
setLoadingFailed();
return;
}
auto const transferJob = static_cast<KIO::StoredTransferJob *>(job);
const auto json = QJsonDocument::fromJson(transferJob->data());
const auto pageObject = json.object().value(QLatin1String("query")).toObject().value(QLatin1String("pages")).toArray().at(0).toObject();
auto missingIt = pageObject.find(QLatin1String("missing"));
if ((missingIt != pageObject.end()) && missingIt.value().toBool(false)) {
// fallback to unprotected variant in case there is no protected variant
if (pageProtectionState == ProtectedPage) {
qCDebug(KORGANIZERPICOFTHEDAYPLUGIN_LOG) << mDate << ": protected page reported as missing, trying unprocteded now.";
auto queryImagesJob = createImagesJsonQueryJob(UnprotectedPage);
connect(queryImagesJob, &KIO::SimpleJob::result, this, &POTDElement::handleUnprotectedImagesJsonResponse);
return;
}
// no POTD set
qCDebug(KORGANIZERPICOFTHEDAYPLUGIN_LOG) << mDate << ": also unprotected page reported as missing, Seems no POTD is declared.";
setLoadingFailed();
return;
}
const auto imageObject = pageObject.value(QLatin1String("images")).toArray().at(0).toObject();
const QString imageFile = imageObject.value(QLatin1String("title")).toString();
if (imageFile.isEmpty()) {
qCWarning(KORGANIZERPICOFTHEDAYPLUGIN_LOG) << mDate << ": missing images data in reply:" << json;
setLoadingFailed();
return;
}
// store data
mData->mPictureName = imageFile;
mData->mState = NeedingBasicImageInfo;
queryBasicImageInfoJson();
}
void POTDElement::handleUnprotectedImagesJsonResponse(KJob *job)
{
handleImagesJsonResponse(job, UnprotectedPage);
}
void POTDElement::handleProtectedImagesJsonResponse(KJob *job)
{
handleImagesJsonResponse(job, ProtectedPage);
}
void POTDElement::queryBasicImageInfoJson()
{
const QList<QueryItem> otherQueryItems{
{QStringLiteral("iiprop"), QStringLiteral("url|size|canonicaltitle")},
};
auto queryBasicImageInfoJob = createJsonQueryJob(QStringLiteral("imageinfo"), mData->mPictureName, otherQueryItems);
connect(queryBasicImageInfoJob, &KIO::SimpleJob::result, this, &POTDElement::handleBasicImageInfoJsonResponse);
}
void POTDElement::handleBasicImageInfoJsonResponse(KJob *job)
{
if (job->error()) {
qCWarning(KORGANIZERPICOFTHEDAYPLUGIN_LOG) << mDate << ": could not get POTD file name:" << job->errorString();
setLoadingFailed();
return;
}
auto const transferJob = static_cast<KIO::StoredTransferJob *>(job);
const auto json = QJsonDocument::fromJson(transferJob->data());
const auto pagesObject = json.object().value(QLatin1String("query")).toObject().value(QLatin1String("pages")).toObject();
const auto pageObject = pagesObject.isEmpty() ? QJsonObject() : pagesObject.begin()->toObject();
const auto imageInfo = pageObject.value(QLatin1String("imageinfo")).toArray().at(0).toObject();
const QString url = imageInfo.value(QLatin1String("url")).toString();
if (url.isEmpty()) {
qCWarning(KORGANIZERPICOFTHEDAYPLUGIN_LOG) << mDate << ": missing imageinfo data in reply:" << json;
setLoadingFailed();
return;
}
const QString descriptionUrl = imageInfo.value(QLatin1String("descriptionurl")).toString();
mData->mAboutPageUrl = QUrl(descriptionUrl);
const QString description = imageInfo.value(QLatin1String("canonicaltitle")).toString();
mData->mTitle = i18n("Wikipedia POTD: %1", description);
const int width = imageInfo.value(QLatin1String("width")).toInt();
const int height = imageInfo.value(QLatin1String("height")).toInt();
mData->mPictureHWRatio = ((width != 0) && (height != 0)) ? height / static_cast<float>(width) : 1.0;
qCDebug(KORGANIZERPICOFTHEDAYPLUGIN_LOG) << mDate << ": thumb width" << width << " thumb height" << height << "ratio" << mData->mPictureHWRatio;
mData->updateFetchedThumbSize();
mData->mState = NeedingFirstThumbImageInfo;
queryThumbImageInfoJson();
}
void POTDElement::queryThumbImageInfoJson()
{
qCDebug(KORGANIZERPICOFTHEDAYPLUGIN_LOG) << mDate << ": thumb size" << mData->mThumbSize << " adapted size" << mData->mFetchedThumbSize;
const QList<QueryItem> otherQueryItems{
{QStringLiteral("iiprop"), QStringLiteral("url")},
{QStringLiteral("iiurlwidth"), QString::number(mData->mFetchedThumbSize.width())},
{QStringLiteral("iiurlheight"), QString::number(mData->mFetchedThumbSize.height())},
};
mQueryThumbImageInfoJob = createJsonQueryJob(QStringLiteral("imageinfo"), mData->mPictureName, otherQueryItems);
connect(mQueryThumbImageInfoJob, &KIO::SimpleJob::result, this, &POTDElement::handleThumbImageInfoJsonResponse);
}
void POTDElement::handleThumbImageInfoJsonResponse(KJob *job)
{
mQueryThumbImageInfoJob = nullptr;
if (job->error()) {
qCWarning(KORGANIZERPICOFTHEDAYPLUGIN_LOG) << mDate << ": could not get thumb info:" << job->errorString();
if (mData->mState == NeedingFirstThumbImageInfo) {
setLoadingFailed();
}
return;
}
auto const transferJob = static_cast<KIO::StoredTransferJob *>(job);
const auto json = QJsonDocument::fromJson(transferJob->data());
auto pagesObject = json.object().value(QLatin1String("query")).toObject().value(QLatin1String("pages")).toObject();
auto pageObject = pagesObject.isEmpty() ? QJsonObject() : pagesObject.begin()->toObject();
auto imageInfo = pageObject.value(QLatin1String("imageinfo")).toArray().at(0).toObject();
const QString thumbUrl = imageInfo.value(QStringLiteral("thumburl")).toString();
if (thumbUrl.isEmpty()) {
qCWarning(KORGANIZERPICOFTHEDAYPLUGIN_LOG) << mDate << ": missing imageinfo data in reply:" << json;
return;
}
mData->mState = (mData->mState == NeedingFirstThumbImageInfo) ? NeedingFirstThumbImage : NeedingNextThumbImage;
getThumbImage(QUrl(thumbUrl));
}
void POTDElement::getThumbImage(const QUrl &thumbUrl)
{
if (mGetThumbImageJob) {
mGetThumbImageJob->kill();
}
qCDebug(KORGANIZERPICOFTHEDAYPLUGIN_LOG) << mDate << ": fetching POTD thumbnail:" << thumbUrl;
mGetThumbImageJob = KIO::storedGet(thumbUrl, KIO::NoReload, KIO::HideProgressInfo);
KIO::Scheduler::setJobPriority(mGetThumbImageJob, 1);
connect(mGetThumbImageJob, &KIO::SimpleJob::result, this, &POTDElement::handleGetThumbImageResponse);
}
void POTDElement::handleGetThumbImageResponse(KJob *job)
{
mGetThumbImageJob = nullptr;
const bool isAboutFirstThumbImage = (mData->mState == NeedingFirstThumbImage);
if (job->error()) {
qCWarning(KORGANIZERPICOFTHEDAYPLUGIN_LOG) << mDate << ": could not get POTD thumb:" << job->errorString();
if (isAboutFirstThumbImage) {
setLoadingFailed();
}
return;
}
// Last step completed: we get the pixmap from the transfer job's data
auto const transferJob = static_cast<KIO::StoredTransferJob *>(job);
if (!mData->mThumbnail.loadFromData(transferJob->data())) {
qCWarning(KORGANIZERPICOFTHEDAYPLUGIN_LOG) << mDate << ": could not load POTD thumb data.";
if (isAboutFirstThumbImage) {
setLoadingFailed();
}
return;
}
mData->mState = DataLoaded;
if (isAboutFirstThumbImage) {
// update other properties
Q_EMIT gotNewShortText(shortText());
Q_EMIT gotNewLongText(mData->mTitle);
Q_EMIT gotNewUrl(mData->mAboutPageUrl);
}
if (!mRequestedThumbSize.isNull()) {
Q_EMIT gotNewPixmap(mData->mThumbnail.scaled(mRequestedThumbSize, Qt::KeepAspectRatio, Qt::SmoothTransformation));
}
}
void POTDElement::setLoadingFailed()
{
mData->mState = LoadingFailed;
Q_EMIT gotNewShortText(QString());
Q_EMIT gotNewLongText(QString());
}
QString POTDElement::shortText() const
{
return (mData->mState >= DataLoaded) ? i18n("Picture Page") : (mData->mState >= NeedingPageData) ? i18n("Loading...") : QString();
}
QString POTDElement::longText() const
{
return (mData->mState >= DataLoaded) ? mData->mTitle
: (mData->mState >= NeedingPageData) ? i18n("<qt>Loading <i>Picture of the Day</i>...</qt>")
: QString();
}
QUrl POTDElement::url() const
{
return (mData->mState >= DataLoaded) ? mData->mAboutPageUrl : QUrl();
}
QPixmap POTDElement::newPixmap(const QSize &size)
{
mRequestedThumbSize = size;
if ((mData->mThumbSize.width() < size.width()) || (mData->mThumbSize.height() < size.height())) {
qCDebug(KORGANIZERPICOFTHEDAYPLUGIN_LOG) << mDate << ": called for a new pixmap size (" << size << "instead of" << mData->mThumbSize
<< ", stored pixmap:" << mData->mThumbnail.size() << ")";
mData->mThumbSize = size;
if (mData->mState >= NeedingFirstThumbImageInfo) {
mData->updateFetchedThumbSize();
if ((mData->mFetchedThumbSize.width() < size.width()) || (mData->mFetchedThumbSize.height() < size.height())) {
// only if there is already an initial pixmap to show at least something,
// kill current update and trigger new delayed update
if (mData->mState >= DataLoaded) {
if (mQueryThumbImageInfoJob) {
mQueryThumbImageInfoJob->kill();
mQueryThumbImageInfoJob = nullptr;
}
if (mGetThumbImageJob) {
mGetThumbImageJob->kill();
mGetThumbImageJob = nullptr;
}
mData->mState = NeedingNextThumbImageInfo;
}
// We start a new thumbnail download a little later; the following code
// is to avoid too frequent transfers e.g. when resizing
mThumbImageGetDelayTimer->start();
}
}
}
/* else, either we already got a sufficiently big pixmap (stored in mData->mThumbnail),
or we will get one anytime soon (we are downloading it already) and we will
actualize what we return here later via gotNewPixmap */
if (mData->mThumbnail.isNull()) {
return QPixmap();
}
return mData->mThumbnail.scaled(mRequestedThumbSize, Qt::KeepAspectRatio, Qt::SmoothTransformation);
}
/*
This file is part of KOrganizer.
SPDX-FileCopyrightText: 2001 Cornelius Schumacher <schumacher@kde.org>
SPDX-FileCopyrightText: 2007 Loïc Corbasson <loic.corbasson@gmail.com>
SPDX-FileCopyrightText: 2021 Friedrich W. H. Kossebau <kossebau@kde.org>
SPDX-License-Identifier: GPL-2.0-or-later
*/
#pragma once
#include <EventViews/CalendarDecoration>
using namespace EventViews::CalendarDecoration;
#include <KIO/SimpleJob>
#include <QUrl>
enum DataState {
LoadingFailed = -1,
NeedingPageData = 0,
NeedingBasicImageInfo,
NeedingFirstThumbImageInfo,
NeedingFirstThumbImage,
DataLoaded,
NeedingNextThumbImageInfo,
NeedingNextThumbImage,
};
struct ElementData {
float mPictureHWRatio = 1;
QString mPictureName;
QUrl mAboutPageUrl;
QSize mThumbSize;
QSize mFetchedThumbSize;
QPixmap mThumbnail;
QString mTitle;
DataState mState = NeedingPageData;
void updateFetchedThumbSize();
};
class POTDElement : public Element
{
Q_OBJECT
public:
POTDElement(const QString &id, QDate date, ElementData *data);
~POTDElement() override;
public: // Element API
Q_REQUIRED_RESULT QString shortText() const override;
Q_REQUIRED_RESULT QString longText() const override;
Q_REQUIRED_RESULT QUrl url() const override;
Q_REQUIRED_RESULT QPixmap newPixmap(const QSize &size) override;
private:
void queryImagesJson();
void queryBasicImageInfoJson();
void queryThumbImageInfoJson();
void getThumbImage(const QUrl &thumbUrl);
// POTD pages once decided about should get an edit-protected variant, but not all have that
enum PageProtectionState { ProtectedPage, UnprotectedPage };
KIO::SimpleJob *createImagesJsonQueryJob(PageProtectionState pageProtectionState);
struct QueryItem {
QString key;
QString value;
};
KIO::SimpleJob *createJsonQueryJob(const QString &property, const QString &title, const QList<QueryItem> &otherQueryItems = {});
void handleImagesJsonResponse(KJob *job, PageProtectionState pageProtectionState);
void setLoadingFailed();
private Q_SLOTS:
void handleProtectedImagesJsonResponse(KJob *job);
void handleUnprotectedImagesJsonResponse(KJob *job);
void handleBasicImageInfoJsonResponse(KJob *job);
void handleThumbImageInfoJsonResponse(KJob *job);
void handleGetThumbImageResponse(KJob *job);
void completeMissingData();
private:
const QDate mDate;
QSize mRequestedThumbSize;
ElementData *mData;
QTimer *mThumbImageGetDelayTimer = nullptr;
KIO::SimpleJob *mQueryThumbImageInfoJob = nullptr;
KIO::SimpleJob *mGetThumbImageJob = nullptr;
};
......@@ -2,26 +2,32 @@
This file is part of KOrganizer.
SPDX-FileCopyrightText: 2001 Cornelius Schumacher <schumacher@kde.org>
SPDX-FileCopyrightText: 2007 Loïc Corbasson <loic.corbasson@gmail.com>
SPDX-FileCopyrightText: 2021 Friedrich W. H. Kossebau <kossebau@kde.org>
SPDX-License-Identifier: GPL-2.0-or-later
*/
#include "picoftheday.h"
#include "configdialog.h"
#include "element.h"
#include "korganizer_picoftheday_plugin_debug.h"
#include <KConfig>
#include <KConfigGroup>
#include <KIO/Scheduler>
#include <KLocalizedString>
#include <KPluginFactory>
#include <QDomDocument>
#include <chrono>
#include <QCache>
#include <QGlobalStatic>
K_PLUGIN_FACTORY(PicofthedayFactory, registerPlugin<Picoftheday>();)
using namespace std::chrono_literals;
// TODO: add also disc cache to avoid even more network traffic
using Cache = QCache<QDate, ElementData>;
constexpr int cacheElementMaxSize = 6 * 7; // rows by weekdays, a full gregorian month's view
Q_GLOBAL_STATIC_WITH_ARGS(Cache, s_cache, (cacheElementMaxSize))
// https://www.mediawiki.org/wiki/API:Picture_of_the_day_viewer
Picoftheday::Picoftheday(QObject *parent, const QVariantList &args)
: Decoration(parent, args)
......@@ -48,290 +54,27 @@ Element::List Picoftheday::createDayElements(const QDate &date)
{
Element::List elements;
auto element = new POTDElement(QStringLiteral("main element"), date, mThumbSize);
elements.append(element);
return elements;
}
////////////////////////////////////////////////////////////////////////////////
POTDElement::POTDElement(const QString &id, QDate date, QSize initialThumbSize)
: StoredElement(id)
, mDate(date)
, mThumbSize(initialThumbSize)
{
setShortText(i18n("Loading..."));
setLongText(i18n("<qt>Loading <i>Picture of the Day</i>...</qt>"));
mTimer = new QTimer(this);
mTimer->setSingleShot(true);
step1StartDownload();
}
/** First step of three in the download process */
void POTDElement::step1StartDownload()
{
// Start downloading the picture
if (!mFirstStepCompleted && !mFirstStepJob) {
QUrl url = QUrl(QStringLiteral("https://en.wikipedia.org/w/index.php?title=Template:POTD_protected/") + mDate.toString(Qt::ISODate)
+ QStringLiteral("&action=raw"));
// The file at that URL contains the file name for the POTD
qCWarning(KORGANIZERPICOFTHEDAYPLUGIN_LOG) << "step1StartDownload url :" << url;
mFirstStepJob = KIO::storedGet(url, KIO::NoReload, KIO::HideProgressInfo);
KIO::Scheduler::setJobPriority(mFirstStepJob, 1);
connect(mFirstStepJob, &KIO::SimpleJob::result, this, &POTDElement::step1Result);
connect(this, &POTDElement::step1Success, this, &POTDElement::step2GetImagePage);
auto data = s_cache->take(date);
qCDebug(KORGANIZERPICOFTHEDAYPLUGIN_LOG) << date << ": taking from cache" << data;
if (!data) {
data = new ElementData;
data->mThumbSize = mThumbSize;
}
}
/**
Give it a job which fetched the raw page,
and it'll give you the image file name hiding in it.
*/
void POTDElement::step1Result(KJob *job)
{
if (job->error()) {
qCWarning(KORGANIZERPICOFTHEDAYPLUGIN_LOG) << "POTD:" << mDate << ": could not get POTD file name:" << job->errorString();
qCDebug(KORGANIZERPICOFTHEDAYPLUGIN_LOG) << "POTD:" << mDate << ": file name:" << mFileName;
qCDebug(KORGANIZERPICOFTHEDAYPLUGIN_LOG) << "POTD:" << mDate << ": full-size image:" << mFullSizeImageUrl;
qCDebug(KORGANIZERPICOFTHEDAYPLUGIN_LOG) << "POTD:" << mDate << ": thumbnail:" << mThumbUrl;
mFirstStepCompleted = false;
return;
}
// First step completed: we now know the POTD's file name
auto const transferJob = static_cast<KIO::StoredTransferJob *>(job);
const QStringList lines = QString::fromUtf8(transferJob->data().data(), transferJob->data().size()).split(QLatin1Char('\n'));
for (const QString &line : lines)