NeverTooManyBooks fetches book metadata from a variety of online sources, each implemented as aDocumentation Index
Fetch the complete documentation index at: https://mintlify.com/tfonteyn/NeverTooManyBooks/llms.txt
Use this file to discover all available pages before exploring further.
SearchEngine. The system is designed so that adding a new source is straightforward: implement the right sub-interface, register the engine, and plug it into the site list. This page walks through the architecture and shows a complete skeleton implementation so you can get a new data source running with minimal friction.
The SearchEngine Interface
SearchEngine (in com.hardbacknutter.nevertoomanybooks.searchengines) is the root interface every book data source must implement. It extends Cancellable, which means the framework can interrupt a running search at any point — every implementation must honour cancellation checks.
Every search method is annotated
@WorkerThread. The framework always invokes searches off the main thread via SearchCoordinator; never call search methods directly from the UI thread.Choosing a Sub-Interface
TheSearchEngine root interface alone defines no search behaviour. Each engine must implement at least one of the following search sub-interfaces — these are what the SearchCoordinator dispatches to:
ByIsbn
Search using a validated ISBN-10 or ISBN-13 string. The most common interface — every engine that can do a simple product lookup should implement this.
ByText
Search using free-text criteria (title, author, optional code). Implement this when the site offers a keyword or catalogue search rather than a strict ISBN lookup.
ByExternalId
Search using a locally stored site-specific identifier (for example a Goodreads book ID already saved in the database). Useful for refreshing data without re-entering an ISBN.
CoverByEdition
Fetch a single cover image for a given
AltEdition. Paired with AlternativeEditions — engines that list alternative editions can then have their covers fetched by any engine implementing this interface.ByBarcode, which extends ByIsbn and adds support for non-standard barcodes. Its default implementation simply delegates to searchByIsbn, so you only need to override it when your site handles non-ISBN barcodes differently.
Exception Contract
All search methods declare three checked exceptions:| Exception | Severity | Meaning |
|---|---|---|
CredentialsException | User must act now | Authentication failed — the user must update credentials before any further searches can succeed. |
StorageException | User must act now | A local storage problem (disk full, missing directory, etc.) prevents completing the search. |
SearchException | Optional action | A wrapped network or parsing error. Should be reported to the user, but retrying later is a valid option. |
SearchEngineConfig
SearchEngineConfig holds the mutable runtime configuration for an engine — things a user can change in Settings. It is built via SearchEngineConfig.Builder inside each engine’s init() method:
SearchEngineConfig:
| Setting | Preference Key Suffix | Default |
|---|---|---|
| Host URL | host.url | Engine’s hard-coded URL |
| Connect timeout | timeout.connect | 5 seconds |
| Read timeout | timeout.read | 10 seconds |
| Prefer ISBN-10 | search.byIsbn.prefer.10 | false |
| Tags to ignore | tags.ignore | empty set |
| Request throttle | — | Throttler.THROTTLER_DEFAULT_MS |
EngineId.getPreferenceKey(), keeping each engine’s preferences isolated.
EngineId: The Central Registry
EngineId is an enum that serves as the immutable registry of every supported search engine. Each constant binds an implementation class to its configuration:
true (always enabled) or a BuildConfig.ENABLE_* variable, which lets in-development engines be gated out of release builds.
EngineId also provides:
createSearchEngine(Context)— instantiates the engine via reflection using the registered constructorsupports(SearchBy)/supports(Class<? extends SearchEngine>)— capability queriesgetPreferenceKey()— stable string key used in SharedPreferences and the database
Base Classes
SearchEngineBase
SearchEngineBase is the abstract base all engines extend. It owns the SearchEngineConfig reference, manages the AtomicBoolean cancelRequested flag, provides a shared OkHttp client, and contains helpers for date parsing, image downloading, and money parsing. Your engine will almost always extend this rather than implementing SearchEngine directly.
JsoupSearchEngineBase
For engines that scrape HTML pages,JsoupSearchEngineBase extends SearchEngineBase and adds a loadDocument(Context, String url, Map<String, String> headers) helper:
loadDocument handles connection and read timeouts from SearchEngineConfig, honours the throttler, and returns a parsed Jsoup Document. Extend this class any time your engine works by fetching and parsing an HTML page rather than consuming a structured API.
Adding a New Search Engine
Implement the SearchEngine interface
Create your engine class extending
SearchEngineBase (or JsoupSearchEngineBase for HTML scraping). Implement at least one search sub-interface. Add a @Keep-annotated constructor that accepts (Context, SearchEngineConfig) and a @Keep-annotated static init() method that returns an EngineId.Builder:The
fetchCovers parameter is a boolean array of length DBKey.NR_OF_BOOK_COVERS (currently 4). Index 0 is the front cover; higher indexes are additional cover slots. Only download cover images for indexes where fetchCovers[i] is true.Register in EngineId
Add your engine to the The enum constructor will reflectively call
EngineId enum. Follow the // NEWTHINGS: adding a new search engine: add an engine class comment in the source:MyBookShopSearchEngine.init() to retrieve the builder. An IllegalStateException is thrown at startup if the init() method is missing or incorrectly named.Add a string resource
Add a non-translatable string resource for the site name in
src/main/res/values/strings-donottranslate.xml. Follow the existing site_* naming pattern:Add to the Site list
Inside Engines added to
EngineId.registerSites(), add your engine to the appropriate Site.Type list(s). At minimum, add it to Site.Type.Data so it participates in book metadata searches. You can conditionally enable it based on the device locale:Site.Type.Covers must implement CoverByEdition. Engines added to Site.Type.AltEditions must implement AlternativeEditions.Optional Interfaces
SearchEngine.Login
SearchEngine.Login
Implement this if your site requires users to log in before searching. The
SearchCoordinator calls login(Context) before dispatching any search method. You must also implement isLoginToSearch(Context) to indicate when login is required.SearchEngine.UserRegistration
SearchEngine.UserRegistration
Implement this if the site has optional or required user registration (API keys, OAuth tokens). Provides
isRegistrationRequired(), hasRegistrationData(Context), getRegistrationInfo(Context), and getPreferenceFragmentClass() — the last returning the Fragment the app will open so the user can enter credentials.SearchEngine.SearchOnSite
SearchEngine.SearchOnSite
Implement this to add a “Search on website” menu option in the book detail view.
createSearchOnSiteUrl(Context, Author, Series) should return a URL the app will open in the browser — at least one of Author or Series will be non-null.SearchEngine.AlternativeEditions
SearchEngine.AlternativeEditions
Implement
searchAlternativeEditions(Context, String validIsbn) to return a list of AltEdition objects for a given ISBN. These are passed to any engine implementing CoverByEdition to fetch alternative covers.