diff --git a/src/fetch/amazonfetcher.cpp b/src/fetch/amazonfetcher.cpp index b5ab27d2..a7ddd996 100644 --- a/src/fetch/amazonfetcher.cpp +++ b/src/fetch/amazonfetcher.cpp @@ -1,1183 +1,1189 @@ /*************************************************************************** Copyright (C) 2004-2020 Robby Stephenson ***************************************************************************/ /*************************************************************************** * * * This program is free software; you can redistribute it and/or * * modify it under the terms of the GNU General Public License as * * published by the Free Software Foundation; either version 2 of * * the License or (at your option) version 3 or any later version * * accepted by the membership of KDE e.V. (or its successor approved * * by the membership of KDE e.V.), which shall act as a proxy * * defined in Section 14 of version 3 of the license. * * * * This program is distributed in the hope that it will be useful, * * but WITHOUT ANY WARRANTY; without even the implied warranty of * * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * * GNU General Public License for more details. * * * * You should have received a copy of the GNU General Public License * * along with this program. If not, see . * * * ***************************************************************************/ #include #include "amazonfetcher.h" #include "amazonrequest.h" #include "../collectionfactory.h" #include "../images/imagefactory.h" #include "../utils/guiproxy.h" #include "../collection.h" #include "../entry.h" #include "../field.h" #include "../fieldformat.h" #include "../utils/string_utils.h" #include "../utils/isbnvalidator.h" #include "../gui/combobox.h" #include "../tellico_debug.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include namespace { static const int AMAZON_RETURNS_PER_REQUEST = 10; static const int AMAZON_MAX_RETURNS_TOTAL = 20; static const char* AMAZON_ASSOC_TOKEN = "tellico-20"; } using namespace Tellico; using Tellico::Fetch::AmazonFetcher; // static // see https://webservices.amazon.com/paapi5/documentation/common-request-parameters.html#host-and-region const AmazonFetcher::SiteData& AmazonFetcher::siteData(int site_) { Q_ASSERT(site_ >= 0); Q_ASSERT(site_ < XX); static SiteData dataVector[16] = { { i18n("Amazon (US)"), "www.amazon.com", "us-east-1", QLatin1String("us"), i18n("United States") }, { i18n("Amazon (UK)"), "www.amazon.co.uk", "eu-west-1", QLatin1String("gb"), i18n("United Kingdom") }, { i18n("Amazon (Germany)"), "www.amazon.de", "eu-west-1", QLatin1String("de"), i18n("Germany") }, { i18n("Amazon (Japan)"), "www.amazon.co.jp", "us-west-2", QLatin1String("jp"), i18n("Japan") }, { i18n("Amazon (France)"), "www.amazon.fr", "eu-west-1", QLatin1String("fr"), i18n("France") }, { i18n("Amazon (Canada)"), "www.amazon.ca", "us-east-1", QLatin1String("ca"), i18n("Canada") }, { // TODO: no chinese in PAAPI-5 yet? i18n("Amazon (China)"), "http://webservices.amazon.cn/onca/xml", "us-west-2", QLatin1String("ch"), i18n("China") }, { i18n("Amazon (Spain)"), "www.amazon.es", "eu-west-1", QLatin1String("es"), i18n("Spain") }, { i18n("Amazon (Italy)"), "www.amazon.it", "eu-west-1", QLatin1String("it"), i18n("Italy") }, { i18n("Amazon (Brazil)"), "www.amazon.com.br", "us-east-1", QLatin1String("br"), i18n("Brazil") }, { i18n("Amazon (Australia)"), "www.amazon.com.au", "us-west-2", QLatin1String("au"), i18n("Australia") }, { i18n("Amazon (India)"), "www.amazon.in", "eu-west-1", QLatin1String("in"), i18n("India") }, { i18n("Amazon (Mexico)"), "www.amazon.com.mx", "us-east-1", QLatin1String("mx"), i18n("Mexico") }, { i18n("Amazon (Turkey)"), "www.amazon.com.tr", "eu-west-1", QLatin1String("tr"), i18n("Turkey") }, { i18n("Amazon (Singapore)"), "www.amazon.sg", "us-west-2", QLatin1String("sg"), i18n("Singapore") }, { i18n("Amazon (UAE)"), "www.amazon.ae", "eu-west-1", QLatin1String("ae"), i18n("United Arab Emirates") } }; return dataVector[qBound(0, site_, static_cast(sizeof(dataVector)/sizeof(SiteData)))]; } AmazonFetcher::AmazonFetcher(QObject* parent_) : Fetcher(parent_), m_site(Unknown), m_imageSize(MediumImage), m_assoc(QLatin1String(AMAZON_ASSOC_TOKEN)), m_limit(AMAZON_MAX_RETURNS_TOTAL), m_countOffset(0), m_page(1), m_total(-1), m_numResults(0), m_job(nullptr), m_started(false) { } AmazonFetcher::~AmazonFetcher() { } QString AmazonFetcher::source() const { return m_name.isEmpty() ? defaultName() : m_name; } QString AmazonFetcher::attribution() const { return i18n("This data is licensed under specific terms.", QLatin1String("https://affiliate-program.amazon.com/gp/advertising/api/detail/agreement.html")); } bool AmazonFetcher::canFetch(int type) const { return type == Data::Collection::Book || type == Data::Collection::ComicBook || type == Data::Collection::Bibtex || type == Data::Collection::Album || type == Data::Collection::Video || type == Data::Collection::Game || type == Data::Collection::BoardGame; } bool AmazonFetcher::canSearch(FetchKey k) const { // no UPC in Canada return k == Title || k == Person || k == ISBN || k == UPC || k == Keyword; } void AmazonFetcher::readConfigHook(const KConfigGroup& config_) { const int site = config_.readEntry("Site", int(Unknown)); Q_ASSERT(site != Unknown); m_site = static_cast(site); if(m_name.isEmpty()) { m_name = siteData(m_site).title; } QString s = config_.readEntry("AccessKey"); if(!s.isEmpty()) { m_accessKey = s; } else { myWarning() << "No Amazon access key"; } s = config_.readEntry("AssocToken"); if(!s.isEmpty()) { m_assoc = s; } s = config_.readEntry("SecretKey"); if(!s.isEmpty()) { m_secretKey = s; } else { myWarning() << "No Amazon secret key"; } int imageSize = config_.readEntry("Image Size", -1); if(imageSize > -1) { m_imageSize = static_cast(imageSize); } } void AmazonFetcher::search() { m_started = true; m_page = 1; m_total = -1; m_countOffset = 0; m_numResults = 0; doSearch(); } void AmazonFetcher::continueSearch() { m_started = true; m_limit += AMAZON_MAX_RETURNS_TOTAL; doSearch(); } void AmazonFetcher::doSearch() { if(m_secretKey.isEmpty() || m_accessKey.isEmpty()) { // this message is split in two since the first half is reused later message(i18n("Access to data from Amazon.com requires an AWS Access Key ID and a Secret Key.") + QLatin1Char(' ') + i18n("Those values must be entered in the data source settings."), MessageHandler::Error); stop(); return; } const QByteArray payload = requestPayload(request()); if(payload.isEmpty()) { stop(); return; } AmazonRequest request(m_accessKey, m_secretKey); request.setHost(siteData(m_site).host); request.setRegion(siteData(m_site).region); // debugging check if(m_testResultsFile.isEmpty()) { QUrl u; u.setHost(QString::fromUtf8(siteData(m_site).host)); m_job = KIO::storedHttpPost(payload, u, KIO::HideProgressInfo); QMapIterator i(request.headers(payload)); while(i.hasNext()) { i.next(); m_job->addMetaData(QStringLiteral("customHTTPHeader"), QString::fromUtf8(i.key() + ": " + i.value())); } } else { myDebug() << "Reading" << m_testResultsFile; m_job = KIO::storedGet(QUrl::fromLocalFile(m_testResultsFile), KIO::NoReload, KIO::HideProgressInfo); } KJobWidgets::setWindow(m_job, GUI::Proxy::widget()); connect(m_job.data(), &KJob::result, this, &AmazonFetcher::slotComplete); } void AmazonFetcher::stop() { if(!m_started) { return; } // myDebug(); if(m_job) { m_job->kill(); m_job = nullptr; } m_started = false; emit signalDone(this); } void AmazonFetcher::slotComplete(KJob*) { if(m_job->error()) { m_job->uiDelegate()->showErrorMessage(); stop(); return; } const QByteArray data = m_job->data(); if(data.isEmpty()) { myDebug() << "no data"; stop(); return; } // since the fetch is done, don't worry about holding the job pointer m_job = nullptr; #if 0 myWarning() << "Remove debug from amazonfetcher.cpp"; - QFile f(QString::fromLatin1("/tmp/test%1.xml").arg(m_page)); + QFile f(QString::fromLatin1("/tmp/test%1.json").arg(m_page)); if(f.open(QIODevice::WriteOnly)) { QTextStream t(&f); t.setCodec("UTF-8"); t << data; } f.close(); #endif QJsonParseError jsonError; QJsonObject databject = QJsonDocument::fromJson(data, &jsonError).object(); if(jsonError.error != QJsonParseError::NoError) { myDebug() << "AmazonFetcher: JSON error -" << jsonError.errorString(); message(jsonError.errorString(), MessageHandler::Error); stop(); return; } QJsonObject resultObject = databject.value(QStringLiteral("SearchResult")).toObject(); if(resultObject.isEmpty()) { resultObject = databject.value(QStringLiteral("ItemsResult")).toObject(); } if(m_total == -1) { int totalResults = resultObject.value(QStringLiteral("TotalResultCount")).toInt(); if(totalResults > 0) { m_total = totalResults; // myDebug() << "Total results is" << totalResults; } } QStringList errors; QJsonValue errorValue = databject.value(QLatin1String("Errors")); if(!errorValue.isNull()) { foreach(const QJsonValue& error, errorValue.toArray()) { errors += error.toObject().value(QLatin1String("Message")).toString(); } } if(!errors.isEmpty()) { for(QStringList::ConstIterator it = errors.constBegin(); it != errors.constEnd(); ++it) { myDebug() << "AmazonFetcher::" << *it; } message(errors[0], MessageHandler::Error); stop(); return; } Data::CollPtr coll = createCollection(); if(!coll) { myDebug() << "no collection pointer"; stop(); return; } int count = -1; foreach(const QJsonValue& item, resultObject.value(QLatin1String("Items")).toArray()) { ++count; if(m_numResults >= m_limit) { break; } if(!m_started) { // might get aborted break; } Data::EntryPtr entry(new Data::Entry(coll)); populateEntry(entry, item.toObject()); // special case book author // amazon is really bad about not putting spaces after periods if(coll->type() == Data::Collection::Book) { QRegExp rx(QLatin1String("\\.([^\\s])")); QStringList values = FieldFormat::splitValue(entry->field(QStringLiteral("author"))); for(QStringList::Iterator it = values.begin(); it != values.end(); ++it) { (*it).replace(rx, QStringLiteral(". \\1")); } entry->setField(QStringLiteral("author"), values.join(FieldFormat::delimiterString())); } // UK puts the year in the title for some reason if(m_site == UK && coll->type() == Data::Collection::Video) { QRegExp rx(QLatin1String("\\[(\\d{4})\\]")); QString t = entry->title(); if(rx.indexIn(t) > -1) { QString y = rx.cap(1); t = t.remove(rx).simplified(); entry->setField(QStringLiteral("title"), t); if(entry->field(QStringLiteral("year")).isEmpty()) { entry->setField(QStringLiteral("year"), y); } } } - // strip HTML from comments, or plot in movies - // tentatively don't do this, looks like ECS 4 cleaned everything up -/* - if(coll->type() == Data::Collection::Video) { - QString plot = entry->field(QLatin1String("plot")); - plot.remove(stripHTML); - entry->setField(QLatin1String("plot"), plot); - } else if(coll->type() == Data::Collection::Game) { - QString desc = entry->field(QLatin1String("description")); - desc.remove(stripHTML); - entry->setField(QLatin1String("description"), desc); - } else { - QString comments = entry->field(QLatin1String("comments")); - comments.remove(stripHTML); - entry->setField(QLatin1String("comments"), comments); - } -*/ // myDebug() << entry->title(); FetchResult* r = new FetchResult(Fetcher::Ptr(this), entry); m_entries.insert(r->uid, entry); emit signalResultFound(r); ++m_numResults; } // we might have gotten aborted if(!m_started) { return; } // are there any additional results to get? - m_hasMoreResults = m_page * AMAZON_RETURNS_PER_REQUEST < m_total; + m_hasMoreResults = m_testResultsFile.isEmpty() && (m_page * AMAZON_RETURNS_PER_REQUEST < m_total); const int currentTotal = qMin(m_total, m_limit); - if(m_page * AMAZON_RETURNS_PER_REQUEST < currentTotal) { + if(m_testResultsFile.isEmpty() && (m_page * AMAZON_RETURNS_PER_REQUEST < currentTotal)) { int foundCount = (m_page-1) * AMAZON_RETURNS_PER_REQUEST + coll->entryCount(); message(i18n("Results from %1: %2/%3", source(), foundCount, m_total), MessageHandler::Status); ++m_page; m_countOffset = 0; doSearch(); } else if(request().value.count(QLatin1Char(';')) > 9) { // start new request after cutting off first 10 isbn values FetchRequest newRequest = request(); newRequest.value = request().value.section(QLatin1Char(';'), 10); startSearch(newRequest); } else { m_countOffset = m_entries.count() % AMAZON_RETURNS_PER_REQUEST; if(m_countOffset == 0) { ++m_page; // need to go to next page } stop(); } } Tellico::Data::EntryPtr AmazonFetcher::fetchEntryHook(uint uid_) { Data::EntryPtr entry = m_entries[uid_]; if(!entry) { myWarning() << "no entry in dict"; return entry; } // do what we can to remove useless keywords const int type = collectionType(); switch(type) { case Data::Collection::Book: case Data::Collection::ComicBook: case Data::Collection::Bibtex: if(optionalFields().contains(QStringLiteral("keyword"))) { StringSet newWords; const QStringList keywords = FieldFormat::splitValue(entry->field(QStringLiteral("keyword"))); foreach(const QString& keyword, keywords) { if(keyword == QLatin1String("General") || keyword == QLatin1String("Subjects") || keyword == QLatin1String("Par prix") || // french stuff keyword == QLatin1String("Divers") || // french stuff keyword.startsWith(QLatin1Char('(')) || keyword.startsWith(QLatin1String("Authors"))) { continue; } newWords.add(keyword); } entry->setField(QStringLiteral("keyword"), newWords.toList().join(FieldFormat::delimiterString())); } entry->setField(QStringLiteral("comments"), Tellico::decodeHTML(entry->field(QStringLiteral("comments")))); break; case Data::Collection::Video: { const QString genres = QStringLiteral("genre"); QStringList oldWords = FieldFormat::splitValue(entry->field(genres)); StringSet words; // only care about genres that have "Genres" in the amazon response // and take the first word after that for(QStringList::Iterator it = oldWords.begin(); it != oldWords.end(); ++it) { if((*it).indexOf(QLatin1String("Genres")) == -1) { continue; } // the amazon2tellico stylesheet separates words with '/' QStringList nodes = (*it).split(QLatin1Char('/')); for(QStringList::Iterator it2 = nodes.begin(); it2 != nodes.end(); ++it2) { if(*it2 != QLatin1String("Genres")) { continue; } ++it2; if(it2 != nodes.end() && *it2 != QLatin1String("General")) { words.add(*it2); } break; // we're done } } entry->setField(genres, words.toList().join(FieldFormat::delimiterString())); // language tracks get duplicated, too words.clear(); words.add(FieldFormat::splitValue(entry->field(QStringLiteral("language")))); entry->setField(QStringLiteral("language"), words.toList().join(FieldFormat::delimiterString())); } entry->setField(QStringLiteral("plot"), Tellico::decodeHTML(entry->field(QStringLiteral("plot")))); break; case Data::Collection::Album: { const QString genres = QStringLiteral("genre"); QStringList oldWords = FieldFormat::splitValue(entry->field(genres)); StringSet words; // only care about genres that have "Styles" in the amazon response // and take the first word after that for(QStringList::Iterator it = oldWords.begin(); it != oldWords.end(); ++it) { if((*it).indexOf(QLatin1String("Styles")) == -1) { continue; } // the amazon2tellico stylesheet separates words with '/' QStringList nodes = (*it).split(QLatin1Char('/')); bool isStyle = false; for(QStringList::Iterator it2 = nodes.begin(); it2 != nodes.end(); ++it2) { if(!isStyle) { if(*it2 == QLatin1String("Styles")) { isStyle = true; } continue; } if(*it2 != QLatin1String("General")) { words.add(*it2); } } } entry->setField(genres, words.toList().join(FieldFormat::delimiterString())); } entry->setField(QStringLiteral("comments"), Tellico::decodeHTML(entry->field(QStringLiteral("comments")))); break; case Data::Collection::Game: entry->setField(QStringLiteral("description"), Tellico::decodeHTML(entry->field(QStringLiteral("description")))); break; } // clean up the title parseTitle(entry); // also sometimes table fields have rows but no values Data::FieldList fields = entry->collection()->fields(); QRegExp blank(QLatin1String("[\\s") + FieldFormat::columnDelimiterString() + FieldFormat::delimiterString() + QLatin1String("]+")); // only white space, column separators and value separators foreach(Data::FieldPtr fIt, fields) { if(fIt->type() != Data::Field::Table) { continue; } if(blank.exactMatch(entry->field(fIt))) { entry->setField(fIt, QString()); } } // don't want to show image urls in the fetch dialog // so clear them after reading the URL QString imageURL; switch(m_imageSize) { case SmallImage: imageURL = entry->field(QStringLiteral("small-image")); entry->setField(QStringLiteral("small-image"), QString()); break; case MediumImage: imageURL = entry->field(QStringLiteral("medium-image")); entry->setField(QStringLiteral("medium-image"), QString()); break; case LargeImage: imageURL = entry->field(QStringLiteral("large-image")); entry->setField(QStringLiteral("large-image"), QString()); break; case NoImage: default: break; } -// myDebug() << "grabbing " << imageURL; + if(!imageURL.isEmpty()) { +// myDebug() << "grabbing " << imageURL; QString id = ImageFactory::addImage(QUrl::fromUserInput(imageURL), true); if(id.isEmpty()) { message(i18n("The cover image could not be loaded."), MessageHandler::Warning); } else { // amazon serves up 1x1 gifs occasionally, but that's caught in the image constructor // all relevant collection types have cover fields entry->setField(QStringLiteral("cover"), id); } } return entry; } Tellico::Fetch::FetchRequest AmazonFetcher::updateRequest(Data::EntryPtr entry_) { const int type = entry_->collection()->type(); const QString t = entry_->field(QStringLiteral("title")); if(type == Data::Collection::Book || type == Data::Collection::ComicBook || type == Data::Collection::Bibtex) { const QString isbn = entry_->field(QStringLiteral("isbn")); if(!isbn.isEmpty()) { return FetchRequest(Fetch::ISBN, isbn); } const QString a = entry_->field(QStringLiteral("author")); if(!a.isEmpty()) { return t.isEmpty() ? FetchRequest(Fetch::Person, a) : FetchRequest(Fetch::Keyword, t + QLatin1Char('-') + a); } } else if(type == Data::Collection::Album) { const QString a = entry_->field(QStringLiteral("artist")); if(!a.isEmpty()) { return t.isEmpty() ? FetchRequest(Fetch::Person, a) : FetchRequest(Fetch::Keyword, t + QLatin1Char('-') + a); } } // optimistically try searching for title and rely on Collection::sameEntry() to figure things out if(!t.isEmpty()) { return FetchRequest(Fetch::Title, t); } return FetchRequest(); } QByteArray AmazonFetcher::requestPayload(FetchRequest request_) { QJsonObject payload; payload.insert(QLatin1String("PartnerTag"), m_assoc); payload.insert(QLatin1String("PartnerType"), QLatin1String("Associates")); payload.insert(QLatin1String("Operation"), QLatin1String("SearchItems")); payload.insert(QLatin1String("SortBy"), QLatin1String("Relevance")); // not mandatory // payload.insert(QLatin1String("Marketplace"), QLatin1String(siteData(m_site).host)); if(m_page > 1) { payload.insert(QLatin1String("ItemPage"), m_page); } QJsonArray resources; resources.append(QLatin1String("ItemInfo.Title")); resources.append(QLatin1String("ItemInfo.ContentInfo")); resources.append(QLatin1String("ItemInfo.ByLineInfo")); resources.append(QLatin1String("ItemInfo.TechnicalInfo")); const int type = request_.collectionType; switch(type) { case Data::Collection::Book: case Data::Collection::ComicBook: case Data::Collection::Bibtex: payload.insert(QLatin1String("SearchIndex"), QLatin1String("Books")); resources.append(QLatin1String("ItemInfo.ExternalIds")); resources.append(QLatin1String("ItemInfo.ManufactureInfo")); break; case Data::Collection::Album: payload.insert(QLatin1String("SearchIndex"), QLatin1String("Music")); break; case Data::Collection::Video: // CA and JP appear to have a bug where Video only returns VHS or Music results // DVD will return DVD, Blu-ray, etc. so just ignore VHS for those users payload.insert(QLatin1String("SearchIndex"), QLatin1String("MoviesAndTV")); if(m_site == CA || m_site == JP || m_site == IT || m_site == ES) { payload.insert(QStringLiteral("SearchIndex"), QStringLiteral("DVD")); } else { payload.insert(QStringLiteral("SearchIndex"), QStringLiteral("Video")); } // params.insert(QStringLiteral("SortIndex"), QStringLiteral("relevancerank")); resources.append(QLatin1String("ItemInfo.ContentRating")); break; case Data::Collection::Game: payload.insert(QLatin1String("SearchIndex"), QLatin1String("VideoGames")); break; case Data::Collection::BoardGame: payload.insert(QLatin1String("SearchIndex"), QLatin1String("ToysAndGames")); // params.insert(QStringLiteral("SortIndex"), QStringLiteral("relevancerank")); break; case Data::Collection::Coin: case Data::Collection::Stamp: case Data::Collection::Wine: case Data::Collection::Base: case Data::Collection::Card: myDebug() << "can't fetch this type:" << collectionType(); return QByteArray(); } switch(request_.key) { case Title: payload.insert(QLatin1String("Title"), request_.value); break; case Person: if(type == Data::Collection::Video) { payload.insert(QStringLiteral("Actor"), request_.value); // payload.insert(QStringLiteral("Director"), request_.value); } else if(type == Data::Collection::Album) { payload.insert(QStringLiteral("Artist"), request_.value); } else if(type == Data::Collection::Book) { payload.insert(QLatin1String("Author"), request_.value); } else { payload.insert(QLatin1String("Keywords"), request_.value); } break; case ISBN: { QString cleanValue = request_.value; cleanValue.remove(QLatin1Char('-')); // ISBN only get digits or 'X' QStringList isbns = FieldFormat::splitValue(cleanValue); // Amazon isbn13 search is still very flaky, so if possible, we're going to convert // all of them to isbn10. If we run into a 979 isbn13, then we're forced to do an // isbn13 search bool isbn13 = false; for(QStringList::Iterator it = isbns.begin(); it != isbns.end(); ) { if((*it).startsWith(QLatin1String("979"))) { isbn13 = true; break; } ++it; } // if we want isbn10, then convert all if(!isbn13) { for(QStringList::Iterator it = isbns.begin(); it != isbns.end(); ++it) { if((*it).length() > 12) { (*it) = ISBNValidator::isbn10(*it); (*it).remove(QLatin1Char('-')); } } } // limit to first 10 while(isbns.size() > 10) { isbns.pop_back(); } payload.insert(QLatin1String("Keywords"), isbns.join(QLatin1String("|"))); if(isbn13) { // params.insert(QStringLiteral("IdType"), QStringLiteral("EAN")); } } break; case UPC: { QString cleanValue = request_.value; cleanValue.remove(QLatin1Char('-')); // for EAN values, add 0 to beginning if not 13 characters // in order to assume US country code from UPC value QStringList values; foreach(const QString& splitValue, cleanValue.split(FieldFormat::delimiterString())) { QString tmpValue = splitValue; if(m_site != US && tmpValue.length() == 12) { tmpValue.prepend(QLatin1Char('0')); } values << tmpValue; // limit to first 10 values if(values.length() >= 10) { break; } } payload.insert(QLatin1String("Keywords"), values.join(QLatin1String("|"))); } break; case Keyword: payload.insert(QLatin1String("Keywords"), request_.value); break; case Raw: { QString key = request_.value.section(QLatin1Char('='), 0, 0).trimmed(); QString str = request_.value.section(QLatin1Char('='), 1).trimmed(); payload.insert(key, str); } break; default: myWarning() << "key not recognized: " << request().key; return QByteArray(); } switch(m_imageSize) { case SmallImage: resources.append(QLatin1String("Images.Primary.Small")); break; case MediumImage: resources.append(QLatin1String("Images.Primary.Medium")); break; case LargeImage: resources.append(QLatin1String("Images.Primary.Large")); break; case NoImage: break; } payload.insert(QLatin1String("Resources"), resources); return QJsonDocument(payload).toJson(QJsonDocument::Compact); } Tellico::Data::CollPtr AmazonFetcher::createCollection() { Data::CollPtr coll = CollectionFactory::collection(collectionType(), true); if(!coll) { return coll; } QString imageFieldName; switch(m_imageSize) { case SmallImage: imageFieldName = QStringLiteral("small-image"); break; case MediumImage: imageFieldName = QStringLiteral("medium-image"); break; case LargeImage: imageFieldName = QStringLiteral("large-image"); break; case NoImage: break; } if(!imageFieldName.isEmpty()) { Data::FieldPtr field(new Data::Field(imageFieldName, QString(), Data::Field::URL)); coll->addField(field); } if(optionalFields().contains(QStringLiteral("amazon"))) { Data::FieldPtr field(new Data::Field(QStringLiteral("amazon"), i18n("Amazon Link"), Data::Field::URL)); field->setCategory(i18n("General")); coll->addField(field); } return coll; } void AmazonFetcher::populateEntry(Data::EntryPtr entry_, const QJsonObject& info_) { QVariantMap itemMap = info_.value(QLatin1String("ItemInfo")).toObject().toVariantMap(); entry_->setField(QStringLiteral("title"), mapValue(itemMap, "Title", "DisplayValue")); const QString isbn = mapValue(itemMap, "ExternalIds", "ISBNs", "DisplayValues"); if(!isbn.isEmpty()) { - entry_->setField(QStringLiteral("isbn"), isbn); + // could be duplicate isbn10 and isbn13 values + QStringList isbns = FieldFormat::splitValue(isbn, FieldFormat::StringSplit); + for(QStringList::Iterator it = isbns.begin(); it != isbns.end(); ++it) { + if((*it).length() > 12) { + (*it) = ISBNValidator::isbn10(*it); + (*it).remove(QLatin1Char('-')); + } + } + isbns.removeDuplicates(); + entry_->setField(QStringLiteral("isbn"), isbns.join(FieldFormat::delimiterString())); } QStringList actors, artists, authors, illustrators, publishers; QVariantMap byLineMap = itemMap.value(QLatin1String("ByLineInfo")).toMap(); QVariantList contribArray = byLineMap.value(QLatin1String("Contributors")).toList(); foreach(const QVariant& v, contribArray) { const QVariantMap contribMap = v.toMap(); const QString role = contribMap.value(QLatin1String("Role")).toString(); const QString name = contribMap.value(QLatin1String("Name")).toString(); if(role == QLatin1String("Actor")) { actors += name; } else if(role == QLatin1String("Artist")) { artists += name; } else if(role == QLatin1String("Author")) { authors += name; } else if(role == QLatin1String("Illustrator")) { illustrators += name; } else if(role == QLatin1String("Publisher")) { publishers += name; } } // assume for books that the manufacturer is the publishers if(collectionType() == Data::Collection::Book || collectionType() == Data::Collection::Bibtex || collectionType() == Data::Collection::ComicBook) { const QString manufacturer = byLineMap.value(QLatin1String("Manufacturer")).toMap() .value(QLatin1String("DisplayValue")).toString(); publishers += manufacturer; } actors.removeDuplicates(); artists.removeDuplicates(); authors.removeDuplicates(); illustrators.removeDuplicates(); publishers.removeDuplicates(); - if(!actors.isEmpty()) { entry_->setField(QStringLiteral("cast"), actors.join(FieldFormat::delimiterString())); } if(!artists.isEmpty()) { entry_->setField(QStringLiteral("artist"), artists.join(FieldFormat::delimiterString())); } if(!authors.isEmpty()) { entry_->setField(QStringLiteral("author"), authors.join(FieldFormat::delimiterString())); } if(!illustrators.isEmpty()) { entry_->setField(QStringLiteral("illustrator"), illustrators.join(FieldFormat::delimiterString())); } if(!publishers.isEmpty()) { entry_->setField(QStringLiteral("publisher"), publishers.join(FieldFormat::delimiterString())); } QVariantMap contentMap = itemMap.value(QLatin1String("ContentInfo")).toMap(); entry_->setField(QStringLiteral("edition"), mapValue(contentMap, "Edition", "DisplayValue")); entry_->setField(QStringLiteral("pages"), mapValue(contentMap, "PagesCount", "DisplayValue")); const QString pubDate = mapValue(contentMap, "PublicationDate", "DisplayValue"); if(!pubDate.isEmpty()) { entry_->setField(QStringLiteral("pub_year"), pubDate.left(4)); } QVariantList langArray = itemMap.value(QLatin1String("ContentInfo")).toMap() .value(QStringLiteral("Languages")).toMap() .value(QStringLiteral("DisplayValues")).toList(); QStringList langs; foreach(const QVariant& v, langArray) { langs += mapValue(v.toMap(), "DisplayValue"); } langs.removeDuplicates(); langs.removeAll(QString()); entry_->setField(QStringLiteral("language"), langs.join(FieldFormat::delimiterString())); - QVariantMap technicalMap = itemMap.value(QLatin1String("TechnicalInfo")).toMap(); if(collectionType() == Data::Collection::Book || collectionType() == Data::Collection::Bibtex || collectionType() == Data::Collection::ComicBook) { - entry_->setField(QStringLiteral("binding"), mapValue(technicalMap, "Formats", "DisplayValues")); + QVariantMap classificationsMap = itemMap.value(QLatin1String("Classifications")).toMap(); + QVariantMap technicalMap = itemMap.value(QLatin1String("TechnicalInfo")).toMap(); + QString binding = mapValue(classificationsMap, "Binding", "DisplayValue"); + if(binding.isEmpty()) { + binding = mapValue(technicalMap, "Formats", "DisplayValues"); + } + if(binding.contains(QStringLiteral("Paperback")) && binding != QStringLiteral("Trade Paperback")) { + binding = i18n("Paperback"); + } else if(binding.contains(QStringLiteral("Hard"))) { // could be Hardcover or Hardback + binding = i18n("Hardback"); + } + entry_->setField(QStringLiteral("binding"), binding); } QVariantMap imagesMap = info_.value(QLatin1String("Images")).toObject().toVariantMap(); switch(m_imageSize) { case SmallImage: entry_->setField(QStringLiteral("small-image"), mapValue(imagesMap, "Primary", "Small", "URL")); break; case MediumImage: entry_->setField(QStringLiteral("medium-image"), mapValue(imagesMap, "Primary", "Medium", "URL")); break; case LargeImage: entry_->setField(QStringLiteral("large-image"), mapValue(imagesMap, "Primary", "Large", "URL")); break; case NoImage: break; } if(optionalFields().contains(QStringLiteral("amazon"))) { entry_->setField(QStringLiteral("amazon"), mapValue(info_.toVariantMap(), "DetailPageURL")); } } void AmazonFetcher::parseTitle(Tellico::Data::EntryPtr entry_) { // assume that everything in brackets or parentheses is extra static const QRegularExpression rx(QLatin1String("[\\(\\[](.*?)[\\)\\]]")); QString title = entry_->field(QStringLiteral("title")); int pos = 0; QRegularExpressionMatch match = rx.match(title, pos); while(match.hasMatch()) { pos = match.capturedStart(); if(parseTitleToken(entry_, match.captured(1))) { title.remove(match.capturedStart(), match.capturedLength()); --pos; // search again there } match = rx.match(title, pos+1); } entry_->setField(QStringLiteral("title"), title.simplified()); } bool AmazonFetcher::parseTitleToken(Tellico::Data::EntryPtr entry_, const QString& token_) { // myDebug() << "title token:" << token_; // if res = true, then the token gets removed from the title bool res = false; if(token_.indexOf(QLatin1String("widescreen"), 0, Qt::CaseInsensitive) > -1 || token_.indexOf(i18n("Widescreen"), 0, Qt::CaseInsensitive) > -1) { entry_->setField(QStringLiteral("widescreen"), QStringLiteral("true")); // res = true; leave it in the title } else if(token_.indexOf(QLatin1String("full screen"), 0, Qt::CaseInsensitive) > -1) { // skip, but go ahead and remove from title res = true; } else if(token_.indexOf(QLatin1String("import"), 0, Qt::CaseInsensitive) > -1) { // skip, but go ahead and remove from title res = true; } if(token_.indexOf(QLatin1String("blu-ray"), 0, Qt::CaseInsensitive) > -1) { entry_->setField(QStringLiteral("medium"), i18n("Blu-ray")); res = true; } else if(token_.indexOf(QLatin1String("hd dvd"), 0, Qt::CaseInsensitive) > -1) { entry_->setField(QStringLiteral("medium"), i18n("HD DVD")); res = true; } else if(token_.indexOf(QLatin1String("vhs"), 0, Qt::CaseInsensitive) > -1) { entry_->setField(QStringLiteral("medium"), i18n("VHS")); res = true; } if(token_.indexOf(QLatin1String("director's cut"), 0, Qt::CaseInsensitive) > -1 || token_.indexOf(i18n("Director's Cut"), 0, Qt::CaseInsensitive) > -1) { entry_->setField(QStringLiteral("directors-cut"), QStringLiteral("true")); // res = true; leave it in the title } if(token_.toLower() == QLatin1String("ntsc")) { entry_->setField(QStringLiteral("format"), i18n("NTSC")); res = true; } if(token_.toLower() == QLatin1String("dvd")) { entry_->setField(QStringLiteral("medium"), i18n("DVD")); res = true; } + if(token_.indexOf(QLatin1String("series"), 0, Qt::CaseInsensitive) > -1) { + entry_->setField(QStringLiteral("series"), token_); + res = true; + } static const QRegularExpression regionRx(QLatin1String("Region [1-9]")); QRegularExpressionMatch match = regionRx.match(token_); if(match.hasMatch()) { entry_->setField(QStringLiteral("region"), i18n(match.captured().toUtf8().constData())); res = true; } if(entry_->collection()->type() == Data::Collection::Game) { Data::FieldPtr f = entry_->collection()->fieldByName(QStringLiteral("platform")); if(f && f->allowed().contains(token_)) { res = true; } } return res; } //static QString AmazonFetcher::defaultName() { return i18n("Amazon.com Web Services"); } QString AmazonFetcher::defaultIcon() { return favIcon("http://www.amazon.com"); } Tellico::StringHash AmazonFetcher::allOptionalFields() { StringHash hash; hash[QStringLiteral("keyword")] = i18n("Keywords"); hash[QStringLiteral("amazon")] = i18n("Amazon Link"); return hash; } Tellico::Fetch::ConfigWidget* AmazonFetcher::configWidget(QWidget* parent_) const { return new AmazonFetcher::ConfigWidget(parent_, this); } AmazonFetcher::ConfigWidget::ConfigWidget(QWidget* parent_, const AmazonFetcher* fetcher_/*=0*/) : Fetch::ConfigWidget(parent_) { QGridLayout* l = new QGridLayout(optionsWidget()); l->setSpacing(4); l->setColumnStretch(1, 10); int row = -1; QLabel* al = new QLabel(i18n("Registration is required for accessing the %1 data source. " "If you agree to the terms and conditions, sign " "up for an account, and enter your information below.", AmazonFetcher::defaultName(), QLatin1String("https://affiliate-program.amazon.com/gp/flex/advertising/api/sign-in.html")), optionsWidget()); al->setOpenExternalLinks(true); al->setWordWrap(true); ++row; l->addWidget(al, row, 0, 1, 2); // richtext gets weird with size al->setMinimumWidth(al->sizeHint().width()); QLabel* label = new QLabel(i18n("Access key: "), optionsWidget()); l->addWidget(label, ++row, 0); m_accessEdit = new QLineEdit(optionsWidget()); connect(m_accessEdit, &QLineEdit::textChanged, this, &ConfigWidget::slotSetModified); l->addWidget(m_accessEdit, row, 1); QString w = i18n("Access to data from Amazon.com requires an AWS Access Key ID and a Secret Key."); label->setWhatsThis(w); m_accessEdit->setWhatsThis(w); label->setBuddy(m_accessEdit); label = new QLabel(i18n("Secret key: "), optionsWidget()); l->addWidget(label, ++row, 0); m_secretKeyEdit = new QLineEdit(optionsWidget()); // m_secretKeyEdit->setEchoMode(QLineEdit::PasswordEchoOnEdit); connect(m_secretKeyEdit, &QLineEdit::textChanged, this, &ConfigWidget::slotSetModified); l->addWidget(m_secretKeyEdit, row, 1); label->setWhatsThis(w); m_secretKeyEdit->setWhatsThis(w); label->setBuddy(m_secretKeyEdit); label = new QLabel(i18n("Country: "), optionsWidget()); l->addWidget(label, ++row, 0); m_siteCombo = new GUI::ComboBox(optionsWidget()); for(int i = 0; i < XX; ++i) { const AmazonFetcher::SiteData& siteData = AmazonFetcher::siteData(i); QIcon icon(QStandardPaths::locate(QStandardPaths::GenericDataLocation, QStringLiteral("kf5/locale/countries/%1/flag.png").arg(siteData.country))); m_siteCombo->addItem(icon, siteData.countryName, i); m_siteCombo->model()->sort(0); } void (GUI::ComboBox::* activatedInt)(int) = &GUI::ComboBox::activated; connect(m_siteCombo, activatedInt, this, &ConfigWidget::slotSetModified); connect(m_siteCombo, activatedInt, this, &ConfigWidget::slotSiteChanged); l->addWidget(m_siteCombo, row, 1); w = i18n("Amazon.com provides data from several different localized sites. Choose the one " "you wish to use for this data source."); label->setWhatsThis(w); m_siteCombo->setWhatsThis(w); label->setBuddy(m_siteCombo); label = new QLabel(i18n("&Image size: "), optionsWidget()); l->addWidget(label, ++row, 0); m_imageCombo = new GUI::ComboBox(optionsWidget()); m_imageCombo->addItem(i18n("Small Image"), SmallImage); m_imageCombo->addItem(i18n("Medium Image"), MediumImage); m_imageCombo->addItem(i18n("Large Image"), LargeImage); m_imageCombo->addItem(i18n("No Image"), NoImage); connect(m_imageCombo, activatedInt, this, &ConfigWidget::slotSetModified); l->addWidget(m_imageCombo, row, 1); w = i18n("The cover image may be downloaded as well. However, too many large images in the " "collection may degrade performance."); label->setWhatsThis(w); m_imageCombo->setWhatsThis(w); label->setBuddy(m_imageCombo); label = new QLabel(i18n("&Associate's ID: "), optionsWidget()); l->addWidget(label, ++row, 0); m_assocEdit = new QLineEdit(optionsWidget()); void (QLineEdit::* textChanged)(const QString&) = &QLineEdit::textChanged; connect(m_assocEdit, textChanged, this, &ConfigWidget::slotSetModified); l->addWidget(m_assocEdit, row, 1); w = i18n("The associate's id identifies the person accessing the Amazon.com Web Services, and is included " "in any links to the Amazon.com site."); label->setWhatsThis(w); m_assocEdit->setWhatsThis(w); label->setBuddy(m_assocEdit); l->setRowStretch(++row, 10); if(fetcher_) { m_siteCombo->setCurrentData(fetcher_->m_site); m_accessEdit->setText(fetcher_->m_accessKey); m_secretKeyEdit->setText(fetcher_->m_secretKey); m_assocEdit->setText(fetcher_->m_assoc); m_imageCombo->setCurrentData(fetcher_->m_imageSize); } else { // defaults m_assocEdit->setText(QLatin1String(AMAZON_ASSOC_TOKEN)); m_imageCombo->setCurrentData(MediumImage); } addFieldsWidget(AmazonFetcher::allOptionalFields(), fetcher_ ? fetcher_->optionalFields() : QStringList()); KAcceleratorManager::manage(optionsWidget()); } void AmazonFetcher::ConfigWidget::saveConfigHook(KConfigGroup& config_) { int n = m_siteCombo->currentData().toInt(); config_.writeEntry("Site", n); QString s = m_accessEdit->text().trimmed(); if(!s.isEmpty()) { config_.writeEntry("AccessKey", s); } s = m_secretKeyEdit->text().trimmed(); if(!s.isEmpty()) { config_.writeEntry("SecretKey", s); } s = m_assocEdit->text().trimmed(); if(!s.isEmpty()) { config_.writeEntry("AssocToken", s); } n = m_imageCombo->currentData().toInt(); config_.writeEntry("Image Size", n); } QString AmazonFetcher::ConfigWidget::preferredName() const { return AmazonFetcher::siteData(m_siteCombo->currentData().toInt()).title; } void AmazonFetcher::ConfigWidget::slotSiteChanged() { emit signalName(preferredName()); } diff --git a/src/tests/amazonfetchertest.cpp b/src/tests/amazonfetchertest.cpp index f40d1ed4..7c449ca9 100644 --- a/src/tests/amazonfetchertest.cpp +++ b/src/tests/amazonfetchertest.cpp @@ -1,559 +1,636 @@ /*************************************************************************** Copyright (C) 2010-2011 Robby Stephenson ***************************************************************************/ /*************************************************************************** * * * This program is free software; you can redistribute it and/or * * modify it under the terms of the GNU General Public License as * * published by the Free Software Foundation; either version 2 of * * the License or (at your option) version 3 or any later version * * accepted by the membership of KDE e.V. (or its successor approved * * by the membership of KDE e.V.), which shall act as a proxy * * defined in Section 14 of version 3 of the license. * * * * This program is distributed in the hope that it will be useful, * * but WITHOUT ANY WARRANTY; without even the implied warranty of * * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * * GNU General Public License for more details. * * * * You should have received a copy of the GNU General Public License * * along with this program. If not, see . * * * ***************************************************************************/ #undef QT_NO_CAST_FROM_ASCII #include "amazonfetchertest.h" #include "../fetch/amazonfetcher.h" #include "../fetch/amazonrequest.h" #include "../fetch/messagelogger.h" #include "../collections/bookcollection.h" #include "../collections/musiccollection.h" #include "../collections/videocollection.h" #include "../collections/gamecollection.h" #include "../collectionfactory.h" #include "../entry.h" #include "../images/imagefactory.h" #include #include QTEST_GUILESS_MAIN( AmazonFetcherTest ) AmazonFetcherTest::AmazonFetcherTest() : AbstractFetcherTest(), m_hasConfigFile(false) , m_config(QFINDTESTDATA("tellicotest_private.config"), KConfig::SimpleConfig) { } void AmazonFetcherTest::initTestCase() { Tellico::RegisterCollection registerBook(Tellico::Data::Collection::Book, "book"); Tellico::RegisterCollection registerMusic(Tellico::Data::Collection::Album, "music"); Tellico::RegisterCollection registerVideo(Tellico::Data::Collection::Video, "mvideo"); Tellico::RegisterCollection registerGame(Tellico::Data::Collection::Game, "game"); Tellico::ImageFactory::init(); m_hasConfigFile = QFile::exists(QFINDTESTDATA("tellicotest_private.config")); QHash practicalRdf; practicalRdf.insert(QStringLiteral("title"), QStringLiteral("Practical RDF")); practicalRdf.insert(QStringLiteral("isbn"), QStringLiteral("0-596-00263-7")); practicalRdf.insert(QStringLiteral("author"), QStringLiteral("Shelley Powers")); practicalRdf.insert(QStringLiteral("binding"), QStringLiteral("Paperback")); practicalRdf.insert(QStringLiteral("publisher"), QStringLiteral("O'Reilly Media")); practicalRdf.insert(QStringLiteral("pages"), QStringLiteral("331")); QHash gloryRevealed; gloryRevealed.insert(QStringLiteral("title"), QStringLiteral("Glory Revealed II")); gloryRevealed.insert(QStringLiteral("medium"), QStringLiteral("Compact Disc")); // gloryRevealed.insert(QStringLiteral("artist"), QStringLiteral("Various Artists")); gloryRevealed.insert(QStringLiteral("label"), QStringLiteral("Reunion")); gloryRevealed.insert(QStringLiteral("year"), QStringLiteral("2009")); QHash incredibles; incredibles.insert(QStringLiteral("title"), QStringLiteral("Incredibles")); incredibles.insert(QStringLiteral("medium"), QStringLiteral("DVD")); // incredibles.insert(QStringLiteral("certification"), QStringLiteral("PG (USA)")); // incredibles.insert(QStringLiteral("studio"), QStringLiteral("Walt Disney Home Entertainment")); // incredibles.insert(QStringLiteral("year"), QStringLiteral("2004")); incredibles.insert(QStringLiteral("widescreen"), QStringLiteral("true")); incredibles.insert(QStringLiteral("director"), QStringLiteral("Brad Bird; Bud Luckey; Roger Gould")); QHash pacteDesLoups; pacteDesLoups.insert(QStringLiteral("title"), QStringLiteral("Le Pacte des Loups")); pacteDesLoups.insert(QStringLiteral("medium"), QStringLiteral("Blu-ray")); // pacteDesLoups.insert(QStringLiteral("region"), QStringLiteral("Region 2")); pacteDesLoups.insert(QStringLiteral("studio"), QStringLiteral("StudioCanal")); pacteDesLoups.insert(QStringLiteral("year"), QStringLiteral("2001")); pacteDesLoups.insert(QStringLiteral("director"), QStringLiteral("Christophe Gans")); // pacteDesLoups.insert(QStringLiteral("format"), QStringLiteral("PAL")); QHash petitPrinceCN; petitPrinceCN.insert(QStringLiteral("title"), QStringLiteral("小王子(65周年纪念版)")); petitPrinceCN.insert(QStringLiteral("author"), QStringLiteral("圣埃克絮佩里 (Saint-Exupery)")); m_fieldValues.insert(QStringLiteral("practicalRdf"), practicalRdf); m_fieldValues.insert(QStringLiteral("gloryRevealed"), gloryRevealed); m_fieldValues.insert(QStringLiteral("incredibles"), incredibles); m_fieldValues.insert(QStringLiteral("pacteDesLoups"), pacteDesLoups); m_fieldValues.insert(QStringLiteral("petitPrinceCN"), petitPrinceCN); } void AmazonFetcherTest::testTitle() { QFETCH(QString, locale); QFETCH(int, collType); QFETCH(QString, searchValue); QFETCH(QString, resultName); QString groupName = QStringLiteral("Amazon ") + locale; if(!m_hasConfigFile || !m_config.hasGroup(groupName)) { QSKIP("This test requires a config file with Amazon settings.", SkipAll); } KConfigGroup cg(&m_config, groupName); Tellico::Fetch::FetchRequest request(collType, Tellico::Fetch::Title, searchValue); Tellico::Fetch::Fetcher::Ptr fetcher(new Tellico::Fetch::AmazonFetcher(this)); fetcher->readConfig(cg, cg.name()); Tellico::Data::EntryList results = DO_FETCH(fetcher, request); QVERIFY(!results.isEmpty()); Tellico::Data::EntryPtr entry = results.at(0); QHashIterator i(m_fieldValues.value(resultName)); while(i.hasNext()) { i.next(); // a known bug is CA video titles result in music results, so only title matches if(i.key() != QStringLiteral("title")) { QEXPECT_FAIL("CA video title", "CA video titles show music results for some reason", Continue); } QString result = entry->field(i.key()).toLower(); // several titles have edition info in the title if(collType == Tellico::Data::Collection::Video && i.key() == QStringLiteral("title") && (locale == QStringLiteral("CA") || locale == QStringLiteral("FR") || locale == QStringLiteral("ES") || locale == QStringLiteral("CN") || locale == QStringLiteral("IT") || locale == QStringLiteral("DE"))) { QVERIFY2(result.contains(i.value(), Qt::CaseInsensitive), qPrintable(i.key())); } else if(collType == Tellico::Data::Collection::Video && i.key() == QStringLiteral("year") && locale == QStringLiteral("FR")) { // france has no year for movie QCOMPARE(result, QString()); } else if(collType == Tellico::Data::Collection::Video && i.key() == QStringLiteral("medium") && (locale == QStringLiteral("ES") || locale == QStringLiteral("IT"))) { // ES and IT think it's a DVD QCOMPARE(result, QStringLiteral("dvd")); } else if(i.key() == QStringLiteral("pages") && (locale == QStringLiteral("UK") || locale == QStringLiteral("CA"))) { // UK and CA have different page count QCOMPARE(result, QStringLiteral("352")); } else if((i.key() == QStringLiteral("director") || i.key() == QStringLiteral("studio") || i.key() == QStringLiteral("year")) && (locale == QStringLiteral("ES") || locale == QStringLiteral("IT"))) { // ES and IT have no director or studio or year info QCOMPARE(result, QString()); } else { QCOMPARE(result, i.value().toLower()); } } QVERIFY(!entry->field(QStringLiteral("cover")).isEmpty()); QVERIFY(!entry->field(QStringLiteral("cover")).contains(QLatin1Char('/'))); } void AmazonFetcherTest::testTitle_data() { QTest::addColumn("locale"); QTest::addColumn("collType"); QTest::addColumn("searchValue"); QTest::addColumn("resultName"); QTest::newRow("US book title") << QStringLiteral("US") << static_cast(Tellico::Data::Collection::Book) << QStringLiteral("Practical RDF") << QStringLiteral("practicalRdf"); QTest::newRow("UK book title") << QStringLiteral("UK") << static_cast(Tellico::Data::Collection::Book) << QStringLiteral("Practical RDF") << QStringLiteral("practicalRdf"); // QTest::newRow("DE") << QString::fromLatin1("DE"); // QTest::newRow("JP") << QString::fromLatin1("JP"); // QTest::newRow("FR") << QString::fromLatin1("FR"); QTest::newRow("CA book title") << QStringLiteral("CA") << static_cast(Tellico::Data::Collection::Book) << QStringLiteral("Practical RDF") << QStringLiteral("practicalRdf"); QTest::newRow("CN book title") << QStringLiteral("CN") << static_cast(Tellico::Data::Collection::Book) << QStringLiteral("小王子(65周年纪念版)") << QStringLiteral("petitPrinceCN"); // a known bug is CA video titles result in music results, so only title matches // QTest::newRow("CA video title") << QString::fromLatin1("CA") // << static_cast(Tellico::Data::Collection::Video) // << QString::fromLatin1("Le Pacte des Loups") // << QString::fromLatin1("pacteDesLoups"); QTest::newRow("FR video title") << QStringLiteral("FR") << static_cast(Tellico::Data::Collection::Video) << QStringLiteral("Le Pacte des Loups") << QStringLiteral("pacteDesLoups"); QTest::newRow("ES video title") << QStringLiteral("ES") << static_cast(Tellico::Data::Collection::Video) << QStringLiteral("Le Pacte des Loups") << QStringLiteral("pacteDesLoups"); QTest::newRow("IT video title") << QStringLiteral("IT") << static_cast(Tellico::Data::Collection::Video) << QStringLiteral("Le Pacte des Loups") << QStringLiteral("pacteDesLoups"); } void AmazonFetcherTest::testTitleVideoGame() { QString groupName = QStringLiteral("Amazon US"); if(!m_hasConfigFile || !m_config.hasGroup(groupName)) { QSKIP("This test requires a config file with Amazon settings.", SkipAll); } KConfigGroup cg(&m_config, groupName); Tellico::Fetch::FetchRequest request(Tellico::Data::Collection::Game, Tellico::Fetch::Title, QStringLiteral("Ghostbusters Story Pack - LEGO Dimensions")); Tellico::Fetch::Fetcher::Ptr fetcher(new Tellico::Fetch::AmazonFetcher(this)); fetcher->readConfig(cg, cg.name()); Tellico::Data::EntryList results = DO_FETCH(fetcher, request); QVERIFY(!results.isEmpty()); Tellico::Data::EntryPtr entry = results.at(0); QVERIFY(entry); QCOMPARE(entry->field("title"), QStringLiteral("Ghostbusters Story Pack - LEGO Dimensions")); QCOMPARE(entry->field("publisher"), QStringLiteral("Warner Home Video - Games")); // the E10+ ESRB rating was added to Tellico in 2017 in version 3.0.1 QCOMPARE(entry->field("certification"), QStringLiteral("Everyone 10+")); } void AmazonFetcherTest::testIsbn() { QFETCH(QString, locale); QFETCH(QString, searchValue); QFETCH(QString, resultName); QString groupName = QStringLiteral("Amazon ") + locale; if(!m_hasConfigFile || !m_config.hasGroup(groupName)) { QSKIP("This test requires a config file with Amazon settings.", SkipAll); } KConfigGroup cg(&m_config, groupName); // also testing multiple values Tellico::Fetch::FetchRequest request(Tellico::Data::Collection::Book, Tellico::Fetch::ISBN, searchValue); Tellico::Fetch::Fetcher::Ptr fetcher(new Tellico::Fetch::AmazonFetcher(this)); fetcher->readConfig(cg, cg.name()); Tellico::Data::EntryList results = DO_FETCH(fetcher, request); QCOMPARE(results.size(), 2); Tellico::Data::EntryPtr entry = results.at(0); QHashIterator i(m_fieldValues.value(resultName)); while(i.hasNext()) { i.next(); QString result = entry->field(i.key()).toLower(); if(i.key() == QStringLiteral("pages") && (locale == QStringLiteral("UK") || locale == QStringLiteral("CA"))) { // UK and CA have different page count QCOMPARE(result, QStringLiteral("352")); } else { QCOMPARE(result, i.value().toLower()); } } QVERIFY(!entry->field(QStringLiteral("cover")).isEmpty()); QVERIFY(!entry->field(QStringLiteral("cover")).contains(QLatin1Char('/'))); } void AmazonFetcherTest::testIsbn_data() { QTest::addColumn("locale"); QTest::addColumn("searchValue"); QTest::addColumn("resultName"); QTest::newRow("US isbn") << QStringLiteral("US") << QStringLiteral("0-596-00263-7; 978-1-59059-831-3") << QStringLiteral("practicalRdf"); QTest::newRow("UK isbn") << QStringLiteral("UK") << QStringLiteral("0-596-00263-7; 978-1-59059-831-3") << QStringLiteral("practicalRdf"); // QTest::newRow("DE") << QString::fromLatin1("DE"); // QTest::newRow("JP") << QString::fromLatin1("JP"); // QTest::newRow("FR") << QString::fromLatin1("FR"); QTest::newRow("CA isbn") << QStringLiteral("CA") << QStringLiteral("0-596-00263-7; 978-1-59059-831-3") << QStringLiteral("practicalRdf"); /* QTest::newRow("CN isbn") << QString::fromLatin1("CN") << QString::fromLatin1("7511305202") << QString::fromLatin1("petitPrinceCN"); */ } void AmazonFetcherTest::testUpc() { QFETCH(QString, locale); QFETCH(int, collType); QFETCH(QString, searchValue); QFETCH(QString, resultName); QString groupName = QStringLiteral("Amazon ") + locale; if(!m_hasConfigFile || !m_config.hasGroup(groupName)) { QSKIP("This test requires a config file with Amazon settings.", SkipAll); } KConfigGroup cg(&m_config, groupName); Tellico::Fetch::FetchRequest request(collType, Tellico::Fetch::UPC, searchValue); Tellico::Fetch::Fetcher::Ptr fetcher(new Tellico::Fetch::AmazonFetcher(this)); fetcher->readConfig(cg, cg.name()); Tellico::Data::EntryList results = DO_FETCH(fetcher, request); QVERIFY(!results.isEmpty()); Tellico::Data::EntryPtr entry = results.at(0); QHashIterator i(m_fieldValues.value(resultName)); while(i.hasNext()) { i.next(); QString result = entry->field(i.key()).toLower(); // exception for UK label different than US // and FR title having edition info if((i.key() == QStringLiteral("label") && locale == QStringLiteral("UK")) || (i.key() == QStringLiteral("title"))) { QVERIFY(result.contains(i.value(), Qt::CaseInsensitive)); } else if(i.key() == QStringLiteral("year") && locale == QStringLiteral("FR")) { // france has no year for movie QCOMPARE(result, QString()); } else { QCOMPARE(result, i.value().toLower()); } } QVERIFY(!entry->field(QStringLiteral("cover")).isEmpty()); QVERIFY(!entry->field(QStringLiteral("cover")).contains(QLatin1Char('/'))); if(collType == Tellico::Data::Collection::Album) { QVERIFY(!entry->field(QStringLiteral("genre")).isEmpty()); QVERIFY(!entry->field(QStringLiteral("track")).isEmpty()); } else if(collType == Tellico::Data::Collection::Video) { QVERIFY(!entry->field(QStringLiteral("cast")).isEmpty()); } } void AmazonFetcherTest::testUpc_data() { QTest::addColumn("locale"); QTest::addColumn("collType"); QTest::addColumn("searchValue"); QTest::addColumn("resultName"); QTest::newRow("US music upc") << QStringLiteral("US") << static_cast(Tellico::Data::Collection::Album) << QStringLiteral("602341013727") << QStringLiteral("gloryRevealed"); // non-US should work with or without the initial 0 country code QTest::newRow("UK music upc1") << QStringLiteral("UK") << static_cast(Tellico::Data::Collection::Album) << QStringLiteral("602341013727") << QStringLiteral("gloryRevealed"); QTest::newRow("UK music upc2") << QStringLiteral("UK") << static_cast(Tellico::Data::Collection::Album) << QStringLiteral("0602341013727") << QStringLiteral("gloryRevealed"); QTest::newRow("CA music upc") << QStringLiteral("CA") << static_cast(Tellico::Data::Collection::Album) << QStringLiteral("0602341013727") << QStringLiteral("gloryRevealed"); QTest::newRow("US movie upc") << QStringLiteral("US") << static_cast(Tellico::Data::Collection::Video) << QStringLiteral("786936244250") << QStringLiteral("incredibles"); QTest::newRow("UK movie upc") << QStringLiteral("UK") << static_cast(Tellico::Data::Collection::Video) << QStringLiteral("0786936244250") << QStringLiteral("incredibles"); QTest::newRow("CA movie upc") << QStringLiteral("CA") << static_cast(Tellico::Data::Collection::Video) << QStringLiteral("0786936244250") << QStringLiteral("incredibles"); QTest::newRow("FR movie upc") << QStringLiteral("FR") << static_cast(Tellico::Data::Collection::Video) << QStringLiteral("5050582560985") << QStringLiteral("pacteDesLoups"); } void AmazonFetcherTest::testRequest() { // from aws-sig-v4-test-suite/post-vanilla Tellico::Fetch::AmazonRequest req("AKIDEXAMPLE", "wJalrXUtnFEMI/K7MDENG+bPxRfiCYEXAMPLEKEY"); req.setHost("example.amazonaws.com"); req.m_headers.insert("host", req.m_host); req.m_amzDate = "20150830T123600Z"; req.m_headers.insert("x-amz-date", req.m_amzDate); req.m_path = "/"; QByteArray res1("POST\n/\n\nhost:example.amazonaws.com\nx-amz-date:20150830T123600Z\n\nhost;x-amz-date\ne3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"); QCOMPARE(req.prepareCanonicalRequest(""), res1); req.m_region = "us-east-1"; req.m_service = "service"; QByteArray res2("AWS4-HMAC-SHA256\n20150830T123600Z\n20150830/us-east-1/service/aws4_request\n553f88c9e4d10fc9e109e2aeb65f030801b70c2f6468faca261d401ae622fc87"); QCOMPARE(req.prepareStringToSign(res1), res2); QByteArray res3("AWS4-HMAC-SHA256 Credential=AKIDEXAMPLE/20150830/us-east-1/service/aws4_request, SignedHeaders=host;x-amz-date, Signature=5da7c1a2acd57cee7505fc6676e4e544621c30862966e37dddb68e92efbe5d6b"); QCOMPARE(req.buildAuthorizationString(req.calculateSignature(res2)), res3); QByteArray res4("com.amazon.paapi5.v1.servicev1.SearchItems"); QCOMPARE(req.targetOperation(), res4); } void AmazonFetcherTest::testPayload() { QString groupName = QStringLiteral("Amazon US"); if(!m_hasConfigFile || !m_config.hasGroup(groupName)) { QSKIP("This test requires a config file with Amazon settings.", SkipAll); } KConfigGroup cg(&m_config, groupName); Tellico::Fetch::AmazonFetcher* fetcher = new Tellico::Fetch::AmazonFetcher(this); fetcher->readConfig(cg, cg.name()); Tellico::Fetch::FetchRequest req(Tellico::Data::Collection::Book, Tellico::Fetch::UPC, "717356278525"); QByteArray payload = fetcher->requestPayload(req); QByteArray res1("{\ \"Keywords\":\"717356278525\",\ \"Operation\":\"SearchItems\",\ \"PartnerTag\":\"tellico-20\",\ \"PartnerType\":\"Associates\",\ \"Resources\":[\"ItemInfo.Title\",\"ItemInfo.ExternalIds\",\"Images.Primary.Medium\"],\ \"SearchIndex\":\"Books\"\ }"); QCOMPARE(payload, res1); } void AmazonFetcherTest::testError() { Tellico::Fetch::FetchRequest request(Tellico::Data::Collection::Book, Tellico::Fetch::UPC, "717356278525"); Tellico::Fetch::AmazonFetcher* f = new Tellico::Fetch::AmazonFetcher(this); Tellico::Fetch::Fetcher::Ptr fetcher(f); Tellico::Fetch::MessageLogger* logger = new Tellico::Fetch::MessageLogger; f->setMessageHandler(logger); f->m_site = Tellico::Fetch::AmazonFetcher::US; f->m_accessKey = QStringLiteral("test"); f->m_secretKey = QStringLiteral("test"); f->m_testResultsFile = QFINDTESTDATA("data/amazon-paapi-error1.json"); Tellico::Data::EntryList results = DO_FETCH1(fetcher, request, 1); QVERIFY(!logger->errorList.isEmpty()); QCOMPARE(logger->errorList[0], QStringLiteral("The Access Key ID or security token included in the request is invalid.")); } void AmazonFetcherTest::testUpc1() { QString groupName = QStringLiteral("Amazon US"); if(!m_hasConfigFile || !m_config.hasGroup(groupName)) { QSKIP("This test requires a config file with Amazon settings.", SkipAll); } KConfigGroup cg(&m_config, groupName); Tellico::Fetch::FetchRequest request(Tellico::Data::Collection::Book, Tellico::Fetch::UPC, "717356278525"); Tellico::Fetch::AmazonFetcher* f = new Tellico::Fetch::AmazonFetcher(this); Tellico::Fetch::Fetcher::Ptr fetcher(f); fetcher->readConfig(cg, cg.name()); f->m_testResultsFile = QFINDTESTDATA("data/amazon-paapi-upc1.json"); Tellico::Data::EntryList results = DO_FETCH(fetcher, request); QCOMPARE(results.size(), 1); Tellico::Data::EntryPtr entry = results.at(0); QVERIFY(entry); QCOMPARE(entry->field("title"), QStringLiteral("Harry Potter Paperback Box Set (Books 1-7)")); QCOMPARE(entry->field("isbn"), QStringLiteral("0545162076")); QVERIFY(!entry->field(QStringLiteral("cover")).isEmpty()); QVERIFY(!entry->field(QStringLiteral("cover")).contains(QLatin1Char('/'))); } void AmazonFetcherTest::testUpc2() { QString groupName = QStringLiteral("Amazon US"); if(!m_hasConfigFile || !m_config.hasGroup(groupName)) { QSKIP("This test requires a config file with Amazon settings.", SkipAll); } KConfigGroup cg(&m_config, groupName); Tellico::Fetch::FetchRequest request(Tellico::Data::Collection::Book, Tellico::Fetch::UPC, "717356278525; 842776102270"); Tellico::Fetch::AmazonFetcher* f = new Tellico::Fetch::AmazonFetcher(this); Tellico::Fetch::Fetcher::Ptr fetcher(f); fetcher->readConfig(cg, cg.name()); QByteArray payload = f->requestPayload(request); // verify the format of the multiple UPC keyword QVERIFY(payload.contains("\"Keywords\":\"717356278525|842776102270\"")); f->m_testResultsFile = QFINDTESTDATA("data/amazon-paapi-upc2.json"); Tellico::Data::EntryList results = DO_FETCH(fetcher, request); QCOMPARE(results.size(), 2); Tellico::Data::EntryPtr entry = results.at(0); QVERIFY(entry); QCOMPARE(entry->field("title"), QStringLiteral("Harry Potter Paperback Box Set (Books 1-7)")); QCOMPARE(entry->field("isbn"), QStringLiteral("0545162076")); QVERIFY(!entry->field(QStringLiteral("cover")).isEmpty()); QVERIFY(!entry->field(QStringLiteral("cover")).contains(QLatin1Char('/'))); } // from https://github.com/dkam/paapi/blob/master/test/data/get_item_no_author.json void AmazonFetcherTest::testBasicBook() { QString groupName = QStringLiteral("Amazon UK"); if(!m_hasConfigFile || !m_config.hasGroup(groupName)) { QSKIP("This test requires a config file with Amazon settings.", SkipAll); } KConfigGroup cg(&m_config, groupName); Tellico::Fetch::FetchRequest request(Tellico::Data::Collection::Book, Tellico::Fetch::ISBN, "1921878657"); Tellico::Fetch::AmazonFetcher* f = new Tellico::Fetch::AmazonFetcher(this); Tellico::Fetch::Fetcher::Ptr fetcher(f); fetcher->readConfig(cg, cg.name()); f->m_testResultsFile = QFINDTESTDATA("data/amazon-paapi-book.json"); Tellico::Data::EntryList results = DO_FETCH(fetcher, request); QCOMPARE(results.size(), 1); Tellico::Data::EntryPtr entry = results.at(0); QVERIFY(entry); - QCOMPARE(entry->field("title"), QStringLiteral("Muscle Car Mania: 100 legendary Australian motoring stories (Motoring Series)")); + QCOMPARE(entry->field("title"), QStringLiteral("Muscle Car Mania: 100 legendary Australian motoring stories")); QCOMPARE(entry->field("author"), QStringLiteral("No Author")); QCOMPARE(entry->field("publisher"), QStringLiteral("Rockpool Publishing")); QCOMPARE(entry->field("edition"), QStringLiteral("Slp")); QCOMPARE(entry->field("binding"), QStringLiteral("Paperback")); + QCOMPARE(entry->field("series"), QStringLiteral("Motoring Series")); QCOMPARE(entry->field("isbn"), QStringLiteral("1921878657")); QCOMPARE(entry->field("pages"), QStringLiteral("224")); QCOMPARE(entry->field("language"), QStringLiteral("English")); QCOMPARE(entry->field("pub_year"), QStringLiteral("2013")); QCOMPARE(entry->field("amazon"), QStringLiteral("https://www.amazon.com/dp/1921878657?tag=bookie09-20&linkCode=ogi&th=1&psc=1")); QVERIFY(entry->field(QStringLiteral("cover")).isEmpty()); // because image size as NoImage } void AmazonFetcherTest::testTitleParsing() { Tellico::Data::CollPtr coll(new Tellico::Data::VideoCollection(true)); Tellico::Data::EntryPtr entry(new Tellico::Data::Entry(coll)); entry->setField(QStringLiteral("title"), QStringLiteral("title1 [DVD] (Widescreen) (NTSC) [Region 1]")); Tellico::Fetch::AmazonFetcher* f = new Tellico::Fetch::AmazonFetcher(this); Tellico::Fetch::Fetcher::Ptr fetcher(f); f->parseTitle(entry); // the fetcher leaves widescreen in the title but adds the field value QCOMPARE(entry->field("title"), QStringLiteral("title1 (Widescreen)")); QCOMPARE(entry->field("medium"), QStringLiteral("DVD")); QCOMPARE(entry->field("widescreen"), QStringLiteral("true")); QCOMPARE(entry->field("format"), QStringLiteral("NTSC")); QCOMPARE(entry->field("region"), QStringLiteral("Region 1")); } + +// from https://github.com/utekaravinash/gopaapi5/blob/master/_response/search_items.json +void AmazonFetcherTest::testSearchItems_gopaapi5() { + QString groupName = QStringLiteral("Amazon UK"); + if(!m_hasConfigFile || !m_config.hasGroup(groupName)) { + QSKIP("This test requires a config file with Amazon settings.", SkipAll); + } + KConfigGroup cg(&m_config, groupName); + + Tellico::Fetch::FetchRequest request(Tellico::Data::Collection::Book, Tellico::Fetch::ISBN, "1921878657"); + Tellico::Fetch::AmazonFetcher* f = new Tellico::Fetch::AmazonFetcher(this); + Tellico::Fetch::Fetcher::Ptr fetcher(f); + fetcher->readConfig(cg, cg.name()); + + f->m_testResultsFile = QFINDTESTDATA("data/amazon-paapi-search-items-gopaapi5.json"); + + Tellico::Data::EntryList results = DO_FETCH(fetcher, request); + + QCOMPARE(results.size(), 3); + + Tellico::Data::EntryPtr entry = results.at(0); + QVERIFY(entry); + QCOMPARE(entry->field("title"), QStringLiteral("Go Programming Language, The")); + QCOMPARE(entry->field("author"), QStringLiteral("Donovan, Alan A. A.")); + QCOMPARE(entry->field("publisher"), QStringLiteral("Addison-Wesley Professional")); + QCOMPARE(entry->field("edition"), QStringLiteral("1")); + QCOMPARE(entry->field("binding"), QStringLiteral("Paperback")); + QCOMPARE(entry->field("series"), QStringLiteral("Addison-Wesley Professional Computing Series")); + QCOMPARE(entry->field("isbn"), QStringLiteral("0134190440")); + QCOMPARE(entry->field("pages"), QStringLiteral("398")); + QCOMPARE(entry->field("language"), QStringLiteral("English")); + QCOMPARE(entry->field("pub_year"), QStringLiteral("2015")); + QCOMPARE(entry->field("amazon"), QStringLiteral("https://www.amazon.com/dp/0134190440?tag=associateTag-20&linkCode=osi&th=1&psc=1")); + QVERIFY(entry->field(QStringLiteral("cover")).isEmpty()); // because image size as NoImage + + entry = results.at(2); + QVERIFY(entry); + QCOMPARE(entry->field("title"), QStringLiteral("Black Hat Go: Go Programming For Hackers and Pentesters")); + QCOMPARE(entry->field("author"), QStringLiteral("Steele, Tom; Patten, Chris; Kottmann, Dan")); +} + +// from https://github.com/utekaravinash/gopaapi5/blob/master/_response/get_items.json +void AmazonFetcherTest::testGetItems_gopaapi5() { + QString groupName = QStringLiteral("Amazon UK"); + if(!m_hasConfigFile || !m_config.hasGroup(groupName)) { + QSKIP("This test requires a config file with Amazon settings.", SkipAll); + } + KConfigGroup cg(&m_config, groupName); + + Tellico::Fetch::FetchRequest request(Tellico::Data::Collection::Book, Tellico::Fetch::ISBN, "1921878657"); + Tellico::Fetch::AmazonFetcher* f = new Tellico::Fetch::AmazonFetcher(this); + Tellico::Fetch::Fetcher::Ptr fetcher(f); + fetcher->readConfig(cg, cg.name()); + + f->m_testResultsFile = QFINDTESTDATA("data/amazon-paapi-get-items-gopaapi5.json"); + + Tellico::Data::EntryList results = DO_FETCH(fetcher, request); + + QCOMPARE(results.size(), 2); + + Tellico::Data::EntryPtr entry = results.at(0); + QVERIFY(entry); + QVERIFY(entry->collection()); + QCOMPARE(entry->collection()->type(), Tellico::Data::Collection::Book); + QCOMPARE(entry->field("title"), QStringLiteral("Light on Yoga: The Bible of Modern Yoga")); + QCOMPARE(entry->field("author"), QStringLiteral("B. K. S. Iyengar")); + QCOMPARE(entry->field("publisher"), QStringLiteral("Schocken")); + QCOMPARE(entry->field("edition"), QStringLiteral("Revised")); + QCOMPARE(entry->field("binding"), QStringLiteral("Paperback")); + QCOMPARE(entry->field("isbn"), QStringLiteral("0805210318")); + QCOMPARE(entry->field("pages"), QStringLiteral("544")); + QCOMPARE(entry->field("language"), QStringLiteral("English")); + QCOMPARE(entry->field("pub_year"), QStringLiteral("1979")); // it's a 1995 revised edition of a 1979 publication apparently + QCOMPARE(entry->field("amazon"), QStringLiteral("https://www.amazon.com/dp/0805210318?tag=associateTag-20&linkCode=ogi&th=1&psc=1")); + QVERIFY(entry->field(QStringLiteral("cover")).isEmpty()); // because image size as NoImage +} diff --git a/src/tests/amazonfetchertest.h b/src/tests/amazonfetchertest.h index 41529032..dc7148bc 100644 --- a/src/tests/amazonfetchertest.h +++ b/src/tests/amazonfetchertest.h @@ -1,60 +1,62 @@ /*************************************************************************** Copyright (C) 2010-2011 Robby Stephenson ***************************************************************************/ /*************************************************************************** * * * This program is free software; you can redistribute it and/or * * modify it under the terms of the GNU General Public License as * * published by the Free Software Foundation; either version 2 of * * the License or (at your option) version 3 or any later version * * accepted by the membership of KDE e.V. (or its successor approved * * by the membership of KDE e.V.), which shall act as a proxy * * defined in Section 14 of version 3 of the license. * * * * This program is distributed in the hope that it will be useful, * * but WITHOUT ANY WARRANTY; without even the implied warranty of * * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * * GNU General Public License for more details. * * * * You should have received a copy of the GNU General Public License * * along with this program. If not, see . * * * ***************************************************************************/ #ifndef AMAZONFETCHERTEST_H #define AMAZONFETCHERTEST_H #include "abstractfetchertest.h" #include class AmazonFetcherTest : public AbstractFetcherTest { Q_OBJECT public: AmazonFetcherTest(); private Q_SLOTS: void initTestCase(); void testTitle(); void testTitle_data(); void testTitleVideoGame(); void testIsbn(); void testIsbn_data(); void testUpc(); void testUpc_data(); void testRequest(); void testPayload(); void testError(); void testUpc1(); void testUpc2(); void testBasicBook(); void testTitleParsing(); + void testSearchItems_gopaapi5(); + void testGetItems_gopaapi5(); private: bool m_hasConfigFile; KConfig m_config; QHash > m_fieldValues; }; #endif diff --git a/src/tests/data/amazon-paapi-get-items-gopaapi5.json b/src/tests/data/amazon-paapi-get-items-gopaapi5.json new file mode 100644 index 00000000..3830bd27 --- /dev/null +++ b/src/tests/data/amazon-paapi-get-items-gopaapi5.json @@ -0,0 +1,717 @@ +{ + "ItemsResult": { + "Items": [ + { + "ASIN": "0805210318", + "BrowseNodeInfo": { + "BrowseNodes": [ + { + "Ancestor": { + "Ancestor": { + "Ancestor": { + "Ancestor": { + "ContextFreeName": "Books", + "DisplayName": "Books", + "Id": "283155" + }, + "ContextFreeName": "Subjects", + "DisplayName": "Subjects", + "Id": "1000" + }, + "ContextFreeName": "Health, Fitness & Dieting", + "DisplayName": "Health, Fitness & Dieting", + "Id": "10" + }, + "ContextFreeName": "Alternative Medicine", + "DisplayName": "Alternative Medicine", + "Id": "4696" + }, + "ContextFreeName": "Meditation", + "DisplayName": "Meditation", + "Id": "4713", + "IsRoot": false, + "SalesRank": 30 + }, + { + "Ancestor": { + "Ancestor": { + "Ancestor": { + "Ancestor": { + "ContextFreeName": "Books", + "DisplayName": "Books", + "Id": "283155" + }, + "ContextFreeName": "Subjects", + "DisplayName": "Subjects", + "Id": "1000" + }, + "ContextFreeName": "Health, Fitness & Dieting", + "DisplayName": "Health, Fitness & Dieting", + "Id": "10" + }, + "ContextFreeName": "Exercise & Fitness", + "DisplayName": "Exercise & Fitness", + "Id": "4645" + }, + "ContextFreeName": "Yoga", + "DisplayName": "Yoga", + "Id": "4653", + "IsRoot": false, + "SalesRank": 5 + }, + { + "Ancestor": { + "Ancestor": { + "Ancestor": { + "Ancestor": { + "ContextFreeName": "Books", + "DisplayName": "Books", + "Id": "283155" + }, + "ContextFreeName": "Subjects", + "DisplayName": "Subjects", + "Id": "1000" + }, + "ContextFreeName": "Religion & Spirituality", + "DisplayName": "Religion & Spirituality", + "Id": "22" + }, + "ContextFreeName": "New Age & Spirituality", + "DisplayName": "New Age & Spirituality", + "Id": "12621" + }, + "ContextFreeName": "Chakras", + "DisplayName": "Chakras", + "Id": "12631", + "IsRoot": false, + "SalesRank": 10 + } + ], + "WebsiteSalesRank": { + "ContextFreeName": "Books", + "DisplayName": "Books", + "SalesRank": 1565 + } + }, + "DetailPageURL": "https://www.amazon.com/dp/0805210318?tag=associateTag-20&linkCode=ogi&th=1&psc=1", + "Images": { + "Primary": { + "Large": { + "Height": 500, + "URL": "https://m.media-amazon.com/images/I/41CnWolqViL.jpg", + "Width": 324 + }, + "Medium": { + "Height": 160, + "URL": "https://m.media-amazon.com/images/I/41CnWolqViL._SL160_.jpg", + "Width": 104 + }, + "Small": { + "Height": 75, + "URL": "https://m.media-amazon.com/images/I/41CnWolqViL._SL75_.jpg", + "Width": 49 + } + }, + "Variants": [ + { + "Large": { + "Height": 500, + "URL": "https://m.media-amazon.com/images/I/31vJLdB77mL.jpg", + "Width": 375 + }, + "Medium": { + "Height": 160, + "URL": "https://m.media-amazon.com/images/I/31vJLdB77mL._SL160_.jpg", + "Width": 120 + }, + "Small": { + "Height": 75, + "URL": "https://m.media-amazon.com/images/I/31vJLdB77mL._SL75_.jpg", + "Width": 56 + } + } + ] + }, + "ItemInfo": { + "ByLineInfo": { + "Brand": { + "DisplayValue": "Schocken", + "Label": "Brand", + "Locale": "en_US" + }, + "Contributors": [ + { + "Locale": "en_US", + "Name": "B. K. S. Iyengar", + "Role": "Author", + "RoleType": "author" + }, + { + "Locale": "en_US", + "Name": "Yehudi Menuhin", + "Role": "Foreword", + "RoleType": "foreword" + } + ], + "Manufacturer": { + "DisplayValue": "Schocken", + "Label": "Manufacturer", + "Locale": "en_US" + } + }, + "Classifications": { + "Binding": { + "DisplayValue": "Paperback", + "Label": "Binding", + "Locale": "en_US" + }, + "ProductGroup": { + "DisplayValue": "Book", + "Label": "ProductGroup", + "Locale": "en_US" + } + }, + "ContentInfo": { + "Edition": { + "DisplayValue": "Revised", + "Label": "Edition", + "Locale": "en_US" + }, + "Languages": { + "DisplayValues": [ + { + "DisplayValue": "English", + "Type": "Published" + }, + { + "DisplayValue": "English", + "Type": "Original Language" + }, + { + "DisplayValue": "English", + "Type": "Unknown" + } + ], + "Label": "Language", + "Locale": "en_US" + }, + "PagesCount": { + "DisplayValue": 544, + "Label": "NumberOfPages", + "Locale": "en_US" + }, + "PublicationDate": { + "DisplayValue": "1979T", + "Label": "PublicationDate", + "Locale": "en_US" + } + }, + "ExternalIds": { + "EANs": { + "DisplayValues": [ + "8580001044736", + "8601404412103", + "8601300502786", + "9780805210316" + ], + "Label": "EAN", + "Locale": "en_US" + }, + "ISBNs": { + "DisplayValues": [ + "0805210318", + "9780805210316" + ], + "Label": "ISBN", + "Locale": "en_US" + } + }, + "Features": { + "DisplayValues": [ + "With complete descriptions and illustations of all the postures and breathing techniques." + ], + "Label": "Features", + "Locale": "en_US" + }, + "ManufactureInfo": { + "ItemPartNumber": { + "DisplayValue": "9780805210316", + "Label": "PartNumber", + "Locale": "en_US" + } + }, + "ProductInfo": { + "Color": { + "DisplayValue": "White", + "Label": "Color", + "Locale": "en_US" + }, + "IsAdultProduct": { + "DisplayValue": false, + "Label": "IsAdultProduct", + "Locale": "en_US" + }, + "ItemDimensions": { + "Height": { + "DisplayValue": 7.93, + "Label": "Height", + "Locale": "en_US", + "Unit": "Inches" + }, + "Length": { + "DisplayValue": 5.15, + "Label": "Length", + "Locale": "en_US", + "Unit": "Inches" + }, + "Weight": { + "DisplayValue": 1.10010668738, + "Label": "Weight", + "Locale": "en_US", + "Unit": "Pounds" + }, + "Width": { + "DisplayValue": 1.06, + "Label": "Width", + "Locale": "en_US", + "Unit": "Inches" + } + }, + "ReleaseDate": { + "DisplayValue": "1995-01-03T00:00:01Z", + "Label": "ReleaseDate", + "Locale": "en_US" + }, + "UnitCount": { + "DisplayValue": 1, + "Label": "NumberOfItems", + "Locale": "en_US" + } + }, + "Title": { + "DisplayValue": "Light on Yoga: The Bible of Modern Yoga", + "Label": "Title", + "Locale": "en_US" + } + }, + "Offers": { + "Listings": [ + { + "Availability": { + "Message": "In Stock.", + "MinOrderQuantity": 1, + "Type": "Now" + }, + "Condition": { + "SubCondition": { + "Value": "New" + }, + "Value": "New" + }, + "DeliveryInfo": { + "IsAmazonFulfilled": true, + "IsFreeShippingEligible": true, + "IsPrimeEligible": true + }, + "Id": "a1Z9Bxb%2BApxyhGByHRFDSEzp6S0Axr2oLZWtZlajzohbj2jHRTW1cuwljnC%2BKdnB8wo50vkIX1iVMbLp3QrMOMkheLZoWngSJFVBHEmDzjo%3D", + "IsBuyBoxWinner": true, + "MerchantInfo": { + "Id": "ATVPDKIKX0DER", + "Name": "Amazon.com" + }, + "Price": { + "Amount": 13.59, + "Currency": "USD", + "DisplayAmount": "$13.59", + "Savings": { + "Amount": 8.41, + "Currency": "USD", + "DisplayAmount": "$8.41 (38%)", + "Percentage": 38 + } + }, + "ProgramEligibility": { + "IsPrimeExclusive": false, + "IsPrimePantry": false + }, + "SavingBasis": { + "Amount": 22, + "Currency": "USD", + "DisplayAmount": "$22.00" + }, + "ViolatesMAP": false + } + ], + "Summaries": [ + { + "Condition": { + "Value": "New" + }, + "HighestPrice": { + "Amount": 50.95, + "Currency": "USD", + "DisplayAmount": "$50.95" + }, + "LowestPrice": { + "Amount": 13.25, + "Currency": "USD", + "DisplayAmount": "$13.25" + }, + "OfferCount": 53 + }, + { + "Condition": { + "Value": "Used" + }, + "HighestPrice": { + "Amount": 515.4, + "Currency": "USD", + "DisplayAmount": "$515.40" + }, + "LowestPrice": { + "Amount": 3.85, + "Currency": "USD", + "DisplayAmount": "$3.85" + }, + "OfferCount": 181 + }, + { + "Condition": { + "Value": "Collectible" + }, + "HighestPrice": { + "Amount": 678, + "Currency": "USD", + "DisplayAmount": "$678.00" + }, + "LowestPrice": { + "Amount": 12.95, + "Currency": "USD", + "DisplayAmount": "$12.95" + }, + "OfferCount": 2 + } + ] + } + }, + { + "ASIN": "0892131349", + "BrowseNodeInfo": { + "BrowseNodes": [ + { + "Ancestor": { + "Ancestor": { + "Ancestor": { + "Ancestor": { + "Ancestor": { + "ContextFreeName": "Books", + "DisplayName": "Books", + "Id": "283155" + }, + "ContextFreeName": "Subjects", + "DisplayName": "Subjects", + "Id": "1000" + }, + "ContextFreeName": "Religion & Spirituality", + "DisplayName": "Religion & Spirituality", + "Id": "22" + }, + "ContextFreeName": "Hinduism", + "DisplayName": "Hinduism", + "Id": "12506" + }, + "ContextFreeName": "Sacred Hindu Writings", + "DisplayName": "Sacred Writings", + "Id": "11309340011" + }, + "ContextFreeName": "Bhagavad Gita", + "DisplayName": "Bhagavad Gita", + "Id": "12508", + "IsRoot": false, + "SalesRank": 6 + } + ], + "WebsiteSalesRank": { + "ContextFreeName": "Books", + "DisplayName": "Books", + "SalesRank": 15006 + } + }, + "DetailPageURL": "https://www.amazon.com/dp/0892131349?tag=associateTag-20&linkCode=ogi&th=1&psc=1", + "Images": { + "Primary": { + "Large": { + "Height": 500, + "URL": "https://m.media-amazon.com/images/I/51iixzFhJDL.jpg", + "Width": 287 + }, + "Medium": { + "Height": 160, + "URL": "https://m.media-amazon.com/images/I/51iixzFhJDL._SL160_.jpg", + "Width": 92 + }, + "Small": { + "Height": 75, + "URL": "https://m.media-amazon.com/images/I/51iixzFhJDL._SL75_.jpg", + "Width": 43 + } + }, + "Variants": [ + { + "Large": { + "Height": 500, + "URL": "https://m.media-amazon.com/images/I/31zlupl4ZiL.jpg", + "Width": 375 + }, + "Medium": { + "Height": 160, + "URL": "https://m.media-amazon.com/images/I/31zlupl4ZiL._SL160_.jpg", + "Width": 120 + }, + "Small": { + "Height": 75, + "URL": "https://m.media-amazon.com/images/I/31zlupl4ZiL._SL75_.jpg", + "Width": 56 + } + } + ] + }, + "ItemInfo": { + "ByLineInfo": { + "Contributors": [ + { + "Locale": "en_US", + "Name": "A. C. Bhaktivedanta Swami Prabhupada", + "Role": "Author", + "RoleType": "author" + } + ], + "Manufacturer": { + "DisplayValue": "Bhaktivedanta Book Trust", + "Label": "Manufacturer", + "Locale": "en_US" + } + }, + "Classifications": { + "Binding": { + "DisplayValue": "Mass Market Paperback", + "Label": "Binding", + "Locale": "en_US" + }, + "ProductGroup": { + "DisplayValue": "Book", + "Label": "ProductGroup", + "Locale": "en_US" + } + }, + "ContentInfo": { + "Edition": { + "DisplayValue": "De Luxe Ed", + "Label": "Edition", + "Locale": "en_US" + }, + "Languages": { + "DisplayValues": [ + { + "DisplayValue": "English", + "Type": "Published" + }, + { + "DisplayValue": "Sanskrit", + "Type": "Original Language" + }, + { + "DisplayValue": "English", + "Type": "Unknown" + } + ], + "Label": "Language", + "Locale": "en_US" + }, + "PagesCount": { + "DisplayValue": 703, + "Label": "NumberOfPages", + "Locale": "en_US" + }, + "PublicationDate": { + "DisplayValue": "1989-06-01T00:00:01Z", + "Label": "PublicationDate", + "Locale": "en_US" + } + }, + "ExternalIds": { + "EANs": { + "DisplayValues": [ + "9780892131341" + ], + "Label": "EAN", + "Locale": "en_US" + }, + "ISBNs": { + "DisplayValues": [ + "9780892131341", + "0892131349" + ], + "Label": "ISBN", + "Locale": "en_US" + } + }, + "Features": { + "DisplayValues": [ + "Most popular book", + "song of god", + "life manual", + "gives big picture of life", + "shows how to live happily from inside" + ], + "Label": "Features", + "Locale": "en_US" + }, + "ManufactureInfo": { + "ItemPartNumber": { + "DisplayValue": "colour illustrations", + "Label": "PartNumber", + "Locale": "en_US" + } + }, + "ProductInfo": { + "ItemDimensions": { + "Height": { + "DisplayValue": 7, + "Label": "Height", + "Locale": "en_US", + "Unit": "Inches" + }, + "Length": { + "DisplayValue": 4, + "Label": "Length", + "Locale": "en_US", + "Unit": "Inches" + }, + "Weight": { + "DisplayValue": 0.95, + "Label": "Weight", + "Locale": "en_US", + "Unit": "Pounds" + }, + "Width": { + "DisplayValue": 1.75, + "Label": "Width", + "Locale": "en_US", + "Unit": "Inches" + } + }, + "UnitCount": { + "DisplayValue": 1, + "Label": "NumberOfItems", + "Locale": "en_US" + } + }, + "TechnicalInfo": { + "Formats": { + "DisplayValues": [ + "Deluxe Edition" + ], + "Label": "Format", + "Locale": "en_US" + } + }, + "Title": { + "DisplayValue": "Bhagavad-Gita As It Is (Paperback)", + "Label": "Title", + "Locale": "en_US" + } + }, + "Offers": { + "Listings": [ + { + "Availability": { + "MaxOrderQuantity": 80, + "Message": "In Stock.", + "MinOrderQuantity": 1, + "Type": "Now" + }, + "Condition": { + "SubCondition": { + "Value": "New" + }, + "Value": "New" + }, + "DeliveryInfo": { + "IsAmazonFulfilled": true, + "IsFreeShippingEligible": true, + "IsPrimeEligible": true + }, + "Id": "a1Z9Bxb%2BApyW824hgnTU1Bupb9wXmyIXzXGidNbjm2S7k%2BF0L3X9CtpcxSVGU7TLspotILDwRGaIWQhcMpgvV1jyAtjRr0UNypSGY0cSNHY%3D", + "IsBuyBoxWinner": true, + "MerchantInfo": { + "Id": "ATVPDKIKX0DER", + "Name": "Amazon.com" + }, + "Price": { + "Amount": 5.95, + "Currency": "USD", + "DisplayAmount": "$5.95" + }, + "ProgramEligibility": { + "IsPrimeExclusive": false, + "IsPrimePantry": false + }, + "ViolatesMAP": false + } + ], + "Summaries": [ + { + "Condition": { + "Value": "New" + }, + "HighestPrice": { + "Amount": 15.99, + "Currency": "USD", + "DisplayAmount": "$15.99" + }, + "LowestPrice": { + "Amount": 1.5, + "Currency": "USD", + "DisplayAmount": "$1.50" + }, + "OfferCount": 86 + }, + { + "Condition": { + "Value": "Used" + }, + "HighestPrice": { + "Amount": 15.79, + "Currency": "USD", + "DisplayAmount": "$15.79" + }, + "LowestPrice": { + "Amount": 0.1, + "Currency": "USD", + "DisplayAmount": "$0.10" + }, + "OfferCount": 301 + }, + { + "Condition": { + "Value": "Collectible" + }, + "HighestPrice": { + "Amount": 18.99, + "Currency": "USD", + "DisplayAmount": "$18.99" + }, + "LowestPrice": { + "Amount": 3.95, + "Currency": "USD", + "DisplayAmount": "$3.95" + }, + "OfferCount": 9 + } + ] + } + } + ] + } +} \ No newline at end of file diff --git a/src/tests/data/amazon-paapi-search-items-gopaapi5.json b/src/tests/data/amazon-paapi-search-items-gopaapi5.json new file mode 100644 index 00000000..d39e8cbe --- /dev/null +++ b/src/tests/data/amazon-paapi-search-items-gopaapi5.json @@ -0,0 +1,1191 @@ +{ + "SearchResult": { + "Items": [ + { + "ASIN": "0134190440", + "BrowseNodeInfo": { + "BrowseNodes": [ + { + "Ancestor": { + "Ancestor": { + "Ancestor": { + "ContextFreeName": "Books", + "DisplayName": "Books", + "Id": "283155" + }, + "ContextFreeName": "Subjects", + "DisplayName": "Subjects", + "Id": "1000" + }, + "ContextFreeName": "Computers & Technology", + "DisplayName": "Computers & Technology", + "Id": "5" + }, + "ContextFreeName": "Programming Languages", + "DisplayName": "Programming Languages", + "Id": "3952", + "IsRoot": false, + "SalesRank": 35 + }, + { + "Ancestor": { + "Ancestor": { + "Ancestor": { + "Ancestor": { + "ContextFreeName": "Books", + "DisplayName": "Books", + "Id": "283155" + }, + "ContextFreeName": "Specialty Boutique", + "DisplayName": "Specialty Boutique", + "Id": "2349030011" + }, + "ContextFreeName": "Textbooks", + "DisplayName": "New, Used & Rental Textbooks", + "Id": "465600" + }, + "ContextFreeName": "Computer Science", + "DisplayName": "Computer Science", + "Id": "468204" + }, + "ContextFreeName": "Computer Programming Languages", + "DisplayName": "Programming Languages", + "Id": "491314", + "IsRoot": false, + "SalesRank": 17 + } + ], + "WebsiteSalesRank": { + "ContextFreeName": "Books", + "DisplayName": "Books", + "SalesRank": 11005 + } + }, + "DetailPageURL": "https://www.amazon.com/dp/0134190440?tag=associateTag-20&linkCode=osi&th=1&psc=1", + "Images": { + "Primary": { + "Large": { + "Height": 500, + "URL": "https://m.media-amazon.com/images/I/41aSIiETPPL.jpg", + "Width": 404 + }, + "Medium": { + "Height": 160, + "URL": "https://m.media-amazon.com/images/I/41aSIiETPPL._SL160_.jpg", + "Width": 129 + }, + "Small": { + "Height": 75, + "URL": "https://m.media-amazon.com/images/I/41aSIiETPPL._SL75_.jpg", + "Width": 61 + } + }, + "Variants": [ + { + "Large": { + "Height": 500, + "URL": "https://m.media-amazon.com/images/I/51ul4zg-hoL.jpg", + "Width": 404 + }, + "Medium": { + "Height": 160, + "URL": "https://m.media-amazon.com/images/I/51ul4zg-hoL._SL160_.jpg", + "Width": 129 + }, + "Small": { + "Height": 75, + "URL": "https://m.media-amazon.com/images/I/51ul4zg-hoL._SL75_.jpg", + "Width": 61 + } + }, + { + "Large": { + "Height": 500, + "URL": "https://m.media-amazon.com/images/I/31WBfgP59cL.jpg", + "Width": 375 + }, + "Medium": { + "Height": 160, + "URL": "https://m.media-amazon.com/images/I/31WBfgP59cL._SL160_.jpg", + "Width": 120 + }, + "Small": { + "Height": 75, + "URL": "https://m.media-amazon.com/images/I/31WBfgP59cL._SL75_.jpg", + "Width": 56 + } + } + ] + }, + "ItemInfo": { + "ByLineInfo": { + "Contributors": [ + { + "Locale": "en_US", + "Name": "Donovan, Alan A. A.", + "Role": "Author", + "RoleType": "author" + } + ], + "Manufacturer": { + "DisplayValue": "Addison-Wesley Professional", + "Label": "Manufacturer", + "Locale": "en_US" + } + }, + "Classifications": { + "Binding": { + "DisplayValue": "Paperback", + "Label": "Binding", + "Locale": "en_US" + }, + "ProductGroup": { + "DisplayValue": "Book", + "Label": "ProductGroup", + "Locale": "en_US" + } + }, + "ContentInfo": { + "Edition": { + "DisplayValue": "1", + "Label": "Edition", + "Locale": "en_US" + }, + "Languages": { + "DisplayValues": [ + { + "DisplayValue": "English", + "Type": "Published" + }, + { + "DisplayValue": "English", + "Type": "Original Language" + }, + { + "DisplayValue": "English", + "Type": "Unknown" + } + ], + "Label": "Language", + "Locale": "en_US" + }, + "PagesCount": { + "DisplayValue": 398, + "Label": "NumberOfPages", + "Locale": "en_US" + }, + "PublicationDate": { + "DisplayValue": "2015-11-05T00:00:01Z", + "Label": "PublicationDate", + "Locale": "en_US" + } + }, + "ExternalIds": { + "EANs": { + "DisplayValues": [ + "9780134190440" + ], + "Label": "EAN", + "Locale": "en_US" + }, + "ISBNs": { + "DisplayValues": [ + "9780134190440", + "0134190440" + ], + "Label": "ISBN", + "Locale": "en_US" + } + }, + "ManufactureInfo": { + "ItemPartNumber": { + "DisplayValue": "42375234", + "Label": "PartNumber", + "Locale": "en_US" + } + }, + "ProductInfo": { + "IsAdultProduct": { + "DisplayValue": false, + "Label": "IsAdultProduct", + "Locale": "en_US" + }, + "ItemDimensions": { + "Height": { + "DisplayValue": 1, + "Label": "Height", + "Locale": "en_US", + "Unit": "Inches" + }, + "Length": { + "DisplayValue": 9.1, + "Label": "Length", + "Locale": "en_US", + "Unit": "Inches" + }, + "Weight": { + "DisplayValue": 1.45064168396, + "Label": "Weight", + "Locale": "en_US", + "Unit": "Pounds" + }, + "Width": { + "DisplayValue": 7.3, + "Label": "Width", + "Locale": "en_US", + "Unit": "Inches" + } + }, + "ReleaseDate": { + "DisplayValue": "2015-10-26T00:00:01Z", + "Label": "ReleaseDate", + "Locale": "en_US" + }, + "UnitCount": { + "DisplayValue": 1, + "Label": "NumberOfItems", + "Locale": "en_US" + } + }, + "Title": { + "DisplayValue": "Go Programming Language, The (Addison-Wesley Professional Computing Series)", + "Label": "Title", + "Locale": "en_US" + } + }, + "Offers": { + "Listings": [ + { + "Availability": { + "MaxOrderQuantity": 80, + "Message": "In Stock.", + "MinOrderQuantity": 1, + "Type": "Now" + }, + "Condition": { + "SubCondition": { + "Value": "New" + }, + "Value": "New" + }, + "DeliveryInfo": { + "IsAmazonFulfilled": true, + "IsFreeShippingEligible": true, + "IsPrimeEligible": true + }, + "Id": "H26dkOXFz3wk9n5bAuiKx5GM7RO4h9rOU5VNvDPADlz2evXUj7INzDWa5KETU0wsMkbCD3T0m4Bm%2BqkpGsTzjTj6PqqvbViQpRKgW%2FUtak4pF358CdeS1Q%3D%3D", + "IsBuyBoxWinner": true, + "MerchantInfo": { + "Id": "ATVPDKIKX0DER", + "Name": "Amazon.com" + }, + "Price": { + "Amount": 26.49, + "Currency": "USD", + "DisplayAmount": "$26.49", + "Savings": { + "Amount": 13.5, + "Currency": "USD", + "DisplayAmount": "$13.50 (34%)", + "Percentage": 34 + } + }, + "ProgramEligibility": { + "IsPrimeExclusive": false, + "IsPrimePantry": false + }, + "SavingBasis": { + "Amount": 39.99, + "Currency": "USD", + "DisplayAmount": "$39.99" + }, + "ViolatesMAP": false + } + ], + "Summaries": [ + { + "Condition": { + "Value": "New" + }, + "HighestPrice": { + "Amount": 71.35, + "Currency": "USD", + "DisplayAmount": "$71.35" + }, + "LowestPrice": { + "Amount": 21.18, + "Currency": "USD", + "DisplayAmount": "$21.18" + }, + "OfferCount": 18 + }, + { + "Condition": { + "Value": "Used" + }, + "HighestPrice": { + "Amount": 41.79, + "Currency": "USD", + "DisplayAmount": "$41.79" + }, + "LowestPrice": { + "Amount": 23.23, + "Currency": "USD", + "DisplayAmount": "$23.23" + }, + "OfferCount": 8 + } + ] + } + }, + { + "ASIN": "1838559337", + "BrowseNodeInfo": { + "BrowseNodes": [ + { + "Ancestor": { + "Ancestor": { + "Ancestor": { + "Ancestor": { + "ContextFreeName": "Books", + "DisplayName": "Books", + "Id": "283155" + }, + "ContextFreeName": "Subjects", + "DisplayName": "Subjects", + "Id": "1000" + }, + "ContextFreeName": "Computers & Technology", + "DisplayName": "Computers & Technology", + "Id": "5" + }, + "ContextFreeName": "Databases & Big Data", + "DisplayName": "Databases & Big Data", + "Id": "549646" + }, + "ContextFreeName": "Data Modeling & Design", + "DisplayName": "Data Modeling & Design", + "Id": "379382011", + "IsRoot": false, + "SalesRank": 102 + }, + { + "Ancestor": { + "Ancestor": { + "Ancestor": { + "Ancestor": { + "Ancestor": { + "ContextFreeName": "Books", + "DisplayName": "Books", + "Id": "283155" + }, + "ContextFreeName": "Subjects", + "DisplayName": "Subjects", + "Id": "1000" + }, + "ContextFreeName": "Computers & Technology", + "DisplayName": "Computers & Technology", + "Id": "5" + }, + "ContextFreeName": "Networking & Cloud Computing", + "DisplayName": "Networking & Cloud Computing", + "Id": "3652" + }, + "ContextFreeName": "Enterprise Data Computing", + "DisplayName": "Data in the Enterprise", + "Id": "3666" + }, + "ContextFreeName": "Client-Server Networking Systems", + "DisplayName": "Client-Server Systems", + "Id": "3668", + "IsRoot": false, + "SalesRank": 55 + }, + { + "Ancestor": { + "Ancestor": { + "Ancestor": { + "Ancestor": { + "ContextFreeName": "Books", + "DisplayName": "Books", + "Id": "283155" + }, + "ContextFreeName": "Subjects", + "DisplayName": "Subjects", + "Id": "1000" + }, + "ContextFreeName": "Computers & Technology", + "DisplayName": "Computers & Technology", + "Id": "5" + }, + "ContextFreeName": "Networking & Cloud Computing", + "DisplayName": "Networking & Cloud Computing", + "Id": "3652" + }, + "ContextFreeName": "Cloud Computing", + "DisplayName": "Cloud Computing", + "Id": "10806612011", + "IsRoot": false, + "SalesRank": 154 + }, + { + "Ancestor": { + "Ancestor": { + "Ancestor": { + "Ancestor": { + "ContextFreeName": "Books", + "DisplayName": "Books", + "Id": "283155" + }, + "ContextFreeName": "Subjects", + "DisplayName": "Subjects", + "Id": "1000" + }, + "ContextFreeName": "Computers & Technology", + "DisplayName": "Computers & Technology", + "Id": "5" + }, + "ContextFreeName": "Web Development & Design", + "DisplayName": "Web Development & Design", + "Id": "3510" + }, + "ContextFreeName": "Web Development & Design Programming", + "DisplayName": "Programming", + "Id": "3600", + "IsRoot": false + }, + { + "Ancestor": { + "Ancestor": { + "Ancestor": { + "ContextFreeName": "Books", + "DisplayName": "Books", + "Id": "283155" + }, + "ContextFreeName": "Subjects", + "DisplayName": "Subjects", + "Id": "1000" + }, + "ContextFreeName": "Computers & Technology", + "DisplayName": "Computers & Technology", + "Id": "5" + }, + "ContextFreeName": "Programming Languages", + "DisplayName": "Programming Languages", + "Id": "3952", + "IsRoot": false + } + ], + "WebsiteSalesRank": { + "ContextFreeName": "Books", + "DisplayName": "Books", + "SalesRank": 151024 + } + }, + "DetailPageURL": "https://www.amazon.com/dp/1838559337?tag=associateTag-20&linkCode=osi&th=1&psc=1", + "Images": { + "Primary": { + "Large": { + "Height": 500, + "URL": "https://m.media-amazon.com/images/I/51R0SXcmImL.jpg", + "Width": 406 + }, + "Medium": { + "Height": 160, + "URL": "https://m.media-amazon.com/images/I/51R0SXcmImL._SL160_.jpg", + "Width": 130 + }, + "Small": { + "Height": 75, + "URL": "https://m.media-amazon.com/images/I/51R0SXcmImL._SL75_.jpg", + "Width": 61 + } + }, + "Variants": [ + { + "Large": { + "Height": 500, + "URL": "https://m.media-amazon.com/images/I/51d-h-k-SdL.jpg", + "Width": 406 + }, + "Medium": { + "Height": 160, + "URL": "https://m.media-amazon.com/images/I/51d-h-k-SdL._SL160_.jpg", + "Width": 130 + }, + "Small": { + "Height": 75, + "URL": "https://m.media-amazon.com/images/I/51d-h-k-SdL._SL75_.jpg", + "Width": 61 + } + } + ] + }, + "ItemInfo": { + "ByLineInfo": { + "Contributors": [ + { + "Locale": "en_US", + "Name": "Tsoukalos, Mihalis", + "Role": "Author", + "RoleType": "author" + } + ], + "Manufacturer": { + "DisplayValue": "Packt Publishing", + "Label": "Manufacturer", + "Locale": "en_US" + } + }, + "Classifications": { + "Binding": { + "DisplayValue": "Paperback", + "Label": "Binding", + "Locale": "en_US" + }, + "ProductGroup": { + "DisplayValue": "Book", + "Label": "ProductGroup", + "Locale": "en_US" + } + }, + "ContentInfo": { + "Languages": { + "DisplayValues": [ + { + "DisplayValue": "English", + "Type": "Published" + }, + { + "DisplayValue": "English", + "Type": "Original Language" + }, + { + "DisplayValue": "English", + "Type": "Unknown" + } + ], + "Label": "Language", + "Locale": "en_US" + }, + "PagesCount": { + "DisplayValue": 798, + "Label": "NumberOfPages", + "Locale": "en_US" + }, + "PublicationDate": { + "DisplayValue": "2019-08-29T00:00:01Z", + "Label": "PublicationDate", + "Locale": "en_US" + } + }, + "ExternalIds": { + "EANs": { + "DisplayValues": [ + "9781838559335" + ], + "Label": "EAN", + "Locale": "en_US" + }, + "ISBNs": { + "DisplayValues": [ + "1838559337" + ], + "Label": "ISBN", + "Locale": "en_US" + } + }, + "ProductInfo": { + "IsAdultProduct": { + "DisplayValue": false, + "Label": "IsAdultProduct", + "Locale": "en_US" + }, + "ItemDimensions": { + "Height": { + "DisplayValue": 9.25, + "Label": "Height", + "Locale": "en_US", + "Unit": "Inches" + }, + "Length": { + "DisplayValue": 7.5, + "Label": "Length", + "Locale": "en_US", + "Unit": "Inches" + }, + "Weight": { + "DisplayValue": 2.96, + "Label": "Weight", + "Locale": "en_US", + "Unit": "Pounds" + }, + "Width": { + "DisplayValue": 1.8, + "Label": "Width", + "Locale": "en_US", + "Unit": "Inches" + } + }, + "ReleaseDate": { + "DisplayValue": "2019-08-29T00:00:01Z", + "Label": "ReleaseDate", + "Locale": "en_US" + }, + "UnitCount": { + "DisplayValue": 1, + "Label": "NumberOfItems", + "Locale": "en_US" + } + }, + "Title": { + "DisplayValue": "Mastering Go: Create Golang production applications using network libraries, concurrency, machine learning, and advanced data structures, 2nd Edition", + "Label": "Title", + "Locale": "en_US" + } + }, + "Offers": { + "Listings": [ + { + "Availability": { + "Message": "In Stock.", + "MinOrderQuantity": 1, + "Type": "Now" + }, + "Condition": { + "SubCondition": { + "Value": "New" + }, + "Value": "New" + }, + "DeliveryInfo": { + "IsAmazonFulfilled": true, + "IsFreeShippingEligible": true, + "IsPrimeEligible": true + }, + "Id": "H26dkOXFz3zUErquCtGkasp0ByRdzlE8azTBB1I3U7%2F%2BjrmprKqPCogrZ5p5G3387IeqhCTvjl%2BV3kDhzzEy9lgSXS%2Bjq5wEtrkDCJEk6kNSxYzcHU5KjA%3D%3D", + "IsBuyBoxWinner": true, + "MerchantInfo": { + "Id": "ATVPDKIKX0DER", + "Name": "Amazon.com" + }, + "Price": { + "Amount": 49.99, + "Currency": "USD", + "DisplayAmount": "$49.99" + }, + "ProgramEligibility": { + "IsPrimeExclusive": false, + "IsPrimePantry": false + }, + "ViolatesMAP": false + } + ], + "Summaries": [ + { + "Condition": { + "Value": "New" + }, + "HighestPrice": { + "Amount": 82.6, + "Currency": "USD", + "DisplayAmount": "$82.60" + }, + "LowestPrice": { + "Amount": 49.99, + "Currency": "USD", + "DisplayAmount": "$49.99" + }, + "OfferCount": 7 + }, + { + "Condition": { + "Value": "Used" + }, + "HighestPrice": { + "Amount": 92.8, + "Currency": "USD", + "DisplayAmount": "$92.80" + }, + "LowestPrice": { + "Amount": 88.8, + "Currency": "USD", + "DisplayAmount": "$88.80" + }, + "OfferCount": 2 + } + ] + } + }, + { + "ASIN": "1593278659", + "BrowseNodeInfo": { + "BrowseNodes": [ + { + "Ancestor": { + "Ancestor": { + "Ancestor": { + "Ancestor": { + "ContextFreeName": "Books", + "DisplayName": "Books", + "Id": "283155" + }, + "ContextFreeName": "Subjects", + "DisplayName": "Subjects", + "Id": "1000" + }, + "ContextFreeName": "Computers & Technology", + "DisplayName": "Computers & Technology", + "Id": "5" + }, + "ContextFreeName": "Networking & Cloud Computing", + "DisplayName": "Networking & Cloud Computing", + "Id": "3652" + }, + "ContextFreeName": "Computer Network Security", + "DisplayName": "Network Security", + "Id": "3746", + "IsRoot": false, + "SalesRank": 10 + }, + { + "Ancestor": { + "Ancestor": { + "Ancestor": { + "Ancestor": { + "Ancestor": { + "ContextFreeName": "Books", + "DisplayName": "Books", + "Id": "283155" + }, + "ContextFreeName": "Subjects", + "DisplayName": "Subjects", + "Id": "1000" + }, + "ContextFreeName": "Computers & Technology", + "DisplayName": "Computers & Technology", + "Id": "5" + }, + "ContextFreeName": "Computer Programming", + "DisplayName": "Programming", + "Id": "3839" + }, + "ContextFreeName": "Software Design, Testing & Engineering", + "DisplayName": "Software Design, Testing & Engineering", + "Id": "4011" + }, + "ContextFreeName": "Software Testing", + "DisplayName": "Testing", + "Id": "4133", + "IsRoot": false, + "SalesRank": 9 + }, + { + "Ancestor": { + "Ancestor": { + "Ancestor": { + "Ancestor": { + "ContextFreeName": "Books", + "DisplayName": "Books", + "Id": "283155" + }, + "ContextFreeName": "Subjects", + "DisplayName": "Subjects", + "Id": "1000" + }, + "ContextFreeName": "Computers & Technology", + "DisplayName": "Computers & Technology", + "Id": "5" + }, + "ContextFreeName": "Internet & Social Media", + "DisplayName": "Internet & Social Media", + "Id": "69766" + }, + "ContextFreeName": "Computer Hacking", + "DisplayName": "Hacking", + "Id": "3648", + "IsRoot": false + }, + { + "Ancestor": { + "Ancestor": { + "Ancestor": { + "Ancestor": { + "ContextFreeName": "Books", + "DisplayName": "Books", + "Id": "283155" + }, + "ContextFreeName": "Subjects", + "DisplayName": "Subjects", + "Id": "1000" + }, + "ContextFreeName": "Computers & Technology", + "DisplayName": "Computers & Technology", + "Id": "5" + }, + "ContextFreeName": "Computer Security & Encryption", + "DisplayName": "Security & Encryption", + "Id": "377560011" + }, + "ContextFreeName": "Computer Viruses", + "DisplayName": "Viruses", + "Id": "3636", + "IsRoot": false, + "SalesRank": 4 + } + ], + "WebsiteSalesRank": { + "ContextFreeName": "Books", + "DisplayName": "Books", + "SalesRank": 10424 + } + }, + "DetailPageURL": "https://www.amazon.com/dp/1593278659?tag=associateTag-20&linkCode=osi&th=1&psc=1", + "Images": { + "Primary": { + "Large": { + "Height": 500, + "URL": "https://m.media-amazon.com/images/I/51Eznheb+3L.jpg", + "Width": 378 + }, + "Medium": { + "Height": 160, + "URL": "https://m.media-amazon.com/images/I/51Eznheb+3L._SL160_.jpg", + "Width": 121 + }, + "Small": { + "Height": 75, + "URL": "https://m.media-amazon.com/images/I/51Eznheb+3L._SL75_.jpg", + "Width": 57 + } + }, + "Variants": [ + { + "Large": { + "Height": 500, + "URL": "https://m.media-amazon.com/images/I/51PJIRJ6cOL.jpg", + "Width": 378 + }, + "Medium": { + "Height": 160, + "URL": "https://m.media-amazon.com/images/I/51PJIRJ6cOL._SL160_.jpg", + "Width": 121 + }, + "Small": { + "Height": 75, + "URL": "https://m.media-amazon.com/images/I/51PJIRJ6cOL._SL75_.jpg", + "Width": 57 + } + }, + { + "Large": { + "Height": 500, + "URL": "https://m.media-amazon.com/images/I/41bVAz0e7XL.jpg", + "Width": 378 + }, + "Medium": { + "Height": 160, + "URL": "https://m.media-amazon.com/images/I/41bVAz0e7XL._SL160_.jpg", + "Width": 121 + }, + "Small": { + "Height": 75, + "URL": "https://m.media-amazon.com/images/I/41bVAz0e7XL._SL75_.jpg", + "Width": 57 + } + }, + { + "Large": { + "Height": 500, + "URL": "https://m.media-amazon.com/images/I/41bbpNdF4FL.jpg", + "Width": 378 + }, + "Medium": { + "Height": 160, + "URL": "https://m.media-amazon.com/images/I/41bbpNdF4FL._SL160_.jpg", + "Width": 121 + }, + "Small": { + "Height": 75, + "URL": "https://m.media-amazon.com/images/I/41bbpNdF4FL._SL75_.jpg", + "Width": 57 + } + }, + { + "Large": { + "Height": 500, + "URL": "https://m.media-amazon.com/images/I/51ucroxGBVL.jpg", + "Width": 378 + }, + "Medium": { + "Height": 160, + "URL": "https://m.media-amazon.com/images/I/51ucroxGBVL._SL160_.jpg", + "Width": 121 + }, + "Small": { + "Height": 75, + "URL": "https://m.media-amazon.com/images/I/51ucroxGBVL._SL75_.jpg", + "Width": 57 + } + }, + { + "Large": { + "Height": 500, + "URL": "https://m.media-amazon.com/images/I/51EXsdd3bcL.jpg", + "Width": 378 + }, + "Medium": { + "Height": 160, + "URL": "https://m.media-amazon.com/images/I/51EXsdd3bcL._SL160_.jpg", + "Width": 121 + }, + "Small": { + "Height": 75, + "URL": "https://m.media-amazon.com/images/I/51EXsdd3bcL._SL75_.jpg", + "Width": 57 + } + } + ] + }, + "ItemInfo": { + "ByLineInfo": { + "Contributors": [ + { + "Locale": "en_US", + "Name": "Steele, Tom", + "Role": "Author", + "RoleType": "author" + }, + { + "Locale": "en_US", + "Name": "Patten, Chris", + "Role": "Author", + "RoleType": "author" + }, + { + "Locale": "en_US", + "Name": "Kottmann, Dan", + "Role": "Author", + "RoleType": "author" + } + ], + "Manufacturer": { + "DisplayValue": "No Starch Press", + "Label": "Manufacturer", + "Locale": "en_US" + } + }, + "Classifications": { + "Binding": { + "DisplayValue": "Paperback", + "Label": "Binding", + "Locale": "en_US" + }, + "ProductGroup": { + "DisplayValue": "Book", + "Label": "ProductGroup", + "Locale": "en_US" + } + }, + "ContentInfo": { + "Languages": { + "DisplayValues": [ + { + "DisplayValue": "English", + "Type": "Published" + }, + { + "DisplayValue": "English", + "Type": "Original Language" + }, + { + "DisplayValue": "English", + "Type": "Unknown" + } + ], + "Label": "Language", + "Locale": "en_US" + }, + "PagesCount": { + "DisplayValue": 368, + "Label": "NumberOfPages", + "Locale": "en_US" + }, + "PublicationDate": { + "DisplayValue": "2020-01-24T00:00:01Z", + "Label": "PublicationDate", + "Locale": "en_US" + } + }, + "ExternalIds": { + "EANs": { + "DisplayValues": [ + "9781593278656" + ], + "Label": "EAN", + "Locale": "en_US" + }, + "ISBNs": { + "DisplayValues": [ + "1593278659" + ], + "Label": "ISBN", + "Locale": "en_US" + } + }, + "ProductInfo": { + "ItemDimensions": { + "Height": { + "DisplayValue": 9.19, + "Label": "Height", + "Locale": "en_US", + "Unit": "Inches" + }, + "Length": { + "DisplayValue": 7, + "Label": "Length", + "Locale": "en_US", + "Unit": "Inches" + }, + "Weight": { + "DisplayValue": 1.55, + "Label": "Weight", + "Locale": "en_US", + "Unit": "Pounds" + }, + "Width": { + "DisplayValue": 0.86, + "Label": "Width", + "Locale": "en_US", + "Unit": "Inches" + } + }, + "ReleaseDate": { + "DisplayValue": "2020-01-24T00:00:01Z", + "Label": "ReleaseDate", + "Locale": "en_US" + } + }, + "Title": { + "DisplayValue": "Black Hat Go: Go Programming For Hackers and Pentesters", + "Label": "Title", + "Locale": "en_US" + } + }, + "Offers": { + "Listings": [ + { + "Availability": { + "Message": "In Stock.", + "MinOrderQuantity": 1, + "Type": "Now" + }, + "Condition": { + "SubCondition": { + "Value": "New" + }, + "Value": "New" + }, + "DeliveryInfo": { + "IsAmazonFulfilled": true, + "IsFreeShippingEligible": true, + "IsPrimeEligible": true + }, + "Id": "H26dkOXFz3wZmCrmj%2FYxrIeDi4T0C7c4DcpkwtKMwqik23CM63QRy5C1CKFdgAhBHMVTIblk06rmzbr2%2F1dHfdEJWfLQOsNwV2aFHfobYBLj4%2BjJ%2Bmm9zg%3D%3D", + "IsBuyBoxWinner": true, + "MerchantInfo": { + "Id": "ATVPDKIKX0DER", + "Name": "Amazon.com" + }, + "Price": { + "Amount": 26.93, + "Currency": "USD", + "DisplayAmount": "$26.93", + "Savings": { + "Amount": 13.02, + "Currency": "USD", + "DisplayAmount": "$13.02 (33%)", + "Percentage": 33 + } + }, + "ProgramEligibility": { + "IsPrimeExclusive": false, + "IsPrimePantry": false + }, + "SavingBasis": { + "Amount": 39.95, + "Currency": "USD", + "DisplayAmount": "$39.95" + }, + "ViolatesMAP": false + } + ], + "Summaries": [ + { + "Condition": { + "Value": "New" + }, + "HighestPrice": { + "Amount": 56.93, + "Currency": "USD", + "DisplayAmount": "$56.93" + }, + "LowestPrice": { + "Amount": 24.49, + "Currency": "USD", + "DisplayAmount": "$24.49" + }, + "OfferCount": 27 + }, + { + "Condition": { + "Value": "Used" + }, + "HighestPrice": { + "Amount": 45.7, + "Currency": "USD", + "DisplayAmount": "$45.70" + }, + "LowestPrice": { + "Amount": 24.54, + "Currency": "USD", + "DisplayAmount": "$24.54" + }, + "OfferCount": 11 + } + ] + } + } + ], + "SearchRefinements": { + "SearchIndex": { + "Bins": [ + { + "DisplayName": "Books", + "Id": "Books" + }, + { + "DisplayName": "Kindle Store", + "Id": "KindleStore" + }, + { + "DisplayName": "CDs & Vinyl", + "Id": "Music" + }, + { + "DisplayName": "Digital Music", + "Id": "DigitalMusic" + }, + { + "DisplayName": "Apps & Games", + "Id": "MobileApps" + } + ], + "DisplayName": "Department", + "Id": "SearchIndex" + } + }, + "SearchURL": "https://www.amazon.com/s?k=golang&rh=p_n_availability%3A-1&tag=associateTag-20&linkCode=osi", + "TotalResultCount": 146 + } +} \ No newline at end of file