Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 42 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,48 @@

All notable changes to Library Manager will be documented in this file.

## [0.9.0-beta.153] - 2026-06-12

### Security
- **#255: Validate and sanitize BookDB response data** — Added `_sanitize_api_response()` to strip null bytes, control characters, HTML tags, and truncate oversized fields from all Skaldleita/BookDB API responses. Prevents path traversal, XSS, and memory issues from malformed responses.
- **#256: Signing nonce and tighter replay window** — Added UUID nonce (`X-LM-Nonce`) to signed request headers for replay attack prevention. Reduced timestamp tolerance from 300s to 120s. Backwards compatible with existing BookDB.
- **#236: SSRF prevention on plugin endpoints** — (from 0.9.0-beta.152)

### Fixed
- **#252: Rate limiter aligned with actual BookDB tiers** — Increased min_delay from 3.6s (1000/hr) to 12.0s (300/hr) to match BookDB's API key tier. Added adaptive backoff from Retry-After headers and specific 429 handling in audio identification.
- **#253: server_notice handled in audio identification** — BookDB's abort/notice signals are now processed in `identify_audio_with_bookdb()` (submit, poll, and result stages), matching existing coverage in `search_bookdb()`.

### Enhanced
- **#254: Differentiated BookDB source trust weighting** — `sl_source` field now maps to distinct source names (`bookdb_audio`, `bookdb_transcript`, `bookdb_scrape`) with graduated weights (65/55/45) instead of treating all BookDB audio results identically.

### Documentation
- **#257: ABS plugin architecture** — Added `docs/ABS-Plugin-Architecture.md` scoping Library Manager as an Audiobookshelf metadata provider plugin.

---

## [0.9.0-beta.152] - 2026-06-12

### Security
- **#219: Library boundary enforcement** on undo/replace/remove file operations
- **#237: XSS fix** — escapeHtml covers all 5 HTML entities including single quotes
- **#236: SSRF prevention** on custom plugin endpoints

### Fixed
- **#220: Atomic status guard** on apply_fix prevents race condition double-renames
- **#221: Worker stop checks** in all 4 legacy batch loops
- **#222-224: Rate limits and circuit breakers** for BookDB contribute, community consensus, Gemini language detect, OpenRouter identify, plus process endpoint re-entrancy guard
- **#225-226: Undo parent mkdir** and path sanitize false positive fix
- **#227: L1 preserves original metadata** on validation failure
- **#228-229: Pipeline stuck prevention** — L3 trust_sl no longer deletes queue entry before L4, AI verify creates history on null drastic check
- **#230: Custom source weights** now actually used in confidence calculations
- **#231: Fixed broken route** in queue multi-edit modal
- **#232: Settings save** now persists sl_trust_mode and enable_isbn_lookup, removed duplicate field
- **#233: Double-click prevention** on all 11 history page action buttons
- **#234: Famous numeric titles** (1984, 2001, etc.) no longer flagged as garbage
- **#235: Confidence scale normalization** handles both 0-1 and 0-100 scales

---

## [0.9.0-beta.151] - 2026-06-05

### Changed
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

**Smart Audiobook Library Organizer with Multi-Source Metadata & AI Verification**

[![Version](https://img.shields.io/badge/version-0.9.0--beta.152-blue.svg)](CHANGELOG.md)
[![Version](https://img.shields.io/badge/version-0.9.0--beta.153-blue.svg)](CHANGELOG.md)
[![Docker](https://img.shields.io/badge/docker-ghcr.io-blue.svg)](https://ghcr.io/deucebucket/library-manager)
[![License](https://img.shields.io/badge/license-AGPL--3.0-blue.svg)](LICENSE)

Expand Down
2 changes: 1 addition & 1 deletion app.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
- Multi-provider AI (Gemini, OpenRouter, Ollama)
"""

APP_VERSION = "0.9.0-beta.152"
APP_VERSION = "0.9.0-beta.153"
GITHUB_REPO = "deucebucket/library-manager" # Your GitHub repo

# Versioning Guide:
Expand Down Expand Up @@ -738,7 +738,7 @@
try:
with open(ERROR_REPORTS_PATH, 'r') as f:
reports = json.load(f)
except:

Check failure on line 741 in app.py

View workflow job for this annotation

GitHub Actions / lint

ruff (E722)

app.py:741:13: E722 Do not use bare `except`

Check failure on line 741 in app.py

View workflow job for this annotation

GitHub Actions / lint

ruff (E722)

app.py:741:13: E722 Do not use bare `except`
reports = []

# Add new report (keep last 100 reports to avoid file bloat)
Expand All @@ -762,7 +762,7 @@
try:
with open(ERROR_REPORTS_PATH, 'r') as f:
return json.load(f)
except:

Check failure on line 765 in app.py

View workflow job for this annotation

GitHub Actions / lint

ruff (E722)

app.py:765:9: E722 Do not use bare `except`

Check failure on line 765 in app.py

View workflow job for this annotation

GitHub Actions / lint

ruff (E722)

app.py:765:9: E722 Do not use bare `except`
return []
return []

Expand Down Expand Up @@ -1717,7 +1717,7 @@
continue
result = call_gemini(prompt, merged_config)
if result:
logger.info(f"[PROVIDER CHAIN] Success with gemini")

Check failure on line 1720 in app.py

View workflow job for this annotation

GitHub Actions / lint

ruff (F541)

app.py:1720:33: F541 f-string without any placeholders help: Remove extraneous `f` prefix

Check failure on line 1720 in app.py

View workflow job for this annotation

GitHub Actions / lint

ruff (F541)

app.py:1720:33: F541 f-string without any placeholders help: Remove extraneous `f` prefix
return result

elif provider == 'openrouter':
Expand All @@ -1726,13 +1726,13 @@
continue
result = call_openrouter(prompt, merged_config)
if result:
logger.info(f"[PROVIDER CHAIN] Success with openrouter")

Check failure on line 1729 in app.py

View workflow job for this annotation

GitHub Actions / lint

ruff (F541)

app.py:1729:33: F541 f-string without any placeholders help: Remove extraneous `f` prefix

Check failure on line 1729 in app.py

View workflow job for this annotation

GitHub Actions / lint

ruff (F541)

app.py:1729:33: F541 f-string without any placeholders help: Remove extraneous `f` prefix
return result

elif provider == 'ollama':
result = call_ollama(prompt, merged_config)
if result:
logger.info(f"[PROVIDER CHAIN] Success with ollama")

Check failure on line 1735 in app.py

View workflow job for this annotation

GitHub Actions / lint

ruff (F541)

app.py:1735:33: F541 f-string without any placeholders help: Remove extraneous `f` prefix

Check failure on line 1735 in app.py

View workflow job for this annotation

GitHub Actions / lint

ruff (F541)

app.py:1735:33: F541 f-string without any placeholders help: Remove extraneous `f` prefix
return result

else:
Expand Down Expand Up @@ -1834,7 +1834,7 @@
return result
elif result and result.get('transcript'):
# Got transcript but no match - still useful, return for potential AI fallback
logger.info(f"[AUDIO CHAIN] BookDB returned transcript only")

Check failure on line 1837 in app.py

View workflow job for this annotation

GitHub Actions / lint

ruff (F541)

app.py:1837:37: F541 f-string without any placeholders help: Remove extraneous `f` prefix

Check failure on line 1837 in app.py

View workflow job for this annotation

GitHub Actions / lint

ruff (F541)

app.py:1837:37: F541 f-string without any placeholders help: Remove extraneous `f` prefix
return result
elif result is None and attempt < max_retries - 1:
# Connection might be down, wait and retry
Expand Down Expand Up @@ -2166,11 +2166,11 @@
device = "cuda"
# int8 works on all CUDA devices including GTX 1080 (compute 6.1)
# float16 only works on newer GPUs (compute 7.0+)
logger.info(f"[WHISPER] Using CUDA GPU acceleration (10x faster)")

Check failure on line 2169 in app.py

View workflow job for this annotation

GitHub Actions / lint

ruff (F541)

app.py:2169:29: F541 f-string without any placeholders help: Remove extraneous `f` prefix

Check failure on line 2169 in app.py

View workflow job for this annotation

GitHub Actions / lint

ruff (F541)

app.py:2169:29: F541 f-string without any placeholders help: Remove extraneous `f` prefix
else:
logger.info(f"[WHISPER] Using CPU (no CUDA GPU detected)")

Check failure on line 2171 in app.py

View workflow job for this annotation

GitHub Actions / lint

ruff (F541)

app.py:2171:29: F541 f-string without any placeholders help: Remove extraneous `f` prefix

Check failure on line 2171 in app.py

View workflow job for this annotation

GitHub Actions / lint

ruff (F541)

app.py:2171:29: F541 f-string without any placeholders help: Remove extraneous `f` prefix
except ImportError:
logger.info(f"[WHISPER] Using CPU (ctranslate2 not available)")

Check failure on line 2173 in app.py

View workflow job for this annotation

GitHub Actions / lint

ruff (F541)

app.py:2173:25: F541 f-string without any placeholders help: Remove extraneous `f` prefix

Check failure on line 2173 in app.py

View workflow job for this annotation

GitHub Actions / lint

ruff (F541)

app.py:2173:25: F541 f-string without any placeholders help: Remove extraneous `f` prefix

_whisper_model = WhisperModel(model_name, device=device, compute_type=compute_type)
_whisper_model_name = model_name
Expand Down Expand Up @@ -2381,7 +2381,7 @@
if sample_path and os.path.exists(sample_path):
try:
os.unlink(sample_path)
except:

Check failure on line 2384 in app.py

View workflow job for this annotation

GitHub Actions / lint

ruff (E722)

app.py:2384:13: E722 Do not use bare `except`

Check failure on line 2384 in app.py

View workflow job for this annotation

GitHub Actions / lint

ruff (E722)

app.py:2384:13: E722 Do not use bare `except`
pass

return result
Expand Down
252 changes: 252 additions & 0 deletions docs/ABS-Plugin-Architecture.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,252 @@
# Audiobookshelf Plugin Architecture for Library Manager

## Overview

Library Manager as a metadata provider plugin for Audiobookshelf (ABS). When ABS needs to identify or enrich audiobook metadata, it queries Library Manager, which leverages the full Skaldleita pipeline: GPU Whisper transcription, 50M+ book database matching, multi-source API lookups, and AI-powered consensus verification.

**Current state:** LM has an `abs_client.py` that pulls data FROM ABS (listening progress, library items, user management). This document covers the reverse direction — making LM available TO ABS as a metadata agent.

---

## Data Flow

```
Audiobookshelf Library Manager Skaldleita (BookDB)
│ │ │
│ 1. "Identify this book" │ │
│ ─────────────────────────> │ │
│ (title, author, audio clip) │ │
│ │ 2. Audio → Whisper queue │
│ │ ─────────────────────────> │
│ │ │
│ │ 3. Metadata match (50M DB) │
│ │ <───────────────────────── │
│ │ │
│ │ 4. API enrichment │
│ │ (Audnexus, Google Books, │
│ │ OpenLibrary, Hardcover) │
│ │ │
│ │ 5. AI consensus verify │
│ │ (Gemini / OpenRouter) │
│ │ │
│ 6. Enriched metadata │ │
│ <───────────────────────── │ │
│ (author, title, narrator, │ │
│ series, year, confidence) │ │
```

---

## Architecture Options

### Option A: LM as ABS Metadata Provider (Recommended)

ABS has a metadata provider plugin system. LM registers as a provider that ABS queries during its "Match" and "Quick Match" flows.

**How it works:**
- ABS sends search queries to LM's API
- LM runs its full pipeline (BookDB + multi-source + AI verification)
- LM returns structured metadata in ABS's expected format
- ABS users see LM results alongside other providers (Google Books, Audible, etc.)

**LM endpoints needed:**
```
GET /api/abs/search?query=<title>&author=<author>
→ Returns ABS-formatted search results

GET /api/abs/match?title=<title>&author=<author>&narrator=<narrator>
→ Returns single best match with confidence

POST /api/abs/identify-audio
→ Accepts audio clip, returns identification via Skaldleita Whisper

GET /api/abs/cover?title=<title>&author=<author>
→ Returns cover image URL if available
```

**Pros:** Native ABS integration, users see LM in their provider list
**Cons:** ABS metadata provider API has a specific contract we must match exactly

### Option B: LM as Standalone Enrichment Service

LM runs alongside ABS and periodically scans the ABS library for books with missing/low-quality metadata, then pushes corrections back via ABS API.

**How it works:**
- LM uses existing `abs_client.py` to read ABS library
- Identifies books with missing narrators, series info, etc.
- Runs its pipeline to find correct metadata
- Pushes updates back to ABS via API

**Pros:** Works without ABS plugin system, LM controls the schedule
**Cons:** Delayed updates, LM needs write access to ABS

### Option C: Hybrid (Both A + B)

Register as metadata provider AND run background enrichment. Provider handles new additions; background scan catches existing gaps.

---

## Recommended Approach: Option A (Metadata Provider)

### ABS Metadata Provider Contract

ABS expects providers to implement these operations:

```json
// Search request
GET /search?query=Brandon+Sanderson+Mistborn

// Search response
{
"matches": [
{
"title": "The Final Empire",
"subtitle": "Mistborn Book 1",
"author": "Brandon Sanderson",
"narrator": "Michael Kramer",
"publisher": "Macmillan Audio",
"publishedYear": "2006",
"description": "...",
"cover": "https://...",
"isbn": "9780765350381",
"asin": "B002V1O7UE",
"series": [
{ "series": "Mistborn", "sequence": "1" }
],
"language": "en",
"duration": 95940
}
]
}
```

### LM → ABS Field Mapping

| ABS Field | LM Source | Notes |
|-----------|-----------|-------|
| `title` | BookProfile.title | Highest confidence value |
| `author` | BookProfile.author | Consensus from all sources |
| `narrator` | BookProfile.narrator | Often from audio credits (L2) or Audnexus |
| `series[].series` | BookProfile.series | From BookDB or API lookups |
| `series[].sequence` | BookProfile.series_num | Position in series |
| `publishedYear` | BookProfile.year | From API lookups |
| `cover` | External API | Audnexus or Google Books cover URL |
| `isbn` | API lookups | Google Books or OpenLibrary |
| `asin` | Audnexus | Audible ASIN if available |
| `duration` | Audio file analysis | From ffprobe or ABS library item |
| `language` | Gemini detection | Audio language detection (L2) |
| `description` | API enrichment | Google Books or OpenLibrary |

### Confidence Translation

LM uses 0-100 confidence scores. ABS doesn't have a native confidence field, but we can:
1. Only return results above a configurable threshold (default: 60)
2. Sort results by confidence (best first)
3. Include confidence in a custom field for display

### New LM Files

```
library_manager/abs_provider.py # ABS metadata provider Blueprint
- /api/abs/search # Search endpoint
- /api/abs/match # Best-match endpoint
- /api/abs/identify # Audio identification endpoint
- /api/abs/covers/<isbn> # Cover proxy
```

### Authentication

Options:
1. **Shared secret** — LM generates an API key, user enters it in ABS custom provider config
2. **Token exchange** — ABS sends its API token, LM validates via ABS `/api/me` endpoint
3. **None (local only)** — If both run on same machine, trust localhost

Recommend: Shared secret (simple, works across networks).

### Configuration (LM Settings Page)

Add to the Integrations tab:

```
ABS Metadata Provider
├── Enable as ABS metadata source: [toggle]
├── Provider API Key: [auto-generated, copyable]
├── Minimum confidence threshold: [slider 0-100, default 60]
├── Include audio identification: [toggle, default on]
└── Provider URL for ABS config: http://<lm-host>:5757/api/abs
```

---

## Security Considerations

### What This Opens
- A new API surface (`/api/abs/*`) accessible from the network
- Audio clip uploads from ABS to LM
- LM's metadata forwarded to ABS (already LM's core function)

### Mitigations
- API key authentication on all `/api/abs/*` endpoints
- Rate limiting (reuse existing rate limiter infrastructure)
- Audio clip size limits (same as existing Skaldleita submission limits)
- Input validation on all query parameters (sanitize before pipeline)
- SSRF protection already in place for plugin endpoints (issue #236)
- No new filesystem access — ABS provider only reads/returns metadata

### What NOT to Expose
- Direct Skaldleita API passthrough (LM should be the intermediary, not a proxy)
- BookDB API keys or signing credentials
- Internal BookProfile objects (serialize to ABS format only)
- Direct database access

---

## Implementation Phases

### Phase 1: Search Provider
- New Blueprint: `abs_provider.py`
- `/api/abs/search` endpoint using existing `search_bookdb()` + API lookups
- API key auth
- Settings UI toggle
- Basic test with ABS custom provider

### Phase 2: Audio Identification
- `/api/abs/identify` endpoint
- Accept audio clips from ABS
- Route through Skaldleita Whisper pipeline
- Return structured results

### Phase 3: Background Enrichment (Option B hybrid)
- Scan ABS library via existing `abs_client.py`
- Queue items with missing metadata
- Push enrichments back to ABS
- Configurable schedule (daily/weekly)

### Phase 4: Cover Art + Descriptions
- `/api/abs/covers/<isbn>` proxy endpoint
- Cache covers locally
- Pull descriptions from Google Books / OpenLibrary enrichment

---

## ABS Custom Provider Setup (User-Facing)

In ABS Settings → Providers → Custom:
```
Name: Library Manager
Base URL: http://<lm-ip>:5757/api/abs
API Key: <paste from LM settings>
```

Then in ABS, when matching a book:
1. Click "Match" on a library item
2. Select "Library Manager" as provider
3. See results from LM's multi-source pipeline
4. One-click apply metadata

---

## Related Issues
- #257 — This feature
- #252-#256 — Integration fixes that improve the pipeline feeding this provider
- abs_client.py — Existing read-only ABS integration (complements this)
5 changes: 4 additions & 1 deletion library_manager/models/book_profile.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,10 @@
'id3': 80, # Embedded by producer/publisher
'json': 75, # Explicit metadata file
'nfo': 70, # Release info files
'bookdb': 65, # Verified database match
'bookdb': 65, # Verified database match
'bookdb_audio': 65, # BookDB database match (same as bookdb)
'bookdb_transcript': 55, # BookDB Whisper transcript only (unverified)
'bookdb_scrape': 45, # BookDB live web scrape (unreliable)
'ai': 60, # AI verification
'audnexus': 55, # Audible data
'googlebooks': 50, # Google Books API
Expand Down
14 changes: 8 additions & 6 deletions library_manager/pipeline/layer_audio_id.py
Original file line number Diff line number Diff line change
Expand Up @@ -714,18 +714,20 @@ def process_layer_1_audio(
c = conn.cursor()

# Update book with audio-identified info
sl_source = result.get('sl_source', 'audio_transcription')
# Use the mapped source name (bookdb_audio/bookdb_transcript/bookdb_scrape)
# for proper trust weighting in BookProfile, falling back to sl_source
profile_source = result.get('source', result.get('sl_source', 'audio_transcription'))
base_confidence = 85 if confidence == 'high' else 70
profile = {
'author': {'value': author, 'source': sl_source, 'confidence': base_confidence},
'title': {'value': title, 'source': sl_source, 'confidence': base_confidence},
'author': {'value': author, 'source': profile_source, 'confidence': base_confidence},
'title': {'value': title, 'source': profile_source, 'confidence': base_confidence},
}
if narrator:
profile['narrator'] = {'value': narrator, 'source': sl_source, 'confidence': 80}
profile['narrator'] = {'value': narrator, 'source': profile_source, 'confidence': 80}
if series:
profile['series'] = {'value': series, 'source': sl_source, 'confidence': 75}
profile['series'] = {'value': series, 'source': profile_source, 'confidence': 75}
if series_num:
profile['series_num'] = {'value': str(series_num), 'source': sl_source, 'confidence': 75}
profile['series_num'] = {'value': str(series_num), 'source': profile_source, 'confidence': 75}

# Issue #227: Save original author/title BEFORE updating the DB.
# If validation fails later, we must revert these to prevent
Expand Down
Loading
Loading