diff --git a/backend/app/routers/libraries.py b/backend/app/routers/libraries.py index e08b710..b4eb762 100644 --- a/backend/app/routers/libraries.py +++ b/backend/app/routers/libraries.py @@ -219,3 +219,33 @@ async def scan_library( background_tasks.add_task(scan_library_task, library_id, job.id) return {"id": job.id, "type": "scan", "libraryId": library_id} + + +@router.post("/{library_id}/match-all") +async def match_all_items( + library_id: str, + background_tasks: BackgroundTasks, + admin: User = Depends(require_admin), + db: AsyncSession = Depends(get_db), +): + """Startet Matching für alle nicht-gematchten Items der Library.""" + result = await db.execute(select(Library).where(Library.id == library_id)) + if not result.scalar_one_or_none(): + raise HTTPException(status_code=404, detail="Library not found") + + items_result = await db.execute( + select(LibraryItem).where( + LibraryItem.library_id == library_id, + LibraryItem.match_locked == False, + ) + ) + items = items_result.scalars().all() + item_ids = [i.id for i in items] + + async def _run_all(): + from ..services.matcher import match_audiobook + for iid in item_ids: + await match_audiobook(iid) + + background_tasks.add_task(_run_all) + return {"queued": len(item_ids), "libraryId": library_id} diff --git a/backend/app/services/matcher.py b/backend/app/services/matcher.py index 5f7a2cc..b4092fe 100644 --- a/backend/app/services/matcher.py +++ b/backend/app/services/matcher.py @@ -233,12 +233,15 @@ async def match_audiobook(item_id: str): 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} ({best_score:.2f})") + try: + await _apply_match(db, item, best, best_score) + await db.commit() + logger.info(f"Match angewendet: '{item.title}' ← {best.source} ({best_score:.2f})") + except Exception as e: + logger.error(f"_apply_match fehlgeschlagen für '{title}': {e}", exc_info=True) else: logger.info(f"Kein Match für '{title}' (beste Konfidenz: {best_score:.2f})") - - await db.commit() + await db.commit() async def search_for_item(title: str, author: str | None = None) -> list[dict]: diff --git a/backend/app/services/matching/base.py b/backend/app/services/matching/base.py index ca98985..50129c6 100644 --- a/backend/app/services/matching/base.py +++ b/backend/app/services/matching/base.py @@ -7,6 +7,7 @@ class MatchResult: source: str # musicbrainz / open_library / google_books source_id: str title: str + subtitle: str | None = None author: str | None = None narrator: str | None = None description: str | None = None diff --git a/frontend/src/api/libraries.ts b/frontend/src/api/libraries.ts index 8678cb1..fa60406 100644 --- a/frontend/src/api/libraries.ts +++ b/frontend/src/api/libraries.ts @@ -14,6 +14,9 @@ export const searchLibrary = (libraryId: string, q: string) => export const scanLibrary = (libraryId: string) => api.post(`/api/libraries/${libraryId}/scan`).then((r) => r.data) +export const matchAllLibrary = (libraryId: string) => + api.post(`/api/libraries/${libraryId}/match-all`).then((r) => r.data) + export const createLibrary = (data: object) => api.post('/api/libraries', data).then((r) => r.data) diff --git a/frontend/src/pages/Admin.tsx b/frontend/src/pages/Admin.tsx index b358b66..35b7c80 100644 --- a/frontend/src/pages/Admin.tsx +++ b/frontend/src/pages/Admin.tsx @@ -1,7 +1,7 @@ import React, { useEffect, useState } from 'react' -import { Users, Library, Settings, Trash2, Plus, RefreshCw, Loader2, Check, X, FolderOpen, Pencil } from 'lucide-react' +import { Users, Library, Settings, Trash2, Plus, RefreshCw, Loader2, Check, X, FolderOpen, Pencil, Sparkles } from 'lucide-react' import { getUsers, createUser, deleteUser, getSettings, updateSettings } from '../api/me' -import { getLibraries, scanLibrary, createLibrary, updateLibrary, deleteLibrary } from '../api/libraries' +import { getLibraries, scanLibrary, matchAllLibrary, createLibrary, updateLibrary, deleteLibrary } from '../api/libraries' import FileBrowser from '../components/common/FileBrowser' type Tab = 'users' | 'libraries' | 'settings' @@ -214,6 +214,7 @@ function LibrariesPanel() { const [libraries, setLibraries] = useState([]) const [loading, setLoading] = useState(true) const [scanning, setScanning] = useState(null) + const [matching, setMatching] = useState(null) const [showCreate, setShowCreate] = useState(false) const [editingId, setEditingId] = useState(null) @@ -226,6 +227,12 @@ function LibrariesPanel() { setTimeout(() => setScanning(null), 5000) } + const handleMatchAll = async (id: string) => { + setMatching(id) + await matchAllLibrary(id).catch(() => {}) + setTimeout(() => setMatching(null), 3000) + } + const handleCreate = async (form: { name: string; path: string; mediaType: string; matchSources: string[] }) => { await createLibrary({ name: form.name, folders: [{ fullPath: form.path }], media_type: form.mediaType, @@ -293,6 +300,14 @@ function LibrariesPanel() { Scan + {lib.mediaType !== 'podcast' && ( + + )}