# Audiobookshelf Metadata Management

Fix wrong metadata in Audiobookshelf (ABS) — author, narrator, series, description, publisher, year, ASIN.

## Architecture

ABS metadata precedence (configurable per library):
```
folderStructure > audioMetatags > absMetadata > nfoFile > txtFiles > opfFile
```

**Critical:** `audioMetatags` is above `absMetadata` by default. This means any metadata you set via the ABS API (`PATCH /api/items/{id}/media`) will be **overwritten by the ID3 tags** in the MP3 files on every scan. To fix metadata permanently, you must fix the ID3 tags first.

## API Access

### Token retrieval
```bash
sqlite3 "/DATA/AppData/audiobookshelf/config/absdatabase.sqlite" \
  "SELECT * FROM users;"
```
The last column (JSON) contains the JWT token. Example token format:
`eyJhbGciOiJIUzI1NiIs...`

### Library ID
```bash
curl -s -H "Authorization: Bearer $TOKEN" http://127.0.0.1:13378/api/libraries
```
Returns `libraries[].id` — use this for all item operations.

### List all items
```bash
curl -s -H "Authorization: Bearer $TOKEN" \
  "http://127.0.0.1:13378/api/libraries/{libId}/items"
```

### Get single item
```bash
curl -s -H "Authorization: Bearer $TOKEN" \
  "http://127.0.0.1:13378/api/items/{itemId}"
```

### Update metadata (PATCH)
```bash
curl -s -X PATCH -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
  "http://127.0.0.1:13378/api/items/{itemId}/media" \
  -d '{"metadata":{"title":"New Title","authorName":"Author","narratorName":"Narrator","publisher":"Pub","publishedYear":"2024","asin":"B0XXXXXXX","description":"Full description text..."}}'
```

### Set series (PATCH — array format)
Series are set via the `series` array in metadata, NOT via `seriesName`/`seriesSequence`:
```bash
curl -s -X PATCH -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
  "http://127.0.0.1:13378/api/items/{itemId}/media" \
  -d '{"metadata":{"series":[{"name":"Medici","sequence":"1"}]}}'
```
This creates the series if it doesn't exist and links the item. The `series` field in the response will show `[{id, name, sequence}]`.

### Delete item
```bash
curl -s -X DELETE -H "Authorization: Bearer $TOKEN" \
  "http://127.0.0.1:13378/api/items/{itemId}"
```

### Rescan library
```bash
curl -s -X POST -H "Authorization: Bearer $TOKEN" \
  "http://127.0.0.1:13378/api/libraries/{libId}/scan"
```

### Change metadata precedence
```bash
curl -s -X PATCH -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
  "http://127.0.0.1:13378/api/libraries/{libId}" \
  -d '{"settings":{"metadataPrecedence":["folderStructure","absMetadata","audioMetatags","nfoFile","txtFiles","opfFile"]}}'
```
Put `absMetadata` before `audioMetatags` if you want API-set metadata to take priority over embedded tags.

## Cover Upload

Upload cover images via multipart form. **Unlike Plex, ABS accepts multipart correctly** — use `-F`, not `--data-binary`.

```bash
curl -s -X POST -H "Authorization: Bearer $TOKEN" \
  -F "cover=@/path/to/cover.jpg;type=image/jpeg" \
  "http://127.0.0.1:13378/api/items/{itemId}/cover"
```

Cover sources (Audible.de): Get the ASIN, then download from `https://m.media-amazon.com/images/I/{imageId}._SL1215_.jpg`. The product API `product_images` field gives 500px URLs — replace `_SL500_` with `_SL1215_` for full resolution.

### Cover upload from Audible API (Python)

```python
import urllib.request, json

# Get cover URL from Audible API
url = f"https://api.audible.de/1.0/catalog/products/{asin}?response_groups=media"
req = urllib.request.Request(url, headers={"User-Agent": "...", "Accept": "application/json"})
data = json.loads(urllib.request.urlopen(req))
img_500 = data['product']['product_images']['500']
img_full = img_500.replace('_SL500_', '_SL1215_')

# Download
with open('/tmp/cover.jpg', 'wb') as f:
    f.write(urllib.request.urlopen(img_full).read())
```

## Full Fix Workflow

When ABS shows wrong author, narrator, or series:

### Simplified workflow (tags already correct, just series/covers missing)

When MP3s were already tagged with correct ID3 data (e.g., just tagged via audible-api-tagging workflow), ABS imports metadata correctly on first scan. You only need:

1. **Set series via PATCH** (array format, creates series if needed):
   ```bash
   curl -s -X PATCH -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
     "http://127.0.0.1:13378/api/items/{itemId}/media" \
     -d '{"metadata":{"series":[{"name":"Medici","sequence":"1"}]}}'
   ```
2. **Upload covers** (see Cover Upload section above)
3. **No delete/re-import needed** — ABS reads correct tags on initial scan. No metadata precedence changes required.

### Full workflow (when ID3 tags are wrong)

### 1. Diagnose
Check what ABS currently has:
```bash
curl -s -H "Authorization: Bearer $TOKEN" "http://127.0.0.1:13378/api/libraries/{libId}/items" | python3 -c "
import sys, json
for item in json.load(sys.stdin)['results']:
    m = item['media']['metadata']
    print(f\"{m['title']} | author={m.get('authorName')} | narrator={m.get('narratorName')} | series={m.get('series')}\")
"
```

### 2. Fix ID3 tags in MP3 files
The root cause is usually wrong ID3 tags. Fix them with mutagen:
```bash
sudo PYTHONPATH=/DATA/.local/lib/python3.12/site-packages python3 fix_tags.py
```

Key ID3 frames to fix:
- `TPE1` (Artist) → Author name
- `TPE2` (Album Artist) → Author name
- `TCOM` (Composer) → Narrator name
- `TALB` (Album) → Book title
- `TPUB` (Publisher) → Publisher name
- `TDRC` (Year) → Publication year

**Pitfall:** `mutagen` is installed at `/DATA/.local/lib/python3.12/site-packages/` but `sudo python3` doesn't see it. Use `sudo PYTHONPATH=/DATA/.local/lib/python3.12/site-packages python3`.

### 3. Delete and re-import
ABS caches old metadata even after tag fixes. Delete items and rescan:
```bash
for id in <item_ids>; do
  curl -s -X DELETE -H "Authorization: Bearer $TOKEN" "http://127.0.0.1:13378/api/items/$id"
done
curl -s -X POST -H "Authorization: Bearer $TOKEN" "http://127.0.0.1:13378/api/libraries/{libId}/scan"
sleep 8  # Wait for scan to complete
```

### 4. Set series and descriptions
After re-import, set series and descriptions via PATCH:
```bash
# Series (array format — creates series if needed)
curl -s -X PATCH ... -d '{"metadata":{"series":[{"name":"Medici","sequence":"1"}]}}'

# Description
curl -s -X PATCH ... -d '{"metadata":{"description":"Full description..."}}'
```

### 5. Verify
```bash
curl -s -H "Authorization: Bearer $TOKEN" "http://127.0.0.1:13378/api/items/{itemId}" | python3 -c "
import sys, json
m = json.load(sys.stdin)['media']['metadata']
print(f\"Series: {m.get('series')}\")
print(f\"Author: {m.get('authorName')}\")
print(f\"Narrator: {m.get('narratorName')}\")
print(f\"Desc: {(m.get('description') or '')[:100]}\")
"
```

## Pitfalls

- **`audioMetatags` overrides `absMetadata`:** API patches are silently overwritten on next scan if ID3 tags are wrong. Fix tags first.
- **`seriesName`/`seriesSequence` don't work:** ABS ignores these flat fields. Use the `series` array format: `[{"name":"...","sequence":"N"}]`.
- **`PATCH /api/items/{id}/media` with `series` array creates the series** if it doesn't exist — no need to create series separately.
- **`POST /api/series` returns 404:** ABS v2.30.0 doesn't expose a series creation endpoint. Use the item PATCH with series array instead.
- **Library list endpoint doesn't show series:** The `/api/libraries/{id}/items` list omits `series` from metadata. Use `/api/items/{id}` for single-item detail.
- **metadata.json on disk is overwritten on scan:** Writing directly to `/DATA/AppData/audiobookshelf/metadata/items/{id}/metadata.json` is lost on next scan. Use the API.
- **Mutagen needs `sudo PYTHONPATH`:** The system Python doesn't include user site-packages when run with sudo.
- **Audible API degraded (June 2026):** Search returns wrong results, product detail omits contributors. Scrape `audible.de/pd/{ASIN}` HTML for author/narrator/description.
- **Cover upload uses multipart:** ABS accepts `curl -F` correctly (unlike Plex which corrupts multipart). Use `-F "cover=@file.jpg;type=image/jpeg"`. Direct data-binary POST with `Content-Type: image/jpeg` returns 400 "no file or url".
- **`SL500` → `SL1215`:** Audible product_images returns 500px URLs. Replace `_SL500_` with `_SL1215_` in the URL for full-resolution covers.
