bindery-cli 0.30.0__tar.gz → 0.31.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.
- {bindery_cli-0.30.0 → bindery_cli-0.31.0}/CLAUDE.md +1 -1
- {bindery_cli-0.30.0 → bindery_cli-0.31.0}/PKG-INFO +2 -2
- {bindery_cli-0.30.0 → bindery_cli-0.31.0}/patchnotes.md +13 -0
- {bindery_cli-0.30.0 → bindery_cli-0.31.0}/pyproject.toml +2 -2
- {bindery_cli-0.30.0 → bindery_cli-0.31.0}/roadmap.md +310 -2
- bindery_cli-0.31.0/scripts/FastSweepExtract.java +44 -0
- bindery_cli-0.31.0/scripts/sweep.sh +34 -0
- {bindery_cli-0.30.0 → bindery_cli-0.31.0}/src/bindery/__init__.py +1 -1
- {bindery_cli-0.30.0 → bindery_cli-0.31.0}/src/bindery/library.py +9 -8
- {bindery_cli-0.30.0 → bindery_cli-0.31.0}/tests/test_library.py +5 -4
- {bindery_cli-0.30.0 → bindery_cli-0.31.0}/uv.lock +5 -5
- {bindery_cli-0.30.0 → bindery_cli-0.31.0}/.github/workflows/ci.yml +0 -0
- {bindery_cli-0.30.0 → bindery_cli-0.31.0}/.github/workflows/publish.yml +0 -0
- {bindery_cli-0.30.0 → bindery_cli-0.31.0}/.gitignore +0 -0
- {bindery_cli-0.30.0 → bindery_cli-0.31.0}/AGENTS.md +0 -0
- {bindery_cli-0.30.0 → bindery_cli-0.31.0}/LICENSE +0 -0
- {bindery_cli-0.30.0 → bindery_cli-0.31.0}/README.md +0 -0
- {bindery_cli-0.30.0 → bindery_cli-0.31.0}/docs/screenshots/library-sweep.png +0 -0
- {bindery_cli-0.30.0 → bindery_cli-0.31.0}/logo.svg +0 -0
- {bindery_cli-0.30.0 → bindery_cli-0.31.0}/run_tests.sh +0 -0
- {bindery_cli-0.30.0 → bindery_cli-0.31.0}/scripts/FastSweep.java +0 -0
- {bindery_cli-0.30.0 → bindery_cli-0.31.0}/scripts/fast_sweep.py +0 -0
- {bindery_cli-0.30.0 → bindery_cli-0.31.0}/scripts/find_block_in_inline.py +0 -0
- {bindery_cli-0.30.0 → bindery_cli-0.31.0}/scripts/find_css_illegal_tags.py +0 -0
- {bindery_cli-0.30.0 → bindery_cli-0.31.0}/scripts/find_duplicate_playorder.py +0 -0
- {bindery_cli-0.30.0 → bindery_cli-0.31.0}/scripts/find_empty_body.py +0 -0
- {bindery_cli-0.30.0 → bindery_cli-0.31.0}/scripts/find_illegal_tags.py +0 -0
- {bindery_cli-0.30.0 → bindery_cli-0.31.0}/scripts/find_invalid_ids.py +0 -0
- {bindery_cli-0.30.0 → bindery_cli-0.31.0}/scripts/find_invalid_st_tag.py +0 -0
- {bindery_cli-0.30.0 → bindery_cli-0.31.0}/scripts/find_invalid_value_attr.py +0 -0
- {bindery_cli-0.30.0 → bindery_cli-0.31.0}/scripts/find_missing_images.py +0 -0
- {bindery_cli-0.30.0 → bindery_cli-0.31.0}/scripts/find_missing_title.py +0 -0
- {bindery_cli-0.30.0 → bindery_cli-0.31.0}/spec.md +0 -0
- {bindery_cli-0.30.0 → bindery_cli-0.31.0}/src/bindery/__main__.py +0 -0
- {bindery_cli-0.30.0 → bindery_cli-0.31.0}/src/bindery/audit.py +0 -0
- {bindery_cli-0.30.0 → bindery_cli-0.31.0}/src/bindery/cli.py +0 -0
- {bindery_cli-0.30.0 → bindery_cli-0.31.0}/src/bindery/epub.py +0 -0
- {bindery_cli-0.30.0 → bindery_cli-0.31.0}/src/bindery/pagination.py +0 -0
- {bindery_cli-0.30.0 → bindery_cli-0.31.0}/src/bindery/reserialize.py +0 -0
- {bindery_cli-0.30.0 → bindery_cli-0.31.0}/src/bindery/transforms.py +0 -0
- {bindery_cli-0.30.0 → bindery_cli-0.31.0}/src/bindery/validate.py +0 -0
- {bindery_cli-0.30.0 → bindery_cli-0.31.0}/src/bindery/watermark.py +0 -0
- {bindery_cli-0.30.0 → bindery_cli-0.31.0}/test_facility/agda.epub +0 -0
- {bindery_cli-0.30.0 → bindery_cli-0.31.0}/test_facility/agda_fixed.epub +0 -0
- {bindery_cli-0.30.0 → bindery_cli-0.31.0}/test_facility/last_man_out.epub +0 -0
- {bindery_cli-0.30.0 → bindery_cli-0.31.0}/test_facility/last_man_out_fixed.epub +0 -0
- {bindery_cli-0.30.0 → bindery_cli-0.31.0}/test_facility/leaves_of_grass.epub +0 -0
- {bindery_cli-0.30.0 → bindery_cli-0.31.0}/test_facility/leaves_of_grass_fixed.epub +0 -0
- {bindery_cli-0.30.0 → bindery_cli-0.31.0}/test_facility/leaves_of_grass_st_fixed.epub +0 -0
- {bindery_cli-0.30.0 → bindery_cli-0.31.0}/test_facility/leaves_of_grass_tags_fixed.epub +0 -0
- {bindery_cli-0.30.0 → bindery_cli-0.31.0}/test_facility/muse_of_nightmares.epub +0 -0
- {bindery_cli-0.30.0 → bindery_cli-0.31.0}/test_facility/muse_of_nightmares_fixed.epub +0 -0
- {bindery_cli-0.30.0 → bindery_cli-0.31.0}/test_facility/poor_mans_fight.epub +0 -0
- {bindery_cli-0.30.0 → bindery_cli-0.31.0}/test_facility/poor_mans_fight_fixed.epub +0 -0
- {bindery_cli-0.30.0 → bindery_cli-0.31.0}/test_facility/test_block_in_inline.py +0 -0
- {bindery_cli-0.30.0 → bindery_cli-0.31.0}/test_facility/test_duplicate_playorder.py +0 -0
- {bindery_cli-0.30.0 → bindery_cli-0.31.0}/test_facility/test_empty_body.py +0 -0
- {bindery_cli-0.30.0 → bindery_cli-0.31.0}/test_facility/test_illegal_tags.py +0 -0
- {bindery_cli-0.30.0 → bindery_cli-0.31.0}/test_facility/test_invalid_ids.py +0 -0
- {bindery_cli-0.30.0 → bindery_cli-0.31.0}/test_facility/test_invalid_value.py +0 -0
- {bindery_cli-0.30.0 → bindery_cli-0.31.0}/test_facility/test_missing_title.py +0 -0
- {bindery_cli-0.30.0 → bindery_cli-0.31.0}/test_facility/unworthy_gods.epub +0 -0
- {bindery_cli-0.30.0 → bindery_cli-0.31.0}/test_facility/unworthy_gods_fixed.epub +0 -0
- {bindery_cli-0.30.0 → bindery_cli-0.31.0}/testing_facility/books_to_fix/.gitkeep +0 -0
- {bindery_cli-0.30.0 → bindery_cli-0.31.0}/testing_facility/top500candidates/REPORT.md +0 -0
- {bindery_cli-0.30.0 → bindery_cli-0.31.0}/tests/test_audit.py +0 -0
- {bindery_cli-0.30.0 → bindery_cli-0.31.0}/tests/test_cli.py +0 -0
- {bindery_cli-0.30.0 → bindery_cli-0.31.0}/tests/test_epub.py +0 -0
- {bindery_cli-0.30.0 → bindery_cli-0.31.0}/tests/test_pagination.py +0 -0
- {bindery_cli-0.30.0 → bindery_cli-0.31.0}/tests/test_reserialize.py +0 -0
- {bindery_cli-0.30.0 → bindery_cli-0.31.0}/tests/test_transforms.py +0 -0
- {bindery_cli-0.30.0 → bindery_cli-0.31.0}/tests/test_validate.py +0 -0
- {bindery_cli-0.30.0 → bindery_cli-0.31.0}/tests/test_version.py +0 -0
- {bindery_cli-0.30.0 → bindery_cli-0.31.0}/tests/test_watermark.py +0 -0
|
@@ -66,7 +66,7 @@ Born from the 2026 library audit (see the user memory `calibre-library-epubcheck
|
|
|
66
66
|
- `src/bindery/epub.py`: archive rewrite, NCX uid sync, RepairReport, mismatch detection, and the opt-in structural-repair plumbing (including the CSS precondition scan).
|
|
67
67
|
- `src/bindery/validate.py`: epubcheck wrapper, the `gate` (improvement) and `no_worse` (no-regression, for the lossy strips) acceptance bars.
|
|
68
68
|
- `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.
|
|
69
|
-
- `src/bindery/cli.py`: `repair` and `library` subcommands, including `--all` and `--install-to-calibre`; since v0.29.0 also the `run` verbs
|
|
69
|
+
- `src/bindery/cli.py`: `repair` and `library` subcommands, including `--all` and `--install-to-calibre`; since v0.29.0 also the `run` verbs: `phase1` (pre-import vetting over loose files: the audit battery composed with one gated repair sweep; read-only until `--apply-lossy`, which IS the lossy-strip consent) and `phase3` (the scoped post-import apply step: `library --id --sweep --only all --apply --all --install-to-calibre` plus a pre/post summary; mechanically refuses unscoped sweeps with exit 2). Both take `--non-interactive` and surface open questions as `decisions_needed` in their JSON, never prompts. They drive the shipped `run_library` through the real parser, so no flag can drift between the verb and the subcommand it wraps. `library --workers N` (v0.30.0) parallelizes only the sweep's candidate pass, in windows of N consumed in order so `--limit` stays lazy; the repair phase is never parallel (shared workdir, atomic-replacement contract).
|
|
70
70
|
- `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.
|
|
71
71
|
|
|
72
72
|
## Conventions
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: bindery-cli
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.31.0
|
|
4
4
|
Summary: Audit EPUB body text and repair broken EPUBs with deterministic, epubcheck-gated fixes; atomic in-place replacement in a Calibre library.
|
|
5
5
|
Author: vrnvctss
|
|
6
6
|
License: MIT
|
|
@@ -13,7 +13,7 @@ Classifier: License :: OSI Approved :: MIT License
|
|
|
13
13
|
Classifier: Programming Language :: Python :: 3.14
|
|
14
14
|
Classifier: Topic :: Utilities
|
|
15
15
|
Requires-Python: >=3.14
|
|
16
|
-
Requires-Dist: cquarry>=1.
|
|
16
|
+
Requires-Dist: cquarry>=1.17.0
|
|
17
17
|
Requires-Dist: tqdm
|
|
18
18
|
Requires-Dist: vir-tui>=2.2.0
|
|
19
19
|
Provides-Extra: reserialize
|
|
@@ -1,4 +1,17 @@
|
|
|
1
1
|
# bindery-cli Patch Notes
|
|
2
|
+
## v0.31.0 (2026-09-09)
|
|
3
|
+
|
|
4
|
+
### Cascade: install through cquarry 1.17's set_format
|
|
5
|
+
|
|
6
|
+
- **`--install-to-calibre` replaces the format row through
|
|
7
|
+
`WritableCalibreDB.set_format`** (cquarry 1.17's sanctioned remove+add
|
|
8
|
+
in one transaction) instead of composing `remove_format` + `add_format`
|
|
9
|
+
in a `batch()` by hand. Same contract as before: same `data.name`,
|
|
10
|
+
truthful size, OPF-resync queue, and a database failure still degrades
|
|
11
|
+
to the in-place save with a warning. The cquarry floor moves from
|
|
12
|
+
>=1.9.0 to **>=1.17.0** (`uv.lock` re-resolved), and the failure-mode
|
|
13
|
+
test now injects through `set_format`.
|
|
14
|
+
|
|
2
15
|
## v0.30.0 (2026-09-06)
|
|
3
16
|
|
|
4
17
|
### Phase 8 stretch: concurrent sweep workers
|
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "bindery-cli"
|
|
7
|
-
version = "0.
|
|
7
|
+
version = "0.31.0"
|
|
8
8
|
description = "Audit EPUB body text and repair broken EPUBs with deterministic, epubcheck-gated fixes; atomic in-place replacement in a Calibre library."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
classifiers = [
|
|
@@ -27,7 +27,7 @@ authors = [
|
|
|
27
27
|
dependencies = [
|
|
28
28
|
"tqdm",
|
|
29
29
|
"vir-tui>=2.2.0",
|
|
30
|
-
"cquarry>=1.
|
|
30
|
+
"cquarry>=1.17.0",
|
|
31
31
|
]
|
|
32
32
|
|
|
33
33
|
[project.optional-dependencies]
|
|
@@ -609,6 +609,58 @@ hand-rolled zipfile sweep.
|
|
|
609
609
|
*(Fixed in v0.23.1; RETIRED entirely by Phase 11 in v0.24.0 — the calibredb
|
|
610
610
|
subprocess is gone, so the crash class no longer exists.)*
|
|
611
611
|
|
|
612
|
+
### watermark anchored pass can delete whole paragraphs of real prose (2026-09-08 sweep)
|
|
613
|
+
**Bug in `--strip-watermarks`:** the anchored stamp regex
|
|
614
|
+
`<a\b[^>]*?oceanofpdf[^>]*?>.*?</a>` is DOTALL with no tag budget, so an
|
|
615
|
+
unclosed stamp anchor (exactly the kind of broken book this tool repairs)
|
|
616
|
+
swallows everything up to the next unrelated `</a>`, and when no pure wrapper
|
|
617
|
+
exists the fallback deletes the entire match. Demonstrated: two paragraphs of
|
|
618
|
+
real content vanish with count 1; the output is well-formed with equal or
|
|
619
|
+
fewer fatals, so the `no_worse` bar accepts it. `watermark.py:207-213`.
|
|
620
|
+
**Fix:** require the anchored match to be tag-free (or stamp-length visible
|
|
621
|
+
text) before falling back to whole-match deletion; anything larger goes to
|
|
622
|
+
`decisions_needed` for manual repair. *(Unfixed as of the 2026-09-08 sweep.)*
|
|
623
|
+
|
|
624
|
+
### roman-numeral detector reads ordinary English words as page numbers (2026-09-08 sweep)
|
|
625
|
+
**Bug in `--strip-pagination`:** `_ROMAN_RE = [ivxlcdm]{2,7}\Z` (IGNORECASE)
|
|
626
|
+
matches words like `mid`, `dim`, `mix`, `lid`, `civil` (`number_value("mid")
|
|
627
|
+
= 1499`), so a standalone `<p>mid</p>` followed by a lowercase-starting
|
|
628
|
+
paragraph is a confident page-number hit and is deleted and merged away.
|
|
629
|
+
epubcheck is blind to deleted text, so `no_worse` passes. `pagination.py:44`,
|
|
630
|
+
`70-78`, `180-184`.
|
|
631
|
+
**Fix:** require uppercase romans, or validate with a strict roman grammar
|
|
632
|
+
(`M?(D?C{0,3}|C[MD])(X?L{0,3}|L?X{0,3})(I?V{0,3}|V?I{0,3})`). *(Unfixed as of
|
|
633
|
+
the 2026-09-08 sweep.)*
|
|
634
|
+
|
|
635
|
+
### install_format can write a wrong size into metadata.db for stray files (2026-09-08 sweep)
|
|
636
|
+
**Bug in `library --apply --install-to-calibre` without `--id`:**
|
|
637
|
+
`iter_epubs` yields uncatalogued stray `.epub` files inside book dirs; the
|
|
638
|
+
resolver returns None, the `(id)` directory-name guess fires, and the
|
|
639
|
+
remove+add batch re-registers the catalogued format name with the stray
|
|
640
|
+
file's size. Demonstrated on a synthetic library: the catalogued 999-byte
|
|
641
|
+
file untouched on disk while `data` recorded 8123 bytes. Silent,
|
|
642
|
+
order-dependent catalog corruption; a stale `(N)` directory for a deleted
|
|
643
|
+
book instead raises an uncaught `TypeError` at the `book["path"]` access.
|
|
644
|
+
`library.py:169-231`.
|
|
645
|
+
**Fix:** when the id came from the directory guess, verify the target
|
|
646
|
+
matches the DB's book path and format name before the batch; otherwise save
|
|
647
|
+
in place and warn without touching the row, and catch the `TypeError` path.
|
|
648
|
+
*(Unfixed as of the 2026-09-08 sweep.)*
|
|
649
|
+
|
|
650
|
+
### EPUB2-targeted structural fixes fire on EPUB3 books under --all (2026-09-08 sweep)
|
|
651
|
+
**Bug in `--strip-epub3-attrs` / `--downgrade-epub3-tags` via the run verbs
|
|
652
|
+
and `--all`:** neither fix checks `package@version`, so every EPUB3 book in
|
|
653
|
+
the sweep gets `epub:type`/`aria-label`/`page-progression-direction` stripped
|
|
654
|
+
and EPUB3 tags downgraded. Two demonstrated outcomes: (a) a repairable EPUB3
|
|
655
|
+
book gains a net-new error from stripping `epub:type="toc"` off the nav and
|
|
656
|
+
the whole repair is rejected as a regression; (b) a legal count-neutral
|
|
657
|
+
`epub:type="chapter"` is silently removed because the gate only sees
|
|
658
|
+
epubcheck counts. phase1 and phase3 always pass `--all`.
|
|
659
|
+
`epub.py:251-260`, `903-937`, `957-967`; `cli.py:830`, `976-987`.
|
|
660
|
+
**Fix:** gate both fixes on the package version carried in `opf_text`, or
|
|
661
|
+
make them finding-driven (only strip what the before-pass epubcheck flagged).
|
|
662
|
+
*(Unfixed as of the 2026-09-08 sweep.)*
|
|
663
|
+
|
|
612
664
|
## Phase 11: Migrate install-to-calibre to native cquarry API (proposed 2026-08-31)
|
|
613
665
|
|
|
614
666
|
*Context: `bindery library --install-to-calibre` currently shells out to the external `calibredb add_format` CLI binary. This is fragile (it crashed on 2026-08-31 due to a non-existent `--replace` flag) and bypasses the transaction and trigger-safety guarantees built into the `cquarry` library. Since bindery-cli already imports `cquarry` for path resolution, it should adopt the native write module.*
|
|
@@ -715,7 +767,7 @@ classes, no behavior change to existing modes.*
|
|
|
715
767
|
Exit codes 0/1/2 per the `library` contract. No DRM scan, no duplicate
|
|
716
768
|
screen, no manifest assembly (CalibreQuarry's by the frontend split).
|
|
717
769
|
*(Shipped in v0.29.0. Two composed stages: the audit battery through its
|
|
718
|
-
own --json payload, then ONE fused repair sweep with the --all set
|
|
770
|
+
own --json payload, then ONE fused repair sweep with the --all set: the
|
|
719
771
|
skill ran watermark detection and repairability as two sweeps, the verb
|
|
720
772
|
pays epubcheck once and reads watermark hits out of the per-book fix
|
|
721
773
|
summary. Read-only until --apply-lossy; --backup passes through (keep
|
|
@@ -732,7 +784,7 @@ classes, no behavior change to existing modes.*
|
|
|
732
784
|
the real parser, so its flags cannot drift from the subcommand it wraps.
|
|
733
785
|
The pre/post summary sums before/after epubcheck counts over the swept
|
|
734
786
|
books; books left partial or unreadable surface as decisions_needed.
|
|
735
|
-
The verb is the apply step by design
|
|
787
|
+
The verb is the apply step by design: the batch was vetted in phase 1,
|
|
736
788
|
and the gate plus the atomic-replacement contract still govern every
|
|
737
789
|
write.)*
|
|
738
790
|
- [x] **Non-interactive contract**: every `run` verb takes
|
|
@@ -757,3 +809,259 @@ Non-goals: no manifest format ownership (CalibreQuarry owns the
|
|
|
757
809
|
`acquisition-manifest` schema and the orchestration; this repo emits reports
|
|
758
810
|
and consumes id lists); no TUI beyond the existing audit rendering; no PDF/DJVU
|
|
759
811
|
work (never this repo's charter).
|
|
812
|
+
|
|
813
|
+
## Phase 14: hardening backlog from the 2026-09-08 audit sweep (proposed 2026-09-08, digging only)
|
|
814
|
+
|
|
815
|
+
*Context: a five-agent adversarial sweep of the whole repo (repair pipeline,
|
|
816
|
+
audit, CLI/library/validate, tests and scripts, docs). No code was changed;
|
|
817
|
+
every finding below was demonstrated against the shipped code, most with a
|
|
818
|
+
live run. The four sharpest bugs are written up under Bug Reports above; this
|
|
819
|
+
phase is the rest of the backlog. Nothing here is committed work: it is the
|
|
820
|
+
map of what the sweep found, for when work resumes.*
|
|
821
|
+
|
|
822
|
+
*Verification postscript (2026-09-08, an independent second batch re-derived
|
|
823
|
+
the five sharpest claims; all five CONFIRMED, with scoping notes): the
|
|
824
|
+
watermark loss is invisible to `no_worse` and can even clear a fatal, so the
|
|
825
|
+
book ships as a success; the window is the standalone `--strip-watermarks`
|
|
826
|
+
flag (the anchorless pass never fallback-deletes, and under `--all`
|
|
827
|
+
`--reserialize` closes the anchor first). The roman deletion needs a
|
|
828
|
+
120+ char prose neighbor (`PROSE_MIN`) and a lowercase-starting continuation
|
|
829
|
+
paragraph, and no test acknowledges word-shaped romans. The `install_format`
|
|
830
|
+
exposure is exactly directory-mode `--apply --install-to-calibre`; phase3
|
|
831
|
+
passes `--id` and is not exposed. The `fix_id_colons` dangling NCX ref
|
|
832
|
+
survives the gate whenever colon fixes outnumber NCX references (the minimal
|
|
833
|
+
1:1 case is refused as a noop), and the phantom count additionally triggers
|
|
834
|
+
the re-encode that the untouched-file guard exists to prevent. The EPUB3
|
|
835
|
+
hard reject needs a clean book; in already-error books the attribute strip
|
|
836
|
+
can be net-neutral and ships silently.*
|
|
837
|
+
|
|
838
|
+
### Repair-pipeline correctness
|
|
839
|
+
|
|
840
|
+
- [ ] **Anchor attribute-name regexes to real start tags.** Three findings
|
|
841
|
+
share one root cause: bare attribute-name regexes that are not
|
|
842
|
+
quote-aware and not tag-anchored. `strip_invalid_value` matches the
|
|
843
|
+
`value` in `data-value="42"` (the `\b` matches between `-` and `v`)
|
|
844
|
+
and rewrites it to a malformed `<span data->`
|
|
845
|
+
(`transforms.py:375`); `_EPUB3_ATTR_RE` is not anchored to tags at all,
|
|
846
|
+
so `strip_epub3_attributes` deletes visible prose like
|
|
847
|
+
`Use epub:type="chapter" here` down to `Use here` (`epub.py:245-248`),
|
|
848
|
+
contradicting that fix's own docstring. Reuse the quote-aware
|
|
849
|
+
start-tag matcher + protected-span machinery the module already has,
|
|
850
|
+
and a `(?<![\w:.-])value` lookbehind.
|
|
851
|
+
- [ ] **Make `fix_id_colons` consistent and honest.** It rewrites fragments
|
|
852
|
+
of external URLs (`http://example.com/page#sec:1` becomes `#sec_1`,
|
|
853
|
+
breaking the link; `transforms.py:524`, `536`, docstring at 509 claims
|
|
854
|
+
otherwise) and rewrites colon-bearing `data-id`/`xml:id`; it never
|
|
855
|
+
translates the NCX's `content src` fragments, so renaming
|
|
856
|
+
manufactures dangling NCX refs end to end
|
|
857
|
+
(`epub.py:976-979` vs the NCX branch at `873-901`); and it counts
|
|
858
|
+
every matched `id` attribute even when no colon was replaced, so
|
|
859
|
+
byte-identical reruns report phantom changes (`transforms.py:513-516`).
|
|
860
|
+
- [ ] **Run anchor stripping last and protect CDATA/comments everywhere.**
|
|
861
|
+
`strip_broken_anchors`' id snapshot predates `unwrap_block_in_inline`
|
|
862
|
+
and `unwrap_illegal_tags`, which can delete ids the snapshot thinks
|
|
863
|
+
exist (`epub.py:980-997`); `strip_broken_tags` and
|
|
864
|
+
`unwrap_illegal_tags` are not wrapped in the module's own
|
|
865
|
+
protected-span machinery, so they edit inside CDATA (rendered text)
|
|
866
|
+
and comments (`transforms.py:444-450`, `619-623`).
|
|
867
|
+
- [ ] **Stop re-encoding non-UTF-8 documents.** Any fix that fires on a
|
|
868
|
+
windows-1252 or UTF-16 document decodes with `replace` and re-encodes
|
|
869
|
+
UTF-8, materializing mojibake under an XML declaration that still
|
|
870
|
+
names the old encoding; usually well-formed, so the gate cannot see
|
|
871
|
+
it (`epub.py:874`, `902`, `939`, `1027`). Detect and skip with a
|
|
872
|
+
report entry, per the project's manual-repair philosophy.
|
|
873
|
+
- [ ] **Scope `--reserialize` to HTML roots.** A broken non-HTML `.xml`
|
|
874
|
+
sidecar gets html5lib's HTML algorithm and comes back
|
|
875
|
+
html/body-wrapped with `ns0:` prefixes, structurally rewritten while
|
|
876
|
+
staying well-formed (`epub.py:938`, `reserialize.py:39-42`). Require
|
|
877
|
+
an `html` root (or manifest media-type) before rebuilding.
|
|
878
|
+
- [ ] **Smaller repair papercuts:** `fix_ncx_playorder` can rewrite
|
|
879
|
+
`playOrder="N"`-shaped text inside nav labels (unanchored pattern,
|
|
880
|
+
`transforms.py:326-328`); mimetype fixes land in `report.fixes`
|
|
881
|
+
without incrementing `files_changed` (`epub.py:837-840`);
|
|
882
|
+
`fix_manifest_ids` is a silent count-0 no-op on single-quoted OPFs
|
|
883
|
+
(`epub.py:59`); `css_protected_tags` misses namespaced
|
|
884
|
+
(`svg|st`) and functional (`:is(st, w)`) selectors, and doesn't scan
|
|
885
|
+
`.xpgt` templates (`transforms.py:419-421`, `epub.py:790-794`); the
|
|
886
|
+
watermark `_norm` doesn't unescape entities so `OceanofPDF.com `
|
|
887
|
+
stamps are missed; drop_duplicate_xmlns/strip_prolog_junk edit inside
|
|
888
|
+
comments; the archive comment field is dropped by the rewrite.
|
|
889
|
+
|
|
890
|
+
### audit.py correctness
|
|
891
|
+
|
|
892
|
+
- [ ] **Read nav/NCX inside the open zip and resolve NCX srcs against the
|
|
893
|
+
NCX's own directory.** The post-close `_read` of a manifest-declared
|
|
894
|
+
but absent nav/NCX poisons `Book.corrupt`, so an otherwise healthy
|
|
895
|
+
book with a leftover `toc.ncx` manifest entry is branded CORRUPT
|
|
896
|
+
"re-source" (`audit.py:252-272`, `_read` at `173-177`). And `full()`
|
|
897
|
+
resolves NCX `content src` against the OPF's directory, not the NCX's,
|
|
898
|
+
so a spec-compliant nested NCX counts every target absent and can
|
|
899
|
+
reach the FRAGMENT "quarantine" alarm (`audit.py:187`, `205-213`,
|
|
900
|
+
`249`, `263-272`). Also `html.unescape` captured ToC hrefs, which are
|
|
901
|
+
currently compared raw against archive names (`audit.py:258`, `268-272`).
|
|
902
|
+
- [ ] **Make archive/spine first-class in library mode.** `run_library`'s
|
|
903
|
+
report loop iterates the content-analyzer tuple only, so the
|
|
904
|
+
`archive`/`spine` branches and their section builders are dead code:
|
|
905
|
+
a CRC-corrupt book prints `emptytext CLEAN` and exits 0 in library
|
|
906
|
+
mode while directory mode says CORRUPT and exits 1, contradicting the
|
|
907
|
+
v0.22.0 "its own verdict in every mode" promise (`audit.py:1565`,
|
|
908
|
+
`1783-1803`, `1520-1558`).
|
|
909
|
+
- [ ] **Harden the edges of the record pipeline:** a failed `--json` write
|
|
910
|
+
raises after the scan and loses the whole run's summary and exit code
|
|
911
|
+
(`audit.py:1616`, `2023-2043`); `--tag` tags books whose content hits
|
|
912
|
+
the run itself marked expected-foreign (`audit.py:1830-1832` vs the
|
|
913
|
+
rc filter); a decoded-href vs stored-name spine miss silently reads
|
|
914
|
+
as EMPTY on a book full of prose, with no diagnostic
|
|
915
|
+
(`audit.py:219`); DRM-encrypted entries report as CORRUPT
|
|
916
|
+
"re-source" instead of their own status (`audit.py:173-177`); the
|
|
917
|
+
broken-span heuristic treats duplicate trailing numbers
|
|
918
|
+
(`part1a/part1b/part2`) and duplicate spine itemrefs as FRAGMENT
|
|
919
|
+
(`audit.py:1066-1079`).
|
|
920
|
+
- [ ] **Delete the dead surfaces:** `audit.main()` + its argparse block
|
|
921
|
+
duplicate `cli.run_audit_cmd` (`audit.py:2212-2315`);
|
|
922
|
+
`analyze_brokentags` is unreachable (`audit.py:1260-1274`); duplicate
|
|
923
|
+
imports at the top of the file; and standalone `audit <dir>` silently
|
|
924
|
+
skips uppercase `.EPUB` files (`audit.py:1937`).
|
|
925
|
+
- [ ] **Smaller audit papercuts:** `_Blocks.handle_endtag` pops the stack
|
|
926
|
+
top regardless of which tag closed, so mis-nested blockquotes lose
|
|
927
|
+
their in_quote guard (`audit.py:739-743`); nav selection matches
|
|
928
|
+
`data-nav` by substring (`audit.py:196`); tagging failure can surface
|
|
929
|
+
an undocumented exit 3 (`audit.py:2203-2208`); the JSON `analyzers`
|
|
930
|
+
list omits the always-on archive/spine verdicts and the console
|
|
931
|
+
problem counter can disagree with `summary.problems`
|
|
932
|
+
(`audit.py:1603`); `--min-chars`/`--thin-chars` are not validated
|
|
933
|
+
against each other; duplicate zip entries silently resolve last-wins
|
|
934
|
+
with no note in the record.
|
|
935
|
+
|
|
936
|
+
### Apply-path and oracle safety
|
|
937
|
+
|
|
938
|
+
- [ ] **Move the write-back inside the per-book exception isolation.**
|
|
939
|
+
`make_backup`/`atomic_replace`/`install_format` sit bare in the
|
|
940
|
+
sweep loop, so an ENOSPC or EACCES partway through a multi-hour run
|
|
941
|
+
aborts raw with no summary, no JSON, and no record of what was
|
|
942
|
+
already applied (`cli.py:498-508` vs `544-557`). Wrap the block,
|
|
943
|
+
record an error Outcome, and still emit the report; a run journal
|
|
944
|
+
(one line per applied book, resumable) is the stronger version.
|
|
945
|
+
- [ ] **Drive the exit code from partial books.** The documented contract
|
|
946
|
+
says exit 2 means "ran fine but some books are in trouble", but
|
|
947
|
+
partial books land in `still_fatal` and return 0, in both `library`
|
|
948
|
+
and phase3, while phase1 maps partial to problem/exit 2; the layers
|
|
949
|
+
disagree and scripts can miss trouble (`cli.py:260`, `526-534`, `634`,
|
|
950
|
+
`1030-1032`).
|
|
951
|
+
- [ ] **Give backups overwrite protection and keep them out of the
|
|
952
|
+
candidate set.** `make_backup` clobbers an existing backup, so a
|
|
953
|
+
second `--apply` destroys the only copy of the author original
|
|
954
|
+
(`library.py:37-41`); a `--backup` dir inside the library root gets
|
|
955
|
+
its `.epub`-named copies swept as candidates on the next run
|
|
956
|
+
(`library.py:29-34`). Refuse overwrite (or rotate) and reject an
|
|
957
|
+
in-tree backup path.
|
|
958
|
+
- [ ] **Make `--workers` parallelism real and bound the daemon.** The
|
|
959
|
+
epubcheck daemon holds its lock across the whole blocking
|
|
960
|
+
round-trip, so every worker serializes behind it and `--workers N`
|
|
961
|
+
degrades to serial exactly when epubcheck is present
|
|
962
|
+
(`validate.py:155-169`, `cli.py:213-255`); the daemon path ignores
|
|
963
|
+
`timeout` entirely, so a JVM hang hangs the sweep forever
|
|
964
|
+
(`validate.py:196`); a dead daemon never resets `_proc`, so every
|
|
965
|
+
later book reads as `error` for the life of the process
|
|
966
|
+
(`validate.py:160-170`); a failed daemon start leaks its
|
|
967
|
+
`/tmp/bindery-daemon-*` workdir and retries the javac compile per
|
|
968
|
+
book (`validate.py:130-153`); and `--only ncx` crashes with a raw
|
|
969
|
+
RuntimeError on encrypted archives because `_select` probes outside
|
|
970
|
+
the exception net (`epub.py:688`, `cli.py:184`).
|
|
971
|
+
- [ ] **Smaller apply papercuts:** `--limit -1` and a missing
|
|
972
|
+
`--audit` file raise tracebacks instead of usage errors
|
|
973
|
+
(`cli.py:434`, `373`); the fresh-format branch of `install_format`
|
|
974
|
+
registers the format under the name `repaired`
|
|
975
|
+
(`library.py:223-230`); phase1 ignores `--backup` without
|
|
976
|
+
`--apply-lossy` silently; the lossy override can upgrade a noop to
|
|
977
|
+
partial in the still-fatal listing (conservative direction only).
|
|
978
|
+
|
|
979
|
+
### Tests
|
|
980
|
+
|
|
981
|
+
- [ ] **Add the missing direct tests for the safety contract:** `gate()` and
|
|
982
|
+
`no_worse()` have zero direct tests (the subtle fatal-fixing
|
|
983
|
+
error-unmasking branch is asserted nowhere); `_EpubcheckDaemon` is
|
|
984
|
+
completely untested (and hides the no-reset liveness bug above);
|
|
985
|
+
`--strip-broken-tags` has zero tests anywhere despite being one of
|
|
986
|
+
the three named lossy strips; `--strip-watermarks`' gated apply path
|
|
987
|
+
has no analogue of the pagination gate test; `fix_ncx_playorder`
|
|
988
|
+
(core, always-on) is untested; `atomic_replace`'s failure path
|
|
989
|
+
(temp cleaned, target untouched, re-raise) has no failure-injection
|
|
990
|
+
test; `--install-to-calibre` CLI wiring and library-mode `--tag`
|
|
991
|
+
end-to-end are untested.
|
|
992
|
+
- [ ] **Clean the weak 2%:** delete the three mid-file
|
|
993
|
+
`if __name__ == "__main__": unittest.main()` blocks
|
|
994
|
+
(`test_audit.py:806`, `:943`; `test_cli.py:475`) that make direct
|
|
995
|
+
file runs silently skip ~40 tests; drop the constant-assertion
|
|
996
|
+
`test_all_tuple_has_ocr`; consolidate the verbatim-duplicated
|
|
997
|
+
roman/number implementations and their twin test classes (audit copy
|
|
998
|
+
lacks pagination's year exclusion, which is its own question);
|
|
999
|
+
remove the dead `sys.argv` patches in three CLI tests; strengthen or
|
|
1000
|
+
delete `test_non_interactive_flag_wiring` (asserts argparse, not the
|
|
1001
|
+
runner); de-alias the `pagenum`/`emptytext`/`ocr` module aliases.
|
|
1002
|
+
- [ ] **Add a ruff step to run_tests.sh** so the local loop matches CI
|
|
1003
|
+
(`uvx ruff check . && uvx ruff format --check .`), and note the
|
|
1004
|
+
html5lib-dependent tests silently skip without uv.
|
|
1005
|
+
|
|
1006
|
+
### Scripts and repo hygiene
|
|
1007
|
+
|
|
1008
|
+
- [ ] **Retire the subsumed find_* diagnostics.** All ten `scripts/find_*.py`
|
|
1009
|
+
detection wedges became shipped flags with tests
|
|
1010
|
+
(the css one says so in its own docstring port note); keeping both
|
|
1011
|
+
copies means future behavior changes land in one only. Delete (git
|
|
1012
|
+
preserves) or move to an attic note; also delete `sweep.sh` and
|
|
1013
|
+
`FastSweepExtract.java` (superseded by `fast_sweep.py --mode=extract`
|
|
1014
|
+
and `FastSweep.java`), keeping `fast_sweep.py` + `FastSweep.java`
|
|
1015
|
+
(a live, different tool: one JVM saturated across cores vs the
|
|
1016
|
+
library sweep's N subprocesses). The untracked `.class` files are
|
|
1017
|
+
local build leftovers; already gitignored.
|
|
1018
|
+
- [ ] **Decide what test_facility/ is.** It carries 10 tracked commercial
|
|
1019
|
+
EPUBs (muse-of-nightmares, last-man-out, etc.; only leaves-of-grass is
|
|
1020
|
+
public domain), a second never-run "suite" of bare functions that
|
|
1021
|
+
`unittest discover` reports as 0 tests, and it mutates the tracked
|
|
1022
|
+
books in place. `.gitignore`'s own comment says books are ignored to
|
|
1023
|
+
prevent IP leaks, but that rule only covers `testing_facility/`;
|
|
1024
|
+
`test_facility/` predates it. At minimum stop tracking the EPUBs and
|
|
1025
|
+
delete or properly convert the dead suite; stripping them from
|
|
1026
|
+
history is the thorough option (repo is public).
|
|
1027
|
+
- [ ] **Mention validate.py's Java epubcheck daemon in the docs.** It is
|
|
1028
|
+
the first oracle tried at runtime, it compiles `_DAEMON_JAVA`
|
|
1029
|
+
(`validate.py:73-105`) into a tempdir, and nothing in README/spec
|
|
1030
|
+
admits it exists; it is also the scariest untested code in the repo.
|
|
1031
|
+
|
|
1032
|
+
### Documentation (drift is real but narrow)
|
|
1033
|
+
|
|
1034
|
+
- [ ] **Reconcile spec.md with the shipped opt-ins.** The contract's
|
|
1035
|
+
non-goals forbid exactly what `--reserialize` (v0.3.0, the lone
|
|
1036
|
+
html5lib dependency) and `--strip-bad-attrs` (v0.4.0) do, and neither
|
|
1037
|
+
has a spec section; the OPF half of `--fix-ids` is missing;
|
|
1038
|
+
"the OPF is left untouched" is true only of the default pass; the
|
|
1039
|
+
NCX sentence at spec.md:52-53 is garbled (a dangling "to the OPF
|
|
1040
|
+
unique identifier" tail). A contributor reading spec.md first would
|
|
1041
|
+
conclude two shipped flags violate the charter.
|
|
1042
|
+
- [ ] **Fix the exit-code contract.** README and spec document usage
|
|
1043
|
+
errors as exit 1, but argparse-level misuse exits 2 (the same code
|
|
1044
|
+
as "book in trouble"); either a custom parser exit or updated docs.
|
|
1045
|
+
- [ ] **README completeness:** document `--min-chars`, `--thin-chars`,
|
|
1046
|
+
`--max-doc-chars`, and `--limit` in the flag reference; fix the
|
|
1047
|
+
`--only fatals` bullet to "needs `--audit` or `--sweep`" (it
|
|
1048
|
+
currently contradicts the `--sweep` bullet one line over).
|
|
1049
|
+
- [ ] **CLAUDE.md's exception taxonomy** lists twelve structural repairs
|
|
1050
|
+
and three lossy strips but omits the four safe opt-ins
|
|
1051
|
+
(`--fix-ids`, `--add-img-alt`, `--strip-bad-attrs`,
|
|
1052
|
+
`--escape-unknown-entities`), which reads as a complete inventory.
|
|
1053
|
+
|
|
1054
|
+
### Completeness verdict from the sweep
|
|
1055
|
+
|
|
1056
|
+
*The engine is close to complete for its charter: the five-fixture core is
|
|
1057
|
+
solid under adversarial attack (zip machinery, splice engine, entity
|
|
1058
|
+
handling, and the gate all held), the suite is fast, deterministic, and about
|
|
1059
|
+
95% load-bearing, and the docs are truthful everywhere except spec.md's
|
|
1060
|
+
never-absorbed v0.3/v0.4 era. The gap between "almost as complete as it can
|
|
1061
|
+
be" and "done" is one coherent theme, not scattered work: several opt-in
|
|
1062
|
+
repairs trust their regexes where they should trust the parse (the
|
|
1063
|
+
anchored-tag cluster), the audit's newest machinery (archive/spine) is not
|
|
1064
|
+
wired into every mode it promises, and the apply path's failure modes (crash
|
|
1065
|
+
mid-run, backup clobber, daemon stall) are unpadded. Fix the four Bug
|
|
1066
|
+
Reports entries and the anchored-regex cluster before any further features;
|
|
1067
|
+
everything else is backlog, not danger.*
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import java.io.File;
|
|
2
|
+
import java.io.PrintWriter;
|
|
3
|
+
import java.io.StringWriter;
|
|
4
|
+
import java.util.Scanner;
|
|
5
|
+
import java.util.ArrayList;
|
|
6
|
+
import java.util.List;
|
|
7
|
+
import com.adobe.epubcheck.api.EpubCheck;
|
|
8
|
+
import com.adobe.epubcheck.reporting.CheckingReport;
|
|
9
|
+
|
|
10
|
+
public class FastSweepExtract {
|
|
11
|
+
public static void main(String[] args) throws Exception {
|
|
12
|
+
Scanner scanner = new Scanner(System.in);
|
|
13
|
+
List<String> paths = new ArrayList<>();
|
|
14
|
+
while (scanner.hasNextLine()) {
|
|
15
|
+
String path = scanner.nextLine();
|
|
16
|
+
if (!path.trim().isEmpty()) paths.add(path);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
paths.parallelStream().forEach(path -> {
|
|
20
|
+
File epub = new File(path);
|
|
21
|
+
if (!epub.exists()) return;
|
|
22
|
+
try {
|
|
23
|
+
StringWriter sw = new StringWriter();
|
|
24
|
+
PrintWriter out = new PrintWriter(sw);
|
|
25
|
+
CheckingReport report = new CheckingReport(out, epub.getName());
|
|
26
|
+
EpubCheck check = new EpubCheck(epub, report);
|
|
27
|
+
check.doValidate();
|
|
28
|
+
out.flush();
|
|
29
|
+
String text = sw.toString();
|
|
30
|
+
if (report.getErrorCount() > 0 || report.getFatalErrorCount() > 0 || report.getWarningCount() > 0) {
|
|
31
|
+
// Extract error codes (e.g. RSC-005, CSS-008)
|
|
32
|
+
java.util.regex.Matcher m = java.util.regex.Pattern.compile("([A-Z]{3}-\\d{3})").matcher(text);
|
|
33
|
+
List<String> codes = new ArrayList<>();
|
|
34
|
+
while (m.find()) {
|
|
35
|
+
codes.add(m.group(1));
|
|
36
|
+
}
|
|
37
|
+
synchronized(System.out) {
|
|
38
|
+
System.out.println(epub.getCanonicalPath() + " ||| " + String.join(",", codes));
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
} catch (Exception e) {}
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
javac --release 25 -cp "/home/bdkl/.local/opt/epubcheck-5.3.0/epubcheck.jar:/home/bdkl/.local/opt/epubcheck-5.3.0/lib/*" FastSweepExtract.java
|
|
3
|
+
find "$HOME/docs/Calibre Library" -type f -name "*.epub" > all_epubs.txt
|
|
4
|
+
java -cp ".:/home/bdkl/.local/opt/epubcheck-5.3.0/epubcheck.jar:/home/bdkl/.local/opt/epubcheck-5.3.0/lib/*" FastSweepExtract < all_epubs.txt > raw_errors.txt
|
|
5
|
+
python3 -c '
|
|
6
|
+
import sys
|
|
7
|
+
from collections import Counter, defaultdict
|
|
8
|
+
|
|
9
|
+
error_counts = Counter()
|
|
10
|
+
file_errors = defaultdict(set)
|
|
11
|
+
|
|
12
|
+
with open("raw_errors.txt") as f:
|
|
13
|
+
for line in f:
|
|
14
|
+
line = line.strip()
|
|
15
|
+
if " ||| " not in line: continue
|
|
16
|
+
path, codes_str = line.split(" ||| ", 1)
|
|
17
|
+
if not codes_str: continue
|
|
18
|
+
codes = codes_str.split(",")
|
|
19
|
+
for c in codes:
|
|
20
|
+
error_counts[c] += 1
|
|
21
|
+
file_errors[c].add(path)
|
|
22
|
+
|
|
23
|
+
print("TOP ERRORS:")
|
|
24
|
+
top = error_counts.most_common(500)
|
|
25
|
+
for c, count in top:
|
|
26
|
+
print(f"{c}: {count}")
|
|
27
|
+
|
|
28
|
+
with open("top_errors.txt", "w") as out:
|
|
29
|
+
for c, count in top:
|
|
30
|
+
out.write(f"{c}: {count}\n")
|
|
31
|
+
# Save a few candidate files for each error
|
|
32
|
+
for f in list(file_errors[c])[:5]:
|
|
33
|
+
out.write(f" {f}\n")
|
|
34
|
+
' > sweep_summary.txt
|
|
@@ -148,9 +148,9 @@ def install_format(
|
|
|
148
148
|
With a book id, the repaired file is placed in the book's directory —
|
|
149
149
|
an atomic replace over the catalogued file when one exists (same path,
|
|
150
150
|
same ``data.name``; Calibre's layout never changes) — and the ``data``
|
|
151
|
-
row follows through ``WritableCalibreDB``: ``
|
|
152
|
-
|
|
153
|
-
|
|
151
|
+
row follows through ``WritableCalibreDB``: ``set_format`` (cquarry
|
|
152
|
+
1.17's sanctioned remove+add in one transaction) when the format
|
|
153
|
+
exists, a fresh ``add_format`` otherwise. The
|
|
154
154
|
row's size stays truthful and the book lands in ``metadata_dirtied``, so
|
|
155
155
|
Calibre regenerates its sidecar .opf. Files are the caller's
|
|
156
156
|
responsibility in cquarry; they are placed here, atomically, before the
|
|
@@ -210,11 +210,12 @@ def install_format(
|
|
|
210
210
|
if row is not None:
|
|
211
211
|
atomic_replace(target, new_file)
|
|
212
212
|
placed = True
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
)
|
|
213
|
+
# cquarry 1.17's set_format is the sanctioned remove+add;
|
|
214
|
+
# the explicit two-call batch this used to compose is
|
|
215
|
+
# retired with it.
|
|
216
|
+
wdb.set_format(
|
|
217
|
+
int(calibre_id), "EPUB", row["name"], new_file.stat().st_size
|
|
218
|
+
)
|
|
218
219
|
else:
|
|
219
220
|
book = wdb.conn.execute(
|
|
220
221
|
"SELECT path FROM books WHERE id = ?", (int(calibre_id),)
|
|
@@ -203,17 +203,18 @@ class TestInstallFormat(unittest.TestCase):
|
|
|
203
203
|
|
|
204
204
|
def test_replace_survives_a_database_error(self):
|
|
205
205
|
# A database failure must never lose the repair: the file is saved in
|
|
206
|
-
# place and the warning says the row is stale.
|
|
207
|
-
#
|
|
206
|
+
# place and the warning says the row is stale. set_format (cquarry
|
|
207
|
+
# 1.17's sanctioned remove+add) fails and rolls its own transaction
|
|
208
|
+
# back, so the old row survives untouched.
|
|
208
209
|
import contextlib
|
|
209
210
|
import io
|
|
210
211
|
|
|
211
|
-
with mock.patch("cquarry.write.WritableCalibreDB.
|
|
212
|
+
with mock.patch("cquarry.write.WritableCalibreDB.set_format") as m:
|
|
212
213
|
m.side_effect = sqlite3.OperationalError("database is locked")
|
|
213
214
|
with contextlib.redirect_stderr(io.StringIO()):
|
|
214
215
|
install_format(self.epub, self.new, CalibreIdResolver(self.root))
|
|
215
216
|
self.assertEqual(self.epub.read_bytes(), b"REPAIRED")
|
|
216
|
-
#
|
|
217
|
+
# set_format rolled back: the old row survives untouched
|
|
217
218
|
self.assertEqual(self._rows(), [("EPUB", "Title - Author", 10)])
|
|
218
219
|
|
|
219
220
|
def test_guess_fallback_uses_calibre_dbpath(self):
|
|
@@ -4,7 +4,7 @@ requires-python = ">=3.14"
|
|
|
4
4
|
|
|
5
5
|
[[package]]
|
|
6
6
|
name = "bindery-cli"
|
|
7
|
-
version = "0.
|
|
7
|
+
version = "0.31.0"
|
|
8
8
|
source = { editable = "." }
|
|
9
9
|
dependencies = [
|
|
10
10
|
{ name = "cquarry" },
|
|
@@ -19,7 +19,7 @@ reserialize = [
|
|
|
19
19
|
|
|
20
20
|
[package.metadata]
|
|
21
21
|
requires-dist = [
|
|
22
|
-
{ name = "cquarry", specifier = ">=1.
|
|
22
|
+
{ name = "cquarry", specifier = ">=1.17.0" },
|
|
23
23
|
{ name = "html5lib", marker = "extra == 'reserialize'", specifier = ">=1.1" },
|
|
24
24
|
{ name = "tqdm" },
|
|
25
25
|
{ name = "vir-tui", specifier = ">=2.2.0" },
|
|
@@ -37,11 +37,11 @@ wheels = [
|
|
|
37
37
|
|
|
38
38
|
[[package]]
|
|
39
39
|
name = "cquarry"
|
|
40
|
-
version = "1.
|
|
40
|
+
version = "1.17.0"
|
|
41
41
|
source = { registry = "https://pypi.org/simple" }
|
|
42
|
-
sdist = { url = "https://files.pythonhosted.org/packages/
|
|
42
|
+
sdist = { url = "https://files.pythonhosted.org/packages/4d/6b/e2c80511bf62e3b008c52d47a05b29e43e2782614cf211aadc8c965a2be1/cquarry-1.17.0.tar.gz", hash = "sha256:07ec6c43ebc11f8921645a963373537ef4808985c88ad885de5dfc05ba08d737", size = 197288, upload-time = "2026-09-09T21:48:07.208Z" }
|
|
43
43
|
wheels = [
|
|
44
|
-
{ url = "https://files.pythonhosted.org/packages/
|
|
44
|
+
{ url = "https://files.pythonhosted.org/packages/4d/5f/90f6fe1c24a3ce8616cb377cbedee3dfb82c4eca84cbb1ec8b02a7034cb1/cquarry-1.17.0-py3-none-any.whl", hash = "sha256:f01c5b4ecb0863c78379e9fc899fd0da7d2518e815ed2a9000acec09cbea054b", size = 70998, upload-time = "2026-09-09T21:48:05.798Z" },
|
|
45
45
|
]
|
|
46
46
|
|
|
47
47
|
[[package]]
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|