hardcover_tools is a maintainable rewrite of a messy but working monolithic Calibre/Hardcover audit script.
The repo keeps the current script as the behavioural source of truth, but restructures the project around:
- thin CLI entrypoints
- reusable shared core modules
- conservative mutation boundaries
- parity-focused tests around the fragile ranking and routing rules
Editable install:
python3 -m venv .venv
. .venv/bin/activate
python -m pip install -U pip setuptools wheel
python -m pip install -e .Editable install with test dependencies:
python -m pip install -e '.[dev]'Quote '.[dev]' under zsh so the extras spec is not treated as a glob.
Non-editable install:
python -m pip install .After installation the console scripts are:
hardcover-audit --help
hardcover-discovery --help
hardcover-apply --helpYou can still use the module entrypoints directly:
python3 -m hardcover_tools.cli.audit --help
python3 -m hardcover_tools.cli.discovery --help
python3 -m hardcover_tools.cli.apply --helpThe project exposes three workflows:
-
auditAudits books already in Calibre against Hardcover and local ebook metadata. -
discoveryFinds missing series entries and related author books, then classifies them into shortlist/review/suppressed buckets. It can also optionally export or push conservative downstream queues for Bookshelf and Shelfmark from those already-classified rows. -
applyReadsaudit/write_plan.csvand applies approved identifier updates, with optional Calibre title/author updates and optional file-level metadata write-back.
- a Calibre library root
- a
metadata.dbinside that library root, or an explicit--metadata-db - the working ebook files referenced by Calibre
audit and discovery require HARDCOVER_TOKEN as a raw token:
export HARDCOVER_TOKEN='your_raw_token_here'Use the raw token only. Do not prefix it with Bearer .
--cache-pathfor the Hardcover cache database--author-aliases-jsonfor author normalization aliases--ebook-meta-commandor--docker-ebook-meta-containerwhen ebook metadata extraction needs an explicit tool path/container--cwa-app-dband/or--cwa-dirs-jsonfor opt-in Calibre-Web-Automated runtime resolution
If ebook-meta is unavailable, the tools still run, but metadata probing falls back to weaker EPUB/ZIP/content extraction paths. For real-library audit quality, a working ebook-meta on the host or in the configured Docker container is strongly preferred.
audit, discovery, and apply can optionally resolve their effective --library-root and --metadata-db from Calibre-Web-Automated runtime files.
--cwa-app-db /config/app.db--cwa-dirs-json /app/calibre-web-automated/dirs.json- the flags are opt-in and do nothing unless you pass them
- explicit
--library-rootand--metadata-dbstill win - when CWA split-library mode is enabled in
app.db, the tools use:config_calibre_split_diras the library root<config_calibre_dir>/metadata.dbas the metadata DB
- when split-library mode is not enabled:
dirs.json.calibre_library_diris preferred for the library root when available- otherwise
app.db.config_calibre_diris used - the metadata DB defaults to
<library-root>/metadata.db
Unless you override them explicitly:
--metadata-dbdefaults to<library-root>/metadata.db--cache-pathdefaults to<library-root>/hardcover_cache.sqlite--output-dirdefaults to<library-root>/audit_output_YYYYMMDD_HHMMSSapply --write-planfirst looks for<library-root>/audit/write_plan.csv, then falls back to<library-root>/write_plan.csvrun.logis written at the root of each output directory
If an older <library-root>/hardcover_cache.json exists, it is detected and imported into the SQLite cache on first use.
Each command writes a timestamped output directory by default unless you pass --output-dir.
audit/actions_operator.csvaudit/actions.csvaudit/write_plan.csvaudit/summary.mdrun.log
discovery/candidates.csvdiscovery/summary.mddiscovery/bookshelf_queue.csvwhen--export-bookshelfor--push-bookshelfis useddiscovery/bookshelf_queue.jsonwhen--export-bookshelfor--push-bookshelfis useddiscovery/bookshelf_push_log.csvwhen--export-bookshelfor--push-bookshelfis useddiscovery/bookshelf_summary.mdwhen--export-bookshelfor--push-bookshelfis useddiscovery/shelfmark_queue.csvwhen--export-shelfmarkor--push-shelfmarkis useddiscovery/shelfmark_queue.jsonwhen--export-shelfmarkor--push-shelfmarkis useddiscovery/shelfmark_push_log.csvwhen--export-shelfmarkor--push-shelfmarkis useddiscovery/shelfmark_release_candidates.csvwhen--export-shelfmark-releasesor--push-shelfmark-downloadis useddiscovery/shelfmark_release_candidates.jsonwhen--export-shelfmark-releasesor--push-shelfmark-downloadis useddiscovery/shelfmark_selected_releases.csvwhen--export-shelfmark-releasesor--push-shelfmark-downloadis useddiscovery/shelfmark_download_log.csvwhen--export-shelfmark-releasesor--push-shelfmark-downloadis useddiscovery/shelfmark_summary.mdwhen any Shelfmark integration flag is usedrun.log
apply/apply_log.csvapply/summary.mdrun.log
Recommended end-to-end flow:
- Export
HARDCOVER_TOKEN. - Run
auditagainst the Calibre library root. - Review
audit/summary.md,audit/actions.csv, andaudit/write_plan.csv.actions_operator.csvis the compact triage sheet.actions.csvis the full forensic non-keep/review sheet.write_plan.csvis the full-library apply sheet and can includekeep_hardcover_idconfirmation rows as well as unsafe/manual-review rows. - Optionally run
discoveryto generate shortlist/review/suppressed discovery candidates. - Run
apply --dry-runagainst the chosenwrite_plan.csv. - Only rerun
applywithout--dry-runonce the dry-run output looks correct.
Example audit:
HARDCOVER_TOKEN='your_raw_token_here' \
hardcover-audit \
--library-root /path/to/calibre-libraryExample audit against a standard CWA deployment:
HARDCOVER_TOKEN='your_raw_token_here' \
hardcover-audit \
--cwa-app-db /config/app.db \
--cwa-dirs-json /app/calibre-web-automated/dirs.jsonExample apply against a CWA split-library deployment:
hardcover-apply \
--cwa-app-db /config/app.db \
--cwa-dirs-json /app/calibre-web-automated/dirs.json \
--write-plan /path/to/output/audit/write_plan.csv \
--dry-runAudit logging defaults to compact phase/progress summaries. Add --verbose for per-book decision lines, and add --debug-hardcover only when you need low-level Hardcover HTTP/cache chatter.
Example discovery:
HARDCOVER_TOKEN='your_raw_token_here' \
hardcover-discovery \
--library-root /path/to/calibre-libraryThe discovery workflow can optionally turn conservative discovery rows into a native Bookshelf queue, and can optionally push that queue into a Bookshelf instance.
Safety model:
- fully opt-in: nothing is exported or pushed unless you pass
--export-bookshelfor--push-bookshelf - default approval is
--bookshelf-approval shortlist-only shortlist-onlyexports onlyeligible_for_shortlist_boolean=Truerowssafe-onlyis stricter and keeps only the plainshortlistbucketall-approvedkeeps all non-suppressed discovery rows- suppressed rows are never exported or pushed
- live search triggering is off by default
--dry-runresolves lookup strategy, records queue/log outputs, and skips add/search mutations
Bookshelf connection and add settings:
--bookshelf-url--bookshelf-api-key--bookshelf-root-folder--bookshelf-quality-profile-id--bookshelf-metadata-profile-id- optional
--bookshelf-trigger-search - optional
--bookshelf-mode book|author|autowith defaultbook
Lookup routing:
discoverycallsGET /api/v1/config/developmentand inspectsmetadataSource.- If
metadataSourceis clearly Hardcover-backed, the integration allows provider-neutral direct ID lookups:edition:<hardcover-edition>work:<hardcover-id>
- If
metadataSourceis not clearly Hardcover-backed, raw Hardcover IDs are treated as unsafe and lookup falls back to:isbn:<isbn13>asin:<asin>- title + author text search
- Ambiguous matches are skipped and logged instead of forced.
Bookshelf add behavior:
- uses
GET /api/v1/search?term=...for lookup - uses the returned
BookResourceorAuthorResourceas the add payload base - fills required Bookshelf settings conservatively
- disables silent post-add searching unless
--bookshelf-trigger-searchis explicitly set - uses explicit command enqueueing for post-add search when requested
- writes queue and push trace files under
discovery/
Example queue export only:
HARDCOVER_TOKEN='your_raw_token_here' \
hardcover-discovery \
--library-root /path/to/calibre-library \
--export-bookshelfExample dry-run push:
HARDCOVER_TOKEN='your_raw_token_here' \
hardcover-discovery \
--library-root /path/to/calibre-library \
--push-bookshelf \
--bookshelf-url http://bookshelf.local:8787 \
--bookshelf-api-key YOUR_BOOKSHELF_API_KEY \
--bookshelf-root-folder /library/books \
--bookshelf-quality-profile-id 1 \
--bookshelf-metadata-profile-id 1 \
--dry-runExample live push with explicit post-add search:
HARDCOVER_TOKEN='your_raw_token_here' \
hardcover-discovery \
--library-root /path/to/calibre-library \
--push-bookshelf \
--bookshelf-url http://bookshelf.local:8787 \
--bookshelf-api-key YOUR_BOOKSHELF_API_KEY \
--bookshelf-root-folder /library/books \
--bookshelf-quality-profile-id 1 \
--bookshelf-metadata-profile-id 1 \
--bookshelf-trigger-searchExample Hardcover-backed direct-ID mode:
HARDCOVER_TOKEN='your_raw_token_here' \
hardcover-discovery \
--library-root /path/to/calibre-library \
--push-bookshelf \
--bookshelf-url http://bookshelf-hardcover.local:8787 \
--bookshelf-api-key YOUR_BOOKSHELF_API_KEY \
--bookshelf-root-folder /library/books \
--bookshelf-quality-profile-id 1 \
--bookshelf-metadata-profile-id 1This mode only uses raw Hardcover IDs when the target instance reports a clearly Hardcover-backed metadataSource.
Example non-Hardcover fallback mode:
HARDCOVER_TOKEN='your_raw_token_here' \
hardcover-discovery \
--library-root /path/to/calibre-library \
--push-bookshelf \
--bookshelf-url http://bookshelf-nonhardcover.local:8787 \
--bookshelf-api-key YOUR_BOOKSHELF_API_KEY \
--bookshelf-root-folder /library/books \
--bookshelf-quality-profile-id 1 \
--bookshelf-metadata-profile-id 1If that Bookshelf instance reports a non-Hardcover metadataSource, the integration falls back to ISBN, ASIN, and title+author lookup only.
Shelfmark now has two separate discovery-side workflows in hardcover-discovery:
- a conservative metadata request workflow
- a concrete release search workflow with optional explicit download queueing
Both workflows are entirely opt-in and preserve the existing discovery ranking and classification behavior.
Safety model:
- fully opt-in: nothing is exported or pushed unless you pass a Shelfmark flag
- default approval is
--shelfmark-approval shortlist-only shortlist-onlyexports onlyeligible_for_shortlist_boolean=Truerowssafe-onlyis stricter and keeps only the plainshortlistbucketall-approvedkeeps all non-suppressed discovery rows- suppressed rows are never exported or pushed
- source selection for concrete releases is always explicit via
--shelfmark-source - queue/download is never attempted unless you pass
--push-shelfmark-download --dry-runwrites the same queue/log artifacts and skips live request or download mutations
Shelfmark connection and control flags:
--shelfmark-url- optional
--shelfmark-username - optional
--shelfmark-password - optional
--shelfmark-note - optional
--shelfmark-approval shortlist-only|safe-only|all-approved - optional
--export-shelfmark - optional
--push-shelfmark - optional
--export-shelfmark-releases - optional
--push-shelfmark-download - required for release workflows:
--shelfmark-source - optional for release workflows:
--shelfmark-content-type - optional for release workflows:
--shelfmark-selection best|most_seeders|first|largest|preferred-format - optional for release workflows:
--shelfmark-format-keywords epub,kepub,pdf,... - optional for release workflows:
--shelfmark-min-seeders N
Shelfmark request workflow:
discoverybuilds a request queue from already-classified discovery rows.- Live request push logs in with
POST /api/auth/login. - It checks
GET /api/request-policy. - Live request submission only proceeds when:
requests_enabled=true- the ebook request policy resolves to
request_book
- Discovery rows are submitted as Hardcover-backed book requests:
provider=hardcoverprovider_id=<hardcover-id>content_type=ebookrequest_level=booksource=*
- Duplicate pending requests are logged as explicit duplicate skips.
Shelfmark release workflow:
discoverysearches Shelfmark releases for eligible rows.- It prefers the metadata-backed route first:
GET /api/releases?provider=hardcover&book_id=<hardcover-id>&source=<explicit-source>&content_type=<content-type>
- If that yields no releases, it falls back conservatively to a title+author search against the same explicit source.
- Returned releases are filtered locally for:
- explicit source match
- requested content type when the release advertises one
- requested format keywords when provided
- minimum seeders when provided
- presence of concrete
sourceandsource_id
- One accepted release is selected with an explicit deterministic rule:
firstmost_seederslargestpreferred-formatbest
- Only
--push-shelfmark-downloadqueues the selected release toPOST /api/releases/download. Without that flag,discoveryexports the chosen release and logs what would have been queued.
Selection rules:
first: first accepted release in Shelfmark response ordermost_seeders: highest seeders, then larger size, then stable response orderlargest: largestsize_bytes, then higher seeders, then stable response orderpreferred-format: earliest matching keyword in--shelfmark-format-keywords, then higher seeders, then larger sizebest: if format keywords are provided, earliest matching keyword wins; otherwise a conservative built-in format priority is used, then higher seeders, then larger size
Auth model:
- the request workflow requires username/password because it depends on Shelfmark’s authenticated request API
- the release workflow can run anonymously against a no-auth Shelfmark instance, or with username/password when the instance requires login
- no dedicated Shelfmark API-key flow was found in the inspected Shelfmark codebase, so this integration uses the real username/password auth surface when auth is needed
Artifacts:
discovery/shelfmark_queue.csvdiscovery/shelfmark_queue.jsondiscovery/shelfmark_push_log.csvdiscovery/shelfmark_release_candidates.csvdiscovery/shelfmark_release_candidates.jsondiscovery/shelfmark_selected_releases.csvdiscovery/shelfmark_download_log.csvdiscovery/shelfmark_summary.md
Example request queue export only:
HARDCOVER_TOKEN='your_raw_token_here' \
hardcover-discovery \
--library-root /path/to/calibre-library \
--export-shelfmarkExample request dry-run validation:
HARDCOVER_TOKEN='your_raw_token_here' \
hardcover-discovery \
--library-root /path/to/calibre-library \
--push-shelfmark \
--shelfmark-url http://shelfmark.local:8084 \
--shelfmark-username YOUR_SHELFMARK_USERNAME \
--shelfmark-password YOUR_SHELFMARK_PASSWORD \
--dry-runExample release export only:
HARDCOVER_TOKEN='your_raw_token_here' \
hardcover-discovery \
--library-root /path/to/calibre-library \
--export-shelfmark-releases \
--shelfmark-url http://shelfmark.local:8084 \
--shelfmark-source libgenExample dry-run release search:
HARDCOVER_TOKEN='your_raw_token_here' \
hardcover-discovery \
--library-root /path/to/calibre-library \
--export-shelfmark-releases \
--shelfmark-url http://shelfmark.local:8084 \
--shelfmark-source libgen \
--shelfmark-content-type ebook \
--dry-runExample dry-run queue/download:
HARDCOVER_TOKEN='your_raw_token_here' \
hardcover-discovery \
--library-root /path/to/calibre-library \
--push-shelfmark-download \
--shelfmark-url http://shelfmark.local:8084 \
--shelfmark-source libgen \
--shelfmark-content-type ebook \
--dry-runExample live queue/download with explicit source selection:
HARDCOVER_TOKEN='your_raw_token_here' \
hardcover-discovery \
--library-root /path/to/calibre-library \
--push-shelfmark-download \
--shelfmark-url http://shelfmark.local:8084 \
--shelfmark-source libgen \
--shelfmark-content-type ebook \
--shelfmark-selection bestExample ebook-only mode:
HARDCOVER_TOKEN='your_raw_token_here' \
hardcover-discovery \
--library-root /path/to/calibre-library \
--export-shelfmark-releases \
--shelfmark-url http://shelfmark.local:8084 \
--shelfmark-source libgen \
--shelfmark-content-type ebook \
--shelfmark-format-keywords epub,kepub,pdfExample most-seeders mode:
HARDCOVER_TOKEN='your_raw_token_here' \
hardcover-discovery \
--library-root /path/to/calibre-library \
--push-shelfmark-download \
--shelfmark-url http://shelfmark.local:8084 \
--shelfmark-source libgen \
--shelfmark-selection most_seeders \
--shelfmark-min-seeders 10Limitations:
- direct queue/download still depends on Shelfmark returning at least one concrete release with
sourceandsource_id - if all returned releases fail source/content-type/format/min-seeder checks, the row is exported and logged as skipped
- the request workflow and the release workflow are separate by design; request policy does not control direct release downloads
- the release workflow does not invent or guess a release when multiple results exist; it only applies the explicit operator-selected rule and logs why that rule chose the winner
apply is intentionally conservative.
Default behavior:
- consumes
audit/write_plan.csvonly - skips manual-review style rows by default
- applies only rows marked
safe_to_apply_boolean=True - updates Hardcover identifiers only
- updates
metadata.dbonly - does not write sidecar OPF or internal EPUB metadata unless you explicitly request it
- does not update Calibre title/author unless you explicitly ask for it
- uses a transaction and rolls back on failure
Recommended workflow:
- Run
auditand inspectaudit/actions.csv,audit/write_plan.csv, andaudit/summary.md. Treatactions_operator.csvas the short review queue. Treatactions.csvas the full forensic audit artifact. Treatwrite_plan.csvas the apply sheet, and filter it usingsafe_to_apply_boolean,safe_to_apply_reason, andaction_type. - Run
applywith--dry-runfirst. - If the dry-run looks correct, rerun without
--dry-run. - Only add
--include-calibre-title-authorwhen you explicitly want Calibre title/author rewrites as well as identifier updates. - Only add file-write flags after a dry-run and only when you want Calibre-sidecar or EPUB metadata to mirror the DB update.
Supported opt-in file-write targets:
- Calibre sidecar OPF files:
metadata.opf- or a single
.opffile in the book folder when that is the only available OPF target
- internal OPF metadata in EPUB-family files:
EPUBKEPUBOEBZIP
Unsupported formats are skipped explicitly and recorded in apply/apply_log.csv.
Safety model for file writes:
metadata.dbremains the primary source of truth- DB-only apply remains the default
- file writes are opt-in
- dry-run resolves and logs file targets without mutating files
- live file writes use best-effort backup/restore, but there is no single atomic transaction spanning both
metadata.dband multiple files - for real libraries, keep normal Calibre/library backups before enabling file-write modes
Example dry-run:
python3 -m hardcover_tools.cli.apply \
--library-root /path/to/calibre-library \
--write-plan /path/to/output/audit/write_plan.csv \
--dry-runExample safe apply with selected actions:
python3 -m hardcover_tools.cli.apply \
--library-root /path/to/calibre-library \
--write-plan /path/to/output/audit/write_plan.csv \
--apply-actions replace_hardcover_id,safe_auto_fix,update_calibre_metadataExample safe apply including Calibre title/author changes:
python3 -m hardcover_tools.cli.apply \
--library-root /path/to/calibre-library \
--write-plan /path/to/output/audit/write_plan.csv \
--apply-actions replace_hardcover_id,safe_auto_fix,update_calibre_metadata \
--include-calibre-title-authorConsole-script equivalents:
hardcover-apply \
--library-root /path/to/calibre-library \
--write-plan /path/to/output/audit/write_plan.csv \
--dry-runExample DB + sidecar OPF apply:
hardcover-apply \
--library-root /path/to/calibre-library \
--write-plan /path/to/output/audit/write_plan.csv \
--write-sidecar-opfExample DB + internal EPUB metadata apply:
hardcover-apply \
--library-root /path/to/calibre-library \
--write-plan /path/to/output/audit/write_plan.csv \
--write-epub-opfExample dry-run for file metadata writes:
hardcover-apply \
--library-root /path/to/calibre-library \
--write-plan /path/to/output/audit/write_plan.csv \
--write-ebook-metadata \
--dry-runExample files-only sidecar dry-run:
hardcover-apply \
--library-root /path/to/calibre-library \
--write-plan /path/to/output/audit/write_plan.csv \
--files-only \
--write-sidecar-opf \
--dry-runThe canonical identifier contract is fixed.
Write-back and internal canonical names are exactly:
hardcover-idhardcover-slughardcover-edition
Legacy aliases may still be accepted when reading older metadata, but new writes are normalized back to those canonical names.
Run the current parity-focused test set with:
pytest -qFor a more explicit runbook, see docs/operator_guide.md.