Declarative tag normalization and Audiobookshelf-friendly folder reorganization for personal audiobook libraries. One source of truth (your catalog file) drives both the embedded tags on the audio files and the on-disk folder tree, so the two can't drift.
The library shipped with this repository is the author's; the engine is
generic. Bring your own catalog/books.py and the tools will normalize
yours.
- Declarative. You describe each audiobook once in
catalog/books.py;retagandreorgderive the embedded metadata and the folder layout from the same dict. - Reversible. Every apply pass writes a crash-safe backup of the prior
tag state (
backup.json, flushed per file, one-deep) and a manifest of every completed move (reorg-manifest.json, flushed per move). Both operations have explicit--restore/--reversemodes, both undos have a--dry-runpreview, and a restore snapshots the current tags first so it can itself be undone. - Idempotent. A dry run after an apply reports zero remaining changes.
- Dry-run by default. Nothing is written unless you pass
--apply. - In place. Tags are edited via mutagen with no audio re-encode; cover art and m4b chapter tracks are preserved.
- MP4 and ID3. Writes both Apple-style atoms (
@nam,aART,----:com.apple.iTunes:SERIES,stik=2) and ID3 frames (TIT2,TPE1,TXXX:SERIES,COMM) to the matching files.
Requires Python 3.10+ and uv (or pip).
git clone https://github.com/VirInvictus/AudiobookTools.git
cd AudiobookTools
uv pip install -e .Or, without installing, run any subcommand via uv run:
uv run --with mutagen python -m audiobooktools.cli retag --helpAssuming your library is at /path/to/Audiobooks and your catalog file is at
./catalog/books.py:
# Plan: show every tag change that would be made; write nothing.
audiobooktools retag --library /path/to/Audiobooks
# Execute: back up the prior state, then write the new tags.
audiobooktools retag --library /path/to/Audiobooks --apply
# Undo: restore from the backup file the apply pass wrote.
audiobooktools retag --restore /path/to/Audiobooks/.audiobooktools/backup.jsonThe same dry/apply/reverse triple applies to reorg:
audiobooktools reorg --library /path/to/Audiobooks # dry run
audiobooktools reorg --library /path/to/Audiobooks --apply # execute
audiobooktools reorg --reverse /path/to/Audiobooks/.audiobooktools/reorg-manifest.jsonShort alias abt is registered for both:
abt retag --library /path/to/Audiobooks
abt reorg --library /path/to/Audiobooks --applyFour commands introspect the catalog and library without writing anything.
# Summary: books, on-disk vs. catalogued, total runtime, unmatched files,
# catalogue entries not present on disk, near-match hints for path typos,
# and possible series-index gaps.
abt status --library /path/to/Audiobooks
# Lint the catalog: missing covers (embedded art counts), unused DESC keys,
# duplicate/mismatched/mixed-padding series indices, junk narrators,
# implausible years. Exits non-zero on findings.
abt validate --library /path/to/Audiobooks
# Draft catalog entries for new books from their existing tags (scans
# <library>/Unfiltered by default), ready to review and paste into books.py.
abt discover --library /path/to/Audiobooks
# Dump one file's physical tags (read-only; doubles as the
# malformed-file diagnostic).
abt tags "/path/to/Audiobooks/Andy Weir/Project Hail Mary/Project Hail Mary.m4b"A collapsed reorg preview (one line per destination directory instead of every file):
abt reorg --library /path/to/Audiobooks --diffAudiobooks/
├── Andy Weir/
│ └── Project Hail Mary/
│ ├── Project Hail Mary.m4b
│ └── cover.jpg
├── Joe Abercrombie/
│ ├── The First Law/
│ │ ├── 1 - The Blade Itself/
│ │ ├── 2 - Before They Are Hanged/
│ │ └── 3 - Last Argument of Kings/
│ └── The Devils/ # lone series entry stays flat
└── Steven Erikson/
└── Malazan Book of the Fallen/
└── 1 - Gardens of the Moon/
A series gets its own subfolder when 2+ books of that series are owned;
standalones and lone series entries go directly under the author. Names are
NTFS-safe (illegal characters stripped, ": " → " - ").
catalog/books.py exports BOOKS: list[dict]. Each dict describes one
audiobook (or one volume of a series). The shape is documented in full in
spec.md; the short version:
from audiobooktools.schema import single, GENRE
from catalog.descriptions import DESC
BOOKS = [
single(
"Andy Weir - Project Hail Mary", # source folder
"Project Hail Mary.m4b", # source filename
title="Project Hail Mary",
subtitle="",
author="Andy Weir",
narrator="Ray Porter",
album="Project Hail Mary",
year="2021", # audio edition year
series="",
series_index="",
description=DESC["project_hail_mary"], # optional
),
# ... more entries
]Starting fresh? Copy catalog-template/ to catalog/
next to your library and replace the example entries with your own.
See docs/curation-rules.md for the editorial
conventions the shipped catalog follows (year = recording year, high-marks
narrator policy, etc).
- Never deletes audio.
reorgmoves and renames; it never removes a source file and never renames onto an occupied path. The one removal it performs is now-empty source directories, after their files have moved out. Removing books from the shelf is an explicit user action. - Never re-encodes. Tags are edited in place via mutagen; the audio stream is untouched.
- No remote sync by default. AudiobookTools is local-first. It produces
an Audiobookshelf-shaped tree, and the only network call in the codebase
is the opt-in rescan push (
reorg --apply --abs-url ...), which asks a configured Audiobookshelf server to rescan and never runs in a dry run.
audiobooktools/ schema (data model, extension sets), retag + reorg
engines, report (status/validate), discover, tags dump, CLI
catalog/ user data: BOOKS list + DESC descriptions
docs/ workflows, curation rules
tests/ unit + synthetic end-to-end tests (idempotency, undo)
See spec.md for the data contract, roadmap.md
for what's done and what's next, and patchnotes.md for the
release log.
MIT. See LICENSE.
If AudiobookTools is useful to you and you'd like to chip in:
- liberapay · liberapay.com/bdkl
- bitcoin
bc1qkge6zr45tzqfwfmvma2ylumt6mg7wlwmhr05yv