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 }