File indexing completed on 2024-05-12 16:45:42
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/isbnvalidator.h" 0038 #include "../gui/combobox.h" 0039 #include "../tellico_debug.h" 0040 0041 #include <KLocalizedString> 0042 #include <KIO/Job> 0043 #include <KIO/JobUiDelegate> 0044 #include <KSeparator> 0045 #include <KComboBox> 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 int count = -1; 0410 foreach(const QJsonValue& item, resultObject.value(QLatin1String("Items")).toArray()) { 0411 ++count; 0412 if(m_numResults >= m_limit) { 0413 break; 0414 } 0415 if(!m_started) { 0416 // might get aborted 0417 break; 0418 } 0419 Data::EntryPtr entry(new Data::Entry(coll)); 0420 populateEntry(entry, item.toObject()); 0421 0422 // special case book author 0423 // amazon is really bad about not putting spaces after periods 0424 if(coll->type() == Data::Collection::Book) { 0425 QRegExp rx(QLatin1String("\\.([^\\s])")); 0426 QStringList values = FieldFormat::splitValue(entry->field(QStringLiteral("author"))); 0427 for(QStringList::Iterator it = values.begin(); it != values.end(); ++it) { 0428 (*it).replace(rx, QStringLiteral(". \\1")); 0429 } 0430 entry->setField(QStringLiteral("author"), values.join(FieldFormat::delimiterString())); 0431 } 0432 0433 // UK puts the year in the title for some reason 0434 if(m_site == UK && coll->type() == Data::Collection::Video) { 0435 QRegExp rx(QLatin1String("\\[(\\d{4})\\]")); 0436 QString t = entry->title(); 0437 if(rx.indexIn(t) > -1) { 0438 QString y = rx.cap(1); 0439 t = t.remove(rx).simplified(); 0440 entry->setField(QStringLiteral("title"), t); 0441 if(entry->field(QStringLiteral("year")).isEmpty()) { 0442 entry->setField(QStringLiteral("year"), y); 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 QRegExp 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 if(blank.exactMatch(entry->field(fIt))) { 0604 entry->setField(fIt, QString()); 0605 } 0606 } 0607 0608 // don't want to show image urls in the fetch dialog 0609 // so clear them after reading the URL 0610 QString imageURL; 0611 switch(m_imageSize) { 0612 case SmallImage: 0613 imageURL = entry->field(QStringLiteral("small-image")); 0614 entry->setField(QStringLiteral("small-image"), QString()); 0615 break; 0616 case MediumImage: 0617 imageURL = entry->field(QStringLiteral("medium-image")); 0618 entry->setField(QStringLiteral("medium-image"), QString()); 0619 break; 0620 case LargeImage: 0621 imageURL = entry->field(QStringLiteral("large-image")); 0622 entry->setField(QStringLiteral("large-image"), QString()); 0623 break; 0624 case NoImage: 0625 default: 0626 break; 0627 } 0628 0629 if(!imageURL.isEmpty()) { 0630 // myDebug() << "grabbing " << imageURL; 0631 QString id = ImageFactory::addImage(QUrl::fromUserInput(imageURL), true); 0632 if(id.isEmpty()) { 0633 message(i18n("The cover image could not be loaded."), MessageHandler::Warning); 0634 } else { // amazon serves up 1x1 gifs occasionally, but that's caught in the image constructor 0635 // all relevant collection types have cover fields 0636 entry->setField(QStringLiteral("cover"), id); 0637 } 0638 } 0639 0640 return entry; 0641 } 0642 0643 Tellico::Fetch::FetchRequest AmazonFetcher::updateRequest(Data::EntryPtr entry_) { 0644 const int type = entry_->collection()->type(); 0645 const QString t = entry_->field(QStringLiteral("title")); 0646 if(type == Data::Collection::Book || type == Data::Collection::ComicBook || type == Data::Collection::Bibtex) { 0647 const QString isbn = entry_->field(QStringLiteral("isbn")); 0648 if(!isbn.isEmpty()) { 0649 return FetchRequest(Fetch::ISBN, isbn); 0650 } 0651 const QString a = entry_->field(QStringLiteral("author")); 0652 if(!a.isEmpty()) { 0653 return t.isEmpty() ? FetchRequest(Fetch::Person, a) 0654 : FetchRequest(Fetch::Keyword, t + QLatin1Char('-') + a); 0655 } 0656 } else if(type == Data::Collection::Album) { 0657 const QString a = entry_->field(QStringLiteral("artist")); 0658 if(!a.isEmpty()) { 0659 return t.isEmpty() ? FetchRequest(Fetch::Person, a) 0660 : FetchRequest(Fetch::Keyword, t + QLatin1Char('-') + a); 0661 } 0662 } 0663 0664 // optimistically try searching for title and rely on Collection::sameEntry() to figure things out 0665 if(!t.isEmpty()) { 0666 return FetchRequest(Fetch::Title, t); 0667 } 0668 0669 return FetchRequest(); 0670 } 0671 0672 QByteArray AmazonFetcher::requestPayload(Fetch::FetchRequest request_) { 0673 QJsonObject payload; 0674 payload.insert(QLatin1String("PartnerTag"), m_assoc); 0675 payload.insert(QLatin1String("PartnerType"), QLatin1String("Associates")); 0676 payload.insert(QLatin1String("Service"), QLatin1String("ProductAdvertisingAPIv1")); 0677 payload.insert(QLatin1String("Operation"), QLatin1String("SearchItems")); 0678 payload.insert(QLatin1String("SortBy"), QLatin1String("Relevance")); 0679 // not mandatory 0680 // payload.insert(QLatin1String("Marketplace"), QLatin1String(siteData(m_site).host)); 0681 if(m_page > 1) { 0682 payload.insert(QLatin1String("ItemPage"), m_page); 0683 } 0684 0685 QJsonArray resources; 0686 resources.append(QLatin1String("ItemInfo.Title")); 0687 resources.append(QLatin1String("ItemInfo.ContentInfo")); 0688 resources.append(QLatin1String("ItemInfo.ByLineInfo")); 0689 resources.append(QLatin1String("ItemInfo.TechnicalInfo")); 0690 0691 const int type = request_.collectionType(); 0692 switch(type) { 0693 case Data::Collection::Book: 0694 case Data::Collection::ComicBook: 0695 case Data::Collection::Bibtex: 0696 payload.insert(QLatin1String("SearchIndex"), QLatin1String("Books")); 0697 resources.append(QLatin1String("ItemInfo.ExternalIds")); 0698 resources.append(QLatin1String("ItemInfo.ManufactureInfo")); 0699 break; 0700 0701 case Data::Collection::Album: 0702 payload.insert(QLatin1String("SearchIndex"), QLatin1String("Music")); 0703 break; 0704 0705 case Data::Collection::Video: 0706 // CA and JP appear to have a bug where Video only returns VHS or Music results 0707 // DVD will return DVD, Blu-ray, etc. so just ignore VHS for those users 0708 payload.insert(QLatin1String("SearchIndex"), QLatin1String("MoviesAndTV")); 0709 if(m_site == CA || m_site == JP || m_site == IT || m_site == ES) { 0710 payload.insert(QStringLiteral("SearchIndex"), QStringLiteral("DVD")); 0711 } else { 0712 payload.insert(QStringLiteral("SearchIndex"), QStringLiteral("Video")); 0713 } 0714 // params.insert(QStringLiteral("SortIndex"), QStringLiteral("relevancerank")); 0715 resources.append(QLatin1String("ItemInfo.ContentRating")); 0716 break; 0717 0718 case Data::Collection::Game: 0719 payload.insert(QLatin1String("SearchIndex"), QLatin1String("VideoGames")); 0720 break; 0721 0722 case Data::Collection::BoardGame: 0723 payload.insert(QLatin1String("SearchIndex"), QLatin1String("ToysAndGames")); 0724 // params.insert(QStringLiteral("SortIndex"), QStringLiteral("relevancerank")); 0725 break; 0726 0727 case Data::Collection::Coin: 0728 case Data::Collection::Stamp: 0729 case Data::Collection::Wine: 0730 case Data::Collection::Base: 0731 case Data::Collection::Card: 0732 myDebug() << "can't fetch this type:" << collectionType(); 0733 return QByteArray(); 0734 } 0735 0736 switch(request_.key()) { 0737 case Title: 0738 payload.insert(QLatin1String("Title"), request_.value()); 0739 break; 0740 0741 case Person: 0742 if(type == Data::Collection::Video) { 0743 payload.insert(QStringLiteral("Actor"), request_.value()); 0744 // payload.insert(QStringLiteral("Director"), request_.value()); 0745 } else if(type == Data::Collection::Album) { 0746 payload.insert(QStringLiteral("Artist"), request_.value()); 0747 } else if(type == Data::Collection::Book) { 0748 payload.insert(QLatin1String("Author"), request_.value()); 0749 } else { 0750 payload.insert(QLatin1String("Keywords"), request_.value()); 0751 } 0752 break; 0753 0754 case ISBN: 0755 { 0756 QString cleanValue = request_.value(); 0757 cleanValue.remove(QLatin1Char('-')); 0758 // ISBN only get digits or 'X' 0759 QStringList isbns = FieldFormat::splitValue(cleanValue); 0760 // Amazon isbn13 search is still very flaky, so if possible, we're going to convert 0761 // all of them to isbn10. If we run into a 979 isbn13, then we're forced to do an 0762 // isbn13 search 0763 bool isbn13 = false; 0764 for(QStringList::Iterator it = isbns.begin(); it != isbns.end(); ) { 0765 if((*it).startsWith(QLatin1String("979"))) { 0766 isbn13 = true; 0767 break; 0768 } 0769 ++it; 0770 } 0771 // if we want isbn10, then convert all 0772 if(!isbn13) { 0773 for(QStringList::Iterator it = isbns.begin(); it != isbns.end(); ++it) { 0774 if((*it).length() > 12) { 0775 (*it) = ISBNValidator::isbn10(*it); 0776 (*it).remove(QLatin1Char('-')); 0777 } 0778 } 0779 } 0780 // limit to first 10 0781 while(isbns.size() > 10) { 0782 isbns.pop_back(); 0783 } 0784 payload.insert(QLatin1String("Keywords"), isbns.join(QLatin1String("|"))); 0785 if(isbn13) { 0786 // params.insert(QStringLiteral("IdType"), QStringLiteral("EAN")); 0787 } 0788 } 0789 break; 0790 0791 case UPC: 0792 { 0793 QString cleanValue = request_.value(); 0794 cleanValue.remove(QLatin1Char('-')); 0795 // for EAN values, add 0 to beginning if not 13 characters 0796 // in order to assume US country code from UPC value 0797 QStringList values; 0798 foreach(const QString& splitValue, cleanValue.split(FieldFormat::delimiterString())) { 0799 QString tmpValue = splitValue; 0800 if(m_site != US && tmpValue.length() == 12) { 0801 tmpValue.prepend(QLatin1Char('0')); 0802 } 0803 values << tmpValue; 0804 // limit to first 10 values 0805 if(values.length() >= 10) { 0806 break; 0807 } 0808 } 0809 0810 payload.insert(QLatin1String("Keywords"), values.join(QLatin1String("|"))); 0811 } 0812 break; 0813 0814 case Keyword: 0815 payload.insert(QLatin1String("Keywords"), request_.value()); 0816 break; 0817 0818 case Raw: 0819 { 0820 QString key = request_.value().section(QLatin1Char('='), 0, 0).trimmed(); 0821 QString str = request_.value().section(QLatin1Char('='), 1).trimmed(); 0822 payload.insert(key, str); 0823 } 0824 break; 0825 0826 default: 0827 myWarning() << "key not recognized: " << request().key(); 0828 return QByteArray(); 0829 } 0830 0831 switch(m_imageSize) { 0832 case SmallImage: resources.append(QLatin1String("Images.Primary.Small")); break; 0833 case MediumImage: resources.append(QLatin1String("Images.Primary.Medium")); break; 0834 case LargeImage: resources.append(QLatin1String("Images.Primary.Large")); break; 0835 case NoImage: break; 0836 } 0837 0838 payload.insert(QLatin1String("Resources"), resources); 0839 return QJsonDocument(payload).toJson(QJsonDocument::Compact); 0840 } 0841 0842 Tellico::Data::CollPtr AmazonFetcher::createCollection() { 0843 Data::CollPtr coll = CollectionFactory::collection(collectionType(), true); 0844 if(!coll) { 0845 return coll; 0846 } 0847 0848 QString imageFieldName; 0849 switch(m_imageSize) { 0850 case SmallImage: imageFieldName = QStringLiteral("small-image"); break; 0851 case MediumImage: imageFieldName = QStringLiteral("medium-image"); break; 0852 case LargeImage: imageFieldName = QStringLiteral("large-image"); break; 0853 case NoImage: break; 0854 } 0855 0856 if(!imageFieldName.isEmpty()) { 0857 Data::FieldPtr field(new Data::Field(imageFieldName, QString(), Data::Field::URL)); 0858 coll->addField(field); 0859 } 0860 0861 if(optionalFields().contains(QStringLiteral("amazon"))) { 0862 Data::FieldPtr field(new Data::Field(QStringLiteral("amazon"), i18n("Amazon Link"), Data::Field::URL)); 0863 field->setCategory(i18n("General")); 0864 coll->addField(field); 0865 } 0866 0867 return coll; 0868 } 0869 0870 void AmazonFetcher::populateEntry(Data::EntryPtr entry_, const QJsonObject& info_) { 0871 QVariantMap itemMap = info_.value(QLatin1String("ItemInfo")).toObject().toVariantMap(); 0872 entry_->setField(QStringLiteral("title"), mapValue(itemMap, "Title", "DisplayValue")); 0873 const QString isbn = mapValue(itemMap, "ExternalIds", "ISBNs", "DisplayValues"); 0874 if(!isbn.isEmpty()) { 0875 // could be duplicate isbn10 and isbn13 values 0876 QStringList isbns = FieldFormat::splitValue(isbn, FieldFormat::StringSplit); 0877 for(QStringList::Iterator it = isbns.begin(); it != isbns.end(); ++it) { 0878 if((*it).length() > 12) { 0879 (*it) = ISBNValidator::isbn10(*it); 0880 (*it).remove(QLatin1Char('-')); 0881 } 0882 } 0883 isbns.removeDuplicates(); 0884 entry_->setField(QStringLiteral("isbn"), isbns.join(FieldFormat::delimiterString())); 0885 } 0886 0887 QStringList actors, artists, authors, illustrators, publishers; 0888 QVariantMap byLineMap = itemMap.value(QLatin1String("ByLineInfo")).toMap(); 0889 QVariantList contribArray = byLineMap.value(QLatin1String("Contributors")).toList(); 0890 foreach(const QVariant& v, contribArray) { 0891 const QVariantMap contribMap = v.toMap(); 0892 const QString role = contribMap.value(QLatin1String("Role")).toString(); 0893 const QString name = contribMap.value(QLatin1String("Name")).toString(); 0894 if(role == QLatin1String("Actor")) { 0895 actors += name; 0896 } else if(role == QLatin1String("Artist")) { 0897 artists += name; 0898 } else if(role == QLatin1String("Author")) { 0899 authors += name; 0900 } else if(role == QLatin1String("Illustrator")) { 0901 illustrators += name; 0902 } else if(role == QLatin1String("Publisher")) { 0903 publishers += name; 0904 } 0905 } 0906 // assume for books that the manufacturer is the publishers 0907 if(collectionType() == Data::Collection::Book || 0908 collectionType() == Data::Collection::Bibtex || 0909 collectionType() == Data::Collection::ComicBook) { 0910 const QString manufacturer = byLineMap.value(QLatin1String("Manufacturer")).toMap() 0911 .value(QLatin1String("DisplayValue")).toString(); 0912 publishers += manufacturer; 0913 } 0914 0915 actors.removeDuplicates(); 0916 artists.removeDuplicates(); 0917 authors.removeDuplicates(); 0918 illustrators.removeDuplicates(); 0919 publishers.removeDuplicates(); 0920 0921 if(!actors.isEmpty()) { 0922 entry_->setField(QStringLiteral("cast"), actors.join(FieldFormat::delimiterString())); 0923 } 0924 if(!artists.isEmpty()) { 0925 entry_->setField(QStringLiteral("artist"), artists.join(FieldFormat::delimiterString())); 0926 } 0927 if(!authors.isEmpty()) { 0928 entry_->setField(QStringLiteral("author"), authors.join(FieldFormat::delimiterString())); 0929 } 0930 if(!illustrators.isEmpty()) { 0931 entry_->setField(QStringLiteral("illustrator"), illustrators.join(FieldFormat::delimiterString())); 0932 } 0933 if(!publishers.isEmpty()) { 0934 entry_->setField(QStringLiteral("publisher"), publishers.join(FieldFormat::delimiterString())); 0935 } 0936 0937 QVariantMap contentMap = itemMap.value(QLatin1String("ContentInfo")).toMap(); 0938 entry_->setField(QStringLiteral("edition"), mapValue(contentMap, "Edition", "DisplayValue")); 0939 entry_->setField(QStringLiteral("pages"), mapValue(contentMap, "PagesCount", "DisplayValue")); 0940 const QString pubDate = mapValue(contentMap, "PublicationDate", "DisplayValue"); 0941 if(!pubDate.isEmpty()) { 0942 entry_->setField(QStringLiteral("pub_year"), pubDate.left(4)); 0943 } 0944 QVariantList langArray = itemMap.value(QLatin1String("ContentInfo")).toMap() 0945 .value(QStringLiteral("Languages")).toMap() 0946 .value(QStringLiteral("DisplayValues")).toList(); 0947 QStringList langs; 0948 foreach(const QVariant& v, langArray) { 0949 langs += mapValue(v.toMap(), "DisplayValue"); 0950 } 0951 langs.removeDuplicates(); 0952 langs.removeAll(QString()); 0953 entry_->setField(QStringLiteral("language"), langs.join(FieldFormat::delimiterString())); 0954 0955 if(collectionType() == Data::Collection::Book || 0956 collectionType() == Data::Collection::Bibtex || 0957 collectionType() == Data::Collection::ComicBook) { 0958 QVariantMap classificationsMap = itemMap.value(QLatin1String("Classifications")).toMap(); 0959 QVariantMap technicalMap = itemMap.value(QLatin1String("TechnicalInfo")).toMap(); 0960 QString binding = mapValue(classificationsMap, "Binding", "DisplayValue"); 0961 if(binding.isEmpty()) { 0962 binding = mapValue(technicalMap, "Formats", "DisplayValues"); 0963 } 0964 if(binding.contains(QStringLiteral("Paperback")) && binding != QStringLiteral("Trade Paperback")) { 0965 binding = i18n("Paperback"); 0966 } else if(binding.contains(QStringLiteral("Hard"))) { // could be Hardcover or Hardback 0967 binding = i18n("Hardback"); 0968 } 0969 entry_->setField(QStringLiteral("binding"), binding); 0970 } 0971 0972 QVariantMap imagesMap = info_.value(QLatin1String("Images")).toObject().toVariantMap(); 0973 switch(m_imageSize) { 0974 case SmallImage: 0975 entry_->setField(QStringLiteral("small-image"), mapValue(imagesMap, "Primary", "Small", "URL")); 0976 break; 0977 case MediumImage: 0978 entry_->setField(QStringLiteral("medium-image"), mapValue(imagesMap, "Primary", "Medium", "URL")); 0979 break; 0980 case LargeImage: 0981 entry_->setField(QStringLiteral("large-image"), mapValue(imagesMap, "Primary", "Large", "URL")); 0982 break; 0983 case NoImage: 0984 break; 0985 } 0986 0987 if(optionalFields().contains(QStringLiteral("amazon"))) { 0988 entry_->setField(QStringLiteral("amazon"), mapValue(info_.toVariantMap(), "DetailPageURL")); 0989 } 0990 } 0991 0992 void AmazonFetcher::parseTitle(Tellico::Data::EntryPtr entry_) { 0993 // assume that everything in brackets or parentheses is extra 0994 static const QRegularExpression rx(QLatin1String("[\\(\\[](.*?)[\\)\\]]")); 0995 QString title = entry_->field(QStringLiteral("title")); 0996 int pos = 0; 0997 QRegularExpressionMatch match = rx.match(title, pos); 0998 while(match.hasMatch()) { 0999 pos = match.capturedStart(); 1000 if(parseTitleToken(entry_, match.captured(1))) { 1001 title.remove(match.capturedStart(), match.capturedLength()); 1002 --pos; // search again there 1003 } 1004 match = rx.match(title, pos+1); 1005 } 1006 entry_->setField(QStringLiteral("title"), title.simplified()); 1007 } 1008 1009 bool AmazonFetcher::parseTitleToken(Tellico::Data::EntryPtr entry_, const QString& token_) { 1010 // myDebug() << "title token:" << token_; 1011 // if res = true, then the token gets removed from the title 1012 bool res = false; 1013 if(token_.indexOf(QLatin1String("widescreen"), 0, Qt::CaseInsensitive) > -1 || 1014 token_.indexOf(i18n("Widescreen"), 0, Qt::CaseInsensitive) > -1) { 1015 entry_->setField(QStringLiteral("widescreen"), QStringLiteral("true")); 1016 // res = true; leave it in the title 1017 } else if(token_.indexOf(QLatin1String("full screen"), 0, Qt::CaseInsensitive) > -1) { 1018 // skip, but go ahead and remove from title 1019 res = true; 1020 } else if(token_.indexOf(QLatin1String("import"), 0, Qt::CaseInsensitive) > -1) { 1021 // skip, but go ahead and remove from title 1022 res = true; 1023 } 1024 if(token_.indexOf(QLatin1String("blu-ray"), 0, Qt::CaseInsensitive) > -1) { 1025 entry_->setField(QStringLiteral("medium"), i18n("Blu-ray")); 1026 res = true; 1027 } else if(token_.indexOf(QLatin1String("hd dvd"), 0, Qt::CaseInsensitive) > -1) { 1028 entry_->setField(QStringLiteral("medium"), i18n("HD DVD")); 1029 res = true; 1030 } else if(token_.indexOf(QLatin1String("vhs"), 0, Qt::CaseInsensitive) > -1) { 1031 entry_->setField(QStringLiteral("medium"), i18n("VHS")); 1032 res = true; 1033 } 1034 if(token_.indexOf(QLatin1String("director's cut"), 0, Qt::CaseInsensitive) > -1 || 1035 token_.indexOf(i18n("Director's Cut"), 0, Qt::CaseInsensitive) > -1) { 1036 entry_->setField(QStringLiteral("directors-cut"), QStringLiteral("true")); 1037 // res = true; leave it in the title 1038 } 1039 const QString tokenLower = token_.toLower(); 1040 if(tokenLower == QLatin1String("ntsc")) { 1041 entry_->setField(QStringLiteral("format"), i18n("NTSC")); 1042 res = true; 1043 } 1044 if(tokenLower == QLatin1String("dvd")) { 1045 entry_->setField(QStringLiteral("medium"), i18n("DVD")); 1046 res = true; 1047 } 1048 if(token_.indexOf(QLatin1String("series"), 0, Qt::CaseInsensitive) > -1) { 1049 entry_->setField(QStringLiteral("series"), token_); 1050 res = true; 1051 } 1052 static const QRegularExpression regionRx(QLatin1String("Region [1-9]")); 1053 QRegularExpressionMatch match = regionRx.match(token_); 1054 if(match.hasMatch()) { 1055 entry_->setField(QStringLiteral("region"), i18n(match.captured().toUtf8().constData())); 1056 res = true; 1057 } 1058 if(entry_->collection()->type() == Data::Collection::Game) { 1059 Data::FieldPtr f = entry_->collection()->fieldByName(QStringLiteral("platform")); 1060 if(f && f->allowed().contains(token_)) { 1061 res = true; 1062 } 1063 } 1064 return res; 1065 } 1066 1067 //static 1068 QString AmazonFetcher::defaultName() { 1069 return i18n("Amazon.com Web Services"); 1070 } 1071 1072 QString AmazonFetcher::defaultIcon() { 1073 return favIcon("http://www.amazon.com"); 1074 } 1075 1076 Tellico::StringHash AmazonFetcher::allOptionalFields() { 1077 StringHash hash; 1078 hash[QStringLiteral("keyword")] = i18n("Keywords"); 1079 hash[QStringLiteral("amazon")] = i18n("Amazon Link"); 1080 return hash; 1081 } 1082 1083 Tellico::Fetch::ConfigWidget* AmazonFetcher::configWidget(QWidget* parent_) const { 1084 return new AmazonFetcher::ConfigWidget(parent_, this); 1085 } 1086 1087 AmazonFetcher::ConfigWidget::ConfigWidget(QWidget* parent_, const AmazonFetcher* fetcher_/*=0*/) 1088 : Fetch::ConfigWidget(parent_) { 1089 QGridLayout* l = new QGridLayout(optionsWidget()); 1090 l->setSpacing(4); 1091 l->setColumnStretch(1, 10); 1092 1093 int row = -1; 1094 1095 QLabel* al = new QLabel(i18n("Registration is required for accessing this data source. " 1096 "If you agree to the terms and conditions, <a href='%1'>sign " 1097 "up for an account</a>, and enter your information below.", 1098 QLatin1String("https://affiliate-program.amazon.com/gp/flex/advertising/api/sign-in.html")), 1099 optionsWidget()); 1100 al->setOpenExternalLinks(true); 1101 al->setWordWrap(true); 1102 ++row; 1103 l->addWidget(al, row, 0, 1, 2); 1104 // richtext gets weird with size 1105 al->setMinimumWidth(al->sizeHint().width()); 1106 1107 QLabel* label = new QLabel(i18n("Access key: "), optionsWidget()); 1108 l->addWidget(label, ++row, 0); 1109 m_accessEdit = new QLineEdit(optionsWidget()); 1110 connect(m_accessEdit, &QLineEdit::textChanged, this, &ConfigWidget::slotSetModified); 1111 l->addWidget(m_accessEdit, row, 1); 1112 QString w = i18n("Access to data from Amazon.com requires an AWS Access Key ID and a Secret Key."); 1113 label->setWhatsThis(w); 1114 m_accessEdit->setWhatsThis(w); 1115 label->setBuddy(m_accessEdit); 1116 1117 label = new QLabel(i18n("Secret key: "), optionsWidget()); 1118 l->addWidget(label, ++row, 0); 1119 m_secretKeyEdit = new QLineEdit(optionsWidget()); 1120 // m_secretKeyEdit->setEchoMode(QLineEdit::PasswordEchoOnEdit); 1121 connect(m_secretKeyEdit, &QLineEdit::textChanged, this, &ConfigWidget::slotSetModified); 1122 l->addWidget(m_secretKeyEdit, row, 1); 1123 label->setWhatsThis(w); 1124 m_secretKeyEdit->setWhatsThis(w); 1125 label->setBuddy(m_secretKeyEdit); 1126 1127 label = new QLabel(i18n("Country: "), optionsWidget()); 1128 l->addWidget(label, ++row, 0); 1129 m_siteCombo = new GUI::ComboBox(optionsWidget()); 1130 for(int i = 0; i < XX; ++i) { 1131 const AmazonFetcher::SiteData& siteData = AmazonFetcher::siteData(i); 1132 QIcon icon(QStandardPaths::locate(QStandardPaths::GenericDataLocation, 1133 QStringLiteral("kf5/locale/countries/%1/flag.png").arg(siteData.country))); 1134 m_siteCombo->addItem(icon, siteData.countryName, i); 1135 m_siteCombo->model()->sort(0); 1136 } 1137 1138 void (GUI::ComboBox::* activatedInt)(int) = &GUI::ComboBox::activated; 1139 connect(m_siteCombo, activatedInt, this, &ConfigWidget::slotSetModified); 1140 connect(m_siteCombo, activatedInt, this, &ConfigWidget::slotSiteChanged); 1141 l->addWidget(m_siteCombo, row, 1); 1142 w = i18n("Amazon.com provides data from several different localized sites. Choose the one " 1143 "you wish to use for this data source."); 1144 label->setWhatsThis(w); 1145 m_siteCombo->setWhatsThis(w); 1146 label->setBuddy(m_siteCombo); 1147 1148 label = new QLabel(i18n("&Image size: "), optionsWidget()); 1149 l->addWidget(label, ++row, 0); 1150 m_imageCombo = new GUI::ComboBox(optionsWidget()); 1151 m_imageCombo->addItem(i18n("Small Image"), SmallImage); 1152 m_imageCombo->addItem(i18n("Medium Image"), MediumImage); 1153 m_imageCombo->addItem(i18n("Large Image"), LargeImage); 1154 m_imageCombo->addItem(i18n("No Image"), NoImage); 1155 connect(m_imageCombo, activatedInt, this, &ConfigWidget::slotSetModified); 1156 l->addWidget(m_imageCombo, row, 1); 1157 w = i18n("The cover image may be downloaded as well. However, too many large images in the " 1158 "collection may degrade performance."); 1159 label->setWhatsThis(w); 1160 m_imageCombo->setWhatsThis(w); 1161 label->setBuddy(m_imageCombo); 1162 1163 label = new QLabel(i18n("&Associate's ID: "), optionsWidget()); 1164 l->addWidget(label, ++row, 0); 1165 m_assocEdit = new QLineEdit(optionsWidget()); 1166 void (QLineEdit::* textChanged)(const QString&) = &QLineEdit::textChanged; 1167 connect(m_assocEdit, textChanged, this, &ConfigWidget::slotSetModified); 1168 l->addWidget(m_assocEdit, row, 1); 1169 w = i18n("The associate's id identifies the person accessing the Amazon.com Web Services, and is included " 1170 "in any links to the Amazon.com site."); 1171 label->setWhatsThis(w); 1172 m_assocEdit->setWhatsThis(w); 1173 label->setBuddy(m_assocEdit); 1174 1175 l->setRowStretch(++row, 10); 1176 1177 if(fetcher_) { 1178 m_siteCombo->setCurrentData(fetcher_->m_site); 1179 m_accessEdit->setText(fetcher_->m_accessKey); 1180 m_secretKeyEdit->setText(fetcher_->m_secretKey); 1181 m_assocEdit->setText(fetcher_->m_assoc); 1182 m_imageCombo->setCurrentData(fetcher_->m_imageSize); 1183 } else { // defaults 1184 m_assocEdit->setText(QLatin1String(AMAZON_ASSOC_TOKEN)); 1185 m_imageCombo->setCurrentData(MediumImage); 1186 } 1187 1188 addFieldsWidget(AmazonFetcher::allOptionalFields(), fetcher_ ? fetcher_->optionalFields() : QStringList()); 1189 1190 KAcceleratorManager::manage(optionsWidget()); 1191 } 1192 1193 void AmazonFetcher::ConfigWidget::saveConfigHook(KConfigGroup& config_) { 1194 int n = m_siteCombo->currentData().toInt(); 1195 config_.writeEntry("Site", n); 1196 QString s = m_accessEdit->text().trimmed(); 1197 if(!s.isEmpty()) { 1198 config_.writeEntry("AccessKey", s); 1199 } 1200 s = m_secretKeyEdit->text().trimmed(); 1201 if(!s.isEmpty()) { 1202 config_.writeEntry("SecretKey", s); 1203 } 1204 s = m_assocEdit->text().trimmed(); 1205 if(!s.isEmpty()) { 1206 config_.writeEntry("AssocToken", s); 1207 } 1208 n = m_imageCombo->currentData().toInt(); 1209 config_.writeEntry("Image Size", n); 1210 } 1211 1212 QString AmazonFetcher::ConfigWidget::preferredName() const { 1213 return AmazonFetcher::siteData(m_siteCombo->currentData().toInt()).title; 1214 } 1215 1216 void AmazonFetcher::ConfigWidget::slotSiteChanged() { 1217 emit signalName(preferredName()); 1218 }