bindery-cli 0.27.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (70) hide show
  1. bindery_cli-0.27.0/.github/workflows/ci.yml +25 -0
  2. bindery_cli-0.27.0/.github/workflows/publish.yml +45 -0
  3. bindery_cli-0.27.0/.gitignore +12 -0
  4. bindery_cli-0.27.0/AGENTS.md +1 -0
  5. bindery_cli-0.27.0/CLAUDE.md +79 -0
  6. bindery_cli-0.27.0/LICENSE +21 -0
  7. bindery_cli-0.27.0/PKG-INFO +218 -0
  8. bindery_cli-0.27.0/README.md +202 -0
  9. bindery_cli-0.27.0/docs/screenshots/library-sweep.png +0 -0
  10. bindery_cli-0.27.0/logo.svg +15 -0
  11. bindery_cli-0.27.0/patchnotes.md +774 -0
  12. bindery_cli-0.27.0/pyproject.toml +47 -0
  13. bindery_cli-0.27.0/roadmap.md +572 -0
  14. bindery_cli-0.27.0/run_tests.sh +10 -0
  15. bindery_cli-0.27.0/scripts/FastSweep.java +40 -0
  16. bindery_cli-0.27.0/scripts/find_block_in_inline.py +44 -0
  17. bindery_cli-0.27.0/scripts/find_css_illegal_tags.py +64 -0
  18. bindery_cli-0.27.0/scripts/find_duplicate_playorder.py +50 -0
  19. bindery_cli-0.27.0/scripts/find_empty_body.py +44 -0
  20. bindery_cli-0.27.0/scripts/find_illegal_tags.py +169 -0
  21. bindery_cli-0.27.0/scripts/find_invalid_ids.py +53 -0
  22. bindery_cli-0.27.0/scripts/find_invalid_st_tag.py +43 -0
  23. bindery_cli-0.27.0/scripts/find_invalid_value_attr.py +46 -0
  24. bindery_cli-0.27.0/scripts/find_missing_images.py +81 -0
  25. bindery_cli-0.27.0/scripts/find_missing_title.py +46 -0
  26. bindery_cli-0.27.0/spec.md +276 -0
  27. bindery_cli-0.27.0/src/bindery/__init__.py +12 -0
  28. bindery_cli-0.27.0/src/bindery/__main__.py +4 -0
  29. bindery_cli-0.27.0/src/bindery/audit.py +2137 -0
  30. bindery_cli-0.27.0/src/bindery/cli.py +961 -0
  31. bindery_cli-0.27.0/src/bindery/epub.py +618 -0
  32. bindery_cli-0.27.0/src/bindery/library.py +244 -0
  33. bindery_cli-0.27.0/src/bindery/pagination.py +359 -0
  34. bindery_cli-0.27.0/src/bindery/reserialize.py +42 -0
  35. bindery_cli-0.27.0/src/bindery/transforms.py +579 -0
  36. bindery_cli-0.27.0/src/bindery/validate.py +254 -0
  37. bindery_cli-0.27.0/src/bindery/watermark.py +244 -0
  38. bindery_cli-0.27.0/test_facility/agda.epub +0 -0
  39. bindery_cli-0.27.0/test_facility/agda_fixed.epub +0 -0
  40. bindery_cli-0.27.0/test_facility/last_man_out.epub +0 -0
  41. bindery_cli-0.27.0/test_facility/last_man_out_fixed.epub +0 -0
  42. bindery_cli-0.27.0/test_facility/leaves_of_grass.epub +0 -0
  43. bindery_cli-0.27.0/test_facility/leaves_of_grass_fixed.epub +0 -0
  44. bindery_cli-0.27.0/test_facility/leaves_of_grass_st_fixed.epub +0 -0
  45. bindery_cli-0.27.0/test_facility/leaves_of_grass_tags_fixed.epub +0 -0
  46. bindery_cli-0.27.0/test_facility/muse_of_nightmares.epub +0 -0
  47. bindery_cli-0.27.0/test_facility/muse_of_nightmares_fixed.epub +0 -0
  48. bindery_cli-0.27.0/test_facility/poor_mans_fight.epub +0 -0
  49. bindery_cli-0.27.0/test_facility/poor_mans_fight_fixed.epub +0 -0
  50. bindery_cli-0.27.0/test_facility/test_block_in_inline.py +69 -0
  51. bindery_cli-0.27.0/test_facility/test_duplicate_playorder.py +63 -0
  52. bindery_cli-0.27.0/test_facility/test_empty_body.py +59 -0
  53. bindery_cli-0.27.0/test_facility/test_illegal_tags.py +64 -0
  54. bindery_cli-0.27.0/test_facility/test_invalid_ids.py +79 -0
  55. bindery_cli-0.27.0/test_facility/test_invalid_value.py +65 -0
  56. bindery_cli-0.27.0/test_facility/test_missing_title.py +80 -0
  57. bindery_cli-0.27.0/test_facility/unworthy_gods.epub +0 -0
  58. bindery_cli-0.27.0/test_facility/unworthy_gods_fixed.epub +0 -0
  59. bindery_cli-0.27.0/testing_facility/books_to_fix/.gitkeep +0 -0
  60. bindery_cli-0.27.0/tests/test_audit.py +1075 -0
  61. bindery_cli-0.27.0/tests/test_cli.py +702 -0
  62. bindery_cli-0.27.0/tests/test_epub.py +705 -0
  63. bindery_cli-0.27.0/tests/test_library.py +308 -0
  64. bindery_cli-0.27.0/tests/test_pagination.py +176 -0
  65. bindery_cli-0.27.0/tests/test_reserialize.py +53 -0
  66. bindery_cli-0.27.0/tests/test_transforms.py +442 -0
  67. bindery_cli-0.27.0/tests/test_validate.py +150 -0
  68. bindery_cli-0.27.0/tests/test_version.py +39 -0
  69. bindery_cli-0.27.0/tests/test_watermark.py +220 -0
  70. bindery_cli-0.27.0/uv.lock +89 -0
@@ -0,0 +1,25 @@
1
+ name: CI
2
+ on:
3
+ push:
4
+ branches: [main]
5
+ pull_request:
6
+ permissions:
7
+ contents: read
8
+ jobs:
9
+ test:
10
+ runs-on: ubuntu-latest
11
+ steps:
12
+ - uses: actions/checkout@v5
13
+ - uses: actions/setup-python@v5
14
+ with:
15
+ python-version: "3.14"
16
+ # html5lib is the project's declared optional `reserialize` extra (core stays
17
+ # stdlib-only); installing it here lets CI exercise the --reserialize tests
18
+ # instead of skipping them. ruff is a dev-only linter.
19
+ - run: pip install . html5lib ruff==0.16.2
20
+ # The rule set lives in pyproject.toml, so these mean the same thing locally.
21
+ # Checking `.` rather than named directories keeps a new top-level script from
22
+ # silently escaping lint.
23
+ - run: ruff check .
24
+ - run: ruff format --check .
25
+ - run: PYTHONPATH=src python -m unittest discover -s tests
@@ -0,0 +1,45 @@
1
+ name: Publish to PyPI
2
+
3
+ on:
4
+ push:
5
+ tags:
6
+ - "v*.*.*"
7
+
8
+ jobs:
9
+ test:
10
+ runs-on: ubuntu-latest
11
+ steps:
12
+ - uses: actions/checkout@v4
13
+ - uses: actions/setup-python@v5
14
+ with:
15
+ python-version: "3.14"
16
+
17
+ - name: Run test suite
18
+ run: |
19
+ pip install .[reserialize]
20
+ PYTHONPATH=src python -m unittest discover -s tests
21
+
22
+ build-and-publish:
23
+ name: Build distribution and publish to PyPI
24
+ needs: test
25
+ runs-on: ubuntu-latest
26
+ environment:
27
+ name: pypi
28
+ url: https://pypi.org/p/bindery-cli
29
+ permissions:
30
+ id-token: write # Required for OIDC authentication with PyPI
31
+
32
+ steps:
33
+ - uses: actions/checkout@v4
34
+ - uses: actions/setup-python@v5
35
+ with:
36
+ python-version: "3.14"
37
+
38
+ - name: Install build
39
+ run: python -m pip install --upgrade build
40
+
41
+ - name: Build sdist and wheel
42
+ run: python -m build
43
+
44
+ - name: Publish to PyPI
45
+ uses: pypa/gh-action-pypi-publish@release/v1
@@ -0,0 +1,12 @@
1
+ __pycache__/
2
+ *.py[cod]
3
+ *.egg-info/
4
+ build/
5
+ dist/
6
+ .venv/
7
+ .ruff_cache/
8
+ .pytest_cache/
9
+
10
+ # Ignore actual books used for testing to prevent IP leaks
11
+ testing_facility/books_to_fix/*
12
+ !testing_facility/books_to_fix/.gitkeep
@@ -0,0 +1 @@
1
+ CLAUDE.md
@@ -0,0 +1,79 @@
1
+ # CLAUDE.md (Bindery)
2
+
3
+ Per-project guidance. Overrides the global file where they conflict.
4
+
5
+ ## What this is
6
+
7
+ A focused EPUB repair and diagnostic tool: deterministic well-formedness fixes, gated by epubcheck,
8
+ with atomic in-place replacement in a Calibre library. Absorbed the retired oceanstrip at
9
+ v0.12.0 (2026-08-22) as the lossy `--strip-watermarks` flag; the standalone repo is gone from
10
+ the workspace (2026-08-26), and any instruction that runs `python -m oceanstrip` is dead.
11
+ Born from the 2026 library audit (see the user memory `calibre-library-epubcheck-audit`).
12
+
13
+ ## Hard constraints
14
+
15
+ - **Minimal Dependencies.** Runtime deps are exactly: `tqdm` (progress/output),
16
+ `vir-tui` (shared TUI rendering) and `cquarry` (read-only Calibre `metadata.db` access,
17
+ adopted in v0.16.0). `vir-tui` is PINNED to an exact commit in pyproject.toml
18
+ (bump deliberately); `cquarry` deliberately tracks `@main` since v0.19.0 so CI always
19
+ exercises the current library. `html5lib` remains the one approved
20
+ heavy-parsing exception (used only for the `--reserialize` fix, imported lazily so every
21
+ other mode runs without it). Tests use the standard `unittest` framework. epubcheck is an
22
+ external CLI dependency, and `calibredb` is required for the `--install-to-calibre`
23
+ feature. Before adding any further Python package, stop and ask.
24
+ - **Semantics-preserving transforms by default, everything else fenced behind a flag.**
25
+ The always-on core is exactly five well-formedness fixes (prolog junk, duplicate
26
+ `xmlns`, bare `&`, named entities, void self-closing) plus the NCX pipeline; every core
27
+ fix must render identically to the author's intent: never add, remove, or reorder
28
+ visible content. Two kinds of exceptions exist, both opt-in and off unless requested:
29
+ * **Structural repairs** (`--fix-empty-body`, `--fix-missing-title`, `--fix-id-colons`,
30
+ `--fix-page-map`, `--strip-epub3-attrs`, `--downgrade-epub3-tags`,
31
+ `--unwrap-block-in-inline`, `--strip-invalid-value`, `--unwrap-illegal-tags`;
32
+ transforms.py, threaded through epub.py): they alter markup structure or fabricate
33
+ minimal content. v0.14–v0.16 ran these unconditionally, which broke this rule;
34
+ v0.17.0 restored it. `--unwrap-illegal-tags` additionally protects any illegal-tag
35
+ name that an EPUB stylesheet styles as an element selector (`css_protected_tags`,
36
+ book-wide, inline `<style>` blocks included).
37
+ * **Lossy strips** (`--strip-pagination`, `--strip-broken-tags`, `--strip-watermarks`;
38
+ pagination.py, watermark.py): remove only what a converter injected (page numbers,
39
+ running headers, leaked tags, watermarks), fenced behind character conservation, tag
40
+ balance, and the epubcheck no-regression bar.
41
+ Do not let any NEW fix touch content without its own flag; if a candidate repair cannot
42
+ be made deterministically safe, it does not belong here — report it for manual repair
43
+ instead.
44
+ - **The gate is the safety contract.** Never apply a repair epubcheck has not accepted.
45
+ Respect the two-mode logic in `validate.gate` (fatal-fixing tolerates error unmasking;
46
+ error-cleanup does not). The lossy strips (`--strip-pagination`, `--strip-broken-tags`,
47
+ `--strip-watermarks`) are accepted by `validate.no_worse` instead (their gain is
48
+ invisible to epubcheck, so it only forbids a regression, never demands a measured
49
+ improvement). The structural opt-ins go through the normal `gate`: their gain IS
50
+ visible (they clear errors), so a run with no measurable improvement is a noop and
51
+ nothing is applied. Changing either bar means re-running the library dry run.
52
+ - **Library writes are sacred.** Replacement must stay atomic (temp in same dir, then
53
+ `os.replace`), touch only the `.epub`, preserve mode, and be dry-run by default. Calibre integrations should seamlessly swap formats using `calibredb add_format -replace`.
54
+ Never write to the library without `-apply`. Test every change on `/tmp` copies first.
55
+
56
+ ## Layout
57
+
58
+ - `src/bindery/transforms.py`: pure `str -> (str, int)` text transforms (including `strip_broken_tags`).
59
+ - `src/bindery/pagination.py`: the opt-in lossy page-number strip (runhead detection, page-layer decision, block-centric removal/merge, safety nets).
60
+ - `src/bindery/watermark.py`: the opt-in lossy watermark strip (anchored and anchorless signature removal).
61
+ - `src/bindery/reserialize.py`: structural repair via `html5lib`.
62
+ - `src/bindery/audit.py`: read-only body-text audits (`content`, `pagenumbers`, `emptytext`, `ocr`, and `monolithic` since v0.21.0, `--max-doc-chars N`) behind the `audit` subcommand (v0.15.0). Since v0.18.0 library mode resolves EPUB paths through `cquarry.get_format_path()` and can apply a tag to flagged books via `cquarry.write.WritableCalibreDB` (only with explicit `--tag`; the only sanctioned write path). Since v0.19.0 `audit --id BOOK_ID` audits a single library book through cquarry's single-entity `get_book()` fetch (no library-wide cache; supports `--tag`; incompatible with the directory argument; comma lists since v0.23.0). Since v0.22.0 every archive entry is fully read (CRC + decompression) before analysis — a damaged entry reports its own CORRUPT verdict instead of feeding emptytext — and manifest/NCX references to absent files classify as `convention` (bloated ToC, consecutive span) or `fragment` (broken span). Hazard: the scan loop reuses `tag` as its per-book display column, so the `--tag` argument is captured as `audit_tag` at function entry — do not collapse them again.
63
+ - `src/bindery/epub.py`: archive rewrite, NCX uid sync, RepairReport, mismatch detection, and the opt-in structural-repair plumbing (including the CSS precondition scan).
64
+ - `src/bindery/validate.py`: epubcheck wrapper, the `gate` (improvement) and `no_worse` (no-regression, for the lossy strips) acceptance bars.
65
+ - `src/bindery/library.py`: Calibre walk, atomic replace, backups, and native format installation. Since v0.19.0 `CalibreIdResolver` resolves the book id from `metadata.db` through cquarry (one lazy path→id map per run; since v0.20.0 the map comes from `CalibreDB.format_path_index()`, re-normalized with `resolve().lower()` for the resolver's symlinked-directory and case-insensitive matching) — the `(id)` directory-name regex is only the no-catalog fallback. Since v0.24.0 `install_format()` places the repaired file atomically and updates the `data` row through `cquarry.write.WritableCalibreDB` (remove+add in one `batch()`; `add_format` refuses duplicates); the external `calibredb` CLI is gone. Never reintroduce directory-name guessing as the primary source: renamed/mismatched directories would replace the wrong book.
66
+ - `src/bindery/cli.py`: `repair` and `library` subcommands, including `--all` and `--install-to-calibre`.
67
+ - `tests/`: transforms, end-to-end repair, atomic replace, pagination, watermarks, the audit analyzers (content/pagenumbers/emptytext/ocr/monolithic, archive integrity, spine classification), the library sweep and `CalibreIdResolver`, validate, reserialize, CLI wiring, and the version pin.
68
+
69
+ ## Conventions
70
+
71
+ - Type hints, `from __future__ import annotations`, ruff for lint and format.
72
+ - `VERSION` lives in `src/bindery/__init__.py`, mirrored in `pyproject.toml`. Bump both.
73
+ - Run tests with `./run_tests.sh`.
74
+
75
+ ## Validation workflow
76
+
77
+ The library is real data. The loop is always: dry run on `/tmp` copies, inspect the
78
+ report, then apply with backups. epubcheck is the oracle; a repaired book that still has
79
+ fatals is `partial` and must be left for manual work, never auto-applied.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 vrnvctss
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,218 @@
1
+ Metadata-Version: 2.5
2
+ Name: bindery-cli
3
+ Version: 0.27.0
4
+ Summary: High-speed EPUB repair surgeon with a persistent Java validation daemon, safe HTML unwrapping, and native Calibre integration.
5
+ Author: vrnvctss
6
+ License: MIT
7
+ License-File: LICENSE
8
+ Keywords: calibre,ebook,epub,epubcheck,html-repair,repair,watermarks
9
+ Requires-Python: >=3.14
10
+ Requires-Dist: cquarry>=1.9.0
11
+ Requires-Dist: tqdm
12
+ Requires-Dist: vir-tui>=2.2.0
13
+ Provides-Extra: reserialize
14
+ Requires-Dist: html5lib>=1.1; extra == 'reserialize'
15
+ Description-Content-Type: text/markdown
16
+
17
+ <div align="center">
18
+ <img src="logo.svg" width="96" height="96" alt="Bindery logo"/>
19
+ <h1>Bindery</h1>
20
+ <p>Audit EPUB body text and repair broken EPUBs with safe, deterministic fixes, validated by epubcheck, with optional in-place replacement in a Calibre library.</p>
21
+ </div>
22
+
23
+ <p align="center">
24
+ <img src="docs/screenshots/library-sweep.png" alt="Bindery's dry-run library sweep: per-book epubcheck results and a summary table ending in 'no files written'">
25
+ </p>
26
+
27
+ ## What it fixes
28
+
29
+ Bindery makes accidentally broken markup well-formed again. It does not rewrite or reflow content. The **default pass** applies only a small set of deterministic, semantics-preserving well-formedness fixes that real-world EPUBs (especially Calibre conversions) trip over — nothing else changes without an explicit flag:
30
+
31
+ - **Unclosed void elements** (`<link>`, `<br>`, `<img>`, ...) get self-closed.
32
+ - **Undeclared named entities** (`&nbsp;`, `&deg;`, `&eacute;`, ...) become numeric character references that every XML parser understands.
33
+ - **Bare `&`** (common in `toc.ncx`) is escaped to `&amp;`.
34
+ - **Junk before the XML prolog** (BOM, stray bytes) is stripped.
35
+ - **Duplicate `xmlns`** on the root `<html>` is collapsed to one.
36
+ - **NCX-001**: `toc.ncx` `dtb:uid` is synced to the OPF unique identifier; duplicated NCX `playOrder` values are resequenced whenever the NCX is processed (part of the always-on NCX pipeline, not a flag).
37
+ - **mimetype** is rewritten first and stored, fixing the common ordering defect; a missing entry is added and wrong or whitespace-padded content is normalized to the OCF constant.
38
+
39
+ ### Opt-in repairs
40
+
41
+ Everything below is off until its flag is passed (or all at once via `--all`). They go further than well-formedness — altering structure or adding minimal content — so they never run by default:
42
+
43
+ - **`--fix-ids`**: rewrite ids that are not valid XML names (start with a digit, contain a colon) in the OPF manifest, updating every reference to them (spine, fallback, media-overlay, the EPUB 2 cover meta), and in the NCX (where old conversions stamp navPoint ids from UUIDs). Touches the OPF, so it is off by default; the dc: metadata is never altered.
44
+ - **`--add-img-alt`**: add `alt=""` to `<img>` elements missing the required attribute. Renders identically, but it adds markup the author never wrote, and an empty alt tells a screen reader the image is decorative; hence opt-in.
45
+ - **`--reserialize`**: rebuild content documents that are still malformed by re-parsing them with html5lib and re-emitting XHTML, closing unclosed `<p>`/`<div>`/`<span>`/`<blockquote>` that the regex transforms cannot. Runs only on documents that are not already well-formed, so good files are untouched.
46
+ - **`--strip-bad-attrs`**: drop attributes that are invalid XML (a name starting with a digit, or a namespaced name whose prefix is never declared, like Office VML `v:shapes`). Surgical and a no-op on well-formed files.
47
+ - **`--escape-unknown-entities`**: escape entity names that are not in the HTML5 table (`&foo;` becomes `&amp;foo;`), which renders exactly as browsers already render an unknown entity. Documents whose DOCTYPE carries an internal subset are skipped wholesale, since a subset can declare custom entities.
48
+ - **`--fix-id-colons`**: translates illegal colons in `id="X:Y"` and their matching `#X:Y` fragment references to valid underscores.
49
+ - **`--fix-empty-body`**: appends a non-breaking space `&nbsp;` to strictly empty body tags to satisfy parser requirements.
50
+ - **`--fix-missing-title`**: injects a `<title>Unknown</title>` fallback in the `<head>` if missing, handling both empty `<title/>` self-closing tags and entirely absent tags.
51
+ - **`--unwrap-block-in-inline`**: safely unwraps `<span>` tags that illegally contain a block-level element (e.g. `<div>` or `<p>`), leaving the block element intact.
52
+ - **`--strip-invalid-value`**: systematically strips invalid `value="..."` attributes from elements like `<div>`, `<span>`, `<p>`, etc.
53
+ - **`--fix-page-map`**: normalizes legacy page-map markup — drops the non-standard `page-map="..."` attribute from the OPF `<spine>` and adds `class="pages"` to classless NCX `<pageList>` elements (epubcheck rejects both on older HarperCollins / Anna's Archive conversions).
54
+ - **`--strip-epub3-attrs`**: scrubs the EPUB3-only attributes epubcheck rejects on an EPUB2 package — `page-progression-direction`, `epub:type`, `aria-label` (a fixed, documented set; rendering is unchanged, and lookalikes like `type` or the wider aria family survive).
55
+ - **`--downgrade-epub3-tags`**: downgrades EPUB3/HTML5 semantic elements to their EPUB2 equivalents — `figure`/`section` to `div`, `figcaption` to `p` — keeping existing classes and appending the semantic name (`class="figure"`) as the styling hook. Tag names a stylesheet styles as an element selector are protected book-wide, so styled formatting is never destroyed.
56
+ - **`--unwrap-illegal-tags`**: strips completely invalid or deprecated HTML tags that break EPUB3 validation (`<st>`, `<sentence>`, `<o>`, `<w>`, `<pagebreak>`) while retaining their inner text. Any of those names styled as an *element selector* by an EPUB stylesheet (`.css` entries and inline `<style>` blocks alike; class/id selectors like `.st`/`#w` don't count) is protected for the whole book, guaranteeing format preservation.
57
+
58
+
59
+ Three opt-in fixes are **lossy** and stand apart from the semantics-preserving rest:
60
+
61
+ - **`--strip-pagination`**: remove print page numbers and running headers that a PDF/OCR conversion baked into the body text as literal paragraphs (so they reflow into the middle of a sentence: "where the hay cart **16** was taking him"). It removes only that injected furniture, never the author's prose: where a number split a sentence it rejoins the two paragraphs (closing up a word split like `compli-` / `mentary`), and it preserves roman chapter numbers, page-list nav anchors, and years. A book is only treated as paginated when it has both a dense run of arabic numbers and several confident mid-sentence interrupts, so a merely chapter-numbered book is left alone. Three safety nets guard every edit (character conservation, tag balance, and an epubcheck no-regression check); any failure leaves the document untouched.
62
+ - **`--strip-broken-tags`**: remove leaked HTML closing tags missing their open brackets (e.g. `</p>`) that render as raw text.
63
+ - **`--strip-watermarks`**: remove known producer and distributor watermarks (e.g. OceanofPDF, ABC Amber LIT Converter) and stray marker files. It locates the stamp and deletes the outermost wrapper whose entire visible text is the watermark, ensuring prose that merely mentions the URL is preserved.
64
+
65
+ All lossy edits are invisible to epubcheck, so they are accepted when the result is *no worse* rather than measurably better.
66
+
67
+ ## The safety contract
68
+
69
+ Every repair is gated by [epubcheck](https://github.com/w3c/epubcheck). The acceptance rule is two-mode, because a fatal parse error makes epubcheck stop reading a file and hides every downstream error:
70
+
71
+ - If a book **had fatals**, success means **fewer fatals**. The error count may rise as previously-hidden schema warnings become visible once the file parses; that is the book going from "won't open" to "opens with nits," not a regression.
72
+ - If a book had **no fatals**, an error increase is a real regression, so a strict error decrease is required.
73
+
74
+ Introducing a net-new fatal is always rejected. If epubcheck itself fails to run (crash, timeout, unparsable output), the book is reported as an error and never applied; only an explicit `--no-validate` skips the gate. Originals are never modified except by an explicit, atomic in-place replace (see below), and even then only after the gate accepts the result.
75
+
76
+ The lossy modes (`--strip-pagination`, `--strip-broken-tags`, and `--strip-watermarks`) are the exception to the "must improve" rule. Since they remove visible markup rather than correcting XML schema violations, epubcheck counts often remain unchanged. They are accepted when the result is **no worse** (no net-new fatals or errors), relying on strict programmatic safety nets instead.
77
+
78
+ ## Install
79
+
80
+ Python 3.14+, plus epubcheck on `PATH` for the gate. Dependencies: `tqdm` for progress output, plus the pinned VirInvictus libraries `vir-tui` (TUI rendering) and `cquarry` (read-only Calibre database access); `html5lib` remains an optional extra, needed only for `--reserialize`.
81
+
82
+ ```sh
83
+ uv tool install bindery-cli # core tools
84
+ uv tool install "bindery-cli[reserialize]" # incl. --reserialize
85
+ ```
86
+
87
+ ## Usage
88
+
89
+ Three verbs: `bindery repair` fixes a single EPUB epubcheck-gated, `bindery audit`
90
+ reports content flaws without touching anything, and `bindery library` sweeps a Calibre
91
+ library tree (dry run by default; `--apply` replaces accepted books atomically in place).
92
+ The sections below take each in turn.
93
+
94
+ ## Auditing
95
+
96
+ Bindery includes a comprehensive auditing tool to inspect EPUB body text for non-schema flaws that epubcheck cannot catch. It extracts and analyzes the visible text to detect content issues, producing console reports that can be used to filter your library or feed into `bindery repair`.
97
+
98
+ Scan a Calibre library for specific issues:
99
+
100
+ ```sh
101
+ # Detect non-English content (e.g., Cyrillic or CJK in an English library)
102
+ cd ~/docs/Calibre\ Library && bindery audit content
103
+
104
+ # Find books with hardcoded print page numbers interrupting the text
105
+ bindery audit pagenumbers ~/docs/Calibre\ Library
106
+
107
+ # Find books that are empty or severely truncated
108
+ bindery audit emptytext ~/docs/Calibre\ Library
109
+
110
+ # Find books with severe OCR damage (garbage characters, excessive hyphenation)
111
+ bindery audit ocr ~/docs/Calibre\ Library
112
+
113
+ # Flag books where a single content document is too large for readers to render
114
+ bindery audit monolithic ~/docs/Calibre\ Library
115
+
116
+ # Run all audits and generate a comprehensive CSV
117
+ cd ~/docs/Calibre\ Library && bindery audit all
118
+ ```
119
+
120
+ Audits can also be run on a directory of loose `.epub` files by passing the path as the second argument:
121
+ ```sh
122
+ bindery audit pagenumbers /path/to/loose/epubs
123
+ ```
124
+
125
+ Or on one or more library books by Calibre id (comma-separated) — fetched through cquarry's single-entity `get_book()` (no library-wide scan), with the EPUB resolved via cquarry's own path logic:
126
+ ```sh
127
+ cd ~/docs/Calibre\ Library && bindery audit all --id 1234,1235
128
+ ```
129
+ `--id` composes with `--tag`: the book is tagged only if the audit flags it.
130
+
131
+ **Spine-integrity reporting:** Both `audit` and `library` reports now classify manifest/NCX references that point to absent files. A `convention` verdict means the ToC is bloated but the present documents form a consecutive chapter span (e.g., the Wandering Inn official-build pattern, safe). A `fragment` verdict means the span itself is broken.
132
+
133
+ **Archive integrity:** every audit fully reads each archive entry (CRC + decompression), so a damaged download is reported CORRUPT — with the first broken entry named — instead of being mislabeled EMPTY by `emptytext`. `library --sweep` splits its `unreadable` bucket into `not_a_zip` / `truncated` / `encrypted` / `corrupt_entry`, so the right disease is visible without leaving the sweep.
134
+
135
+ ### Tagging flagged books (opt-in)
136
+
137
+ By default an audit writes nothing. With `--tag TAG` (library mode only), every book the audit
138
+ flags is tagged in `metadata.db` through cquarry's trigger-safe write module — useful for piping
139
+ flagged books back into Calibre views:
140
+
141
+ ```sh
142
+ cd ~/docs/Calibre\ Library && bindery audit content --tag "Audit Flagged"
143
+ ```
144
+
145
+ The audit itself stays read-only; only the final tagging pass writes, it skips books that already
146
+ carry the tag, and THIN emptytext advisories are never tagged. Close Calibre first so the write
147
+ does not fight its lock. Tagged books are recorded in Calibre's `metadata_dirtied` queue, so the
148
+ desktop app regenerates their sidecar `.opf`s (and re-pushes metadata to wireless readers) on its
149
+ next startup — no manual resync needed.
150
+
151
+
152
+ Repair one book to a new file (gated; writes only if it is an improvement):
153
+
154
+ ```sh
155
+ bindery repair broken.epub # -> "broken (repaired).epub"
156
+ bindery repair broken.epub fixed.epub
157
+ bindery repair scanned.epub --strip-pagination # also remove baked-in page numbers
158
+ ```
159
+
160
+ Scan a Calibre library and see what would be fixed, writing nothing:
161
+
162
+ ```sh
163
+ bindery library ~/docs/Calibre\ Library --only fatals --audit epub_audit.csv
164
+ ```
165
+
166
+ Apply accepted repairs in place, atomically, with backups:
167
+
168
+ ```sh
169
+ bindery library ~/docs/Calibre\ Library --only fatals --apply --backup ~/bindery-backups
170
+ ```
171
+
172
+ Apply all safe and lossy repairs directly into the Calibre database natively:
173
+
174
+ ```sh
175
+ bindery library ~/docs/Calibre\ Library --only all --apply --all --install-to-calibre
176
+ ```
177
+
178
+ - `--only {fatals,ncx,all}` restricts the candidate set. `ncx` targets NCX-001 mismatches (detected without epubcheck); `fatals` needs `--audit`.
179
+ - `--id <ids>` limits the sweep to a comma-separated list of Calibre book IDs, skipping the full library walk.
180
+ - `--audit CSV` (the `fatals,errors,warnings,path` format produced by an epubcheck sweep) skips clean books so a run is fast. Paths are resolved on both sides, and a CSV that matches nothing triggers a loud warning instead of silently selecting zero books.
181
+ - `--sweep` replaces the CSV step entirely: it runs a live epubcheck sweep for candidate selection and reuses each result as that book's before-measurement, so no book is checked twice. Bindery launches a transparent, persistent Java daemon (`EpubcheckDaemon`) to evaluate candidates in the background, eliminating the JVM startup penalty. This reduces validation time from ~5 seconds per book down to ~0.05 seconds, allowing `--sweep` to scan a 5,000-book library in just 10 minutes. Combine with `--only fatals` for a self-contained, lightning-fast "find and fix the broken books" run.
182
+ - `--json FILE` writes a machine-readable report of the whole run (per-book status, before/after counts, applied flag, summary totals). `--manual-list FILE` writes the paths of every book that was not auto-repaired, one per line, ready for manual follow-up.
183
+ - `--apply` is required to write; the default is a dry run. `--backup DIR` mirrors originals before replacing; `--backup-inplace` writes `.epub.bak` beside each file.
184
+ - `--install-to-calibre` resolves the Calibre book id from `metadata.db` via cquarry (one read-only path→id map per run), so a hand-renamed `Author/Title (id)/` directory can never send the repaired file to the wrong book. The directory-name regex is only a no-catalog fallback; with neither, the file is saved atomically in place.
185
+ - `--install-to-calibre` installs the repaired EPUB through cquarry's write module: the file is placed atomically at the catalogued path (same filename, Calibre's layout never changes) and the format row is re-registered in one transaction, so the stored size stays truthful and the book lands in `metadata_dirtied` — Calibre regenerates its sidecar .opf on next start. No external `calibredb` call is involved. It falls back to atomic file replacement if the Calibre database ID cannot be resolved, and a database failure degrades to the in-place save with a warning.
186
+ - `--all` automatically turns on all opt-in non-fatal fixes and lossy strips (pagination, watermarks, bad attributes, unknown entities, image alt tags, etc.) in a single run.
187
+ - Only the `.epub` is replaced. `metadata.opf`, `cover.jpg`, and `metadata.db` are left for Calibre's Quality Check sync to reconcile.
188
+ - A per-book progress line goes to stderr (stdout stays a clean report); `--quiet` suppresses it. A corrupt or unreadable book is reported and skipped, never aborting the sweep.
189
+ - Exit codes: 0 for a clean sweep, 1 for a usage error, 2 when any book was rejected, unreadable, or failed epubcheck (for scripts and cron).
190
+ - `repair` refuses to overwrite an existing output file unless `--force` is given.
191
+
192
+ ## Companion scripts
193
+
194
+ `scripts/` holds standalone, read-only utilities that are useful for EPUB maintenance but fall outside Bindery's repair contract (fixing what they find would be a content change, which Bindery makes only via the opt-in `--strip-pagination`):
195
+
196
+ - `find_missing_images.py`: scans a library tree and reports every book whose `<img>` tags point at files that do not exist inside the archive (a common defect in converted EPUBs). Reads the archives in place; nothing is unpacked or written. The library path is set at the bottom of the script.
197
+
198
+ ## Development
199
+
200
+ ```sh
201
+ ./run_tests.sh # unittest suite
202
+ ```
203
+
204
+ See [spec.md](spec.md) for the full contract and [roadmap.md](roadmap.md) for what is planned.
205
+
206
+ ## License
207
+
208
+ MIT. See [LICENSE](LICENSE).
209
+
210
+ ## Support
211
+
212
+ If Bindery's useful to you and you'd like to chip in:
213
+
214
+ - liberapay · [liberapay.com/bdkl](https://liberapay.com/bdkl/)
215
+ - bitcoin
216
+ ```
217
+ bc1qkge6zr45tzqfwfmvma2ylumt6mg7wlwmhr05yv
218
+ ```
@@ -0,0 +1,202 @@
1
+ <div align="center">
2
+ <img src="logo.svg" width="96" height="96" alt="Bindery logo"/>
3
+ <h1>Bindery</h1>
4
+ <p>Audit EPUB body text and repair broken EPUBs with safe, deterministic fixes, validated by epubcheck, with optional in-place replacement in a Calibre library.</p>
5
+ </div>
6
+
7
+ <p align="center">
8
+ <img src="docs/screenshots/library-sweep.png" alt="Bindery's dry-run library sweep: per-book epubcheck results and a summary table ending in 'no files written'">
9
+ </p>
10
+
11
+ ## What it fixes
12
+
13
+ Bindery makes accidentally broken markup well-formed again. It does not rewrite or reflow content. The **default pass** applies only a small set of deterministic, semantics-preserving well-formedness fixes that real-world EPUBs (especially Calibre conversions) trip over — nothing else changes without an explicit flag:
14
+
15
+ - **Unclosed void elements** (`<link>`, `<br>`, `<img>`, ...) get self-closed.
16
+ - **Undeclared named entities** (`&nbsp;`, `&deg;`, `&eacute;`, ...) become numeric character references that every XML parser understands.
17
+ - **Bare `&`** (common in `toc.ncx`) is escaped to `&amp;`.
18
+ - **Junk before the XML prolog** (BOM, stray bytes) is stripped.
19
+ - **Duplicate `xmlns`** on the root `<html>` is collapsed to one.
20
+ - **NCX-001**: `toc.ncx` `dtb:uid` is synced to the OPF unique identifier; duplicated NCX `playOrder` values are resequenced whenever the NCX is processed (part of the always-on NCX pipeline, not a flag).
21
+ - **mimetype** is rewritten first and stored, fixing the common ordering defect; a missing entry is added and wrong or whitespace-padded content is normalized to the OCF constant.
22
+
23
+ ### Opt-in repairs
24
+
25
+ Everything below is off until its flag is passed (or all at once via `--all`). They go further than well-formedness — altering structure or adding minimal content — so they never run by default:
26
+
27
+ - **`--fix-ids`**: rewrite ids that are not valid XML names (start with a digit, contain a colon) in the OPF manifest, updating every reference to them (spine, fallback, media-overlay, the EPUB 2 cover meta), and in the NCX (where old conversions stamp navPoint ids from UUIDs). Touches the OPF, so it is off by default; the dc: metadata is never altered.
28
+ - **`--add-img-alt`**: add `alt=""` to `<img>` elements missing the required attribute. Renders identically, but it adds markup the author never wrote, and an empty alt tells a screen reader the image is decorative; hence opt-in.
29
+ - **`--reserialize`**: rebuild content documents that are still malformed by re-parsing them with html5lib and re-emitting XHTML, closing unclosed `<p>`/`<div>`/`<span>`/`<blockquote>` that the regex transforms cannot. Runs only on documents that are not already well-formed, so good files are untouched.
30
+ - **`--strip-bad-attrs`**: drop attributes that are invalid XML (a name starting with a digit, or a namespaced name whose prefix is never declared, like Office VML `v:shapes`). Surgical and a no-op on well-formed files.
31
+ - **`--escape-unknown-entities`**: escape entity names that are not in the HTML5 table (`&foo;` becomes `&amp;foo;`), which renders exactly as browsers already render an unknown entity. Documents whose DOCTYPE carries an internal subset are skipped wholesale, since a subset can declare custom entities.
32
+ - **`--fix-id-colons`**: translates illegal colons in `id="X:Y"` and their matching `#X:Y` fragment references to valid underscores.
33
+ - **`--fix-empty-body`**: appends a non-breaking space `&nbsp;` to strictly empty body tags to satisfy parser requirements.
34
+ - **`--fix-missing-title`**: injects a `<title>Unknown</title>` fallback in the `<head>` if missing, handling both empty `<title/>` self-closing tags and entirely absent tags.
35
+ - **`--unwrap-block-in-inline`**: safely unwraps `<span>` tags that illegally contain a block-level element (e.g. `<div>` or `<p>`), leaving the block element intact.
36
+ - **`--strip-invalid-value`**: systematically strips invalid `value="..."` attributes from elements like `<div>`, `<span>`, `<p>`, etc.
37
+ - **`--fix-page-map`**: normalizes legacy page-map markup — drops the non-standard `page-map="..."` attribute from the OPF `<spine>` and adds `class="pages"` to classless NCX `<pageList>` elements (epubcheck rejects both on older HarperCollins / Anna's Archive conversions).
38
+ - **`--strip-epub3-attrs`**: scrubs the EPUB3-only attributes epubcheck rejects on an EPUB2 package — `page-progression-direction`, `epub:type`, `aria-label` (a fixed, documented set; rendering is unchanged, and lookalikes like `type` or the wider aria family survive).
39
+ - **`--downgrade-epub3-tags`**: downgrades EPUB3/HTML5 semantic elements to their EPUB2 equivalents — `figure`/`section` to `div`, `figcaption` to `p` — keeping existing classes and appending the semantic name (`class="figure"`) as the styling hook. Tag names a stylesheet styles as an element selector are protected book-wide, so styled formatting is never destroyed.
40
+ - **`--unwrap-illegal-tags`**: strips completely invalid or deprecated HTML tags that break EPUB3 validation (`<st>`, `<sentence>`, `<o>`, `<w>`, `<pagebreak>`) while retaining their inner text. Any of those names styled as an *element selector* by an EPUB stylesheet (`.css` entries and inline `<style>` blocks alike; class/id selectors like `.st`/`#w` don't count) is protected for the whole book, guaranteeing format preservation.
41
+
42
+
43
+ Three opt-in fixes are **lossy** and stand apart from the semantics-preserving rest:
44
+
45
+ - **`--strip-pagination`**: remove print page numbers and running headers that a PDF/OCR conversion baked into the body text as literal paragraphs (so they reflow into the middle of a sentence: "where the hay cart **16** was taking him"). It removes only that injected furniture, never the author's prose: where a number split a sentence it rejoins the two paragraphs (closing up a word split like `compli-` / `mentary`), and it preserves roman chapter numbers, page-list nav anchors, and years. A book is only treated as paginated when it has both a dense run of arabic numbers and several confident mid-sentence interrupts, so a merely chapter-numbered book is left alone. Three safety nets guard every edit (character conservation, tag balance, and an epubcheck no-regression check); any failure leaves the document untouched.
46
+ - **`--strip-broken-tags`**: remove leaked HTML closing tags missing their open brackets (e.g. `</p>`) that render as raw text.
47
+ - **`--strip-watermarks`**: remove known producer and distributor watermarks (e.g. OceanofPDF, ABC Amber LIT Converter) and stray marker files. It locates the stamp and deletes the outermost wrapper whose entire visible text is the watermark, ensuring prose that merely mentions the URL is preserved.
48
+
49
+ All lossy edits are invisible to epubcheck, so they are accepted when the result is *no worse* rather than measurably better.
50
+
51
+ ## The safety contract
52
+
53
+ Every repair is gated by [epubcheck](https://github.com/w3c/epubcheck). The acceptance rule is two-mode, because a fatal parse error makes epubcheck stop reading a file and hides every downstream error:
54
+
55
+ - If a book **had fatals**, success means **fewer fatals**. The error count may rise as previously-hidden schema warnings become visible once the file parses; that is the book going from "won't open" to "opens with nits," not a regression.
56
+ - If a book had **no fatals**, an error increase is a real regression, so a strict error decrease is required.
57
+
58
+ Introducing a net-new fatal is always rejected. If epubcheck itself fails to run (crash, timeout, unparsable output), the book is reported as an error and never applied; only an explicit `--no-validate` skips the gate. Originals are never modified except by an explicit, atomic in-place replace (see below), and even then only after the gate accepts the result.
59
+
60
+ The lossy modes (`--strip-pagination`, `--strip-broken-tags`, and `--strip-watermarks`) are the exception to the "must improve" rule. Since they remove visible markup rather than correcting XML schema violations, epubcheck counts often remain unchanged. They are accepted when the result is **no worse** (no net-new fatals or errors), relying on strict programmatic safety nets instead.
61
+
62
+ ## Install
63
+
64
+ Python 3.14+, plus epubcheck on `PATH` for the gate. Dependencies: `tqdm` for progress output, plus the pinned VirInvictus libraries `vir-tui` (TUI rendering) and `cquarry` (read-only Calibre database access); `html5lib` remains an optional extra, needed only for `--reserialize`.
65
+
66
+ ```sh
67
+ uv tool install bindery-cli # core tools
68
+ uv tool install "bindery-cli[reserialize]" # incl. --reserialize
69
+ ```
70
+
71
+ ## Usage
72
+
73
+ Three verbs: `bindery repair` fixes a single EPUB epubcheck-gated, `bindery audit`
74
+ reports content flaws without touching anything, and `bindery library` sweeps a Calibre
75
+ library tree (dry run by default; `--apply` replaces accepted books atomically in place).
76
+ The sections below take each in turn.
77
+
78
+ ## Auditing
79
+
80
+ Bindery includes a comprehensive auditing tool to inspect EPUB body text for non-schema flaws that epubcheck cannot catch. It extracts and analyzes the visible text to detect content issues, producing console reports that can be used to filter your library or feed into `bindery repair`.
81
+
82
+ Scan a Calibre library for specific issues:
83
+
84
+ ```sh
85
+ # Detect non-English content (e.g., Cyrillic or CJK in an English library)
86
+ cd ~/docs/Calibre\ Library && bindery audit content
87
+
88
+ # Find books with hardcoded print page numbers interrupting the text
89
+ bindery audit pagenumbers ~/docs/Calibre\ Library
90
+
91
+ # Find books that are empty or severely truncated
92
+ bindery audit emptytext ~/docs/Calibre\ Library
93
+
94
+ # Find books with severe OCR damage (garbage characters, excessive hyphenation)
95
+ bindery audit ocr ~/docs/Calibre\ Library
96
+
97
+ # Flag books where a single content document is too large for readers to render
98
+ bindery audit monolithic ~/docs/Calibre\ Library
99
+
100
+ # Run all audits and generate a comprehensive CSV
101
+ cd ~/docs/Calibre\ Library && bindery audit all
102
+ ```
103
+
104
+ Audits can also be run on a directory of loose `.epub` files by passing the path as the second argument:
105
+ ```sh
106
+ bindery audit pagenumbers /path/to/loose/epubs
107
+ ```
108
+
109
+ Or on one or more library books by Calibre id (comma-separated) — fetched through cquarry's single-entity `get_book()` (no library-wide scan), with the EPUB resolved via cquarry's own path logic:
110
+ ```sh
111
+ cd ~/docs/Calibre\ Library && bindery audit all --id 1234,1235
112
+ ```
113
+ `--id` composes with `--tag`: the book is tagged only if the audit flags it.
114
+
115
+ **Spine-integrity reporting:** Both `audit` and `library` reports now classify manifest/NCX references that point to absent files. A `convention` verdict means the ToC is bloated but the present documents form a consecutive chapter span (e.g., the Wandering Inn official-build pattern, safe). A `fragment` verdict means the span itself is broken.
116
+
117
+ **Archive integrity:** every audit fully reads each archive entry (CRC + decompression), so a damaged download is reported CORRUPT — with the first broken entry named — instead of being mislabeled EMPTY by `emptytext`. `library --sweep` splits its `unreadable` bucket into `not_a_zip` / `truncated` / `encrypted` / `corrupt_entry`, so the right disease is visible without leaving the sweep.
118
+
119
+ ### Tagging flagged books (opt-in)
120
+
121
+ By default an audit writes nothing. With `--tag TAG` (library mode only), every book the audit
122
+ flags is tagged in `metadata.db` through cquarry's trigger-safe write module — useful for piping
123
+ flagged books back into Calibre views:
124
+
125
+ ```sh
126
+ cd ~/docs/Calibre\ Library && bindery audit content --tag "Audit Flagged"
127
+ ```
128
+
129
+ The audit itself stays read-only; only the final tagging pass writes, it skips books that already
130
+ carry the tag, and THIN emptytext advisories are never tagged. Close Calibre first so the write
131
+ does not fight its lock. Tagged books are recorded in Calibre's `metadata_dirtied` queue, so the
132
+ desktop app regenerates their sidecar `.opf`s (and re-pushes metadata to wireless readers) on its
133
+ next startup — no manual resync needed.
134
+
135
+
136
+ Repair one book to a new file (gated; writes only if it is an improvement):
137
+
138
+ ```sh
139
+ bindery repair broken.epub # -> "broken (repaired).epub"
140
+ bindery repair broken.epub fixed.epub
141
+ bindery repair scanned.epub --strip-pagination # also remove baked-in page numbers
142
+ ```
143
+
144
+ Scan a Calibre library and see what would be fixed, writing nothing:
145
+
146
+ ```sh
147
+ bindery library ~/docs/Calibre\ Library --only fatals --audit epub_audit.csv
148
+ ```
149
+
150
+ Apply accepted repairs in place, atomically, with backups:
151
+
152
+ ```sh
153
+ bindery library ~/docs/Calibre\ Library --only fatals --apply --backup ~/bindery-backups
154
+ ```
155
+
156
+ Apply all safe and lossy repairs directly into the Calibre database natively:
157
+
158
+ ```sh
159
+ bindery library ~/docs/Calibre\ Library --only all --apply --all --install-to-calibre
160
+ ```
161
+
162
+ - `--only {fatals,ncx,all}` restricts the candidate set. `ncx` targets NCX-001 mismatches (detected without epubcheck); `fatals` needs `--audit`.
163
+ - `--id <ids>` limits the sweep to a comma-separated list of Calibre book IDs, skipping the full library walk.
164
+ - `--audit CSV` (the `fatals,errors,warnings,path` format produced by an epubcheck sweep) skips clean books so a run is fast. Paths are resolved on both sides, and a CSV that matches nothing triggers a loud warning instead of silently selecting zero books.
165
+ - `--sweep` replaces the CSV step entirely: it runs a live epubcheck sweep for candidate selection and reuses each result as that book's before-measurement, so no book is checked twice. Bindery launches a transparent, persistent Java daemon (`EpubcheckDaemon`) to evaluate candidates in the background, eliminating the JVM startup penalty. This reduces validation time from ~5 seconds per book down to ~0.05 seconds, allowing `--sweep` to scan a 5,000-book library in just 10 minutes. Combine with `--only fatals` for a self-contained, lightning-fast "find and fix the broken books" run.
166
+ - `--json FILE` writes a machine-readable report of the whole run (per-book status, before/after counts, applied flag, summary totals). `--manual-list FILE` writes the paths of every book that was not auto-repaired, one per line, ready for manual follow-up.
167
+ - `--apply` is required to write; the default is a dry run. `--backup DIR` mirrors originals before replacing; `--backup-inplace` writes `.epub.bak` beside each file.
168
+ - `--install-to-calibre` resolves the Calibre book id from `metadata.db` via cquarry (one read-only path→id map per run), so a hand-renamed `Author/Title (id)/` directory can never send the repaired file to the wrong book. The directory-name regex is only a no-catalog fallback; with neither, the file is saved atomically in place.
169
+ - `--install-to-calibre` installs the repaired EPUB through cquarry's write module: the file is placed atomically at the catalogued path (same filename, Calibre's layout never changes) and the format row is re-registered in one transaction, so the stored size stays truthful and the book lands in `metadata_dirtied` — Calibre regenerates its sidecar .opf on next start. No external `calibredb` call is involved. It falls back to atomic file replacement if the Calibre database ID cannot be resolved, and a database failure degrades to the in-place save with a warning.
170
+ - `--all` automatically turns on all opt-in non-fatal fixes and lossy strips (pagination, watermarks, bad attributes, unknown entities, image alt tags, etc.) in a single run.
171
+ - Only the `.epub` is replaced. `metadata.opf`, `cover.jpg`, and `metadata.db` are left for Calibre's Quality Check sync to reconcile.
172
+ - A per-book progress line goes to stderr (stdout stays a clean report); `--quiet` suppresses it. A corrupt or unreadable book is reported and skipped, never aborting the sweep.
173
+ - Exit codes: 0 for a clean sweep, 1 for a usage error, 2 when any book was rejected, unreadable, or failed epubcheck (for scripts and cron).
174
+ - `repair` refuses to overwrite an existing output file unless `--force` is given.
175
+
176
+ ## Companion scripts
177
+
178
+ `scripts/` holds standalone, read-only utilities that are useful for EPUB maintenance but fall outside Bindery's repair contract (fixing what they find would be a content change, which Bindery makes only via the opt-in `--strip-pagination`):
179
+
180
+ - `find_missing_images.py`: scans a library tree and reports every book whose `<img>` tags point at files that do not exist inside the archive (a common defect in converted EPUBs). Reads the archives in place; nothing is unpacked or written. The library path is set at the bottom of the script.
181
+
182
+ ## Development
183
+
184
+ ```sh
185
+ ./run_tests.sh # unittest suite
186
+ ```
187
+
188
+ See [spec.md](spec.md) for the full contract and [roadmap.md](roadmap.md) for what is planned.
189
+
190
+ ## License
191
+
192
+ MIT. See [LICENSE](LICENSE).
193
+
194
+ ## Support
195
+
196
+ If Bindery's useful to you and you'd like to chip in:
197
+
198
+ - liberapay · [liberapay.com/bdkl](https://liberapay.com/bdkl/)
199
+ - bitcoin
200
+ ```
201
+ bc1qkge6zr45tzqfwfmvma2ylumt6mg7wlwmhr05yv
202
+ ```