Commit graph

5 commits

Author SHA1 Message Date
dev
967ad2a026 refactor(verification): import path delegates to shared core
verify_audio_file now calls audio_verification.evaluate() and re-exports
normalize/similarity/_alias_aware_artist_sim from the core, so import and the
library scan can no longer drift apart. Alias-rescue diagnostic moved to the core.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-11 01:27:28 +02:00
BoulderBadgeDad
ba6c39bae3 AcoustID: report errors honestly instead of masking them as 'Skipped'
An invalid API key (and rate limits / missing chromaprint / fingerprint
failures) all collapsed into the same None as a genuine no-match, so:
  - every download showed a benign 'AcoustID: Skipped', and
  - the 'Test API key' button reported a dead key as VALID
because test_api_key trusted 'no exception = valid' but fingerprint_and_lookup
swallows the error and returns None. A broken AcoustID setup looked completely
normal — which cost a real debugging session to untangle.

- New AcoustIDClient.lookup_with_status() returns a structured result that
  distinguishes ok / no_match / error / no_backend / fingerprint_error /
  unavailable. fingerprint_and_lookup() stays dict-or-None (library scanner /
  auto-import / their tests unchanged) as a thin wrapper over it.
- verify_audio_file() uses it: a real error -> new VerificationResult.ERROR
  (-> _acoustid_result='error' -> the existing red 'Error' history badge),
  a genuine no-match -> SKIP 'No match in AcoustID database'. ERROR never
  quarantines (an outage/bad key must not punish good files).
- test_api_key() now validates via the authoritative direct API call (error
  code 4 = invalid key) instead of the swallowed-exception path.

Tests: structured-status distinction, legacy-wrapper contract, verify ERROR vs
SKIP, and test_api_key invalid-vs-accepted. Existing verify tests updated to
stub lookup_with_status (a stub returning just recordings is inferred as ok).
2026-05-31 20:10:31 -07:00
Broque Thomas
bc34d39ce9 Tighten alias-lookup trust + add ambiguity gate + diagnostic log
Cin pre-review pass on the false-positive risk. Three tightenings:

# 1. Bumped MB-search trust threshold from 0.6 → 0.85

`MusicBrainzService.lookup_artist_aliases` previously trusted any
MB search match scoring ≥ 0.6 combined (name-similarity + MB
relevance). For distinctive cross-script artists the user-reported
case targets (Hiroyuki Sawano, Сергей Лазарев, etc.) real matches
score ~1.0 — well above 0.85. The 0.6 floor was loose enough to
let in moderate matches for ambiguous names, risking aliases for
the wrong artist getting cached + applied.

Bumped to 0.85. Tighter without rejecting any of the legit
cross-script cases the PR is for.

# 2. Ambiguity gate — skip when results within 0.1 of best

When MB search returns multiple results all scoring high (within
0.1 of the best), the artist name is ambiguous — common name with
multiple distinct artists ("John Smith" returning 10 different
John Smiths). Pulling aliases for any one of them risks the wrong
artist's data bridging incorrectly to a file's tag.

Added explicit ambiguity detection: when 2+ results within 0.1,
skip alias lookup entirely + cache empty. Matches Cin's
"explicit > implicit" — the prior code just picked the highest
score blindly.

# 3. Diagnostic log when alias rescues a comparison

When the alias path triggers a PASS that direct similarity would
have FAILed, emit an INFO log: `Artist alias rescued comparison:
expected='X' vs actual='Y' (direct sim=0.00, alias 'Z' →
score=1.00)`.

Lets future bug reports trace which alias triggered which decision.
Doesn't change behavior — visibility only. Logs ONLY the rescue
case, not happy-path direct matches (no log spam).

# Tests added (5)

`test_artist_alias_service.py` (+3):
- `test_moderate_confidence_match_now_skipped_strict_threshold`
- `test_ambiguous_results_skipped`
- `test_unambiguous_high_confidence_match_succeeds`

`test_acoustid_verification_aliases.py` (+3):
- `test_alias_rescue_emits_info_log` — direct-fail + alias-pass
  emits INFO log
- `test_no_log_when_direct_match_succeeds` — happy path quiet
- `test_no_log_when_alias_doesnt_help` — failed path also quiet

# Test infrastructure note

Logging tests use a directly-attached `ListHandler` on
`soulsync.acoustid.verification` (the actual logger name —
dot-separated by `get_logger`), NOT pytest's caplog. Same pattern
as the prior watchdog-test fix — caplog is intermittently flaky
in full-suite runs for soulsync namespace loggers. An owned
handler sidesteps both issues.

# Verification

- 85/85 matching tests pass (+5 from prior commit)
- 2543 full suite passes (+6 from prior, +85 PR-total)
- Ruff clean
- Reporter's Japanese + Russian regression tests still pass —
  legit cross-script case (sim ≈ 1.0) clears the new 0.85
  threshold easily
2026-05-10 17:38:03 -07:00
Broque Thomas
11397307b2 Alias resolution polish: lazy-fire on direct-match failure + worker backfill
Two perf gaps that would have failed Cin's review:

# Gap #1: alias lookup fired unconditionally

Pre-fix in this commit, `_resolve_expected_artist_aliases` ran at
the top of every `verify_audio_file` call regardless of whether
the direct artist match would have passed. For users whose library
is mostly same-script (95% of cases), every successful verification
was paying for a wasted DB query (and possibly a wasted MB API
call for un-enriched artists).

Restructured the helper to accept a callable provider instead of a
pre-resolved list. Provider invoked LAZILY only when direct
similarity falls below `ARTIST_MATCH_THRESHOLD`. Verifier passes a
memoising thunk that resolves once across the 3 comparison sites
within one verification.

`_alias_aware_artist_sim` now accepts `aliases` as either:

- iterable of strings (used eagerly — backward compat with tests
  that already know the aliases)
- callable returning the iterable (resolved on first need within
  a verification)

Happy path (direct match passes): zero DB queries, zero MB calls.
Cross-script case: one resolution shared across 3 sites — same as
the prior contract.

# Gap #2: existing-MBID artists never got alias backfill

Worker's `_process_item` artist branch had an `existing_id` short-
circuit (line 296) that updated MBID status but skipped alias
fetch. Result: every user with an already-enriched library had
MBIDs but NULL aliases on day-one of this PR. Live MB lookup at
verify-time covered them, but at the cost of N live calls for N
artists across the library.

Added one-time backfill: when existing-MBID is found AND
`artists.aliases` for that row is empty, fetch + persist aliases.
Subsequent re-scan cycles short-circuit on the populated column —
no repeated MB calls.

New helper `_artist_aliases_empty(artist_id)` does the cheap NULL
check via direct SQL. Best-effort: defensively returns True on
errors so backfill happens (a redundant MB call is cheaper than
missing the backfill entirely).

# Tests added (9)

`test_acoustid_verification_aliases.py` (+6):
- `TestLazyAliasResolution` (3): no lookup when direct match passes,
  lookup fires only when direct fails, lookup memoised across the
  3 sites within one verification.
- `TestAliasProviderCallable` (3): iterable passed directly,
  callable resolves lazily, callable returning empty falls back to
  direct sim.

`test_artist_alias_service.py` (+3):
- `test_existing_mbid_path_backfills_aliases_when_column_empty`
- `test_existing_mbid_path_skips_backfill_when_aliases_already_set`
- `test_existing_mbid_backfill_failure_does_not_break_match`

# Verification

- 79/79 matching tests pass (+9 from prior commit)
- 2537 full suite passes (+9, +79 PR-total)
- Ruff clean
- Backward compat: every prior-commit test still passes (the
  iterable-shape API still works alongside the new callable shape)
2026-05-10 17:02:02 -07:00
Broque Thomas
7066233c37 Wire alias-aware artist match into AcoustID verifier — fixes #442
This is the user-visible commit. The reporter's exact two cases
(Japanese kanji, Russian Cyrillic) now pass verification instead of
being quarantined.

# What changed

Verifier's three artist-similarity sites now route through the
shared `core.matching.artist_aliases.artist_names_match` helper
instead of raw `_similarity`:

- `_find_best_title_artist_match` (per-recording scoring at the
  best-match stage)
- Secondary scan when title matches but best-match's artist doesn't
  (line ~355 pre-fix)
- Final fallback scan over all recordings (line ~403 pre-fix)

Aliases for the expected artist are resolved ONCE at the top of
`verify_audio_file` via `_resolve_expected_artist_aliases`, which
calls the new `MusicBrainzService.lookup_artist_aliases` chain
(library DB → cache → live MB). Single resolution per verification
regardless of how many AcoustID recordings come back — pinned by
test.

New helper `_alias_aware_artist_sim(expected, actual, aliases)`
wraps the pure helper with the verifier's normaliser
(`_similarity`) and threshold (`ARTIST_MATCH_THRESHOLD`). Returns a
single float so existing threshold-comparison code paths keep their
shape — minimal diff.

# Reporter's cases — verified

Case 1 (issue #442 verbatim):
    File:     YAMANAIAME by 澤野弘之
    Expected: YAMANAIAME by Hiroyuki Sawano
    Pre-fix:  Quarantined (artist=0%)
    Post-fix: PASS (alias '澤野弘之' resolved from MB)

Case 2 (issue #442 verbatim):
    File:     On the Other Side by Sergey Lazarev
    Expected: On the other side by Сергей Лазарев
    Pre-fix:  Quarantined (artist=7%)
    Post-fix: PASS (alias 'Sergey Lazarev' resolved from MB)

Both reproduced as regression tests with stubbed MB service.

# Backward compat

Three test cases pin that no-aliases / failure paths preserve
pre-fix behaviour exactly:

- Clear artist mismatch (different artist, same script) still
  FAILs — aliases bridge synonyms, not unrelated artists.
- Exact title + artist match still PASSes regardless of aliases.
- MB service raise → verifier completes with direct similarity
  (treats failure as "no aliases available" — same as pre-fix).

Also covers manual import: the import-modal "Search for Match"
flow goes through the same verifier, so the reporter's complaint
that "manual import simply throws them back in quarantine again"
is fixed by the same change.

# Tests added (11)

`tests/matching/test_acoustid_verification_aliases.py`:
- `_alias_aware_artist_sim`: alias bridges score ↑, no-aliases
  falls back, aliases don't mask genuine mismatches
- `_find_best_title_artist_match` accepts + uses aliases
- Reporter's case 1 (Japanese) end-to-end
- Reporter's case 2 (Russian) end-to-end
- Backward compat: no-aliases mismatch still fails, exact match
  still passes, MB-service-raise doesn't break verification
- Performance: alias lookup fires ONCE per verification regardless
  of recording count

# Verification

- 11 new verifier tests pass
- 31 prior service tests pass
- 28 prior helper tests pass
- 294 matching + imports tests pass total (no regression)
- Ruff clean
2026-05-10 16:33:54 -07:00