feat: add MusicBrainz fallback to !similar and !tags commands
Some checks failed
CI / gitleaks (push) Failing after 2s
CI / lint (push) Failing after 23s
CI / test (3.11) (push) Has been skipped
CI / test (3.12) (push) Has been skipped
CI / test (3.13) (push) Has been skipped
CI / build (push) Has been skipped

Remove early return on missing Last.fm API key. Both commands now
fall back to MusicBrainz (mb_search_artist -> mb_artist_tags ->
mb_find_similar_recordings) when no API key is configured or when
Last.fm returns empty results. Same pattern used by discover_similar.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
user
2026-02-23 21:56:39 +01:00
parent a87f75adf1
commit 135a3791e2
2 changed files with 170 additions and 51 deletions

View File

@@ -339,12 +339,56 @@ class TestFmtMatch:
class TestCmdSimilar:
def test_no_api_key(self):
def test_no_api_key_mb_fallback(self):
"""No API key falls back to MusicBrainz for similar results."""
bot = _FakeBot(api_key="")
msg = _Msg(text="!similar Tool")
with patch.dict("os.environ", {}, clear=True):
mb_picks = [{"artist": "MB Artist", "title": "MB Song"}]
with patch.dict("os.environ", {}, clear=True), \
patch("plugins._musicbrainz.mb_search_artist",
return_value="mbid-123"), \
patch("plugins._musicbrainz.mb_artist_tags",
return_value=["rock", "metal"]), \
patch("plugins._musicbrainz.mb_find_similar_recordings",
return_value=mb_picks):
asyncio.run(_mod.cmd_similar(bot, msg))
assert any("not configured" in r for r in bot.replied)
assert any("Similar to Tool" in r for r in bot.replied)
assert any("MB Artist" in r for r in bot.replied)
def test_no_api_key_mb_no_results(self):
"""No API key + MusicBrainz returns nothing shows 'no similar'."""
bot = _FakeBot(api_key="")
msg = _Msg(text="!similar Tool")
with patch.dict("os.environ", {}, clear=True), \
patch("plugins._musicbrainz.mb_search_artist",
return_value=None):
asyncio.run(_mod.cmd_similar(bot, msg))
assert any("No similar artists" in r for r in bot.replied)
def test_no_api_key_play_mode(self):
"""No API key + play mode delegates to cmd_play via MB results."""
bot = _FakeBot(api_key="")
msg = _Msg(text="!similar play Tool")
mb_picks = [{"artist": "MB Band", "title": "MB Track"}]
play_called = []
async def fake_play(b, m):
play_called.append(m.text)
music_mod = MagicMock()
music_mod.cmd_play = fake_play
bot.registry._modules["music"] = music_mod
with patch.dict("os.environ", {}, clear=True), \
patch("plugins._musicbrainz.mb_search_artist",
return_value="mbid-123"), \
patch("plugins._musicbrainz.mb_artist_tags",
return_value=["rock"]), \
patch("plugins._musicbrainz.mb_find_similar_recordings",
return_value=mb_picks):
asyncio.run(_mod.cmd_similar(bot, msg))
assert len(play_called) == 1
assert "MB Band" in play_called[0]
def test_no_artist_nothing_playing(self):
bot = _FakeBot()
@@ -468,12 +512,29 @@ class TestCmdSimilar:
class TestCmdTags:
def test_no_api_key(self):
def test_no_api_key_mb_fallback(self):
"""No API key falls back to MusicBrainz for tags."""
bot = _FakeBot(api_key="")
msg = _Msg(text="!tags Tool")
with patch.dict("os.environ", {}, clear=True):
with patch.dict("os.environ", {}, clear=True), \
patch("plugins._musicbrainz.mb_search_artist",
return_value="mbid-123"), \
patch("plugins._musicbrainz.mb_artist_tags",
return_value=["rock", "progressive metal", "art rock"]):
asyncio.run(_mod.cmd_tags(bot, msg))
assert any("not configured" in r for r in bot.replied)
assert any("Tool:" in r for r in bot.replied)
assert any("rock" in r for r in bot.replied)
assert any("progressive metal" in r for r in bot.replied)
def test_no_api_key_mb_no_results(self):
"""No API key + MusicBrainz returns nothing shows 'no tags'."""
bot = _FakeBot(api_key="")
msg = _Msg(text="!tags Obscure")
with patch.dict("os.environ", {}, clear=True), \
patch("plugins._musicbrainz.mb_search_artist",
return_value=None):
asyncio.run(_mod.cmd_tags(bot, msg))
assert any("No tags found" in r for r in bot.replied)
def test_no_artist_nothing_playing(self):
bot = _FakeBot()