Git commit bb38ab07f6041e4ea05281285cf578ab2f38fdea by Robby Stephenson. Committed on 03/09/2026 at 01:57. Pushed by rstephenson into branch 'master'.
Add Hardcover data source M +12 -4 doc/configuration.docbook M +2 -0 src/fetch/CMakeLists.txt M +2 -1 src/fetch/fetch.h M +2 -0 src/fetch/fetcherinitializer.cpp M +2 -2 src/fetch/goodreadsfetcher.h A +377 -0 src/fetch/hardcoverfetcher.cpp [License: GPL (v2/3)] C +44 -22 src/fetch/hardcoverfetcher.h [from: src/fetch/goodreadsfetcher.h - 066% similarity] C +49 -105 src/fetch/hardcoverfetcher_query.cpp [from: src/fetch/fetch.h - 051% similarity] M +7 -0 src/tests/CMakeLists.txt A +74 -0 src/tests/hardcoverfetchertest.cpp [License: GPL (v2/3)] C +16 -104 src/tests/hardcoverfetchertest.h [from: src/fetch/fetch.h - 051% similarity] https://invent.kde.org/office/tellico/-/commit/bb38ab07f6041e4ea05281285cf578ab2f38fdea diff --git a/doc/configuration.docbook b/doc/configuration.docbook index b549c62f3..3daa88fb3 100644 --- a/doc/configuration.docbook +++ b/doc/configuration.docbook @@ -162,6 +162,7 @@ while the full list is <ulink url="https://tellico-project.org/data-sources">ava <!-- books --> <listitem><simpara><link linkend="amazon-web-services">Amazon.com Web Services</link>,</simpara></listitem> <listitem><simpara><link linkend="isbndb">ISBNdb.com</link>,</simpara></listitem> +<listitem><simpara><link linkend="hardcover">Hardcover</link>,</simpara></listitem> <listitem><simpara><link linkend="openlibrary">OpenLibrary.org</link>,</simpara></listitem> <listitem><simpara><link linkend="opds">OPDS catalogs</link>,</simpara></listitem> <!-- movies --> @@ -291,10 +292,17 @@ SRU request by the POST method, if <userinput>x-tellico-method</userinput>=<user </sect3> <sect3 id="isbndb"> -<title>ISBNdb.com</title> -<para> -<ulink url="http://isbndb.com">ISBNdb.com</ulink> is an online book database, from libraries around the world. -</para> + <title>ISBNdb.com</title> + <para> + <ulink url="http://isbndb.com">ISBNdb.com</ulink> is an online book database, from libraries around the world. + </para> +</sect3> + +<sect3 id="hardcover"> + <title>Hardcover</title> + <para> + <ulink url="https://hardcover.app">Hardcover</ulink> is a social network and reading tracker. + </para> </sect3> <sect3 id="openlibrary"> diff --git a/src/fetch/CMakeLists.txt b/src/fetch/CMakeLists.txt index 14e07a2f8..648be3a2e 100644 --- a/src/fetch/CMakeLists.txt +++ b/src/fetch/CMakeLists.txt @@ -39,6 +39,8 @@ set(fetch_STAT_SRCS goodreadsfetcher.cpp googlebookfetcher.cpp googlescholarfetcher.cpp + hardcoverfetcher.cpp + hardcoverfetcher_query.cpp hathitrustfetcher.cpp ibsfetcher.cpp igdbfetcher.cpp diff --git a/src/fetch/fetch.h b/src/fetch/fetch.h index 0bdbaa878..24667fefa 100644 --- a/src/fetch/fetch.h +++ b/src/fetch/fetch.h @@ -124,7 +124,8 @@ enum Type { ISFDB, Metron, DOIorg, - Goodreads + Goodreads, + Hardcover }; } diff --git a/src/fetch/fetcherinitializer.cpp b/src/fetch/fetcherinitializer.cpp index eb13ca92b..207f53e94 100644 --- a/src/fetch/fetcherinitializer.cpp +++ b/src/fetch/fetcherinitializer.cpp @@ -84,6 +84,7 @@ #include "metronfetcher.h" #include "doifetcher.h" #include "goodreadsfetcher.h" +#include "hardcoverfetcher.h" /** * Ideally, I'd like these initializations to be in each cpp file for each collection type @@ -145,6 +146,7 @@ Tellico::Fetch::FetcherInitializer::FetcherInitializer() { RegisterFetcher<Fetch::ISFDBFetcher> registerISFDB(ISFDB); RegisterFetcher<Fetch::MetronFetcher> registerMetron(Metron); RegisterFetcher<Fetch::GoodreadsFetcher> registerGoodreads(Goodreads); + RegisterFetcher<Fetch::HardcoverFetcher> registerHardcover(Hardcover); // these data sources depend on being able to import bibtex #ifdef ENABLE_BTPARSE diff --git a/src/fetch/goodreadsfetcher.h b/src/fetch/goodreadsfetcher.h index 24a992cb4..6f23d9169 100644 --- a/src/fetch/goodreadsfetcher.h +++ b/src/fetch/goodreadsfetcher.h @@ -22,8 +22,8 @@ * * ***************************************************************************/ -#ifndef TELLICO_GoodreadsFetcher_H -#define TELLICO_GoodreadsFetcher_H +#ifndef TELLICO_GOODREADSFETCHER_H +#define TELLICO_GOODREADSFETCHER_H #include "xmlfetcher.h" #include "configwidget.h" diff --git a/src/fetch/hardcoverfetcher.cpp b/src/fetch/hardcoverfetcher.cpp new file mode 100644 index 000000000..910b54390 --- /dev/null +++ b/src/fetch/hardcoverfetcher.cpp @@ -0,0 +1,377 @@ +/*************************************************************************** + Copyright (C) 2026 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 "hardcoverfetcher.h" +#include "../collections/bookcollection.h" +#include "../images/imagefactory.h" +#include "../utils/guiproxy.h" +#include "../utils/string_utils.h" +#include "../utils/tellico_utils.h" +#include "../utils/objvalue.h" +#include "../utils/isbnvalidator.h" +#include "../core/tellico_strings.h" +#include "../tellico_debug.h" + +#include <KLocalizedString> +#include <KConfigGroup> +#include <KJob> +#include <KJobUiDelegate> +#include <KJobWidgets> +#include <KIO/StoredTransferJob> +#include <KLanguageName> + +#include <QLabel> +#include <QFile> +#include <QTextStream> +#include <QVBoxLayout> +#include <QJsonDocument> +#include <QJsonObject> +#include <QJsonArray> +#include <QUrlQuery> +#include <QLineEdit> + +namespace { + static const int HARDCOVER_MAX_RETURNS_TOTAL = 20; + static const char* HARDCOVER_API_URL = "https://api.hardcover.app/v1/graphql"; +} + +using namespace Tellico; +using Tellico::Fetch::HardcoverFetcher; +using namespace Qt::Literals::StringLiterals; + +HardcoverFetcher::HardcoverFetcher(QObject* parent_) + : Fetcher(parent_) + , m_started(false) { +} + +HardcoverFetcher::~HardcoverFetcher() = default; + +QString HardcoverFetcher::source() const { + return m_name.isEmpty() ? defaultName() : m_name; +} + +QString HardcoverFetcher::attribution() const { + return TC_I18N3(providedBy, QStringLiteral("https://hardcover.app"), defaultName()); +} + +bool HardcoverFetcher::canSearch(Fetch::FetchKey k) const { + return k == ISBN; +} + +bool HardcoverFetcher::canFetch(int type) const { + return type == Data::Collection::Book; +} + +void HardcoverFetcher::readConfigHook(const KConfigGroup& config_) { + m_apiKey = config_.readEntry("API Key"); +} + +void HardcoverFetcher::search() { + continueSearch(); +} + +void HardcoverFetcher::continueSearch() { + m_started = true; + + if(m_apiKey.isEmpty()) { + myDebug() << source() << "- empty API key"; + message(i18n("An access key is required to use this data source.") + + QLatin1Char(' ') + + i18n("Those values must be entered in the data source settings."), MessageHandler::Error); + stop(); + return; + } + + QString operationName, query; + QJsonObject vars; + switch(request().key()) { + case ISBN: + operationName = QStringLiteral("GetBookByISBN"); + query = isbnQuery(); + { + QJsonArray isbns; + const auto valueList = FieldFormat::splitValue(request().value()); + for(auto value : valueList) { + value.remove(QLatin1Char('-')); + isbns += value; + } + vars.insert(QLatin1String("isbns"), isbns); + } + break; + + case Raw: + default: + myWarning() << source() << "- key not recognized:" << request().key(); + stop(); + return; + } + + QJsonObject payload; + payload.insert(QLatin1String("operationName"), operationName); + payload.insert(QLatin1String("query"), query); + payload.insert(QLatin1String("variables"), vars); + + m_job = KIO::storedHttpPost(QJsonDocument(payload).toJson(), + QUrl(QString::fromLatin1(HARDCOVER_API_URL)), + KIO::HideProgressInfo); + configureJob(m_job); + connect(m_job.data(), &KJob::result, this, &HardcoverFetcher::slotComplete); +} + +void HardcoverFetcher::stop() { + if(!m_started) { + return; + } + if(m_job) { + m_job->kill(); + m_job = nullptr; + } + m_started = false; + Q_EMIT signalDone(this); +} + +Tellico::Fetch::FetchRequest HardcoverFetcher::updateRequest(Data::EntryPtr entry_) { + const QString isbn = entry_->field(QStringLiteral("isbn")); + if(!isbn.isEmpty()) { + return FetchRequest(ISBN, isbn); + } + return FetchRequest(); +} + +void HardcoverFetcher::slotComplete(KJob* job_) { + KIO::StoredTransferJob* job = static_cast<KIO::StoredTransferJob*>(job_); + + if(job->error()) { + job->uiDelegate()->showErrorMessage(); + stop(); + return; + } + + const QByteArray data = job->data(); + if(data.isEmpty()) { + myDebug() << "Hardcover: 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 1 + myWarning() << "Remove debug from hardcoverfetcher.cpp"; + QFile f(QStringLiteral("/tmp/test-hardcover.json")); + if(f.open(QIODevice::WriteOnly)) { + QTextStream t(&f); + t << data; + } + f.close(); +#endif + + Data::CollPtr coll(new Data::BookCollection(true)); + QJsonDocument doc = QJsonDocument::fromJson(data); + + if(doc.isNull()) { + myDebug() << job->errorString(); + message(job->errorString(), MessageHandler::Error); + stop(); + } + const auto docObj = doc.object(); + const auto results = docObj["data"_L1]["editions"_L1].toArray(); + const auto errors = docObj["errors"_L1].toArray(); + + if(results.isEmpty()) { + if(errors.isEmpty()) { + myLog() << "No results"; + } else { + QStringList msgList; + for(const QJsonValue& error : errors) { + msgList += error["message"_L1].toString(); + myDebug() << error["message"_L1].toString(); + } + if(!msgList.isEmpty()) { + message(msgList.join("\n"_L1), MessageHandler::Error); + } + } + stop(); + return; + } + + int count = 0; + for(const QJsonValue& result : results) { + Data::EntryPtr entry(new Data::Entry(coll)); + populateEntry(entry, result.toObject()); + + FetchResult* r = new FetchResult(this, entry); + m_entries.insert(r->uid, entry); + Q_EMIT signalResultFound(r); + ++count; + if(count >= HARDCOVER_MAX_RETURNS_TOTAL) { + break; + } + } + + stop(); +} + +Tellico::Data::EntryPtr HardcoverFetcher::fetchEntryHook(uint uid_) { + Data::EntryPtr entry = m_entries.value(uid_); + if(!entry) { + myWarning() << "no entry in dict"; + return Data::EntryPtr(); + } + + const QString isbnName = QStringLiteral("isbn"); + QString isbn = entry->field(isbnName); + ISBNValidator val; + val.fixup(isbn); + entry->setField(isbnName, isbn); + + // image might still be a URL + const QString image_id = entry->field(QStringLiteral("cover")); + if(image_id.contains(QLatin1Char('/'))) { + const QString id = ImageFactory::addImage(QUrl::fromUserInput(image_id), true /* quiet */); + if(id.isEmpty()) { + message(i18n("The cover image could not be loaded."), MessageHandler::Warning); + } + // empty image ID is ok + entry->setField(QStringLiteral("cover"), id); + } + return entry; +} + +void HardcoverFetcher::populateEntry(Data::EntryPtr entry_, const QJsonObject& obj_) { + entry_->setField(QStringLiteral("title"), objValue(obj_, "title")); + entry_->setField(QStringLiteral("subtitle"), objValue(obj_, "subtitle")); + entry_->setField(QStringLiteral("pub_year"), objValue(obj_, "release_date").left(4)); + entry_->setField(QStringLiteral("pages"), objValue(obj_, "pages")); + entry_->setField(QStringLiteral("language"), objValue(obj_, "language", "language")); + QString binding = objValue(obj_, "edition_format"); + if(binding.contains(QLatin1StringView("Paperback"))) { + binding = i18n("Paperback"); + } else if(binding == QLatin1StringView("Hardcover")) { + binding = i18n("Hardback"); + } else { + binding = i18n(binding.toUtf8().constData()); + } + entry_->setField(QStringLiteral("binding"), binding); + entry_->setField(QStringLiteral("publisher"), objValue(obj_, "publisher", "name")); + + QString isbn = objValue(obj_, "isbn_10"); + if(isbn.isEmpty()) { + isbn = objValue(obj_, "isbn_13"); + } + entry_->setField(QStringLiteral("isbn"), isbn); + + QString cover = objValue(obj_, "image", "url"); + if(cover.isEmpty()) { + cover = objValue(obj_, "book", "image", "url"); + } + entry_->setField(QStringLiteral("cover"), cover); + + QStringList authors; + auto list = obj_["book"_L1]["contributions"_L1].toArray(); + for(const auto& person: std::as_const(list)) { + auto obj = person.toObject(); + const auto role = objValue(obj, "contribution"); + if(role.isEmpty() || role == "Author"_L1) { + authors += objValue(obj, "author", "name"); + } + } + entry_->setField(QStringLiteral("author"), authors.join(FieldFormat::delimiterString())); + + entry_->setField(QStringLiteral("comments"), objValue(obj_, "book", "description")); +} + +void HardcoverFetcher::configureJob(QPointer<KIO::StoredTransferJob> job_) { + KJobWidgets::setWindow(job_, GUI::Proxy::widget()); + job_->addMetaData(QStringLiteral("accept"), QStringLiteral("application/json")); + job_->addMetaData(QStringLiteral("customHTTPHeader"), QStringLiteral("Authorization: Bearer ") + m_apiKey); + Tellico::addUserAgent(job_); +} + +Tellico::Fetch::ConfigWidget* HardcoverFetcher::configWidget(QWidget* parent_) const { + return new HardcoverFetcher::ConfigWidget(parent_, this); +} + +QString HardcoverFetcher::defaultName() { + return QStringLiteral("Hardcover"); +} + +QString HardcoverFetcher::defaultIcon() { + return favIcon("https://hardcover.app"); +} + +Tellico::StringHash HardcoverFetcher::allOptionalFields() { + StringHash hash; + return hash; +} + +HardcoverFetcher::ConfigWidget::ConfigWidget(QWidget* parent_, const HardcoverFetcher* fetcher_) + : Fetch::ConfigWidget(parent_) { + QGridLayout* l = new QGridLayout(optionsWidget()); + l->setSpacing(4); + l->setColumnStretch(1, 10); + + int row = -1; + + QLabel* al = new QLabel(i18n("Registration is required for accessing this data source. " + "If you agree to the terms and conditions, <a href='%1'>sign " + "up for an account</a>, and enter your information below.", + QLatin1String("https://hardcover.app/account/api/keys/new?scope=read%3Acatalog")), + optionsWidget()); + al->setOpenExternalLinks(true); + al->setWordWrap(true); + ++row; + l->addWidget(al, row, 0, 1, 2); + // richtext gets weird with size + al->setMinimumWidth(al->sizeHint().width()); + + 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); + + l->setRowStretch(++row, 10); + + // now add additional fields widget + addFieldsWidget(HardcoverFetcher::allOptionalFields(), fetcher_ ? fetcher_->optionalFields() : QStringList()); + + if(fetcher_) { + m_apiKeyEdit->setText(fetcher_->m_apiKey); + } +} + +QString HardcoverFetcher::ConfigWidget::preferredName() const { + return HardcoverFetcher::defaultName(); +} + +void HardcoverFetcher::ConfigWidget::saveConfigHook(KConfigGroup& config_) { + const QString apiKey = m_apiKeyEdit->text().trimmed(); + if(!apiKey.isEmpty()) { + config_.writeEntry("API Key", apiKey); + } +} diff --git a/src/fetch/goodreadsfetcher.h b/src/fetch/hardcoverfetcher.h similarity index 66% copy from src/fetch/goodreadsfetcher.h copy to src/fetch/hardcoverfetcher.h index 24a992cb4..270a1edb9 100644 --- a/src/fetch/goodreadsfetcher.h +++ b/src/fetch/hardcoverfetcher.h @@ -22,72 +22,94 @@ * * ***************************************************************************/ -#ifndef TELLICO_GoodreadsFetcher_H -#define TELLICO_GoodreadsFetcher_H +#ifndef TELLICO_HARDCOVERFETCHER_H +#define TELLICO_HARDCOVERFETCHER_H -#include "xmlfetcher.h" +#include "fetcher.h" #include "configwidget.h" #include "../datavectors.h" +#include <QPointer> + class QLineEdit; +class KJob; +namespace KIO { + class StoredTransferJob; +} namespace Tellico { - namespace Fetch { /** - * A fetcher for goodreads.com + * A fetcher for thetvdb.com * * @author Robby Stephenson */ -class GoodreadsFetcher : public XMLFetcher { +class HardcoverFetcher : public Fetcher { Q_OBJECT public: /** */ - GoodreadsFetcher(QObject* parent); + HardcoverFetcher(QObject* parent); /** */ - virtual ~GoodreadsFetcher(); + virtual ~HardcoverFetcher(); /** */ virtual QString source() const override; virtual QString attribution() const override; + virtual bool isSearching() const override { return m_started; } virtual bool canSearch(FetchKey k) const override; - virtual bool canSearchMultiple() const override; - virtual Type type() const override { return Goodreads; } + virtual void stop() override; + virtual Data::EntryPtr fetchEntryHook(uint uid) override; + virtual Type type() const override { return Hardcover; } virtual bool canFetch(int type) const override; virtual void readConfigHook(const KConfigGroup& config) override; +// virtual void saveConfigHook(KConfigGroup& config) override; + virtual void continueSearch() override; /** * Returns a widget for modifying the fetcher's config. */ virtual Fetch::ConfigWidget* configWidget(QWidget* parent) const override; - class ConfigWidget : public Fetch::ConfigWidget { - public: - explicit ConfigWidget(QWidget* parent_, const GoodreadsFetcher* fetcher = nullptr); - virtual void saveConfigHook(KConfigGroup&) override {} - virtual QString preferredName() const override; - private: - QLineEdit* m_apiKeyEdit; - }; + class ConfigWidget; friend class ConfigWidget; static QString defaultName(); static QString defaultIcon(); static StringHash allOptionalFields(); +private Q_SLOTS: + void slotComplete(KJob* job); + private: + static QString isbnQuery(); + + virtual void search() override; virtual FetchRequest updateRequest(Data::EntryPtr entry) override; - virtual void resetSearch() override {} - virtual QUrl searchUrl() override; - virtual void parseData(QByteArray&) override {} - virtual Data::EntryPtr fetchEntryHookData(Data::EntryPtr entry) override; + void populateEntry(Data::EntryPtr entry, const QJsonObject& result); + + void configureJob(QPointer<KIO::StoredTransferJob> job); + + bool m_started; QString m_apiKey; + QHash<uint, Data::EntryPtr> m_entries; + QPointer<KIO::StoredTransferJob> m_job; +}; + +class HardcoverFetcher::ConfigWidget : public Fetch::ConfigWidget { + +public: + explicit ConfigWidget(QWidget* parent_, const HardcoverFetcher* fetcher = nullptr); + virtual void saveConfigHook(KConfigGroup&) override; + virtual QString preferredName() const override; + +private: + QLineEdit* m_apiKeyEdit; }; } // end namespace diff --git a/src/fetch/fetch.h b/src/fetch/hardcoverfetcher_query.cpp similarity index 51% copy from src/fetch/fetch.h copy to src/fetch/hardcoverfetcher_query.cpp index 0bdbaa878..269439d5e 100644 --- a/src/fetch/fetch.h +++ b/src/fetch/hardcoverfetcher_query.cpp @@ -1,5 +1,5 @@ /*************************************************************************** - Copyright (C) 2003-2009 Robby Stephenson <[email protected]> + Copyright (C) 2026 Robby Stephenson <[email protected]> ***************************************************************************/ /*************************************************************************** @@ -22,112 +22,56 @@ * * ***************************************************************************/ -#ifndef TELLICO_FETCH_H -#define TELLICO_FETCH_H +#include "hardcoverfetcher.h" -namespace Tellico { - namespace Fetch { - -/** - * 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, - User1, // 10: all user-entered key names must be consecutive before Raw - User2, - User3, - User4, - User5, - Raw, - ExecUpdate, - FetchLast -}; +namespace { + static const uint HARDCOVER_MAX_RESULTS = 20; +} -// real ones must start at 0! -enum Type { - Unknown = -1, - Amazon = 0, - IMDB, - Z3950, - SRU, - Entrez, - ExecExternal, - Yahoo, // Removed - AnimeNfo, // Removed - 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, // Removed - 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, - Numista, - TVmaze, - UPCItemDb, - TheTVDB, - RPGGeek, - GamingHistory, - FilmAffinity, - Itunes, - OPDS, - ADS, - VGCollect, - ISFDB, - Metron, - DOIorg, - Goodreads -}; +using Tellico::Fetch::HardcoverFetcher; +QString HardcoverFetcher::isbnQuery() { + static const auto query(QStringLiteral(R"( +query GetBookByISBN($isbns: [String!]!) { + editions(where: { + _or: [ + { isbn_10: { _in: $isbns } }, + { isbn_13: { _in: $isbns } } + ] + }) { + id + title + subtitle + isbn_10 + isbn_13 + edition_format + pages + release_date + language { + language + } + image { + url + } + publisher { + name + } + book { + id + title + description + contributions { + contribution + author { + name + } + } + image { + url + } + } } } - -#endif +)")); + return query; +} diff --git a/src/tests/CMakeLists.txt b/src/tests/CMakeLists.txt index 3932a1ac6..491cef9de 100644 --- a/src/tests/CMakeLists.txt +++ b/src/tests/CMakeLists.txt @@ -745,6 +745,13 @@ ecm_add_test(googlescholarfetchertest.cpp LINK_LIBRARIES fetcherstest translatorstest ${TELLICO_BTPARSE_LIBS} ${TELLICO_TEST_LIBS} ) +ecm_add_test(hardcoverfetchertest.cpp + ../fetch/hardcoverfetcher.cpp + ../fetch/hardcoverfetcher_query.cpp + TEST_NAME hardcoverfetchertest + LINK_LIBRARIES fetcherstest ${TELLICO_TEST_LIBS} +) + ecm_add_test(hathitrustfetchertest.cpp ../fetch/hathitrustfetcher.cpp TEST_NAME hathitrustfetchertest diff --git a/src/tests/hardcoverfetchertest.cpp b/src/tests/hardcoverfetchertest.cpp new file mode 100644 index 000000000..bde30c1a6 --- /dev/null +++ b/src/tests/hardcoverfetchertest.cpp @@ -0,0 +1,74 @@ +/*************************************************************************** + Copyright (C) 2026 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 "hardcoverfetchertest.h" + +#include "../fetch/hardcoverfetcher.h" +#include "../collections/bookcollection.h" +#include "../entry.h" +#include "../images/imagefactory.h" + +#include <KSharedConfig> + +#include <QTest> + +QTEST_GUILESS_MAIN( HardcoverFetcherTest ) + +HardcoverFetcherTest::HardcoverFetcherTest() : AbstractFetcherTest() { +} + +void HardcoverFetcherTest::initTestCase() { + Tellico::ImageFactory::init(); + + auto configFile = QFINDTESTDATA("tellicotest_private.config"); + if(QFile::exists(configFile)) { + m_config = KSharedConfig::openConfig(configFile, KConfig::SimpleConfig)->group(QStringLiteral("Hardcover")); + } +} + +void HardcoverFetcherTest::testIsbn() { + Tellico::Fetch::FetchRequest request(Tellico::Data::Collection::Book, + Tellico::Fetch::ISBN, + QStringLiteral("0441172717;0-393339912")); + Tellico::Fetch::Fetcher::Ptr fetcher(new Tellico::Fetch::HardcoverFetcher(this)); + fetcher->readConfig(m_config); + QVERIFY(fetcher->canSearch(request.key())); + + Tellico::Data::EntryList results = DO_FETCH1(fetcher, request, 2); + + QCOMPARE(results.size(), 2); + + Tellico::Data::EntryPtr entry = results.at(1); + QCOMPARE(entry->field(QStringLiteral("title")), QStringLiteral("Dune")); + QCOMPARE(entry->field(QStringLiteral("author")), QStringLiteral("Frank Herbert")); + QCOMPARE(entry->field(QStringLiteral("isbn")), QStringLiteral("0-441-17271-7")); + QCOMPARE(entry->field(QStringLiteral("binding")), QStringLiteral("Paperback")); + QCOMPARE(entry->field(QStringLiteral("publisher")), QStringLiteral("Penguin")); + QCOMPARE(entry->field(QStringLiteral("pub_year")), QStringLiteral("1965")); + QCOMPARE(entry->field(QStringLiteral("pages")), QStringLiteral("704")); + QCOMPARE(entry->field(QStringLiteral("language")), QStringLiteral("English")); + QVERIFY(!entry->field(QStringLiteral("comments")).isEmpty()); + QVERIFY(!entry->field(QStringLiteral("cover")).isEmpty()); + QVERIFY(!entry->field(QStringLiteral("cover")).contains(QLatin1Char('/'))); +} diff --git a/src/fetch/fetch.h b/src/tests/hardcoverfetchertest.h similarity index 51% copy from src/fetch/fetch.h copy to src/tests/hardcoverfetchertest.h index 0bdbaa878..f5dd65d83 100644 --- a/src/fetch/fetch.h +++ b/src/tests/hardcoverfetchertest.h @@ -1,5 +1,5 @@ /*************************************************************************** - Copyright (C) 2003-2009 Robby Stephenson <[email protected]> + Copyright (C) 2026 Robby Stephenson <[email protected]> ***************************************************************************/ /*************************************************************************** @@ -22,112 +22,24 @@ * * ***************************************************************************/ -#ifndef TELLICO_FETCH_H -#define TELLICO_FETCH_H +#ifndef HARDCOVERFETCHERTEST_H +#define HARDCOVERFETCHERTEST_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, - User1, // 10: all user-entered key names must be consecutive before Raw - User2, - User3, - User4, - User5, - Raw, - ExecUpdate, - FetchLast -}; +#include <KConfigGroup> -// real ones must start at 0! -enum Type { - Unknown = -1, - Amazon = 0, - IMDB, - Z3950, - SRU, - Entrez, - ExecExternal, - Yahoo, // Removed - AnimeNfo, // Removed - 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, // Removed - 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, - Numista, - TVmaze, - UPCItemDb, - TheTVDB, - RPGGeek, - GamingHistory, - FilmAffinity, - Itunes, - OPDS, - ADS, - VGCollect, - ISFDB, - Metron, - DOIorg, - Goodreads -}; +class HardcoverFetcherTest : public AbstractFetcherTest { +Q_OBJECT +public: + HardcoverFetcherTest(); - } -} +private Q_SLOTS: + void initTestCase(); + void testIsbn(); + +private: + KConfigGroup m_config; +}; #endif
