File indexing completed on 2024-05-12 05:09:29
0001 /*************************************************************************** 0002 Copyright (C) 2019-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 "colnectfetcher.h" 0026 #include "../collections/coincollection.h" 0027 #include "../collections/stampcollection.h" 0028 #include "../collections/comicbookcollection.h" 0029 #include "../collections/cardcollection.h" 0030 #include "../collections/gamecollection.h" 0031 #include "../images/imagefactory.h" 0032 #include "../gui/combobox.h" 0033 #include "../utils/guiproxy.h" 0034 #include "../entry.h" 0035 #include "../fieldformat.h" 0036 #include "../core/filehandler.h" 0037 #include "../tellico_debug.h" 0038 0039 #include <KLocalizedString> 0040 #include <KConfigGroup> 0041 #include <KJob> 0042 #include <KJobUiDelegate> 0043 #include <KJobWidgets/KJobWidgets> 0044 #include <KIO/StoredTransferJob> 0045 0046 #include <QLabel> 0047 #include <QFile> 0048 #include <QTextStream> 0049 #include <QGridLayout> 0050 #include <QTextCodec> 0051 #include <QJsonDocument> 0052 #include <QJsonArray> 0053 #include <QJsonValue> 0054 #include <QRegularExpression> 0055 #include <QStandardPaths> 0056 0057 namespace { 0058 static const char* COLNECT_API_URL = "https://api.tellico-project.org/colnect"; 0059 // static const char* COLNECT_API_URL = "https://api.colnect.net"; 0060 static const char* COLNECT_IMAGE_URL = "https://i.colnect.net"; 0061 static const char* COLNECT_LINK_URL = "https://colnect.com"; 0062 } 0063 0064 using namespace Tellico; 0065 using Tellico::Fetch::ColnectFetcher; 0066 0067 ColnectFetcher::ColnectFetcher(QObject* parent_) 0068 : Fetcher(parent_) 0069 , m_started(false) 0070 , m_locale(QStringLiteral("en")) 0071 , m_imageSize(LargeImage) 0072 , m_lastCollType(-1) { 0073 } 0074 0075 ColnectFetcher::~ColnectFetcher() { 0076 } 0077 0078 QString ColnectFetcher::source() const { 0079 return m_name.isEmpty() ? defaultName() : m_name; 0080 } 0081 0082 QString ColnectFetcher::attribution() const { 0083 return QStringLiteral("Catalog information courtesy of Colnect, an online collectors community."); 0084 } 0085 0086 bool ColnectFetcher::canSearch(Fetch::FetchKey k) const { 0087 return k == Title || k == Keyword; 0088 } 0089 0090 bool ColnectFetcher::canFetch(int type) const { 0091 return type == Data::Collection::Coin 0092 || type == Data::Collection::Stamp 0093 || type == Data::Collection::Card 0094 || type == Data::Collection::ComicBook 0095 || type == Data::Collection::Game; 0096 } 0097 0098 void ColnectFetcher::readConfigHook(const KConfigGroup& config_) { 0099 QString k = config_.readEntry("Locale", "en"); 0100 if(!k.isEmpty()) { 0101 m_locale = k.toLower(); 0102 } 0103 Q_ASSERT_X(m_locale.length() == 2, "ColnectFetcher::readConfigHook", "lang should be 2 char short iso"); 0104 const int imageSize = config_.readEntry("Image Size", -1); 0105 if(imageSize > -1) { 0106 m_imageSize = static_cast<ImageSize>(imageSize); 0107 } 0108 } 0109 0110 void ColnectFetcher::search() { 0111 m_started = true; 0112 m_year.clear(); 0113 0114 QUrl u(QString::fromLatin1(COLNECT_API_URL)); 0115 // Colnect API calls are encoded as a path 0116 QString query(QLatin1Char('/') + m_locale); 0117 0118 switch(collectionType()) { 0119 case Data::Collection::Coin: 0120 m_category = QStringLiteral("coins"); 0121 break; 0122 case Data::Collection::Stamp: 0123 m_category = QStringLiteral("stamps"); 0124 break; 0125 case Data::Collection::ComicBook: 0126 m_category = QStringLiteral("comics"); 0127 break; 0128 case Data::Collection::Card: 0129 m_category = QStringLiteral("sports_cards"); 0130 break; 0131 case Data::Collection::Game: 0132 m_category = QStringLiteral("video_games"); 0133 break; 0134 default: 0135 myWarning() << "Colnect category type not available for" << collectionType(); 0136 stop(); 0137 return; 0138 } 0139 0140 QString value = request().value(); 0141 switch(request().key()) { 0142 case Title: 0143 { 0144 query += QStringLiteral("/list/cat/") + m_category; 0145 // pull out year, keep the regexp a little loose 0146 QRegularExpression yearRX(QStringLiteral("[0-9]{4}")); 0147 QRegularExpressionMatch match = yearRX.match(value); 0148 if(match.hasMatch()) { 0149 m_year = match.captured(0); 0150 if(collectionType() == Data::Collection::Coin) { 0151 query += QStringLiteral("/mint_year/"); 0152 } else { 0153 query += QStringLiteral("/year/"); 0154 } 0155 query += m_year; 0156 value = value.remove(yearRX); 0157 } 0158 } 0159 // everything left is for the item description 0160 query += QStringLiteral("/item_name/") + value.simplified(); 0161 break; 0162 0163 case Keyword: 0164 { 0165 query += QStringLiteral("/list/cat/") + m_category; 0166 // pull out year, keep the regexp a little loose 0167 QRegularExpression yearRX(QStringLiteral("[0-9]{4}")); 0168 QRegularExpressionMatch match = yearRX.match(value); 0169 if(match.hasMatch()) { 0170 m_year = match.captured(0); 0171 if(collectionType() == Data::Collection::Coin) { 0172 query += QStringLiteral("/mint_year/"); 0173 } else { 0174 query += QStringLiteral("/year/"); 0175 } 0176 query += m_year; 0177 value = value.remove(yearRX); 0178 } 0179 } 0180 // everything left is for the item description 0181 query += QStringLiteral("/description/") + value.simplified(); 0182 break; 0183 0184 case Raw: 0185 query += QStringLiteral("/item/cat/") + m_category + QStringLiteral("/id/") + value; 0186 break; 0187 0188 default: 0189 myWarning() << source() << "- key not recognized:" << request().key(); 0190 stop(); 0191 return; 0192 } 0193 0194 u.setPath(u.path() + query); 0195 myLog() << "Reading" << u.toDisplayString(); 0196 0197 m_job = KIO::storedGet(u, KIO::NoReload, KIO::HideProgressInfo); 0198 KJobWidgets::setWindow(m_job, GUI::Proxy::widget()); 0199 connect(m_job.data(), &KJob::result, this, &ColnectFetcher::slotComplete); 0200 } 0201 0202 void ColnectFetcher::stop() { 0203 if(!m_started) { 0204 return; 0205 } 0206 if(m_job) { 0207 m_job->kill(); 0208 m_job = nullptr; 0209 } 0210 m_started = false; 0211 emit signalDone(this); 0212 } 0213 0214 Tellico::Data::EntryPtr ColnectFetcher::fetchEntryHook(uint uid_) { 0215 Data::EntryPtr entry = m_entries.value(uid_); 0216 if(!entry) { 0217 myWarning() << "no entry in dict"; 0218 return Data::EntryPtr(); 0219 } 0220 0221 // if there's a colnect-id in the entry, need to fetch all the data 0222 const QString id = entry->field(QStringLiteral("colnect-id")); 0223 if(!id.isEmpty()) { 0224 QUrl u(QString::fromLatin1(COLNECT_API_URL)); 0225 QString query(QLatin1Char('/') + m_locale + QStringLiteral("/item/cat/") 0226 + m_category + QStringLiteral("/id/") + id); 0227 u.setPath(u.path() + query); 0228 myLog() << "Reading" << u.toDisplayString(); 0229 0230 QPointer<KIO::StoredTransferJob> job = KIO::storedGet(u, KIO::NoReload, KIO::HideProgressInfo); 0231 KJobWidgets::setWindow(job, GUI::Proxy::widget()); 0232 if(!job->exec()) { 0233 myDebug() << "Colnect item data:" << job->errorString() << u; 0234 return entry; 0235 } 0236 const QByteArray data = job->data(); 0237 if(data.isEmpty()) { 0238 myDebug() << "no colnect item data for" << u; 0239 return entry; 0240 } 0241 #if 0 0242 myWarning() << "Remove item debug from colnectfetcher.cpp [colnectitemtest.json]"; 0243 QFile file(QStringLiteral("/tmp/colnectitemtest.json")); 0244 if(file.open(QIODevice::WriteOnly)) { 0245 QTextStream t(&file); 0246 t.setCodec("UTF-8"); 0247 t << data; 0248 } 0249 file.close(); 0250 #endif 0251 QJsonParseError jsonError; 0252 QJsonDocument doc = QJsonDocument::fromJson(data, &jsonError); 0253 Q_ASSERT_X(!doc.isNull(), "colnect", jsonError.errorString().toUtf8().constData()); 0254 const QVariantList resultList = doc.array().toVariantList(); 0255 Q_ASSERT_X(!resultList.isEmpty(), "colnect", "no item results"); 0256 Q_ASSERT_X(static_cast<QMetaType::Type>(resultList.at(0).type()) == QMetaType::QString, "colnect", 0257 "Weird single item result, first value is not a string"); 0258 populateEntry(entry, resultList); 0259 } 0260 0261 // image might still be a URL only 0262 loadImage(entry, QStringLiteral("obverse")); 0263 loadImage(entry, QStringLiteral("reverse")); 0264 loadImage(entry, QStringLiteral("image")); // stamp image 0265 loadImage(entry, QStringLiteral("cover")); 0266 loadImage(entry, QStringLiteral("front")); 0267 loadImage(entry, QStringLiteral("back")); 0268 0269 // don't want to include id 0270 entry->setField(QStringLiteral("colnect-id"), QString()); 0271 return entry; 0272 } 0273 0274 Tellico::Fetch::FetchRequest ColnectFetcher::updateRequest(Data::EntryPtr entry_) { 0275 const QString title = entry_->field(QStringLiteral("title")); 0276 if(!title.isEmpty()) { 0277 return FetchRequest(Keyword, title); 0278 } 0279 return FetchRequest(); 0280 } 0281 0282 void ColnectFetcher::slotComplete(KJob* job_) { 0283 KIO::StoredTransferJob* job = static_cast<KIO::StoredTransferJob*>(job_); 0284 0285 if(job->error()) { 0286 job->uiDelegate()->showErrorMessage(); 0287 stop(); 0288 return; 0289 } 0290 0291 const QByteArray data = job->data(); 0292 if(data.isEmpty()) { 0293 myDebug() << "no data"; 0294 stop(); 0295 return; 0296 } 0297 // see bug 319662. If fetcher is canceled, job is killed 0298 // if the pointer is retained, it gets double-deleted 0299 m_job = nullptr; 0300 0301 #if 0 0302 myWarning() << "Remove debug from colnectfetcher.cpp [colnecttest.json]"; 0303 QFile f(QStringLiteral("/tmp/colnecttest.json")); 0304 if(f.open(QIODevice::WriteOnly)) { 0305 QTextStream t(&f); 0306 t.setCodec("UTF-8"); 0307 t << data; 0308 } 0309 f.close(); 0310 #endif 0311 0312 QJsonParseError jsonError; 0313 QJsonDocument doc = QJsonDocument::fromJson(data, &jsonError); 0314 if(doc.isNull()) { 0315 myDebug() << "null JSON document:" << jsonError.errorString(); 0316 message(jsonError.errorString(), MessageHandler::Error); 0317 stop(); 0318 return; 0319 } 0320 QVariantList resultList = doc.array().toVariantList(); 0321 if(resultList.isEmpty()) { 0322 myLog() << "No results"; 0323 stop(); 0324 return; 0325 } 0326 0327 m_hasMoreResults = false; // for now, no continued searches 0328 0329 Data::CollPtr coll; 0330 switch(collectionType()) { 0331 case Data::Collection::Coin: 0332 coll = new Data::CoinCollection(true); 0333 break; 0334 case Data::Collection::Stamp: 0335 coll = new Data::StampCollection(true); 0336 break; 0337 case Data::Collection::ComicBook: 0338 coll = new Data::ComicBookCollection(true); 0339 break; 0340 case Data::Collection::Card: 0341 coll = new Data::CardCollection(true); 0342 break; 0343 case Data::Collection::Game: 0344 coll = new Data::GameCollection(true); 0345 break; 0346 default: 0347 myWarning() << "no collection pointer for type" << collectionType(); 0348 break; 0349 } 0350 Q_ASSERT(coll); 0351 if(!coll) { 0352 stop(); 0353 return; 0354 } 0355 // placeholder for colnect id, to be removed later 0356 Data::FieldPtr f1(new Data::Field(QStringLiteral("colnect-id"), QString())); 0357 coll->addField(f1); 0358 0359 const QString series(QStringLiteral("series")); 0360 if(!coll->hasField(series) && optionalFields().contains(series)) { 0361 Data::FieldPtr field(new Data::Field(series, i18n("Series"))); 0362 field->setCategory(i18n("General")); 0363 coll->addField(field); 0364 } 0365 0366 const QString desc(QStringLiteral("description")); 0367 if(!coll->hasField(desc) && optionalFields().contains(desc)) { 0368 Data::FieldPtr field(new Data::Field(desc, i18n("Description"), Data::Field::Para)); 0369 coll->addField(field); 0370 } 0371 0372 // if the first item in the array is a string, probably a single item result, possibly from a Raw query 0373 if(!resultList.isEmpty() && 0374 static_cast<QMetaType::Type>(resultList.at(0).type()) == QMetaType::QString) { 0375 Data::EntryPtr entry(new Data::Entry(coll)); 0376 populateEntry(entry, resultList); 0377 0378 FetchResult* r = new FetchResult(this, entry); 0379 m_entries.insert(r->uid, entry); 0380 emit signalResultFound(r); 0381 0382 stop(); 0383 return; 0384 } 0385 0386 // here, we have multiple results to loop through 0387 myLog() << "Reading" << resultList.size() << "results"; 0388 foreach(const QVariant& result, resultList) { 0389 // be sure to check that the fetcher has not been stopped 0390 // crashes can occur if not 0391 if(!m_started) { 0392 break; 0393 } 0394 0395 Data::EntryPtr entry(new Data::Entry(coll)); 0396 //list action - returns array of [item_id,series_id,producer_id,front_picture_id, back_picture_id,item_description,catalog_codes,item_name] 0397 // comics, title is field #7 0398 const QVariantList values = result.toJsonArray().toVariantList(); 0399 entry->setField(QStringLiteral("colnect-id"), values.first().toString()); 0400 if(optionalFields().contains(desc)) { 0401 entry->setField(desc, values.last().toString()); 0402 } 0403 // since card collection use a dependent field for title, fake a description with the series field 0404 if(collectionType() == Data::Collection::Card) { 0405 entry->setField(QStringLiteral("series"), values.at(7).toString()); 0406 } else { 0407 entry->setField(QStringLiteral("title"), values.at(7).toString()); 0408 } 0409 if(collectionType() == Data::Collection::ComicBook) { 0410 entry->setField(QStringLiteral("pub_year"), m_year); 0411 } else { 0412 entry->setField(QStringLiteral("year"), m_year); 0413 } 0414 0415 FetchResult* r = new FetchResult(this, entry); 0416 m_entries.insert(r->uid, entry); 0417 emit signalResultFound(r); 0418 } 0419 0420 stop(); 0421 } 0422 0423 #define READ_AND_SET(NAME, FIELD) \ 0424 { \ 0425 const int idx = m_colnectFields.value(QStringLiteral(NAME), -1); \ 0426 if(idx > -1) { \ 0427 entry_->setField(QStringLiteral(FIELD), resultList_.at(idx).toString()); \ 0428 } \ 0429 } 0430 #define READ_AND_SET_OPTIONAL(NAME, FIELD) \ 0431 { \ 0432 const QString fieldName = QStringLiteral(FIELD); \ 0433 const int idx = m_colnectFields.value(QStringLiteral(NAME), -1); \ 0434 if(idx > -1 && optionalFields().contains(fieldName)) { \ 0435 entry_->setField(fieldName, resultList_.at(idx).toString()); \ 0436 } \ 0437 } 0438 #define READ_AND_SET_IMAGE(NAME, FIELD) \ 0439 { \ 0440 const int idx = m_colnectFields.value(QStringLiteral(NAME), -1); \ 0441 if(idx > -1) { \ 0442 entry_->setField(QStringLiteral(FIELD), \ 0443 imageUrl(resultList_.at(0).toString(), resultList_.at(idx).toString())); \ 0444 } \ 0445 } 0446 #define READ_AND_SET_OPTIONAL_IMAGE(NAME, FIELD) \ 0447 { \ 0448 const QString fieldName = QStringLiteral(FIELD); \ 0449 const int idx = m_colnectFields.value(QStringLiteral(NAME), -1); \ 0450 if(idx > -1 && optionalFields().contains(fieldName)) { \ 0451 entry_->setField(fieldName, \ 0452 imageUrl(resultList_.at(0).toString(), resultList_.at(idx).toString())); \ 0453 } \ 0454 } 0455 0456 void ColnectFetcher::populateEntry(Data::EntryPtr entry_, const QVariantList& resultList_) { 0457 if(m_colnectFields.isEmpty() || m_lastCollType != collectionType()) { 0458 m_lastCollType = collectionType(); 0459 readDataList(); 0460 // set minimum size of list here (cards are 23) 0461 if(m_colnectFields.count() < 23) { 0462 myDebug() << "below minimum field count," << m_colnectFields.count(); 0463 return; 0464 } 0465 } 0466 if(resultList_.count() != m_colnectFields.count()) { 0467 myDebug() << "field count mismatch! Got" << resultList_.count() << ", expected" << m_colnectFields.count(); 0468 return; 0469 } 0470 0471 #if 0 0472 auto i = m_colnectFields.constBegin(); 0473 while(i != m_colnectFields.constEnd()) { 0474 if(!resultList_.at(i.value()).toString().isEmpty()) 0475 myDebug() << i.key() << ": " << resultList_.at(i.value()).toString(); 0476 ++i; 0477 } 0478 #endif 0479 0480 READ_AND_SET_OPTIONAL("Series", "series"); 0481 0482 int idx = m_colnectFields.value(QStringLiteral("Description"), -1); 0483 static const QString desc(QStringLiteral("description")); 0484 if(idx > -1 && optionalFields().contains(desc)) { 0485 static const QString name(QStringLiteral("Name")); 0486 auto idxName = m_colnectFields.value(name, -1); 0487 QString s = resultList_.at(idx).toString().trimmed(); 0488 // use the name as the description for stamps since the title includes it 0489 // put the description text into the comments 0490 if(collectionType() == Data::Collection::Stamp) { 0491 if(idxName > -1) { 0492 entry_->setField(desc, resultList_.at(idxName).toString()); 0493 } 0494 entry_->setField(QStringLiteral("comments"), s); 0495 } else { 0496 // if description is empty, just use the name 0497 if(s.isEmpty() && idxName > -1) { 0498 entry_->setField(desc, resultList_.at(idxName).toString()); 0499 } else { 0500 entry_->setField(desc, s); 0501 } 0502 } 0503 } 0504 switch(collectionType()) { 0505 case Data::Collection::Coin: 0506 populateCoinEntry(entry_, resultList_); 0507 break; 0508 case Data::Collection::Stamp: 0509 populateStampEntry(entry_, resultList_); 0510 break; 0511 case Data::Collection::ComicBook: 0512 populateComicEntry(entry_, resultList_); 0513 break; 0514 case Data::Collection::Card: 0515 populateCardEntry(entry_, resultList_); 0516 break; 0517 case Data::Collection::Game: 0518 populateGameEntry(entry_, resultList_); 0519 break; 0520 } 0521 0522 static const QString colnect(QStringLiteral("colnect")); 0523 if(optionalFields().contains(colnect)) { 0524 if(!entry_->collection()->hasField(colnect)) { 0525 Data::FieldPtr field(new Data::Field(colnect, i18n("Colnect Link"), Data::Field::URL)); 0526 field->setCategory(i18n("General")); 0527 entry_->collection()->addField(field); 0528 } 0529 QUrl link(QString::fromLatin1(COLNECT_LINK_URL)); 0530 const QString path(QLatin1Char('/') + m_locale + 0531 QLatin1Char('/') + m_category + 0532 QLatin1Char('/') + m_category.chopped(1) + 0533 QLatin1Char('/') + entry_->field(QStringLiteral("colnect-id")) + 0534 QLatin1Char('-') + URLize(resultList_.at(0).toString())); 0535 link.setPath(link.path() + path); 0536 entry_->setField(colnect, link.url()); 0537 } 0538 } 0539 0540 void ColnectFetcher::populateCoinEntry(Data::EntryPtr entry_, const QVariantList& resultList_) { 0541 auto coll = entry_->collection(); 0542 const QString mintage(QStringLiteral("mintage")); 0543 if(!coll->hasField(mintage) && optionalFields().contains(mintage)) { 0544 Data::FieldPtr field(new Data::Field(mintage, i18n("Mintage"), Data::Field::Number)); 0545 field->setCategory(i18n("General")); 0546 coll->addField(field); 0547 } 0548 0549 int idx = m_colnectFields.value(QStringLiteral("Issued on"), -1); 0550 // the year may have already been set in the query term 0551 if(m_year.isEmpty() && idx > -1) { 0552 entry_->setField(QStringLiteral("year"), resultList_.at(idx).toString().left(4)); 0553 } 0554 0555 idx = m_colnectFields.value(QStringLiteral("Currency"), -1); 0556 if(idx > -1) { 0557 entry_->setField(QStringLiteral("currency"), resultList_.at(idx).toString()); 0558 idx = m_colnectFields.value(QStringLiteral("FaceValue"), -1); 0559 if(idx > -1) { 0560 // bad assumption, but go with it. First char is currency symbol 0561 QString currency = entry_->field(QStringLiteral("currency")); 0562 if(!currency.isEmpty()) currency.truncate(1); 0563 const double value = resultList_.at(idx).toDouble(); 0564 // don't assume the value is in system currency 0565 entry_->setField(QStringLiteral("denomination"), 0566 QLocale::system().toCurrencyString(value, currency)); 0567 } 0568 } 0569 0570 READ_AND_SET("Country", "country"); 0571 READ_AND_SET_OPTIONAL("Known mintage", "mintage"); 0572 READ_AND_SET_OPTIONAL_IMAGE("FrontPicture", "obverse"); 0573 READ_AND_SET_OPTIONAL_IMAGE("BackPicture", "reverse"); 0574 } 0575 0576 void ColnectFetcher::populateStampEntry(Data::EntryPtr entry_, const QVariantList& resultList_) { 0577 auto coll = entry_->collection(); 0578 const QString stanleygibbons(QStringLiteral("stanley-gibbons")); 0579 if(!coll->hasField(stanleygibbons) && optionalFields().contains(stanleygibbons)) { 0580 Data::FieldPtr field(new Data::Field(stanleygibbons, i18nc("Stanley Gibbons stamp catalog code", "Stanley Gibbons"))); 0581 field->setCategory(i18n("General")); 0582 coll->addField(field); 0583 } 0584 0585 const QString michel(QStringLiteral("michel")); 0586 if(!coll->hasField(michel) && optionalFields().contains(michel)) { 0587 Data::FieldPtr field(new Data::Field(michel, i18nc("Michel stamp catalog code", "Michel"))); 0588 field->setCategory(i18n("General")); 0589 coll->addField(field); 0590 } 0591 0592 int idx = m_colnectFields.value(QStringLiteral("Issued on"), -1); 0593 // the year may have already been set in the query term 0594 if(m_year.isEmpty() && idx > -1) { 0595 entry_->setField(QStringLiteral("year"), resultList_.at(idx).toString().left(4)); 0596 } 0597 0598 idx = m_colnectFields.value(QStringLiteral("Currency"), -1); 0599 if(idx > -1) { 0600 entry_->setField(QStringLiteral("currency"), resultList_.at(idx).toString()); 0601 idx = m_colnectFields.value(QStringLiteral("FaceValue"), -1); 0602 if(idx > -1) { 0603 // bad assumption, but go with it. First char is currency symbol 0604 QString currency = entry_->field(QStringLiteral("currency")); 0605 if(!currency.isEmpty()) currency.truncate(1); 0606 const double value = resultList_.at(idx).toDouble(); 0607 // don't assume the value is in system currency 0608 entry_->setField(QStringLiteral("denomination"), 0609 QLocale::system().toCurrencyString(value, currency)); 0610 } 0611 } 0612 0613 READ_AND_SET("Gum", "gummed"); 0614 READ_AND_SET("Country", "country"); 0615 0616 idx = m_colnectFields.value(QStringLiteral("Colors"), -1); 0617 if(idx > -1) { 0618 const int colorId = resultList_.at(idx).toInt(); 0619 if(colorId > 0) { 0620 if(!m_itemNames.contains("colors")) { 0621 readItemNames("colors"); 0622 } 0623 entry_->setField(QStringLiteral("color"), m_itemNames.value("colors").value(colorId)); 0624 } 0625 } 0626 0627 // catalog codes 0628 idx = m_colnectFields.value(QStringLiteral("Catalog Codes"), -1); 0629 if(idx > -1) { 0630 // split by comma, look for prefix 0631 QStringList codes = resultList_.at(idx).toString().split(QLatin1Char(',')); 0632 Q_FOREACH(const QString& code, codes) { 0633 const QString prefix = code.section(QLatin1Char(':'), 0, 0).trimmed(); 0634 const QString value = code.section(QLatin1Char(':'), 1, 1).trimmed(); 0635 // 'SG' for Stanley Gibbons, 'Sc' for Scott, 'Mi' for Michel and 'Yv' for Yvert & Tellier. 0636 if(prefix == QLatin1String("Sc")) { 0637 entry_->setField(QStringLiteral("scott"), value); 0638 } else if(prefix == QLatin1String("Sg") && optionalFields().contains(QStringLiteral("stanley-gibbons"))) { 0639 entry_->setField(QStringLiteral("stanley-gibbons"), value); 0640 } else if(prefix == QLatin1String("Mi") && optionalFields().contains(QStringLiteral("michel"))) { 0641 entry_->setField(QStringLiteral("michel"), value); 0642 } 0643 } 0644 } 0645 0646 READ_AND_SET_IMAGE("FrontPicture", "image"); 0647 } 0648 0649 void ColnectFetcher::populateComicEntry(Data::EntryPtr entry_, const QVariantList& resultList_) { 0650 READ_AND_SET("Name", "title"); 0651 0652 int idx = m_colnectFields.value(QStringLiteral("Issued on"), -1); 0653 // the year may have already been set in the query term 0654 if(m_year.isEmpty() && idx > -1) { 0655 entry_->setField(QStringLiteral("pub_year"), resultList_.at(idx).toString().left(4)); 0656 } 0657 0658 static const QRegularExpression spaceCommaRx(QLatin1String("\\s*,\\s*")); 0659 idx = m_colnectFields.value(QStringLiteral("Writer"), -1); 0660 if(idx > -1) { 0661 QString writer = resultList_.at(idx).toString(); 0662 writer.replace(spaceCommaRx, FieldFormat::delimiterString()); 0663 entry_->setField(QStringLiteral("writer"), writer); 0664 } 0665 0666 idx = m_colnectFields.value(QStringLiteral("CoverArtist"), -1); 0667 if(idx > -1) { 0668 QString artist = resultList_.at(idx).toString(); 0669 artist.replace(spaceCommaRx, FieldFormat::delimiterString()); 0670 entry_->setField(QStringLiteral("artist"), artist); 0671 } 0672 0673 READ_AND_SET("Publisher", "publisher"); 0674 READ_AND_SET("IssuingNumber", "issue"); 0675 READ_AND_SET("Edition", "edition"); 0676 READ_AND_SET("Genre", "genre"); 0677 READ_AND_SET_IMAGE("FrontPicture", "cover"); 0678 } 0679 0680 void ColnectFetcher::populateCardEntry(Data::EntryPtr entry_, const QVariantList& resultList_) { 0681 READ_AND_SET("Type", "series"); 0682 READ_AND_SET("Brand", "brand"); 0683 READ_AND_SET("Number", "number"); 0684 READ_AND_SET("League", "type"); 0685 0686 int idx = m_colnectFields.value(QStringLiteral("Issued on"), -1); 0687 // the year may have already been set in the query term 0688 if(m_year.isEmpty() && idx > -1) { 0689 entry_->setField(QStringLiteral("year"), resultList_.at(idx).toString().left(4)); 0690 } 0691 0692 idx = m_colnectFields.value(QStringLiteral("ZscCardPlayer"), -1); 0693 if(idx > -1) { 0694 const int playerId = resultList_.at(idx).toInt(); 0695 if(playerId > 0) { 0696 if(!m_itemNames.contains("players")) { 0697 readItemNames("players"); 0698 } 0699 entry_->setField(QStringLiteral("player"), m_itemNames.value("players").value(playerId)); 0700 } 0701 } 0702 0703 idx = m_colnectFields.value(QStringLiteral("ZscCardTeam"), -1); 0704 if(idx > -1) { 0705 const int teamId = resultList_.at(idx).toInt(); 0706 if(teamId > 0) { 0707 if(!m_itemNames.contains("teams")) { 0708 readItemNames("teams"); 0709 } 0710 entry_->setField(QStringLiteral("team"), m_itemNames.value("teams").value(teamId)); 0711 } 0712 } 0713 0714 READ_AND_SET_IMAGE("FrontPicture", "front"); 0715 READ_AND_SET_IMAGE("BackPicture", "back"); 0716 } 0717 0718 void ColnectFetcher::populateGameEntry(Data::EntryPtr entry_, const QVariantList& resultList_) { 0719 auto coll = entry_->collection(); 0720 const QString pegi(QStringLiteral("pegi")); 0721 if(!coll->hasField(pegi) && optionalFields().contains(pegi)) { 0722 coll->addField(Data::Field::createDefaultField(Data::Field::PegiField)); 0723 } 0724 0725 READ_AND_SET("Name", "title"); 0726 READ_AND_SET("Publisher", "publisher"); 0727 READ_AND_SET("Description", "description"); 0728 READ_AND_SET("Genre", "genre"); 0729 READ_AND_SET_IMAGE("FrontPicture", "cover"); 0730 0731 int idx = m_colnectFields.value(QStringLiteral("Console"), -1); 0732 if(idx > -1) { 0733 entry_->setField(QStringLiteral("platform"), 0734 Data::GameCollection::normalizePlatform(resultList_.at(idx).toString())); 0735 } 0736 0737 idx = m_colnectFields.value(QStringLiteral("Issued on"), -1); 0738 // the year may have already been set in the query term 0739 if(m_year.isEmpty() && idx > -1) { 0740 entry_->setField(QStringLiteral("year"), resultList_.at(idx).toString().left(4)); 0741 } 0742 0743 idx = m_colnectFields.value(QStringLiteral("Rating"), -1); 0744 if(idx > -1) { 0745 // can have both esrb and pegi rating 0746 QStringList ratings = resultList_.at(idx).toString().split(QLatin1String("/")); 0747 Q_FOREACH(QString rating, ratings) { 0748 rating = rating.simplified(); 0749 if(rating.startsWith(QLatin1String("ESRB"))) { 0750 rating = rating.mid(5); 0751 Data::GameCollection::EsrbRating esrb = Data::GameCollection::UnknownEsrb; 0752 if(rating == QLatin1String("U")) esrb = Data::GameCollection::Unrated; 0753 else if(rating == QLatin1String("T")) esrb = Data::GameCollection::Teen; 0754 else if(rating == QLatin1String("E")) esrb = Data::GameCollection::Everyone; 0755 else if(rating == QLatin1String("E10+")) esrb = Data::GameCollection::Everyone10; 0756 else if(rating == QLatin1String("EC")) esrb = Data::GameCollection::EarlyChildhood; 0757 else if(rating == QLatin1String("A")) esrb = Data::GameCollection::Adults; 0758 else if(rating == QLatin1String("M")) esrb = Data::GameCollection::Mature; 0759 else if(rating == QLatin1String("RP")) esrb = Data::GameCollection::Pending; 0760 if(rating != Data::GameCollection::UnknownEsrb) { 0761 entry_->setField(QStringLiteral("certification"), Data::GameCollection::esrbRating(esrb)); 0762 } 0763 } else if(rating.startsWith(QLatin1String("PEGI")) && optionalFields().contains(QStringLiteral("pegi"))) { 0764 static const QRegularExpression pegiRx(QStringLiteral("^PEGI \\d\\d?")); 0765 auto pegiMatch = pegiRx.match(rating); 0766 if(pegiMatch.hasMatch()) { 0767 entry_->setField(QStringLiteral("pegi"), pegiMatch.captured(0)); 0768 } 0769 } 0770 } 0771 } 0772 } 0773 0774 #undef READ_AND_SET 0775 #undef READ_AND_SET_OPTIONAL 0776 #undef READ_AND_SET_IMAGE 0777 #undef READ_AND_SET_OPTIONAL_IMAGE 0778 0779 void ColnectFetcher::loadImage(Data::EntryPtr entry_, const QString& fieldName_) { 0780 const QString image = entry_->field(fieldName_); 0781 if(image.contains(QLatin1Char('/'))) { 0782 const QString id = ImageFactory::addImage(QUrl::fromUserInput(image), true /* quiet */); 0783 if(id.isEmpty()) { 0784 myLog() << "Failed to load" << image; 0785 message(i18n("The cover image could not be loaded."), MessageHandler::Warning); 0786 } 0787 // empty image ID is ok 0788 entry_->setField(fieldName_, id); 0789 } 0790 } 0791 0792 Tellico::Fetch::ConfigWidget* ColnectFetcher::configWidget(QWidget* parent_) const { 0793 return new ColnectFetcher::ConfigWidget(parent_, this); 0794 } 0795 0796 QString ColnectFetcher::defaultName() { 0797 return QStringLiteral("Colnect"); // no translation 0798 } 0799 0800 QString ColnectFetcher::defaultIcon() { 0801 return favIcon("https://colnect.com"); 0802 } 0803 0804 Tellico::StringHash ColnectFetcher::allOptionalFields() { 0805 StringHash hash; 0806 hash[QStringLiteral("colnect")] = i18n("Colnect Link"); 0807 // treat images as optional since Colnect doesn't break out different images for each year 0808 hash[QStringLiteral("obverse")] = i18n("Obverse"); 0809 hash[QStringLiteral("reverse")] = i18n("Reverse"); 0810 hash[QStringLiteral("series")] = i18n("Series"); 0811 /* TRANSLATORS: Mintage refers to the number of coins minted */ 0812 hash[QStringLiteral("mintage")] = i18n("Mintage"); 0813 hash[QStringLiteral("description")] = i18n("Description"); 0814 hash[QStringLiteral("stanley-gibbons")] = i18nc("Stanley Gibbons stamp catalog code", "Stanley Gibbons"); 0815 hash[QStringLiteral("michel")] = i18nc("Michel stamp catalog code", "Michel"); 0816 hash[QStringLiteral("pegi")] = i18n("PEGI Rating"); 0817 return hash; 0818 } 0819 0820 // Colnect specific method of turning name text into a slug 0821 // $str = html_entity_decode($str, ENT_QUOTES, 'UTF-8'); 0822 // $str = preg_replace('/&[^;]+;/', '_', $str); # change HTML elements to underscore 0823 // $str = str_replace(array('.', '"', '>', '<', '\\', ':', '/', '?', '#', '[', ']', '@', '!', '$', '&', '\'', '(', ')', '*', '+', ',', ';', '='), '', $str); 0824 // $str = preg_replace('/[\s_]+/', '_', $str); # any space sequence becomes a single underscore 0825 // $str = trim($str, '_'); # trim underscores 0826 QString ColnectFetcher::URLize(const QString& name_) { 0827 QString slug = name_; 0828 static const QString underscore(QStringLiteral("_")); 0829 static const QRegularExpression htmlElements(QStringLiteral("&[^;]+;")); 0830 static const QRegularExpression toRemove(QStringLiteral("[.\"><\\:/?#\\[\\]@!$&'()*+,;=]")); 0831 static const QRegularExpression spaces(QStringLiteral("\\s")); 0832 slug.replace(htmlElements, underscore); 0833 slug.remove(toRemove); 0834 slug.replace(spaces, underscore); 0835 while(slug.startsWith(underscore)) slug = slug.mid(1); 0836 while(slug.endsWith(underscore)) slug.chop(1); 0837 return slug; 0838 } 0839 0840 QString ColnectFetcher::imageUrl(const QString& name_, const QString& id_) { 0841 if(m_imageSize == NoImage) return QString(); 0842 const QString nameSlug = URLize(name_); 0843 const int id = id_.toInt(); 0844 QUrl u(QString::fromLatin1(COLNECT_IMAGE_URL)); 0845 // uses 't' for thumbnail, use 'f' for full-size 0846 u.setPath(QString::fromLatin1("/%1/%2/%3/%4.jpg") 0847 .arg(m_imageSize == SmallImage ? QLatin1Char('t') : QLatin1Char('f')) 0848 .arg(id / 1000) 0849 .arg(id % 1000, 3, 10, QLatin1Char('0')) 0850 .arg(nameSlug)); 0851 // myDebug() << "Image url:" << u; 0852 return u.toString(); 0853 } 0854 0855 void ColnectFetcher::readDataList() { 0856 QUrl u(QString::fromLatin1(COLNECT_API_URL)); 0857 // Colnect API calls are encoded as a path 0858 QString query(QLatin1Char('/') + m_locale + QStringLiteral("/fields/cat/") + m_category + QLatin1Char('/')); 0859 u.setPath(u.path() + query); 0860 myLog() << "Reading Colnect fields from" << u.toDisplayString(); 0861 0862 const QByteArray data = FileHandler::readDataFile(u, true); 0863 QJsonDocument doc = QJsonDocument::fromJson(data); 0864 if(doc.isNull()) { 0865 myDebug() << "null JSON document in colnect fields"; 0866 return; 0867 } 0868 QVariantList resultList = doc.array().toVariantList(); 0869 if(resultList.isEmpty()) { 0870 myDebug() << "no colnect field results"; 0871 return; 0872 } 0873 m_colnectFields.clear(); 0874 for(int i = 0; i < resultList.size(); ++i) { 0875 m_colnectFields.insert(resultList.at(i).toString(), i); 0876 } 0877 // myDebug() << "Colnect fields:" << m_colnectFields; 0878 } 0879 0880 void ColnectFetcher::readItemNames(const QByteArray& item_) { 0881 QUrl u(QString::fromLatin1(COLNECT_API_URL)); 0882 // Colnect API calls are encoded as a path 0883 QString query(QLatin1Char('/') + m_locale + QLatin1Char('/') + QLatin1String(item_) + QStringLiteral("/cat/") + m_category + QLatin1Char('/')); 0884 u.setPath(u.path() + query); 0885 myLog() << "Reading item names from" << u.toDisplayString(); 0886 0887 const QByteArray data = FileHandler::readDataFile(u, true); 0888 QJsonDocument doc = QJsonDocument::fromJson(data); 0889 if(doc.isNull()) { 0890 myDebug() << "null JSON document in colnect results"; 0891 return; 0892 } 0893 QJsonArray resultList = doc.array(); 0894 if(resultList.isEmpty()) { 0895 myDebug() << "no item results"; 0896 return; 0897 } 0898 QHash<int, QString> itemNames; 0899 for(int i = 0; i < resultList.size(); ++i) { 0900 // an array of arrays, first value is id, second is name 0901 const QJsonArray values = resultList.at(i).toArray(); 0902 if(values.size() > 2) { 0903 itemNames.insert(values.at(0).toInt(), values.at(1).toString()); 0904 } 0905 } 0906 m_itemNames.insert(item_, itemNames); 0907 } 0908 0909 ColnectFetcher::ConfigWidget::ConfigWidget(QWidget* parent_, const ColnectFetcher* fetcher_) 0910 : Fetch::ConfigWidget(parent_) { 0911 QGridLayout* l = new QGridLayout(optionsWidget()); 0912 l->setSpacing(4); 0913 l->setColumnStretch(1, 10); 0914 0915 int row = -1; 0916 0917 QLabel* label = new QLabel(i18n("Language: "), optionsWidget()); 0918 l->addWidget(label, ++row, 0); 0919 m_langCombo = new GUI::ComboBox(optionsWidget()); 0920 0921 #define LANG_ITEM(NAME, CY, ISO) \ 0922 m_langCombo->addItem(QIcon(QStandardPaths::locate(QStandardPaths::GenericDataLocation, \ 0923 QStringLiteral("kf5/locale/countries/" CY "/flag.png"))), \ 0924 i18nc("Language", NAME), \ 0925 QLatin1String(ISO)); 0926 LANG_ITEM("English", "us", "en"); 0927 LANG_ITEM("French", "fr", "fr"); 0928 LANG_ITEM("German", "de", "de"); 0929 LANG_ITEM("Spanish", "es", "es"); 0930 #undef LANG_ITEM 0931 0932 // instead of trying to include all possible languages offered by Colnect 0933 // allow the user to enter it 0934 m_langCombo->setEditable(true); 0935 QRegularExpression rx(QLatin1String("\\w\\w")); // only 2 characters 0936 QRegularExpressionValidator* val = new QRegularExpressionValidator(rx, this); 0937 m_langCombo->setValidator(val); 0938 0939 void (GUI::ComboBox::* activatedInt)(int) = &GUI::ComboBox::activated; 0940 connect(m_langCombo, activatedInt, this, &ConfigWidget::slotSetModified); 0941 connect(m_langCombo, activatedInt, this, &ConfigWidget::slotLangChanged); 0942 l->addWidget(m_langCombo, row, 1); 0943 label->setBuddy(m_langCombo); 0944 0945 label = new QLabel(i18n("&Image size: "), optionsWidget()); 0946 l->addWidget(label, ++row, 0); 0947 m_imageCombo = new GUI::ComboBox(optionsWidget()); 0948 m_imageCombo->addItem(i18n("No Image"), NoImage); 0949 m_imageCombo->addItem(i18n("Small Image"), SmallImage); 0950 m_imageCombo->addItem(i18n("Large Image"), LargeImage); 0951 connect(m_imageCombo, activatedInt, this, &ConfigWidget::slotSetModified); 0952 l->addWidget(m_imageCombo, row, 1); 0953 label->setBuddy(m_imageCombo); 0954 0955 l->setRowStretch(++row, 10); 0956 0957 // now add additional fields widget 0958 addFieldsWidget(ColnectFetcher::allOptionalFields(), fetcher_ ? fetcher_->optionalFields() : QStringList()); 0959 0960 if(fetcher_) { 0961 bool success = m_langCombo->setCurrentData(fetcher_->m_locale); 0962 // a user-entered iso code might not be in the data list, insert it if not 0963 if(!success) { 0964 m_langCombo->addItem(fetcher_->m_locale, fetcher_->m_locale); 0965 m_langCombo->setCurrentIndex(m_langCombo->count()-1); 0966 } 0967 m_imageCombo->setCurrentData(fetcher_->m_imageSize); 0968 } 0969 } 0970 0971 void ColnectFetcher::ConfigWidget::saveConfigHook(KConfigGroup& config_) { 0972 QString lang = m_langCombo->currentData().toString(); 0973 if(lang.isEmpty()) { 0974 // might be user-entered 0975 lang = m_langCombo->currentText(); 0976 } 0977 config_.writeEntry("Locale", lang); 0978 0979 const int n = m_imageCombo->currentData().toInt(); 0980 config_.writeEntry("Image Size", n); 0981 } 0982 0983 QString ColnectFetcher::ConfigWidget::preferredName() const { 0984 return QString::fromLatin1("Colnect (%1)").arg(m_langCombo->currentText()); 0985 } 0986 0987 void ColnectFetcher::ConfigWidget::slotLangChanged() { 0988 emit signalName(preferredName()); 0989 }