Git commit cd21106a0da2b222656ef2ff95945a3808432c37 by Robby Stephenson. Committed on 20/09/2020 at 00:52. Pushed by rstephenson into branch 'master'.
Add Numista data source M +4 -0 ChangeLog M +9 -0 doc/configuration.docbook M +1 -0 src/fetch/CMakeLists.txt M +2 -1 src/fetch/fetch.h M +3 -1 src/fetch/fetcherinitializer.cpp M +1 -0 src/fetch/fetchmanager.cpp A +466 -0 src/fetch/numistafetcher.cpp [License: GPL (v2/3)] A +131 -0 src/fetch/numistafetcher.h [License: GPL (v2/3)] M +8 -0 src/tests/CMakeLists.txt A +98 -0 src/tests/numistafetchertest.cpp [License: GPL (v2/3)] C +12 -84 src/tests/numistafetchertest.h [from: src/fetch/fetch.h - 057% similarity] M +3 -0 src/tests/tellicotest.config https://invent.kde.org/office/tellico/commit/cd21106a0da2b222656ef2ff95945a3808432c37 diff --git a/ChangeLog b/ChangeLog index 3c969582..5ef04b38 100644 --- a/ChangeLog +++ b/ChangeLog @@ -1,3 +1,7 @@ +2020-09-17 Robby Stephenson <[email protected]> + + * Added data source for Numista.com. + 2020-09-16 Robby Stephenson <[email protected]> * Enabled general keyword search for MusicBrainz data source (Bug 426560). diff --git a/doc/configuration.docbook b/doc/configuration.docbook index 08ad65fe..800d38d2 100644 --- a/doc/configuration.docbook +++ b/doc/configuration.docbook @@ -189,6 +189,7 @@ while the full list is <ulink url="https://tellico-project.org/data-sources">ava <listitem><simpara><link linkend="sru">SRU servers</link>,</simpara></listitem> <!-- coins --> <listitem><simpara><link linkend="colnect">Colnect</link>,</simpara></listitem> +<listitem><simpara><link linkend="numista">Numista</link>,</simpara></listitem> <!-- others --> <listitem><simpara><link linkend="externalexec">other external scripts or applications</link>, and</simpara></listitem> <listitem><simpara><link linkend="multiple-sources">combinations of any of the above sources</link>.</simpara></listitem> @@ -465,6 +466,14 @@ The <ulink url="http://www.imdb.com">Internet Movie Database</ulink> provides in </para> </sect3> +<sect3 id="numista"> +<title>Numista</title> +<para> +<ulink url="https://numista.com">Numista</ulink> is a world coin catalog which grows thanks to member contributions, offering +online collection management, tools to easily exchange with other collectors, and a forum. +</para> +</sect3> + </sect2> <sect2 id="variety-type-sources"> diff --git a/src/fetch/CMakeLists.txt b/src/fetch/CMakeLists.txt index bf04e0bf..9f34e0ab 100644 --- a/src/fetch/CMakeLists.txt +++ b/src/fetch/CMakeLists.txt @@ -50,6 +50,7 @@ SET(fetch_STAT_SRCS mrlookupfetcher.cpp multifetcher.cpp musicbrainzfetcher.cpp + numistafetcher.cpp omdbfetcher.cpp openlibraryfetcher.cpp springerfetcher.cpp diff --git a/src/fetch/fetch.h b/src/fetch/fetch.h index 22d0878b..9d6abe05 100644 --- a/src/fetch/fetch.h +++ b/src/fetch/fetch.h @@ -104,7 +104,8 @@ enum Type { MobyGames, ComicVine, KinoTeatr, - Colnect + Colnect, + Numista }; } diff --git a/src/fetch/fetcherinitializer.cpp b/src/fetch/fetcherinitializer.cpp index 71ae2533..e902f862 100644 --- a/src/fetch/fetcherinitializer.cpp +++ b/src/fetch/fetcherinitializer.cpp @@ -1,5 +1,5 @@ /*************************************************************************** - Copyright (C) 2009-2011 Robby Stephenson <[email protected]> + Copyright (C) 2009-2020 Robby Stephenson <[email protected]> ***************************************************************************/ /*************************************************************************** @@ -72,6 +72,7 @@ #include "comicvinefetcher.h" #include "kinoteatrfetcher.h" #include "colnectfetcher.h" +#include "numistafetcher.h" /** * Ideally, I'd like these initializations to be in each cpp file for each collection type @@ -125,6 +126,7 @@ Tellico::Fetch::FetcherInitializer::FetcherInitializer() { RegisterFetcher<Fetch::ComicVineFetcher> registerComicVine(ComicVine); RegisterFetcher<Fetch::KinoTeatrFetcher> registerTeatr(KinoTeatr); RegisterFetcher<Fetch::ColnectFetcher> registerColnect(Colnect); + RegisterFetcher<Fetch::NumistaFetcher> registerNumista(Numista); Fetch::Manager::self()->loadFetchers(); } diff --git a/src/fetch/fetchmanager.cpp b/src/fetch/fetchmanager.cpp index a6895c7a..eb61e43d 100644 --- a/src/fetch/fetchmanager.cpp +++ b/src/fetch/fetchmanager.cpp @@ -324,6 +324,7 @@ Tellico::Fetch::FetcherVec Manager::defaultFetchers() { FETCHER_ADD(IMDB); // coins and stamps FETCHER_ADD(Colnect); + FETCHER_ADD(Numista); QStringList langs = QLocale().uiLanguages(); if(langs.first().contains(QLatin1Char('-'))) { // I'm not sure QT always include two-letter locale codes diff --git a/src/fetch/numistafetcher.cpp b/src/fetch/numistafetcher.cpp new file mode 100644 index 00000000..af27fdb3 --- /dev/null +++ b/src/fetch/numistafetcher.cpp @@ -0,0 +1,466 @@ +/*************************************************************************** + Copyright (C) 2020 Robby Stephenson <[email protected]> + ***************************************************************************/ + +/*************************************************************************** + * * + * This program is free software; you can redistribute it and/or * + * modify it under the terms of the GNU General Public License as * + * published by the Free Software Foundation; either version 2 of * + * the License or (at your option) version 3 or any later version * + * accepted by the membership of KDE e.V. (or its successor approved * + * by the membership of KDE e.V.), which shall act as a proxy * + * defined in Section 14 of version 3 of the license. * + * * + * This program is distributed in the hope that it will be useful, * + * but WITHOUT ANY WARRANTY; without even the implied warranty of * + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * + * GNU General Public License for more details. * + * * + * You should have received a copy of the GNU General Public License * + * along with this program. If not, see <http://www.gnu.org/licenses/>. * + * * + ***************************************************************************/ + +#include "numistafetcher.h" +#include "../collections/coincollection.h" +#include "../entry.h" +#include "../images/imagefactory.h" +#include "../gui/combobox.h" +#include "../utils/guiproxy.h" +#include "../utils/string_utils.h" +#include "../tellico_debug.h" + +#include <KLocalizedString> +#include <KIO/Job> +#include <KJobUiDelegate> +#include <KJobWidgets/KJobWidgets> +#include <KConfigGroup> + +#include <QLabel> +#include <QLineEdit> +#include <QFile> +#include <QTextStream> +#include <QGridLayout> +#include <QUrlQuery> +#include <QJsonDocument> +#include <QJsonArray> +#include <QJsonObject> +#include <QStandardPaths> + +namespace { + static const int NUMISTA_MAX_RETURNS_TOTAL = 20; + static const char* NUMISTA_API_URL = "https://api.numista.com/api/v1"; + static const char* NUMISTA_MAGIC_TOKEN = "2e19b8f32c5e8fbd96aeb2c0590d70458ef81d5b0657b1f6741685e1f9cf7a0983d7d0e0a2c69bcca7cfb4c08fde1c5a562e083e2d44a492a5e4b9c3d2a42a7c536a99f8511bfdbca9fb6d29f587fbbf"; +} + +using namespace Tellico; +using Tellico::Fetch::NumistaFetcher; + +NumistaFetcher::NumistaFetcher(QObject* parent_) + : Fetcher(parent_) + , m_limit(NUMISTA_MAX_RETURNS_TOTAL) + , m_total(-1) + , m_page(1) + , m_job(nullptr) + , m_locale(QStringLiteral("en")) + , m_started(false) { +} + +NumistaFetcher::~NumistaFetcher() { +} + +QString NumistaFetcher::source() const { + return m_name.isEmpty() ? defaultName() : m_name; +} + +bool NumistaFetcher::canFetch(int type) const { + return type == Data::Collection::Coin; +} + +void NumistaFetcher::readConfigHook(const KConfigGroup& config_) { + QString k = config_.readEntry("API Key"); + if(!k.isEmpty()) { + m_apiKey = k; + } + k = config_.readEntry("Locale", "en"); + if(!k.isEmpty()) { + m_locale = k.toLower(); + } +} + +void NumistaFetcher::setLimit(int limit_) { + m_limit = qBound(1, limit_, NUMISTA_MAX_RETURNS_TOTAL); +} + +void NumistaFetcher::search() { + m_started = true; + m_total = -1; + m_page = 1; + m_year.clear(); + doSearch(); +} + +void NumistaFetcher::continueSearch() { + m_started = true; + doSearch(); +} + +void NumistaFetcher::doSearch() { + QUrl u(QString::fromLatin1(NUMISTA_API_URL)); + // all searches are for coins + u.setPath(u.path() + QStringLiteral("/coins")); + + if(m_apiKey.isEmpty()) { + m_apiKey = Tellico::reverseObfuscate(NUMISTA_MAGIC_TOKEN); + } + + // pull out year, keep the regexp a little loose + QRegularExpression yearRX(QStringLiteral("[0-9]{4}")); + QRegularExpressionMatch match = yearRX.match(request().value); + if(match.hasMatch()) { + m_year = match.captured(0); + } + + QString queryString; + switch(request().key) { + case Keyword: + queryString = request().value; + break; + + default: + myWarning() << "key not recognized: " << request().key; + stop(); + return; + } + QUrlQuery q; + q.addQueryItem(QStringLiteral("q"), queryString); + q.addQueryItem(QStringLiteral("count"), QString::number(m_limit)); + q.addQueryItem(QStringLiteral("page"), QString::number(m_page)); + q.addQueryItem(QStringLiteral("lang"), m_locale); + u.setQuery(q); +// myDebug() << "url: " << u.url(); + + m_job = KIO::storedGet(u, KIO::NoReload, KIO::HideProgressInfo); + m_job->addMetaData(QStringLiteral("customHTTPHeader"), QStringLiteral("Numista-API-Key: ") + m_apiKey); + KJobWidgets::setWindow(m_job, GUI::Proxy::widget()); + connect(m_job.data(), &KJob::result, + this, &NumistaFetcher::slotComplete); +} + +void NumistaFetcher::stop() { + if(!m_started) { + return; + } + if(m_job) { + m_job->kill(); + m_job = nullptr; + } + m_started = false; + emit signalDone(this); +} + +void NumistaFetcher::slotComplete(KJob* ) { + if(m_job->error()) { + m_job->uiDelegate()->showErrorMessage(); + stop(); + return; + } + + QByteArray data = m_job->data(); + if(data.isEmpty()) { + myDebug() << "no data"; + stop(); + return; + } + // see bug 319662. If fetcher is cancelled, job is killed + // if the pointer is retained, it gets double-deleted + m_job = nullptr; + +#if 0 + myWarning() << "Remove debug from numistafetcher.cpp"; + QFile f(QStringLiteral("/tmp/test.json")); + if(f.open(QIODevice::WriteOnly)) { + QTextStream t(&f); + t.setCodec("UTF-8"); + t << data; + } + f.close(); +#endif + + QJsonParseError jsonError; + QJsonDocument doc = QJsonDocument::fromJson(data, &jsonError); + if(doc.isNull()) { + myDebug() << "null JSON document:" << jsonError.errorString(); + message(jsonError.errorString(), MessageHandler::Error); + stop(); + return; + } + QJsonObject obj = doc.object(); + + // check for error + if(obj.contains(QStringLiteral("error"))) { + const QString msg = obj.value(QStringLiteral("error")).toString(); + message(msg, MessageHandler::Error); + myDebug() << "NunmistaFetcher -" << msg; + stop(); + return; + } + + m_total = obj.value(QLatin1String("count")).toString().toInt(); + + int count = 0; + QJsonArray results = obj.value(QLatin1String("coins")).toArray(); + for(QJsonArray::const_iterator i = results.constBegin(); i != results.constEnd(); ++i) { + if(count >= m_limit) { + break; + } + QJsonObject result = (*i).toObject(); + + QString desc = result.value(QLatin1String("issuer")).toObject() + .value(QLatin1String("name")).toString(); + desc += QLatin1Char('/'); + const QString minYear = result.value(QLatin1String("minYear")).toString(); + if(!minYear.isEmpty()) { + desc += minYear + QLatin1Char('-') + result.value(QLatin1String("maxYear")).toString(); + } + FetchResult* r = new FetchResult(Fetcher::Ptr(this), + result.value(QLatin1String("title")).toString(), + desc); + m_matches.insert(r->uid, result.value(QLatin1String("id")).toInt()); + emit signalResultFound(r); + ++count; + } + + stop(); // required +} + +Tellico::Data::EntryPtr NumistaFetcher::fetchEntryHook(uint uid_) { + Data::EntryPtr entry = m_entries.value(uid_); + if(entry) { + return entry; + } + + if(!m_matches.contains(uid_)) { + myWarning() << "no matching coin id"; + return Data::EntryPtr(); + } + + QUrl url(QString::fromLatin1(NUMISTA_API_URL)); + url.setPath(url.path() + QLatin1String("/coins/") + QString::number(m_matches[uid_])); +// myDebug() << url.url(); + QPointer<KIO::StoredTransferJob> job = KIO::storedGet(url, KIO::NoReload, KIO::HideProgressInfo); + job->addMetaData(QStringLiteral("customHTTPHeader"), QStringLiteral("Numista-API-Key: ") + m_apiKey); + KJobWidgets::setWindow(job, GUI::Proxy::widget()); + if(!job->exec()) { + myDebug() << job->errorString() << url; + return Data::EntryPtr(); + } + const QByteArray data = job->data(); + if(data.isEmpty()) { + myDebug() << "no data for" << url; + return Data::EntryPtr(); + } +#if 0 + myWarning() << "Remove debug2 from numistafetcher.cpp"; + QFile f(QStringLiteral("/tmp/test2-numista.json")); + if(f.open(QIODevice::WriteOnly)) { + QTextStream t(&f); + t.setCodec("UTF-8"); + t << data; + } + f.close(); +#endif + + entry = parseEntry(data); + if(!entry) { + myDebug() << "No discernible entry data"; + return Data::EntryPtr(); + } + + QString image = entry->field(QStringLiteral("obverse")); + if(!image.isEmpty() && optionalFields().contains(QStringLiteral("obverse"))) { + const QString id = ImageFactory::addImage(QUrl::fromUserInput(image), true /* quiet */); + if(id.isEmpty()) { + message(i18n("The cover image could not be loaded."), MessageHandler::Warning); + } + entry->setField(QStringLiteral("obverse"), id); + } + image = entry->field(QStringLiteral("reverse")); + if(!image.isEmpty() && optionalFields().contains(QStringLiteral("reverse"))) { + const QString id = ImageFactory::addImage(QUrl::fromUserInput(image), true /* quiet */); + if(id.isEmpty()) { + message(i18n("The cover image could not be loaded."), MessageHandler::Warning); + } + entry->setField(QStringLiteral("reverse"), id); + } + + return entry; +} + +Tellico::Data::EntryPtr NumistaFetcher::parseEntry(const QByteArray& data_) { + QJsonParseError parseError; + QJsonDocument doc = QJsonDocument::fromJson(data_, &parseError); + if(doc.isNull()) { + myDebug() << "Bad json data:" << parseError.errorString(); + return Data::EntryPtr(); + } + + Data::CollPtr coll(new Data::CoinCollection(true)); + Data::EntryPtr entry(new Data::Entry(coll)); + coll->addEntries(entry); + + QVariantMap objectMap = doc.object().toVariantMap(); + // for type, try to tease out from title + // use ruler name as a possible fallback + QRegularExpression titleQuote(QStringLiteral(""(.+)"")); + QRegularExpressionMatch quoteMatch = titleQuote.match(mapValue(objectMap, "title")); + if(quoteMatch.hasMatch()) { + entry->setField(QStringLiteral("type"), quoteMatch.captured(1)); + } else { + entry->setField(QStringLiteral("type"), mapValue(objectMap, "ruler", "name")); + } + + entry->setField(QStringLiteral("denomination"), mapValue(objectMap, "value", "text")); + entry->setField(QStringLiteral("currency"), mapValue(objectMap, "value", "currency", "name")); + entry->setField(QStringLiteral("country"), mapValue(objectMap, "issuer", "name")); + entry->setField(QStringLiteral("mintmark"), mapValue(objectMap, "mintLetter")); + + // if minyear = maxyear, then set the year of the coin + auto year = objectMap.value(QLatin1String("minYear")); + if(year == objectMap.value(QLatin1String("maxYear"))) { + entry->setField(QStringLiteral("year"), year.toString()); + } else if(!m_year.isEmpty()) { + entry->setField(QStringLiteral("year"), m_year); + } + + entry->setField(QStringLiteral("obverse"), mapValue(objectMap, "obverse", "picture")); + entry->setField(QStringLiteral("reverse"), mapValue(objectMap, "reverse", "picture")); + + const QString numista(QStringLiteral("numista")); + if(optionalFields().contains(numista)) { + Data::FieldPtr field(new Data::Field(numista, i18n("Numista Link"), Data::Field::URL)); + field->setCategory(i18n("General")); + coll->addField(field); + entry->setField(numista, mapValue(objectMap, "url")); + } + + const QString desc(QStringLiteral("description")); + if(!coll->hasField(desc) && optionalFields().contains(desc)) { + Data::FieldPtr field(new Data::Field(desc, i18n("Description"), Data::Field::Para)); + coll->addField(field); + entry->setField(QStringLiteral("description"), mapValue(objectMap, "comments")); + } + + QVariantList refs = objectMap.value(QStringLiteral("references")).toList(); + const QString krause(QStringLiteral("km")); + if(!coll->hasField(krause) && optionalFields().contains(krause)) { + Data::FieldPtr field(new Data::Field(krause, allOptionalFields().value(krause))); + field->setCategory(i18n("General")); + coll->addField(field); + foreach(const QVariant& ref, refs) { + QVariantMap refMap = ref.toMap(); + if(mapValue(refMap, "catalogue", "code") == QLatin1String("KM")) { + entry->setField(krause, mapValue(refMap, "number")); + // don't break out, there could be multiple KM values and we want the last one + } + } + } + + return entry; +} + +Tellico::Fetch::FetchRequest NumistaFetcher::updateRequest(Data::EntryPtr entry_) { + QString t = entry_->field(QStringLiteral("type")); + QString c = entry_->field(QStringLiteral("country")); + if(!t.isEmpty()) { + return FetchRequest(Fetch::Keyword, t + QLatin1Char(' ') + c); + } + + return FetchRequest(); +} + +Tellico::Fetch::ConfigWidget* NumistaFetcher::configWidget(QWidget* parent_) const { + return new NumistaFetcher::ConfigWidget(parent_, this); +} + +QString NumistaFetcher::defaultName() { + return QStringLiteral("Numista"); // no translation +} + +QString NumistaFetcher::defaultIcon() { + return favIcon("https://en.numista.com"); +} + +Tellico::StringHash NumistaFetcher::allOptionalFields() { + StringHash hash; + hash[QStringLiteral("numista")] = i18n("Numista Link"); + hash[QStringLiteral("description")] = i18n("Description"); + // treat images as optional since Numista doesn't break out different images for each year + hash[QStringLiteral("obverse")] = i18n("Obverse"); + hash[QStringLiteral("reverse")] = i18n("Reverse"); + hash[QStringLiteral("km")] = i18nc("Standard catalog of world coins number", "Krause-Mishler"); + return hash; +} + +NumistaFetcher::ConfigWidget::ConfigWidget(QWidget* parent_, const NumistaFetcher* fetcher_) + : Fetch::ConfigWidget(parent_) { + QGridLayout* l = new QGridLayout(optionsWidget()); + l->setSpacing(4); + l->setColumnStretch(1, 10); + + int row = -1; + + QLabel* label = new QLabel(i18n("Access key: "), optionsWidget()); + l->addWidget(label, ++row, 0); + + m_apiKeyEdit = new QLineEdit(optionsWidget()); + connect(m_apiKeyEdit, &QLineEdit::textChanged, this, &ConfigWidget::slotSetModified); + l->addWidget(m_apiKeyEdit, row, 1); + label->setBuddy(m_apiKeyEdit); + + label = new QLabel(i18n("Language: "), optionsWidget()); + l->addWidget(label, ++row, 0); + m_langCombo = new GUI::ComboBox(optionsWidget()); + QIcon iconUS(QStandardPaths::locate(QStandardPaths::GenericDataLocation, + QStringLiteral("kf5/locale/countries/us/flag.png"))); + m_langCombo->addItem(iconUS, i18nc("Language", "English"), QLatin1String("en")); + QIcon iconFR(QStandardPaths::locate(QStandardPaths::GenericDataLocation, + QStringLiteral("kf5/locale/countries/fr/flag.png"))); + m_langCombo->addItem(iconFR, i18nc("Language", "French"), QLatin1String("fr")); + void (GUI::ComboBox::* activatedInt)(int) = &GUI::ComboBox::activated; + connect(m_langCombo, activatedInt, this, &ConfigWidget::slotSetModified); + connect(m_langCombo, activatedInt, this, &ConfigWidget::slotLangChanged); + l->addWidget(m_langCombo, row, 1); + label->setBuddy(m_langCombo); + + l->setRowStretch(++row, 10); + + // now add additional fields widget + addFieldsWidget(NumistaFetcher::allOptionalFields(), fetcher_ ? fetcher_->optionalFields() : QStringList()); + + // don't show the default API key + if(fetcher_) { + if(fetcher_->m_apiKey != Tellico::reverseObfuscate(NUMISTA_MAGIC_TOKEN)) { + m_apiKeyEdit->setText(fetcher_->m_apiKey); + } + m_langCombo->setCurrentData(fetcher_->m_locale); + } +} + +void NumistaFetcher::ConfigWidget::saveConfigHook(KConfigGroup& config_) { + const QString apiKey = m_apiKeyEdit->text().trimmed(); + if(!apiKey.isEmpty()) { + config_.writeEntry("API Key", apiKey); + } + const QString lang = m_langCombo->currentData().toString(); + config_.writeEntry("Locale", lang); +} + +QString NumistaFetcher::ConfigWidget::preferredName() const { + return i18n("Numista (%1)", m_langCombo->currentText()); +} + +void NumistaFetcher::ConfigWidget::slotLangChanged() { + emit signalName(preferredName()); +} diff --git a/src/fetch/numistafetcher.h b/src/fetch/numistafetcher.h new file mode 100644 index 00000000..27634842 --- /dev/null +++ b/src/fetch/numistafetcher.h @@ -0,0 +1,131 @@ +/*************************************************************************** + Copyright (C) 2020 Robby Stephenson <[email protected]> + ***************************************************************************/ + +/*************************************************************************** + * * + * This program is free software; you can redistribute it and/or * + * modify it under the terms of the GNU General Public License as * + * published by the Free Software Foundation; either version 2 of * + * the License or (at your option) version 3 or any later version * + * accepted by the membership of KDE e.V. (or its successor approved * + * by the membership of KDE e.V.), which shall act as a proxy * + * defined in Section 14 of version 3 of the license. * + * * + * This program is distributed in the hope that it will be useful, * + * but WITHOUT ANY WARRANTY; without even the implied warranty of * + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * + * GNU General Public License for more details. * + * * + * You should have received a copy of the GNU General Public License * + * along with this program. If not, see <http://www.gnu.org/licenses/>. * + * * + ***************************************************************************/ + +#ifndef TELLICO_NUMISTAFETCHER_H +#define TELLICO_NUMISTAFETCHER_H + +#include "fetcher.h" +#include "configwidget.h" +#include "../datavectors.h" + +#include <QPointer> + +class QLineEdit; + +class KJob; +namespace KIO { + class StoredTransferJob; +} + +namespace Tellico { + namespace GUI { + class ComboBox; + } + + namespace Fetch { + +/** + * A fetcher for numista.com + * + * @author Robby Stephenson + */ +class NumistaFetcher : public Fetcher { +Q_OBJECT + +public: + /** + */ + NumistaFetcher(QObject* parent); + /** + */ + virtual ~NumistaFetcher(); + + /** + */ + virtual QString source() const Q_DECL_OVERRIDE; + virtual bool isSearching() const Q_DECL_OVERRIDE { return m_started; } + virtual void continueSearch() Q_DECL_OVERRIDE; + // amazon can search title or person + virtual bool canSearch(FetchKey k) const Q_DECL_OVERRIDE { return k == Keyword; } + virtual void stop() Q_DECL_OVERRIDE; + virtual Data::EntryPtr fetchEntryHook(uint uid) Q_DECL_OVERRIDE; + virtual Type type() const Q_DECL_OVERRIDE { return Numista; } + virtual bool canFetch(int type) const Q_DECL_OVERRIDE; + virtual void readConfigHook(const KConfigGroup& config) Q_DECL_OVERRIDE; + void setLimit(int limit); + + /** + * Returns a widget for modifying the fetcher's config. + */ + virtual Fetch::ConfigWidget* configWidget(QWidget* parent) const Q_DECL_OVERRIDE; + + class ConfigWidget; + friend class ConfigWidget; + + static QString defaultName(); + static QString defaultIcon(); + static StringHash allOptionalFields(); + +private Q_SLOTS: + void slotComplete(KJob* job); + +private: + virtual void search() Q_DECL_OVERRIDE; + virtual FetchRequest updateRequest(Data::EntryPtr entry) Q_DECL_OVERRIDE; + void doSearch(); + Data::EntryPtr parseEntry(const QByteArray& data); + + int m_limit; + int m_total; + int m_page; + + QHash<uint, int> m_matches; // search result id to coin id + QHash<uint, Data::EntryPtr> m_entries; + QPointer<KIO::StoredTransferJob> m_job; + QString m_apiKey; + QString m_locale; + QString m_year; + + bool m_started; +}; + +class NumistaFetcher::ConfigWidget : public Fetch::ConfigWidget { +Q_OBJECT + +public: + explicit ConfigWidget(QWidget* parent_, const NumistaFetcher* fetcher = nullptr); + virtual void saveConfigHook(KConfigGroup&) Q_DECL_OVERRIDE; + virtual QString preferredName() const Q_DECL_OVERRIDE; + +private Q_SLOTS: + void slotLangChanged(); + +private: + QLineEdit* m_apiKeyEdit; + GUI::ComboBox* m_langCombo; +}; + + } // end namespace +} // end namespace +#endif diff --git a/src/tests/CMakeLists.txt b/src/tests/CMakeLists.txt index b3e2a82e..c5fccb74 100644 --- a/src/tests/CMakeLists.txt +++ b/src/tests/CMakeLists.txt @@ -802,6 +802,14 @@ add_test(musicbrainzfetchertest musicbrainzfetchertest) ecm_mark_as_test(musicbrainzfetchertest) TARGET_LINK_LIBRARIES(musicbrainzfetchertest fetcherstest ${TELLICO_TEST_LIBS}) +add_executable(numistafetchertest numistafetchertest.cpp abstractfetchertest.cpp + ../fetch/numistafetcher.cpp +) +ecm_mark_nongui_executable(numistafetchertest) +add_test(numistafetchertest numistafetchertest) +ecm_mark_as_test(numistafetchertest) +TARGET_LINK_LIBRARIES(numistafetchertest fetcherstest ${TELLICO_TEST_LIBS}) + add_executable(openlibraryfetchertest openlibraryfetchertest.cpp abstractfetchertest.cpp ../fetch/openlibraryfetcher.cpp ) diff --git a/src/tests/numistafetchertest.cpp b/src/tests/numistafetchertest.cpp new file mode 100644 index 00000000..8cab6781 --- /dev/null +++ b/src/tests/numistafetchertest.cpp @@ -0,0 +1,98 @@ +/*************************************************************************** + Copyright (C) 2020 Robby Stephenson <[email protected]> + ***************************************************************************/ + +/*************************************************************************** + * * + * This program is free software; you can redistribute it and/or * + * modify it under the terms of the GNU General Public License as * + * published by the Free Software Foundation; either version 2 of * + * the License or (at your option) version 3 or any later version * + * accepted by the membership of KDE e.V. (or its successor approved * + * by the membership of KDE e.V.), which shall act as a proxy * + * defined in Section 14 of version 3 of the license. * + * * + * This program is distributed in the hope that it will be useful, * + * but WITHOUT ANY WARRANTY; without even the implied warranty of * + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * + * GNU General Public License for more details. * + * * + * You should have received a copy of the GNU General Public License * + * along with this program. If not, see <http://www.gnu.org/licenses/>. * + * * + ***************************************************************************/ + +#undef QT_NO_CAST_FROM_ASCII + +#include "numistafetchertest.h" + +#include "../fetch/numistafetcher.h" +#include "../collections/coincollection.h" +#include "../entry.h" +#include "../images/imagefactory.h" + +#include <KConfig> +#include <KConfigGroup> + +#include <QTest> + +QTEST_GUILESS_MAIN( NumistaFetcherTest ) + +NumistaFetcherTest::NumistaFetcherTest() : AbstractFetcherTest() { +} + +void NumistaFetcherTest::initTestCase() { + Tellico::ImageFactory::init(); +} + +void NumistaFetcherTest::testSacagawea() { + KConfig config(QFINDTESTDATA("tellicotest.config"), KConfig::SimpleConfig); + QString groupName = QStringLiteral("numista"); + if(!config.hasGroup(groupName)) { + QSKIP("This test requires a config file.", SkipAll); + } + KConfigGroup cg(&config, groupName); + + Tellico::Fetch::FetchRequest request(Tellico::Data::Collection::Coin, + Tellico::Fetch::Keyword, + QStringLiteral("2019 Sacagawea")); + Tellico::Fetch::Fetcher::Ptr fetcher(new Tellico::Fetch::NumistaFetcher(this)); + fetcher->readConfig(cg, cg.name()); + + static_cast<Tellico::Fetch::NumistaFetcher*>(fetcher.data())->setLimit(1); + Tellico::Data::EntryList results = DO_FETCH1(fetcher, request, 1); + + QCOMPARE(results.size(), 1); + Tellico::Data::EntryPtr entry = results.at(0); + + QCOMPARE(entry->field(QStringLiteral("type")), QStringLiteral("Native American Dollar")); + QCOMPARE(entry->field(QStringLiteral("year")), QStringLiteral("2019")); + QCOMPARE(entry->field(QStringLiteral("country")), QStringLiteral("United States")); + QCOMPARE(entry->field(QStringLiteral("denomination")), QStringLiteral("1 Dollar")); + QCOMPARE(entry->field(QStringLiteral("currency")), QStringLiteral("Dollar")); + QCOMPARE(entry->field(QStringLiteral("numista")), QStringLiteral("https://en.numista.com/catalogue/pieces155679.html")); + QVERIFY(!entry->field(QStringLiteral("description")).isEmpty()); + QVERIFY(!entry->field(QStringLiteral("obverse")).isEmpty()); + QVERIFY(!entry->field(QStringLiteral("obverse")).contains(QLatin1Char('/'))); + QVERIFY(!entry->field(QStringLiteral("reverse")).isEmpty()); + QVERIFY(!entry->field(QStringLiteral("reverse")).contains(QLatin1Char('/'))); +} + +void NumistaFetcherTest::testJefferson() { + Tellico::Fetch::FetchRequest request(Tellico::Data::Collection::Coin, + Tellico::Fetch::Keyword, + QStringLiteral("1974 jefferson nickel")); + Tellico::Fetch::Fetcher::Ptr fetcher(new Tellico::Fetch::NumistaFetcher(this)); + + Tellico::Data::EntryList results = DO_FETCH(fetcher, request); + + QVERIFY(!results.isEmpty()); + Tellico::Data::EntryPtr entry = results.at(0); + QVERIFY(entry); + + QCOMPARE(entry->field(QStringLiteral("type")), QStringLiteral("Jefferson Nickel")); + QCOMPARE(entry->field(QStringLiteral("year")), QStringLiteral("1974")); + QCOMPARE(entry->field(QStringLiteral("country")), QStringLiteral("United States")); + QCOMPARE(entry->field(QStringLiteral("denomination")), QStringLiteral("5 Cents")); + QCOMPARE(entry->field(QStringLiteral("currency")), QStringLiteral("Dollar")); +} diff --git a/src/fetch/fetch.h b/src/tests/numistafetchertest.h similarity index 57% copy from src/fetch/fetch.h copy to src/tests/numistafetchertest.h index 22d0878b..5614dd2b 100644 --- a/src/fetch/fetch.h +++ b/src/tests/numistafetchertest.h @@ -1,5 +1,5 @@ /*************************************************************************** - Copyright (C) 2003-2009 Robby Stephenson <[email protected]> + Copyright (C) 2020 Robby Stephenson <[email protected]> ***************************************************************************/ /*************************************************************************** @@ -22,92 +22,20 @@ * * ***************************************************************************/ -#ifndef TELLICO_FETCH_H -#define TELLICO_FETCH_H +#ifndef NUMISTAFETCHERTEST_H +#define NUMISTAFETCHERTEST_H -namespace Tellico { - namespace Fetch { +#include "abstractfetchertest.h" -/** - * FetchFirst must be first, and the rest must follow consecutively in value. - * FetchLast must be last! - */ -enum FetchKey { - FetchFirst = 0, - Title, - Person, - ISBN, - UPC, - Keyword, - DOI, - ArxivID, - PubmedID, - LCCN, - Raw, - ExecUpdate, - FetchLast -}; +class NumistaFetcherTest : public AbstractFetcherTest { +Q_OBJECT +public: + NumistaFetcherTest(); -// real ones must start at 0! -enum Type { - Unknown = -1, - Amazon = 0, - IMDB, - Z3950, - SRU, - Entrez, - ExecExternal, - Yahoo, // Removed - AnimeNfo, - IBS, - ISBNdb, - GCstarPlugin, - CrossRef, - Citebase, // Removed - Arxiv, - Bibsonomy, - GoogleScholar, - Discogs, - WineCom, - TheMovieDB, - MusicBrainz, - GiantBomb, - OpenLibrary, - Multiple, - Freebase, // Removed - DVDFr, - Filmaster, - Douban, - BiblioShare, - MovieMeter, - GoogleBook, - MAS, // Removed - Springer, - Allocine, - ScreenRush, // Removed - FilmStarts, // Removed - SensaCine, // Removed - Beyazperde, // Removed - HathiTrust, - TheGamesDB, - DBLP, - VNDB, - MRLookup, - BoardGameGeek, - Bedetheque, - OMDB, - KinoPoisk, - VideoGameGeek, - DBC, - IGDB, - Kino, - MobyGames, - ComicVine, - KinoTeatr, - Colnect +private Q_SLOTS: + void initTestCase(); + void testSacagawea(); + void testJefferson(); }; - } -} - #endif diff --git a/src/tests/tellicotest.config b/src/tests/tellicotest.config index cf4d5f87..bde4a544 100644 --- a/src/tests/tellicotest.config +++ b/src/tests/tellicotest.config @@ -81,3 +81,6 @@ Custom Fields=obverse,reverse,series,mintage,description [colnect stamps] Custom Fields=image,series,description,stanley-gibbons,michel + +[numista] +Custom Fields=numista,description,obverse,reverse,km
