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 }