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:
@@ -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()
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import os
|
||||
import uuid
|
||||
import asyncio
|
||||
import logging
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
@@ -110,6 +111,7 @@ async def scan_library_task(library_id: str, job_id: str):
|
||||
all_books.extend(_discover_audiobook_folders(folder_path))
|
||||
|
||||
items_found = 0
|
||||
new_item_ids: list[str] = []
|
||||
for book_info in all_books:
|
||||
folder_path = book_info["path"]
|
||||
audio_files = book_info["files"]
|
||||
@@ -164,6 +166,8 @@ async def scan_library_task(library_id: str, job_id: str):
|
||||
)
|
||||
db.add(bf)
|
||||
|
||||
new_item_ids.append(item.id)
|
||||
|
||||
items_found += 1
|
||||
|
||||
await db.commit()
|
||||
@@ -187,6 +191,12 @@ async def scan_library_task(library_id: str, job_id: str):
|
||||
|
||||
logger.info(f"Scan abgeschlossen: {items_found} Items in Library {library_id}")
|
||||
|
||||
# Auto-Matching für neue Items starten
|
||||
if new_item_ids:
|
||||
from .matcher import match_audiobook
|
||||
for iid in new_item_ids:
|
||||
asyncio.create_task(match_audiobook(iid))
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Scan-Fehler für Library {library_id}: {e}", exc_info=True)
|
||||
async with AsyncSessionLocal() as err_db:
|
||||
|
||||
Reference in New Issue
Block a user