* library: metadata-enrichment plumbing (cache table + worker lifecycle) — P7
The pipeline around a deliberately NO-OP matcher, so the real
MusicBrainz text matcher (next slice) replaces exactly one function and
inherits the queue, throttle, lifecycle, and safety contracts:
- song_enrichment cache table: one row per song holding the match
lifecycle (unscanned -> matched(source,score) | manual | failed) plus
the canonical values a confident match supplies. A DISPLAY cache -
canonical values are never auto-written into pack files. Never purged
on rescan (only by the explicit per-song delete); dead rows filtered
at read time; re-derivable, so a lost row just re-enriches.
- Identity hashing: sha1 of normalized artist|title|album|duration.
Filename-free, so a renamed pack keeps its enrichment; unchanged hash
makes re-enrichment a no-op (idempotent).
- Queue rules (test-pinned): no row / unscanned / identity-changed ->
re-match; matched + current hash = settled; a MANUAL row is the
user's pinned pick and is never auto-reset (state and hash both
survive metadata edits); failed waits for the matcher's backoff
policy (attempts column ready).
- Worker: _kick_enrich/_enrich_runner mirror the scan's single-flight +
coalescing pattern, kicked when a scan pass fully completes (the scan
pool is a no-network process pool by design; the 5-minute periodic
rescan is the natural retry hook). One bounded pass per kick - no
drain-loop, since the no-op matcher legitimately leaves rows
unscanned. _enrich_throttle() is the <=1 req/s seam every matcher
must call before a network request, and the never-hold-meta_db._lock-
across-a-fetch rule is documented at the seam.
- CONFIG_DIR/art_cache dir helper (the cover-art slice adds the LRU
cap) + GET /api/enrichment/status (worker flags + counts by state).
8 new tests; full-suite failure set identical to unmodified main.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Nm7tHs1Yvjjtnnu4nzJgdN
* fix(library): lock enrichment reads on shared conn + skip redundant stub writes (PR #707 review)
Issue 1: wrap the SELECT+fetch in enrichment_pending, get_enrichment and
enrichment_state_counts in self._lock so request-thread reads no longer
interleave with the worker's execute+commit on the shared connection.
Issue 2: guard upsert_enrichment_stub so an already-settled row (manual
pick, or a non-manual row whose content_hash already matches) skips the
UPDATE/commit — stops the no-op matcher re-writing every song each pass.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Co-authored-by: byrongamatos <xasiklas@gmail.com>