feat(worker): Last.fm artist.getSimilar as a second discovery SimilaritySource
Adds LastfmSource, resolving Last.fm's often-empty artist mbid via the injected MB browser (name->mbid cached) so every emitted SimilarArtist.mbid is a real MusicBrainz artist MBID. Registered in build_similarity_sources, gated on lastfm.api_key so the ListenBrainz-only path is unchanged when unset.
This commit is contained in:
@@ -0,0 +1,108 @@
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
import requests
|
||||
|
||||
from lyra_worker.browser import ArtistHit
|
||||
from lyra_worker.similarity._lastfm import LastfmSource
|
||||
from lyra_worker.similarity.base import SimilarArtist
|
||||
|
||||
|
||||
def _resp(payload, status=200):
|
||||
r = MagicMock()
|
||||
r.status_code = status
|
||||
r.json.return_value = payload
|
||||
return r
|
||||
|
||||
|
||||
class _FakeBrowser:
|
||||
"""Records search_artist calls; resolves via a name -> [ArtistHit] map."""
|
||||
|
||||
def __init__(self, hits=None):
|
||||
self._hits = dict(hits or {})
|
||||
self.calls = []
|
||||
|
||||
def search_artist(self, name):
|
||||
self.calls.append(name)
|
||||
return list(self._hits.get(name, []))
|
||||
|
||||
|
||||
def test_similar_artists_parses_and_uses_supplied_mbid():
|
||||
payload = {"similarartists": {"artist": [
|
||||
{"name": "Cand One", "mbid": "c1", "match": "0.9"},
|
||||
]}}
|
||||
browser = _FakeBrowser()
|
||||
with patch("lyra_worker.similarity._lastfm.requests.get", return_value=_resp(payload)):
|
||||
out = LastfmSource(api_key="k", browser=browser).similar_artists("seed")
|
||||
assert out == [SimilarArtist("c1", "Cand One", 0.9)]
|
||||
assert browser.calls == [] # mbid supplied, no resolution needed
|
||||
|
||||
|
||||
def test_similar_artists_resolves_missing_mbid_and_caches():
|
||||
payload = {"similarartists": {"artist": [
|
||||
{"name": "Cand Two", "mbid": "", "match": "0.5"},
|
||||
{"name": "Cand Two", "mbid": "", "match": "0.4"},
|
||||
]}}
|
||||
browser = _FakeBrowser(hits={"Cand Two": [ArtistHit(mbid="c2", name="Cand Two")]})
|
||||
with patch("lyra_worker.similarity._lastfm.requests.get", return_value=_resp(payload)):
|
||||
out = LastfmSource(api_key="k", browser=browser).similar_artists("seed")
|
||||
assert out == [
|
||||
SimilarArtist("c2", "Cand Two", 0.5),
|
||||
SimilarArtist("c2", "Cand Two", 0.4),
|
||||
]
|
||||
assert browser.calls == ["Cand Two"] # cached after first lookup
|
||||
|
||||
|
||||
def test_similar_artists_skips_unresolvable_row():
|
||||
payload = {"similarartists": {"artist": [
|
||||
{"name": "Unknown Artist", "mbid": "", "match": "0.5"},
|
||||
{"name": "Cand One", "mbid": "c1", "match": "0.9"},
|
||||
]}}
|
||||
browser = _FakeBrowser(hits={}) # no hits for "Unknown Artist"
|
||||
with patch("lyra_worker.similarity._lastfm.requests.get", return_value=_resp(payload)):
|
||||
out = LastfmSource(api_key="k", browser=browser).similar_artists("seed")
|
||||
assert out == [SimilarArtist("c1", "Cand One", 0.9)]
|
||||
|
||||
|
||||
def test_similar_artists_skips_self():
|
||||
payload = {"similarartists": {"artist": [
|
||||
{"name": "Seed", "mbid": "seed", "match": "1.0"},
|
||||
{"name": "Cand One", "mbid": "c1", "match": "0.9"},
|
||||
]}}
|
||||
browser = _FakeBrowser()
|
||||
with patch("lyra_worker.similarity._lastfm.requests.get", return_value=_resp(payload)):
|
||||
out = LastfmSource(api_key="k", browser=browser).similar_artists("seed")
|
||||
assert out == [SimilarArtist("c1", "Cand One", 0.9)]
|
||||
|
||||
|
||||
def test_similar_artists_coerces_single_object():
|
||||
payload = {"similarartists": {"artist": {"name": "Cand One", "mbid": "c1", "match": "0.9"}}}
|
||||
browser = _FakeBrowser()
|
||||
with patch("lyra_worker.similarity._lastfm.requests.get", return_value=_resp(payload)):
|
||||
out = LastfmSource(api_key="k", browser=browser).similar_artists("seed")
|
||||
assert out == [SimilarArtist("c1", "Cand One", 0.9)]
|
||||
|
||||
|
||||
def test_similar_artists_error_envelope_returns_empty_list():
|
||||
payload = {"error": 6, "message": "no artist"}
|
||||
browser = _FakeBrowser()
|
||||
with patch("lyra_worker.similarity._lastfm.requests.get", return_value=_resp(payload)):
|
||||
out = LastfmSource(api_key="k", browser=browser).similar_artists("seed")
|
||||
assert out == []
|
||||
|
||||
|
||||
def test_health_true_on_normal_response():
|
||||
payload = {"similarartists": {"artist": [{"name": "Radiohead", "mbid": "x", "match": "1.0"}]}}
|
||||
with patch("lyra_worker.similarity._lastfm.requests.get", return_value=_resp(payload)):
|
||||
assert LastfmSource(api_key="k", browser=_FakeBrowser()).health() is True
|
||||
|
||||
|
||||
def test_health_false_on_error_envelope():
|
||||
payload = {"error": 10, "message": "invalid api key"}
|
||||
with patch("lyra_worker.similarity._lastfm.requests.get", return_value=_resp(payload)):
|
||||
assert LastfmSource(api_key="k", browser=_FakeBrowser()).health() is False
|
||||
|
||||
|
||||
def test_health_false_on_request_exception():
|
||||
with patch("lyra_worker.similarity._lastfm.requests.get",
|
||||
side_effect=requests.ConnectionError("down")):
|
||||
assert LastfmSource(api_key="k", browser=_FakeBrowser()).health() is False
|
||||
@@ -1,4 +1,5 @@
|
||||
from lyra_worker.registry import build_similarity_sources
|
||||
from lyra_worker.similarity._lastfm import LastfmSource
|
||||
from lyra_worker.similarity._listenbrainz import ListenBrainzSource
|
||||
|
||||
|
||||
@@ -12,3 +13,17 @@ def test_build_returns_listenbrainz_by_default():
|
||||
def test_base_url_override_from_config():
|
||||
src = build_similarity_sources({"discover.listenBrainzUrl": "http://lb.local"})[0]
|
||||
assert src._base == "http://lb.local"
|
||||
|
||||
|
||||
def test_lastfm_added_when_api_key_configured():
|
||||
sources = build_similarity_sources({"lastfm.api_key": "secret-key"})
|
||||
names = [s.name for s in sources]
|
||||
assert names == ["listenbrainz", "lastfm"]
|
||||
lastfm = next(s for s in sources if s.name == "lastfm")
|
||||
assert isinstance(lastfm, LastfmSource)
|
||||
assert lastfm._key == "secret-key"
|
||||
|
||||
|
||||
def test_lastfm_omitted_without_api_key():
|
||||
sources = build_similarity_sources({})
|
||||
assert all(s.name != "lastfm" for s in sources)
|
||||
|
||||
Reference in New Issue
Block a user