NeverTooManyBooks can synchronise your library with external services — currently a Calibre Content Server and the StripInfo.be website. The sync framework is built on a small set of composable abstractions: 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.
SyncServer enum identifies the target, DataReader and DataWriter interfaces handle the data transport, and a per-field SyncAction system gives users fine-grained control over what gets updated on import. This page explains each layer so you can understand how sync works and extend it to a new server if needed.
SyncServer: Registering a Sync Target
SyncServer is an enum in com.hardbacknutter.nevertoomanybooks.sync that acts as the registry of all supported sync destinations. Each constant is both a label and a factory for the reader and writer that talk to that destination:
| Parameter | Meaning |
|---|---|
labelResId | String resource shown in the sync selection UI |
hasLastUpdateDateField | Whether the server tracks a per-book last-modified timestamp that can be used for incremental sync |
syncDateIsUserEditable | Whether the user can manually set the cut-off date in Settings |
SyncServer must implement five abstract methods: isEnabled(), createWriter(), createReader(), getSyncPreferencePrefix(), and createSyncProcessorBuilder().
The Calibre Content Server (
CalibreCS) is the primary reference implementation for the sync framework. If you are adding a new sync target, study the classes under com.hardbacknutter.nevertoomanybooks.sync.calibre — they demonstrate the full reader/writer/processor pattern end to end.DataReader and DataWriter
TheDataReader and DataWriter interfaces in com.hardbacknutter.nevertoomanybooks.io are the generic I/O contracts used by both the sync system and the file-based backup system.
- DataReader
- DataWriter
- Updates enum
readMetaData is called first to give the UI a preview of what will be imported. validate runs lightweight checks (e.g. confirming the Calibre server version is supported). read is the heavy operation that processes every book record.SyncReaderHelper and SyncWriterHelper
These two classes orchestrate the lifecycle of a sync operation from the UI layer. Neither is aViewModel or an Android component — they are plain coordinators that hold configuration and create the underlying reader or writer.
SyncReaderHelper (extends DataReaderHelperBase):
- Holds the target
SyncServerand aSyncReaderProcessor.Builder - Pre-populates
RecordType.BooksandRecordType.Coveras the default record types to import - Accepts an optional
LocalDateTime syncDateto limit the import to records newer than a given date - Accepts extra arguments in a
Bundlefor server-specific parameters (e.g. which Calibre library to read from)
SyncWriterHelper (extends DataWriterHelperBase):
- Holds the target
SyncServer - Pre-populates
RecordType.BooksandRecordType.Coveras defaults - Has a
deleteLocalBooksflag: whentrue, books removed from the remote server are also deleted locally - Also carries a server-specific
Bundle extraArgs
DataReader/DataWriter instances created by SyncServer.createReader() / SyncServer.createWriter().
SyncField and SyncAction
The sync framework gives users per-field control over what happens to each book attribute during an import. Two types drive this:SyncField represents a single book field that participates in sync — for example, the book title, an author list, or a cover image. Each SyncField has:
- A
Type(STRING,LIST, orOTHER) that determines whichSyncActionvalues make sense - A database key (
fieldKey) that maps it to the localBookobject - An optional
enabledKeyfor fields that can be toggled independently
SyncAction is the per-field policy enum. The user selects one action per field in the sync settings screen:
SyncReaderProcessor
SyncReaderProcessor applies the configured SyncAction for each field to every book record received from the sync source. It is built from a SyncReaderProcessor.Builder that collects all the SyncField definitions and reads back the user’s chosen actions from SharedPreferences.
The processor is created inside SyncServer.createReader():
syncProcessorBuilder can be passed in (for testing or programmatic usage); when null the server’s own default builder is used. This pattern means tests can inject a known processor without touching preferences.
Worked Example: Field-Level Sync Flow
The sequence below shows what happens when a Calibre sync import runs for a single book:SyncReaderHelper creates the reader
SyncReaderHelper calls SyncServer.CalibreCS.createReader(...), which builds a SyncReaderProcessor from stored preferences and returns a CalibreContentServerReader.readMetaData is called
The reader connects to the Calibre server and fetches library metadata (library name, book count, server version). This is shown to the user in the confirmation dialog before the import starts.
validate is called
The reader confirms the server API version is supported and that credentials are valid. A
CredentialsException here prompts the user to re-enter their password.read iterates book records
For each book returned by the Calibre API, the
CalibreContentServerReader builds a Book bundle and passes it to SyncReaderProcessor.process(context, localBook, incomingBook).SyncReaderProcessor applies SyncActions
For every registered
SyncField, the processor checks the stored SyncAction. A title field set to CopyIfBlank is only written if localBook.getTitle() is empty. An author list field set to Append merges the incoming author list with the existing one, avoiding duplicates.Adding a New Sync Server
To wire up an entirely new sync target, add a new constant toSyncServer and implement the five abstract methods. Concretely:
- Create a
DataReaderimplementation that connects to your server, fetches book records, and maps them ontoBookbundles - Create a
DataWriterimplementation that reads local books and pushes updates back to your server - Implement
createSyncProcessorBuilder()to declare which fields your server supports and their default actions - Implement
getSyncPreferencePrefix()with a stable, unique preference key prefix - Implement
isEnabled()— typically delegating to a handler class or aBuildConfigflag for gating in-development servers