The crash-safety release. The two HIGH findings that let a mid-run failure break the safety identity are closed, the never-deletes story's remaining holes are plugged, and the recorded 5.22 lane (the Audiobookshelf rescan push) ships. 111 tests, up from 71.
- Crash-safe apply journaling.
backup.jsonandreorg-manifest.jsonare rewritten atomically after every mutation instead of once at the end of the pass, so a crashed pass leaves a journal that reverses exactly what happened instead of losing the undo. An apply with nothing to change still leaves the existing journal alone. The journal is one-deep, and the depth is now documented (decision 2026-09-15). - Malformed-audio guard. A corrupt file raises
MutagenErrorper file (warn, skip, continue) inretag,discover, andrestore, instead of aborting the whole run mid-pass. restore()works on headerless MP3s (it now saves with an explicit path; the old argument-lesssave()raisedTypeErrormid-restore), skips missing or unreadable entries with a warning, and returns a non-zero exit code when it had to skip.narrator: Nonepreserves companion-key credits. A file carrying the narrator only in the freeform key (TXXX:NARRATOR/----:NARRATOR) no longer gets both slots blanked behind--apply.reorg --reverserefuses occupied paths instead of silently replacing whatever arrived at the original staging path, and returns a non-zero exit code on refusals.- Both engines pre-flight catalog paths. Absolute paths and
..escapes are refused before any work, instead of crashing mid-apply (retag) or after all renames with no manifest (reorg).
- Audiobookshelf rescan push (the 5.22 lane).
reorg --apply --abs-url ...asks a configured Audiobookshelf server to rescan its libraries: opt-in, best-effort (a failed push never fails the completed moves), stdliburllibonly, and it never fires in a dry run. The spec's absolute no-network clause is amended at both recorded sites. retag --fail-on-change: a dry run that exits non-zero when changes are pending. The idempotency property becomes a schedulable drift monitor and the natural pre-flight for the rescan push.- Undo previews and a pre-restore snapshot.
--restoreand--reverseaccept--dry-run(per-file plan, nothing written); a real restore first snapshots the current tags topre-restore.json, so restoring from the wrong backup is itself reversible. abt tags FILE: a read-only dump of one file's physical tags that doubles as the malformed-file diagnostic (unreadable files print the mutagen error cleanly and exit 1).--book NAMEscoping (repeatable, case-insensitive) forretagandreorg: landing one book no longer costs four full-library passes. The help notes the sharp edge: a scoped--applyoverwrites the one-deep undo journal with just the scoped files.- validate riders: a series-index padding-consistency lint (mixed
padding mis-orders series folders), and embedded cover art now counts
toward
missing_covers(13 false positives gone on the real library). - status cross-reference: pairs not-on-disk entries with nearly named unclaimed folders, the signature of a path typo in the catalog.
- discover warns when a multi-file set's tags disagree with the drafted first-file values, so a wrong first track no longer propagates silently.
python -m audiobooktoolsworks (__main__.py); both console scripts already did.
- One home for the extension constants:
schema.AUDIO_EXT(with.mp4now visible to every engine),schema.MP4_EXT,schema.IMAGE_EXT; the four drifted per-module copies are gone. Thediscslayout accepts non-mp3 tracks instead of hardcoding*.mp3. - Chapter titles keep legitimate trailing numbers ("05 Episode 42" stays "Episode 42"); only the leading track number is stripped.
series_gapsno longer lets fractional indices fill whole-number slots ("2.5" used to mask a missing book 2).- The single-layout fallback only fires when a folder holds exactly one audio file, so a missing named file can never silently tag another book's file.
load_catalogevicts thecatalogpackage between loads, so two catalogs in one process no longer share the first catalog'sDESC.
.gitignoreis a lean project-specific file (the stock template's trap rules like*.logandlib/are gone) and ignores.audiobooktools/;.gitattributesmarkscatalog/**linguist-vendored so the author's data stops counting as engine code.- Docs truth sweep: the spec's Restore-and-reverse section now states the
real journal contract (format, depth, timing, pre-restore snapshot,
occupied-path refusal, the unrecorded empty-dir prune), the dead
~/.claudepointers are re-pointed, workflows.md gains a read-only inspection section and marks the staging-vs-NAS section historical, the roadmap's PyPI phase reflects the pushed tag, and curation-rules drops the culled dramatized edition as an example.
- 111 tests, up from 71, including the end-to-end idempotency checks the docs promised (synthetic-library retag and reorg), crash-journal behavior under simulated mid-apply failures, malformed-file skips, the restore/reverse guards, the path pre-flights, and every new command.
Phase 2 (quality of life). Three read-only introspection commands and a collapsed reorg view. Nothing here writes to the library; every new command is safe to run at any time.
status— one-screen summary of the catalog against a library: book count and how many entries actually have files on disk, resolved file count and total runtime (summed from decoded durations, with unreadable files reported separately), catalogue entries not present on disk, audio files on disk that no entry claims, and possible series-index gaps.validate— lint pass over the catalog: missing covers, unusedDESCkeys, duplicateseries_indexvalues, subtitle/index disagreements (decimal.5indices compared numerically so they do not false-positive), empty or junk narrator strings, and implausibleyearvalues. Exits non-zero when it finds anything, so it can gate CI; a clean catalog exits 0.discover— scans a directory (default<library>/Unfiltered) and prints paste-ready draft entries pre-filled from each file's existing tags. Layout is inferred from the folder shape; multi-file layouts are emitted with a review marker. Dedups against a discoverable catalog, so it surfaces only new books. This also covers the "catalog autocompletion" idea: point--pathat one new folder to draft that single entry.
reorg --diff— a collapsed dry run: one line per destination directory with its file count, instead of enumerating every move.
- New
audiobooktools.report(status + validate, with the mechanical checks factored into pure, unit-tested helpers) andaudiobooktools.discover(tag reading, layout inference, and draft rendering). reorg.rungains adiffkeyword;reorg.print_diff_summaryis the collapsed printer.
- New tests across
test_report.py,test_discover.py,test_release_notes.py, and a--diffcase intest_reorg.py(71 total, up from 40).
- CI. GitHub Actions runs ruff (lint + format check) and pytest on Python
3.10–3.13 for every push and PR, plus a build job that fails if the author's
catalog/ever leaks into a distribution. - Distribution hygiene. The wheel is engine-only and the sdist now excludes
catalog/, so personal library data never ships. Verified against a realuv build. - Runtime version source.
audiobooktools.__version__resolves from installed-package metadata first (importlib.metadata) and falls back to the repo'sVERSIONfile, so wheel installs report the real version instead of0.0.0+unknown(theVERSIONfile does not ship in the wheel). - Release automation.
scripts/release_notes.pyextracts a version's section from this changelog; a dormantpublish.ymluses it to cut a GitHub Release and publish to PyPI (trusted publishing) when avX.Y.Ztag is pushed. Inert until a PyPI trusted publisher is configured. - Catalog template.
catalog-template/is a minimal, copyable starting point for a freshcatalog/.
- Typing and Linting: Fixed mypy typing issues by ignoring missing stub imports in
pyproject.toml. Code passes strict checks and is now fully formatted by Ruff. - Documentation: Added comprehensive docstrings to all functions in
cli.py,reorg.py,retag.py, andschema.py. - Robustness:
reorg.py: Improved collision checks during planning (_check) to safely detect and block operations where a destination file already exists and isn't being natively renamed or moved away in topological order.retag.py: Improvedresolve_filesrobustness around canonical vs staging paths.
- Standalone Book Promotion: Fixed a bug in
retag.py'sresolve_fileswhere a book previously added as a standalone (and correctly reorganized to its standalone path) would not be found if it was later promoted to a series (by adding a second book to the series). Added a new probe to check the standalone alternate path if the file has not yet been moved to the series folder.
- File Resolution Fallback: Fixed a critical bug in
retag.py'sresolve_files. Previously, if asinglelayout explicitly requested a file via thefileattribute and that file was missing from its staging directory, the tool would fall back to_top_audioand incorrectly overwrite the tags of the first unrelated audio file it found. It now correctly returns an empty list and prints a missing file warning.
Initial release. Extracts and formalizes the toolset that previously lived
inside the author's audiobook library as a private .retag/ directory.
- Two-engine design.
retagwrites embedded tags from the catalog;reorgderives the folder tree from the same catalog. Both are dry-run by default and idempotent. - Reversible. Each apply pass writes a backup (
backup.json) or manifest (reorg-manifest.json) that fully reverses the operation via--restore/--reverse. - Catalog separated from engine.
audiobooktools/is the engine;catalog/is the user's data. The CLI loads the catalog by file path so one engine install can drive many catalogs.
audiobooktools.schema—sanitize,owned_counts,book_dir,singleconvenience constructor,GENREconstant. Helpers now takebooksas a parameter instead of reading from module-level globals.audiobooktools.retag— refactored from the originalretag.py; the module-levelROOTconstant is gone;library_rootis threaded through the call chain. Programmatic entry point:retag.run(library_root, books, *, apply, verbose, backup_path).audiobooktools.reorg— same treatment.reorg.run(library_root, books, *, apply, manifest_path).audiobooktools.cli— top-level CLI withretag/reorgsubcommands, registered as bothaudiobooktoolsandabtconsole scripts viapyproject.toml. Discovery:--catalogdefaults to the nearestcatalog/books.pywalking up from$PWD;--librarydefaults to the catalog's grandparent if it looks like a library, else$PWD.
- Pruned to 54 currently-owned audiobook series-entries (from the prior 103-entry spec). DESC dictionary pruned from 74 to 30 entries to match.
- Six Pierce Brown Audible Studios m4bs onboarded as
Red Risingseries entries 1-6 (Tim Gerard Reynolds narration), replacing the priorRed Rising (Dramatized Adaptation)entry. - Past entries remain available in the original
.retag/spec.pyand the pre-extraction git history.
README.md— public-facing pitch and three-command tour.spec.md— full data contract: BOOK dict schema, layout vocabulary, MP4/ID3 tag mapping, folder scheme, idempotency guarantee.roadmap.md— Phase 1 (extraction) done, Phase 2 candidates listed.docs/workflows.md— operational walkthroughs (adding fromUnfiltered/, normal apply, restore, reverse).docs/curation-rules.md— high-marks principle, narrator-quality decisions, year = recording year.CLAUDE.md— project-local guidance for AI sessions.
- All pure-helper tests carried over with import updates.
tests/test_catalog_imports.py— smoke test that catalog imports cleanly and every entry is well-formed.- The original on-disk integration test (which poked the live library) is
replaced with a
tmp_path-based collision-detection test.