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 }