Case-insensitive login, auto-matching after scan, per-library sources

- 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>
This commit is contained in:
Audiolib
2026-05-26 15:01:56 +02:00
parent b8984f0c2c
commit 6bb07ff873
6 changed files with 104 additions and 57 deletions

View File

@@ -17,6 +17,7 @@ from sqlalchemy import select
from ..config import get_settings
from ..models.media_item import LibraryItem, BookFile, Chapter
from ..models.library import Library
from ..models.session import ServerSetting
from ..database import AsyncSessionLocal
from .matching.base import MatchResult
@@ -154,9 +155,17 @@ async def _apply_match(db: AsyncSession, item: LibraryItem, result: MatchResult,
item.tags = [t for t in tags if t != "zu_prüfen"]
_SOURCE_FUNCS = {
"musicbrainz": (search_musicbrainz, get_release_details),
"open_library": (search_open_library, get_work_details),
"google_books": (search_google_books, None),
}
async def match_audiobook(item_id: str):
"""
Haupt-Matching-Funktion. Wird nach dem Scan als Hintergrund-Task gestartet.
Quellen und Reihenfolge werden aus den Library-Settings gelesen.
"""
async with AsyncSessionLocal() as db:
result_row = await db.execute(select(LibraryItem).where(LibraryItem.id == item_id))
@@ -164,7 +173,7 @@ async def match_audiobook(item_id: str):
if not item or item.match_locked:
return
# Einstellung prüfen
# Globale Auto-Match Einstellung prüfen
setting = await db.execute(
select(ServerSetting).where(ServerSetting.key == "autoMatchBooks")
)
@@ -172,10 +181,17 @@ async def match_audiobook(item_id: str):
if s and s.value is False:
return
# Matching-Quellen aus Library-Settings lesen
lib_row = await db.execute(select(Library).where(Library.id == item.library_id))
lib = lib_row.scalar_one_or_none()
sources: list[str] = (
(lib.settings or {}).get("match_sources", list(_SOURCE_FUNCS.keys()))
if lib else list(_SOURCE_FUNCS.keys())
)
title = item.title or ""
author = item.author
# Serien-Erkennung verbessert den Suchbegriff
series, episode = detect_series(title)
search_title = title
if series:
@@ -185,65 +201,42 @@ async def match_audiobook(item_id: str):
if not item.series_sequence and episode:
item.series_sequence = episode
logger.info(f"Matche: '{title}' (Serie: {series}, Folge: {episode})")
logger.info(f"Matche: '{title}' | Quellen: {sources}")
best: MatchResult | None = None
best_score = 0.0
# 1. MusicBrainz
try:
mb_results = await search_musicbrainz(search_title, author)
for r in mb_results:
score = _score_result(r, title, author)
if score > best_score:
best_score = score
best = r
except Exception as e:
logger.warning(f"MusicBrainz Fehler: {e}")
# Wenn guter MB-Treffer → Details holen (Tracklist + Cover)
if best and best_score >= UNCERTAIN_THRESHOLD and best.source == "musicbrainz":
for source_name in sources:
if best_score >= UNCERTAIN_THRESHOLD:
break
funcs = _SOURCE_FUNCS.get(source_name)
if not funcs:
continue
search_func, details_func = funcs
try:
details = await get_release_details(best.source_id)
if details:
details.confidence = best_score
best = details
except Exception as e:
logger.warning(f"MusicBrainz Details Fehler: {e}")
# 2. OpenLibrary als Fallback
if best_score < UNCERTAIN_THRESHOLD:
try:
ol_results = await search_open_library(search_title, author)
for r in ol_results:
results = await search_func(search_title, author)
for r in results:
score = _score_result(r, title, author)
if score > best_score:
best_score = score
best = r
if best and best.source == "open_library" and best_score >= UNCERTAIN_THRESHOLD:
details = await get_work_details(best.source_id)
if details and details.description:
best.description = details.description
# Details holen wenn Treffer gut genug (z.B. MB Tracklist)
if best and best.source == source_name and best_score >= UNCERTAIN_THRESHOLD and details_func:
try:
details = await details_func(best.source_id)
if details:
details.confidence = best_score
best = details
except Exception as e:
logger.warning(f"{source_name} Details Fehler: {e}")
except Exception as e:
logger.warning(f"OpenLibrary Fehler: {e}")
# 3. Google Books als letzter Fallback
if best_score < UNCERTAIN_THRESHOLD:
try:
gb_results = await search_google_books(search_title, author)
for r in gb_results:
score = _score_result(r, title, author)
if score > best_score:
best_score = score
best = r
except Exception as e:
logger.warning(f"Google Books Fehler: {e}")
logger.warning(f"{source_name} Fehler: {e}")
if best and best_score >= UNCERTAIN_THRESHOLD:
await _apply_match(db, item, best, best_score)
logger.info(f"Match angewendet: '{item.title}'{best.source} (Konfidenz: {best_score:.2f})")
logger.info(f"Match angewendet: '{item.title}'{best.source} ({best_score:.2f})")
else:
logger.info(f"Kein Match gefunden für '{title}' (beste Konfidenz: {best_score:.2f})")
logger.info(f"Kein Match für '{title}' (beste Konfidenz: {best_score:.2f})")
await db.commit()