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'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"