Files
audiobook-merge/docs/superpowers/specs/2026-08-26-audiobook-tui-design.md
2026-08-26 19:43:49 -04:00

254 lines
14 KiB
Markdown

# Portable Audiobook TUI Design
## Purpose
Build a portable Docker Compose project for manually processing audiobook sources on a headless server over SSH. The project converts one pending book at a time to M4B, requires explicit approval before destructive or expensive actions, and uses filesystem locations as workflow state.
## Success Criteria
- Launches through Docker Compose in an interactive SSH terminal without a desktop GUI.
- Processes a directory containing multiple MP3 files or one standalone MP3 into one M4B.
- Shows naturally sorted source tracks before conversion.
- Supports chapter names from embedded titles, filenames, or a validated `chapters.txt` file.
- Uses `m4b-tool` as the merge and conversion engine.
- Offers optional Audnexus enrichment through public unauthenticated endpoints and continues normally when Audnexus is disabled or unavailable.
- Requires typed confirmation before every conversion and before moving a source to review.
- Moves successful output and source into ready and done states without overwriting existing files.
- Ships as a documented project that can be copied to Enterprise-D and launched with Docker Compose.
## Runtime Architecture
Use one Compose service built from a pinned `sandreas/m4b-tool` base image. Add Python, the terminal application, and its pinned Python dependencies to that image. One container avoids mounting the Docker socket, coordinating worker containers, or reconciling permissions and signals across services. Compose remains the deployment boundary and can gain more services later without changing the user workflow.
Run the application as an ephemeral interactive container:
```bash
docker compose run --rm audiobook-tui
```
Compose must allocate stdin and a TTY. It maps the invoking server user's numeric UID and GID into the container so completed files do not become root-owned. The project supports Linux `amd64`; additional architectures are supported only when the selected pinned base image publishes them.
The image pin is exposed as a Docker build argument. Documentation must recommend a dated `m4b-tool` tag or immutable digest and explain how to test and intentionally upgrade it. `latest` is not the default because upstream describes it as potentially unstable.
## Host and Container Paths
Default host paths:
```text
/mnt/riker/alexandria/process/audiobook/
/mnt/riker/alexandria/process/audiobook-ready/
/mnt/riker/alexandria/process/audiobook-done/
/mnt/riker/alexandria/process/audiobook-review/
```
Compose maps these paths to stable container roots:
```text
/data/pending
/data/ready
/data/done
/data/review
```
Project-local persistent state maps to `/app/state` and contains logs and stale-work records. Temporary conversion data uses a configurable work directory. All configured roots are resolved before use. Inputs and destinations must remain beneath their expected root. Symlinks resolving outside those roots are rejected.
## Filesystem State Model
- **Pending:** source directory or supported loose source file exists under pending root.
- **Ready:** validated final `.m4b` exists under ready root.
- **Done:** original source has moved beneath done root after successful output validation.
- **Review:** source has moved beneath review root following explicit user confirmation.
No database tracks queue state. Directory contents remain source of truth.
A conversion writes `<name>.partial.m4b` under ready root. Final output appears only through atomic rename after validation. Existing final output is never overwritten. Name collisions require user to edit output name or cancel.
Source moves to done only after final output exists and passes validation. Same-filesystem moves use atomic rename. Cross-filesystem moves use copy to a temporary destination, verify copied tree, rename destination into place, then remove source. Failure before verified destination creation leaves source intact. Failure during source removal is reported as a reconciliation error and never treated as clean success.
## Queue and Input Discovery
Pending book units are:
- Immediate child directories containing supported audio files.
- Supported loose audio files directly beneath pending root; each loose file is one book.
Initial required input type is MP3. Scanner structure may allow later formats, but interface and tests promise MP3 only. Hidden files, partial outputs, and unsupported files are ignored with visible warnings where relevant.
Track order uses a deterministic natural sort over relative path names, case-insensitive with stable case-sensitive fallback. Before conversion, preview shows ordinal, relative filename, duration, embedded title, track number, disc number, and scan warnings. User cannot approve conversion until probing succeeds for every selected track.
## Terminal Interaction
Use a keyboard-driven Python terminal interface suitable for common SSH terminals. Application remains usable at 80 columns and does not depend on mouse input, browser access, or terminal graphics protocols.
Flow:
1. Queue screen lists pending books and state counts.
2. User selects one book.
3. Track preview shows sorted inputs and probe results.
4. Metadata screen offers local inference, optional Audnexus results, and manual field editing.
5. Chapter screen selects embedded titles, filenames, or `chapters.txt`.
6. Final preview shows metadata, cover, chapters, source and destination paths, warnings, and shell-escaped conversion command.
7. User must type `CONVERT` exactly to begin conversion.
8. On success, application validates output, finalizes ready file, and moves source to done.
Moving a book to review is separate from conversion and requires typing `REVIEW` exactly. Escape/back navigation and quit do not mutate state.
## Metadata
Local metadata precedence is:
1. User edits made in current session.
2. Accepted Audnexus result.
3. Consistent embedded tags.
4. Folder or filename inference.
Core editable fields are title, author, narrator, series, series position, year, description, and output filename. Cover selection prefers an explicitly chosen local image, then conventional local filenames such as `cover.jpg`, then an accepted Audnexus image. Remote cover download failure does not block conversion.
Metadata and generated files are staged in temporary work space. Original MP3 files are never retagged.
## Audnexus Policy
Audnexus support is optional and disabled through configuration. It calls only documented public unauthenticated book, search, author, chapter, and image-related data paths rooted at `https://api.audnex.us`. It does not call authenticated metrics endpoints and exposes no API-key configuration.
Requests use configurable short connect and read timeouts, identify this client, and avoid automatic unbounded retries. Network failure, invalid response, rate limiting, missing result, or service outage returns user to local/manual metadata without blocking processing. Responses are untrusted external data: validate types and lengths before display, filesystem use, or command construction.
If public endpoints later require payment, subscription, or credentials, README instructs user to disable Audnexus; application continues with local metadata.
## Chapter Modes
### Embedded Titles
Sorted track boundaries become chapter boundaries. Each chapter title comes from its source file's embedded title tag. Missing titles produce a visible validation error and must be corrected manually or by choosing another mode.
### Filenames
Sorted track boundaries become chapter boundaries. Chapter names come from basenames with file extension removed and conservative leading track-number punctuation removed. Preview always shows resulting title; no original file is renamed.
### Manual `chapters.txt`
Book directory contains UTF-8 `chapters.txt`, one chapter per nonblank line:
```text
00:00:00 Opening Credits
00:01:42.500 Chapter 1
01:14:09 Chapter 2
```
Parser accepts `HH:MM:SS` and `HH:MM:SS.mmm`. Timestamps must be nonnegative and strictly increasing, first timestamp must equal zero, every title must be nonempty, and every timestamp must precede probed total duration. Duplicate timestamps, malformed lines, invalid UTF-8, and chapters past duration block conversion with line-specific errors.
Implementation must inspect pinned `m4b-tool --help` during container verification. If pinned version provides reliable direct chapter-file ingestion, use it. Otherwise `m4b-tool` performs merge/conversion and bundled `ffmpeg` applies staged ffmetadata chapters to the converted temporary M4B. This fallback does not replace `m4b-tool` as conversion engine.
## Conversion Command
Command builder produces an argument vector and never invokes a shell. It passes selected sorted inputs, output path, metadata, cover, and configured encoding/job settings to `m4b-tool`. Final preview renders shell-escaped form for operator inspection only.
Multiple MP3 inputs merge in displayed order. Single MP3 input passes through same conversion workflow and produces M4B. Generated chapter metadata never changes source files.
Configuration exposes safe operational values such as job count and optional additional allowlisted `m4b-tool` settings. It does not accept an arbitrary command string.
## Validation and Recovery
Successful converter exit alone is insufficient. Output validation checks:
- Process exit status is zero.
- Partial output exists and has nonzero size.
- Media probe reads an audio duration greater than zero.
- Duration is within a small documented tolerance of source total.
- Expected chapter count and chapter titles are present.
Converter stdout and stderr stream into TUI and persistent timestamped log. Ctrl-C sends termination to child process, waits a bounded interval, then escalates termination if required. Interrupted or failed conversion leaves source pending and records partial/work artifacts.
On startup, stale partial files and work records are detected. Application shows exact paths and offers explicit cleanup or preservation. It never resumes an ambiguous conversion automatically and never deletes artifacts outside configured ready/work roots.
## Configuration
Ship:
- `compose.yaml` with default Enterprise-D bind mounts and TTY settings.
- `.env.example` for host path, UID, GID, image pin, and other deployment substitutions.
- `config.toml` for container paths, Audnexus toggle/timeouts, jobs, validation tolerance, and logging.
- Dockerfile with pinned Python dependencies layered onto pinned `m4b-tool` image.
Environment variables may override deployment-specific path and identity settings. Application settings stay in TOML. Startup validates configuration and reports all errors before scanning files.
## Code Boundaries
- `app/config.py`: load and validate TOML/environment configuration.
- `app/models.py`: immutable book, track, metadata, chapter, and conversion-plan types.
- `app/scanner.py`: discover queue items, natural-sort tracks, and probe media.
- `app/chapters.py`: build and validate all chapter modes.
- `app/metadata.py`: infer and normalize local metadata.
- `app/audnexus.py`: bounded public API client and response validation.
- `app/planner.py`: resolve safe destinations and build conversion argument vectors.
- `app/converter.py`: execute process, stream logs, handle signals, and validate output.
- `app/workflow.py`: finalize output and move sources between filesystem states.
- `app/tui.py`: screens, navigation, previews, and typed confirmations.
- `app/main.py`: application entry point and dependency assembly.
Modules communicate through explicit data types. Scanner, chapter builder, planner, converter, and workflow remain usable without TUI for tests.
## Testing Strategy
Unit tests cover:
- Natural sorting and discovery filtering.
- Probe result normalization.
- Metadata precedence and safe output names.
- Every `chapters.txt` validation rule.
- Embedded-title and filename chapter generation.
- Root containment and symlink rejection.
- Collision behavior and argument-vector construction.
- Audnexus schema validation and graceful failure.
- State transition preconditions.
Integration tests use temporary roots and a fake converter executable to cover:
- Multi-MP3 successful conversion.
- Single-MP3 successful conversion.
- Each chapter mode.
- Converter failure and interruption.
- Invalid output and destination collision.
- Successful done move and confirmed review move.
- Audnexus timeout and invalid response fallback.
- Stale artifact detection and cleanup boundaries.
Small synthetic audio fixtures are generated during tests; repository contains no copyrighted audiobook samples.
Container verification covers:
- `docker compose config` succeeds.
- Image builds for target platform.
- Python application help starts without TUI failure.
- `m4b-tool`, `ffmpeg`, and `ffprobe` are present.
- Pinned `m4b-tool --help` supports planned arguments or triggers documented ffmetadata fallback.
- Synthetic end-to-end MP3-to-M4B smoke conversion succeeds and validates chapters.
README includes manual Enterprise-D acceptance checklist: SSH TTY behavior, mounted-path visibility, UID/GID ownership, real conversion, Ctrl-C recovery, collision refusal, and correct pending/ready/done/review transitions.
## Deliverables
```text
audiobook-tui/
├── app/
├── tests/
├── Dockerfile
├── compose.yaml
├── config.toml
├── .env.example
├── pyproject.toml
├── README.md
└── LICENSE
```
README covers copying project to Enterprise-D, creating sibling directories, setting UID/GID, selecting or upgrading image pin, building, launching over SSH, configuration, chapter file format, recovery, logs, backup expectations, and troubleshooting permissions.
## Non-Goals
- Desktop GUI or web UI.
- Automated unattended queue conversion.
- Paid metadata services or credentialed Audnexus access.
- Database-backed history.
- Editing original MP3 tags.
- Recursive library management beyond immediate pending book units.
- Downloading, ripping, or acquiring audiobook media.