Commit 924fe0926b6b

Vincent Demeester <vincent@sbr.pm>
2025-12-14 22:09:12
feat(pkgs): Add audible-converter for Audible to Audiobookshelf workflow
- Enable automated audiobook downloads from Audible library - Convert DRM-protected AAX files to M4B format with chapters - Streamline audiobook ingestion for Audiobookshelf media server ๐Ÿค– Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com> Signed-off-by: Vincent Demeester <vincent@sbr.pm>
1 parent 55b7b91
Changed files (4)
pkgs/audible-converter/convert.sh
@@ -0,0 +1,417 @@
+#!/usr/bin/env bash
+# Audible to Audiobookshelf converter
+# Downloads audiobooks from Audible and converts them to M4B format
+
+set -euo pipefail
+
+# Configuration
+TEMP_DIR="${AUDIBLE_TEMP_DIR:-/tmp/audible-download}"
+OUTPUT_DIR="${AUDIBLE_OUTPUT_DIR:-$HOME/audiobooks}"
+QUALITY="${AUDIBLE_QUALITY:-best}"
+FORMAT="${AUDIBLE_FORMAT:-m4b}"
+AUTHCODE="${AUDIBLE_AUTHCODE:-}"
+
+# Colors for output
+RED='\033[0;31m'
+GREEN='\033[0;32m'
+YELLOW='\033[1;33m'
+NC='\033[0m' # No Color
+
+# Logging functions
+log_info() {
+    echo -e "${GREEN}[INFO]${NC} $*"
+}
+
+log_warn() {
+    echo -e "${YELLOW}[WARN]${NC} $*"
+}
+
+log_error() {
+    echo -e "${RED}[ERROR]${NC} $*"
+}
+
+# Usage information
+usage() {
+    cat <<EOF
+Usage: $(basename "$0") [OPTIONS] [COMMAND]
+
+Audible to Audiobookshelf converter - Downloads and converts audiobooks
+
+COMMANDS:
+    download-all    Download all books from Audible library
+    download ASIN   Download specific book by ASIN
+    convert FILE    Convert existing AAX file to M4B
+    sync            Download and convert all new books (default)
+    list            List Audible library
+
+OPTIONS:
+    -o, --output DIR     Output directory (default: \$HOME/audiobooks)
+    -t, --temp DIR       Temporary download directory (default: /tmp/audible-download)
+    -q, --quality QUAL   Audio quality: best, high, normal (default: $QUALITY)
+    -f, --format FMT     Output format: m4b, mp3, m4a (default: $FORMAT)
+    -a, --authcode CODE  Audible activation bytes for AAX DRM removal
+    -h, --help           Show this help message
+
+ENVIRONMENT VARIABLES:
+    AUDIBLE_OUTPUT_DIR   Output directory for converted books
+    AUDIBLE_TEMP_DIR     Temporary directory for downloads
+    AUDIBLE_QUALITY      Audio quality setting
+    AUDIBLE_FORMAT       Output format
+    AUDIBLE_AUTHCODE     Activation bytes for AAX DRM removal
+
+EXAMPLES:
+    # Sync library (download and convert new books)
+    $(basename "$0") sync
+
+    # Download specific book
+    $(basename "$0") download B01234567X
+
+    # Convert existing AAX file
+    $(basename "$0") convert /path/to/book.aax
+
+    # Download all books to custom directory
+    $(basename "$0") --output /mnt/audiobooks download-all
+
+EOF
+}
+
+# Check dependencies
+check_dependencies() {
+    local deps=("audible" "aaxtomp3" "ffmpeg" "mediainfo" "jq")
+    local missing=()
+
+    for dep in "${deps[@]}"; do
+        if ! command -v "$dep" &> /dev/null; then
+            missing+=("$dep")
+        fi
+    done
+
+    if [ ${#missing[@]} -ne 0 ]; then
+        log_error "Missing required dependencies: ${missing[*]}"
+        log_error "Please install: nix-shell -p audible-cli aaxtomp3 ffmpeg mediainfo jq"
+        exit 1
+    fi
+}
+
+# Check if authenticated with Audible
+check_auth() {
+    if ! audible library list &> /dev/null; then
+        log_error "Not authenticated with Audible"
+        log_error "Please run: audible quickstart"
+        exit 1
+    fi
+}
+
+# Create directories
+setup_dirs() {
+    mkdir -p "$TEMP_DIR"
+    mkdir -p "$OUTPUT_DIR"
+}
+
+# Cleanup temporary files
+cleanup() {
+    if [ -d "$TEMP_DIR" ]; then
+        log_info "Cleaning up temporary files..."
+        rm -rf "$TEMP_DIR"
+    fi
+}
+
+# Download entire library
+download_all() {
+    log_info "Exporting library metadata..."
+    local library_json="$TEMP_DIR/library.json"
+
+    audible library export --output "$library_json"
+
+    local total_books
+    total_books=$(jq length "$library_json")
+    log_info "Found $total_books books in library"
+
+    local count=0
+    jq -r '.[].asin' "$library_json" | while read -r asin; do
+        count=$((count + 1))
+        log_info "Downloading book $count/$total_books (ASIN: $asin)..."
+
+        if ! audible download \
+            --asin "$asin" \
+            --aax \
+            --quality "$QUALITY" \
+            --output-dir "$TEMP_DIR" 2>&1; then
+            log_warn "Failed to download ASIN: $asin (may already be downloaded)"
+        fi
+    done
+}
+
+# Download specific book by ASIN
+download_book() {
+    local asin="$1"
+    log_info "Downloading book ASIN: $asin..."
+
+    audible download \
+        --asin "$asin" \
+        --aax \
+        --quality "$QUALITY" \
+        --output-dir "$TEMP_DIR"
+}
+
+# Convert AAX files to M4B
+# Usage: convert_books [directory|file]
+convert_books() {
+    local source_path="${1:-$TEMP_DIR}"
+    local aax_files=()
+
+    # Determine if we're converting a directory or a single file
+    if [ -d "$source_path" ]; then
+        # Find all AAX files in directory using find
+        log_info "Searching for AAX files in: $source_path"
+        mapfile -t aax_files < <(find "$source_path" -maxdepth 1 -type f -name "*.aax" -o -name "*.AAX")
+
+        if [ ${#aax_files[@]} -eq 0 ]; then
+            log_warn "No AAX files found in $source_path"
+            return 0
+        fi
+        log_info "Found ${#aax_files[@]} AAX file(s)"
+    elif [ -f "$source_path" ]; then
+        # Single file
+        aax_files=("$source_path")
+    else
+        log_error "Invalid path: $source_path"
+        return 1
+    fi
+
+    local total_files=${#aax_files[@]}
+    log_info "Converting $total_files AAX file(s) to $FORMAT format..."
+
+    # Get authcode (either provided or from audible-cli)
+    local authcode="$AUTHCODE"
+    if [ -z "$authcode" ]; then
+        # Try to get activation bytes from audible-cli
+        log_info "Attempting to retrieve activation bytes from audible-cli..."
+        if authcode=$(audible activation-bytes 2>/dev/null); then
+            if [ -n "$authcode" ]; then
+                log_info "Using activation bytes from audible-cli: $authcode"
+            else
+                log_warn "Activation bytes command succeeded but returned empty value"
+                authcode=""
+            fi
+        else
+            log_warn "Could not retrieve activation bytes from audible-cli"
+            authcode=""
+        fi
+    fi
+
+    # Base aaxtomp3 options (no --batch, we'll loop instead)
+    local aaxtomp3_opts=(--target_dir "$OUTPUT_DIR" --no-clobber)
+
+    # Add authcode if available, otherwise use audible-cli data
+    if [ -n "$authcode" ]; then
+        aaxtomp3_opts+=(--authcode "$authcode")
+    else
+        log_warn "No authcode available, trying --use-audible-cli-data flag"
+        aaxtomp3_opts+=(--use-audible-cli-data)
+    fi
+
+    case "$FORMAT" in
+        m4b)
+            aaxtomp3_opts+=(--single)
+            ;;
+        mp3)
+            aaxtomp3_opts+=(--codec libmp3lame)
+            ;;
+        m4a)
+            # Default M4A output
+            ;;
+        *)
+            log_error "Unknown format: $FORMAT"
+            exit 1
+            ;;
+    esac
+
+    # Convert each file individually (--batch doesn't work with --authcode)
+    # Note: --no-clobber flag makes aaxtomp3 skip existing files automatically
+    local converted=0
+    local failed=0
+    local current=0
+
+    log_info "Starting conversion loop for ${#aax_files[@]} files..."
+
+    for aax_file in "${aax_files[@]}"; do
+        local base_filename
+        base_filename=$(basename "$aax_file")
+        current=$((current + 1))
+
+        log_info "[$current/$total_files] Converting: $base_filename"
+        echo "----------------------------------------"
+
+        # Run aaxtomp3 directly (let output flow to terminal)
+        # Temporarily disable exit-on-error for this command
+        set +e
+        aaxtomp3 "${aaxtomp3_opts[@]}" "$aax_file"
+        local exit_code=$?
+        set -e
+
+        if [ $exit_code -eq 0 ]; then
+            converted=$((converted + 1))
+            log_info "โœ“ Successfully converted: $base_filename"
+        else
+            failed=$((failed + 1))
+            log_warn "โœ— Failed to convert: $base_filename (exit code: $exit_code)"
+        fi
+        echo ""
+    done
+
+    # Report summary
+    local skipped=$((total_files - converted - failed))
+    log_info "Conversion complete!"
+    log_info "  Converted: $converted/$total_files"
+    if [ $skipped -gt 0 ]; then
+        log_info "  Skipped: $skipped/$total_files (already existed)"
+    fi
+    if [ $failed -gt 0 ]; then
+        log_warn "  Failed: $failed/$total_files"
+        log_error ""
+        log_error "If you got 'Missing authcode' errors, follow these steps:"
+        log_error "  1. Authenticate with Audible:"
+        log_error "     audible quickstart"
+        log_error ""
+        log_error "  2. Verify activation bytes are available:"
+        log_error "     audible activation-bytes"
+        log_error ""
+        log_error "  3. Alternatively, provide authcode manually:"
+        log_error "     --authcode YOUR_ACTIVATION_BYTES"
+        log_error "     or: export AUDIBLE_AUTHCODE=YOUR_ACTIVATION_BYTES"
+        exit 1
+    fi
+    log_info "Books saved to: $OUTPUT_DIR"
+}
+
+# List Audible library
+list_library() {
+    log_info "Fetching Audible library..."
+    audible library list | jq -r '.[] | "\(.title) by \(.authors) (ASIN: \(.asin))"'
+}
+
+# Sync library (download and convert new books)
+sync_library() {
+    log_info "Syncing Audible library..."
+    download_all
+    convert_books
+}
+
+# Main function
+main() {
+    local command="sync"
+    local -a positional_args=()
+
+    # Parse arguments
+    while [[ $# -gt 0 ]]; do
+        case $1 in
+            -o|--output)
+                OUTPUT_DIR="$2"
+                shift 2
+                ;;
+            -t|--temp)
+                TEMP_DIR="$2"
+                shift 2
+                ;;
+            -q|--quality)
+                QUALITY="$2"
+                shift 2
+                ;;
+            -f|--format)
+                FORMAT="$2"
+                shift 2
+                ;;
+            -a|--authcode)
+                AUTHCODE="$2"
+                shift 2
+                ;;
+            -h|--help)
+                usage
+                exit 0
+                ;;
+            download-all|download|convert|sync|list)
+                command="$1"
+                shift
+                ;;
+            -*)
+                log_error "Unknown option: $1"
+                usage
+                exit 1
+                ;;
+            *)
+                # Collect positional arguments (file paths, ASINs, etc.)
+                positional_args+=("$1")
+                shift
+                ;;
+        esac
+    done
+
+    # Trap cleanup on exit
+    trap cleanup EXIT
+
+    # Check dependencies
+    check_dependencies
+
+    # Setup directories
+    setup_dirs
+
+    # Execute command
+    case $command in
+        download-all)
+            check_auth
+            download_all
+            ;;
+        download)
+            if [ ${#positional_args[@]} -eq 0 ]; then
+                log_error "ASIN required for download command"
+                usage
+                exit 1
+            fi
+            check_auth
+            download_book "${positional_args[0]}"
+            ;;
+        convert)
+            if [ ${#positional_args[@]} -eq 0 ]; then
+                log_error "File or directory path required for convert command"
+                usage
+                exit 1
+            fi
+            # Get the last positional argument as the path
+            local convert_path="${positional_args[-1]}"
+            if [ ! -e "$convert_path" ]; then
+                log_error "Path not found: $convert_path"
+                exit 1
+            fi
+
+            # If it's a directory, convert all AAX files in it
+            # If it's a file, convert just that specific file
+            if [ -d "$convert_path" ]; then
+                log_info "Converting all AAX files in directory: $convert_path"
+                convert_books "$convert_path"
+            elif [ -f "$convert_path" ]; then
+                log_info "Converting single file: $(basename "$convert_path")"
+                convert_books "$convert_path"
+            else
+                log_error "Path must be a file or directory: $convert_path"
+                exit 1
+            fi
+            ;;
+        sync)
+            check_auth
+            sync_library
+            ;;
+        list)
+            check_auth
+            list_library
+            ;;
+        *)
+            log_error "Unknown command: $command"
+            usage
+            exit 1
+            ;;
+    esac
+}
+
+# Run main function
+main "$@"
pkgs/audible-converter/default.nix
@@ -0,0 +1,42 @@
+{
+  lib,
+  writeShellApplication,
+  audible-cli,
+  aaxtomp3,
+  ffmpeg,
+  mediainfo,
+  jq,
+}:
+
+writeShellApplication {
+  name = "audible-converter";
+
+  runtimeInputs = [
+    audible-cli
+    aaxtomp3
+    ffmpeg
+    mediainfo
+    jq
+  ];
+
+  text = builtins.readFile ./convert.sh;
+
+  meta = {
+    description = "Download and convert Audible audiobooks to Audiobookshelf-compatible formats";
+    longDescription = ''
+      A wrapper tool that combines audible-cli and AAXtoMP3 to download
+      audiobooks from Audible and convert them to M4B format for use with
+      Audiobookshelf or other audiobook players.
+
+      Features:
+      - Download entire Audible library or specific books
+      - Convert AAX to M4B/MP3/M4A
+      - Preserve chapter markers and metadata
+      - Organize output for Audiobookshelf
+    '';
+    homepage = "https://github.com/vdemeester/home";
+    license = lib.licenses.mit;
+    platforms = lib.platforms.linux;
+    maintainers = [ ];
+  };
+}
pkgs/audible-converter/README.md
@@ -0,0 +1,249 @@
+# audible-converter
+
+Download and convert Audible audiobooks to Audiobookshelf-compatible formats.
+
+## Overview
+
+`audible-converter` is a wrapper tool that combines [audible-cli](https://github.com/mkb79/audible-cli) and [aaxtomp3](https://github.com/KrumpetPirate/AAXtoMP3) to:
+
+- Download audiobooks from your Audible library
+- Convert AAX files to M4B/MP3/M4A formats
+- Preserve chapter markers and metadata
+- Organize output for Audiobookshelf
+
+## Features
+
+- **Download from Audible**: Sync your entire library or download specific books
+- **Multiple output formats**: M4B (recommended), MP3, or M4A
+- **Chapter preservation**: Maintains chapter markers and navigation
+- **Metadata embedding**: Preserves title, author, narrator, cover art
+- **Batch processing**: Handle multiple books at once
+- **Quality options**: Choose between best, high, or normal audio quality
+
+## Installation
+
+This package is available in the home repository flake:
+
+```nix
+# In your NixOS configuration
+environment.systemPackages = with pkgs; [
+  audible-converter
+];
+```
+
+Or install directly:
+
+```bash
+nix profile install .#audible-converter
+```
+
+## First-Time Setup
+
+Before using the tool, authenticate with Audible:
+
+```bash
+# Step 1: Authenticate with Audible
+audible quickstart
+```
+
+This will:
+1. Prompt for your Audible credentials
+2. Store authentication tokens securely
+3. Set up your default region (US, UK, etc.)
+
+```bash
+# Step 2: Verify activation bytes are available
+audible activation-bytes
+```
+
+This command retrieves your 8-character activation bytes (e.g., `1a2b3c4d`) which are used to decrypt AAX files. The tool will automatically use these bytes when converting.
+
+## Usage
+
+### Commands
+
+```bash
+# Sync library (download and convert new books)
+audible-converter sync
+
+# Download entire library
+audible-converter download-all
+
+# Download specific book by ASIN
+audible-converter download B01234567X
+
+# Convert existing AAX file
+audible-converter convert /path/to/book.aax
+
+# List your Audible library
+audible-converter list
+```
+
+### Options
+
+```bash
+-o, --output DIR     Output directory (default: $HOME/audiobooks)
+-t, --temp DIR       Temporary download directory (default: /tmp/audible-download)
+-q, --quality QUAL   Audio quality: best, high, normal (default: best)
+-f, --format FMT     Output format: m4b, mp3, m4a (default: m4b)
+-h, --help           Show help message
+```
+
+### Examples
+
+```bash
+# Download and convert all books to /mnt/audiobooks
+audible-converter --output /mnt/audiobooks sync
+
+# Download specific book in MP3 format
+audible-converter --format mp3 download B01234567X
+
+# Convert existing AAX file with custom output
+audible-converter --output ~/audiobooks convert book.aax
+```
+
+## Environment Variables
+
+Configure defaults using environment variables:
+
+- `AUDIBLE_OUTPUT_DIR` - Output directory for converted books
+- `AUDIBLE_TEMP_DIR` - Temporary directory for downloads
+- `AUDIBLE_QUALITY` - Audio quality setting (best, high, normal)
+- `AUDIBLE_FORMAT` - Output format (m4b, mp3, m4a)
+
+Example:
+
+```bash
+export AUDIBLE_OUTPUT_DIR="$HOME/audiobooks"
+export AUDIBLE_FORMAT="m4b"
+audible-converter sync
+```
+
+On rhea (with NFS-mounted storage):
+
+```bash
+export AUDIBLE_OUTPUT_DIR="/neo/audiobooks"
+audible-converter sync
+```
+
+## Output Structure
+
+Books are automatically organized for Audiobookshelf:
+
+```
+/neo/audiobooks/
+โ”œโ”€โ”€ Brandon Sanderson/
+โ”‚   โ”œโ”€โ”€ Mistborn The Final Empire/
+โ”‚   โ”‚   โ”œโ”€โ”€ cover.jpg
+โ”‚   โ”‚   โ””โ”€โ”€ Mistborn The Final Empire.m4b
+โ”‚   โ””โ”€โ”€ The Way of Kings/
+โ”‚       โ”œโ”€โ”€ cover.jpg
+โ”‚       โ””โ”€โ”€ The Way of Kings.m4b
+โ””โ”€โ”€ Andy Weir/
+    โ””โ”€โ”€ The Martian/
+        โ”œโ”€โ”€ cover.jpg
+        โ””โ”€โ”€ The Martian.m4b
+```
+
+Each book includes:
+- Single M4B file with embedded chapters
+- Cover art (embedded and separate file)
+- Metadata (title, author, narrator, etc.)
+
+## Audiobookshelf Integration
+
+Point your Audiobookshelf library to the output directory:
+
+1. Open Audiobookshelf web interface
+2. Go to Settings โ†’ Libraries
+3. Add new library with path: `/neo/audiobooks`
+4. Scan library to import books
+
+## Format Comparison
+
+| Format | Size | Quality | Compatibility | Chapters | Recommended |
+|--------|------|---------|---------------|----------|-------------|
+| M4B    | Small | High | Good | Yes | โœ… Best choice |
+| MP3    | Medium | Good | Excellent | Limited | For legacy devices |
+| M4A    | Small | High | Good | Yes | Alternative to M4B |
+
+**Recommendation**: Use M4B format for best balance of quality, size, and chapter support.
+
+## Troubleshooting
+
+### Authentication Issues
+
+If you get authentication errors:
+
+```bash
+# Re-run authentication
+audible quickstart
+
+# Check stored credentials
+audible library list
+```
+
+### Activation Bytes / Authcode Issues
+
+The tool automatically retrieves activation bytes using `audible activation-bytes`. If this fails:
+
+**Check activation bytes are available:**
+```bash
+audible activation-bytes
+```
+
+**If command returns empty or errors:**
+1. Ensure you're authenticated: `audible quickstart`
+2. Try re-authenticating if needed
+3. Activation bytes are account-specific and permanent (you only need to get them once)
+
+**Manual authcode override:**
+```bash
+# Option 1: Command line
+audible-converter --authcode 1a2b3c4d convert file.aax
+
+# Option 2: Environment variable
+export AUDIBLE_AUTHCODE=1a2b3c4d
+audible-converter convert file.aax
+```
+
+**Note**: Your activation bytes are an 8-character hexadecimal code (e.g., `1a2b3c4d`). They are tied to your Audible account and remain constant.
+
+### Download Failures
+
+Some books may fail to download (already purchased, region restrictions):
+
+- The tool will warn about failed downloads and continue
+- Check the ASIN is correct: `audible library list`
+- Verify the book is in your library for your region
+
+### Conversion Issues
+
+If AAX conversion fails:
+
+1. Ensure you have the required dependencies (included in the package)
+2. Check the AAX file is not corrupted
+3. Try downloading the book again
+
+### Permission Errors
+
+Ensure you have write permissions to the output directory:
+
+```bash
+# Check permissions
+ls -ld /neo/audiobooks
+
+# Fix if needed
+sudo chown -R $USER:users /neo/audiobooks
+```
+
+## Related Documentation
+
+- [Audible CLI Documentation](https://audible-cli.readthedocs.io/)
+- [aaxtomp3 GitHub](https://github.com/KrumpetPirate/AAXtoMP3)
+- [Audiobookshelf Documentation](https://www.audiobookshelf.org/docs)
+
+## Implementation Note
+
+For detailed information about the conversion workflow and technical decisions, see:
+- Note: `~/desktop/org/notes/20251213T095555--audible-to-audiobookshelf-conversion-guide__audible_audiobooks_audiobookshelf_conversion_homelab_nixos_reference.org`
pkgs/default.nix
@@ -27,6 +27,7 @@ in
   cliphist-cleanup = pkgs.callPackage ../tools/cliphist-cleanup { };
   toggle-color-scheme = pkgs.callPackage ./toggle-color-scheme { };
   homepage = pkgs.callPackage ./homepage { inherit globals; };
+  audible-converter = pkgs.callPackage ./audible-converter { };
 
   chmouzies-ai = pkgs.callPackage ./chmouzies/ai.nix { };
   chmouzies-git = pkgs.callPackage ./chmouzies/git.nix { };