File indexing completed on 2024-05-12 05:09:36

0001 /***************************************************************************
0002     Copyright (C) 2006-2020 Robby Stephenson <robby@periapsis.org>
0003  ***************************************************************************/
0004 
0005 /***************************************************************************
0006  *                                                                         *
0007  *   This program is free software; you can redistribute it and/or         *
0008  *   modify it under the terms of the GNU General Public License as        *
0009  *   published by the Free Software Foundation; either version 2 of        *
0010  *   the License or (at your option) version 3 or any later version        *
0011  *   accepted by the membership of KDE e.V. (or its successor approved     *
0012  *   by the membership of KDE e.V.), which shall act as a proxy            *
0013  *   defined in Section 14 of version 3 of the license.                    *
0014  *                                                                         *
0015  *   This program is distributed in the hope that it will be useful,       *
0016  *   but WITHOUT ANY WARRANTY; without even the implied warranty of        *
0017  *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the         *
0018  *   GNU General Public License for more details.                          *
0019  *                                                                         *
0020  *   You should have received a copy of the GNU General Public License     *
0021  *   along with this program.  If not, see <http://www.gnu.org/licenses/>. *
0022  *                                                                         *
0023  ***************************************************************************/
0024 
0025 #include "isbndbfetcher.h"
0026 #include "../collections/bookcollection.h"
0027 #include "../images/imagefactory.h"
0028 #include "../utils/guiproxy.h"
0029 #include "../utils/mapvalue.h"
0030 #include "../tellico_debug.h"
0031 
0032 #include <KLocalizedString>
0033 #include <KIO/Job>
0034 #include <KIO/JobUiDelegate>
0035 #include <KConfigGroup>
0036 #include <KJobWidgets/KJobWidgets>
0037 
0038 #include <QLineEdit>
0039 #include <QCheckBox>
0040 #include <QLabel>
0041 #include <QFile>
0042 #include <QTextStream>
0043 #include <QVBoxLayout>
0044 #include <QTextCodec>
0045 #include <QJsonDocument>
0046 #include <QJsonObject>
0047 #include <QUrlQuery>
0048 
0049 namespace {
0050   static const int ISBNDB_MAX_RETURNS_TOTAL = 25;
0051   static const char* ISBNDB_BASE_URL = "https://api2.isbndb.com";
0052 }
0053 
0054 using namespace Tellico;
0055 using Tellico::Fetch::ISBNdbFetcher;
0056 
0057 ISBNdbFetcher::ISBNdbFetcher(QObject* parent_)
0058     : Fetcher(parent_),
0059       m_limit(ISBNDB_MAX_RETURNS_TOTAL), m_total(-1), m_numResults(0),
0060       m_started(false),
0061       m_batchIsbn(false) {
0062 }
0063 
0064 ISBNdbFetcher::~ISBNdbFetcher() {
0065 }
0066 
0067 QString ISBNdbFetcher::source() const {
0068   return m_name.isEmpty() ? defaultName() : m_name;
0069 }
0070 
0071 bool ISBNdbFetcher::canFetch(int type) const {
0072   return type == Data::Collection::Book || type == Data::Collection::Bibtex;
0073 }
0074 
0075 bool ISBNdbFetcher::canSearch(Fetch::FetchKey k) const {
0076   return k == Title || k == Person || k == ISBN || k == Keyword;
0077 }
0078 
0079 void ISBNdbFetcher::readConfigHook(const KConfigGroup& config_) {
0080   QString k = config_.readEntry("API Key");
0081   if(!k.isEmpty()) {
0082     m_apiKey = k;
0083   }
0084   m_batchIsbn = config_.readEntry("Batch ISBN", false);
0085 }
0086 
0087 void ISBNdbFetcher::search() {
0088   m_started = true;
0089   m_total = -1;
0090   m_numResults = 0;
0091 
0092   // we only split ISBN when not doing batch searching
0093   QStringList searchTerms;
0094   if(request().key() == ISBN && !m_batchIsbn) {
0095     searchTerms = FieldFormat::splitValue(request().value());
0096   } else  {
0097     searchTerms += request().value();
0098   }
0099   foreach(const QString& searchTerm, searchTerms) {
0100     doSearch(searchTerm);
0101   }
0102   if(m_jobs.isEmpty()) {
0103     stop();
0104   }
0105 }
0106 
0107 void ISBNdbFetcher::continueSearch() {
0108   m_started = true;
0109 
0110   doSearch(request().value());
0111 }
0112 
0113 void ISBNdbFetcher::doSearch(const QString& term_) {
0114   const bool multipleIsbn = request().key() == ISBN && term_.contains(QLatin1Char(';'));
0115 
0116   QUrl u(QString::fromLatin1(ISBNDB_BASE_URL));
0117   switch(request().key()) {
0118     case Title:
0119       u.setPath(QStringLiteral("/books/") + term_);
0120       break;
0121 
0122     case Person:
0123       // the /books/query search endpoint seems to not work with the author column yet [2020-09-02]
0124       // so continue to user /author/query search (which may not return all the same info)
0125       u.setPath(QStringLiteral("/author/") + term_);
0126       break;
0127 
0128     case ISBN:
0129       if(multipleIsbn) {
0130         u.setPath(QStringLiteral("/books"));
0131       } else {
0132         u.setPath(QStringLiteral("/book/"));
0133         // can only grab first value
0134         QString v = term_.section(QLatin1Char(';'), 0);
0135         v.remove(QLatin1Char('-'));
0136         u.setPath(u.path() + v);
0137       }
0138       break;
0139 
0140     case Keyword:
0141       // the /books/query search endpoint seems to not work with the author column yet [2020-09-02]
0142       // so continue to user /author/query search (which may not return all the same info)
0143       u.setPath(QStringLiteral("/books/") + term_);
0144       {
0145          QUrlQuery q;
0146          q.addQueryItem(QStringLiteral("page"), QLatin1String("1"));
0147          q.addQueryItem(QStringLiteral("pageSize"), QString::number(ISBNDB_MAX_RETURNS_TOTAL));
0148          // disable beta searching
0149          q.addQueryItem(QStringLiteral("beta"), QLatin1String("0"));
0150          u.setQuery(q);
0151       }
0152       break;
0153 
0154     default:
0155       myWarning() << source() << "- key not recognized:" << request().key();
0156       stop();
0157       return;
0158   }
0159 
0160   if(m_apiKey.isEmpty()) {
0161     myDebug() << source() << "- empty API key";
0162     message(i18n("An access key is required to use this data source.")
0163             + QLatin1Char(' ') +
0164             i18n("Those values must be entered in the data source settings."), MessageHandler::Error);
0165     stop();
0166     return;
0167   }
0168 
0169 //  myDebug() << "url: " << u.url();
0170 
0171   QPointer<KIO::StoredTransferJob> job;
0172   if(multipleIsbn) {
0173     QString postData = request().value();
0174     postData = postData.replace(QLatin1Char(';'), QLatin1Char(','))
0175                        .remove(QLatin1Char('-'))
0176                        .remove(QLatin1Char(' '));
0177     postData.prepend(QStringLiteral("isbns="));
0178 //    myDebug() << "posting" << postData;
0179     job = KIO::storedHttpPost(postData.toUtf8(), u, KIO::HideProgressInfo);
0180   } else {
0181     job = KIO::storedGet(u, KIO::NoReload, KIO::HideProgressInfo);
0182   }
0183 
0184   job->addMetaData(QStringLiteral("customHTTPHeader"), QStringLiteral("Authorization: ") + m_apiKey);
0185   job->addMetaData(QStringLiteral("content-type"), QStringLiteral("application/json"));
0186   KJobWidgets::setWindow(job, GUI::Proxy::widget());
0187   connect(job.data(), &KJob::result, this, &ISBNdbFetcher::slotComplete);
0188   m_jobs << job;
0189 }
0190 
0191 void ISBNdbFetcher::endJob(KIO::StoredTransferJob* job_) {
0192   m_jobs.removeAll(job_);
0193   if(m_jobs.isEmpty())  {
0194     stop();
0195   }
0196 }
0197 
0198 void ISBNdbFetcher::stop() {
0199   if(!m_started) {
0200     return;
0201   }
0202   foreach(QPointer<KIO::StoredTransferJob> job, m_jobs) {
0203     if(job) {
0204       job->kill();
0205     }
0206   }
0207   m_jobs.clear();
0208   m_started = false;
0209   emit signalDone(this);
0210 }
0211 
0212 void ISBNdbFetcher::slotComplete(KJob* job_) {
0213   KIO::StoredTransferJob* job = static_cast<KIO::StoredTransferJob*>(job_);
0214 
0215   if(job->error()) {
0216     job->uiDelegate()->showErrorMessage();
0217     endJob(job);
0218     return;
0219   }
0220 
0221   const QByteArray data = job->data();
0222   if(data.isEmpty()) {
0223     myDebug() << "no data";
0224     endJob(job);
0225     return;
0226   }
0227 
0228 #if 0
0229   myWarning() << "Remove debug from isbndbfetcher.cpp";
0230   QFile file(QString::fromLatin1("/tmp/test-isbndb.json"));
0231   if(file.open(QIODevice::WriteOnly)) {
0232     QTextStream t(&file);
0233     t.setCodec("UTF-8");
0234     t << data;
0235   }
0236   file.close();
0237 #endif
0238 
0239   QJsonDocument doc = QJsonDocument::fromJson(data);
0240   QVariantMap result = doc.object().toVariantMap();
0241   QVariantList resultList;
0242   if(result.contains(QStringLiteral("book"))) {
0243     resultList += result.value(QStringLiteral("book"));
0244     m_total = 1;
0245   } else if(result.contains(QStringLiteral("books"))) {
0246     m_total = result.value(QStringLiteral("total")).toInt();
0247     resultList = result.value(QStringLiteral("books")).toList();
0248   } else if(result.contains(QStringLiteral("data"))) {
0249     m_total = result.value(QStringLiteral("total")).toInt();
0250     resultList = result.value(QStringLiteral("data")).toList();
0251   } else {
0252     QString msg = result.value(QStringLiteral("message")).toString();
0253     if(msg.isEmpty()) msg = result.value(QStringLiteral("errorMessage")).toString();
0254     myDebug() << "no results from ISBNDBFetcher:" << msg;
0255     message(msg, MessageHandler::Error);
0256     endJob(job);
0257     return;
0258   }
0259 //  myDebug() << "Total:" << m_total;
0260 
0261   Data::CollPtr coll(new Data::BookCollection(true));
0262 
0263   int count = 0;
0264   foreach(const QVariant& result, resultList) {
0265 //    myDebug() << "found result:" << result;
0266 
0267     Data::EntryPtr entry(new Data::Entry(coll));
0268     populateEntry(entry, result.toMap());
0269 
0270     FetchResult* r = new FetchResult(this, entry);
0271     m_entries.insert(r->uid, entry);
0272     emit signalResultFound(r);
0273     ++count;
0274     ++m_numResults;
0275     if(count >= m_limit) {
0276       break;
0277     }
0278   }
0279 
0280   endJob(job);
0281 }
0282 
0283 Tellico::Data::EntryPtr ISBNdbFetcher::fetchEntryHook(uint uid_) {
0284   if(!m_entries.contains(uid_)) {
0285     myDebug() << "no entry ptr";
0286     return Data::EntryPtr();
0287   }
0288 
0289   Data::EntryPtr entry = m_entries.value(uid_);
0290 
0291   // image might still be a URL
0292   const QString image_id = entry->field(QStringLiteral("cover"));
0293   if(image_id.contains(QLatin1Char('/'))) {
0294     const QString id = ImageFactory::addImage(QUrl::fromUserInput(image_id), true /* quiet */);
0295     if(id.isEmpty()) {
0296       message(i18n("The cover image could not be loaded."), MessageHandler::Warning);
0297     }
0298     // empty image ID is ok
0299     entry->setField(QStringLiteral("cover"), id);
0300   }
0301 
0302   return entry;
0303 }
0304 
0305 Tellico::Fetch::FetchRequest ISBNdbFetcher::updateRequest(Data::EntryPtr entry_) {
0306   QString isbn = entry_->field(QStringLiteral("isbn"));
0307   if(!isbn.isEmpty()) {
0308     return FetchRequest(Fetch::ISBN, isbn);
0309   }
0310 
0311   // optimistically try searching for title and rely on Collection::sameEntry() to figure things out
0312   QString t = entry_->field(QStringLiteral("title"));
0313   if(!t.isEmpty()) {
0314     return FetchRequest(Fetch::Title, t);
0315   }
0316   return FetchRequest();
0317 }
0318 
0319 void ISBNdbFetcher::populateEntry(Data::EntryPtr entry_, const QVariantMap& resultMap_) {
0320   static const QRegularExpression nonDigits(QStringLiteral("[^\\d]"));
0321   entry_->setField(QStringLiteral("title"), mapValue(resultMap_, "title"));
0322   entry_->setField(QStringLiteral("isbn"), mapValue(resultMap_, "isbn"));
0323   // "date_published" can be "2008-12-13" or "July 2012"
0324   QString pubYear = mapValue(resultMap_, "date_published").remove(nonDigits).left(4);
0325   entry_->setField(QStringLiteral("pub_year"), pubYear);
0326   QStringList authors;
0327   foreach(const QVariant& author, resultMap_.value(QLatin1String("authors")).toList()) {
0328     authors += author.toString();
0329   }
0330   entry_->setField(QStringLiteral("author"), authors.join(FieldFormat::delimiterString()));
0331   entry_->setField(QStringLiteral("publisher"), mapValue(resultMap_, "publisher"));
0332   entry_->setField(QStringLiteral("edition"), mapValue(resultMap_, "edition"));
0333   QString binding = mapValue(resultMap_, "binding");
0334   if(binding.isEmpty()) {
0335     binding = mapValue(resultMap_, "format");
0336   }
0337   if(binding.startsWith(QStringLiteral("Hardcover"))) {
0338     binding = QStringLiteral("Hardback");
0339   } else if(binding.startsWith(QStringLiteral("Paperback"))) {
0340     binding = QStringLiteral("Paperback");
0341   }
0342   if(!binding.isEmpty()) {
0343     entry_->setField(QStringLiteral("binding"), i18n(binding.toUtf8().constData()));
0344   }
0345   QStringList subjects;
0346   foreach(const QVariant& subject, resultMap_.value(QLatin1String("subjects")).toList()) {
0347     subjects += subject.toString();
0348   }
0349   entry_->setField(QStringLiteral("genre"), subjects.join(FieldFormat::delimiterString()));
0350   entry_->setField(QStringLiteral("cover"), mapValue(resultMap_, "image"));
0351   entry_->setField(QStringLiteral("pages"), mapValue(resultMap_, "pages"));
0352   entry_->setField(QStringLiteral("language"), mapValue(resultMap_, "language"));
0353   entry_->setField(QStringLiteral("plot"), mapValue(resultMap_, "overview"));
0354   if(mapValue(resultMap_, "overview").isEmpty()) {
0355     entry_->setField(QStringLiteral("plot"), mapValue(resultMap_, "synopsis"));
0356   }
0357 
0358   const QString dewey = mapValue(resultMap_, "dewey_decimal");
0359   if(!dewey.isEmpty() && optionalFields().contains(QStringLiteral("dewey"))) {
0360     if(!entry_->collection()->hasField(QStringLiteral("dewey"))) {
0361       Data::FieldPtr field(new Data::Field(QStringLiteral("dewey"), i18n("Dewey Decimal"), Data::Field::Line));
0362       field->setCategory(i18n("Publishing"));
0363       entry_->collection()->addField(field);
0364     }
0365     entry_->setField(QStringLiteral("dewey"), dewey);
0366   }
0367 }
0368 
0369 Tellico::Fetch::ConfigWidget* ISBNdbFetcher::configWidget(QWidget* parent_) const {
0370   return new ISBNdbFetcher::ConfigWidget(parent_, this);
0371 }
0372 
0373 QString ISBNdbFetcher::defaultName() {
0374   return i18n("ISBNdb.com");
0375 }
0376 
0377 QString ISBNdbFetcher::defaultIcon() {
0378   return favIcon("https://isbndb.com/sites/default/files/favicon_0.ico");
0379 }
0380 
0381 Tellico::StringHash ISBNdbFetcher::allOptionalFields() {
0382   // same ones as z3950fetcher
0383   StringHash hash;
0384   hash[QStringLiteral("dewey")] = i18nc("Dewey Decimal classification system", "Dewey Decimal");
0385   return hash;
0386 }
0387 
0388 ISBNdbFetcher::ConfigWidget::ConfigWidget(QWidget* parent_, const ISBNdbFetcher* fetcher_)
0389     : Fetch::ConfigWidget(parent_) {
0390   QGridLayout* l = new QGridLayout(optionsWidget());
0391   l->setSpacing(4);
0392   l->setColumnStretch(1, 10);
0393 
0394   int row = -1;
0395   QLabel* al = new QLabel(i18n("Registration is required for accessing this data source. "
0396                                "If you agree to the terms and conditions, <a href='%1'>sign "
0397                                "up for an account</a>, and enter your information below.",
0398                                 QStringLiteral("https://isbndb.com/isbn-database")),
0399                           optionsWidget());
0400   al->setOpenExternalLinks(true);
0401   al->setWordWrap(true);
0402   ++row;
0403   l->addWidget(al, row, 0, 1, 2);
0404   // richtext gets weird with size
0405   al->setMinimumWidth(al->sizeHint().width());
0406 
0407   QLabel* label = new QLabel(i18n("Access key: "), optionsWidget());
0408   l->addWidget(label, ++row, 0);
0409 
0410   m_apiKeyEdit = new QLineEdit(optionsWidget());
0411   connect(m_apiKeyEdit, &QLineEdit::textChanged, this, &ConfigWidget::slotSetModified);
0412   l->addWidget(m_apiKeyEdit, row, 1);
0413   label->setBuddy(m_apiKeyEdit);
0414 
0415   m_enableBatchIsbn = new QCheckBox(i18n("Enable batch ISBN searching (requires Premium or Pro plan)"), optionsWidget());
0416   connect(m_enableBatchIsbn, &QAbstractButton::clicked, this, &ConfigWidget::slotSetModified);
0417   ++row;
0418   l->addWidget(m_enableBatchIsbn, row, 0, 1, 2);
0419   QString w = i18n("Batch searching for ISBN values is faster but only available for Premium or Pro plans.");
0420   m_enableBatchIsbn->setWhatsThis(w);
0421 
0422   l->setRowStretch(++row, 10);
0423 
0424   if(fetcher_) {
0425     m_apiKeyEdit->setText(fetcher_->m_apiKey);
0426     m_enableBatchIsbn->setChecked(fetcher_->m_batchIsbn);
0427   } else { //defaults
0428     m_enableBatchIsbn->setChecked(false);
0429   }
0430 
0431   // now add additional fields widget
0432   addFieldsWidget(ISBNdbFetcher::allOptionalFields(), fetcher_ ? fetcher_->optionalFields() : QStringList());
0433 }
0434 
0435 void ISBNdbFetcher::ConfigWidget::saveConfigHook(KConfigGroup& config_) {
0436   QString apiKey = m_apiKeyEdit->text().trimmed();
0437   if(!apiKey.isEmpty()) {
0438     config_.writeEntry("API Key", apiKey);
0439   }
0440   config_.writeEntry("Batch ISBN", m_enableBatchIsbn->isChecked());
0441 }
0442 
0443 QString ISBNdbFetcher::ConfigWidget::preferredName() const {
0444   return ISBNdbFetcher::defaultName();
0445 }