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

@@ -115,11 +115,17 @@ function UsersPanel() {
)
}
const MATCH_SOURCES = [
{ id: 'musicbrainz', label: 'MusicBrainz' },
{ id: 'open_library', label: 'Open Library' },
{ id: 'google_books', label: 'Google Books' },
]
function LibraryForm({
initial, onSave, onCancel, title,
}: {
initial: { name: string; path: string; mediaType: string }
onSave: (v: { name: string; path: string; mediaType: string }) => Promise<void>
initial: { name: string; path: string; mediaType: string; matchSources: string[] }
onSave: (v: { name: string; path: string; mediaType: string; matchSources: string[] }) => Promise<void>
onCancel: () => void
title: string
}) {
@@ -161,6 +167,36 @@ function LibraryForm({
<option value="book">Hörbücher</option>
<option value="podcast">Podcasts</option>
</select>
{form.mediaType === 'book' && (
<div>
<p className="text-xs text-gray-500 mb-2">Matching-Quellen (Reihenfolge = Priorität)</p>
<div className="flex flex-wrap gap-2">
{MATCH_SOURCES.map((s) => {
const checked = form.matchSources.includes(s.id)
return (
<button
key={s.id}
type="button"
onClick={() => setForm({
...form,
matchSources: checked
? form.matchSources.filter((x) => x !== s.id)
: [...form.matchSources, s.id],
})}
className={`px-3 py-1 rounded-full text-xs border transition-colors ${
checked
? 'bg-primary/20 border-primary text-primary'
: 'bg-white/5 border-white/10 text-gray-400 hover:text-white'
}`}
>
{s.label}
</button>
)
})}
</div>
</div>
)}
<div className="flex gap-2">
<button onClick={submit} disabled={!form.name || !form.path || saving}
className="flex items-center gap-2 bg-primary text-black px-4 py-2 rounded-lg text-sm font-medium disabled:opacity-50">
@@ -190,14 +226,20 @@ function LibrariesPanel() {
setTimeout(() => setScanning(null), 5000)
}
const handleCreate = async (form: { name: string; path: string; mediaType: string }) => {
await createLibrary({ name: form.name, folders: [{ fullPath: form.path }], media_type: form.mediaType })
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,
settings: { match_sources: form.matchSources },
})
await reload()
setShowCreate(false)
}
const handleUpdate = async (id: string, form: { name: string; path: string; mediaType: string }) => {
await updateLibrary(id, { name: form.name, folders: [{ fullPath: form.path }], media_type: form.mediaType })
const handleUpdate = async (id: string, form: { name: string; path: string; mediaType: string; matchSources: string[] }) => {
await updateLibrary(id, {
name: form.name, folders: [{ fullPath: form.path }], media_type: form.mediaType,
settings: { match_sources: form.matchSources },
})
await reload()
setEditingId(null)
}
@@ -221,7 +263,7 @@ function LibrariesPanel() {
{showCreate && (
<LibraryForm
title="Neue Bibliothek"
initial={{ name: '', path: '', mediaType: 'book' }}
initial={{ name: '', path: '', mediaType: 'book', matchSources: ['musicbrainz', 'open_library', 'google_books'] }}
onSave={handleCreate}
onCancel={() => setShowCreate(false)}
/>
@@ -234,7 +276,7 @@ function LibrariesPanel() {
{editingId === lib.id ? (
<LibraryForm
title={`${lib.name}" bearbeiten`}
initial={{ name: lib.name, path: lib.folders?.[0]?.fullPath || '', mediaType: lib.mediaType || 'book' }}
initial={{ name: lib.name, path: lib.folders?.[0]?.fullPath || '', mediaType: lib.mediaType || 'book', matchSources: lib.matchSources || ['musicbrainz', 'open_library', 'google_books'] }}
onSave={(form) => handleUpdate(lib.id, form)}
onCancel={() => setEditingId(null)}
/>