Bookery
A CLI-first ebook library manager inspired by beets and Calibre. Fix your metadata, organize your library, sync to your Kobo — all from the terminal.
Bookery takes the "metadata-first" approach that made beets great for music and applies it to ebooks. It matches your EPUBs against online sources, lets you review and correct metadata interactively, and keeps your originals untouched.
Status
Active development — daily-driver usable. EPUB metadata extraction, Kindle (MOBI/AZW/AZW3) and PDF-to-EPUB conversion, multi-provider matching (Open Library + Google Books) with consensus merging, a SQLite catalog with per-field provenance, non-destructive write-back, Kobo device sync, a local web UI for browsing/editing, and Obsidian vault-export are all working. Plugin architecture is the main item still on the roadmap.
See docs/roadmap.md for the full plan.
⚠️ Back up your Kobo first
Bookery's library write-back is non-destructive, but Kobo sync rewrites EPUB
files and the device database (.kobo/KoboReader.sqlite). Back up both the
files on your Kobo and that database before syncing, and keep those backups.
I daily-drive this tool, but you run it at your own risk — there is no warranty,
and any data loss is on you, not the project. PRs to make it safer are always
welcome.
Features
- EPUB metadata extraction — reads title, author, ISBN, language, publisher, description, cover, and identifiers from any EPUB
- Kindle-to-EPUB conversion — converts DRM-free MOBI, AZW, and AZW3 (KF8) files to EPUB, preserving metadata, images, cover art, and chapter structure (via NCX TOC).
add,convert, andinfoall accept these suffixes in any case. DRM-protected files are rejected with a message naming the file. - PDF-to-EPUB conversion —
bookery adddetects text-based PDFs, extracts their structure with pdfplumber + a local LLM (LM Studio), and produces a reflowable EPUB. Scanned PDFs are refused (OCR not yet supported). - Kobo sync —
bookery sync kobowalks the catalog, converts each EPUB to.kepub.epubviakepubify, and copies the result to a mounted Kobo. The library itself stays format-canonical (EPUB only); kepub is generated on demand at sync time and cached so re-syncs are free when nothing has changed. Books with series metadata group by series on device: sync writesSeries/SeriesNumberinto the Kobo database directly (the same mechanism Calibre's device driver uses — current firmware ignores series metadata inside sideloaded EPUBs). Newly copied books group on the sync after the device has indexed them. - Collections — group books into named lists, either static (hand-picked) or rule-based (membership derived live from a query like
genre:"Science Fiction"orseries:Dune, so it stays current as the library grows). Seebookery collections. - Collection shelves on device — the same
bookery sync kobomirrors each collection to a Kobo shelf (Shelf/ShelfContent). Bookery owns only shelves whoseInternalNameisbookery-<collection_id>; a user-created shelf that shares a name is skipped, never overwritten. Unchanged shelves are skipped on re-sync (membership hash), and a shelf is removed once its collection is deleted.bookery collections show <id> --sync-statusreports per-device shelf state. - Multi-provider metadata matching — Open Library and Google Books in a consensus merger that prefers values agreed on by ≥2 providers and falls back to a priority order otherwise. ISBN-10/13 lookups are normalized and provider responses are cached.
- Per-field provenance & locking — every cataloged field records which provider supplied it and when. User edits are stamped as
userand locked againstrematch; individual fields can be locked/unlocked explicitly withbookery info --lock/--unlock. - Interactive review — presents candidates in a Rich table, lets you accept, compare details, look up by URL, or skip
- Smart normalization — splits mangled filenames like
SteveBerry-TheTemplarLegacyinto clean search queries, detects embedded author names - SQLite catalog — imports books into a local database for querying, tagging, and integrity checks
- Web UI —
bookery servelaunches a local browser UI for paginated/sortable browsing of the catalog, filter chips, cover thumbnails, a responsive mobile card layout, per-book detail/edit pages, collection create/edit (including raw rule strings, plus an append-only query builder for composing clauses without memorizing the syntax) with inline validation and non-persisting query match preview, search-active-providers, and "apply candidate metadata with diff" rematch flow. - Genre management —
bookery genreauto-maps raw provider subjects to a canonical genre vocabulary, withassign/apply/auto/stats/unmatchedfor curation. - Obsidian vault-export —
bookery vault-exportturns an Obsidian vault into a single EPUB with a hierarchical folder/note TOC, A-Z buckets within each folder, leading-article-aware filing ("The Loop" files under L), resolved[[wiki-links]]and![[image]]embeds, and an optional tag index. Can auto-catalog the export so it ships on the next Kobo sync. - Non-destructive — metadata writes always go to a copy; original file contents are never modified.
addcopies each source into your library by default (--movedeletes the source after a successful catalog insert)
Installation
Requires Python 3.12+.
Install it (to use bookery)
Bookery is published on PyPI as bookery-cli (the bookery name was taken).
The command it installs is still bookery.
uv tool install bookery-cli
# or
pipx install bookery-cli
uv tool install puts bookery in its own isolated environment and drops a
bookery command on your PATH (in ~/.local/bin) — no virtualenv to create
or activate. Just run bookery from anywhere. If the command isn't found after
install, run uv tool update-shell once and restart your shell.
Upgrade later with uv tool upgrade bookery-cli; remove with uv tool uninstall bookery-cli.
If you installed from GitHub before the PyPI release, run uv tool uninstall bookery first.
To run unreleased code from main, install straight from GitHub:
uv tool install git+https://github.com/joecotellese/bookery.git
Develop it (to hack on bookery)
git clone https://github.com/joecotellese/bookery.git
cd bookery
uv sync
uv sync installs into the project's .venv. Run the CLI with
uv run bookery ..., or activate the venv first
(source .venv/bin/activate) and call bookery directly.
Optional: PDF conversion
The PDF path in bookery add routes the document
through a single semantic LLM call that reassembles articles/chapters
into a clean EPUB. You'll need an OpenAI-compatible endpoint with a
model strong enough to return structured JSON.
Local (default) — LM Studio with a long-
context instruct model (known-good: Qwen 2.5 7B Instruct 1M).
Load the model with ≥16k context, enable the local server, then point
bookery at it via ~/.bookery/config.toml:
[convert.semantic]
provider = "lm-studio"
model = "qwen2.5-7b-instruct-1m"
base_url = "http://localhost:1234/v1"
api_key_env = "" # empty for local; no key needed
prompt_version = 1
llm_max_retries = 2
Cloud — swap provider, model, base_url, and point
api_key_env at the env var holding your key. Never write the key
into config.toml:
[convert.semantic]
provider = "openai"
model = "gpt-5.4-nano"
base_url = "https://api.openai.com/v1"
api_key_env = "OPENAI_API_KEY"
prompt_version = 1
See docs/config.example.toml for a fully
annotated config with LM Studio, Moonshot/Kimi, and OpenAI examples.
Semantic responses are cached under ~/.bookery/data/convert_cache.db
so re-runs on the same PDF skip the LLM. Safe to delete at any time;
bumping prompt_version invalidates only stale entries.
Quick Start
# Inspect a single EPUB (loose file on disk)
bookery info ~/Books/some-book.epub
# Scan a directory and report format coverage
bookery inventory ~/Books/
# Convert Kindle files (.mobi, .azw, .azw3) to EPUB
bookery convert ~/Books/ -o ~/Books-epub/
# Match EPUBs against Open Library and write corrected copies
bookery match ~/Books/ -o ~/Books-fixed/
# Auto-accept high-confidence matches (no prompts)
bookery match ~/Books/ -o ~/Books-fixed/ --yes
# Add a single EPUB to the catalog (copies into ~/.library/ by default)
bookery --db ~/library.db add ~/Books/dune.epub
# Add an entire directory of EPUBs (recursive)
bookery --db ~/library.db add ~/Books/
# Add and remove the sources after they land in the library
bookery --db ~/library.db add ~/Downloads/ --move
# Search and browse the catalog
bookery search "martian"
bookery ls --tag fiction
bookery info 42
Common options
-
--db PATHis a top-level option; put it before the subcommand so every command picks it up:bookery --db ~/library.db ls. Subcommand-level--dbstill works and overrides the global. -
-y/--yesauto-accepts high-confidence matches without prompting (valid onadd,match,rematch,convert).-q/--quietis a deprecated alias. -
-t/--thresholdsets the confidence cutoff for auto-accept. The default comes from[matching].auto_accept_thresholdin~/.bookery/config.toml(default0.8):[matching] auto_accept_threshold = 0.85 cache_ttl_days = 30 # metadata response cache TTL (default 30) providers = ["openlibrary", "googlebooks", "hardcover"] # priority order; default ["openlibrary"] min_request_interval = 0.1 # seconds between provider HTTP requests (default 0.1) fetch_covers = true # download & embed candidate covers during matching (default true)
-
--no-cacheonmatch/rematchbypasses the on-disk metadata response cache and forces fresh provider lookups. Cached responses live at{data_dir}/metadata_cache.dband expire after[matching].cache_ttl_days. -
--no-coversonadd/match/rematchskips downloading the accepted candidate's cover image (useful offline or for throttled batch runs). The default comes from[matching].fetch_covers(defaulttrue). When enabled, the cover is embedded in the same write as the text metadata; a failed download is non-fatal — the text still applies and one warning line is printed. -
[matching].providersselects and orders metadata sources. With a single entry the named provider is used directly; with two or more, results are merged by a consensus step that prefers values agreed on by ≥2 providers and falls back to the priority order otherwise. Supported:openlibrary,googlebooks,hardcover. Exception:series,series_index,rating, andratings_countprefer Hardcover's value regardless of provider order (it is the only source with reliable series positions); ≥2-provider agreement still wins the vote, but Hardcover's spelling of an agreed series name is used. Title-pattern heuristics fill series data that providers leave empty and are stamped source=heuristic, so they never outrank provider or user values. -
Google Books API key — set the
GOOGLE_BOOKS_API_KEYenvironment variable to authenticate Google Books requests. Without it, requests use the shared anonymous per-IP quota, which a full-libraryrematchexhausts quickly (HTTP 429). Create a free key in the Google Cloud Console: create/select a project, enable the Books API, then Credentials → Create credentials → API key. Thenexport GOOGLE_BOOKS_API_KEY=AIza.... The key is read from the environment only — never written to config. -
Hardcover API key — set the
HARDCOVER_API_KEYenvironment variable to enable thehardcoverprovider. Hardcover is the strongest source for series name/position and community ratings, but it requires a token: create a free account at hardcover.app, then copy the token from Account Settings → Hardcover API. Thenexport HARDCOVER_API_KEY=.... The key is read from the environment only — never written to config. Without it, a configuredhardcoverprovider logs one warning and returns no results. Rate limit is 60 requests/minute; tokens expire January 1st each year. -
[matching].min_request_intervalis the minimum seconds between provider HTTP requests (default0.1). Raise it to throttle a bulkrematchbelow a provider's rate limit; on a429the client also honors the response'sRetry-Afterheader (capped at 60s) before retrying. -
Per-field provenance is recorded for every cataloged book in the
book_field_provenancetable. Usebookery info <id> --provenanceto see which source supplied each field and when it was fetched. Usebookery info <id> --set field=valueto hand-edit a value (it's stamped asuserand locked against overwrite), and--lock field/--unlock fieldto gate fields againstrematch.
Commands
Metadata & Matching
| Command | Description |
|---|---|
match <path> -o <dir> |
Match metadata for loose EPUB files (not yet in the catalog) and write corrected copies |
rematch [book_id] |
Re-run matching on cataloged books and update the database |
series ls |
List series in the catalog with book counts and missing-position gaps |
series backfill [book_id|--all|--tag] |
Fill missing series/series_index from providers (title-pattern heuristics as fallback) and rewrite library EPUBs whose series meta is stale (--dry-run to preview) |
A provider's series is only taken from a candidate whose title actually
corresponds to the book being backfilled. A match confidence is computed across
title, author, ISBN and language, so a book whose own title carries little
searchable signal (Book 17 - Remnant) could clear the threshold against an
entirely unrelated book and inherit its series. Books matched by ISBN skip
this check — the ISBN already settles identity. When a series is declined, the
run says which candidate was rejected and why rather than reporting a bare "no
series found". A series position that merely repeats a Book N prefix from the
book's own title is dropped as well: a missing position is better than a wrong
one.
Conversion
| Command | Description |
|---|---|
convert <path> -o <dir> |
Convert Kindle files (MOBI/AZW/AZW3, DRM-free) to EPUB format (supports --match to chain into matching). Exits 1 if any file fails. |
vault-export --vault <path> -o <file> |
Export an Obsidian vault to a single EPUB with clickable TOC, resolved wiki-links, and an optional tag index. Requires pandoc. |
Library Catalog
| Command | Description |
|---|---|
add <path> |
Add a single EPUB/PDF/Kindle file (MOBI, AZW, AZW3) or a directory of EPUBs to the library (copies into library_root, catalogs). Files default to --match; directories default to --no-match. Supports --move (ignored for PDF and Kindle input), --convert (also picks up Kindle files in a directory), --force-duplicates, -o/--output-dir. import is a deprecated alias. |
remove <id>... |
Delete one or more books from the catalog and disk (-y skips prompt; --keep-file keeps the file) |
prune |
Remove catalog rows whose underlying files are missing |
ls |
List all books in the catalog (filter with --series or --tag) |
info <id-or-path> |
Show metadata for a cataloged book by ID, or for a loose EPUB on disk. Catalog mode supports --provenance, --set field=value, --lock field, --unlock field. inspect is a deprecated alias. |
search <query> |
Search the catalog by title, author, or description |
inventory <path> |
Scan a directory tree and report ebook format coverage |
reveal <query> |
Open the on-disk folder for a book (--print to print the path instead). folder is a deprecated alias. |
Organization
| Command | Description |
|---|---|
tag add <id> <tag> |
Add a tag to a book |
tag rm <id> <tag> |
Remove a tag from a book |
tag ls |
List all tags with book counts |
genre assign <id> <genre> |
Assign a canonical genre to a book |
genre auto-assign |
Auto-assign genres from subjects for cataloged books |
genre auto |
Auto-map provider subjects to canonical genres across the catalog |
genre ls |
List all canonical genres with book counts |
genre stats |
Show the most common subjects that don't map to a canonical genre |
genre unmatched |
Show books with subjects but no genre assigned |
mark finished <id> |
Mark a book as finished (also mark reading <id>, mark unread <id>; supports --bulk-from FILE) |
authors list |
List authors with book counts (--duplicates shows only multi-spelling clusters; --needs-review lists malformed names for manual merge) |
authors normalize |
Reorder Surname, Given → Given Surname (dry-run by default; --apply to write; --include-reversed for names with no twin) |
authors merge <forms…> --into <canonical> |
Merge arbitrary spellings into one canonical name (dry-run by default; --apply to write) |
authors fix-sort |
Backfill opf:file-as so devices sort authors by surname (dry-run by default; --apply to write) |
verify |
Check for missing or changed files (supports --check-hash) |
Collections
Collections group books into named lists. A collection is either static (hand-picked membership) or rule-based (membership derived live from a query). The two are mutually exclusive — a rule-based collection holds no hand-picked rows, and its members are recomputed on every read, so it stays current automatically as the library changes.
| Command | Description |
|---|---|
collections create <name> |
Create a static collection (-d/--description optional) |
collections create <name> --query '<rule>' |
Create a rule-based collection, e.g. --query 'genre:"Science Fiction"' |
collections ls |
List collections with live book counts |
collections show <id> |
Show a collection's books; rule-based collections also show the rule and live match count (--sync-status for per-device shelf state) |
collections add-books <id> <book_id>... |
Add books to a static collection |
collections remove-books <id> <book_id>... |
Remove books from a static collection |
collections edit <id> --query '<rule>' |
Convert a static collection to rule-based (discards hand-picked books) |
collections edit <id> --clear-query |
Convert a rule-based collection to static, snapshotting current members |
collections preview --query '<rule>' |
Show which books a rule matches, without saving |
collections query-help |
Print the full query reference (fields, operators, dates, examples) |
collections rename <id> <new_name> |
Rename a collection |
collections rm <id> |
Delete a collection (books are not deleted) |
Converting between the two kinds is one-way destructive in one direction: setting a rule on a static collection discards its hand-picked books, while clearing a rule snapshots the currently-matching books into a static list. In the web edit form these conversions are gated — when a static collection with hand-picked books gains a rule, or any rule is cleared, the form warns with the exact count of books affected and requires a second confirm before writing.
From the web UI (bookery serve), reach collections via the Collections link
in the header. A book's detail page has a Collections section that lists the
static collections it belongs to (each with a Remove), an Add to
collection picker (static collections only — rule-based membership is derived),
and a New collection from this book link that seeds a rule from the book's
series or author. Deleting a collection from its detail page asks for
confirmation first; your books are never deleted.
Rule query language
A rule query is a Lucene-style expression over a
whitelisted set of fields, parsed permissively and validated restrictively — an
unknown field, bad value, or unsupported shape is rejected with a message naming
what's allowed. Run bookery collections query-help for the in-terminal
reference.
| Field | Matches |
|---|---|
id |
exact book id |
title |
exact, phrase, or prefix* (left-anchored) |
author |
substring (contains) |
series |
exact |
genre |
exact canonical genre |
tag |
exact |
language |
exact |
publisher |
exact |
subject |
substring (contains) |
isbn |
exact |
year |
publication year — =, range, or comparison |
rating |
=, range, or comparison |
added |
date added (ISO YYYY-MM-DD) — =, range, or comparison |
Operators: AND, OR, NOT, grouping with ( ), and +/- prefixes
(require/exclude). The numeric/date fields (year, rating, added) also
accept ranges [a TO b] (inclusive), {a TO b} (exclusive), open-ended *,
and comparisons >= <= > <. Fuzzy (~) and boost (^) are not
supported.
series:Dune
genre:"Science Fiction" AND year:[2020 TO *]
rating:>=4
author:"Ursula K. Le Guin" NOT tag:reread
Web UI
| Command | Description |
|---|---|
serve |
Launch the local web UI (--host, --port; default 127.0.0.1:5000) |
The web UI provides paginated and sortable browsing, filter chips, cover thumbnails, a responsive mobile card layout, per-book detail and edit pages, a "search active providers" flow that lets you apply candidate metadata with a side-by-side diff, and inline delete. A Columns control on the book list toggles the optional Series, ISBN, Language, Publisher, Added, and Enriched columns (Title, Author, and cover always show); the choice is remembered per browser. The default view shows Title, Author, Added, and Enriched. The diff panel has a checkbox per changed field (all checked by default), so you can apply one, some, or all of a candidate's fields — including a cover-only apply; unchecked fields keep their current values. Subjects are among the applyable fields: applying them re-derives the book's genre(s) from the provider's subjects, the same auto-mapping bookery genre performs on the CLI. When you apply a candidate, its cover image (if the provider offers one and the cover row is checked) is fetched and embedded into the non-destructive EPUB copy alongside the text fields; a cover-fetch failure is non-fatal — the text metadata still applies and the result message notes the cover was skipped.
Series get their own read-only browsing surface: a Series item in the masthead leads to an index of every series with book counts (and a hint when positions are missing), each series page lists its books ordered by position, and a book's detail page shows its series as "Series #1 of 14" linking back to the series page. The book list's Series column is sortable, grouping a sorted library by series with unpositioned and unseriesed books sinking to the bottom. Series metadata comes from bookery series backfill (see Metadata & Matching above).
Collections can be created and edited directly from the web UI. "New collection" (on the collections page) opens a form for the name, an optional description, and an optional raw rule query (the same Lucene subset as collections create --query); leave the query blank for a hand-picked static collection. Each collection's detail page has an "Edit" affordance for changing its name, description, and (for an already rule-based collection) its rule. Invalid input — a blank or duplicate name, or an unparseable rule — is reported inline on the form with the field whitelist hint, never as an error page. A "Preview matches" button on the form resolves the rule without saving and shows the true match count plus a capped sample, so you can see exactly which books a rule selects before committing to it.
For beginners who don't want to memorize the query syntax, the form also has a collapsible Query builder (collapsed by default). Pick a field from the whitelist, type a value, and "Add condition (AND)" appends a correctly-quoted field:"value" clause to the rule textarea — quoting is handled server-side so the composed clause is always valid. The builder only ever appends; the raw textarea remains the single source of truth, so advanced users can keep hand-editing it (OR/NOT, grouping, ranges, comparisons) and ignore the builder entirely.
Device sync
| Command | Description |
|---|---|
sync kobo |
Convert library EPUBs to .kepub.epub, copy to a mounted Kobo, and mirror collections to device shelves |
sync kobo --target <path> |
Override auto-detection with an explicit mount point |
sync kobo --dry-run |
Show what would be copied without touching the device |
sync kobo --no-kepub |
Send plain EPUBs untouched — skip kepub conversion (and the kepubify dependency) |
collections show <id> --sync-status |
Show per-device shelf sync state for a collection |
Conversion to .kepub.epub requires the
kepubify binary on PATH
(brew install kepubify on macOS). Files are written to
<kobo>/Bookery/Author/Title/Title.kepub.epub — the dedicated Bookery/
subdirectory keeps synced content visibly separate from Calibre
sideloads, Kobo store purchases, and library borrows. Pass --no-kepub
(or set kepub = false under [sync.kobo]) to copy plain EPUBs as
Title.epub instead; kepub adds richer on-device progress/stats, but
Kobo reads plain sideloaded EPUBs fine, and this path needs no kepubify.
Sync is currently additive: existing files on the device are never
deleted — including when you switch a book's format, so the old
.kepub.epub/.epub is left in place. A SQLite cache at
{data_dir}/kepub_cache.db keyed on the source EPUB hash plus the
kepubify version makes re-syncs effectively free when nothing has
changed.
[sync.kobo]
kepub = false # default true; send plain EPUBs when false
Readers sort authors by the EPUB's opf:file-as key. If a device files
an author under their given name (e.g. "Brandon" instead of "Sanderson"),
the library copies are missing that key — run bookery authors fix-sort
to backfill a surname-first file-as, then re-sync.
With --apply, fix-sort verifies the rewritten metadata before replacing each
library copy. A mismatch names the failed fields and keeps the original file.
If hashing fails after replacement, the book still counts as updated; a warning
explains that its stored hash is stale and verify --check-hash may report it
as modified.
Some devices (Kobo) ignore file-as and sort by the raw dc:creator
text instead, so a name stored as Cussler, Clive files under "C". Fix
the catalog with bookery authors normalize (reorders Surname, Given
to Given Surname) or bookery authors merge (collapses typos and
duplicate spellings into one canonical name). Both are dry-run by default
and back the database up before --apply. The corrected name flows into
the EPUB's dc:creator on the next write-back and re-sync. Use
bookery authors list --duplicates first to see what would change.
The vault-export workflow
Turn an Obsidian vault into a single EPUB — one chapter per note, with a
clickable TOC, resolved [[wiki-links]] and ![[image]] embeds, and an
optional tag index at the end. Requires pandoc
(brew install pandoc on macOS).
Run with flags:
bookery vault-export --vault ~/obsidian-vault -o vault.epub \
--include-folder "3_Permanent Notes" --include-folder "2_Literature Notes" \
--index --exclude-tag type/meeting
--folder is accepted as a deprecated alias for --include-folder and will
be removed in a future release.
Or set defaults once in ~/.bookery/config.toml and just run
bookery vault-export -o vault.epub:
[vault_export]
vault_path = "~/obsidian-vault"
folders = ["3_Permanent Notes", "2_Literature Notes"]
include_index = true
index_exclude_prefixes = ["type/"] # hide tags like `type/permanent` from index
index_min_count = 1
exclude_tags = ["type/meeting"] # drop notes with these exact frontmatter tags
default_author = "Your Name"
uuid_mode = "stable" # "stable" keeps the same dc:identifier
# across re-exports so Kobo updates in place.
# Pass `--random-ids` on the command line
# for a fresh identifier per export.
catalog = true # auto-add the EPUB to the bookery library
# so it ships on the next `sync kobo`
exclude_tags matches the full tag string exactly (type/meeting skips
notes tagged type/meeting but not type/permanent). Callouts, block
references, note embeds (![[note]]), and Dataview queries are not
resolved in this version.
With catalog = true (or --catalog on the command line), the export is
imported into the library and then deployed to your reader on the next
bookery sync kobo — no separate bookery add step. A vault export is a
point-in-time snapshot, so re-running replaces the prior catalog row and
EPUB rather than piling up a new copy each day.
The match workflow
When you run bookery match, Bookery will:
- Extract metadata from each EPUB
- Normalize mangled titles (CamelCase splitting, word segmentation)
- Search Open Library by ISBN first, then fall back to title/author
- Present you with scored candidates to review
- Write your chosen metadata to a copy of the file
During review, you can:
- [1-N] Accept a candidate
- [v1-vN] View a side-by-side comparison
- [u] Look up a specific Open Library URL
- [s] Skip this book
- [k] Keep the original metadata
Project Structure
src/bookery/
cli/ # Click commands
core/ # Pipeline logic (non-destructive write-back, conversion)
formats/ # Format handlers (EPUB via ebooklib, MOBI via KindleUnpack)
metadata/ # Matching engine
openlibrary.py # Open Library API provider
scoring.py # Weighted confidence scoring
normalizer.py # Title/author normalization (CamelCase, wordninja)
candidate.py # MetadataCandidate dataclass
provider.py # MetadataProvider protocol
db/ # SQLite catalog (schema, CRUD, search, migrations)
Development
# Install dev dependencies
uv sync
# Run tests
uv run pytest
# Run linter
uv run ruff check src/ tests/
# Run a specific test file
uv run pytest tests/unit/test_scoring.py -v
Tests are isolated from your real ~/.bookery/library.db by an autouse
guardrail. If you previously ran the suite without this guardrail and your
catalog now reports source missing: /private/var/folders/.../pytest-of-...,
see "Recovering from test pollution"
in CONTRIBUTING.md.
Roadmap
Bookery is being built in phases:
EPUB metadata read/write + CLI skeletonMulti-provider matching (Open Library + Google Books) with consensus merger and interactive reviewSQLite catalog, import pipeline, per-field provenance, query commandsMOBI-to-EPUB conversionPDF-to-EPUB conversion (semantic, local-LLM)Kobo device syncObsidian vault-export to EPUBWeb UI for browse/search/edit- Next: Plugin architecture (format and provider plugins)
- Polish (covers cache, performance benchmarks, config polish)
See docs/roadmap.md for detailed checklists.
Design Principles
- Metadata-first — get the data right before organizing files
- Non-destructive — original file contents are never modified; metadata writes go to a copy in the library
- CLI-first — every feature is a terminal command; no GUI required
- Extensible — providers, formats, and devices will be pluggable
- Respectful — rate-limited API usage, no scraping
Contributing
Bookery is AI-friendly and developed with the help of Claude Code. AI-assisted contributions are welcome alongside traditional ones.
See CONTRIBUTING.md for coding standards, testing requirements, and how to get started.
License
Metadata
Release files for bookery-cli 2026.10.2
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| bookery_cli-2026.10.2.tar.gz | 384.0 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| bookery_cli-2026.10.2-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 821.3 kB
Release files / bookery_cli-2026.10.2.tar.gz
| Download URL | bookery_cli-2026.10.2.tar.gz |
|---|---|
| Size | 384.0 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
ddf35bfc19e4ce97d4acc884c5062bbd99efa7a68a80c2b80edefc525d758ad1
|
|
BLAKE2b-256 checksum How to use checksums |
52ef087bc61ccc842f8b3b584eea32e27a933897598d1063233b4d0b75b61c4d
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
uv/0.13.1 {"installer":{"name":"uv","version":"0.13.1","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
|
Release files / bookery_cli-2026.10.2-py3-none-any.whl
| Download URL | bookery_cli-2026.10.2-py3-none-any.whl |
|---|---|
| Size | 437.3 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
1dbbcc19ce0abfd1aacd3b76ac89cabf7275df902deb66c7d49f7fe7f443ee04
|
|
BLAKE2b-256 checksum How to use checksums |
85cb0652bf23d9886e44bc8a2ee8eaa79155054ada528dbfe4dfecb6485d9e61
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
uv/0.13.1 {"installer":{"name":"uv","version":"0.13.1","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
|