audiobookshelf-metadata-sync

Use when synchronizing audiobookshelf server metadata to local audio files, or when library item metadata and embedded file metadata are inconsistent

Safety Notice

This listing is imported from skills.sh public index metadata. Review upstream SKILL.md and repository scripts before running.

Copy this and send it to your AI assistant to learn

Install skill "audiobookshelf-metadata-sync" with this command: npx skills add zgs225/my-skills/zgs225-my-skills-audiobookshelf-metadata-sync

Audiobookshelf Metadata Sync

Overview

Reference guide for synchronizing metadata between audiobookshelf server and local audio files. When audiobookshelf server metadata (manually verified as correct) differs from embedded audio file metadata, use this skill to update files to match the server.

Core principle: audiobookshelf server metadata is the source of truth. Update local files to match, avoiding duplicate modifications unless explicitly required.

Quick Reference

TaskAPI Endpoint / CommandNotes
LoginPOST /api/loginGet API token
Get all librariesGET /api/librariesList libraries
Get library itemsGET /api/libraries/{id}/itemsList all items in library
Get item detailsGET /api/items/{itemId}Full item with media metadata
Update item mediaPATCH /api/items/{itemId}/mediaUpdate server metadata
Embed metadataffmpeg -i in.m4a -c copy -metadata key="value" out.m4aWrite to file
Batch updatePOST /api/items/batch-updateUpdate multiple items

When to Use

  • audiobookshelf server metadata and local file metadata are inconsistent
  • Need to sync server metadata to local audio files
  • Files were imported/matched but embedded metadata wasn't updated
  • Preparing files for backup or export with correct metadata
  • Need to verify metadata consistency across library

When NOT to Use

  • When you want to use local file metadata as source of truth
  • When only server-side changes are needed (no file modification)
  • When working with podcasts that should preserve original episode metadata

Authentication

Get API Token

Option 1: Login endpoint

curl -X POST "http://your-audiobookshelf-server/api/login" \
  -H "Content-Type: application/json" \
  -d '{"username":"your-username","password":"your-password"}'

Response:

{
  "user": {
    "token": "eyJhbGciOiJIiJ9.eyJ1c2VyIjoiNDEyODc4fQ.ZraBFohS4Tg39NszY...",
    "id": "user-id"
  },
  "serverSettings": {...}
}

Option 2: From web UI

  1. Open browser developer tools
  2. Go to audiobookshelf web UI
  3. Find any API request in Network tab
  4. Copy Authorization: Bearer <token> header

Use API Token

# In header (recommended)
curl -X GET "http://your-audiobookshelf-server/api/libraries" \
  -H "Authorization: Bearer YOUR_TOKEN"

# Or as query parameter for GET requests
curl -X GET "http://your-audiobookshelf-server/api/libraries?token=YOUR_TOKEN"

API Workflow

Step 1: Get All Libraries

curl -X GET "http://your-audiobookshelf-server/api/libraries" \
  -H "Authorization: Bearer YOUR_TOKEN"

Response:

{
  "libraries": [
    {
      "id": "lib-id-1",
      "name": "Audiobooks",
      "mediaType": "book",
      "folders": [{"id": "folder-id", "fullPath": "/path/to/audiobooks"}]
    }
  ]
}

Step 2: Get Library Items

curl -X GET "http://your-audiobookshelf-server/api/libraries/lib-id-1/items" \
  -H "Authorization: Bearer YOUR_TOKEN"

Response:

{
  "results": [
    {
      "id": "item-id-1",
      "libraryId": "lib-id-1",
      "path": "/path/to/audiobooks/Book Title",
      "mediaType": "book",
      "media": {
        "metadata": {
          "title": "Book Title",
          "authorName": "Author Name",
          "seriesName": "Series Name",
          "publishedYear": "2024",
          "genres": ["Fiction", "Adventure"],
          "asin": "B00XXX",
          "isbn": "978-xxx"
        },
        "audioFiles": [
          {
            "index": 0,
            "ino": "file-ino",
            "metadata": {
              "filename": "chapter01.m4a",
              "path": "/path/to/audiobooks/Book Title/chapter01.m4a"
            },
            "duration": 1234.56,
            "bitRate": 128000,
            "format": "mp4",
            "metaTags": {
              "title": "Chapter 1",
              "artist": "Author Name"
            }
          }
        ]
      }
    }
  ]
}

Step 3: Get Specific Item Details

curl -X GET "http://your-audiobookshelf-server/api/items/item-id-1" \
  -H "Authorization: Bearer YOUR_TOKEN"

Response includes chapters information:

{
  "id": "item-id-1",
  "media": {
    "numChapters": 32,
    "chapters": [
      {
        "title": "01 鲁宾逊漂流记 01",
        "start": 0,
        "end": 1262.948
      },
      {
        "title": "02 鲁宾逊漂流记 02",
        "start": 1262.948,
        "end": 2606.32
      }
    ],
    "metadata": { ... }
  }
}

Key fields:

  • media.chapters[].title — Chapter name (set in UI)
  • media.chapters[].start — Chapter start time (seconds)
  • media.chapters[].end — Chapter end time (seconds)

Step 4: Extract Metadata for File Sync

Key metadata fields to sync to audio files:

Book/Podcast Metadata:

  • title — Book or podcast title
  • authorName — Author or podcast author
  • narratorName — Narrator (for audiobooks)
  • seriesName — Series name
  • seriesSequence — Series sequence number
  • genres — Genres/categories
  • publishedYear — Publication year
  • publisher — Publisher name
  • description — Book/podcast description
  • isbn / asin — Identifiers
  • language — Language code
  • explicit — Explicit content flag

Audio File Metadata (embedded tags):

  • title — Chapter/episode title
  • artist — Usually author/narrator
  • album — Book/podcast title
  • albumArtist — Author (useful for compilations)
  • genre — Genre
  • year — Publication year
  • track — Track number (e.g., "1/12")
  • disc — Disc number (for multi-part)
  • comment — Description or notes
  • cover — Embedded album art

Sync Process

Single File Sync with ffmpeg

Important: FFmpeg cannot edit files in-place. You MUST write to a temporary file first, then rename.

Note on chapter titles: Use chapter title from media.chapters[].title (set in audiobookshelf UI), NOT the filename. Filename may be generic like chapter01.m4a, but chapter title is descriptive like 01 鲁宾逊漂流记 01.

# Read current metadata
ffprobe -i "chapter01.m4a" -show_entries format_tags -of default=noprint_wrappers=1

# Chapter title from audiobookshelf (NOT filename)
CHAPTER_TITLE="01 鲁宾逊漂流记 01"
BOOK_TITLE="鲁宾逊漂流记"
AUTHOR="丹尼尔·笛福"

# Step 1: Write to temporary file (must use same extension for format detection)
ffmpeg -i "chapter01.m4a" -c copy \
  -metadata title="$CHAPTER_TITLE" \
  -metadata artist="$AUTHOR" \
  -metadata album="$BOOK_TITLE" \
  -metadata album_artist="$AUTHOR" \
  -y "chapter01.temp.m4a"

# Step 2: Verify temp file was created successfully
if [ -f "chapter01.temp.m4a" ]; then
  # Step 3: Replace original
  mv "chapter01.temp.m4a" "chapter01.m4a"
  echo "Success: metadata updated"
else
  echo "ERROR: Failed to create temp file"
  exit 1
fi

Why temp file is required:

  • FFmpeg error when writing directly: FFmpeg cannot edit existing files in-place.
  • Temp file must have correct extension (.temp.m4a not .tmp) for format detection
  • -y flag overwrites temp file if it exists

Batch Sync Script Pattern

#!/bin/bash
# Sync metadata from audiobookshelf to local files

SERVER="http://your-audiobookshelf-server"
TOKEN="your-api-token"
LIBRARY_ID="lib-id"

# Get all items
items=$(curl -s "$SERVER/api/libraries/$LIBRARY_ID/items" \
  -H "Authorization: Bearer $TOKEN")

# Process each item
echo "$items" | jq -c '.results[]' | while read -r item; do
  item_id=$(echo "$item" | jq -r '.id')

  # Get full item details
  full_item=$(curl -s "$SERVER/api/items/$item_id" \
    -H "Authorization: Bearer $TOKEN")

  # Extract metadata
  title=$(echo "$full_item" | jq -r '.media.metadata.title')
  author=$(echo "$full_item" | jq -r '.media.metadata.authorName')
  genre=$(echo "$full_item" | jq -r '.media.metadata.genres[0] // ""')
  year=$(echo "$full_item" | jq -r '.media.metadata.publishedYear')

  # Process each audio file
  echo "$full_item" | jq -c '.media.audioFiles[]' | while read -r audio_file; do
    file_path=$(echo "$audio_file" | jq -r '.metadata.path')
    file_duration=$(echo "$audio_file" | jq -r '.duration')

    if [ -f "$file_path" ]; then
      echo "Processing: $file_path"
      # ffmpeg command to update metadata
      # ...
    fi
  done
done

Metadata Field Mapping

audiobookshelf fieldID3v2 (MP3)MP4 (M4A)Vorbis (FLAC)
metadata.titletitletitletitle
metadata.authorNameartist / albumartistartist / albumartistartist / albumartist
metadata.seriesNamegroupingseriesseries
metadata.genresgenregenregenre
metadata.publishedYearyearyeardate
metadata.descriptioncommentdescriptioncomment
metadata.isbnisbnisbnisbn
metadata.languagelanguagelanguagelanguage

Path Prefix Mapping

Important: API paths may differ from local filesystem paths due to Docker volumes, NAS mounts, or different system configurations.

Common Scenarios

ScenarioAPI PathLocal Path
Docker container/books/.../mnt/nas/audiobooks/...
NAS mount/audiobooks/.../Volumes/NAS/Media/Books/...
WSL2/mnt/media/books/...D:\Media\Books\...
Different OS/data/audiobooks/.../Volumes/Drive/audiobooks/...

Configuration

Define path prefix mappings at the start of your sync script:

#!/bin/bash

# Path prefix mapping: API path -> Local path
# Add mappings for each library/folder
declare -A PATH_MAPPINGS=(
  ["/books"]="/mnt/nas/audiobooks"
  ["/audiobooks"]="/Volumes/NAS/Media/Books"
  ["/data/media"]="/media"
)

Path Translation Function

# Translate API path to local path
translate_path() {
  local api_path="$1"
  local local_path=""

  for api_prefix in "${!PATH_MAPPINGS[@]}"; do
    if [[ "$api_path" == "$api_prefix"* ]]; then
      local_prefix="${PATH_MAPPINGS[$api_prefix]}"
      local_path="${api_path/$api_prefix/$local_prefix}"
      echo "$local_path"
      return 0
    fi
  done

  # No mapping found, return original
  echo "$api_path"
  return 1
}

# Usage example:
api_path="/books/Book Title/chapter01.m4a"
local_path=$(translate_path "$api_path")
# Result: /mnt/nas/audiobooks/Book Title/chapter01.m4a

Verify Path Translation

# Test path translation before processing
verify_paths() {
  echo "Testing path mappings..."
  for api_prefix in "${!PATH_MAPPINGS[@]}"; do
    local_prefix="${PATH_MAPPINGS[$api_prefix]}"
    if [ -d "$local_prefix" ]; then
      echo "OK: $api_prefix -> $local_prefix"
    else
      echo "WARNING: Local path not found: $local_prefix"
    fi
  done
}

File-Chapter Mapping Logic

For typical audiobooks (1 file = 1 chapter):

Files and chapters are both sorted by order. Match them by index:

audioFiles indexchapters indexResult
audioFiles[0]chapters[0]File 1 → Chapter 1 title
audioFiles[1]chapters[1]File 2 → Chapter 2 title
audioFiles[i]chapters[i]File i+1 → Chapter i+1 title

Why index-based matching works:

  • Audiobookshelf ensures audioFiles and chapters arrays are in the same order
  • Each audio file corresponds to exactly one chapter
  • No need for complex time-range calculations

Batch Sync with Chapters and Path Translation

#!/bin/bash

SERVER="http://your-audiobookshelf-server"
TOKEN="your-api-token"
LIBRARY_ID="lib-id"

# Path mappings
declare -A PATH_MAPPINGS=(
  ["/books"]="/mnt/nas/audiobooks"
)

translate_path() {
  local api_path="$1"
  for api_prefix in "${!PATH_MAPPINGS[@]}"; do
    if [[ "$api_path" == "$api_prefix"* ]]; then
      echo "${api_path/$api_prefix/${PATH_MAPPINGS[$api_prefix]}}"
      return 0
    fi
  done
  echo "$api_path"
  return 1
}

# Get all items
items=$(curl -s "$SERVER/api/libraries/$LIBRARY_ID/items" \
  -H "Authorization: Bearer $TOKEN")

echo "$items" | jq -c '.results[]' | while read -r item; do
  item_id=$(echo "$item" | jq -r '.id')
  full_item=$(curl -s "$SERVER/api/items/$item_id" \
    -H "Authorization: Bearer $TOKEN")

  # Extract book-level metadata
  book_title=$(echo "$full_item" | jq -r '.media.metadata.title')
  author=$(echo "$full_item" | jq -r '.media.metadata.authorName')
  genre=$(echo "$full_item" | jq -r '.media.metadata.genres[0] // ""')
  year=$(echo "$full_item" | jq -r '.media.metadata.publishedYear')

  # Extract chapters array
  chapters=$(echo "$full_item" | jq -c '.media.chapters // []')
  num_chapters=$(echo "$chapters" | jq 'length')

  echo "Processing: $book_title ($num_chapters chapters)"

  # Process each audio file with index-based chapter matching
  # audioFiles[0] matches chapters[0], audioFiles[1] matches chapters[1], etc.
  echo "$full_item" | jq -c '.media.audioFiles[]' | jq -s 'to_entries[]' | while read -r audio_file_entry; do
    # Get file info
    file_index=$(echo "$audio_file_entry" | jq -r '.key')
    audio_file=$(echo "$audio_file_entry" | jq -c '.value')
    api_path=$(echo "$audio_file" | jq -r '.metadata.path')

    # Translate to local path
    local_path=$(translate_path "$api_path")

    if [ ! -f "$local_path" ]; then
      echo "  WARNING: File not found: $local_path"
      continue
    fi

    # Get chapter title by index (simple 1:1 mapping)
    chapter_title=""
    if [ "$file_index" -lt "$num_chapters" ]; then
      chapter_title=$(echo "$chapters" | jq -r ".[$file_index].title")
    fi

    # Fallback: use generic chapter name if no match
    if [ -z "$chapter_title" ] || [ "$chapter_title" = "null" ]; then
      chapter_title="Chapter $((file_index + 1))"
    fi

    echo "  File: $(basename "$local_path")"
    echo "  Chapter: $chapter_title (track $((file_index + 1)))"

    # Get file extension for temp file
    ext="${local_path##*.}"
    temp_file="${local_path%.${ext}}.temp.${ext}"

    # Update metadata: write to temp file first, then rename
    # Use chapter title from audiobookshelf, NOT filename
    if ffmpeg -i "$local_path" -c copy \
      -metadata title="$chapter_title" \
      -metadata artist="$author" \
      -metadata album="$book_title" \
      -metadata album_artist="$author" \
      -metadata genre="$genre" \
      -metadata year="$year" \
      -metadata track="$((file_index + 1))" \
      -y "$temp_file" 2>/dev/null; then
      mv "$temp_file" "$local_path"
      echo "  Success: metadata updated"
    else
      echo "  ERROR: failed to update metadata"
      rm -f "$temp_file"
    fi
  done
done

File-Level Deduplication

To avoid duplicate modifications:

  1. Track processed files:

    # Use a marker file or extended attributes
    xattr -w user.audiobookshelf.synced "true" file.m4a
    
    # Check before processing
    xattr -l file.m4a | grep -q "user.audiobookshelf.synced"
    
  2. Compare metadata before writing:

    # Get current title
    current_title=$(ffprobe -i file.m4a -show_entries format_tags=title \
      -of default=noprint_wrappers=1:nokey=1 2>/dev/null)
    
    # Only update if different
    if [ "$current_title" != "$target_title" ]; then
      # Update metadata
    fi
    
  3. Use checksums:

    # Store hash after sync
    md5sum file.m4a >> .audiobookshelf-sync-cache
    
    # Skip if file unchanged
    

Common Mistakes

MistakeFix
Forgetting -c copyCauses unnecessary re-encoding, quality loss
Not backing up before batch syncAlways backup or create temp files first
Using wrong metadata field namesMP4 uses album_artist, FLAC uses albumartist
Not forcing ID3v2.3 for MP3Add -id3v2_version 3 for compatibility
Syncing all files when one changedTrack per-file sync status
Overwriting manually-corrected filesVerify server metadata is source of truth
Assuming API paths match local pathsAlways configure path prefix mappings
Using hardcoded pathsUse path translation function with mappings
Writing directly to original fileFFmpeg requires temp file first
Using wrong temp extension (.tmp)Use .temp.m4a or same extension as source

Error Handling

Path Translation Failures

# When translated path still doesn't exist
if [ ! -f "$local_path" ]; then
  echo "ERROR: Cannot locate file"
  echo "  API path: $api_path"
  echo "  Translated: $local_path"

  # Option 1: Try to find by filename
  filename=$(basename "$api_path")
  found=$(find /local/search/paths -name "$filename" 2>/dev/null | head -1)
  if [ -n "$found" ]; then
    echo "  Found by filename: $found"
    local_path="$found"
  fi

  # Option 2: Log for manual review
  echo "$api_path|$local_path" >> .sync-path-errors
fi

Authentication Errors

# Check if token expired
curl -I "http://server/api/libraries" \
  -H "Authorization: Bearer $TOKEN"

# If 401, re-login

File Not Found

# Server path may differ from actual filesystem
# Verify file exists before processing
if [ ! -f "$file_path" ]; then
  echo "File not found: $file_path"
  # Option: scan filesystem to find matching file
fi

Metadata Write Failures

# Some formats are read-only
# Check file permissions
ls -la file.m4a

# For MP4, may need to rewrite file (not in-place edit)
ffmpeg -i in.m4a -c copy -metadata title="New" temp.m4a && mv temp.m4a in.m4a

Related Tools

  • ffprobe/ffmpeg — Read/write audio file metadata
  • AtomicParsley — MP4/M4A metadata tool
  • id3v2/libid3tag — MP3 metadata tool
  • metaflac — FLAC metadata tool
  • MusicBrainz Picard — Auto-tagging with fingerprinting

API Discovery

Since official API docs are outdated:

  1. Browser DevTools:

    • Open audiobookshelf web UI
    • Perform actions (edit metadata, scan library)
    • Watch Network tab for API calls
  2. Server logs:

    • Check audiobookshelf server logs
    • Shows all API requests
  3. Source code:

    • GitHub: advply/audiobookshelf
    • Check server/controllers/ for API routes
  4. Community resources:

Version History

VersionDateChanges
1.0.32026-03-13Simplify file-chapter mapping: use index-based matching (1 file = 1 chapter)
1.0.22026-03-13Use chapter title from media.chapters[].title instead of filename
1.0.12026-03-12Add temp file requirement for ffmpeg writes (ffmpeg cannot edit in-place)
1.0.02026-03-12Initial release: API reference, metadata mapping, path prefix translation, deduplication

Source Transparency

This detail page is rendered from real SKILL.md content. Trust labels are metadata-based hints, not a safety guarantee.

Related Skills

Related by shared tags or category signals.

General

landing-page-guide-v2

No summary provided by upstream source.

Repository SourceNeeds Review
611-bear2u
General

nixos-best-practices

No summary provided by upstream source.

Repository SourceNeeds Review
General

card-news-generator-v2

No summary provided by upstream source.

Repository SourceNeeds Review
General

landing-page-guide

No summary provided by upstream source.

Repository SourceNeeds Review