File indexing completed on 2024-12-29 05:00:27

0001 // SPDX-FileCopyrightText: 2007 Tobias Koenig <tokoe@kde.org>
0002 // SPDX-FileCopyrightText: 2008 Anne-Marie Mahfouf <annma@kde.org>
0003 // SPDX-FileCopyrightText: 2008 Georges Toth <gtoth@trypill.org>
0004 // SPDX-FileCopyrightText: 2021 Guo Yunhe <i@guoyunhe.me>
0005 //
0006 // SPDX-License-Identifier: GPL-2.0-or-later
0007 
0008 #include "flickrprovider.h"
0009 
0010 #include <random>
0011 
0012 #include <QFileInfo>
0013 #include <QRegularExpression>
0014 #include <QTextDocumentFragment>
0015 #include <QUrlQuery>
0016 
0017 #include <KConfigGroup>
0018 #include <KIO/StoredTransferJob>
0019 #include <KPluginFactory>
0020 #include <KSharedConfig>
0021 
0022 #include "debug.h"
0023 
0024 static QUrl buildUrl(const QDate &date, const QString &apiKey)
0025 {
0026     QUrl url(QLatin1String("https://api.flickr.com/services/rest/"));
0027     QUrlQuery urlQuery(url);
0028     urlQuery.addQueryItem(QStringLiteral("api_key"), apiKey);
0029     urlQuery.addQueryItem(QStringLiteral("method"), QStringLiteral("flickr.interestingness.getList"));
0030     urlQuery.addQueryItem(QStringLiteral("date"), date.toString(Qt::ISODate));
0031     // url_o might be either too small or too large.
0032     urlQuery.addQueryItem(QStringLiteral("extras"), QStringLiteral("url_k,url_h,url_o"));
0033     url.setQuery(urlQuery);
0034 
0035     return url;
0036 }
0037 
0038 FlickrProvider::FlickrProvider(QObject *parent, const KPluginMetaData &data, const QVariantList &args)
0039     : PotdProvider(parent, data, args)
0040 {
0041     connect(this, &FlickrProvider::configLoaded, this, &FlickrProvider::sendXmlRequest);
0042 
0043     loadConfig();
0044 }
0045 
0046 void FlickrProvider::configRequestFinished(KJob *_job)
0047 {
0048     KIO::StoredTransferJob *job = static_cast<KIO::StoredTransferJob *>(_job);
0049     if (job->error()) {
0050         qCWarning(WALLPAPERPOTD) << "configRequestFinished error: failed to fetch data";
0051         Q_EMIT error(this);
0052         return;
0053     }
0054 
0055     KIO::StoredTransferJob *putJob = KIO::storedPut(job->data(), m_configLocalUrl, -1);
0056     connect(putJob, &KIO::StoredTransferJob::finished, this, &FlickrProvider::configWriteFinished);
0057 }
0058 
0059 void FlickrProvider::configWriteFinished(KJob *_job)
0060 {
0061     KIO::StoredTransferJob *job = static_cast<KIO::StoredTransferJob *>(_job);
0062     if (job->error()) {
0063         qCWarning(WALLPAPERPOTD) << "configWriteFinished error: failed to write data." << job->errorText();
0064         Q_EMIT error(this);
0065     } else {
0066         loadConfig();
0067     }
0068 }
0069 
0070 void FlickrProvider::loadConfig()
0071 {
0072     // TODO move to flickr provider
0073     const QString configFileName = QStringLiteral("%1provider.conf").arg(identifier());
0074     m_configRemoteUrl = QUrl(QStringLiteral("https://autoconfig.kde.org/potd/") + configFileName);
0075     m_configLocalPath = QStandardPaths::writableLocation(QStandardPaths::GenericCacheLocation) + QStringLiteral("/plasma_engine_potd/") + configFileName;
0076     m_configLocalUrl = QUrl::fromLocalFile(m_configLocalPath);
0077 
0078     auto config = KSharedConfig::openConfig(m_configLocalPath, KConfig::NoGlobals);
0079     KConfigGroup apiGroup = config->group("API");
0080     QString apiKey = apiGroup.readEntry("API_KEY");
0081     QString apiSecret = apiGroup.readEntry("API_SECRET");
0082 
0083     Q_EMIT configLoaded(apiKey, apiSecret);
0084 }
0085 
0086 void FlickrProvider::refreshConfig()
0087 {
0088     // You can only refresh it once in a provider's life cycle
0089     if (m_refreshed) {
0090         return;
0091     }
0092     // You can only refresh it once in a day
0093     QFileInfo configFileInfo = QFileInfo(m_configLocalPath);
0094     if (configFileInfo.exists() && configFileInfo.lastModified().addDays(1) > QDateTime::currentDateTime()) {
0095         return;
0096     }
0097 
0098     KIO::StoredTransferJob *job = KIO::storedGet(m_configRemoteUrl, KIO::NoReload, KIO::HideProgressInfo);
0099     connect(job, &KIO::StoredTransferJob::finished, this, &FlickrProvider::configRequestFinished);
0100 
0101     m_refreshed = true;
0102 }
0103 
0104 void FlickrProvider::sendXmlRequest(const QString &apiKey)
0105 {
0106     if (apiKey.isNull()) {
0107         refreshConfig();
0108         return;
0109     }
0110 
0111     mApiKey = apiKey;
0112     mActualDate = QDate::currentDate().addDays(-2);
0113 
0114     const QUrl xmlUrl = buildUrl(mActualDate, apiKey);
0115 
0116     KIO::StoredTransferJob *xmlJob = KIO::storedGet(xmlUrl, KIO::NoReload, KIO::HideProgressInfo);
0117     connect(xmlJob, &KIO::StoredTransferJob::finished, this, &FlickrProvider::xmlRequestFinished);
0118 }
0119 
0120 void FlickrProvider::xmlRequestFinished(KJob *_job)
0121 {
0122     KIO::StoredTransferJob *job = static_cast<KIO::StoredTransferJob *>(_job);
0123     if (job->error()) {
0124         qCWarning(WALLPAPERPOTD) << "XML request error:" << job->errorText();
0125         Q_EMIT error(this);
0126         return;
0127     }
0128 
0129     const QString data = QString::fromUtf8(job->data());
0130 
0131     // Clear the list
0132     m_photoList.clear();
0133     m_photoList.reserve(100);
0134 
0135     xml.clear();
0136     xml.addData(data);
0137 
0138     while (!xml.atEnd()) {
0139         xml.readNext();
0140 
0141         if (xml.isStartElement()) {
0142             auto attributes = xml.attributes();
0143             if (xml.name() == QLatin1String("rsp")) {
0144                 /* no pictures available for the specified parameters */
0145                 if (attributes.value(QLatin1String("stat")).toString() != QLatin1String("ok")) {
0146                     qCWarning(WALLPAPERPOTD) << "xmlRequestFinished error: no photos for the query";
0147                     Q_EMIT error(this);
0148                     return;
0149                 }
0150             } else if (xml.name() == QLatin1String("photo")) {
0151                 if (attributes.value(QLatin1String("ispublic")).toString() != QLatin1String("1")) {
0152                     continue;
0153                 }
0154 
0155                 const char *fallbackList[] = {"url_k", "url_h"};
0156 
0157                 bool found = false;
0158                 for (auto urlAttr : fallbackList) {
0159                     // Get the best url.
0160                     QLatin1String urlAttrString(urlAttr);
0161                     if (attributes.hasAttribute(urlAttrString)) {
0162                         QString title, userId, photoId;
0163                         if (attributes.hasAttribute(QStringLiteral("title"))) {
0164                             title = QTextDocumentFragment::fromHtml(attributes.value(QStringLiteral("title")).toString().trimmed()).toPlainText();
0165                         }
0166                         if (attributes.hasAttribute(QStringLiteral("owner")) && attributes.hasAttribute(QStringLiteral("id"))) {
0167                             userId = attributes.value(QStringLiteral("owner")).toString();
0168                             photoId = attributes.value(QStringLiteral("id")).toString();
0169                         }
0170                         m_photoList.emplace_back(PhotoEntry{
0171                             attributes.value(urlAttrString).toString(),
0172                             title,
0173                             userId,
0174                             photoId,
0175                         });
0176                         found = true;
0177                         break;
0178                     }
0179                 }
0180 
0181                 // The logic here is, if url_h or url_k are present, url_o must
0182                 // has higher quality, otherwise, url_o is worse than k/h size.
0183                 // If url_o is better, prefer url_o.
0184                 if (found) {
0185                     QLatin1String originAttr("url_o");
0186                     if (attributes.hasAttribute(originAttr)) {
0187                         m_photoList.back().urlString = attributes.value(QLatin1String(originAttr)).toString();
0188                     }
0189                 }
0190             }
0191         }
0192     }
0193 
0194     if (xml.error() && xml.error() != QXmlStreamReader::PrematureEndOfDocumentError) {
0195         qCWarning(WALLPAPERPOTD) << "XML ERROR at line" << xml.lineNumber() << xml.error();
0196     }
0197 
0198     if (m_photoList.begin() != m_photoList.end()) {
0199         // Plasma 5.24.0 release date
0200         std::mt19937 randomEngine(QDate(2022, 2, 3).daysTo(QDate::currentDate()));
0201         std::uniform_int_distribution<int> distrib(0, m_photoList.size() - 1);
0202 
0203         const PhotoEntry &randomPhotoEntry = m_photoList.at(distrib(randomEngine));
0204         m_remoteUrl = QUrl(randomPhotoEntry.urlString);
0205         m_title = randomPhotoEntry.title;
0206 
0207         /**
0208          * Visit the photo page to get the author
0209          * API document: https://www.flickr.com/services/api/misc.urls.html
0210          * https://www.flickr.com/photos/{user-id}/{photo-id}
0211          */
0212         if (!(randomPhotoEntry.userId.isEmpty() || randomPhotoEntry.photoId.isEmpty())) {
0213             m_infoUrl = QUrl(QStringLiteral("https://www.flickr.com/photos/%1/%2").arg(randomPhotoEntry.userId, randomPhotoEntry.photoId));
0214         }
0215 
0216         KIO::StoredTransferJob *imageJob = KIO::storedGet(m_remoteUrl, KIO::NoReload, KIO::HideProgressInfo);
0217         connect(imageJob, &KIO::StoredTransferJob::finished, this, &FlickrProvider::imageRequestFinished);
0218     } else {
0219         qCWarning(WALLPAPERPOTD) << "List is empty in XML file";
0220         Q_EMIT error(this);
0221     }
0222 }
0223 
0224 void FlickrProvider::imageRequestFinished(KJob *_job)
0225 {
0226     KIO::StoredTransferJob *job = static_cast<KIO::StoredTransferJob *>(_job);
0227     if (job->error()) {
0228         qCWarning(WALLPAPERPOTD) << "Image request error:" << job->errorText();
0229         Q_EMIT error(this);
0230         return;
0231     }
0232 
0233     m_image = QImage::fromData(job->data());
0234 
0235     // Visit the photo page to get the author
0236     if (!m_infoUrl.isEmpty()) {
0237         KIO::StoredTransferJob *pageJob = KIO::storedGet(m_infoUrl, KIO::NoReload, KIO::HideProgressInfo);
0238         connect(pageJob, &KIO::StoredTransferJob::finished, this, &FlickrProvider::pageRequestFinished);
0239     } else {
0240         // No information is fine
0241         Q_EMIT finished(this, m_image);
0242     }
0243 }
0244 
0245 void FlickrProvider::pageRequestFinished(KJob *_job)
0246 {
0247     KIO::StoredTransferJob *job = static_cast<KIO::StoredTransferJob *>(_job);
0248     if (job->error()) {
0249         qCWarning(WALLPAPERPOTD) << "No author available";
0250         Q_EMIT finished(this, m_image);
0251         return;
0252     }
0253 
0254     const QString data = QString::fromUtf8(job->data()).simplified();
0255 
0256     // Example: <a href="/photos/jellybeanzgallery/" class="owner-name truncate" title="Go to Hammerchewer&#x27;s photostream"
0257     // data-track="attributionNameClick">Hammerchewer</a>
0258     QRegularExpression authorRegEx(QStringLiteral("<a.*?class=\"owner-name truncate\".*?>(.+?)</a>"));
0259     QRegularExpressionMatch match = authorRegEx.match(data);
0260 
0261     if (match.hasMatch()) {
0262         m_author = QTextDocumentFragment::fromHtml(match.captured(1).trimmed()).toPlainText();
0263     }
0264 
0265     Q_EMIT finished(this, m_image);
0266 }
0267 
0268 K_PLUGIN_CLASS_WITH_JSON(FlickrProvider, "flickrprovider.json")
0269 
0270 #include "flickrprovider.moc"