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

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