File indexing completed on 2024-05-12 05:09:27
0001 /*************************************************************************** 0002 Copyright (C) 2004-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 <config.h> 0026 0027 #include "amazonfetcher.h" 0028 #include "amazonrequest.h" 0029 #include "../collectionfactory.h" 0030 #include "../images/imagefactory.h" 0031 #include "../utils/guiproxy.h" 0032 #include "../collection.h" 0033 #include "../entry.h" 0034 #include "../field.h" 0035 #include "../fieldformat.h" 0036 #include "../utils/string_utils.h" 0037 #include "../utils/mapvalue.h" 0038 #include "../utils/isbnvalidator.h" 0039 #include "../gui/combobox.h" 0040 #include "../tellico_debug.h" 0041 0042 #include <KLocalizedString> 0043 #include <KIO/Job> 0044 #include <KIO/JobUiDelegate> 0045 #include <KSeparator> 0046 #include <KAcceleratorManager> 0047 #include <KConfigGroup> 0048 #include <KJobWidgets/KJobWidgets> 0049 0050 #include <QLineEdit> 0051 #include <QLabel> 0052 #include <QCheckBox> 0053 #include <QFile> 0054 #include <QDir> 0055 #include <QTextStream> 0056 #include <QTextCodec> 0057 #include <QGridLayout> 0058 #include <QStandardPaths> 0059 #include <QJsonDocument> 0060 #include <QJsonObject> 0061 #include <QJsonArray> 0062 #include <QTemporaryFile> 0063 0064 namespace { 0065 static const int AMAZON_RETURNS_PER_REQUEST = 10; 0066 static const int AMAZON_MAX_RETURNS_TOTAL = 20; 0067 static const char* AMAZON_ASSOC_TOKEN = "tellico-20"; 0068 } 0069 0070 using namespace Tellico; 0071 using Tellico::Fetch::AmazonFetcher; 0072 0073 // static 0074 // see https://webservices.amazon.com/paapi5/documentation/common-request-parameters.html#host-and-region 0075 const AmazonFetcher::SiteData& AmazonFetcher::siteData(int site_) { 0076 Q_ASSERT(site_ >= 0); 0077 Q_ASSERT(site_ < XX); 0078 static SiteData dataVector[16] = { 0079 { 0080 i18n("Amazon (US)"), 0081 "webservices.amazon.com", 0082 "us-east-1", 0083 QLatin1String("us"), 0084 i18n("United States") 0085 }, { 0086 i18n("Amazon (UK)"), 0087 "webservices.amazon.co.uk", 0088 "eu-west-1", 0089 QLatin1String("gb"), 0090 i18n("United Kingdom") 0091 }, { 0092 i18n("Amazon (Germany)"), 0093 "webservices.amazon.de", 0094 "eu-west-1", 0095 QLatin1String("de"), 0096 i18n("Germany") 0097 }, { 0098 i18n("Amazon (Japan)"), 0099 "webservices.amazon.co.jp", 0100 "us-west-2", 0101 QLatin1String("jp"), 0102 i18n("Japan") 0103 }, { 0104 i18n("Amazon (France)"), 0105 "webservices.amazon.fr", 0106 "eu-west-1", 0107 QLatin1String("fr"), 0108 i18n("France") 0109 }, { 0110 i18n("Amazon (Canada)"), 0111 "webservices.amazon.ca", 0112 "us-east-1", 0113 QLatin1String("ca"), 0114 i18n("Canada") 0115 }, { 0116 // TODO: no chinese in PAAPI-5 yet? 0117 i18n("Amazon (China)"), 0118 "webservices.amazon.cn", 0119 "us-west-2", 0120 QLatin1String("ch"), 0121 i18n("China") 0122 }, { 0123 i18n("Amazon (Spain)"), 0124 "webservices.amazon.es", 0125 "eu-west-1", 0126 QLatin1String("es"), 0127 i18n("Spain") 0128 }, { 0129 i18n("Amazon (Italy)"), 0130 "webservices.amazon.it", 0131 "eu-west-1", 0132 QLatin1String("it"), 0133 i18n("Italy") 0134 }, { 0135 i18n("Amazon (Brazil)"), 0136 "webservices.amazon.com.br", 0137 "us-east-1", 0138 QLatin1String("br"), 0139 i18n("Brazil") 0140 }, { 0141 i18n("Amazon (Australia)"), 0142 "webservices.amazon.com.au", 0143 "us-west-2", 0144 QLatin1String("au"), 0145 i18n("Australia") 0146 }, { 0147 i18n("Amazon (India)"), 0148 "webservices.amazon.in", 0149 "eu-west-1", 0150 QLatin1String("in"), 0151 i18n("India") 0152 }, { 0153 i18n("Amazon (Mexico)"), 0154 "webservices.amazon.com.mx", 0155 "us-east-1", 0156 QLatin1String("mx"), 0157 i18n("Mexico") 0158 }, { 0159 i18n("Amazon (Turkey)"), 0160 "webservices.amazon.com.tr", 0161 "eu-west-1", 0162 QLatin1String("tr"), 0163 i18n("Turkey") 0164 }, { 0165 i18n("Amazon (Singapore)"), 0166 "webservices.amazon.sg", 0167 "us-west-2", 0168 QLatin1String("sg"), 0169 i18n("Singapore") 0170 }, { 0171 i18n("Amazon (UAE)"), 0172 "webservices.amazon.ae", 0173 "eu-west-1", 0174 QLatin1String("ae"), 0175 i18n("United Arab Emirates") 0176 } 0177 }; 0178 0179 return dataVector[qBound(0, site_, static_cast<int>(sizeof(dataVector)/sizeof(SiteData)))]; 0180 } 0181 0182 AmazonFetcher::AmazonFetcher(QObject* parent_) 0183 : Fetcher(parent_), m_site(Unknown), m_imageSize(MediumImage), 0184 m_assoc(QLatin1String(AMAZON_ASSOC_TOKEN)), m_limit(AMAZON_MAX_RETURNS_TOTAL), 0185 m_countOffset(0), m_page(1), m_total(-1), m_numResults(0), m_job(nullptr), m_started(false) { 0186 // to facilitate transition to Amazon PAAPI5, allow users to enable logging the Amazon 0187 // results so they can be shared for debugging 0188 const QByteArray enableLog = qgetenv("TELLICO_ENABLE_AMAZON_LOG").trimmed().toLower(); 0189 m_enableLog = (enableLog == "true" || enableLog == "1"); 0190 } 0191 0192 AmazonFetcher::~AmazonFetcher() { 0193 } 0194 0195 QString AmazonFetcher::source() const { 0196 return m_name.isEmpty() ? defaultName() : m_name; 0197 } 0198 0199 QString AmazonFetcher::attribution() const { 0200 return i18n("This data is licensed under <a href=""%1"">specific terms</a>.", 0201 QLatin1String("https://affiliate-program.amazon.com/gp/advertising/api/detail/agreement.html")); 0202 } 0203 0204 bool AmazonFetcher::canFetch(int type) const { 0205 return type == Data::Collection::Book 0206 || type == Data::Collection::ComicBook 0207 || type == Data::Collection::Bibtex 0208 || type == Data::Collection::Album 0209 || type == Data::Collection::Video 0210 || type == Data::Collection::Game 0211 || type == Data::Collection::BoardGame; 0212 } 0213 0214 bool AmazonFetcher::canSearch(Fetch::FetchKey k) const { 0215 // no UPC in Canada 0216 return k == Title 0217 || k == Person 0218 || k == ISBN 0219 || k == UPC 0220 || k == Keyword; 0221 } 0222 0223 void AmazonFetcher::readConfigHook(const KConfigGroup& config_) { 0224 const int site = config_.readEntry("Site", int(Unknown)); 0225 Q_ASSERT(site != Unknown); 0226 m_site = static_cast<Site>(site); 0227 if(m_name.isEmpty()) { 0228 m_name = siteData(m_site).title; 0229 } 0230 QString s = config_.readEntry("AccessKey"); 0231 if(!s.isEmpty()) { 0232 m_accessKey = s; 0233 } else { 0234 myWarning() << "No Amazon access key"; 0235 } 0236 s = config_.readEntry("AssocToken"); 0237 if(!s.isEmpty()) { 0238 m_assoc = s; 0239 } 0240 s = config_.readEntry("SecretKey"); 0241 if(!s.isEmpty()) { 0242 m_secretKey = s; 0243 } else { 0244 myWarning() << "No Amazon secret key"; 0245 } 0246 int imageSize = config_.readEntry("Image Size", -1); 0247 if(imageSize > -1) { 0248 m_imageSize = static_cast<ImageSize>(imageSize); 0249 } 0250 } 0251 0252 void AmazonFetcher::search() { 0253 m_started = true; 0254 m_page = 1; 0255 m_total = -1; 0256 m_countOffset = 0; 0257 m_numResults = 0; 0258 doSearch(); 0259 } 0260 0261 void AmazonFetcher::continueSearch() { 0262 m_started = true; 0263 m_limit += AMAZON_MAX_RETURNS_TOTAL; 0264 doSearch(); 0265 } 0266 0267 void AmazonFetcher::doSearch() { 0268 if(m_secretKey.isEmpty() || m_accessKey.isEmpty()) { 0269 // this message is split in two since the first half is reused later 0270 message(i18n("Access to data from Amazon.com requires an AWS Access Key ID and a Secret Key.") + 0271 QLatin1Char(' ') + 0272 i18n("Those values must be entered in the data source settings."), MessageHandler::Error); 0273 stop(); 0274 return; 0275 } 0276 0277 const QByteArray payload = requestPayload(request()); 0278 if(payload.isEmpty()) { 0279 stop(); 0280 return; 0281 } 0282 0283 QString path(QStringLiteral("/paapi5/searchitems")); 0284 0285 AmazonRequest request(m_accessKey, m_secretKey); 0286 request.setHost(siteData(m_site).host); 0287 request.setRegion(siteData(m_site).region); 0288 request.setPath(path.toUtf8()); 0289 0290 // debugging check 0291 if(m_testResultsFile.isEmpty()) { 0292 QUrl u; 0293 u.setScheme(QLatin1String("https")); 0294 u.setHost(QString::fromUtf8(siteData(m_site).host)); 0295 u.setPath(path); 0296 m_job = KIO::storedHttpPost(payload, u, KIO::HideProgressInfo); 0297 QStringList customHeaders; 0298 QMapIterator<QByteArray, QByteArray> i(request.headers(payload)); 0299 while(i.hasNext()) { 0300 i.next(); 0301 customHeaders += QString::fromUtf8(i.key() + ": " + i.value()); 0302 } 0303 m_job->addMetaData(QStringLiteral("customHTTPHeader"), customHeaders.join(QLatin1String("\r\n"))); 0304 } else { 0305 myDebug() << "Reading" << m_testResultsFile; 0306 m_job = KIO::storedGet(QUrl::fromLocalFile(m_testResultsFile), KIO::NoReload, KIO::HideProgressInfo); 0307 } 0308 KJobWidgets::setWindow(m_job, GUI::Proxy::widget()); 0309 connect(m_job.data(), &KJob::result, 0310 this, &AmazonFetcher::slotComplete); 0311 } 0312 0313 void AmazonFetcher::stop() { 0314 if(!m_started) { 0315 return; 0316 } 0317 if(m_job) { 0318 m_job->kill(); 0319 m_job = nullptr; 0320 } 0321 m_started = false; 0322 emit signalDone(this); 0323 } 0324 0325 void AmazonFetcher::slotComplete(KJob*) { 0326 if(m_job->error()) { 0327 myDebug() << m_job->errorString() << m_job->data(); 0328 myDebug() << "Response code is" << m_job->metaData().value(QStringLiteral("responsecode")); 0329 m_job->uiDelegate()->showErrorMessage(); 0330 stop(); 0331 return; 0332 } 0333 0334 const QByteArray data = m_job->data(); 0335 if(data.isEmpty()) { 0336 myDebug() << "no data"; 0337 stop(); 0338 return; 0339 } 0340 0341 // since the fetch is done, don't worry about holding the job pointer 0342 m_job = nullptr; 0343 0344 if(m_enableLog) { 0345 QTemporaryFile logFile(QDir::tempPath() + QStringLiteral("/amazon-search-items-XXXXXX.json")); 0346 logFile.setAutoRemove(false); 0347 if(logFile.open()) { 0348 QTextStream t(&logFile); 0349 t.setCodec("UTF-8"); 0350 t << data; 0351 myLog() << "Writing Amazon data output to" << logFile.fileName(); 0352 } 0353 } 0354 #if 0 0355 myWarning() << "Remove debug from amazonfetcher.cpp"; 0356 QFile f(QString::fromLatin1("/tmp/test%1.json").arg(m_page)); 0357 if(f.open(QIODevice::WriteOnly)) { 0358 QTextStream t(&f); 0359 t.setCodec("UTF-8"); 0360 t << data; 0361 } 0362 f.close(); 0363 #endif 0364 0365 QJsonParseError jsonError; 0366 QJsonObject databject = QJsonDocument::fromJson(data, &jsonError).object(); 0367 if(jsonError.error != QJsonParseError::NoError) { 0368 myDebug() << "AmazonFetcher: JSON error -" << jsonError.errorString(); 0369 message(jsonError.errorString(), MessageHandler::Error); 0370 stop(); 0371 return; 0372 } 0373 QJsonObject resultObject = databject.value(QStringLiteral("SearchResult")).toObject(); 0374 if(resultObject.isEmpty()) { 0375 resultObject = databject.value(QStringLiteral("ItemsResult")).toObject(); 0376 } 0377 0378 if(m_total == -1) { 0379 int totalResults = resultObject.value(QStringLiteral("TotalResultCount")).toInt(); 0380 if(totalResults > 0) { 0381 m_total = totalResults; 0382 // myDebug() << "Total results is" << totalResults; 0383 } 0384 } 0385 0386 QStringList errors; 0387 QJsonValue errorValue = databject.value(QLatin1String("Errors")); 0388 if(!errorValue.isNull()) { 0389 foreach(const QJsonValue& error, errorValue.toArray()) { 0390 errors += error.toObject().value(QLatin1String("Message")).toString(); 0391 } 0392 } 0393 if(!errors.isEmpty()) { 0394 for(QStringList::ConstIterator it = errors.constBegin(); it != errors.constEnd(); ++it) { 0395 myDebug() << "AmazonFetcher::" << *it; 0396 } 0397 message(errors[0], MessageHandler::Error); 0398 stop(); 0399 return; 0400 } 0401 0402 Data::CollPtr coll = createCollection(); 0403 if(!coll) { 0404 myDebug() << "no collection pointer"; 0405 stop(); 0406 return; 0407 } 0408 0409 foreach(const QJsonValue& item, resultObject.value(QLatin1String("Items")).toArray()) { 0410 if(m_numResults >= m_limit) { 0411 break; 0412 } 0413 if(!m_started) { 0414 // might get aborted 0415 break; 0416 } 0417 Data::EntryPtr entry(new Data::Entry(coll)); 0418 populateEntry(entry, item.toObject()); 0419 0420 // special case book author 0421 // amazon is really bad about not putting spaces after periods 0422 if(coll->type() == Data::Collection::Book) { 0423 static const QRegularExpression rx(QLatin1String("\\.([^\\s])")); 0424 QStringList values = FieldFormat::splitValue(entry->field(QStringLiteral("author"))); 0425 for(QStringList::Iterator it = values.begin(); it != values.end(); ++it) { 0426 (*it).replace(rx, QStringLiteral(". \\1")); 0427 } 0428 entry->setField(QStringLiteral("author"), values.join(FieldFormat::delimiterString())); 0429 } 0430 0431 // UK puts the year in the title for some reason 0432 if(m_site == UK && coll->type() == Data::Collection::Video) { 0433 static const QRegularExpression rx(QLatin1String("\\[(\\d{4})\\]")); 0434 const QString titleString(QStringLiteral("title")); 0435 QString t = entry->field(titleString); 0436 auto match = rx.match(t); 0437 if(match.hasMatch()) { 0438 t = t.remove(rx).simplified(); 0439 entry->setField(titleString, t); 0440 const QString yearString(QStringLiteral("year")); 0441 if(entry->field(yearString).isEmpty()) { 0442 entry->setField(yearString, match.captured(1)); 0443 } 0444 } 0445 } 0446 0447 // myDebug() << entry->title(); 0448 FetchResult* r = new FetchResult(this, entry); 0449 m_entries.insert(r->uid, entry); 0450 emit signalResultFound(r); 0451 ++m_numResults; 0452 } 0453 0454 // we might have gotten aborted 0455 if(!m_started) { 0456 return; 0457 } 0458 0459 // are there any additional results to get? 0460 m_hasMoreResults = m_testResultsFile.isEmpty() && (m_page * AMAZON_RETURNS_PER_REQUEST < m_total); 0461 0462 const int currentTotal = qMin(m_total, m_limit); 0463 if(m_testResultsFile.isEmpty() && (m_page * AMAZON_RETURNS_PER_REQUEST < currentTotal)) { 0464 int foundCount = (m_page-1) * AMAZON_RETURNS_PER_REQUEST + coll->entryCount(); 0465 message(i18n("Results from %1: %2/%3", source(), foundCount, m_total), MessageHandler::Status); 0466 ++m_page; 0467 m_countOffset = 0; 0468 doSearch(); 0469 } else if(request().value().count(QLatin1Char(';')) > 9) { 0470 // start new request after cutting off first 10 isbn values 0471 FetchRequest newRequest(request().collectionType(), 0472 request().key(), 0473 request().value().section(QLatin1Char(';'), 10)); 0474 startSearch(newRequest); 0475 } else { 0476 m_countOffset = m_entries.count() % AMAZON_RETURNS_PER_REQUEST; 0477 if(m_countOffset == 0) { 0478 ++m_page; // need to go to next page 0479 } 0480 stop(); 0481 } 0482 } 0483 0484 Tellico::Data::EntryPtr AmazonFetcher::fetchEntryHook(uint uid_) { 0485 Data::EntryPtr entry = m_entries[uid_]; 0486 if(!entry) { 0487 myWarning() << "no entry in dict"; 0488 return entry; 0489 } 0490 0491 // do what we can to remove useless keywords 0492 const int type = collectionType(); 0493 switch(type) { 0494 case Data::Collection::Book: 0495 case Data::Collection::ComicBook: 0496 case Data::Collection::Bibtex: 0497 if(optionalFields().contains(QStringLiteral("keyword"))) { 0498 QStringList newWords; 0499 const QStringList keywords = FieldFormat::splitValue(entry->field(QStringLiteral("keyword"))); 0500 foreach(const QString& keyword, keywords) { 0501 if(keyword == QLatin1String("General") || 0502 keyword == QLatin1String("Subjects") || 0503 keyword == QLatin1String("Par prix") || // french stuff 0504 keyword == QLatin1String("Divers") || // french stuff 0505 keyword.startsWith(QLatin1Char('(')) || 0506 keyword.startsWith(QLatin1String("Authors"))) { 0507 continue; 0508 } 0509 newWords += keyword; 0510 } 0511 newWords.removeDuplicates(); 0512 entry->setField(QStringLiteral("keyword"), newWords.join(FieldFormat::delimiterString())); 0513 } 0514 entry->setField(QStringLiteral("comments"), Tellico::decodeHTML(entry->field(QStringLiteral("comments")))); 0515 break; 0516 0517 case Data::Collection::Video: 0518 { 0519 const QString genres = QStringLiteral("genre"); 0520 QStringList oldWords = FieldFormat::splitValue(entry->field(genres)); 0521 QStringList newWords; 0522 // only care about genres that have "Genres" in the amazon response 0523 // and take the first word after that 0524 for(QStringList::Iterator it = oldWords.begin(); it != oldWords.end(); ++it) { 0525 if((*it).indexOf(QLatin1String("Genres")) == -1) { 0526 continue; 0527 } 0528 0529 // the amazon2tellico stylesheet separates words with '/' 0530 QStringList nodes = (*it).split(QLatin1Char('/')); 0531 for(QStringList::Iterator it2 = nodes.begin(); it2 != nodes.end(); ++it2) { 0532 if(*it2 != QLatin1String("Genres")) { 0533 continue; 0534 } 0535 ++it2; 0536 if(it2 != nodes.end() && *it2 != QLatin1String("General")) { 0537 newWords += *it2; 0538 } 0539 break; // we're done 0540 } 0541 } 0542 newWords.removeDuplicates(); 0543 entry->setField(genres, newWords.join(FieldFormat::delimiterString())); 0544 // language tracks get duplicated, too 0545 newWords = FieldFormat::splitValue(entry->field(QStringLiteral("language"))); 0546 newWords.removeDuplicates(); 0547 entry->setField(QStringLiteral("language"), newWords.join(FieldFormat::delimiterString())); 0548 } 0549 entry->setField(QStringLiteral("plot"), Tellico::decodeHTML(entry->field(QStringLiteral("plot")))); 0550 break; 0551 0552 case Data::Collection::Album: 0553 { 0554 const QString genres = QStringLiteral("genre"); 0555 QStringList oldWords = FieldFormat::splitValue(entry->field(genres)); 0556 QStringList newWords; 0557 // only care about genres that have "Styles" in the amazon response 0558 // and take the first word after that 0559 for(QStringList::Iterator it = oldWords.begin(); it != oldWords.end(); ++it) { 0560 if((*it).indexOf(QLatin1String("Styles")) == -1) { 0561 continue; 0562 } 0563 0564 // the amazon2tellico stylesheet separates words with '/' 0565 QStringList nodes = (*it).split(QLatin1Char('/')); 0566 bool isStyle = false; 0567 for(QStringList::Iterator it2 = nodes.begin(); it2 != nodes.end(); ++it2) { 0568 if(!isStyle) { 0569 if(*it2 == QLatin1String("Styles")) { 0570 isStyle = true; 0571 } 0572 continue; 0573 } 0574 if(*it2 != QLatin1String("General")) { 0575 newWords += *it2; 0576 } 0577 } 0578 } 0579 newWords.removeDuplicates(); 0580 entry->setField(genres, newWords.join(FieldFormat::delimiterString())); 0581 } 0582 entry->setField(QStringLiteral("comments"), Tellico::decodeHTML(entry->field(QStringLiteral("comments")))); 0583 break; 0584 0585 case Data::Collection::Game: 0586 entry->setField(QStringLiteral("description"), Tellico::decodeHTML(entry->field(QStringLiteral("description")))); 0587 break; 0588 } 0589 0590 // clean up the title 0591 parseTitle(entry); 0592 0593 // also sometimes table fields have rows but no values 0594 Data::FieldList fields = entry->collection()->fields(); 0595 static const QRegularExpression blank(QLatin1String("^[\\s") + 0596 FieldFormat::columnDelimiterString() + 0597 FieldFormat::delimiterString() + 0598 QLatin1String("]+$")); // only white space, column separators and value separators 0599 foreach(Data::FieldPtr fIt, fields) { 0600 if(fIt->type() != Data::Field::Table) { 0601 continue; 0602 } 0603 auto blankMatch = blank.match(entry->field(fIt)); 0604 if(blankMatch.hasMatch()) { 0605 entry->setField(fIt, QString()); 0606 } 0607 } 0608 0609 // don't want to show image urls in the fetch dialog 0610 // so clear them after reading the URL 0611 QString imageURL; 0612 switch(m_imageSize) { 0613 case SmallImage: 0614 imageURL = entry->field(QStringLiteral("small-image")); 0615 entry->setField(QStringLiteral("small-image"), QString()); 0616 break; 0617 case MediumImage: 0618 imageURL = entry->field(QStringLiteral("medium-image")); 0619 entry->setField(QStringLiteral("medium-image"), QString()); 0620 break; 0621 case LargeImage: 0622 imageURL = entry->field(QStringLiteral("large-image")); 0623 entry->setField(QStringLiteral("large-image"), QString()); 0624 break; 0625 case NoImage: 0626 default: 0627 break; 0628 } 0629 0630 if(!imageURL.isEmpty()) { 0631 // myDebug() << "grabbing " << imageURL; 0632 QString id = ImageFactory::addImage(QUrl::fromUserInput(imageURL), true); 0633 if(id.isEmpty()) { 0634 message(i18n("The cover image could not be loaded."), MessageHandler::Warning); 0635 } else { // amazon serves up 1x1 gifs occasionally, but that's caught in the image constructor 0636 // all relevant collection types have cover fields 0637 entry->setField(QStringLiteral("cover"), id); 0638 } 0639 } 0640 0641 return entry; 0642 } 0643 0644 Tellico::Fetch::FetchRequest AmazonFetcher::updateRequest(Data::EntryPtr entry_) { 0645 const int type = entry_->collection()->type(); 0646 const QString t = entry_->field(QStringLiteral("title")); 0647 if(type == Data::Collection::Book || type == Data::Collection::ComicBook || type == Data::Collection::Bibtex) { 0648 const QString isbn = entry_->field(QStringLiteral("isbn")); 0649 if(!isbn.isEmpty()) { 0650 return FetchRequest(Fetch::ISBN, isbn); 0651 } 0652 const QString a = entry_->field(QStringLiteral("author")); 0653 if(!a.isEmpty()) { 0654 return t.isEmpty() ? FetchRequest(Fetch::Person, a) 0655 : FetchRequest(Fetch::Keyword, t + QLatin1Char('-') + a); 0656 } 0657 } else if(type == Data::Collection::Album) { 0658 const QString a = entry_->field(QStringLiteral("artist")); 0659 if(!a.isEmpty()) { 0660 return t.isEmpty() ? FetchRequest(Fetch::Person, a) 0661 : FetchRequest(Fetch::Keyword, t + QLatin1Char('-') + a); 0662 } 0663 } 0664 0665 // optimistically try searching for title and rely on Collection::sameEntry() to figure things out 0666 if(!t.isEmpty()) { 0667 return FetchRequest(Fetch::Title, t); 0668 } 0669 0670 return FetchRequest(); 0671 } 0672 0673 QByteArray AmazonFetcher::requestPayload(const Fetch::FetchRequest& request_) { 0674 QJsonObject payload; 0675 payload.insert(QLatin1String("PartnerTag"), m_assoc); 0676 payload.insert(QLatin1String("PartnerType"), QLatin1String("Associates")); 0677 payload.insert(QLatin1String("Service"), QLatin1String("ProductAdvertisingAPIv1")); 0678 payload.insert(QLatin1String("Operation"), QLatin1String("SearchItems")); 0679 payload.insert(QLatin1String("SortBy"), QLatin1String("Relevance")); 0680 // not mandatory 0681 // payload.insert(QLatin1String("Marketplace"), QLatin1String(siteData(m_site).host)); 0682 if(m_page > 1) { 0683 payload.insert(QLatin1String("ItemPage"), m_page); 0684 } 0685 0686 QJsonArray resources; 0687 resources.append(QLatin1String("ItemInfo.Title")); 0688 resources.append(QLatin1String("ItemInfo.ContentInfo")); 0689 resources.append(QLatin1String("ItemInfo.ByLineInfo")); 0690 resources.append(QLatin1String("ItemInfo.TechnicalInfo")); 0691 0692 const int type = request_.collectionType(); 0693 switch(type) { 0694 case Data::Collection::Book: 0695 case Data::Collection::ComicBook: 0696 case Data::Collection::Bibtex: 0697 payload.insert(QLatin1String("SearchIndex"), QLatin1String("Books")); 0698 resources.append(QLatin1String("ItemInfo.ExternalIds")); 0699 resources.append(QLatin1String("ItemInfo.ManufactureInfo")); 0700 break; 0701 0702 case Data::Collection::Album: 0703 payload.insert(QLatin1String("SearchIndex"), QLatin1String("Music")); 0704 break; 0705 0706 case Data::Collection::Video: 0707 // CA and JP appear to have a bug where Video only returns VHS or Music results 0708 // DVD will return DVD, Blu-ray, etc. so just ignore VHS for those users 0709 payload.insert(QLatin1String("SearchIndex"), QLatin1String("MoviesAndTV")); 0710 if(m_site == CA || m_site == JP || m_site == IT || m_site == ES) { 0711 payload.insert(QStringLiteral("SearchIndex"), QStringLiteral("DVD")); 0712 } else { 0713 payload.insert(QStringLiteral("SearchIndex"), QStringLiteral("Video")); 0714 } 0715 // params.insert(QStringLiteral("SortIndex"), QStringLiteral("relevancerank")); 0716 resources.append(QLatin1String("ItemInfo.ContentRating")); 0717 break; 0718 0719 case Data::Collection::Game: 0720 payload.insert(QLatin1String("SearchIndex"), QLatin1String("VideoGames")); 0721 break; 0722 0723 case Data::Collection::BoardGame: 0724 payload.insert(QLatin1String("SearchIndex"), QLatin1String("ToysAndGames")); 0725 // params.insert(QStringLiteral("SortIndex"), QStringLiteral("relevancerank")); 0726 break; 0727 0728 case Data::Collection::Coin: 0729 case Data::Collection::Stamp: 0730 case Data::Collection::Wine: 0731 case Data::Collection::Base: 0732 case Data::Collection::Card: 0733 myDebug() << "can't fetch this type:" << collectionType(); 0734 return QByteArray(); 0735 } 0736 0737 switch(request_.key()) { 0738 case Title: 0739 payload.insert(QLatin1String("Title"), request_.value()); 0740 break; 0741 0742 case Person: 0743 if(type == Data::Collection::Video) { 0744 payload.insert(QStringLiteral("Actor"), request_.value()); 0745 // payload.insert(QStringLiteral("Director"), request_.value()); 0746 } else if(type == Data::Collection::Album) { 0747 payload.insert(QStringLiteral("Artist"), request_.value()); 0748 } else if(type == Data::Collection::Book) { 0749 payload.insert(QLatin1String("Author"), request_.value()); 0750 } else { 0751 payload.insert(QLatin1String("Keywords"), request_.value()); 0752 } 0753 break; 0754 0755 case ISBN: 0756 { 0757 QString cleanValue = request_.value(); 0758 cleanValue.remove(QLatin1Char('-')); 0759 // ISBN only get digits or 'X' 0760 QStringList isbns = FieldFormat::splitValue(cleanValue); 0761 // Amazon isbn13 search is still very flaky, so if possible, we're going to convert 0762 // all of them to isbn10. If we run into a 979 isbn13, then we're forced to do an 0763 // isbn13 search 0764 bool isbn13 = false; 0765 for(QStringList::Iterator it = isbns.begin(); it != isbns.end(); ) { 0766 if((*it).startsWith(QLatin1String("979"))) { 0767 isbn13 = true; 0768 break; 0769 } 0770 ++it; 0771 } 0772 // if we want isbn10, then convert all 0773 if(!isbn13) { 0774 for(QStringList::Iterator it = isbns.begin(); it != isbns.end(); ++it) { 0775 if((*it).length() > 12) { 0776 (*it) = ISBNValidator::isbn10(*it); 0777 (*it).remove(QLatin1Char('-')); 0778 } 0779 } 0780 } 0781 // limit to first 10 0782 while(isbns.size() > 10) { 0783 isbns.pop_back(); 0784 } 0785 payload.insert(QLatin1String("Keywords"), isbns.join(QLatin1String("|"))); 0786 if(isbn13) { 0787 // params.insert(QStringLiteral("IdType"), QStringLiteral("EAN")); 0788 } 0789 } 0790 break; 0791 0792 case UPC: 0793 { 0794 QString cleanValue = request_.value(); 0795 cleanValue.remove(QLatin1Char('-')); 0796 // for EAN values, add 0 to beginning if not 13 characters 0797 // in order to assume US country code from UPC value 0798 QStringList values; 0799 foreach(const QString& splitValue, cleanValue.split(FieldFormat::delimiterString())) { 0800 QString tmpValue = splitValue; 0801 if(m_site != US && tmpValue.length() == 12) { 0802 tmpValue.prepend(QLatin1Char('0')); 0803 } 0804 values << tmpValue; 0805 // limit to first 10 values 0806 if(values.length() >= 10) { 0807 break; 0808 } 0809 } 0810 0811 payload.insert(QLatin1String("Keywords"), values.join(QLatin1String("|"))); 0812 } 0813 break; 0814 0815 case Keyword: 0816 payload.insert(QLatin1String("Keywords"), request_.value()); 0817 break; 0818 0819 case Raw: 0820 { 0821 QString key = request_.value().section(QLatin1Char('='), 0, 0).trimmed(); 0822 QString str = request_.value().section(QLatin1Char('='), 1).trimmed(); 0823 payload.insert(key, str); 0824 } 0825 break; 0826 0827 default: 0828 myWarning() << source() << "- key not recognized:" << request().key(); 0829 return QByteArray(); 0830 } 0831 0832 switch(m_imageSize) { 0833 case SmallImage: resources.append(QLatin1String("Images.Primary.Small")); break; 0834 case MediumImage: resources.append(QLatin1String("Images.Primary.Medium")); break; 0835 case LargeImage: resources.append(QLatin1String("Images.Primary.Large")); break; 0836 case NoImage: break; 0837 } 0838 0839 payload.insert(QLatin1String("Resources"), resources); 0840 return QJsonDocument(payload).toJson(QJsonDocument::Compact); 0841 } 0842 0843 Tellico::Data::CollPtr AmazonFetcher::createCollection() { 0844 Data::CollPtr coll = CollectionFactory::collection(collectionType(), true); 0845 if(!coll) { 0846 return coll; 0847 } 0848 0849 QString imageFieldName; 0850 switch(m_imageSize) { 0851 case SmallImage: imageFieldName = QStringLiteral("small-image"); break; 0852 case MediumImage: imageFieldName = QStringLiteral("medium-image"); break; 0853 case LargeImage: imageFieldName = QStringLiteral("large-image"); break; 0854 case NoImage: break; 0855 } 0856 0857 if(!imageFieldName.isEmpty()) { 0858 Data::FieldPtr field(new Data::Field(imageFieldName, QString(), Data::Field::URL)); 0859 coll->addField(field); 0860 } 0861 0862 if(optionalFields().contains(QStringLiteral("amazon"))) { 0863 Data::FieldPtr field(new Data::Field(QStringLiteral("amazon"), i18n("Amazon Link"), Data::Field::URL)); 0864 field->setCategory(i18n("General")); 0865 coll->addField(field); 0866 } 0867 0868 return coll; 0869 } 0870 0871 void AmazonFetcher::populateEntry(Data::EntryPtr entry_, const QJsonObject& info_) { 0872 QVariantMap itemMap = info_.value(QLatin1String("ItemInfo")).toObject().toVariantMap(); 0873 entry_->setField(QStringLiteral("title"), mapValue(itemMap, "Title", "DisplayValue")); 0874 const QString isbn = mapValue(itemMap, "ExternalIds", "ISBNs", "DisplayValues"); 0875 if(!isbn.isEmpty()) { 0876 // could be duplicate isbn10 and isbn13 values 0877 QStringList isbns = FieldFormat::splitValue(isbn, FieldFormat::StringSplit); 0878 for(QStringList::Iterator it = isbns.begin(); it != isbns.end(); ++it) { 0879 if((*it).length() > 12) { 0880 (*it) = ISBNValidator::isbn10(*it); 0881 (*it).remove(QLatin1Char('-')); 0882 } 0883 } 0884 isbns.removeDuplicates(); 0885 entry_->setField(QStringLiteral("isbn"), isbns.join(FieldFormat::delimiterString())); 0886 } 0887 0888 QStringList actors, artists, authors, illustrators, publishers; 0889 QVariantMap byLineMap = itemMap.value(QLatin1String("ByLineInfo")).toMap(); 0890 QVariantList contribArray = byLineMap.value(QLatin1String("Contributors")).toList(); 0891 foreach(const QVariant& v, contribArray) { 0892 const QVariantMap contribMap = v.toMap(); 0893 const QString role = contribMap.value(QLatin1String("Role")).toString(); 0894 const QString name = contribMap.value(QLatin1String("Name")).toString(); 0895 if(role == QLatin1String("Actor")) { 0896 actors += name; 0897 } else if(role == QLatin1String("Artist")) { 0898 artists += name; 0899 } else if(role == QLatin1String("Author")) { 0900 authors += name; 0901 } else if(role == QLatin1String("Illustrator")) { 0902 illustrators += name; 0903 } else if(role == QLatin1String("Publisher")) { 0904 publishers += name; 0905 } 0906 } 0907 // assume for books that the manufacturer is the publishers 0908 if(collectionType() == Data::Collection::Book || 0909 collectionType() == Data::Collection::Bibtex || 0910 collectionType() == Data::Collection::ComicBook) { 0911 const QString manufacturer = byLineMap.value(QLatin1String("Manufacturer")).toMap() 0912 .value(QLatin1String("DisplayValue")).toString(); 0913 publishers += manufacturer; 0914 } 0915 0916 actors.removeDuplicates(); 0917 artists.removeDuplicates(); 0918 authors.removeDuplicates(); 0919 illustrators.removeDuplicates(); 0920 publishers.removeDuplicates(); 0921 0922 if(!actors.isEmpty()) { 0923 entry_->setField(QStringLiteral("cast"), actors.join(FieldFormat::delimiterString())); 0924 } 0925 if(!artists.isEmpty()) { 0926 entry_->setField(QStringLiteral("artist"), artists.join(FieldFormat::delimiterString())); 0927 } 0928 if(!authors.isEmpty()) { 0929 entry_->setField(QStringLiteral("author"), authors.join(FieldFormat::delimiterString())); 0930 } 0931 if(!illustrators.isEmpty()) { 0932 entry_->setField(QStringLiteral("illustrator"), illustrators.join(FieldFormat::delimiterString())); 0933 } 0934 if(!publishers.isEmpty()) { 0935 entry_->setField(QStringLiteral("publisher"), publishers.join(FieldFormat::delimiterString())); 0936 } 0937 0938 QVariantMap contentMap = itemMap.value(QLatin1String("ContentInfo")).toMap(); 0939 entry_->setField(QStringLiteral("edition"), mapValue(contentMap, "Edition", "DisplayValue")); 0940 entry_->setField(QStringLiteral("pages"), mapValue(contentMap, "PagesCount", "DisplayValue")); 0941 const QString pubDate = mapValue(contentMap, "PublicationDate", "DisplayValue"); 0942 if(!pubDate.isEmpty()) { 0943 entry_->setField(QStringLiteral("pub_year"), pubDate.left(4)); 0944 } 0945 QVariantList langArray = itemMap.value(QLatin1String("ContentInfo")).toMap() 0946 .value(QStringLiteral("Languages")).toMap() 0947 .value(QStringLiteral("DisplayValues")).toList(); 0948 QStringList langs; 0949 foreach(const QVariant& v, langArray) { 0950 langs += mapValue(v.toMap(), "DisplayValue"); 0951 } 0952 langs.removeDuplicates(); 0953 langs.removeAll(QString()); 0954 entry_->setField(QStringLiteral("language"), langs.join(FieldFormat::delimiterString())); 0955 0956 if(collectionType() == Data::Collection::Book || 0957 collectionType() == Data::Collection::Bibtex || 0958 collectionType() == Data::Collection::ComicBook) { 0959 QVariantMap classificationsMap = itemMap.value(QLatin1String("Classifications")).toMap(); 0960 QString binding = mapValue(classificationsMap, "Binding", "DisplayValue"); 0961 if(binding.isEmpty()) { 0962 QVariantMap technicalMap = itemMap.value(QLatin1String("TechnicalInfo")).toMap(); 0963 binding = mapValue(technicalMap, "Formats", "DisplayValues"); 0964 } 0965 if(binding.contains(QStringLiteral("Paperback")) && binding != QStringLiteral("Trade Paperback")) { 0966 binding = i18n("Paperback"); 0967 } else if(binding.contains(QStringLiteral("Hard"))) { // could be Hardcover or Hardback 0968 binding = i18n("Hardback"); 0969 } 0970 entry_->setField(QStringLiteral("binding"), binding); 0971 } 0972 0973 QVariantMap imagesMap = info_.value(QLatin1String("Images")).toObject().toVariantMap(); 0974 switch(m_imageSize) { 0975 case SmallImage: 0976 entry_->setField(QStringLiteral("small-image"), mapValue(imagesMap, "Primary", "Small", "URL")); 0977 break; 0978 case MediumImage: 0979 entry_->setField(QStringLiteral("medium-image"), mapValue(imagesMap, "Primary", "Medium", "URL")); 0980 break; 0981 case LargeImage: 0982 entry_->setField(QStringLiteral("large-image"), mapValue(imagesMap, "Primary", "Large", "URL")); 0983 break; 0984 case NoImage: 0985 break; 0986 } 0987 0988 if(optionalFields().contains(QStringLiteral("amazon"))) { 0989 entry_->setField(QStringLiteral("amazon"), mapValue(info_.toVariantMap(), "DetailPageURL")); 0990 } 0991 } 0992 0993 void AmazonFetcher::parseTitle(Tellico::Data::EntryPtr entry_) { 0994 // assume that everything in brackets or parentheses is extra 0995 static const QRegularExpression rx(QLatin1String("[\\(\\[](.*?)[\\)\\]]")); 0996 QString title = entry_->field(QStringLiteral("title")); 0997 int pos = 0; 0998 QRegularExpressionMatch match = rx.match(title, pos); 0999 while(match.hasMatch()) { 1000 pos = match.capturedStart(); 1001 if(parseTitleToken(entry_, match.captured(1))) { 1002 title.remove(match.capturedStart(), match.capturedLength()); 1003 --pos; // search again there 1004 } 1005 match = rx.match(title, pos+1); 1006 } 1007 entry_->setField(QStringLiteral("title"), title.simplified()); 1008 } 1009 1010 bool AmazonFetcher::parseTitleToken(Tellico::Data::EntryPtr entry_, const QString& token_) { 1011 // myDebug() << "title token:" << token_; 1012 // if res = true, then the token gets removed from the title 1013 bool res = false; 1014 if(token_.indexOf(QLatin1String("widescreen"), 0, Qt::CaseInsensitive) > -1 || 1015 token_.indexOf(i18n("Widescreen"), 0, Qt::CaseInsensitive) > -1) { 1016 entry_->setField(QStringLiteral("widescreen"), QStringLiteral("true")); 1017 // res = true; leave it in the title 1018 } else if(token_.indexOf(QLatin1String("full screen"), 0, Qt::CaseInsensitive) > -1) { 1019 // skip, but go ahead and remove from title 1020 res = true; 1021 } else if(token_.indexOf(QLatin1String("import"), 0, Qt::CaseInsensitive) > -1) { 1022 // skip, but go ahead and remove from title 1023 res = true; 1024 } 1025 if(token_.indexOf(QLatin1String("blu-ray"), 0, Qt::CaseInsensitive) > -1) { 1026 entry_->setField(QStringLiteral("medium"), i18n("Blu-ray")); 1027 res = true; 1028 } else if(token_.indexOf(QLatin1String("hd dvd"), 0, Qt::CaseInsensitive) > -1) { 1029 entry_->setField(QStringLiteral("medium"), i18n("HD DVD")); 1030 res = true; 1031 } else if(token_.indexOf(QLatin1String("vhs"), 0, Qt::CaseInsensitive) > -1) { 1032 entry_->setField(QStringLiteral("medium"), i18n("VHS")); 1033 res = true; 1034 } 1035 if(token_.indexOf(QLatin1String("director's cut"), 0, Qt::CaseInsensitive) > -1 || 1036 token_.indexOf(i18n("Director's Cut"), 0, Qt::CaseInsensitive) > -1) { 1037 entry_->setField(QStringLiteral("directors-cut"), QStringLiteral("true")); 1038 // res = true; leave it in the title 1039 } 1040 const QString tokenLower = token_.toLower(); 1041 if(tokenLower == QLatin1String("ntsc")) { 1042 entry_->setField(QStringLiteral("format"), i18n("NTSC")); 1043 res = true; 1044 } 1045 if(tokenLower == QLatin1String("dvd")) { 1046 entry_->setField(QStringLiteral("medium"), i18n("DVD")); 1047 res = true; 1048 } 1049 if(token_.indexOf(QLatin1String("series"), 0, Qt::CaseInsensitive) > -1) { 1050 entry_->setField(QStringLiteral("series"), token_); 1051 res = true; 1052 } 1053 static const QRegularExpression regionRx(QLatin1String("Region [1-9]")); 1054 QRegularExpressionMatch match = regionRx.match(token_); 1055 if(match.hasMatch()) { 1056 entry_->setField(QStringLiteral("region"), i18n(match.captured().toUtf8().constData())); 1057 res = true; 1058 } 1059 if(entry_->collection()->type() == Data::Collection::Game) { 1060 Data::FieldPtr f = entry_->collection()->fieldByName(QStringLiteral("platform")); 1061 if(f && f->allowed().contains(token_)) { 1062 res = true; 1063 } 1064 } 1065 return res; 1066 } 1067 1068 //static 1069 QString AmazonFetcher::defaultName() { 1070 return i18n("Amazon.com Web Services"); 1071 } 1072 1073 QString AmazonFetcher::defaultIcon() { 1074 return favIcon("http://www.amazon.com"); 1075 } 1076 1077 Tellico::StringHash AmazonFetcher::allOptionalFields() { 1078 StringHash hash; 1079 hash[QStringLiteral("keyword")] = i18n("Keywords"); 1080 hash[QStringLiteral("amazon")] = i18n("Amazon Link"); 1081 return hash; 1082 } 1083 1084 Tellico::Fetch::ConfigWidget* AmazonFetcher::configWidget(QWidget* parent_) const { 1085 return new AmazonFetcher::ConfigWidget(parent_, this); 1086 } 1087 1088 AmazonFetcher::ConfigWidget::ConfigWidget(QWidget* parent_, const AmazonFetcher* fetcher_/*=0*/) 1089 : Fetch::ConfigWidget(parent_) { 1090 QGridLayout* l = new QGridLayout(optionsWidget()); 1091 l->setSpacing(4); 1092 l->setColumnStretch(1, 10); 1093 1094 int row = -1; 1095 1096 QLabel* al = new QLabel(i18n("Registration is required for accessing this data source. " 1097 "If you agree to the terms and conditions, <a href='%1'>sign " 1098 "up for an account</a>, and enter your information below.", 1099 QLatin1String("https://affiliate-program.amazon.com/gp/flex/advertising/api/sign-in.html")), 1100 optionsWidget()); 1101 al->setOpenExternalLinks(true); 1102 al->setWordWrap(true); 1103 ++row; 1104 l->addWidget(al, row, 0, 1, 2); 1105 // richtext gets weird with size 1106 al->setMinimumWidth(al->sizeHint().width()); 1107 1108 QLabel* label = new QLabel(i18n("Access key: "), optionsWidget()); 1109 l->addWidget(label, ++row, 0); 1110 m_accessEdit = new QLineEdit(optionsWidget()); 1111 connect(m_accessEdit, &QLineEdit::textChanged, this, &ConfigWidget::slotSetModified); 1112 l->addWidget(m_accessEdit, row, 1); 1113 QString w = i18n("Access to data from Amazon.com requires an AWS Access Key ID and a Secret Key."); 1114 label->setWhatsThis(w); 1115 m_accessEdit->setWhatsThis(w); 1116 label->setBuddy(m_accessEdit); 1117 1118 label = new QLabel(i18n("Secret key: "), optionsWidget()); 1119 l->addWidget(label, ++row, 0); 1120 m_secretKeyEdit = new QLineEdit(optionsWidget()); 1121 // m_secretKeyEdit->setEchoMode(QLineEdit::PasswordEchoOnEdit); 1122 connect(m_secretKeyEdit, &QLineEdit::textChanged, this, &ConfigWidget::slotSetModified); 1123 l->addWidget(m_secretKeyEdit, row, 1); 1124 label->setWhatsThis(w); 1125 m_secretKeyEdit->setWhatsThis(w); 1126 label->setBuddy(m_secretKeyEdit); 1127 1128 label = new QLabel(i18n("Country: "), optionsWidget()); 1129 l->addWidget(label, ++row, 0); 1130 m_siteCombo = new GUI::ComboBox(optionsWidget()); 1131 for(int i = 0; i < XX; ++i) { 1132 const AmazonFetcher::SiteData& siteData = AmazonFetcher::siteData(i); 1133 QIcon icon(QStandardPaths::locate(QStandardPaths::GenericDataLocation, 1134 QStringLiteral("kf5/locale/countries/%1/flag.png").arg(siteData.country))); 1135 m_siteCombo->addItem(icon, siteData.countryName, i); 1136 m_siteCombo->model()->sort(0); 1137 } 1138 1139 void (GUI::ComboBox::* activatedInt)(int) = &GUI::ComboBox::activated; 1140 connect(m_siteCombo, activatedInt, this, &ConfigWidget::slotSetModified); 1141 connect(m_siteCombo, activatedInt, this, &ConfigWidget::slotSiteChanged); 1142 l->addWidget(m_siteCombo, row, 1); 1143 w = i18n("Amazon.com provides data from several different localized sites. Choose the one " 1144 "you wish to use for this data source."); 1145 label->setWhatsThis(w); 1146 m_siteCombo->setWhatsThis(w); 1147 label->setBuddy(m_siteCombo); 1148 1149 label = new QLabel(i18n("&Image size: "), optionsWidget()); 1150 l->addWidget(label, ++row, 0); 1151 m_imageCombo = new GUI::ComboBox(optionsWidget()); 1152 m_imageCombo->addItem(i18n("Small Image"), SmallImage); 1153 m_imageCombo->addItem(i18n("Medium Image"), MediumImage); 1154 m_imageCombo->addItem(i18n("Large Image"), LargeImage); 1155 m_imageCombo->addItem(i18n("No Image"), NoImage); 1156 connect(m_imageCombo, activatedInt, this, &ConfigWidget::slotSetModified); 1157 l->addWidget(m_imageCombo, row, 1); 1158 w = i18n("The cover image may be downloaded as well. However, too many large images in the " 1159 "collection may degrade performance."); 1160 label->setWhatsThis(w); 1161 m_imageCombo->setWhatsThis(w); 1162 label->setBuddy(m_imageCombo); 1163 1164 label = new QLabel(i18n("&Associate's ID: "), optionsWidget()); 1165 l->addWidget(label, ++row, 0); 1166 m_assocEdit = new QLineEdit(optionsWidget()); 1167 void (QLineEdit::* textChanged)(const QString&) = &QLineEdit::textChanged; 1168 connect(m_assocEdit, textChanged, this, &ConfigWidget::slotSetModified); 1169 l->addWidget(m_assocEdit, row, 1); 1170 w = i18n("The associate's id identifies the person accessing the Amazon.com Web Services, and is included " 1171 "in any links to the Amazon.com site."); 1172 label->setWhatsThis(w); 1173 m_assocEdit->setWhatsThis(w); 1174 label->setBuddy(m_assocEdit); 1175 1176 l->setRowStretch(++row, 10); 1177 1178 if(fetcher_) { 1179 m_siteCombo->setCurrentData(fetcher_->m_site); 1180 m_accessEdit->setText(fetcher_->m_accessKey); 1181 m_secretKeyEdit->setText(fetcher_->m_secretKey); 1182 m_assocEdit->setText(fetcher_->m_assoc); 1183 m_imageCombo->setCurrentData(fetcher_->m_imageSize); 1184 } else { // defaults 1185 m_assocEdit->setText(QLatin1String(AMAZON_ASSOC_TOKEN)); 1186 m_imageCombo->setCurrentData(MediumImage); 1187 } 1188 1189 addFieldsWidget(AmazonFetcher::allOptionalFields(), fetcher_ ? fetcher_->optionalFields() : QStringList()); 1190 1191 KAcceleratorManager::manage(optionsWidget()); 1192 } 1193 1194 void AmazonFetcher::ConfigWidget::saveConfigHook(KConfigGroup& config_) { 1195 int n = m_siteCombo->currentData().toInt(); 1196 config_.writeEntry("Site", n); 1197 QString s = m_accessEdit->text().trimmed(); 1198 if(!s.isEmpty()) { 1199 config_.writeEntry("AccessKey", s); 1200 } 1201 s = m_secretKeyEdit->text().trimmed(); 1202 if(!s.isEmpty()) { 1203 config_.writeEntry("SecretKey", s); 1204 } 1205 s = m_assocEdit->text().trimmed(); 1206 if(!s.isEmpty()) { 1207 config_.writeEntry("AssocToken", s); 1208 } 1209 n = m_imageCombo->currentData().toInt(); 1210 config_.writeEntry("Image Size", n); 1211 } 1212 1213 QString AmazonFetcher::ConfigWidget::preferredName() const { 1214 return AmazonFetcher::siteData(m_siteCombo->currentData().toInt()).title; 1215 } 1216 1217 void AmazonFetcher::ConfigWidget::slotSiteChanged() { 1218 emit signalName(preferredName()); 1219 }