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

0001 /***************************************************************************
0002     Copyright (C) 2009 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 "openlibraryfetcher.h"
0026 #include "../collections/bookcollection.h"
0027 #include "../collections/comicbookcollection.h"
0028 #include "../images/imagefactory.h"
0029 #include "../utils/isbnvalidator.h"
0030 #include "../utils/guiproxy.h"
0031 #include "../utils/mapvalue.h"
0032 #include "../entry.h"
0033 #include "../core/filehandler.h"
0034 #include "../tellico_debug.h"
0035 
0036 #include <KLocalizedString>
0037 #include <KIO/Job>
0038 #include <KJobUiDelegate>
0039 #include <KJobWidgets/KJobWidgets>
0040 
0041 #include <QLabel>
0042 #include <QFile>
0043 #include <QTextStream>
0044 #include <QGridLayout>
0045 #include <QTextCodec>
0046 #include <QJsonDocument>
0047 #include <QJsonObject>
0048 #include <QJsonArray>
0049 #include <QUrlQuery>
0050 
0051 namespace {
0052   static const char* OPENLIBRARY_QUERY_URL = "https://openlibrary.org/query.json";
0053   static const char* OPENLIBRARY_AUTHOR_QUERY_URL = "https://openlibrary.org/search/authors.json";
0054 }
0055 
0056 using namespace Tellico;
0057 using Tellico::Fetch::OpenLibraryFetcher;
0058 
0059 OpenLibraryFetcher::OpenLibraryFetcher(QObject* parent_)
0060     : Fetcher(parent_), m_started(false) {
0061 }
0062 
0063 OpenLibraryFetcher::~OpenLibraryFetcher() {
0064 }
0065 
0066 QString OpenLibraryFetcher::source() const {
0067   return m_name.isEmpty() ? defaultName() : m_name;
0068 }
0069 
0070 bool OpenLibraryFetcher::canSearch(Fetch::FetchKey k) const {
0071   return k == Title || k == Person || k == ISBN || k == LCCN;
0072 }
0073 
0074 bool OpenLibraryFetcher::canFetch(int type) const {
0075   return type == Data::Collection::Book ||
0076       type == Data::Collection::Bibtex ||
0077       type == Data::Collection::ComicBook;
0078 }
0079 
0080 void OpenLibraryFetcher::readConfigHook(const KConfigGroup&) {
0081 }
0082 
0083 void OpenLibraryFetcher::search() {
0084   m_started = true;
0085   // we only split ISBN and LCCN values
0086   QStringList searchTerms;
0087   if(request().key() == ISBN || request().key() == LCCN) {
0088     searchTerms = FieldFormat::splitValue(request().value());
0089   } else  {
0090     searchTerms += request().value();
0091   }
0092   foreach(const QString& searchTerm, searchTerms) {
0093     doSearch(searchTerm);
0094   }
0095   if(m_jobs.isEmpty()) {
0096     stop();
0097   }
0098 }
0099 
0100 void OpenLibraryFetcher::doSearch(const QString& term_) {
0101   QUrl u(QString::fromLatin1(OPENLIBRARY_QUERY_URL));
0102   QUrlQuery q;
0103   // books are type/edition
0104   q.addQueryItem(QStringLiteral("type"), QStringLiteral("/type/edition"));
0105 
0106   switch(request().key()) {
0107     case Title:
0108       q.addQueryItem(QStringLiteral("title"), term_);
0109       break;
0110 
0111     case Person:
0112       {
0113         QString author = getAuthorKeys(term_);
0114         if(author.isEmpty()) {
0115           myLog() << "No matching authors found";
0116           return;
0117         }
0118         author.prepend(QLatin1String("/authors/"));
0119         q.addQueryItem(QStringLiteral("authors"), author);
0120       }
0121       break;
0122 
0123     case ISBN:
0124       {
0125         const QString isbn = ISBNValidator::cleanValue(term_);
0126         if(isbn.size() > 10) {
0127           q.addQueryItem(QStringLiteral("isbn_13"), isbn);
0128         } else {
0129           q.addQueryItem(QStringLiteral("isbn_10"), isbn);
0130         }
0131       }
0132       break;
0133 
0134     case LCCN:
0135       q.addQueryItem(QStringLiteral("lccn"), term_);
0136       break;
0137 
0138     case Raw:
0139       {
0140         // raw query comes in as a query string, combine it
0141         QUrlQuery newQuery(term_);
0142         foreach(auto item, newQuery.queryItems()) {
0143           q.addQueryItem(item.first, item.second);
0144         }
0145       }
0146       break;
0147 
0148     default:
0149       myWarning() << source() << "- key not recognized:" << request().key();
0150       return;
0151   }
0152   q.addQueryItem(QStringLiteral("*"), QString());
0153   u.setQuery(q);
0154 //  myDebug() << u;
0155 
0156   QPointer<KIO::StoredTransferJob> job = KIO::storedGet(u, KIO::NoReload, KIO::HideProgressInfo);
0157   KJobWidgets::setWindow(job, GUI::Proxy::widget());
0158   connect(job.data(), &KJob::result, this, &OpenLibraryFetcher::slotComplete);
0159   m_jobs << job;
0160 }
0161 
0162 void OpenLibraryFetcher::endJob(KIO::StoredTransferJob* job_) {
0163   m_jobs.removeAll(job_);
0164   if(m_jobs.isEmpty())  {
0165     stop();
0166   }
0167 }
0168 
0169 void OpenLibraryFetcher::stop() {
0170   if(!m_started) {
0171     return;
0172   }
0173   foreach(QPointer<KIO::StoredTransferJob> job, m_jobs) {
0174     if(job) {
0175       job->kill();
0176     }
0177   }
0178   m_jobs.clear();
0179   m_started = false;
0180   emit signalDone(this);
0181 }
0182 
0183 Tellico::Data::EntryPtr OpenLibraryFetcher::fetchEntryHook(uint uid_) {
0184   Data::EntryPtr entry = m_entries.value(uid_);
0185   if(!entry) {
0186     myWarning() << "no entry in dict";
0187     return Data::EntryPtr();
0188   }
0189 
0190   // if the entry is not set, go ahead and try to fetch it
0191   if(entry->field(QStringLiteral("cover")).isEmpty()) {
0192     const QString isbn = ISBNValidator::cleanValue(entry->field(QStringLiteral("isbn")));
0193     if(!isbn.isEmpty()) {
0194       QUrl imageUrl(QStringLiteral("http://covers.openlibrary.org/b/isbn/%1-M.jpg?default=false").arg(isbn));
0195       const QString id = ImageFactory::addImage(imageUrl, true);
0196       if(!id.isEmpty()) {
0197         entry->setField(QStringLiteral("cover"), id);
0198       }
0199     }
0200   }
0201 
0202   return entry;
0203 }
0204 
0205 Tellico::Fetch::FetchRequest OpenLibraryFetcher::updateRequest(Data::EntryPtr entry_) {
0206   const QString isbn = entry_->field(QStringLiteral("isbn"));
0207   if(!isbn.isEmpty()) {
0208     return FetchRequest(ISBN, isbn);
0209   }
0210   const QString lccn = entry_->field(QStringLiteral("lccn"));
0211   if(!lccn.isEmpty()) {
0212     return FetchRequest(LCCN, lccn);
0213   }
0214   const QString title = entry_->field(QStringLiteral("title"));
0215   if(title.isEmpty()) {
0216     return FetchRequest();
0217   }
0218 
0219   // can't search by authors, for now, since the author value is a key reference
0220   // can't search by pub year since many of the publish_date fields are a full month, day, year
0221 
0222   const QString pub = entry_->field(QStringLiteral("publisher"));
0223   auto publishers = FieldFormat::splitValue(pub);
0224   if(!publishers.isEmpty()) {
0225     QUrlQuery q;
0226     q.addQueryItem(QStringLiteral("title"), title);
0227     q.addQueryItem(QStringLiteral("publishers"), publishers.first());
0228     return FetchRequest(Raw, q.query());
0229   }
0230 
0231   // fallback to just title search
0232   return FetchRequest(Title, title);
0233 }
0234 
0235 void OpenLibraryFetcher::slotComplete(KJob* job_) {
0236   KIO::StoredTransferJob* job = static_cast<KIO::StoredTransferJob*>(job_);
0237 
0238   if(job->error()) {
0239     job->uiDelegate()->showErrorMessage();
0240     endJob(job);
0241     return;
0242   }
0243 
0244   QByteArray data = job->data();
0245   if(data.isEmpty()) {
0246     myDebug() << "no data";
0247     endJob(job);
0248     return;
0249   }
0250 
0251 #if 0
0252   myWarning() << "Remove debug from openlibraryfetcher.cpp";
0253   QFile f(QString::fromLatin1("/tmp/test.json"));
0254   if(f.open(QIODevice::WriteOnly)) {
0255     QTextStream t(&f);
0256     t.setCodec("UTF-8");
0257     t << data;
0258   }
0259   f.close();
0260 #endif
0261 
0262   QJsonDocument doc = QJsonDocument::fromJson(data);
0263   QJsonArray array = doc.array();
0264   if(array.isEmpty()) {
0265 //    myDebug() << "no results";
0266     endJob(job);
0267     return;
0268   }
0269 
0270   Data::CollPtr coll;
0271   if(request().collectionType() == Data::Collection::ComicBook) {
0272     coll = new Data::ComicBookCollection(true);
0273   } else {
0274     coll = new Data::BookCollection(true);
0275   }
0276   if(!coll->hasField(QStringLiteral("openlibrary")) && optionalFields().contains(QStringLiteral("openlibrary"))) {
0277     Data::FieldPtr field(new Data::Field(QStringLiteral("openlibrary"), i18n("OpenLibrary Link"), Data::Field::URL));
0278     field->setCategory(i18n("General"));
0279     coll->addField(field);
0280   }
0281 
0282   static const QRegularExpression yearRx(QStringLiteral("\\d{4}"));
0283   for(int i = 0; i < array.count(); i++) {
0284     // be sure to check that the fetcher has not been stopped
0285     // crashes can occur if not
0286     if(!m_started) {
0287       break;
0288     }
0289 
0290     Data::EntryPtr entry(new Data::Entry(coll));
0291     QVariantMap resultMap = array.at(i).toObject().toVariantMap();
0292     entry->setField(QStringLiteral("title"), mapValue(resultMap, "title"));
0293     // only allow comic format for comic book collections
0294     QString binding = mapValue(resultMap, "physical_format");
0295     if(coll->type() == Data::Collection::ComicBook &&
0296        (!binding.isEmpty() && binding != QLatin1String("comic"))) {
0297       myLog() << "Skipping non-comic result:" << entry->title();
0298       continue;
0299     }
0300     if(binding.toLower() == QLatin1String("hardcover")) {
0301       binding = QStringLiteral("Hardback");
0302     } else if(binding.contains(QStringLiteral("paperback"), Qt::CaseInsensitive)) {
0303       binding = QStringLiteral("Paperback");
0304     }
0305     if(!binding.isEmpty()) {
0306       entry->setField(QStringLiteral("binding"), i18n(binding.toUtf8().constData()));
0307     }
0308 
0309     entry->setField(QStringLiteral("subtitle"), mapValue(resultMap, "subtitle"));
0310     auto yearMatch = yearRx.match(mapValue(resultMap, "publish_date"));
0311     if(yearMatch.hasMatch()) {
0312       entry->setField(QStringLiteral("pub_year"), yearMatch.captured());
0313     }
0314     QString isbn = mapValue(resultMap, "isbn_10");
0315     if(isbn.isEmpty()) {
0316       isbn = mapValue(resultMap, "isbn_13");
0317     }
0318     const QString isbnName(QStringLiteral("isbn"));
0319     if(!isbn.isEmpty()) {
0320       if(!coll->hasField(isbnName)) {
0321         coll->addField(Data::Field::createDefaultField(Data::Field::IsbnField));
0322       }
0323       ISBNValidator val(this);
0324       val.fixup(isbn);
0325       entry->setField(isbnName, isbn);
0326     }
0327     const QString lccnName(QStringLiteral("lccn"));
0328     const QString lccn = mapValue(resultMap, "lccn");
0329     if(!lccn.isEmpty()) {
0330       if(!coll->hasField(lccnName)) {
0331         coll->addField(Data::Field::createDefaultField(Data::Field::LccnField));
0332       }
0333       entry->setField(lccnName, lccn);
0334     }
0335     entry->setField(QStringLiteral("genre"), mapValue(resultMap, "genres"));
0336     entry->setField(QStringLiteral("keyword"), mapValue(resultMap, "subjects"));
0337     entry->setField(QStringLiteral("edition"), mapValue(resultMap, "edition_name"));
0338     entry->setField(QStringLiteral("publisher"), mapValue(resultMap, "publishers"));
0339     entry->setField(QStringLiteral("series"), mapValue(resultMap, "series"));
0340     entry->setField(QStringLiteral("pages"), mapValue(resultMap, "number_of_pages"));
0341     entry->setField(QStringLiteral("comments"), mapValue(resultMap, "notes", "value"));
0342 
0343     if(optionalFields().contains(QStringLiteral("openlibrary"))) {
0344       entry->setField(QStringLiteral("openlibrary"), QLatin1String("https://openlibrary.org") + mapValue(resultMap, "key"));
0345     }
0346 
0347     QStringList authors;
0348     foreach(const QVariant& authorMap, resultMap.value(QLatin1String("authors")).toList()) {
0349       const QString key = mapValue(authorMap.toMap(), "key");
0350       if(!key.isEmpty()) {
0351         QUrl authorUrl(QString::fromLatin1(OPENLIBRARY_QUERY_URL));
0352         QUrlQuery q;
0353         q.addQueryItem(QStringLiteral("type"), QStringLiteral("/type/author"));
0354         q.addQueryItem(QStringLiteral("key"), key);
0355         q.addQueryItem(QStringLiteral("name"), QString());
0356         authorUrl.setQuery(q);
0357 
0358         QString output = FileHandler::readTextFile(authorUrl, true /*quiet*/);
0359         QJsonDocument doc = QJsonDocument::fromJson(output.toUtf8());
0360         QJsonArray array = doc.array();
0361         QVariantMap authorResult = array.isEmpty() ? QVariantMap() : array.at(0).toObject().toVariantMap();
0362         const QString name = mapValue(authorResult, "name");
0363         if(!name.isEmpty()) {
0364           authors << name;
0365         }
0366       }
0367     }
0368     if(!authors.isEmpty()) {
0369       entry->setField(QStringLiteral("author"), authors.join(FieldFormat::delimiterString()));
0370     }
0371 
0372     QStringList langs;
0373     foreach(const QVariant& langMap, resultMap.value(QLatin1String("languages")).toList()) {
0374       const QString key = mapValue(langMap.toMap(), "key");
0375       if(!key.isEmpty()) {
0376         QUrl langUrl(QString::fromLatin1(OPENLIBRARY_QUERY_URL));
0377         QUrlQuery q;
0378         q.addQueryItem(QStringLiteral("type"), QStringLiteral("/type/language"));
0379         q.addQueryItem(QStringLiteral("key"), key);
0380         q.addQueryItem(QStringLiteral("name"), QString());
0381         langUrl.setQuery(q);
0382 
0383         QString output = FileHandler::readTextFile(langUrl, true /*quiet*/, true /*utf8*/);
0384         QJsonDocument doc = QJsonDocument::fromJson(output.toUtf8());
0385         QJsonArray array = doc.array();
0386         QVariantMap langResult = array.isEmpty() ? QVariantMap() : array.at(0).toObject().toVariantMap();
0387         const QString name = mapValue(langResult, "name");
0388         if(!name.isEmpty()) {
0389           langs << i18n(name.toUtf8().constData());
0390         }
0391       }
0392     }
0393     if(!langs.isEmpty()) {
0394       entry->setField(QStringLiteral("language"), langs.join(FieldFormat::delimiterString()));
0395     }
0396 
0397     FetchResult* r = new FetchResult(this, entry);
0398     m_entries.insert(r->uid, entry);
0399     emit signalResultFound(r);
0400   }
0401 
0402 //  m_start = m_entries.count();
0403 //  m_hasMoreResults = m_start <= m_total;
0404   m_hasMoreResults = false; // for now, no continued searches
0405   endJob(job);
0406 }
0407 
0408 QString OpenLibraryFetcher::getAuthorKeys(const QString& term_) {
0409   QUrl u(QString::fromLatin1(OPENLIBRARY_AUTHOR_QUERY_URL));
0410   QUrlQuery q;
0411   q.addQueryItem(QStringLiteral("q"), term_);
0412   u.setQuery(q);
0413 
0414 //  myLog() << "Searching for authors:" << u.toDisplayString();
0415   QString output = FileHandler::readTextFile(u, true /*quiet*/, true /*utf8*/);
0416 #if 0
0417   myWarning() << "Remove author debug from openlibraryfetcher.cpp";
0418   QFile f(QString::fromLatin1("/tmp/test-openlibraryauthor.json"));
0419   if(f.open(QIODevice::WriteOnly)) {
0420     QTextStream t(&f);
0421     t.setCodec("UTF-8");
0422     t << output;
0423   }
0424   f.close();
0425 #endif
0426   const QJsonDocument doc = QJsonDocument::fromJson(output.toUtf8());
0427   const auto obj = doc.object();
0428   myLog() << "Found" << obj.value(QLatin1String("numFound")).toInt() << "authors";
0429   // right now, only use the first
0430   const auto array = obj.value(QLatin1String("docs")).toArray();
0431   if(array.isEmpty()) {
0432     return QString();
0433   }
0434   const auto obj1 = array.at(0).toObject();
0435   myLog() << "Using" << obj1.value(QLatin1String("name")).toString();
0436   return obj1.value(QLatin1String("key")).toString();
0437 }
0438 
0439 Tellico::Fetch::ConfigWidget* OpenLibraryFetcher::configWidget(QWidget* parent_) const {
0440   return new OpenLibraryFetcher::ConfigWidget(parent_, this);
0441 }
0442 
0443 QString OpenLibraryFetcher::defaultName() {
0444   return QStringLiteral("Open Library"); // no translation
0445 }
0446 
0447 QString OpenLibraryFetcher::defaultIcon() {
0448   return favIcon("https://openlibrary.org");
0449 }
0450 
0451 Tellico::StringHash OpenLibraryFetcher::allOptionalFields() {
0452   StringHash hash;
0453   hash[QStringLiteral("openlibrary")] = i18n("OpenLibrary Link");
0454   return hash;
0455 }
0456 
0457 OpenLibraryFetcher::ConfigWidget::ConfigWidget(QWidget* parent_, const OpenLibraryFetcher* fetcher_)
0458     : Fetch::ConfigWidget(parent_) {
0459   QVBoxLayout* l = new QVBoxLayout(optionsWidget());
0460   l->addWidget(new QLabel(i18n("This source has no options."), optionsWidget()));
0461   l->addStretch();
0462 
0463   // now add additional fields widget
0464   addFieldsWidget(OpenLibraryFetcher::allOptionalFields(), fetcher_ ? fetcher_->optionalFields() : QStringList());
0465 }
0466 
0467 void OpenLibraryFetcher::ConfigWidget::saveConfigHook(KConfigGroup&) {
0468 }
0469 
0470 QString OpenLibraryFetcher::ConfigWidget::preferredName() const {
0471   return OpenLibraryFetcher::defaultName();
0472 }