soulsync/webui
Broque Thomas 3af2d34cee Auto-import: fall through to other metadata sources when primary returns no match
Discord report: 16 Bandcamp indie albums sat in staging because
auto-import couldn't identify them, but the manual search bar at the
bottom of the Import Music tab found the same albums fine. Trace:
`_search_metadata_source` only queried `get_primary_source()` — single
source, no fallback. Meanwhile `search_import_albums` (manual search bar)
already iterated `get_source_priority(get_primary_source())` and broke
on the first source with results. Asymmetric behavior, same album: manual
worked, auto-import didn't.

Fix: lift `_search_metadata_source` to use the same source-chain pattern.
Try primary first; if it returns nothing OR scores below the 0.4
threshold, fall through to the next source in priority order. First
source producing a strong-enough match wins. Result dict carries the
`source` that actually matched (not the primary name) so downstream
`_match_tracks` calls the right client. Defensive per-source try/except
so a rate-limited or auth-failed source doesn't abort the chain.
Unconfigured sources (client=None) silently skipped.

Cin-shape lift: scoring math extracted to pure `_score_album_search_result`
helper so the weight tweaks (album 50% / artist 20% / track-count 30%)
are pinned at the function boundary, independent of the orchestrator
(per-source iteration, exception containment, threshold check). Weight
constants exposed at module level (`_ALBUM_NAME_WEIGHT`,
`_ARTIST_NAME_WEIGHT`, `_TRACK_COUNT_WEIGHT`) — greppable, bumpable in
one place. Pre-extraction these were magic numbers inline.

27 new tests:
- 9 integration tests in `test_auto_import_multi_source_fallback.py`:
  primary-success path unchanged (no fallback fires, only primary client
  called), primary-empty falls through, primary-weak-score falls through,
  first fallback success stops the chain (no wasted API calls on
  remaining sources), all-sources-fail returns None, per-source
  exception contained, unconfigured-source skipped, result `source`
  field reflects winning source, `identification_confidence` from
  winning source.
- 18 helper tests in `test_album_search_scoring.py`: weights sum to
  1.0, album weight dominant (invariant pin), perfect-match returns 1.0,
  per-component contribution (album / artist / track-count), Bandcamp
  vs streaming track-count mismatch (7-files vs 4-tracks case still
  scores ~0.87 above threshold), zero-track-count and zero-file
  guards, huge-mismatch non-negative guard, list-of-strings artist
  shape, missing `.name` / `.artists` / `None` total_tracks edge cases.

Backwards compatible: single-source users see no change — chain just
has one entry. Existing test `test_search_metadata_source_extracts_artist_id_from_dict_artist`
needed one extra patch line for `get_source_priority`.

Full pytest sweep: 2754 passed.
2026-05-12 12:32:18 -07:00
..
static Auto-import: fall through to other metadata sources when primary returns no match 2026-05-12 12:32:18 -07:00
index.html Tidal: surface Favorite Tracks as virtual playlist (issue #502) 2026-05-10 21:36:22 -07:00