File indexing completed on 2024-05-12 05:09:35

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