Diagnosis from connectivity check: 4/5 APIs reachable (only Google Books
rate-limited). So the network is fine — the search title was the problem.
'Folge 114 Die Villa der Toten' isn't indexed under that name anywhere.
The MP3 itself has the real metadata in ID3 tags (album, artist, year).
Scanner now reads ID3/Vorbis/MP4 tags from the first audio file:
- album → item.title
- albumartist / composer / artist → item.author
- date → publish_year
- organization / publisher → publisher
- language → language
- genre → genres
- artist (heuristic) → series, if it doesn't appear in album title
Parent folder name → series hint (skipped if it's a library root).
Only fills empty fields, never overwrites manually edited or matched data.
Runs on new items AND on re-scan for items without an active match.
Search title normalization improved: 'Folge 123 - X' / 'Band 7: Y' etc.
prefixes and infixes get stripped so APIs see the actual episode title.
New endpoint POST /api/items/{id}/extract-tags + 'Tags lesen' button in
BookDetail — triggers tag extraction on demand for existing items.
Returns before/after diff so user can see what was filled in.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
DNB rewrite:
- Multiple query strategies with fallback (title+author+mat=ton →
title+author → title+mat=ton → title-only → fulltext). Returns on
first hit. Most German audiobooks aren't tagged mat=ton in DNB,
which was killing all searches.
- Strip CQL wildcard chars (?, *, <, >, =, /, quotes) from search
terms. The "???" in "Die drei ???" was breaking the CQL parser.
- Log HTTP status, body snippet on non-200, and numberOfRecords on
every query so log shows exactly what DNB returned.
- Parse SRU diagnostic elements (DNB error messages buried in XML).
- Convert author/narrator from "Lastname, Firstname" to
"Firstname Lastname" for consistency with other sources.
Matcher:
- Split series patterns: WITH_EPISODE (need digit) and SERIES_ONLY
(just the series name). "Die drei ??? und der Fluch des Rubins"
now properly detects "Die drei ???" as series even without folge#.
- New _build_search_title: removes ??? sequences, trailing parens,
collapses whitespace, before sending to APIs.
- Manual search also passes through normalization. Logs source +
hit count per query.
Debug endpoint:
- GET /api/items/match/debug?title=...&author=... returns raw results
from all 4 sources with status, error messages, and full metadata.
- "Debug" button added in BookDetail — shows what each API actually
returns inline, so the user can see if it's a search problem,
parse problem, or threshold problem.
- "Cover aus Datei" button — triggers local cover extraction
(folder.jpg or embedded artwork) on demand.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Streaming: Custom range-aware HTTP endpoint. Returns 206 Partial Content
for Range requests (with Content-Range, Content-Length, Accept-Ranges).
This was the root cause of broken seeking — Starlette's default
FileResponse behavior wasn't reliable across all clients. Now seeking
works natively via standard HTML5 audio.
Player: Full rewrite. Cleaner separation between absolute book time and
per-track time. Track switching uses pendingSeek + canplay/loadedmetadata
handlers. Console logs for debugging. Removed crossOrigin to avoid CORS
issues. Removed hls.js entirely.
Matcher: Critical bug fix — get_work_details (OpenLibrary) was returning
a sparse MatchResult that REPLACED the rich search result, losing cover,
author, year. New _enrich_match merges details into best without
overwriting existing values (except description/chapters which are
preferred from details fetch).
Scoring: Lenient min/max-weighted similarity (better for German episodic
titles like "Die drei ??? - Folge 215"). Thresholds lowered:
UNCERTAIN 0.50→0.40, AUTO_ACCEPT 0.75→0.65.
Search: search_for_item now returns ALL fields (narrator, publisher,
series, genres, description, language) so manual apply has full data.
Apply: apply_match now always constructs from body first, then enriches
with details. Previously OL applies would lose cover/author. Added
detailed logging across matcher and apply paths.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- MatchResult was missing subtitle field, causing AttributeError in
_apply_match that silently killed every background match task
- Wrap _apply_match in try/except with exc_info logging so failures
are visible in docker compose logs backend
- New POST /api/libraries/:id/match-all endpoint to trigger matching
for all unlocked items (useful for items scanned before the fix)
- Admin UI: Match button per library next to the Scan button
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- auth: username lookup is now case-insensitive via func.lower()
- scanner: trigger match_audiobook for each newly found item after scan
- matcher: read match_sources from library settings; refactored to loop
over configured sources in priority order instead of hardcoded sequence
- schemas/routers: expose matchSources in LibraryOut API response
- Admin UI: pill-toggle for MusicBrainz/Open Library/Google Books per library
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>