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