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 }