Read ID3 tags during scan — fixes 'Folge 114 Die Villa der Toten' problem

Diagnosis from connectivity check: 4/5 APIs reachable (only Google Books
rate-limited). So the network is fine — the search title was the problem.
'Folge 114 Die Villa der Toten' isn't indexed under that name anywhere.
The MP3 itself has the real metadata in ID3 tags (album, artist, year).

Scanner now reads ID3/Vorbis/MP4 tags from the first audio file:
- album → item.title
- albumartist / composer / artist → item.author
- date → publish_year
- organization / publisher → publisher
- language → language
- genre → genres
- artist (heuristic) → series, if it doesn't appear in album title

Parent folder name → series hint (skipped if it's a library root).

Only fills empty fields, never overwrites manually edited or matched data.
Runs on new items AND on re-scan for items without an active match.

Search title normalization improved: 'Folge 123 - X' / 'Band 7: Y' etc.
prefixes and infixes get stripped so APIs see the actual episode title.

New endpoint POST /api/items/{id}/extract-tags + 'Tags lesen' button in
BookDetail — triggers tag extraction on demand for existing items.
Returns before/after diff so user can see what was filled in.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Audiolib
2026-05-26 20:15:44 +02:00
parent 4fccb7abae
commit 0824894a7f
5 changed files with 192 additions and 2 deletions

View File

@@ -36,4 +36,7 @@ export const checkConnectivity = () =>
export const extractCover = (id: string) =>
api.post(`/api/items/${id}/extract-cover`).then((r) => r.data)
export const extractTags = (id: string) =>
api.post(`/api/items/${id}/extract-tags`).then((r) => r.data)
export const coverUrl = (id: string) => `/api/items/${id}/cover`

View File

@@ -4,7 +4,7 @@ import {
Play, ArrowLeft, RefreshCw, Search, Check,
Loader2, Trash2, X
} from 'lucide-react'
import { getItem, updateItem, triggerMatch, searchMatch, applyMatch, debugMatch, checkConnectivity, extractCover, coverUrl } from '../api/items'
import { getItem, updateItem, triggerMatch, searchMatch, applyMatch, debugMatch, checkConnectivity, extractCover, extractTags, coverUrl } from '../api/items'
import { getMe, createBookmark, deleteBookmark } from '../api/me'
import { usePlayerStore } from '../store/playerStore'
import CoverImage from '../components/common/CoverImage'
@@ -114,6 +114,19 @@ export default function BookDetail() {
}
}
const handleExtractTags = async () => {
if (!id) return
const res = await extractTags(id)
if (res.success) {
const updated = await getItem(id)
setItem(updated)
const tagKeys = Object.keys(res.tags || {})
alert(`Tags gelesen: ${tagKeys.join(', ') || '(keine)'}\n\nVorher: ${JSON.stringify(res.before, null, 2)}\n\nNachher: ${JSON.stringify(res.after, null, 2)}`)
} else {
alert(res.message || 'Keine Tags gefunden')
}
}
const fmtTime = (s: number) => {
const h = Math.floor(s / 3600)
const m = Math.floor((s % 3600) / 60)
@@ -218,6 +231,13 @@ export default function BookDetail() {
>
Cover aus Datei
</button>
<button
onClick={handleExtractTags}
className="flex items-center gap-2 bg-card border border-divider px-4 py-2 rounded-lg text-sm text-muted hover:text-ink hover:bg-card/80 transition-colors"
title="ID3-Tags (Artist, Album, Jahr) aus MP3 lesen und Metadaten füllen"
>
Tags lesen
</button>
<button
onClick={handleConnectivity}
disabled={connLoading}