File indexing completed on 2024-05-12 16:45:51

0001 /***************************************************************************
0002     Copyright (C) 2011 Robby Stephenson <robby@periapsis.org>
0003  ***************************************************************************/
0004 
0005 /***************************************************************************
0006  *                                                                         *
0007  *   This program is free software; you can redistribute it and/or         *
0008  *   modify it under the terms of the GNU General Public License as        *
0009  *   published by the Free Software Foundation; either version 2 of        *
0010  *   the License or (at your option) version 3 or any later version        *
0011  *   accepted by the membership of KDE e.V. (or its successor approved     *
0012  *   by the membership of KDE e.V.), which shall act as a proxy            *
0013  *   defined in Section 14 of version 3 of the license.                    *
0014  *                                                                         *
0015  *   This program is distributed in the hope that it will be useful,       *
0016  *   but WITHOUT ANY WARRANTY; without even the implied warranty of        *
0017  *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the         *
0018  *   GNU General Public License for more details.                          *
0019  *                                                                         *
0020  *   You should have received a copy of the GNU General Public License     *
0021  *   along with this program.  If not, see <http://www.gnu.org/licenses/>. *
0022  *                                                                         *
0023  ***************************************************************************/
0024 
0025 #include "googlebookfetcher.h"
0026 #include "../collections/bookcollection.h"
0027 #include "../entry.h"
0028 #include "../images/imagefactory.h"
0029 #include "../utils/isbnvalidator.h"
0030 #include "../utils/guiproxy.h"
0031 #include "../utils/string_utils.h"
0032 #include "../core/filehandler.h"
0033 #include "../tellico_debug.h"
0034 
0035 #include <KLocalizedString>
0036 #include <KIO/Job>
0037 #include <KIO/JobUiDelegate>
0038 #include <KJobWidgets/KJobWidgets>
0039 #include <KConfigGroup>
0040 
0041 #include <QLineEdit>
0042 #include <QLabel>
0043 #include <QFile>
0044 #include <QTextStream>
0045 #include <QGridLayout>
0046 #include <QTextCodec>
0047 #include <QJsonDocument>
0048 #include <QJsonObject>
0049 #include <QUrlQuery>
0050 
0051 namespace {
0052   static const int GOOGLEBOOK_MAX_RETURNS = 20;
0053   static const char* GOOGLEBOOK_API_URL = "https://www.googleapis.com/books/v1/volumes";
0054   static const char* GOOGLEBOOK_API_KEY = "b0e1702513773b743b53b1c5566ea0f93e7c3b720351bad197f801491951d29afca54b32712ba6dc4e1e4c7a5b0ad99d9dedfdbab4f10642b7e821403340fc98692bcdb4dc8fd0b14339236ae4a5";
0055 }
0056 
0057 using namespace Tellico;
0058 using Tellico::Fetch::GoogleBookFetcher;
0059 
0060 GoogleBookFetcher::GoogleBookFetcher(QObject* parent_)
0061     : Fetcher(parent_)
0062     , m_started(false)
0063     , m_start(0)
0064     , m_total(0) {
0065   m_apiKey = Tellico::reverseObfuscate(GOOGLEBOOK_API_KEY);
0066 }
0067 
0068 GoogleBookFetcher::~GoogleBookFetcher() {
0069 }
0070 
0071 QString GoogleBookFetcher::source() const {
0072   return m_name.isEmpty() ? defaultName() : m_name;
0073 }
0074 
0075 bool GoogleBookFetcher::canSearch(Fetch::FetchKey k) const {
0076   return k == Title || k == Person || k == ISBN || k == Keyword;
0077 }
0078 
0079 bool GoogleBookFetcher::canFetch(int type) const {
0080   return type == Data::Collection::Book || type == Data::Collection::Bibtex;
0081 }
0082 
0083 void GoogleBookFetcher::readConfigHook(const KConfigGroup& config_) {
0084   // allow an empty key if the config key does exist
0085   m_apiKey = config_.readEntry("API Key", GOOGLEBOOK_API_KEY);
0086 }
0087 
0088 void GoogleBookFetcher::search() {
0089   m_start = 0;
0090   m_total = -1;
0091   continueSearch();
0092 }
0093 
0094 void GoogleBookFetcher::continueSearch() {
0095   m_started = true;
0096   // we only split ISBN and LCCN values
0097   QStringList searchTerms;
0098   if(request().key() == ISBN) {
0099     searchTerms = FieldFormat::splitValue(request().value());
0100   } else  {
0101     searchTerms += request().value();
0102   }
0103   foreach(const QString& searchTerm, searchTerms) {
0104     doSearch(searchTerm);
0105   }
0106   if(m_jobs.isEmpty()) {
0107     stop();
0108   }
0109 }
0110 
0111 void GoogleBookFetcher::doSearch(const QString& term_) {
0112   QUrl u(QString::fromLatin1(GOOGLEBOOK_API_URL));
0113   QUrlQuery q;
0114   q.addQueryItem(QStringLiteral("maxResults"), QString::number(GOOGLEBOOK_MAX_RETURNS));
0115   q.addQueryItem(QStringLiteral("startIndex"), QString::number(m_start));
0116   q.addQueryItem(QStringLiteral("printType"), QStringLiteral("books"));
0117   // we don't require a key, cause it might work without it
0118   if(!m_apiKey.isEmpty()) {
0119     q.addQueryItem(QStringLiteral("key"), m_apiKey);
0120   }
0121 
0122   switch(request().key()) {
0123     case Title:
0124       q.addQueryItem(QStringLiteral("q"), QLatin1String("intitle:") + term_);
0125       break;
0126 
0127     case Person:
0128       // for people, go ahead and enclose in quotes
0129       // risk of missing middle initials, etc. balanced by google splitting front and last name
0130       q.addQueryItem(QStringLiteral("q"), QLatin1String("inauthor:\"") + term_ + QLatin1Char('"'));
0131       break;
0132 
0133     case ISBN:
0134       {
0135         const QString isbn = ISBNValidator::cleanValue(term_);
0136         q.addQueryItem(QStringLiteral("q"), QLatin1String("isbn:") + isbn);
0137       }
0138       break;
0139 
0140     case Keyword:
0141       q.addQueryItem(QStringLiteral("q"), term_);
0142       break;
0143 
0144     default:
0145       myWarning() << "key not recognized:" << request().key();
0146       return;
0147   }
0148   u.setQuery(q);
0149 //  myDebug() << "url:" << u;
0150 
0151   QPointer<KIO::StoredTransferJob> job = KIO::storedGet(u, KIO::NoReload, KIO::HideProgressInfo);
0152   KJobWidgets::setWindow(job, GUI::Proxy::widget());
0153   connect(job.data(), &KJob::result, this, &GoogleBookFetcher::slotComplete);
0154   m_jobs << job;
0155 }
0156 
0157 void GoogleBookFetcher::endJob(KIO::StoredTransferJob* job_) {
0158   m_jobs.removeOne(job_);
0159   if(m_jobs.isEmpty())  {
0160     stop();
0161   }
0162 }
0163 
0164 void GoogleBookFetcher::stop() {
0165   if(!m_started) {
0166     return;
0167   }
0168   foreach(QPointer<KIO::StoredTransferJob> job, m_jobs) {
0169     if(job) {
0170       job->kill();
0171     }
0172   }
0173   m_jobs.clear();
0174   m_started = false;
0175   emit signalDone(this);
0176 }
0177 
0178 Tellico::Data::EntryPtr GoogleBookFetcher::fetchEntryHook(uint uid_) {
0179   Data::EntryPtr entry = m_entries.value(uid_);
0180   if(!entry) {
0181     myWarning() << "no entry in dict";
0182     return Data::EntryPtr();
0183   }
0184 
0185   QString gbs = entry->field(QStringLiteral("gbs-link"));
0186   if(!gbs.isEmpty()) {
0187     // quiet
0188     QByteArray data = FileHandler::readDataFile(QUrl::fromUserInput(gbs), true);
0189     QJsonDocument doc = QJsonDocument::fromJson(data);
0190     populateEntry(entry, doc.object().toVariantMap());
0191   }
0192 
0193   const QString image_id = entry->field(QStringLiteral("cover"));
0194   // if it's still a url, we need to load it
0195   if(image_id.startsWith(QLatin1String("http"))) {
0196     const QString id = ImageFactory::addImage(QUrl::fromUserInput(image_id), true);
0197     if(id.isEmpty()) {
0198       message(i18n("The cover image could not be loaded."), MessageHandler::Warning);
0199       entry->setField(QStringLiteral("cover"), QString());
0200     } else {
0201       entry->setField(QStringLiteral("cover"), id);
0202     }
0203   }
0204 
0205   // don't want to include gbs json link
0206   entry->setField(QStringLiteral("gbs-link"), QString());
0207 
0208   return entry;
0209 }
0210 
0211 Tellico::Fetch::FetchRequest GoogleBookFetcher::updateRequest(Data::EntryPtr entry_) {
0212   const QString isbn = entry_->field(QStringLiteral("isbn"));
0213   if(!isbn.isEmpty()) {
0214     return FetchRequest(ISBN, isbn);
0215   }
0216   QString title = entry_->field(QStringLiteral("title"));
0217   if(!title.isEmpty()) {
0218     return FetchRequest(Title, title);
0219   }
0220   return FetchRequest();
0221 }
0222 
0223 void GoogleBookFetcher::slotComplete(KJob* job_) {
0224   KIO::StoredTransferJob* job = static_cast<KIO::StoredTransferJob*>(job_);
0225 //  myDebug();
0226 
0227   if(job->error()) {
0228     job->uiDelegate()->showErrorMessage();
0229     endJob(job);
0230     return;
0231   }
0232 
0233   QByteArray data = job->data();
0234   if(data.isEmpty()) {
0235     myDebug() << "no data";
0236     endJob(job);
0237     return;
0238   }
0239 
0240 #if 0
0241   myWarning() << "Remove debug from googlebookfetcher.cpp";
0242   QFile f(QString::fromLatin1("/tmp/test.json"));
0243   if(f.open(QIODevice::WriteOnly)) {
0244     QTextStream t(&f);
0245     t.setCodec("UTF-8");
0246     t << data;
0247   }
0248   f.close();
0249 #endif
0250 
0251   Data::CollPtr coll(new Data::BookCollection(true));
0252   // always add the gbs-link for fetchEntryHook
0253   Data::FieldPtr field(new Data::Field(QStringLiteral("gbs-link"), QStringLiteral("GBS Link"), Data::Field::URL));
0254   field->setCategory(i18n("General"));
0255   coll->addField(field);
0256 
0257   if(!coll->hasField(QStringLiteral("googlebook")) && optionalFields().contains(QStringLiteral("googlebook"))) {
0258     Data::FieldPtr field(new Data::Field(QStringLiteral("googlebook"), i18n("Google Book Link"), Data::Field::URL));
0259     field->setCategory(i18n("General"));
0260     coll->addField(field);
0261   }
0262 
0263   QJsonDocument doc = QJsonDocument::fromJson(data);
0264   QVariantMap result = doc.object().toVariantMap();
0265   m_total = result.value(QStringLiteral("totalItems")).toInt();
0266 //  myDebug() << "total:" << m_total;
0267 
0268   QVariantList resultList = result.value(QStringLiteral("items")).toList();
0269   if(resultList.isEmpty()) {
0270     myDebug() << "no results";
0271     endJob(job);
0272     return;
0273   }
0274 
0275   foreach(const QVariant& result, resultList) {
0276   //  myDebug() << "found result:" << result;
0277 
0278     Data::EntryPtr entry(new Data::Entry(coll));
0279     populateEntry(entry, result.toMap());
0280 
0281     FetchResult* r = new FetchResult(this, entry);
0282     m_entries.insert(r->uid, entry);
0283     emit signalResultFound(r);
0284   }
0285 
0286   m_start = m_entries.count();
0287   m_hasMoreResults = request().key() != ISBN && m_start <= m_total;
0288   endJob(job);
0289 }
0290 
0291 void GoogleBookFetcher::populateEntry(Data::EntryPtr entry, const QVariantMap& resultMap) {
0292   if(entry->collection()->hasField(QStringLiteral("gbs-link"))) {
0293     entry->setField(QStringLiteral("gbs-link"), mapValue(resultMap, "selfLink"));
0294   }
0295 
0296   const QVariantMap volumeMap = resultMap.value(QStringLiteral("volumeInfo")).toMap();
0297   entry->setField(QStringLiteral("title"),     mapValue(volumeMap, "title"));
0298   entry->setField(QStringLiteral("subtitle"),  mapValue(volumeMap, "subtitle"));
0299   entry->setField(QStringLiteral("pub_year"),  mapValue(volumeMap, "publishedDate").left(4));
0300   entry->setField(QStringLiteral("author"),    mapValue(volumeMap, "authors"));
0301   // workaround for bug, where publisher can be enclosed in quotes
0302   QString pub = mapValue(volumeMap, "publisher");
0303   if(pub.startsWith(QLatin1Char('"')) && pub.endsWith(QLatin1Char('"'))) {
0304     pub.chop(1);
0305     pub = pub.remove(0, 1);
0306   }
0307   entry->setField(QStringLiteral("publisher"), pub);
0308   entry->setField(QStringLiteral("pages"),     mapValue(volumeMap, "pageCount"));
0309   entry->setField(QStringLiteral("language"),  mapValue(volumeMap, "language"));
0310   entry->setField(QStringLiteral("comments"),  mapValue(volumeMap, "description"));
0311 
0312   const QStringList catList = volumeMap.value(QStringLiteral("categories")).toStringList();
0313   // google is going to give us a lot of categories
0314   const QRegularExpression slash(QLatin1String("\\s*/\\s*"));
0315   QStringList cleanCategories;
0316   foreach(const QString& cat, catList) {
0317     // split them by the '/' character, too
0318     cleanCategories += cat.split(slash);
0319   }
0320   cleanCategories.sort();
0321   cleanCategories.removeDuplicates();
0322   // remove General since it's vague enough to not matter
0323   cleanCategories.removeOne(QStringLiteral("General"));
0324   entry->setField(QStringLiteral("keyword"), cleanCategories.join(FieldFormat::delimiterString()));
0325 
0326   QString isbn;
0327   foreach(const QVariant& idVariant, volumeMap.value(QLatin1String("industryIdentifiers")).toList()) {
0328     const QVariantMap idMap = idVariant.toMap();
0329     if(mapValue(idMap, "type") == QLatin1String("ISBN_10")) {
0330       isbn = mapValue(idMap, "identifier");
0331       break;
0332     } else if(mapValue(idMap, "type") == QLatin1String("ISBN_13")) {
0333       isbn = mapValue(idMap, "identifier");
0334       // allow isbn10 to override, so don't break here
0335     }
0336   }
0337   if(!isbn.isEmpty()) {
0338     ISBNValidator val(this);
0339     val.fixup(isbn);
0340     entry->setField(QStringLiteral("isbn"), isbn);
0341   }
0342 
0343   const QVariantMap imageMap = volumeMap.value(QStringLiteral("imageLinks")).toMap();
0344   if(imageMap.contains(QStringLiteral("small"))) {
0345     entry->setField(QStringLiteral("cover"), mapValue(imageMap, "small"));
0346   } else if(imageMap.contains(QStringLiteral("thumbnail"))) {
0347     entry->setField(QStringLiteral("cover"), mapValue(imageMap, "thumbnail"));
0348   } else if(imageMap.contains(QStringLiteral("smallThumbnail"))) {
0349     entry->setField(QStringLiteral("cover"), mapValue(imageMap, "smallThumbnail"));
0350   }
0351 
0352   if(optionalFields().contains(QStringLiteral("googlebook"))) {
0353     entry->setField(QStringLiteral("googlebook"), mapValue(volumeMap, "infoLink"));
0354   }
0355 }
0356 
0357 Tellico::Fetch::ConfigWidget* GoogleBookFetcher::configWidget(QWidget* parent_) const {
0358   return new GoogleBookFetcher::ConfigWidget(parent_, this);
0359 }
0360 
0361 QString GoogleBookFetcher::defaultName() {
0362   return i18n("Google Book Search");
0363 }
0364 
0365 QString GoogleBookFetcher::defaultIcon() {
0366   return favIcon("http://books.google.com");
0367 }
0368 
0369 Tellico::StringHash GoogleBookFetcher::allOptionalFields() {
0370   StringHash hash;
0371   hash[QStringLiteral("googlebook")] = i18n("Google Book Link");
0372   return hash;
0373 }
0374 
0375 GoogleBookFetcher::ConfigWidget::ConfigWidget(QWidget* parent_, const GoogleBookFetcher* fetcher_)
0376     : Fetch::ConfigWidget(parent_) {
0377   QGridLayout* l = new QGridLayout(optionsWidget());
0378   l->setSpacing(4);
0379   l->setColumnStretch(1, 10);
0380 
0381   int row = -1;
0382   QLabel* al = new QLabel(i18n("Registration is required for accessing this data source. "
0383                                "If you agree to the terms and conditions, <a href='%1'>sign "
0384                                "up for an account</a>, and enter your information below.",
0385                                 QLatin1String("https://code.google.com/apis/console")),
0386                           optionsWidget());
0387   al->setOpenExternalLinks(true);
0388   al->setWordWrap(true);
0389   ++row;
0390   l->addWidget(al, row, 0, 1, 2);
0391   // richtext gets weird with size
0392   al->setMinimumWidth(al->sizeHint().width());
0393 
0394   QLabel* label = new QLabel(i18n("Access key: "), optionsWidget());
0395   l->addWidget(label, ++row, 0);
0396 
0397   m_apiKeyEdit = new QLineEdit(optionsWidget());
0398   connect(m_apiKeyEdit, &QLineEdit::textChanged, this, &ConfigWidget::slotSetModified);
0399   l->addWidget(m_apiKeyEdit, row, 1);
0400   QString w = i18n("The default Tellico key may be used, but searching may fail due to reaching access limits.");
0401   label->setWhatsThis(w);
0402   m_apiKeyEdit->setWhatsThis(w);
0403   label->setBuddy(m_apiKeyEdit);
0404 
0405   l->setRowStretch(++row, 10);
0406 
0407   // now add additional fields widget
0408   addFieldsWidget(GoogleBookFetcher::allOptionalFields(), fetcher_ ? fetcher_->optionalFields() : QStringList());
0409 
0410   if(fetcher_ && fetcher_->m_apiKey != QLatin1String(GOOGLEBOOK_API_KEY)) {
0411     // only show the key if it is not the default Tellico one...
0412     // that way the user is prompted to apply for their own
0413     m_apiKeyEdit->setText(fetcher_->m_apiKey);
0414   }
0415 }
0416 
0417 void GoogleBookFetcher::ConfigWidget::saveConfigHook(KConfigGroup& config_) {
0418   QString apiKey = m_apiKeyEdit->text().trimmed();
0419   if(!apiKey.isEmpty()) {
0420     config_.writeEntry("API Key", apiKey);
0421   }
0422 }
0423 
0424 QString GoogleBookFetcher::ConfigWidget::preferredName() const {
0425   return GoogleBookFetcher::defaultName();
0426 }