Make matching debuggable + fix metadata search blockers
DNB rewrite: - Multiple query strategies with fallback (title+author+mat=ton → title+author → title+mat=ton → title-only → fulltext). Returns on first hit. Most German audiobooks aren't tagged mat=ton in DNB, which was killing all searches. - Strip CQL wildcard chars (?, *, <, >, =, /, quotes) from search terms. The "???" in "Die drei ???" was breaking the CQL parser. - Log HTTP status, body snippet on non-200, and numberOfRecords on every query so log shows exactly what DNB returned. - Parse SRU diagnostic elements (DNB error messages buried in XML). - Convert author/narrator from "Lastname, Firstname" to "Firstname Lastname" for consistency with other sources. Matcher: - Split series patterns: WITH_EPISODE (need digit) and SERIES_ONLY (just the series name). "Die drei ??? und der Fluch des Rubins" now properly detects "Die drei ???" as series even without folge#. - New _build_search_title: removes ??? sequences, trailing parens, collapses whitespace, before sending to APIs. - Manual search also passes through normalization. Logs source + hit count per query. Debug endpoint: - GET /api/items/match/debug?title=...&author=... returns raw results from all 4 sources with status, error messages, and full metadata. - "Debug" button added in BookDetail — shows what each API actually returns inline, so the user can see if it's a search problem, parse problem, or threshold problem. - "Cover aus Datei" button — triggers local cover extraction (folder.jpg or embedded artwork) on demand. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -27,4 +27,10 @@ export const applyMatch = (id: string, match: object) =>
|
||||
export const triggerMatch = (id: string) =>
|
||||
api.post(`/api/items/${id}/match`).then((r) => r.data)
|
||||
|
||||
export const debugMatch = (title: string, author?: string) =>
|
||||
api.get(`/api/items/match/debug`, { params: { title, author } }).then((r) => r.data)
|
||||
|
||||
export const extractCover = (id: string) =>
|
||||
api.post(`/api/items/${id}/extract-cover`).then((r) => r.data)
|
||||
|
||||
export const coverUrl = (id: string) => `/api/items/${id}/cover`
|
||||
|
||||
@@ -4,7 +4,7 @@ import {
|
||||
Play, ArrowLeft, RefreshCw, Search, Check,
|
||||
Loader2, Trash2, X
|
||||
} from 'lucide-react'
|
||||
import { getItem, updateItem, triggerMatch, searchMatch, applyMatch, coverUrl } from '../api/items'
|
||||
import { getItem, updateItem, triggerMatch, searchMatch, applyMatch, debugMatch, extractCover, coverUrl } from '../api/items'
|
||||
import { getMe, createBookmark, deleteBookmark } from '../api/me'
|
||||
import { usePlayerStore } from '../store/playerStore'
|
||||
import CoverImage from '../components/common/CoverImage'
|
||||
@@ -21,6 +21,8 @@ export default function BookDetail() {
|
||||
const [matchQuery, setMatchQuery] = useState('')
|
||||
const [matchLoading, setMatchLoading] = useState(false)
|
||||
const [showMatchPanel, setShowMatchPanel] = useState(false)
|
||||
const [debugData, setDebugData] = useState<any>(null)
|
||||
const [debugLoading, setDebugLoading] = useState(false)
|
||||
const { play, item: currentItem, currentTime } = usePlayerStore()
|
||||
|
||||
useEffect(() => {
|
||||
@@ -79,6 +81,27 @@ export default function BookDetail() {
|
||||
}, 3000)
|
||||
}
|
||||
|
||||
const handleDebug = async () => {
|
||||
setDebugLoading(true)
|
||||
try {
|
||||
const data = await debugMatch(title, author || undefined)
|
||||
setDebugData(data)
|
||||
} finally {
|
||||
setDebugLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
const handleExtractCover = async () => {
|
||||
if (!id) return
|
||||
const res = await extractCover(id)
|
||||
if (res.success) {
|
||||
const updated = await getItem(id)
|
||||
setItem(updated)
|
||||
} else {
|
||||
alert('Kein lokales Cover gefunden')
|
||||
}
|
||||
}
|
||||
|
||||
const fmtTime = (s: number) => {
|
||||
const h = Math.floor(s / 3600)
|
||||
const m = Math.floor((s % 3600) / 60)
|
||||
@@ -167,10 +190,59 @@ export default function BookDetail() {
|
||||
<RefreshCw size={14} className={matchLoading ? 'animate-spin' : ''} />
|
||||
Auto-Match
|
||||
</button>
|
||||
<button
|
||||
onClick={handleDebug}
|
||||
disabled={debugLoading}
|
||||
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 disabled:opacity-50 transition-colors"
|
||||
title="Zeigt was die APIs tatsächlich zurückgeben"
|
||||
>
|
||||
{debugLoading ? <Loader2 size={14} className="animate-spin" /> : <Search size={14} />}
|
||||
Debug
|
||||
</button>
|
||||
<button
|
||||
onClick={handleExtractCover}
|
||||
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="Cover aus Ordner-Datei oder MP3-Tag extrahieren"
|
||||
>
|
||||
Cover aus Datei
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{debugData && (
|
||||
<div className="mb-6 bg-surface border border-divider rounded-xl p-4">
|
||||
<div className="flex items-center justify-between mb-3">
|
||||
<h3 className="text-ink" style={{ fontSize: '13px', fontWeight: 600 }}>API-Debug</h3>
|
||||
<button onClick={() => setDebugData(null)} className="text-muted hover:text-ink">
|
||||
<X size={14} />
|
||||
</button>
|
||||
</div>
|
||||
<p className="text-muted mb-2" style={{ fontSize: '12px' }}>
|
||||
Such-Titel (bereinigt): <span className="font-mono text-ink">{debugData.normalized?.search_title}</span>
|
||||
{debugData.normalized?.series && <> · Serie: <span className="text-primary">{debugData.normalized.series}</span></>}
|
||||
{debugData.normalized?.episode && <> · Folge: <span className="text-primary">{debugData.normalized.episode}</span></>}
|
||||
</p>
|
||||
{debugData.sources?.map((s: any) => (
|
||||
<div key={s.source} className="mb-3 border-t border-divider pt-3">
|
||||
<p className="text-ink mb-1" style={{ fontSize: '12px', fontWeight: 600 }}>
|
||||
{s.source} — {s.ok ? `${s.count} Treffer` : <span className="text-red-400">Fehler: {s.error}</span>}
|
||||
</p>
|
||||
{s.ok && s.results?.slice(0, 3).map((r: any, i: number) => (
|
||||
<div key={i} className="text-muted ml-2 mb-1" style={{ fontSize: '11px' }}>
|
||||
<p className="text-ink">{r.title} {r.author && `— ${r.author}`} {r.year && `(${r.year})`}</p>
|
||||
{r.narrator && <p>Sprecher: {r.narrator}</p>}
|
||||
{r.publisher && <p>Verlag: {r.publisher}</p>}
|
||||
{r.series && <p>Serie: {r.series} {r.series_sequence && `#${r.series_sequence}`}</p>}
|
||||
{r.cover_url && <p className="break-all">Cover: <span className="font-mono">{r.cover_url}</span></p>}
|
||||
<p className="opacity-50">conf={r.confidence} id={r.source_id}</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Description */}
|
||||
{meta.description && (
|
||||
<div className="mb-6">
|
||||
|
||||
Reference in New Issue
Block a user