File indexing completed on 2024-05-12 16:45:52
0001 /*************************************************************************** 0002 Copyright (C) 2017-2020 Robby Stephenson <robby@periapsis.org> 0003 ***************************************************************************/ 0004 0005 /*************************************************************************** 0006 * * 0007 * This program is free software; you can redistribute it and/or * 0008 * modify it under the terms of the GNU General Public License as * 0009 * published by the Free Software Foundation; either version 2 of * 0010 * the License or (at your option) version 3 or any later version * 0011 * accepted by the membership of KDE e.V. (or its successor approved * 0012 * by the membership of KDE e.V.), which shall act as a proxy * 0013 * defined in Section 14 of version 3 of the license. * 0014 * * 0015 * This program is distributed in the hope that it will be useful, * 0016 * but WITHOUT ANY WARRANTY; without even the implied warranty of * 0017 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * 0018 * GNU General Public License for more details. * 0019 * * 0020 * You should have received a copy of the GNU General Public License * 0021 * along with this program. If not, see <http://www.gnu.org/licenses/>. * 0022 * * 0023 ***************************************************************************/ 0024 0025 #include "igdbfetcher.h" 0026 #include "../collections/gamecollection.h" 0027 #include "../images/imagefactory.h" 0028 #include "../core/filehandler.h" 0029 #include "../utils/guiproxy.h" 0030 #include "../utils/string_utils.h" 0031 #include "../utils/tellico_utils.h" 0032 #include "../core/tellico_strings.h" 0033 #include "../tellico_debug.h" 0034 0035 #include <KLocalizedString> 0036 #include <KConfigGroup> 0037 #include <KJob> 0038 #include <KJobUiDelegate> 0039 #include <KJobWidgets/KJobWidgets> 0040 #include <KIO/StoredTransferJob> 0041 0042 #include <QUrl> 0043 #include <QUrlQuery> 0044 #include <QLabel> 0045 #include <QFile> 0046 #include <QTextStream> 0047 #include <QVBoxLayout> 0048 #include <QTextCodec> 0049 #include <QJsonDocument> 0050 #include <QJsonArray> 0051 #include <QJsonObject> 0052 #include <QThread> 0053 #include <QTimer> 0054 0055 namespace { 0056 static const int IGDB_MAX_RETURNS_TOTAL = 20; 0057 static const char* IGDB_API_URL = "https://api.igdb.com/v4"; 0058 static const char* IGDB_CLIENT_ID = "hc7jojgdmkcc6divxmz0mxzzt22ehr"; 0059 static const char* IGDB_TOKEN_URL = "https://api.tellico-project.org/igdb/"; 0060 } 0061 0062 using namespace Tellico; 0063 using Tellico::Fetch::IGDBFetcher; 0064 0065 IGDBFetcher::IGDBFetcher(QObject* parent_) 0066 : Fetcher(parent_) 0067 , m_started(false) { 0068 m_requestTimer.start(); 0069 // delay reading the platform names from the cache file 0070 QTimer::singleShot(0, this, &IGDBFetcher::populateHashes); 0071 } 0072 0073 IGDBFetcher::~IGDBFetcher() { 0074 } 0075 0076 QString IGDBFetcher::source() const { 0077 return m_name.isEmpty() ? defaultName() : m_name; 0078 } 0079 0080 QString IGDBFetcher::attribution() const { 0081 return i18n(providedBy, QLatin1String("https://igdb.com"), QLatin1String("IGDB.com")); 0082 } 0083 0084 bool IGDBFetcher::canSearch(Fetch::FetchKey k) const { 0085 return k == Keyword; 0086 } 0087 0088 bool IGDBFetcher::canFetch(int type) const { 0089 return type == Data::Collection::Game; 0090 } 0091 0092 void IGDBFetcher::readConfigHook(const KConfigGroup& config_) { 0093 const QString k = config_.readEntry("Access Token"); 0094 if(!k.isEmpty()) { 0095 m_accessToken = k; 0096 } 0097 m_accessTokenExpires = config_.readEntry("Access Token Expires", QDateTime()); 0098 } 0099 0100 void IGDBFetcher::saveConfigHook(KConfigGroup& config_) { 0101 config_.writeEntry("Access Token", m_accessToken); 0102 config_.writeEntry("Access Token Expires", m_accessTokenExpires); 0103 } 0104 0105 void IGDBFetcher::search() { 0106 continueSearch(); 0107 } 0108 0109 void IGDBFetcher::continueSearch() { 0110 m_started = true; 0111 0112 QUrl u(QString::fromLatin1(IGDB_API_URL)); 0113 u.setPath(u.path() + QStringLiteral("/games")); 0114 0115 QStringList clauseList; 0116 switch(request().key()) { 0117 case Keyword: 0118 clauseList += QString(QStringLiteral("search \"%1\";")).arg(request().value()); 0119 break; 0120 0121 default: 0122 myWarning() << "key not recognized:" << request().key(); 0123 stop(); 0124 return; 0125 } 0126 clauseList += QStringLiteral("fields *,cover.url,age_ratings.*,involved_companies.*;"); 0127 // exclude some of the bigger unused fields 0128 clauseList += QStringLiteral("exclude keywords,screenshots,tags;"); 0129 clauseList += QString(QStringLiteral("limit %1;")).arg(QString::number(IGDB_MAX_RETURNS_TOTAL)); 0130 // myDebug() << u << clauseList.join(QStringLiteral(" ")); 0131 0132 m_job = igdbJob(u, clauseList.join(QStringLiteral(" "))); 0133 connect(m_job.data(), &KJob::result, this, &IGDBFetcher::slotComplete); 0134 markTime(); 0135 } 0136 0137 void IGDBFetcher::stop() { 0138 if(!m_started) { 0139 return; 0140 } 0141 if(m_job) { 0142 m_job->kill(); 0143 m_job = nullptr; 0144 } 0145 m_started = false; 0146 emit signalDone(this); 0147 } 0148 0149 Tellico::Data::EntryPtr IGDBFetcher::fetchEntryHook(uint uid_) { 0150 if(!m_entries.contains(uid_)) { 0151 myDebug() << "no entry ptr"; 0152 return Data::EntryPtr(); 0153 } 0154 0155 Data::EntryPtr entry = m_entries.value(uid_); 0156 0157 // image might still be a URL 0158 const QString image_id = entry->field(QStringLiteral("cover")); 0159 if(image_id.contains(QLatin1Char('/'))) { 0160 const QString id = ImageFactory::addImage(QUrl::fromUserInput(image_id), true /* quiet */); 0161 if(id.isEmpty()) { 0162 message(i18n("The cover image could not be loaded."), MessageHandler::Warning); 0163 } 0164 // empty image ID is ok 0165 entry->setField(QStringLiteral("cover"), id); 0166 } 0167 0168 return entry; 0169 } 0170 0171 Tellico::Fetch::FetchRequest IGDBFetcher::updateRequest(Data::EntryPtr entry_) { 0172 QString title = entry_->field(QStringLiteral("title")); 0173 if(!title.isEmpty()) { 0174 return FetchRequest(Keyword, title); 0175 } 0176 return FetchRequest(); 0177 } 0178 0179 void IGDBFetcher::slotComplete(KJob* job_) { 0180 KIO::StoredTransferJob* job = static_cast<KIO::StoredTransferJob*>(job_); 0181 0182 if(job->error()) { 0183 job->uiDelegate()->showErrorMessage(); 0184 stop(); 0185 return; 0186 } 0187 0188 const QByteArray data = job->data(); 0189 if(data.isEmpty()) { 0190 myDebug() << "no data"; 0191 stop(); 0192 return; 0193 } 0194 // see bug 319662. If fetcher is cancelled, job is killed 0195 // if the pointer is retained, it gets double-deleted 0196 m_job = nullptr; 0197 0198 #if 0 0199 myWarning() << "Remove debug from igdbfetcher.cpp"; 0200 QFile file(QStringLiteral("/tmp/test.json")); 0201 if(file.open(QIODevice::WriteOnly)) { 0202 QTextStream t(&file); 0203 t.setCodec("UTF-8"); 0204 t << data; 0205 } 0206 file.close(); 0207 #endif 0208 0209 QJsonDocument doc = QJsonDocument::fromJson(data); 0210 if(doc.isObject()) { 0211 // probably an error message 0212 QJsonObject obj = doc.object(); 0213 const QString msg = obj.value(QLatin1String("message")).toString(); 0214 myDebug() << "IGDBFetcher -" << msg; 0215 message(msg, MessageHandler::Error); 0216 stop(); 0217 return; 0218 } 0219 0220 Data::CollPtr coll(new Data::GameCollection(true)); 0221 if(optionalFields().contains(QStringLiteral("pegi"))) { 0222 coll->addField(Data::Field::createDefaultField(Data::Field::PegiField)); 0223 } 0224 if(optionalFields().contains(QStringLiteral("igdb"))) { 0225 Data::FieldPtr field(new Data::Field(QStringLiteral("igdb"), i18n("IGDB Link"), Data::Field::URL)); 0226 field->setCategory(i18n("General")); 0227 coll->addField(field); 0228 } 0229 0230 foreach(const QVariant& result, doc.array().toVariantList()) { 0231 QVariantMap resultMap = result.toMap(); 0232 Data::EntryPtr baseEntry(new Data::Entry(coll)); 0233 populateEntry(baseEntry, resultMap); 0234 0235 // for multiple platforms, return a result for each one 0236 QVariantList platforms = resultMap.value(QStringLiteral("platforms")).toList(); 0237 foreach(const QVariant pVariant, platforms) { 0238 Data::EntryPtr entry(new Data::Entry(*baseEntry)); 0239 const int pId = pVariant.toInt(); 0240 if(!m_platformHash.contains(pId)) { 0241 readDataList(Platform); 0242 } 0243 const QString platform = Data::GameCollection::normalizePlatform(m_platformHash.value(pId)); 0244 // make the assumption that if the platform name isn't already in the allowed list, it should be added 0245 Data::FieldPtr f = coll->fieldByName(QStringLiteral("platform")); 0246 if(f && !f->allowed().contains(platform)) { 0247 f->setAllowed(QStringList(f->allowed()) << platform); 0248 } 0249 entry->setField(QStringLiteral("platform"), platform); 0250 FetchResult* r = new FetchResult(this, entry); 0251 m_entries.insert(r->uid, entry); 0252 emit signalResultFound(r); 0253 } 0254 0255 // also allow case of no platform 0256 if(platforms.isEmpty()) { 0257 FetchResult* r = new FetchResult(this, baseEntry); 0258 m_entries.insert(r->uid, baseEntry); 0259 emit signalResultFound(r); 0260 } 0261 } 0262 0263 stop(); 0264 } 0265 0266 void IGDBFetcher::populateEntry(Data::EntryPtr entry_, const QVariantMap& resultMap_) { 0267 entry_->setField(QStringLiteral("title"), mapValue(resultMap_, "name")); 0268 entry_->setField(QStringLiteral("description"), mapValue(resultMap_, "summary")); 0269 0270 QString cover = mapValue(resultMap_, "cover", "url"); 0271 if(cover.startsWith(QLatin1Char('/'))) { 0272 cover.prepend(QStringLiteral("https:")); 0273 } 0274 entry_->setField(QStringLiteral("cover"), cover); 0275 0276 QVariantList genreIDs = resultMap_.value(QStringLiteral("genres")).toList(); 0277 QStringList genres; 0278 foreach(const QVariant& id, genreIDs) { 0279 const int genreId = id.toInt(); 0280 if(!m_genreHash.contains(genreId)) { 0281 readDataList(Genre); 0282 } 0283 const QString genre = m_genreHash.value(genreId); 0284 if(!genre.isEmpty()) { 0285 genres << genre; 0286 } 0287 } 0288 entry_->setField(QStringLiteral("genre"), genres.join(FieldFormat::delimiterString())); 0289 0290 qlonglong release_t = mapValue(resultMap_, "first_release_date").toLongLong(); 0291 if(release_t > 0) { 0292 // could use QDateTime::fromSecsSinceEpoch but that was introduced in Qt 5.8 0293 // while I still support Qt 5.6, in theory... 0294 QDateTime dt = QDateTime::fromMSecsSinceEpoch(release_t * 1000); 0295 entry_->setField(QStringLiteral("year"), QString::number(dt.date().year())); 0296 } 0297 0298 const QVariantList ageRatingList = resultMap_.value(QStringLiteral("age_ratings")).toList(); 0299 foreach(const QVariant& ageRating, ageRatingList) { 0300 const QVariantMap ratingMap = ageRating.toMap(); 0301 // per Age Rating Enums, ESRB==1, PEGI==2 0302 const int category = ratingMap.value(QStringLiteral("category")).toInt(); 0303 const int rating = ratingMap.value(QStringLiteral("rating")).toInt(); 0304 if(category == 1) { 0305 if(m_esrbHash.contains(rating)) { 0306 entry_->setField(QStringLiteral("certification"), m_esrbHash.value(rating)); 0307 } else { 0308 myDebug() << "No ESRB rating for value =" << rating; 0309 } 0310 } else if(category == 2 && optionalFields().contains(QStringLiteral("pegi"))) { 0311 entry_->setField(QStringLiteral("pegi"), m_pegiHash.value(rating)); 0312 } 0313 } 0314 0315 const QVariantList companyList = resultMap_.value(QStringLiteral("involved_companies")).toList(); 0316 0317 QList<int>companyIdList; 0318 foreach(const QVariant& company, companyList) { 0319 const QVariantMap companyMap = company.toMap(); 0320 const int companyId = companyMap.value(QStringLiteral("company")).toInt(); 0321 if(!m_companyHash.contains(companyId)) { 0322 companyIdList += companyId; 0323 } 0324 } 0325 if(!companyIdList.isEmpty()) { 0326 readDataList(Company, companyIdList); 0327 } 0328 0329 QStringList pubs, devs; 0330 foreach(const QVariant& company, companyList) { 0331 const QVariantMap companyMap = company.toMap(); 0332 const int companyId = companyMap.value(QStringLiteral("company")).toInt(); 0333 const QString companyName = m_companyHash.value(companyId); 0334 if(companyName.isEmpty()) { 0335 continue; 0336 } 0337 if(companyMap.value(QStringLiteral("publisher")).toBool()) { 0338 pubs += companyName; 0339 } else if(companyMap.value(QStringLiteral("developer")).toBool()) { 0340 devs += companyName; 0341 } 0342 } 0343 entry_->setField(QStringLiteral("publisher"), pubs.join(FieldFormat::delimiterString())); 0344 entry_->setField(QStringLiteral("developer"), devs.join(FieldFormat::delimiterString())); 0345 0346 if(optionalFields().contains(QStringLiteral("igdb"))) { 0347 entry_->setField(QStringLiteral("igdb"), mapValue(resultMap_, "url")); 0348 } 0349 } 0350 0351 Tellico::Fetch::ConfigWidget* IGDBFetcher::configWidget(QWidget* parent_) const { 0352 return new IGDBFetcher::ConfigWidget(parent_, this); 0353 } 0354 0355 // Use member hash for certain field names for now. 0356 // Don't expect IGDB values to change. This avoids exponentially multiplying the number of API calls 0357 void IGDBFetcher::populateHashes() { 0358 QFile genreFile(dataFileName(Genre)); 0359 if(genreFile.open(QIODevice::ReadOnly)) { 0360 updateData(Genre, genreFile.readAll()); 0361 } else if(genreFile.exists()) { // don't want errors for non-existing file 0362 myDebug() << "Failed to read genres from" << genreFile.fileName() << genreFile.errorString(); 0363 } 0364 0365 QFile platformFile(dataFileName(Platform)); 0366 if(platformFile.open(QIODevice::ReadOnly)) { 0367 updateData(Platform, platformFile.readAll()); 0368 } else if(platformFile.exists()) { // don't want errors for non-existing file 0369 myDebug() << "Failed to read from" << platformFile.fileName() << platformFile.errorString(); 0370 } 0371 0372 QFile companyFile(dataFileName(Company)); 0373 if(companyFile.open(QIODevice::ReadOnly)) { 0374 updateData(Company, companyFile.readAll()); 0375 } else if(companyFile.exists()) { // don't want errors for non-existing file 0376 myDebug() << "Failed to read from" << companyFile.fileName() << companyFile.errorString(); 0377 } 0378 0379 // grab i18n values for ESRB from default collection 0380 Data::CollPtr c(new Data::GameCollection(true)); 0381 QStringList esrb = c->fieldByName(QStringLiteral("certification"))->allowed(); 0382 Q_ASSERT(esrb.size() == 8); 0383 while(esrb.size() < 8) { 0384 esrb << QString(); 0385 } 0386 // see https://api-docs.igdb.com/#age-rating 0387 m_esrbHash.insert(12, esrb.at(1)); // adults only 0388 m_esrbHash.insert(11, esrb.at(2)); // mature 0389 m_esrbHash.insert(10, esrb.at(3)); // teen 0390 m_esrbHash.insert(9, esrb.at(4)); // e10 0391 m_esrbHash.insert(8, esrb.at(5)); // everyone 0392 m_esrbHash.insert(7, esrb.at(6)); // early childhood 0393 m_esrbHash.insert(6, esrb.at(7)); // pending 0394 0395 m_pegiHash.insert(1, QStringLiteral("PEGI 3")); 0396 m_pegiHash.insert(2, QStringLiteral("PEGI 7")); 0397 m_pegiHash.insert(3, QStringLiteral("PEGI 12")); 0398 m_pegiHash.insert(4, QStringLiteral("PEGI 16")); 0399 m_pegiHash.insert(5, QStringLiteral("PEGI 18")); 0400 } 0401 0402 void IGDBFetcher::updateData(IgdbDataType dataType_, const QByteArray& jsonData_) { 0403 const QString idString(QStringLiteral("id")); 0404 const QString nmString(QStringLiteral("name")); 0405 QHash<int, QString> dataHash; 0406 const QJsonArray array = QJsonDocument::fromJson(jsonData_).array(); 0407 for(int i = 0; i < array.size(); ++i) { 0408 QJsonObject obj = array.at(i).toObject(); 0409 dataHash.insert(obj.value(idString).toInt(), 0410 obj.value(nmString).toString()); 0411 } 0412 0413 // transfer read data into the correct local variable 0414 switch(dataType_) { 0415 case Genre: 0416 m_genreHash = dataHash; 0417 break; 0418 case Platform: 0419 m_platformHash = dataHash; 0420 break; 0421 case Company: 0422 // company list is bigger than request size, so rather than downloading all names 0423 // have to do it in chunks and then merge 0424 #if (QT_VERSION < QT_VERSION_CHECK(5, 15, 0)) 0425 m_companyHash.unite(dataHash); 0426 #else 0427 m_companyHash.insert(dataHash); 0428 #endif 0429 break; 0430 } 0431 } 0432 0433 void IGDBFetcher::readDataList(IgdbDataType dataType_, const QList<int>& idList_) { 0434 QUrl u(QString::fromLatin1(IGDB_API_URL)); 0435 switch(dataType_) { 0436 case Genre: 0437 u.setPath(u.path() + QStringLiteral("/genres")); 0438 break; 0439 case Platform: 0440 u.setPath(u.path() + QStringLiteral("/platforms")); 0441 break; 0442 case Company: 0443 u.setPath(u.path() + QStringLiteral("/companies")); 0444 break; 0445 } 0446 0447 QStringList clauseList; 0448 clauseList += QStringLiteral("fields id,name;"); 0449 // if the id list is not empty, seach for specific data id values 0450 if(!idList_.isEmpty()) { 0451 // where id = (8,9,11); 0452 QString clause = QStringLiteral("where id = (") + QString::number(idList_.at(0)); 0453 for(int i = 1; i < idList_.size(); ++i) { 0454 clause += (QLatin1String(",") + QString::number(idList_.at(i))); 0455 } 0456 clause += QLatin1String(");"); 0457 clauseList += clause; 0458 } 0459 clauseList += QStringLiteral("limit 500;"); // biggest limit is 500 which should be enough for all 0460 0461 QPointer<KIO::StoredTransferJob> job = igdbJob(u, clauseList.join(QStringLiteral(" "))); 0462 markTime(); 0463 if(!job->exec()) { 0464 myDebug() << "IGDB: data request failed"; 0465 myDebug() << job->errorString() << u; 0466 return; 0467 } 0468 0469 const QByteArray data = job->data(); 0470 updateData(dataType_, data); 0471 0472 // now save the date, but instead of just writing the job->data() to the file 0473 // since the company data may have been merged, write the full set of hash values 0474 QByteArray dataToWrite; 0475 if(dataType_ == Company) { 0476 QJsonArray array; 0477 const QString idString(QStringLiteral("id")); 0478 const QString nmString(QStringLiteral("name")); 0479 QHashIterator<int, QString> it(m_companyHash); 0480 while(it.hasNext()) { 0481 it.next(); 0482 QJsonObject obj; 0483 obj.insert(idString, it.key()); 0484 obj.insert(nmString, it.value()); 0485 array.append(obj); 0486 } 0487 dataToWrite = QJsonDocument(array).toJson(); 0488 } else { 0489 dataToWrite = data; 0490 } 0491 0492 QFile file(dataFileName(dataType_)); 0493 if(!file.open(QIODevice::WriteOnly) || file.write(dataToWrite) == -1) { 0494 myDebug() << "unable to write to" << file.fileName() << file.errorString(); 0495 return; 0496 } 0497 file.close(); 0498 } 0499 0500 void IGDBFetcher::markTime() const { 0501 // rate limit is 4 requests per second 0502 if(m_requestTimer.elapsed() < 250) QThread::msleep(250); 0503 m_requestTimer.restart(); 0504 } 0505 0506 void IGDBFetcher::checkAccessToken() { 0507 const QDateTime now = QDateTime::currentDateTimeUtc(); 0508 if(!m_accessToken.isEmpty() && m_accessTokenExpires > now) { 0509 // access token should be fine, nothing to do 0510 return; 0511 } 0512 0513 QUrl u(QString::fromLatin1(IGDB_TOKEN_URL)); 0514 // myDebug() << "Downloading IGDN token from" << u.toString(); 0515 QPointer<KIO::StoredTransferJob> job = KIO::storedHttpPost(QByteArray(), u, KIO::HideProgressInfo); 0516 job->addMetaData(QStringLiteral("accept"), QStringLiteral("application/json")); 0517 KJobWidgets::setWindow(job, GUI::Proxy::widget()); 0518 if(!job->exec()) { 0519 myDebug() << "IGDB: access token request failed"; 0520 myDebug() << job->errorString() << u; 0521 return; 0522 } 0523 0524 QJsonDocument doc = QJsonDocument::fromJson(job->data()); 0525 if(doc.isNull()) { 0526 myDebug() << "IGDB: Invalid JSON"; 0527 return; 0528 } 0529 QJsonObject response = doc.object(); 0530 if(response.contains(QLatin1String("message"))) { 0531 myDebug() << "IGDB:" << response.value(QLatin1String("message")).toString(); 0532 } 0533 m_accessToken = response.value(QLatin1String("access_token")).toString(); 0534 const int expires = response.value(QLatin1String("expires_in")).toInt(); 0535 if(expires > 0) { 0536 m_accessTokenExpires = now.addSecs(expires); 0537 } 0538 // myDebug() << "Received access token" << m_accessToken << m_accessTokenExpires; 0539 } 0540 0541 QString IGDBFetcher::defaultName() { 0542 return i18n("Internet Game Database (IGDB.com)"); 0543 } 0544 0545 QString IGDBFetcher::defaultIcon() { 0546 return favIcon("http://www.igdb.com"); 0547 } 0548 0549 Tellico::StringHash IGDBFetcher::allOptionalFields() { 0550 StringHash hash; 0551 hash[QStringLiteral("pegi")] = i18n("PEGI Rating"); 0552 hash[QStringLiteral("igdb")] = i18n("IGDB Link"); 0553 return hash; 0554 } 0555 0556 QString IGDBFetcher::dataFileName(IgdbDataType dataType_) { 0557 const QString dataDir = Tellico::saveLocation(QStringLiteral("igdb-data/")); 0558 QString fileName; 0559 switch(dataType_) { 0560 case Genre: 0561 fileName = dataDir + QLatin1String("genres.json"); 0562 break; 0563 case Platform: 0564 fileName = dataDir + QLatin1String("platforms.json"); 0565 break; 0566 case Company: 0567 fileName = dataDir + QLatin1String("companies.json"); 0568 break; 0569 } 0570 return fileName; 0571 } 0572 0573 QPointer<KIO::StoredTransferJob> IGDBFetcher::igdbJob(const QUrl& url_, const QString& query_) { 0574 checkAccessToken(); 0575 QPointer<KIO::StoredTransferJob> job = KIO::storedHttpPost(query_.toUtf8(), url_, KIO::HideProgressInfo); 0576 QStringList customHeaders; 0577 customHeaders += (QStringLiteral("Client-ID: ") + QString::fromLatin1(IGDB_CLIENT_ID)); 0578 customHeaders += (QStringLiteral("Authorization: ") + QLatin1String("Bearer ") + m_accessToken); 0579 job->addMetaData(QStringLiteral("customHTTPHeader"), customHeaders.join(QLatin1String("\r\n"))); 0580 job->addMetaData(QStringLiteral("accept"), QStringLiteral("application/json")); 0581 KJobWidgets::setWindow(job, GUI::Proxy::widget()); 0582 return job; 0583 } 0584 0585 IGDBFetcher::ConfigWidget::ConfigWidget(QWidget* parent_, const IGDBFetcher* fetcher_) 0586 : Fetch::ConfigWidget(parent_) { 0587 QVBoxLayout* l = new QVBoxLayout(optionsWidget()); 0588 l->addWidget(new QLabel(i18n("This source has no options."), optionsWidget())); 0589 l->addStretch(); 0590 0591 // now add additional fields widget 0592 addFieldsWidget(IGDBFetcher::allOptionalFields(), fetcher_ ? fetcher_->optionalFields() : QStringList()); 0593 } 0594 0595 void IGDBFetcher::ConfigWidget::saveConfigHook(KConfigGroup&) { 0596 } 0597 0598 QString IGDBFetcher::ConfigWidget::preferredName() const { 0599 return IGDBFetcher::defaultName(); 0600 }