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:
@@ -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)}
|
||||
/>
|
||||
|
||||
Reference in New Issue
Block a user