Audiobook library automation — staging, metadata, uploads, and collection management
Automates the pipeline from Libation audiobook downloads to MAM-ready torrents seeding in qBittorrent
Features • Pipeline • Installation • Usage • Audiobookshelf • Development
| Libation Integration | Trigger scans via libationcli in Docker with automatic book discovery |
| Smart Staging | Hardlink files to upload workspace with MAM-compliant naming (≤225 chars, automatic truncation with hash suffix) |
| Japanese Transliteration | Auto-converts Japanese author names using pykakasi with intelligent romanization |
| Metadata Enrichment | Fetch from Audnex API + MediaInfo with series/volume detection |
| Torrent Creation | Uses mkbrr in Docker with configurable presets and piece sizes |
| qBittorrent Upload | Auto-add torrents with category/tags, ready for cross-seeding |
| Production-Grade Retry | Powered by tenacity with exponential backoff and jitter |
| Robust State Tracking | Atomic writes, automatic backups, stale detection, and checkpoint recovery |
| Audiobookshelf Import | Direct library import with duplicate detection and quality-based trumping |
| Type-Safe Architecture | Strict typing with Pydantic v2 models and mypy verification |
graph LR
A[📖 Libation Scan] --> B[🔍 Discover New]
B --> C[📦 Stage/Hardlink]
C --> D[📋 Metadata]
D --> E[🧲 mkbrr]
E --> F[⬆️ qBittorrent]
F --> G[📚 Audiobookshelf]
Pipeline Details
| Stage | Description | Command |
|---|---|---|
| Scan | Check Audible for new purchases | shelfr libation scan |
| Discover | Find new audiobooks not yet processed | shelfr libation list |
| Stage | Hardlink files with MAM-compliant naming | shelfr tools prepare |
| Metadata | Generate MAM JSON (standalone) | shelfr tools mamff <path> |
| Full Pipeline | Run all steps end-to-end | shelfr run |
| Import | Import to Audiobookshelf (optional) | shelfr abs import |
Note
Repo name is shelfr; the app name/CLI is shelfr.
# Clone the repo
git clone https://github.com/H2OKing89/shelfr.git shelfr
cd shelfr
# Create virtual environment
python -m venv .venv
# Linux/macOS (bash/zsh)
source .venv/bin/activate
# Windows PowerShell
# .\.venv\Scripts\Activate.ps1
# Install in development mode
pip install -e ".[dev]"
# Copy config templates
mkdir -p config
cp config.yaml.example config/config.yaml
cp .env.example config/.env
# Edit with your settings
$EDITOR config/.env config/config.yaml| Requirement | Version | Notes |
|---|---|---|
| Python | 3.11+ |
Required |
| Docker | Latest | For Libation and mkbrr containers |
| qBittorrent | 4.x+ |
With Web UI enabled |
| mediainfo | Latest | CLI tool for audio metadata 1 |
1 Runs on host, not inside Docker.
shelfr uses layered configuration with automatic validation.
Tip
Precedence: config.yaml > .env > defaults
Put secrets in .env, everything else in config.yaml.
config/.env — Secrets only (never commit)
# qBittorrent credentials (REQUIRED)
QB_HOST=http://192.168.1.100:8080
QB_USERNAME=admin
QB_PASSWORD=secret
# Audiobookshelf (only needed for abs import command)
AUDIOBOOKSHELF_HOST=https://abs.example.com
AUDIOBOOKSHELF_API_KEY=your-api-token-here
# Optional runtime settings
Shelfr_ENV=production
LOG_LEVEL=INFO[!NOTE] Docker/Libation settings (
LIBATION_CONTAINER,DOCKER_BIN,TARGET_UID,TARGET_GID) belong inconfig.yaml'senvironment:section, not here.
config/config.yaml — Paths and settings
# Docker/Libation settings (preferred location over .env)
environment:
libation_container: "Libation"
docker_bin: "/usr/bin/docker"
target_uid: 99
target_gid: 100
paths:
library_root: "/mnt/user/data/audio/LibationLibrary"
seed_root: "/mnt/user/data/seedvault/audiobooks"
torrent_output: "/mnt/user/data/downloads/torrents/torrentfiles"
# Optional: override the default XDG locations (platformdirs)
# state_file: "./data/processed.json"
# log_file: "./logs/shelfr.log"
mam:
max_filename_length: 225
allowed_extensions: [".m4b", ".jpg", ".jpeg", ".png", ".pdf", ".cue"]
filters:
# Note: remove_phrases and author_map live in config/naming.json
remove_book_numbers: true
transliterate_japanese: true
naming:
# Optional: "H2OKing" -> appends "[H2OKing]" to folder names
ripper_tag: "H2OKing"
mkbrr:
image: "ghcr.io/autobrr/mkbrr:latest"
preset: "mam"
host_data_root: "/mnt/user/data"
container_data_root: "/data"
qbittorrent:
category: "mam-audiobooks"
tags: ["shelfr"]
auto_start: true
auto_tmm: false
save_path: ""
audnex:
base_url: "https://api.audnex.us"
timeout_seconds: 30
regions: ["us"]Naming rules — config/naming.json
Naming rules control title/subtitle normalization and filtering used by the naming pipeline (e.g., phrases to remove, author mappings). See config/naming.json for the full example.
format_indicators: phrases to remove from titles/subtitles (replaces oldremove_phrases)author_map: explicit foreign name → romanized name mappingsgenre_tags: genre suffixes to strip from titles/subtitlesseries_suffixes: regex patterns to trim from series namessubtitle_patterns: remove/keep subtitle patterns and related optionssubtitle_redundancy_rules: rules to drop redundant subtitlespreserve_exact: exact titles that bypass all normalization
config/categories.json — MAM genre mappings
Maps audiobook genres to MAM category IDs:
{
"fantasy": 39,
"science fiction": 40,
"mystery": 41
}Environment variables — XDG path overrides
shelfr uses XDG-compliant paths by default (via platformdirs):
# Override default data directory (for state files)
# Default: ~/.local/share/shelfr (Linux), ~/Library/Application Support/shelfr (macOS)
export SHELFR_DATA_DIR="/mnt/cache/appdata/shelfr/data"
# Override default cache directory
# Default: ~/.cache/shelfr (Linux), ~/Library/Caches/shelfr (macOS)
export SHELFR_CACHE_DIR="/mnt/cache/appdata/shelfr/cache"
# Override default log directory
# Default: ~/.local/state/shelfr (Linux), ~/Library/Logs/shelfr (macOS)
export SHELFR_LOG_DIR="/mnt/cache/appdata/shelfr/logs"[!NOTE] Explicitly configured paths in
config.yamlalways take precedence over environment variables.
shelfr run # Run everything
shelfr run --skip-scan # Skip Libation scan
shelfr run --skip-metadata # Skip metadata fetching
shelfr --dry-run run # Preview without changes# Libation commands
shelfr libation scan # Check for new Audible purchases
shelfr libation scan --liberate # Scan and download new books
shelfr libation list # List audiobooks in library
shelfr libation list --pending # List pending downloads
# Staging and tools
shelfr tools prepare # Stage files (hardlink + rename)
shelfr tools mamff /path/to/release # Generate MAM JSON
# Full pipeline runs everything: scan → prepare → metadata → torrent → upload
shelfr runshelfr state list # View all processed entries
shelfr state list --failed # Show only failed entries
shelfr state prune # Remove stale entries (missing files)
shelfr state retry <asin-or-id> # Clear failed status for retry
shelfr state clear <asin-or-id> # Remove entry completelyshelfr status # Show processing statistics
shelfr config # Debug: print loaded config
shelfr validate # Validate configuration
shelfr check-duplicates # Find potential duplicate releases| Option | Description |
|---|---|
--dry-run |
Preview without making changes |
-v, --verbose |
Enable DEBUG logging |
-c, --config PATH |
Custom config.yaml path |
-V, --version |
Show version |
Important
Global options like --dry-run must come before the subcommand:
shelfr --dry-run abs import # Correct
shelfr abs import --dry-run # Won't workshelfr supports importing audiobooks directly to Audiobookshelf libraries with duplicate detection and quality-based replacement (trumping).
shelfr abs init # Initialize ABS connection
shelfr abs import # Import staged books to ABS library
shelfr abs check-asin B0ASIN123 # Check if ASIN exists
shelfr abs trump-preview # Preview trumping decisions
shelfr abs cleanup # Clean orphaned files
shelfr abs restore # List/restore archived booksshelfr can optionally generate a Calibre-compatible metadata.opf sidecar inside each imported book folder. This is useful for Audiobookshelf metadata ingestion (especially series detection via Calibre-style meta fields).
Enable via config:
# config/config.yaml
audiobookshelf:
import:
generate_opf_sidecar: trueOr enable per-run with CLI flags:
shelfr abs import --opf # enable
shelfr abs import --no-opf # disable
shelfr --dry-run abs import --opf # combine with global flagsWhen enabled, trumping automatically replaces lower-quality audiobooks with higher-quality versions:
# config/config.yaml
audiobookshelf:
enabled: true
host: "http://localhost:13378"
api_key: "your-api-key"
import:
trumping:
enabled: true
aggressiveness: balanced # conservative | balanced | aggressive
min_bitrate_increase_kbps: 64
archive_root: "/mnt/user/data/audio/archive"Quality Hierarchy & Trumping Decisions
Format Ranking: m4b > m4a > opus > mp3 > flac (for audiobooks)
[!NOTE] FLAC is ranked lowest because speech doesn't benefit from lossless encoding, FLAC lacks chapter support, and file sizes are significantly larger.
Trumping Decisions:
| Decision | Action |
|---|---|
| REPLACE_WITH_NEW | New file is better → archive old, import new |
| KEEP_EXISTING | Existing is equal or better → skip import |
| KEEP_BOTH | Incomparable (different language) → defer to policy |
| REJECT_NEW | New is worse quality → skip entirely |
shelfr uses a modular architecture with clean separation of concerns:
shelfr/
├── src/shelfr/
│ ├── cli.py # CLI parser + main entry point
│ ├── config.py # Configuration loading
│ ├── models.py # Pydantic data models
│ ├── workflow.py # Pipeline orchestration
│ │
│ ├── commands/ # CLI command handlers
│ │ ├── core.py # scan, discover, prepare, etc.
│ │ ├── utility.py # status, check, validate
│ │ ├── diagnostics.py # dry-run, check-duplicates
│ │ ├── state.py # state list/prune/retry/clear
│ │ └── abs.py # Audiobookshelf commands
│ │
│ ├── abs/ # Audiobookshelf integration
│ │ ├── client.py # ABS API client
│ │ ├── importer.py # Import workflow
│ │ └── asin.py # ASIN extraction/resolution
│ │
│ ├── utils/
│ │ ├── naming/ # Modular naming system
│ │ │ ├── filters.py # Title/series filtering
│ │ │ ├── mam_paths.py # MAM path building
│ │ │ ├── normalization.py# Book normalization
│ │ │ └── ... # 8 focused modules
│ │ ├── cmd.py # sh-library subprocess wrapper
│ │ ├── retry.py # tenacity-powered retries
│ │ ├── state.py # State management (v2 schema)
│ │ └── paths.py # Host↔container path mapping
│ │
│ └── schemas/ # Pydantic schemas
│ ├── config.py # Configuration validation
│ └── state.py # State file schema v2
│
├── config/ # Configuration (gitignored)
├── docs/ # Technical documentation
│ ├── archive/ # Completed implementation reports
│ └── audiobookshelf/ # ABS integration guides
├── tests/ # Comprehensive test suite
└── pyproject.toml # Project configuration
Recent Architecture Improvements (December 2025)
| Area | Change |
|---|---|
| CLI Split | cli.py reduced from 4,100 → 820 lines via commands/ subpackage |
| Naming Refactor | naming.py split into 9 focused modules for maintainability |
| State Hardening | Schema v2 with atomic writes, checkpoints, and backup recovery |
| Production Deps | Replaced custom code with tenacity, platformdirs, and sh library |
See docs/README.md for the full documentation layout.
# Install dev dependencies
pip install -e ".[dev]"
# Run tests
pytest
# Run tests with coverage
pytest --cov=src/shelfr --cov-branch --cov-report=term
# Lint
ruff check src/
# Format
ruff format src/
# Type check
mypy src/
# Run all checks (pre-commit)
pre-commit run --all-filesshelfr uses pre-commit for automated code quality:
# .pre-commit-config.yaml (excerpt)
repos:
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.8.0
hooks:
- id: ruff
- id: ruff-format
- repo: local
hooks:
- id: mypy
name: mypy type checking
entry: mypy
language: system
types: [python]
- id: pytest
name: pytest unit tests
entry: pytest
language: system
types: [python]
shelfr is built on top of excellent open-source projects and services:
|
Audiobook metadata |
Self-hosted streaming |
Torrent creation |
BitTorrent client |
|
Audible manager |
Media analysis |
Data validation |
Terminal formatting |
MIT © 2024-2025