1 /***************************************************************************
2 Copyright (C) 2004-2020 Robby Stephenson <robby@periapsis.org>
3 ***************************************************************************/
4
5 /***************************************************************************
6 * *
7 * This program is free software; you can redistribute it and/or *
8 * modify it under the terms of the GNU General Public License as *
9 * published by the Free Software Foundation; either version 2 of *
10 * the License or (at your option) version 3 or any later version *
11 * accepted by the membership of KDE e.V. (or its successor approved *
12 * by the membership of KDE e.V.), which shall act as a proxy *
13 * defined in Section 14 of version 3 of the license. *
14 * *
15 * This program is distributed in the hope that it will be useful, *
16 * but WITHOUT ANY WARRANTY; without even the implied warranty of *
17 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
18 * GNU General Public License for more details. *
19 * *
20 * You should have received a copy of the GNU General Public License *
21 * along with this program. If not, see <http://www.gnu.org/licenses/>. *
22 * *
23 ***************************************************************************/
24
25 #include <config.h>
26
27 #include "amazonfetcher.h"
28 #include "amazonrequest.h"
29 #include "../collectionfactory.h"
30 #include "../images/imagefactory.h"
31 #include "../utils/guiproxy.h"
32 #include "../collection.h"
33 #include "../entry.h"
34 #include "../field.h"
35 #include "../fieldformat.h"
36 #include "../utils/string_utils.h"
37 #include "../utils/isbnvalidator.h"
38 #include "../gui/combobox.h"
39 #include "../tellico_debug.h"
40
41 #include <KLocalizedString>
42 #include <KIO/Job>
43 #include <KIO/JobUiDelegate>
44 #include <KSeparator>
45 #include <KComboBox>
46 #include <KAcceleratorManager>
47 #include <KConfigGroup>
48 #include <KJobWidgets/KJobWidgets>
49
50 #include <QLineEdit>
51 #include <QLabel>
52 #include <QCheckBox>
53 #include <QFile>
54 #include <QDir>
55 #include <QTextStream>
56 #include <QTextCodec>
57 #include <QGridLayout>
58 #include <QStandardPaths>
59 #include <QJsonDocument>
60 #include <QJsonObject>
61 #include <QJsonArray>
62 #include <QTemporaryFile>
63
64 namespace {
65 static const int AMAZON_RETURNS_PER_REQUEST = 10;
66 static const int AMAZON_MAX_RETURNS_TOTAL = 20;
67 static const char* AMAZON_ASSOC_TOKEN = "tellico-20";
68 }
69
70 using namespace Tellico;
71 using Tellico::Fetch::AmazonFetcher;
72
73 // static
74 // see https://webservices.amazon.com/paapi5/documentation/common-request-parameters.html#host-and-region
siteData(int site_)75 const AmazonFetcher::SiteData& AmazonFetcher::siteData(int site_) {
76 Q_ASSERT(site_ >= 0);
77 Q_ASSERT(site_ < XX);
78 static SiteData dataVector[16] = {
79 {
80 i18n("Amazon (US)"),
81 "webservices.amazon.com",
82 "us-east-1",
83 QLatin1String("us"),
84 i18n("United States")
85 }, {
86 i18n("Amazon (UK)"),
87 "webservices.amazon.co.uk",
88 "eu-west-1",
89 QLatin1String("gb"),
90 i18n("United Kingdom")
91 }, {
92 i18n("Amazon (Germany)"),
93 "webservices.amazon.de",
94 "eu-west-1",
95 QLatin1String("de"),
96 i18n("Germany")
97 }, {
98 i18n("Amazon (Japan)"),
99 "webservices.amazon.co.jp",
100 "us-west-2",
101 QLatin1String("jp"),
102 i18n("Japan")
103 }, {
104 i18n("Amazon (France)"),
105 "webservices.amazon.fr",
106 "eu-west-1",
107 QLatin1String("fr"),
108 i18n("France")
109 }, {
110 i18n("Amazon (Canada)"),
111 "webservices.amazon.ca",
112 "us-east-1",
113 QLatin1String("ca"),
114 i18n("Canada")
115 }, {
116 // TODO: no chinese in PAAPI-5 yet?
117 i18n("Amazon (China)"),
118 "webservices.amazon.cn",
119 "us-west-2",
120 QLatin1String("ch"),
121 i18n("China")
122 }, {
123 i18n("Amazon (Spain)"),
124 "webservices.amazon.es",
125 "eu-west-1",
126 QLatin1String("es"),
127 i18n("Spain")
128 }, {
129 i18n("Amazon (Italy)"),
130 "webservices.amazon.it",
131 "eu-west-1",
132 QLatin1String("it"),
133 i18n("Italy")
134 }, {
135 i18n("Amazon (Brazil)"),
136 "webservices.amazon.com.br",
137 "us-east-1",
138 QLatin1String("br"),
139 i18n("Brazil")
140 }, {
141 i18n("Amazon (Australia)"),
142 "webservices.amazon.com.au",
143 "us-west-2",
144 QLatin1String("au"),
145 i18n("Australia")
146 }, {
147 i18n("Amazon (India)"),
148 "webservices.amazon.in",
149 "eu-west-1",
150 QLatin1String("in"),
151 i18n("India")
152 }, {
153 i18n("Amazon (Mexico)"),
154 "webservices.amazon.com.mx",
155 "us-east-1",
156 QLatin1String("mx"),
157 i18n("Mexico")
158 }, {
159 i18n("Amazon (Turkey)"),
160 "webservices.amazon.com.tr",
161 "eu-west-1",
162 QLatin1String("tr"),
163 i18n("Turkey")
164 }, {
165 i18n("Amazon (Singapore)"),
166 "webservices.amazon.sg",
167 "us-west-2",
168 QLatin1String("sg"),
169 i18n("Singapore")
170 }, {
171 i18n("Amazon (UAE)"),
172 "webservices.amazon.ae",
173 "eu-west-1",
174 QLatin1String("ae"),
175 i18n("United Arab Emirates")
176 }
177 };
178
179 return dataVector[qBound(0, site_, static_cast<int>(sizeof(dataVector)/sizeof(SiteData)))];
180 }
181
AmazonFetcher(QObject * parent_)182 AmazonFetcher::AmazonFetcher(QObject* parent_)
183 : Fetcher(parent_), m_site(Unknown), m_imageSize(MediumImage),
184 m_assoc(QLatin1String(AMAZON_ASSOC_TOKEN)), m_limit(AMAZON_MAX_RETURNS_TOTAL),
185 m_countOffset(0), m_page(1), m_total(-1), m_numResults(0), m_job(nullptr), m_started(false) {
186 // to facilitate transition to Amazon PAAPI5, allow users to enable logging the Amazon
187 // results so they can be shared for debugging
188 const QByteArray enableLog = qgetenv("TELLICO_ENABLE_AMAZON_LOG").trimmed().toLower();
189 m_enableLog = (enableLog == "true" || enableLog == "1");
190 }
191
~AmazonFetcher()192 AmazonFetcher::~AmazonFetcher() {
193 }
194
source() const195 QString AmazonFetcher::source() const {
196 return m_name.isEmpty() ? defaultName() : m_name;
197 }
198
attribution() const199 QString AmazonFetcher::attribution() const {
200 return i18n("This data is licensed under <a href=""%1"">specific terms</a>.",
201 QLatin1String("https://affiliate-program.amazon.com/gp/advertising/api/detail/agreement.html"));
202 }
203
canFetch(int type) const204 bool AmazonFetcher::canFetch(int type) const {
205 return type == Data::Collection::Book
206 || type == Data::Collection::ComicBook
207 || type == Data::Collection::Bibtex
208 || type == Data::Collection::Album
209 || type == Data::Collection::Video
210 || type == Data::Collection::Game
211 || type == Data::Collection::BoardGame;
212 }
213
canSearch(Fetch::FetchKey k) const214 bool AmazonFetcher::canSearch(Fetch::FetchKey k) const {
215 // no UPC in Canada
216 return k == Title
217 || k == Person
218 || k == ISBN
219 || k == UPC
220 || k == Keyword;
221 }
222
readConfigHook(const KConfigGroup & config_)223 void AmazonFetcher::readConfigHook(const KConfigGroup& config_) {
224 const int site = config_.readEntry("Site", int(Unknown));
225 Q_ASSERT(site != Unknown);
226 m_site = static_cast<Site>(site);
227 if(m_name.isEmpty()) {
228 m_name = siteData(m_site).title;
229 }
230 QString s = config_.readEntry("AccessKey");
231 if(!s.isEmpty()) {
232 m_accessKey = s;
233 } else {
234 myWarning() << "No Amazon access key";
235 }
236 s = config_.readEntry("AssocToken");
237 if(!s.isEmpty()) {
238 m_assoc = s;
239 }
240 s = config_.readEntry("SecretKey");
241 if(!s.isEmpty()) {
242 m_secretKey = s;
243 } else {
244 myWarning() << "No Amazon secret key";
245 }
246 int imageSize = config_.readEntry("Image Size", -1);
247 if(imageSize > -1) {
248 m_imageSize = static_cast<ImageSize>(imageSize);
249 }
250 }
251
search()252 void AmazonFetcher::search() {
253 m_started = true;
254 m_page = 1;
255 m_total = -1;
256 m_countOffset = 0;
257 m_numResults = 0;
258 doSearch();
259 }
260
continueSearch()261 void AmazonFetcher::continueSearch() {
262 m_started = true;
263 m_limit += AMAZON_MAX_RETURNS_TOTAL;
264 doSearch();
265 }
266
doSearch()267 void AmazonFetcher::doSearch() {
268 if(m_secretKey.isEmpty() || m_accessKey.isEmpty()) {
269 // this message is split in two since the first half is reused later
270 message(i18n("Access to data from Amazon.com requires an AWS Access Key ID and a Secret Key.") +
271 QLatin1Char(' ') +
272 i18n("Those values must be entered in the data source settings."), MessageHandler::Error);
273 stop();
274 return;
275 }
276
277 const QByteArray payload = requestPayload(request());
278 if(payload.isEmpty()) {
279 stop();
280 return;
281 }
282
283 QString path(QStringLiteral("/paapi5/searchitems"));
284
285 AmazonRequest request(m_accessKey, m_secretKey);
286 request.setHost(siteData(m_site).host);
287 request.setRegion(siteData(m_site).region);
288 request.setPath(path.toUtf8());
289
290 // debugging check
291 if(m_testResultsFile.isEmpty()) {
292 QUrl u;
293 u.setScheme(QLatin1String("https"));
294 u.setHost(QString::fromUtf8(siteData(m_site).host));
295 u.setPath(path);
296 m_job = KIO::storedHttpPost(payload, u, KIO::HideProgressInfo);
297 QStringList customHeaders;
298 QMapIterator<QByteArray, QByteArray> i(request.headers(payload));
299 while(i.hasNext()) {
300 i.next();
301 customHeaders += QString::fromUtf8(i.key() + ": " + i.value());
302 }
303 m_job->addMetaData(QStringLiteral("customHTTPHeader"), customHeaders.join(QLatin1String("\r\n")));
304 } else {
305 myDebug() << "Reading" << m_testResultsFile;
306 m_job = KIO::storedGet(QUrl::fromLocalFile(m_testResultsFile), KIO::NoReload, KIO::HideProgressInfo);
307 }
308 KJobWidgets::setWindow(m_job, GUI::Proxy::widget());
309 connect(m_job.data(), &KJob::result,
310 this, &AmazonFetcher::slotComplete);
311 }
312
stop()313 void AmazonFetcher::stop() {
314 if(!m_started) {
315 return;
316 }
317 if(m_job) {
318 m_job->kill();
319 m_job = nullptr;
320 }
321 m_started = false;
322 emit signalDone(this);
323 }
324
slotComplete(KJob *)325 void AmazonFetcher::slotComplete(KJob*) {
326 if(m_job->error()) {
327 myDebug() << m_job->errorString() << m_job->data();
328 myDebug() << "Response code is" << m_job->metaData().value(QStringLiteral("responsecode"));
329 m_job->uiDelegate()->showErrorMessage();
330 stop();
331 return;
332 }
333
334 const QByteArray data = m_job->data();
335 if(data.isEmpty()) {
336 myDebug() << "no data";
337 stop();
338 return;
339 }
340
341 // since the fetch is done, don't worry about holding the job pointer
342 m_job = nullptr;
343
344 if(m_enableLog) {
345 QTemporaryFile logFile(QDir::tempPath() + QStringLiteral("/amazon-search-items-XXXXXX.json"));
346 logFile.setAutoRemove(false);
347 if(logFile.open()) {
348 QTextStream t(&logFile);
349 t.setCodec("UTF-8");
350 t << data;
351 myLog() << "Writing Amazon data output to" << logFile.fileName();
352 }
353 }
354 #if 0
355 myWarning() << "Remove debug from amazonfetcher.cpp";
356 QFile f(QString::fromLatin1("/tmp/test%1.json").arg(m_page));
357 if(f.open(QIODevice::WriteOnly)) {
358 QTextStream t(&f);
359 t.setCodec("UTF-8");
360 t << data;
361 }
362 f.close();
363 #endif
364
365 QJsonParseError jsonError;
366 QJsonObject databject = QJsonDocument::fromJson(data, &jsonError).object();
367 if(jsonError.error != QJsonParseError::NoError) {
368 myDebug() << "AmazonFetcher: JSON error -" << jsonError.errorString();
369 message(jsonError.errorString(), MessageHandler::Error);
370 stop();
371 return;
372 }
373 QJsonObject resultObject = databject.value(QStringLiteral("SearchResult")).toObject();
374 if(resultObject.isEmpty()) {
375 resultObject = databject.value(QStringLiteral("ItemsResult")).toObject();
376 }
377
378 if(m_total == -1) {
379 int totalResults = resultObject.value(QStringLiteral("TotalResultCount")).toInt();
380 if(totalResults > 0) {
381 m_total = totalResults;
382 // myDebug() << "Total results is" << totalResults;
383 }
384 }
385
386 QStringList errors;
387 QJsonValue errorValue = databject.value(QLatin1String("Errors"));
388 if(!errorValue.isNull()) {
389 foreach(const QJsonValue& error, errorValue.toArray()) {
390 errors += error.toObject().value(QLatin1String("Message")).toString();
391 }
392 }
393 if(!errors.isEmpty()) {
394 for(QStringList::ConstIterator it = errors.constBegin(); it != errors.constEnd(); ++it) {
395 myDebug() << "AmazonFetcher::" << *it;
396 }
397 message(errors[0], MessageHandler::Error);
398 stop();
399 return;
400 }
401
402 Data::CollPtr coll = createCollection();
403 if(!coll) {
404 myDebug() << "no collection pointer";
405 stop();
406 return;
407 }
408
409 int count = -1;
410 foreach(const QJsonValue& item, resultObject.value(QLatin1String("Items")).toArray()) {
411 ++count;
412 if(m_numResults >= m_limit) {
413 break;
414 }
415 if(!m_started) {
416 // might get aborted
417 break;
418 }
419 Data::EntryPtr entry(new Data::Entry(coll));
420 populateEntry(entry, item.toObject());
421
422 // special case book author
423 // amazon is really bad about not putting spaces after periods
424 if(coll->type() == Data::Collection::Book) {
425 QRegExp rx(QLatin1String("\\.([^\\s])"));
426 QStringList values = FieldFormat::splitValue(entry->field(QStringLiteral("author")));
427 for(QStringList::Iterator it = values.begin(); it != values.end(); ++it) {
428 (*it).replace(rx, QStringLiteral(". \\1"));
429 }
430 entry->setField(QStringLiteral("author"), values.join(FieldFormat::delimiterString()));
431 }
432
433 // UK puts the year in the title for some reason
434 if(m_site == UK && coll->type() == Data::Collection::Video) {
435 QRegExp rx(QLatin1String("\\[(\\d{4})\\]"));
436 QString t = entry->title();
437 if(rx.indexIn(t) > -1) {
438 QString y = rx.cap(1);
439 t = t.remove(rx).simplified();
440 entry->setField(QStringLiteral("title"), t);
441 if(entry->field(QStringLiteral("year")).isEmpty()) {
442 entry->setField(QStringLiteral("year"), y);
443 }
444 }
445 }
446
447 // myDebug() << entry->title();
448 FetchResult* r = new FetchResult(this, entry);
449 m_entries.insert(r->uid, entry);
450 emit signalResultFound(r);
451 ++m_numResults;
452 }
453
454 // we might have gotten aborted
455 if(!m_started) {
456 return;
457 }
458
459 // are there any additional results to get?
460 m_hasMoreResults = m_testResultsFile.isEmpty() && (m_page * AMAZON_RETURNS_PER_REQUEST < m_total);
461
462 const int currentTotal = qMin(m_total, m_limit);
463 if(m_testResultsFile.isEmpty() && (m_page * AMAZON_RETURNS_PER_REQUEST < currentTotal)) {
464 int foundCount = (m_page-1) * AMAZON_RETURNS_PER_REQUEST + coll->entryCount();
465 message(i18n("Results from %1: %2/%3", source(), foundCount, m_total), MessageHandler::Status);
466 ++m_page;
467 m_countOffset = 0;
468 doSearch();
469 } else if(request().value().count(QLatin1Char(';')) > 9) {
470 // start new request after cutting off first 10 isbn values
471 FetchRequest newRequest(request().collectionType(),
472 request().key(),
473 request().value().section(QLatin1Char(';'), 10));
474 startSearch(newRequest);
475 } else {
476 m_countOffset = m_entries.count() % AMAZON_RETURNS_PER_REQUEST;
477 if(m_countOffset == 0) {
478 ++m_page; // need to go to next page
479 }
480 stop();
481 }
482 }
483
fetchEntryHook(uint uid_)484 Tellico::Data::EntryPtr AmazonFetcher::fetchEntryHook(uint uid_) {
485 Data::EntryPtr entry = m_entries[uid_];
486 if(!entry) {
487 myWarning() << "no entry in dict";
488 return entry;
489 }
490
491 // do what we can to remove useless keywords
492 const int type = collectionType();
493 switch(type) {
494 case Data::Collection::Book:
495 case Data::Collection::ComicBook:
496 case Data::Collection::Bibtex:
497 if(optionalFields().contains(QStringLiteral("keyword"))) {
498 QStringList newWords;
499 const QStringList keywords = FieldFormat::splitValue(entry->field(QStringLiteral("keyword")));
500 foreach(const QString& keyword, keywords) {
501 if(keyword == QLatin1String("General") ||
502 keyword == QLatin1String("Subjects") ||
503 keyword == QLatin1String("Par prix") || // french stuff
504 keyword == QLatin1String("Divers") || // french stuff
505 keyword.startsWith(QLatin1Char('(')) ||
506 keyword.startsWith(QLatin1String("Authors"))) {
507 continue;
508 }
509 newWords += keyword;
510 }
511 newWords.removeDuplicates();
512 entry->setField(QStringLiteral("keyword"), newWords.join(FieldFormat::delimiterString()));
513 }
514 entry->setField(QStringLiteral("comments"), Tellico::decodeHTML(entry->field(QStringLiteral("comments"))));
515 break;
516
517 case Data::Collection::Video:
518 {
519 const QString genres = QStringLiteral("genre");
520 QStringList oldWords = FieldFormat::splitValue(entry->field(genres));
521 QStringList newWords;
522 // only care about genres that have "Genres" in the amazon response
523 // and take the first word after that
524 for(QStringList::Iterator it = oldWords.begin(); it != oldWords.end(); ++it) {
525 if((*it).indexOf(QLatin1String("Genres")) == -1) {
526 continue;
527 }
528
529 // the amazon2tellico stylesheet separates words with '/'
530 QStringList nodes = (*it).split(QLatin1Char('/'));
531 for(QStringList::Iterator it2 = nodes.begin(); it2 != nodes.end(); ++it2) {
532 if(*it2 != QLatin1String("Genres")) {
533 continue;
534 }
535 ++it2;
536 if(it2 != nodes.end() && *it2 != QLatin1String("General")) {
537 newWords += *it2;
538 }
539 break; // we're done
540 }
541 }
542 newWords.removeDuplicates();
543 entry->setField(genres, newWords.join(FieldFormat::delimiterString()));
544 // language tracks get duplicated, too
545 newWords = FieldFormat::splitValue(entry->field(QStringLiteral("language")));
546 newWords.removeDuplicates();
547 entry->setField(QStringLiteral("language"), newWords.join(FieldFormat::delimiterString()));
548 }
549 entry->setField(QStringLiteral("plot"), Tellico::decodeHTML(entry->field(QStringLiteral("plot"))));
550 break;
551
552 case Data::Collection::Album:
553 {
554 const QString genres = QStringLiteral("genre");
555 QStringList oldWords = FieldFormat::splitValue(entry->field(genres));
556 QStringList newWords;
557 // only care about genres that have "Styles" in the amazon response
558 // and take the first word after that
559 for(QStringList::Iterator it = oldWords.begin(); it != oldWords.end(); ++it) {
560 if((*it).indexOf(QLatin1String("Styles")) == -1) {
561 continue;
562 }
563
564 // the amazon2tellico stylesheet separates words with '/'
565 QStringList nodes = (*it).split(QLatin1Char('/'));
566 bool isStyle = false;
567 for(QStringList::Iterator it2 = nodes.begin(); it2 != nodes.end(); ++it2) {
568 if(!isStyle) {
569 if(*it2 == QLatin1String("Styles")) {
570 isStyle = true;
571 }
572 continue;
573 }
574 if(*it2 != QLatin1String("General")) {
575 newWords += *it2;
576 }
577 }
578 }
579 newWords.removeDuplicates();
580 entry->setField(genres, newWords.join(FieldFormat::delimiterString()));
581 }
582 entry->setField(QStringLiteral("comments"), Tellico::decodeHTML(entry->field(QStringLiteral("comments"))));
583 break;
584
585 case Data::Collection::Game:
586 entry->setField(QStringLiteral("description"), Tellico::decodeHTML(entry->field(QStringLiteral("description"))));
587 break;
588 }
589
590 // clean up the title
591 parseTitle(entry);
592
593 // also sometimes table fields have rows but no values
594 Data::FieldList fields = entry->collection()->fields();
595 QRegExp blank(QLatin1String("[\\s") +
596 FieldFormat::columnDelimiterString() +
597 FieldFormat::delimiterString() +
598 QLatin1String("]+")); // only white space, column separators and value separators
599 foreach(Data::FieldPtr fIt, fields) {
600 if(fIt->type() != Data::Field::Table) {
601 continue;
602 }
603 if(blank.exactMatch(entry->field(fIt))) {
604 entry->setField(fIt, QString());
605 }
606 }
607
608 // don't want to show image urls in the fetch dialog
609 // so clear them after reading the URL
610 QString imageURL;
611 switch(m_imageSize) {
612 case SmallImage:
613 imageURL = entry->field(QStringLiteral("small-image"));
614 entry->setField(QStringLiteral("small-image"), QString());
615 break;
616 case MediumImage:
617 imageURL = entry->field(QStringLiteral("medium-image"));
618 entry->setField(QStringLiteral("medium-image"), QString());
619 break;
620 case LargeImage:
621 imageURL = entry->field(QStringLiteral("large-image"));
622 entry->setField(QStringLiteral("large-image"), QString());
623 break;
624 case NoImage:
625 default:
626 break;
627 }
628
629 if(!imageURL.isEmpty()) {
630 // myDebug() << "grabbing " << imageURL;
631 QString id = ImageFactory::addImage(QUrl::fromUserInput(imageURL), true);
632 if(id.isEmpty()) {
633 message(i18n("The cover image could not be loaded."), MessageHandler::Warning);
634 } else { // amazon serves up 1x1 gifs occasionally, but that's caught in the image constructor
635 // all relevant collection types have cover fields
636 entry->setField(QStringLiteral("cover"), id);
637 }
638 }
639
640 return entry;
641 }
642
updateRequest(Data::EntryPtr entry_)643 Tellico::Fetch::FetchRequest AmazonFetcher::updateRequest(Data::EntryPtr entry_) {
644 const int type = entry_->collection()->type();
645 const QString t = entry_->field(QStringLiteral("title"));
646 if(type == Data::Collection::Book || type == Data::Collection::ComicBook || type == Data::Collection::Bibtex) {
647 const QString isbn = entry_->field(QStringLiteral("isbn"));
648 if(!isbn.isEmpty()) {
649 return FetchRequest(Fetch::ISBN, isbn);
650 }
651 const QString a = entry_->field(QStringLiteral("author"));
652 if(!a.isEmpty()) {
653 return t.isEmpty() ? FetchRequest(Fetch::Person, a)
654 : FetchRequest(Fetch::Keyword, t + QLatin1Char('-') + a);
655 }
656 } else if(type == Data::Collection::Album) {
657 const QString a = entry_->field(QStringLiteral("artist"));
658 if(!a.isEmpty()) {
659 return t.isEmpty() ? FetchRequest(Fetch::Person, a)
660 : FetchRequest(Fetch::Keyword, t + QLatin1Char('-') + a);
661 }
662 }
663
664 // optimistically try searching for title and rely on Collection::sameEntry() to figure things out
665 if(!t.isEmpty()) {
666 return FetchRequest(Fetch::Title, t);
667 }
668
669 return FetchRequest();
670 }
671
requestPayload(Fetch::FetchRequest request_)672 QByteArray AmazonFetcher::requestPayload(Fetch::FetchRequest request_) {
673 QJsonObject payload;
674 payload.insert(QLatin1String("PartnerTag"), m_assoc);
675 payload.insert(QLatin1String("PartnerType"), QLatin1String("Associates"));
676 payload.insert(QLatin1String("Service"), QLatin1String("ProductAdvertisingAPIv1"));
677 payload.insert(QLatin1String("Operation"), QLatin1String("SearchItems"));
678 payload.insert(QLatin1String("SortBy"), QLatin1String("Relevance"));
679 // not mandatory
680 // payload.insert(QLatin1String("Marketplace"), QLatin1String(siteData(m_site).host));
681 if(m_page > 1) {
682 payload.insert(QLatin1String("ItemPage"), m_page);
683 }
684
685 QJsonArray resources;
686 resources.append(QLatin1String("ItemInfo.Title"));
687 resources.append(QLatin1String("ItemInfo.ContentInfo"));
688 resources.append(QLatin1String("ItemInfo.ByLineInfo"));
689 resources.append(QLatin1String("ItemInfo.TechnicalInfo"));
690
691 const int type = request_.collectionType();
692 switch(type) {
693 case Data::Collection::Book:
694 case Data::Collection::ComicBook:
695 case Data::Collection::Bibtex:
696 payload.insert(QLatin1String("SearchIndex"), QLatin1String("Books"));
697 resources.append(QLatin1String("ItemInfo.ExternalIds"));
698 resources.append(QLatin1String("ItemInfo.ManufactureInfo"));
699 break;
700
701 case Data::Collection::Album:
702 payload.insert(QLatin1String("SearchIndex"), QLatin1String("Music"));
703 break;
704
705 case Data::Collection::Video:
706 // CA and JP appear to have a bug where Video only returns VHS or Music results
707 // DVD will return DVD, Blu-ray, etc. so just ignore VHS for those users
708 payload.insert(QLatin1String("SearchIndex"), QLatin1String("MoviesAndTV"));
709 if(m_site == CA || m_site == JP || m_site == IT || m_site == ES) {
710 payload.insert(QStringLiteral("SearchIndex"), QStringLiteral("DVD"));
711 } else {
712 payload.insert(QStringLiteral("SearchIndex"), QStringLiteral("Video"));
713 }
714 // params.insert(QStringLiteral("SortIndex"), QStringLiteral("relevancerank"));
715 resources.append(QLatin1String("ItemInfo.ContentRating"));
716 break;
717
718 case Data::Collection::Game:
719 payload.insert(QLatin1String("SearchIndex"), QLatin1String("VideoGames"));
720 break;
721
722 case Data::Collection::BoardGame:
723 payload.insert(QLatin1String("SearchIndex"), QLatin1String("ToysAndGames"));
724 // params.insert(QStringLiteral("SortIndex"), QStringLiteral("relevancerank"));
725 break;
726
727 case Data::Collection::Coin:
728 case Data::Collection::Stamp:
729 case Data::Collection::Wine:
730 case Data::Collection::Base:
731 case Data::Collection::Card:
732 myDebug() << "can't fetch this type:" << collectionType();
733 return QByteArray();
734 }
735
736 switch(request_.key()) {
737 case Title:
738 payload.insert(QLatin1String("Title"), request_.value());
739 break;
740
741 case Person:
742 if(type == Data::Collection::Video) {
743 payload.insert(QStringLiteral("Actor"), request_.value());
744 // payload.insert(QStringLiteral("Director"), request_.value());
745 } else if(type == Data::Collection::Album) {
746 payload.insert(QStringLiteral("Artist"), request_.value());
747 } else if(type == Data::Collection::Book) {
748 payload.insert(QLatin1String("Author"), request_.value());
749 } else {
750 payload.insert(QLatin1String("Keywords"), request_.value());
751 }
752 break;
753
754 case ISBN:
755 {
756 QString cleanValue = request_.value();
757 cleanValue.remove(QLatin1Char('-'));
758 // ISBN only get digits or 'X'
759 QStringList isbns = FieldFormat::splitValue(cleanValue);
760 // Amazon isbn13 search is still very flaky, so if possible, we're going to convert
761 // all of them to isbn10. If we run into a 979 isbn13, then we're forced to do an
762 // isbn13 search
763 bool isbn13 = false;
764 for(QStringList::Iterator it = isbns.begin(); it != isbns.end(); ) {
765 if((*it).startsWith(QLatin1String("979"))) {
766 isbn13 = true;
767 break;
768 }
769 ++it;
770 }
771 // if we want isbn10, then convert all
772 if(!isbn13) {
773 for(QStringList::Iterator it = isbns.begin(); it != isbns.end(); ++it) {
774 if((*it).length() > 12) {
775 (*it) = ISBNValidator::isbn10(*it);
776 (*it).remove(QLatin1Char('-'));
777 }
778 }
779 }
780 // limit to first 10
781 while(isbns.size() > 10) {
782 isbns.pop_back();
783 }
784 payload.insert(QLatin1String("Keywords"), isbns.join(QLatin1String("|")));
785 if(isbn13) {
786 // params.insert(QStringLiteral("IdType"), QStringLiteral("EAN"));
787 }
788 }
789 break;
790
791 case UPC:
792 {
793 QString cleanValue = request_.value();
794 cleanValue.remove(QLatin1Char('-'));
795 // for EAN values, add 0 to beginning if not 13 characters
796 // in order to assume US country code from UPC value
797 QStringList values;
798 foreach(const QString& splitValue, cleanValue.split(FieldFormat::delimiterString())) {
799 QString tmpValue = splitValue;
800 if(m_site != US && tmpValue.length() == 12) {
801 tmpValue.prepend(QLatin1Char('0'));
802 }
803 values << tmpValue;
804 // limit to first 10 values
805 if(values.length() >= 10) {
806 break;
807 }
808 }
809
810 payload.insert(QLatin1String("Keywords"), values.join(QLatin1String("|")));
811 }
812 break;
813
814 case Keyword:
815 payload.insert(QLatin1String("Keywords"), request_.value());
816 break;
817
818 case Raw:
819 {
820 QString key = request_.value().section(QLatin1Char('='), 0, 0).trimmed();
821 QString str = request_.value().section(QLatin1Char('='), 1).trimmed();
822 payload.insert(key, str);
823 }
824 break;
825
826 default:
827 myWarning() << "key not recognized: " << request().key();
828 return QByteArray();
829 }
830
831 switch(m_imageSize) {
832 case SmallImage: resources.append(QLatin1String("Images.Primary.Small")); break;
833 case MediumImage: resources.append(QLatin1String("Images.Primary.Medium")); break;
834 case LargeImage: resources.append(QLatin1String("Images.Primary.Large")); break;
835 case NoImage: break;
836 }
837
838 payload.insert(QLatin1String("Resources"), resources);
839 return QJsonDocument(payload).toJson(QJsonDocument::Compact);
840 }
841
createCollection()842 Tellico::Data::CollPtr AmazonFetcher::createCollection() {
843 Data::CollPtr coll = CollectionFactory::collection(collectionType(), true);
844 if(!coll) {
845 return coll;
846 }
847
848 QString imageFieldName;
849 switch(m_imageSize) {
850 case SmallImage: imageFieldName = QStringLiteral("small-image"); break;
851 case MediumImage: imageFieldName = QStringLiteral("medium-image"); break;
852 case LargeImage: imageFieldName = QStringLiteral("large-image"); break;
853 case NoImage: break;
854 }
855
856 if(!imageFieldName.isEmpty()) {
857 Data::FieldPtr field(new Data::Field(imageFieldName, QString(), Data::Field::URL));
858 coll->addField(field);
859 }
860
861 if(optionalFields().contains(QStringLiteral("amazon"))) {
862 Data::FieldPtr field(new Data::Field(QStringLiteral("amazon"), i18n("Amazon Link"), Data::Field::URL));
863 field->setCategory(i18n("General"));
864 coll->addField(field);
865 }
866
867 return coll;
868 }
869
populateEntry(Data::EntryPtr entry_,const QJsonObject & info_)870 void AmazonFetcher::populateEntry(Data::EntryPtr entry_, const QJsonObject& info_) {
871 QVariantMap itemMap = info_.value(QLatin1String("ItemInfo")).toObject().toVariantMap();
872 entry_->setField(QStringLiteral("title"), mapValue(itemMap, "Title", "DisplayValue"));
873 const QString isbn = mapValue(itemMap, "ExternalIds", "ISBNs", "DisplayValues");
874 if(!isbn.isEmpty()) {
875 // could be duplicate isbn10 and isbn13 values
876 QStringList isbns = FieldFormat::splitValue(isbn, FieldFormat::StringSplit);
877 for(QStringList::Iterator it = isbns.begin(); it != isbns.end(); ++it) {
878 if((*it).length() > 12) {
879 (*it) = ISBNValidator::isbn10(*it);
880 (*it).remove(QLatin1Char('-'));
881 }
882 }
883 isbns.removeDuplicates();
884 entry_->setField(QStringLiteral("isbn"), isbns.join(FieldFormat::delimiterString()));
885 }
886
887 QStringList actors, artists, authors, illustrators, publishers;
888 QVariantMap byLineMap = itemMap.value(QLatin1String("ByLineInfo")).toMap();
889 QVariantList contribArray = byLineMap.value(QLatin1String("Contributors")).toList();
890 foreach(const QVariant& v, contribArray) {
891 const QVariantMap contribMap = v.toMap();
892 const QString role = contribMap.value(QLatin1String("Role")).toString();
893 const QString name = contribMap.value(QLatin1String("Name")).toString();
894 if(role == QLatin1String("Actor")) {
895 actors += name;
896 } else if(role == QLatin1String("Artist")) {
897 artists += name;
898 } else if(role == QLatin1String("Author")) {
899 authors += name;
900 } else if(role == QLatin1String("Illustrator")) {
901 illustrators += name;
902 } else if(role == QLatin1String("Publisher")) {
903 publishers += name;
904 }
905 }
906 // assume for books that the manufacturer is the publishers
907 if(collectionType() == Data::Collection::Book ||
908 collectionType() == Data::Collection::Bibtex ||
909 collectionType() == Data::Collection::ComicBook) {
910 const QString manufacturer = byLineMap.value(QLatin1String("Manufacturer")).toMap()
911 .value(QLatin1String("DisplayValue")).toString();
912 publishers += manufacturer;
913 }
914
915 actors.removeDuplicates();
916 artists.removeDuplicates();
917 authors.removeDuplicates();
918 illustrators.removeDuplicates();
919 publishers.removeDuplicates();
920
921 if(!actors.isEmpty()) {
922 entry_->setField(QStringLiteral("cast"), actors.join(FieldFormat::delimiterString()));
923 }
924 if(!artists.isEmpty()) {
925 entry_->setField(QStringLiteral("artist"), artists.join(FieldFormat::delimiterString()));
926 }
927 if(!authors.isEmpty()) {
928 entry_->setField(QStringLiteral("author"), authors.join(FieldFormat::delimiterString()));
929 }
930 if(!illustrators.isEmpty()) {
931 entry_->setField(QStringLiteral("illustrator"), illustrators.join(FieldFormat::delimiterString()));
932 }
933 if(!publishers.isEmpty()) {
934 entry_->setField(QStringLiteral("publisher"), publishers.join(FieldFormat::delimiterString()));
935 }
936
937 QVariantMap contentMap = itemMap.value(QLatin1String("ContentInfo")).toMap();
938 entry_->setField(QStringLiteral("edition"), mapValue(contentMap, "Edition", "DisplayValue"));
939 entry_->setField(QStringLiteral("pages"), mapValue(contentMap, "PagesCount", "DisplayValue"));
940 const QString pubDate = mapValue(contentMap, "PublicationDate", "DisplayValue");
941 if(!pubDate.isEmpty()) {
942 entry_->setField(QStringLiteral("pub_year"), pubDate.left(4));
943 }
944 QVariantList langArray = itemMap.value(QLatin1String("ContentInfo")).toMap()
945 .value(QStringLiteral("Languages")).toMap()
946 .value(QStringLiteral("DisplayValues")).toList();
947 QStringList langs;
948 foreach(const QVariant& v, langArray) {
949 langs += mapValue(v.toMap(), "DisplayValue");
950 }
951 langs.removeDuplicates();
952 langs.removeAll(QString());
953 entry_->setField(QStringLiteral("language"), langs.join(FieldFormat::delimiterString()));
954
955 if(collectionType() == Data::Collection::Book ||
956 collectionType() == Data::Collection::Bibtex ||
957 collectionType() == Data::Collection::ComicBook) {
958 QVariantMap classificationsMap = itemMap.value(QLatin1String("Classifications")).toMap();
959 QVariantMap technicalMap = itemMap.value(QLatin1String("TechnicalInfo")).toMap();
960 QString binding = mapValue(classificationsMap, "Binding", "DisplayValue");
961 if(binding.isEmpty()) {
962 binding = mapValue(technicalMap, "Formats", "DisplayValues");
963 }
964 if(binding.contains(QStringLiteral("Paperback")) && binding != QStringLiteral("Trade Paperback")) {
965 binding = i18n("Paperback");
966 } else if(binding.contains(QStringLiteral("Hard"))) { // could be Hardcover or Hardback
967 binding = i18n("Hardback");
968 }
969 entry_->setField(QStringLiteral("binding"), binding);
970 }
971
972 QVariantMap imagesMap = info_.value(QLatin1String("Images")).toObject().toVariantMap();
973 switch(m_imageSize) {
974 case SmallImage:
975 entry_->setField(QStringLiteral("small-image"), mapValue(imagesMap, "Primary", "Small", "URL"));
976 break;
977 case MediumImage:
978 entry_->setField(QStringLiteral("medium-image"), mapValue(imagesMap, "Primary", "Medium", "URL"));
979 break;
980 case LargeImage:
981 entry_->setField(QStringLiteral("large-image"), mapValue(imagesMap, "Primary", "Large", "URL"));
982 break;
983 case NoImage:
984 break;
985 }
986
987 if(optionalFields().contains(QStringLiteral("amazon"))) {
988 entry_->setField(QStringLiteral("amazon"), mapValue(info_.toVariantMap(), "DetailPageURL"));
989 }
990 }
991
parseTitle(Tellico::Data::EntryPtr entry_)992 void AmazonFetcher::parseTitle(Tellico::Data::EntryPtr entry_) {
993 // assume that everything in brackets or parentheses is extra
994 static const QRegularExpression rx(QLatin1String("[\\(\\[](.*?)[\\)\\]]"));
995 QString title = entry_->field(QStringLiteral("title"));
996 int pos = 0;
997 QRegularExpressionMatch match = rx.match(title, pos);
998 while(match.hasMatch()) {
999 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
parseTitleToken(Tellico::Data::EntryPtr entry_,const QString & token_)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
defaultName()1068 QString AmazonFetcher::defaultName() {
1069 return i18n("Amazon.com Web Services");
1070 }
1071
defaultIcon()1072 QString AmazonFetcher::defaultIcon() {
1073 return favIcon("http://www.amazon.com");
1074 }
1075
allOptionalFields()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
configWidget(QWidget * parent_) const1083 Tellico::Fetch::ConfigWidget* AmazonFetcher::configWidget(QWidget* parent_) const {
1084 return new AmazonFetcher::ConfigWidget(parent_, this);
1085 }
1086
ConfigWidget(QWidget * parent_,const AmazonFetcher * fetcher_)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
saveConfigHook(KConfigGroup & config_)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
preferredName() const1212 QString AmazonFetcher::ConfigWidget::preferredName() const {
1213 return AmazonFetcher::siteData(m_siteCombo->currentData().toInt()).title;
1214 }
1215
slotSiteChanged()1216 void AmazonFetcher::ConfigWidget::slotSiteChanged() {
1217 emit signalName(preferredName());
1218 }
1219