Manual match: paste a MusicBrainz ID/URL to match directly (Ashh)

Ashh: the manual-match modal fuzzy-searches a service and shows the top 8.
When the right release isn't in those 8 (common title — their example was
"Idols", which returns 8 unrelated releases and not Yungblud's), there was no
way through. But the user usually already knows the exact MBID.

Now the modal's search box doubles as a direct-ID box. Paste a MusicBrainz
MBID (bare UUID or a musicbrainz.org URL) and SoulSync looks that exact
entity up and shows it as the single result to confirm + Match — no fighting
the search ranking.

- core/library/direct_id.py: pure detector, returns the canonical ID only
  when the text unambiguously IS one (whole-query UUID, or a UUID inside a
  musicbrainz.org URL). "Idols", "Yungblud Idols", a UUID buried in free
  text → None, so normal search is never hijacked.
- _search_service: direct-ID fast path before the fuzzy search —
  get_release (→ get_release_group fallback for albums) / get_artist /
  get_recording. A pasted-but-unresolvable ID falls THROUGH to fuzzy search,
  so a typo can't dead-end the modal.
- UI: MusicBrainz placeholder now says "…or paste a MusicBrainz ID/URL".

Detector is service-keyed so Spotify/iTunes/etc. direct IDs can be added
later; today only MusicBrainz has a confirmable direct lookup, matching the
reporter's ask + screenshot. 9 tests: detector truth table (bare/URL/plain/
buried/other-service) + dispatch (confirmed release, release-group fallback,
unresolvable→fuzzy, plain query skips direct lookup).
This commit is contained in:
BoulderBadgeDad 2026-06-07 13:04:17 -07:00
parent 46f03827a2
commit 8b7609cdb2
4 changed files with 224 additions and 1 deletions

53
core/library/direct_id.py Normal file
View file

@ -0,0 +1,53 @@
"""Direct-ID detection for manual matching (Ashh's request).
The manual-match modal fuzzy-searches a service and shows the top 8 hits.
When the right release isn't in those 8 (common name like "Idols"), the user
is stuck. But they often already KNOW the exact ID so let them paste it
and match directly instead of fighting the search ranking.
This module is the pure detector: given a service + the text the user typed,
return the canonical ID if the text *is* an ID (bare or pasted as a URL/URI),
else None. No network, no I/O the caller decides whether to do a direct
lookup or fall through to the normal fuzzy search.
Conservative by design: only return an ID when the text matches that
service's ID shape unambiguously. Anything else returns None so a normal
text search still runs (pasting "Idols" never looks like an ID).
"""
from __future__ import annotations
import re
from typing import Optional
# MusicBrainz MBIDs are UUIDs (8-4-4-4-12 hex). Accept a bare UUID or one
# embedded in a musicbrainz.org URL (/artist/<id>, /release/<id>, ...).
_UUID_RE = re.compile(
r"([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})",
re.IGNORECASE,
)
def extract_direct_id(service: str, entity_type: str, query: str) -> Optional[str]:
"""Return the canonical service ID if ``query`` is one, else None.
``entity_type`` is accepted for future per-type shapes (e.g. Spotify
track vs album URLs); MusicBrainz UUIDs are type-agnostic so it's unused
there today."""
if not query:
return None
text = query.strip()
if not text:
return None
service = (service or "").strip().lower()
if service == "musicbrainz":
# Bare UUID, or a MB URL — but ONLY a UUID. A search like "Idols"
# can't match, so normal search is never hijacked.
m = _UUID_RE.search(text)
if m and (text == m.group(1) or "musicbrainz.org" in text.lower()):
return m.group(1).lower()
return None
return None

View file

@ -59,10 +59,65 @@ def _detect_provider(items, client):
return 'spotify'
def _mb_direct_lookup(entity_type, mbid):
"""Confirm a pasted MusicBrainz MBID by fetching that exact entity.
Returns a one-item result list (same shape as the search path) so the
modal shows it for confirmation, or [] if the ID doesn't resolve."""
if not mb_worker or not mb_worker.mb_service:
raise ValueError("MusicBrainz worker not initialized")
mb_client = mb_worker.mb_service.mb_client
if entity_type == 'artist':
a = mb_client.get_artist(mbid)
if not a:
return []
extra = a.get('disambiguation') or a.get('country') or a.get('type') or ''
return [{'id': a['id'], 'name': a.get('name', ''), 'image': None,
'extra': f"Direct ID match{' · ' + extra if extra else ''}"}]
if entity_type == 'album':
# A pasted album ID may be a release OR a release-group — try release
# first (what the matcher stores), fall back to release-group.
r = mb_client.get_release(mbid) or mb_client.get_release_group(mbid)
if not r:
return []
artists = ', '.join(ac.get('name', '') for ac in r.get('artist-credit', []) if isinstance(ac, dict))
cover_url = f"https://coverartarchive.org/release/{r['id']}/front-250" if r.get('id') else None
bits = ' · '.join(b for b in (artists, r.get('date', '')) if b)
return [{'id': r['id'], 'name': r.get('title', ''), 'image': cover_url,
'extra': f"Direct ID match{' · ' + bits if bits else ''}"}]
if entity_type == 'track':
rec = mb_client.get_recording(mbid)
if not rec:
return []
artists = ', '.join(ac.get('name', '') for ac in rec.get('artist-credit', []) if isinstance(ac, dict))
return [{'id': rec['id'], 'name': rec.get('title', ''), 'image': None,
'extra': f"Direct ID match{' · ' + artists if artists else ''}"}]
return []
def _search_service(service, entity_type, query):
"""Search a service and return normalized results."""
import requests as req_lib
# Direct-ID fast path (Ashh): if the user pasted an exact service ID
# (e.g. a MusicBrainz MBID for a release the top-8 fuzzy search missed),
# confirm it by direct lookup and return just that entity. A failed
# lookup falls through to the normal search, so a paste that merely
# LOOKS like an ID can't dead-end the modal.
from core.library.direct_id import extract_direct_id
direct_id = extract_direct_id(service, entity_type, query)
if direct_id:
try:
if service == 'musicbrainz':
hit = _mb_direct_lookup(entity_type, direct_id)
if hit:
return hit
except Exception as e:
logger.debug("Direct-ID lookup failed for %s %s: %s", service, direct_id, e)
# fall through to fuzzy search
if service == 'spotify':
if not spotify_enrichment_worker or not spotify_enrichment_worker.client:
raise ValueError("Spotify worker not initialized")

View file

@ -0,0 +1,113 @@
"""Direct-ID manual matching (Ashh: 'just slap MB ID in that search').
When the right release isn't in the top-8 fuzzy results, the user pastes the
exact ID. extract_direct_id detects it (pure); _search_service confirms it
via a direct lookup and returns just that entity, falling back to fuzzy
search if the paste only looks ID-ish.
"""
from __future__ import annotations
from types import SimpleNamespace
from unittest.mock import MagicMock
from core.library.direct_id import extract_direct_id
MBID = "1af02ea7-3f00-40ca-804b-41e2dca7e4a9"
# ── pure detector ────────────────────────────────────────────────────────────
def test_bare_mbid_detected():
assert extract_direct_id("musicbrainz", "album", MBID) == MBID
assert extract_direct_id("musicbrainz", "artist", MBID.upper()) == MBID # normalized
def test_mbid_in_url_detected():
for url in (
f"https://musicbrainz.org/release/{MBID}",
f"https://musicbrainz.org/release/{MBID}/cover-art",
f" https://beta.musicbrainz.org/artist/{MBID} ",
):
assert extract_direct_id("musicbrainz", "album", url) == MBID
def test_plain_text_query_is_not_an_id():
assert extract_direct_id("musicbrainz", "album", "Idols") is None
assert extract_direct_id("musicbrainz", "album", "Yungblud Idols") is None
assert extract_direct_id("musicbrainz", "album", "") is None
assert extract_direct_id("musicbrainz", "album", " ") is None
def test_loose_uuid_without_url_context_is_rejected():
# A UUID embedded in free text (not the whole query, no MB URL) is NOT
# treated as a direct ID — avoids hijacking a genuine search.
assert extract_direct_id("musicbrainz", "album", f"album {MBID} deluxe") is None
def test_non_musicbrainz_services_have_no_direct_id_yet():
assert extract_direct_id("spotify", "album", MBID) is None
assert extract_direct_id("deezer", "track", "12345") is None
# ── _search_service direct dispatch ──────────────────────────────────────────
def _wire_mb(monkeypatch, **methods):
import core.library.service_search as ss
mb_client = MagicMock(**methods)
worker = SimpleNamespace(mb_service=SimpleNamespace(mb_client=mb_client))
monkeypatch.setattr(ss, "mb_worker", worker)
return ss, mb_client
def test_pasted_mbid_returns_single_confirmed_release(monkeypatch):
ss, mb_client = _wire_mb(monkeypatch)
mb_client.get_release.return_value = {
"id": MBID, "title": "Idols", "date": "2025-06-20",
"artist-credit": [{"name": "Yungblud"}],
}
results = ss._search_service("musicbrainz", "album", MBID)
assert len(results) == 1
assert results[0]["id"] == MBID
assert results[0]["name"] == "Idols"
assert "Direct ID match" in results[0]["extra"]
assert "Yungblud" in results[0]["extra"]
mb_client.get_release.assert_called_once_with(MBID)
mb_client.search_release.assert_not_called() # never fuzzy-searched
def test_album_falls_back_to_release_group(monkeypatch):
ss, mb_client = _wire_mb(
monkeypatch,
get_release=lambda mbid: None,
get_release_group=lambda mbid: {"id": MBID, "title": "Idols", "artist-credit": []},
)
results = ss._search_service("musicbrainz", "album", MBID)
assert len(results) == 1 and results[0]["name"] == "Idols"
def test_unresolvable_mbid_falls_through_to_fuzzy(monkeypatch):
# ID-shaped but doesn't resolve → don't dead-end; run the normal search.
ss, mb_client = _wire_mb(
monkeypatch,
get_release=lambda mbid: None,
get_release_group=lambda mbid: None,
search_release=lambda q, limit=8, strict=False: [
{"id": "other", "title": "Idols (fuzzy)", "artist-credit": [], "date": "", "score": 90},
],
)
results = ss._search_service("musicbrainz", "album", MBID)
assert len(results) == 1 and results[0]["id"] == "other" # fuzzy result
def test_plain_query_skips_direct_lookup(monkeypatch):
ss, mb_client = _wire_mb(
monkeypatch,
search_release=lambda q, limit=8, strict=False: [
{"id": "r1", "title": "Idols", "artist-credit": [], "date": "", "score": 100},
],
)
results = ss._search_service("musicbrainz", "album", "Idols")
assert results[0]["id"] == "r1"
mb_client.get_release.assert_not_called() # no wasted direct lookup

View file

@ -6065,7 +6065,9 @@ function openManualMatchModal(entityType, entityId, service, defaultQuery, artis
const searchInput = document.createElement('input');
searchInput.type = 'text';
searchInput.className = 'enhanced-match-search-input';
searchInput.placeholder = `Search ${serviceLabels[service] || service}...`;
searchInput.placeholder = service === 'musicbrainz'
? `Search ${serviceLabels[service]}… or paste a MusicBrainz ID/URL`
: `Search ${serviceLabels[service] || service}...`;
searchInput.value = defaultQuery;
searchRow.appendChild(searchInput);
const searchBtn = document.createElement('button');