Commit graph

2 commits

Author SHA1 Message Date
Broque Thomas
15244f24cf Live MB lookup for un-enriched artists with cache
Previous commit only populated `artists.aliases` for artists the MB
worker had enriched. But the AcoustID verifier (next commit) needs
aliases for ANY expected artist — including:

- Artists not yet in the user's library (first download)
- Artists in the library where MB enrichment hasn't run yet
- Artists where MB enrichment ran but found no MBID (NULL aliases)

This commit adds a multi-tier resolution helper that fills those
gaps without thrashing the MB API.

# Multi-tier resolution

`lookup_artist_aliases(artist_name) -> list[str]`:

1. **Library DB** (fast path): existing `get_artist_aliases` lookup
   by name. No network. Most common path once the worker has
   enriched everything.
2. **Cache** (existing `musicbrainz_cache` table, entity_type=
   `artist_aliases`): a prior live lookup for this name. Empty
   cache hit is respected (don't re-query when MB previously had
   nothing).
3. **Live MB**: search artist by name → pick highest-confidence
   match (combined name-similarity + MB relevance) → fetch aliases
   for that MBID → cache the result.

Always returns a list (possibly empty), never raises. Empty result
on any tier means "no alternate spellings found, fall back to
direct match" — identical to the pre-fix behaviour.

# Threshold gate

Live lookup only trusts the MB search result when combined
similarity score >= 0.6. Below that, we'd be guessing at the wrong
artist — searching `John Smith` returns multiple John Smiths and
pulling aliases for one of them could mismatch. Cache the empty
result so we don't keep re-searching the same low-confidence name.

# Performance contract

Critical for the verifier path: 100 quarantine candidates with the
same expected artist must NOT trigger 100 MB API calls. Cache hit
on second + subsequent calls per unique artist name. Verified by
test pinning the call counts.

# Tests added (8)

- Tier 1 library DB hit — no MB API call fired
- Tier 3 live MB lookup → search → fetch → returns aliases
- Tier 2 cache hit on second call — no re-query
- Empty input → empty return + no API call
- Network failure on search → empty + cached so we don't retry
- No search results → empty + cached
- Low-confidence match (sim < 0.6) skipped — defends against
  picking the wrong artist
- Library row exists but aliases NULL → falls through to live
  lookup (defends against the half-enriched state)

# Verification

- 31/31 service tests pass (8 new + 23 prior)
- Ruff clean
2026-05-10 16:25:30 -07:00
Broque Thomas
48d848bb74 MB worker populates artists.aliases on enrichment
Issue #442 — MusicBrainz exposes alternate-spelling aliases (Japanese
kanji `澤野弘之` for `Hiroyuki Sawano`, Cyrillic `Сергей Лазарев` for
`Sergey Lazarev`, etc.) on every artist record. SoulSync's MB
enrichment worker had access to this data via `get_artist(mbid,
includes=['aliases'])` but wasn't reading or persisting it.

This commit wires the alias fetch into the worker's existing
artist-match path, persists to the new `artists.aliases` column
added in the prior commit, and adds a verifier-friendly read-by-
name lookup so the AcoustID verifier (next commit) can resolve
aliases without an MB round-trip when the artist is in the library.

# New service methods

- `fetch_artist_aliases(mbid) -> list[str]` — calls
  `mb_client.get_artist(mbid, includes=['aliases'])`, parses the
  alias array, dedupes case-insensitively. Returns empty list on
  any failure (missing key, network error, malformed response) so
  transient MB outages never trigger stricter quarantine decisions
  than the pre-fix behaviour. Empty mbid → no API call.

- `update_artist_aliases(artist_id, aliases)` — persists as JSON
  array to `artists.aliases`. Idempotent — overwrites prior value.
  Empty list clears the column. None artist_id is a no-op.

- `get_artist_aliases(artist_name) -> list[str]` — reads back by
  artist NAME (not id), case-insensitive. Used by the verifier
  where the expected artist comes from track metadata — there's no
  library row id at quarantine time. Returns empty list for unknown
  artists, missing data, or corrupt JSON (defensive against legacy
  rows).

# Worker integration

`MusicBrainzWorker._process_item` artist branch:
- After `update_artist_mbid` succeeds, fetch aliases for the matched
  MBID and persist via `update_artist_aliases`.
- Best-effort: alias fetch wrapped in try/except, failure logs at
  debug level, doesn't regress the match outcome.
- No alias call when the artist didn't match an MBID (nothing to
  enrich).

# Tests (23)

- `fetch_artist_aliases`: extracts names from MB response,
  case-insensitive dedup, skips empty/null entries, missing-key
  fallback, network failure → empty, empty mbid no API call,
  verifies `inc=aliases` request param.
- `update_artist_aliases`: persists as JSON, idempotent overwrite,
  empty list clears column, None id is no-op.
- `get_artist_aliases`: returns aliases for known artist,
  case-insensitive lookup, empty for unknown artist / no-aliases
  row, handles corrupt JSON + non-list shape gracefully.
- Worker integration: matched artist triggers fetch + persist,
  no alias call when not matched, alias-fetch failure doesn't
  break the match outcome.

# Verification

- 23/23 new tests pass
- Ruff clean
2026-05-10 16:22:23 -07:00