File indexing completed on 2024-05-12 16:46:00

0001 /***************************************************************************
0002     Copyright (C) 2012-2019 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 "thegamesdbfetcher.h"
0026 #include "../collections/gamecollection.h"
0027 #include "../images/imagefactory.h"
0028 #include "../gui/combobox.h"
0029 #include "../core/filehandler.h"
0030 #include "../utils/guiproxy.h"
0031 #include "../utils/string_utils.h"
0032 #include "../utils/tellico_utils.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 <QLabel>
0043 #include <QLineEdit>
0044 #include <QFile>
0045 #include <QTextStream>
0046 #include <QGridLayout>
0047 #include <QTextCodec>
0048 #include <QJsonDocument>
0049 #include <QJsonArray>
0050 #include <QJsonObject>
0051 #include <QUrlQuery>
0052 #include <QTimer>
0053 
0054 namespace {
0055   static const int THEGAMESDB_MAX_RETURNS_TOTAL = 20;
0056   static const char* THEGAMESDB_API_URL = "https://api.thegamesdb.net";
0057   static const char* THEGAMESDB_API_VERSION = "1"; // krazy:exclude=doublequote_chars
0058   static const char* THEGAMESDB_MAGIC_TOKEN = "f7c4fd9c5d6d4a2fcefe3157192f87e260038abe86b0f3977716596edaebdbb82315586e98fc88b0fb9ff4c01576e4d47b4e556d487a4325221abbddfac36f59d7e114753b5fa6c77a1e73423d5f72460f3b526bcbae4f2be0d86a5854600436784e3a5c5d6bc1a3e2d395f798fb35073051f2c232014023e9dda99edfea5767";
0059 }
0060 
0061 using namespace Tellico;
0062 using Tellico::Fetch::TheGamesDBFetcher;
0063 
0064 TheGamesDBFetcher::TheGamesDBFetcher(QObject* parent_)
0065     : Fetcher(parent_)
0066     , m_started(false)
0067     , m_imageSize(SmallImage) {
0068   m_apiKey = Tellico::reverseObfuscate(THEGAMESDB_MAGIC_TOKEN);
0069   // delay reading the platform names from the cache file
0070   QTimer::singleShot(0, this, &TheGamesDBFetcher::loadCachedData);
0071 }
0072 
0073 TheGamesDBFetcher::~TheGamesDBFetcher() {
0074 }
0075 
0076 QString TheGamesDBFetcher::source() const {
0077   return m_name.isEmpty() ? defaultName() : m_name;
0078 }
0079 
0080 bool TheGamesDBFetcher::canSearch(Fetch::FetchKey k) const {
0081   return k == Title;
0082 }
0083 
0084 bool TheGamesDBFetcher::canFetch(int type) const {
0085   return type == Data::Collection::Game;
0086 }
0087 
0088 void TheGamesDBFetcher::readConfigHook(const KConfigGroup& config_) {
0089   const QString k = config_.readEntry("API Key");
0090   if(!k.isEmpty()) {
0091     m_apiKey = k;
0092   }
0093   const int imageSize = config_.readEntry("Image Size", -1);
0094   if(imageSize > -1) {
0095     m_imageSize = static_cast<ImageSize>(imageSize);
0096   }
0097 }
0098 
0099 void TheGamesDBFetcher::search() {
0100   m_started = true;
0101 
0102   if(m_apiKey.isEmpty()) {
0103     myDebug() << "empty API key";
0104     message(i18n("An access key is required to use this data source.")
0105             + QLatin1Char(' ') +
0106             i18n("Those values must be entered in the data source settings."), MessageHandler::Error);
0107     stop();
0108     return;
0109   }
0110 
0111   QUrl u(QString::fromLatin1(THEGAMESDB_API_URL));
0112   u.setPath(QLatin1String("/v") + QLatin1String(THEGAMESDB_API_VERSION));
0113 
0114   switch(request().key()) {
0115     case Title:
0116       u = u.adjusted(QUrl::StripTrailingSlash);
0117       u.setPath(u.path() + QLatin1String("/Games/ByGameName"));
0118       {
0119         QUrlQuery q;
0120         q.addQueryItem(QStringLiteral("apikey"), m_apiKey);
0121         if(optionalFields().contains(QStringLiteral("num-player"))) {
0122           q.addQueryItem(QStringLiteral("fields"), QStringLiteral("players,rating,publishers,genres,overview,platform"));
0123         } else {
0124           q.addQueryItem(QStringLiteral("fields"), QStringLiteral("rating,publishers,genres,overview,platform"));
0125         }
0126         q.addQueryItem(QStringLiteral("include"), QStringLiteral("platform,boxart"));
0127         q.addQueryItem(QStringLiteral("name"), request().value());
0128         if(!request().data().isEmpty()) {
0129           q.addQueryItem(QStringLiteral("filter[platform]"), request().data());
0130         }
0131         u.setQuery(q);
0132       }
0133       break;
0134 
0135     default:
0136       myWarning() << "key not recognized:" << request().key();
0137       stop();
0138       return;
0139   }
0140 //  u = QUrl::fromLocalFile(QStringLiteral("/tmp/test-tgdb.json"));
0141 //  myDebug() << u;
0142 
0143   m_job = KIO::storedGet(u, KIO::NoReload, KIO::HideProgressInfo);
0144   KJobWidgets::setWindow(m_job, GUI::Proxy::widget());
0145   connect(m_job.data(), &KJob::result, this, &TheGamesDBFetcher::slotComplete);
0146 }
0147 
0148 void TheGamesDBFetcher::stop() {
0149   if(!m_started) {
0150     return;
0151   }
0152   if(m_job) {
0153     m_job->kill();
0154     m_job = nullptr;
0155   }
0156   m_started = false;
0157   emit signalDone(this);
0158 }
0159 
0160 Tellico::Data::EntryPtr TheGamesDBFetcher::fetchEntryHook(uint uid_) {
0161   Data::EntryPtr entry = m_entries.value(uid_);
0162   if(!entry) {
0163     myWarning() << "no entry in dict";
0164     return Data::EntryPtr();
0165   }
0166 
0167   // image might still be a URL
0168   const QString image_id = entry->field(QStringLiteral("cover"));
0169   if(image_id.contains(QLatin1Char('/'))) {
0170     const QString id = ImageFactory::addImage(QUrl::fromUserInput(image_id), true /* quiet */);
0171     if(id.isEmpty()) {
0172       message(i18n("The cover image could not be loaded."), MessageHandler::Warning);
0173     }
0174     // empty image ID is ok
0175     entry->setField(QStringLiteral("cover"), id);
0176   }
0177 
0178   // don't want to include TGDb ID field
0179   entry->setField(QStringLiteral("tgdb-id"), QString());
0180 
0181   return entry;
0182 }
0183 
0184 Tellico::Fetch::FetchRequest TheGamesDBFetcher::updateRequest(Data::EntryPtr entry_) {
0185   const QString platform = entry_->field(QStringLiteral("platform"));
0186   int platformId = -1;
0187   // if the platform id is available, it can be used to filter the update search
0188   if(!platform.isEmpty()) {
0189     for(auto i = m_platforms.constBegin(); i != m_platforms.constEnd(); ++i) {
0190       if(i.value() == platform) {
0191         platformId = i.key();
0192         break;
0193       }
0194     }
0195   }
0196 
0197   const QString title = entry_->field(QStringLiteral("title"));
0198   if(!title.isEmpty()) {
0199     FetchRequest req(Title, title);
0200     if(platformId > -1) {
0201       req.setData(QString::number(platformId));
0202     }
0203     return req;
0204   }
0205   return FetchRequest();
0206 }
0207 
0208 void TheGamesDBFetcher::slotComplete(KJob* job_) {
0209   KIO::StoredTransferJob* job = static_cast<KIO::StoredTransferJob*>(job_);
0210 
0211   if(job->error()) {
0212     job->uiDelegate()->showErrorMessage();
0213     stop();
0214     return;
0215   }
0216 
0217   const QByteArray data = job->data();
0218   if(data.isEmpty()) {
0219     myDebug() << "no data";
0220     stop();
0221     return;
0222   }
0223   // see bug 319662. If fetcher is cancelled, job is killed
0224   // if the pointer is retained, it gets double-deleted
0225   m_job = nullptr;
0226 
0227 #if 0
0228   myWarning() << "Remove debug from thegamesdbfetcher.cpp";
0229   QFile f(QStringLiteral("/tmp/test-tgdb.json"));
0230   if(f.open(QIODevice::WriteOnly)) {
0231     QTextStream t(&f);
0232     t.setCodec("UTF-8");
0233     t << data;
0234   }
0235   f.close();
0236 #endif
0237 
0238   Data::CollPtr coll(new Data::GameCollection(true));
0239   // always add the tgdb-id for fetchEntryHook
0240   Data::FieldPtr field(new Data::Field(QStringLiteral("tgdb-id"), QStringLiteral("TGDb ID"), Data::Field::Line));
0241   field->setCategory(i18n("General"));
0242   coll->addField(field);
0243 
0244   if(optionalFields().contains(QStringLiteral("num-player"))) {
0245     Data::FieldPtr field(new Data::Field(QStringLiteral("num-player"), i18n("Number of Players"), Data::Field::Number));
0246     field->setCategory(i18n("General"));
0247     field->setFlags(Data::Field::AllowMultiple | Data::Field::AllowGrouped);
0248     coll->addField(field);
0249   }
0250 
0251   QVariantMap topLevelMap = QJsonDocument::fromJson(data).object().toVariantMap();
0252   if(!topLevelMap.contains(QStringLiteral("data"))) {
0253     myDebug() << "No data in result!";
0254   }
0255   readPlatformList(topLevelMap.value(QStringLiteral("include")).toMap()
0256                               .value(QStringLiteral("platform")).toMap());
0257   readCoverList(topLevelMap.value(QStringLiteral("include")).toMap()
0258                            .value(QStringLiteral("boxart")).toMap());
0259 
0260   QVariantList resultList = topLevelMap.value(QStringLiteral("data")).toMap()
0261                                        .value(QStringLiteral("games")).toList();
0262   if(resultList.isEmpty()) {
0263     myDebug() << "no results";
0264     stop();
0265     return;
0266   }
0267 
0268   int count = 0;
0269   foreach(const QVariant& result, resultList) {
0270     Data::EntryPtr entry(new Data::Entry(coll));
0271     populateEntry(entry, result.toMap());
0272 
0273     FetchResult* r = new FetchResult(this, entry);
0274     m_entries.insert(r->uid, entry);
0275     emit signalResultFound(r);
0276     ++count;
0277     if(count >= THEGAMESDB_MAX_RETURNS_TOTAL) {
0278       break;
0279     }
0280   }
0281 
0282   stop();
0283 }
0284 
0285 void TheGamesDBFetcher::populateEntry(Data::EntryPtr entry_, const QVariantMap& resultMap_) {
0286   entry_->setField(QStringLiteral("tgdb-id"), mapValue(resultMap_, "id"));
0287   entry_->setField(QStringLiteral("title"), mapValue(resultMap_, "game_title"));
0288   entry_->setField(QStringLiteral("year"),  mapValue(resultMap_, "release_date").left(4));
0289   entry_->setField(QStringLiteral("description"), mapValue(resultMap_, "overview"));
0290 
0291   const int platformId = mapValue(resultMap_, "platform").toInt();
0292   if(m_platforms.contains(platformId)) {
0293     const QString platform = m_platforms[platformId];
0294     // make the assumption that if the platform name isn't already in the allowed list, it should be added
0295     Data::FieldPtr f = entry_->collection()->fieldByName(QStringLiteral("platform"));
0296     if(f && !f->allowed().contains(platform)) {
0297       f->setAllowed(QStringList(f->allowed()) << platform);
0298     }
0299     entry_->setField(QStringLiteral("platform"), platform);
0300   }
0301 
0302   const QString esrb = mapValue(resultMap_, "rating")
0303                        .section(QLatin1Char('-'), 0, 0)
0304                        .trimmed(); // value is like "T - Teen"
0305   Data::GameCollection::EsrbRating rating = Data::GameCollection::UnknownEsrb;
0306   if(esrb == QLatin1String("U"))         rating = Data::GameCollection::Unrated;
0307   else if(esrb == QLatin1String("T"))    rating = Data::GameCollection::Teen;
0308   else if(esrb == QLatin1String("E"))    rating = Data::GameCollection::Everyone;
0309   else if(esrb == QLatin1String("E10+")) rating = Data::GameCollection::Everyone10;
0310   else if(esrb == QLatin1String("EC"))   rating = Data::GameCollection::EarlyChildhood;
0311   else if(esrb == QLatin1String("A"))    rating = Data::GameCollection::Adults;
0312   else if(esrb == QLatin1String("M"))    rating = Data::GameCollection::Mature;
0313   else if(esrb == QLatin1String("RP"))   rating = Data::GameCollection::Pending;
0314   if(rating != Data::GameCollection::UnknownEsrb) {
0315     entry_->setField(QStringLiteral("certification"), Data::GameCollection::esrbRating(rating));
0316   }
0317 
0318   const QString coverUrl = m_covers.value(mapValue(resultMap_, "id"));
0319   if(m_imageSize != NoImage) {
0320     entry_->setField(QStringLiteral("cover"), coverUrl);
0321   }
0322 
0323   QStringList genres, pubs, devs;
0324 
0325   bool alreadyAttemptedLoad = false;
0326   QVariantList genreIdList = resultMap_.value(QStringLiteral("genres")).toList();
0327   foreach(const QVariant& v, genreIdList) {
0328     const int id = v.toInt();
0329     if(!m_genres.contains(id) && !alreadyAttemptedLoad) {
0330       readDataList(Genre);
0331       alreadyAttemptedLoad = true;
0332     }
0333     if(m_genres.contains(id)) {
0334       genres << m_genres[id];
0335     }
0336   }
0337 
0338   alreadyAttemptedLoad = false;
0339   QVariantList pubList = resultMap_.value(QStringLiteral("publishers")).toList();
0340   foreach(const QVariant& v, pubList) {
0341     const int id = v.toInt();
0342     if(!m_publishers.contains(id) && !alreadyAttemptedLoad) {
0343       readDataList(Publisher);
0344       alreadyAttemptedLoad = true;
0345     }
0346     if(m_publishers.contains(id)) {
0347       pubs << m_publishers[id];
0348     }
0349   }
0350 
0351   alreadyAttemptedLoad = false;
0352   QVariantList devList = resultMap_.value(QStringLiteral("developers")).toList();
0353   foreach(const QVariant& v, devList) {
0354     const int id = v.toInt();
0355     if(!m_developers.contains(id) && !alreadyAttemptedLoad) {
0356       readDataList(Developer);
0357       alreadyAttemptedLoad = true;
0358     }
0359     if(m_developers.contains(id)) {
0360       devs << m_developers[id];
0361     }
0362   }
0363 
0364   entry_->setField(QStringLiteral("genre"), genres.join(FieldFormat::delimiterString()));
0365   entry_->setField(QStringLiteral("publisher"), pubs.join(FieldFormat::delimiterString()));
0366   entry_->setField(QStringLiteral("developer"), devs.join(FieldFormat::delimiterString()));
0367 
0368 if(entry_->collection()->hasField(QStringLiteral("num-player"))) {
0369     entry_->setField(QStringLiteral("num-player"), mapValue(resultMap_, "players"));
0370   }
0371 }
0372 
0373 void TheGamesDBFetcher::readPlatformList(const QVariantMap& platformMap_) {
0374   QMapIterator<QString, QVariant> i(platformMap_);
0375   while(i.hasNext()) {
0376     i.next();
0377     const QVariantMap map = i.value().toMap();
0378     const QString name = map.value(QStringLiteral("name")).toString();
0379     m_platforms.insert(i.key().toInt(), Data::GameCollection::normalizePlatform(name));
0380   }
0381 
0382   // now write it to cache again
0383   const QString id = QStringLiteral("id");
0384   const QString name = QStringLiteral("name");
0385   QJsonObject platformObj;
0386   for(auto ii = m_platforms.constBegin(); ii != m_platforms.constEnd(); ++ii) {
0387     QJsonObject iObj;
0388     iObj.insert(id, ii.key());
0389     iObj.insert(name, ii.value());
0390     platformObj.insert(QString::number(ii.key()), iObj);
0391   }
0392   QJsonObject dataObj;
0393   dataObj.insert(QStringLiteral("platforms"), platformObj);
0394   QJsonObject docObj;
0395   docObj.insert(QStringLiteral("data"), dataObj);
0396   QJsonDocument doc;
0397   doc.setObject(docObj);
0398   writeDataList(Platform, doc.toJson());
0399 }
0400 
0401 void TheGamesDBFetcher::readCoverList(const QVariantMap& coverDataMap_) {
0402   // first, get the base url
0403   QString imageBase;
0404   switch(m_imageSize) {
0405     case SmallImage:
0406       // this is the default size, using the thumb. Not the small size
0407       imageBase = QStringLiteral("thumb");
0408       break;
0409     case MediumImage:
0410       imageBase = QStringLiteral("medium");
0411       break;
0412     case LargeImage:
0413       imageBase = QStringLiteral("large");
0414       break;
0415     case NoImage:
0416       m_covers.clear();
0417       return; // no need to read anything
0418       break;
0419   }
0420 
0421   QString baseUrl =  coverDataMap_.value(QStringLiteral("base_url")).toMap()
0422                                   .value(imageBase).toString();
0423 
0424   QVariantMap coverMap = coverDataMap_.value(QStringLiteral("data")).toMap();
0425   QMapIterator<QString, QVariant> i(coverMap);
0426   while(i.hasNext()) {
0427     i.next();
0428     foreach(QVariant v, i.value().toList()) {
0429       QVariantMap map = v.toMap();
0430       if(map.value(QStringLiteral("type")) == QLatin1String("boxart") &&
0431          map.value(QStringLiteral("side")) == QLatin1String("front")) {
0432         m_covers.insert(i.key(), baseUrl + mapValue(map, "filename"));
0433         break;
0434       }
0435     }
0436   }
0437 }
0438 
0439 void TheGamesDBFetcher::loadCachedData() {
0440   // The lists of genres, publishers, and developers are separate, with TGDB requesting that
0441   // the data be cached heavily and only updated when necessary
0442   // read the three cached JSON data file for genres, publishers, and developers
0443   // the platform info is sent with each request response, so it doesn't necessarily need
0444   // to be cache. But if an update request is used, having the cached platform id is helpful
0445 
0446   QFile genreFile(dataFileName(Genre));
0447   if(genreFile.open(QIODevice::ReadOnly)) {
0448     updateData(Genre, genreFile.readAll());
0449   }
0450 
0451   QFile publisherFile(dataFileName(Publisher));
0452   if(publisherFile.open(QIODevice::ReadOnly)) {
0453     updateData(Publisher, publisherFile.readAll());
0454   }
0455 
0456   QFile developerFile(dataFileName(Developer));
0457   if(developerFile.open(QIODevice::ReadOnly)) {
0458     updateData(Developer, developerFile.readAll());
0459   }
0460 
0461   QFile platformFile(dataFileName(Platform));
0462   if(platformFile.open(QIODevice::ReadOnly)) {
0463     updateData(Platform, platformFile.readAll());
0464   }
0465 }
0466 
0467 void TheGamesDBFetcher::updateData(TgdbDataType dataType_, const QByteArray& jsonData_) {
0468   QString dataName;
0469   switch(dataType_) {
0470     case Genre:
0471       dataName = QStringLiteral("genres");
0472       break;
0473     case Publisher:
0474       dataName = QStringLiteral("publishers");
0475       break;
0476     case Developer:
0477       dataName = QStringLiteral("developers");
0478       break;
0479     case Platform:
0480       dataName = QStringLiteral("platforms");
0481       break;
0482   }
0483 
0484   QHash<int, QString> dataHash;
0485   const QVariantMap topMap = QJsonDocument::fromJson(jsonData_).object().toVariantMap();
0486   const QVariantMap resultMap = topMap.value(QStringLiteral("data")).toMap()
0487                                       .value(dataName).toMap();
0488   for(QMapIterator<QString, QVariant> i(resultMap); i.hasNext(); ) {
0489     i.next();
0490     const QVariantMap m = i.value().toMap();
0491     dataHash.insert(m.value(QStringLiteral("id")).toInt(), mapValue(m, "name"));
0492   }
0493 
0494   // transfer read data into the correct local variable
0495   switch(dataType_) {
0496     case Genre:
0497       m_genres = dataHash;
0498       break;
0499     case Publisher:
0500       m_publishers = dataHash;
0501       break;
0502     case Developer:
0503       m_developers = dataHash;
0504       break;
0505     case Platform:
0506       m_platforms = dataHash;
0507       break;
0508   }
0509 }
0510 
0511 void TheGamesDBFetcher::readDataList(TgdbDataType dataType_) {
0512   QUrl u(QString::fromLatin1(THEGAMESDB_API_URL));
0513   u.setPath(QLatin1String("/v") + QLatin1String(THEGAMESDB_API_VERSION));
0514   switch(dataType_) {
0515     case Genre:
0516       u.setPath(u.path() + QLatin1String("/Genres"));
0517       break;
0518     case Publisher:
0519       u.setPath(u.path() + QLatin1String("/Publishers"));
0520       break;
0521     case Developer:
0522       u.setPath(u.path() + QLatin1String("/Developers"));
0523       break;
0524     case Platform:
0525       myDebug() << "not trying to read platforms";
0526       // platforms are not read independently, and are only cached
0527       return;
0528   }
0529   QUrlQuery q;
0530   q.addQueryItem(QStringLiteral("apikey"), m_apiKey);
0531   u.setQuery(q);
0532 
0533 //  u = QUrl::fromLocalFile(dataFileName(dataType_)); // for testing
0534 //  myDebug() << "Reading" << u;
0535   const QByteArray data = FileHandler::readDataFile(u, true);
0536   writeDataList(dataType_, data);
0537   updateData(dataType_, data);
0538 }
0539 
0540 void TheGamesDBFetcher::writeDataList(TgdbDataType dataType_, const QByteArray& data_) {
0541   QFile file(dataFileName(dataType_));
0542   if(!file.open(QIODevice::WriteOnly) || file.write(data_) == -1) {
0543     myDebug() << "unable to write to" << file.fileName() << file.errorString();
0544     return;
0545   }
0546   file.close();
0547 }
0548 
0549 Tellico::Fetch::ConfigWidget* TheGamesDBFetcher::configWidget(QWidget* parent_) const {
0550   return new TheGamesDBFetcher::ConfigWidget(parent_, this);
0551 }
0552 
0553 QString TheGamesDBFetcher::defaultName() {
0554   return QStringLiteral("TheGamesDB");
0555 }
0556 
0557 QString TheGamesDBFetcher::defaultIcon() {
0558   return favIcon("https://thegamesdb.net");
0559 }
0560 
0561 Tellico::StringHash TheGamesDBFetcher::allOptionalFields() {
0562   StringHash hash;
0563   hash[QStringLiteral("num-player")] = i18n("Number of Players");
0564   return hash;
0565 }
0566 
0567 QString TheGamesDBFetcher::dataFileName(TgdbDataType dataType_) {
0568   const QString dataDir = Tellico::saveLocation(QStringLiteral("thegamesdb-data/"));
0569   QString fileName;
0570   switch(dataType_) {
0571     case Genre:
0572       fileName = dataDir + QLatin1String("genres.json");
0573       break;
0574     case Publisher:
0575       fileName = dataDir + QLatin1String("publishers.json");
0576       break;
0577     case Developer:
0578       fileName = dataDir + QLatin1String("developers.json");
0579       break;
0580     case Platform:
0581       fileName = dataDir + QLatin1String("platforms.json");
0582       break;
0583   }
0584   return fileName;
0585 }
0586 
0587 TheGamesDBFetcher::ConfigWidget::ConfigWidget(QWidget* parent_, const TheGamesDBFetcher* fetcher_)
0588     : Fetch::ConfigWidget(parent_) {
0589   QGridLayout* l = new QGridLayout(optionsWidget());
0590   l->setSpacing(4);
0591   l->setColumnStretch(1, 10);
0592 
0593   int row = -1;
0594   QLabel* al = new QLabel(i18n("Registration is required for accessing this data source. "
0595                                "If you agree to the terms and conditions, <a href='%1'>sign "
0596                                "up for an account</a>, and enter your information below.",
0597                                 QLatin1String("https://forums.thegamesdb.net/viewforum.php?f=10")),
0598                           optionsWidget());
0599   al->setOpenExternalLinks(true);
0600   al->setWordWrap(true);
0601   ++row;
0602   l->addWidget(al, row, 0, 1, 2);
0603   // richtext gets weird with size
0604   al->setMinimumWidth(al->sizeHint().width());
0605 
0606   QLabel* label = new QLabel(i18n("Access key: "), optionsWidget());
0607   l->addWidget(label, ++row, 0);
0608 
0609   m_apiKeyEdit = new QLineEdit(optionsWidget());
0610   connect(m_apiKeyEdit, &QLineEdit::textChanged, this, &ConfigWidget::slotSetModified);
0611   l->addWidget(m_apiKeyEdit, row, 1);
0612   label->setBuddy(m_apiKeyEdit);
0613 
0614   label = new QLabel(i18n("&Image size: "), optionsWidget());
0615   l->addWidget(label, ++row, 0);
0616   m_imageCombo = new GUI::ComboBox(optionsWidget());
0617   m_imageCombo->addItem(i18n("Small Image"), SmallImage);
0618   m_imageCombo->addItem(i18n("Medium Image"), MediumImage);
0619   m_imageCombo->addItem(i18n("Large Image"), LargeImage);
0620   m_imageCombo->addItem(i18n("No Image"), NoImage);
0621   void (GUI::ComboBox::* activatedInt)(int) = &GUI::ComboBox::activated;
0622   connect(m_imageCombo, activatedInt, this, &ConfigWidget::slotSetModified);
0623   l->addWidget(m_imageCombo, row, 1);
0624   QString w = i18n("The cover image may be downloaded as well. However, too many large images in the "
0625                    "collection may degrade performance.");
0626   label->setWhatsThis(w);
0627   m_imageCombo->setWhatsThis(w);
0628   label->setBuddy(m_imageCombo);
0629 
0630   l->setRowStretch(++row, 10);
0631 
0632   if(fetcher_) {
0633     m_apiKeyEdit->setText(fetcher_->m_apiKey);
0634     m_imageCombo->setCurrentData(fetcher_->m_imageSize);
0635   } else { // defaults
0636     m_apiKeyEdit->setText(Tellico::reverseObfuscate(THEGAMESDB_MAGIC_TOKEN));
0637     m_imageCombo->setCurrentData(SmallImage);
0638   }
0639 
0640   // now add additional fields widget
0641   addFieldsWidget(TheGamesDBFetcher::allOptionalFields(), fetcher_ ? fetcher_->optionalFields() : QStringList());
0642 }
0643 
0644 void TheGamesDBFetcher::ConfigWidget::saveConfigHook(KConfigGroup& config_) {
0645   const QString apiKey = m_apiKeyEdit->text().trimmed();
0646   if(!apiKey.isEmpty() && apiKey != Tellico::reverseObfuscate(THEGAMESDB_MAGIC_TOKEN)) {
0647     config_.writeEntry("API Key", apiKey);
0648   }
0649   const int n = m_imageCombo->currentData().toInt();
0650   config_.writeEntry("Image Size", n);
0651 }
0652 
0653 QString TheGamesDBFetcher::ConfigWidget::preferredName() const {
0654   return TheGamesDBFetcher::defaultName();
0655 }