feat: auto-discover similar tracks during autoplay via Last.fm/MusicBrainz
Every Nth autoplay pick (configurable via discover_ratio), query Last.fm for similar tracks. When Last.fm has no key or returns nothing, fall back to MusicBrainz tag-based recording search (no API key needed). Discovered tracks are resolved via yt-dlp and deduplicated within the session. If discovery fails, the kept-deck shuffle continues as before. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -2,6 +2,7 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
@@ -91,6 +92,19 @@ def _search_track(api_key: str, query: str,
|
||||
# -- Metadata extraction -----------------------------------------------------
|
||||
|
||||
|
||||
def _parse_title(raw_title: str) -> tuple[str, str]:
|
||||
"""Split a raw track title into (artist, title).
|
||||
|
||||
Tries common separators: `` - ``, `` -- ``, `` | ``, `` ~ ``.
|
||||
Returns ``("", raw_title)`` if no separator is found.
|
||||
"""
|
||||
for sep in (" - ", " -- ", " | ", " ~ "):
|
||||
if sep in raw_title:
|
||||
parts = raw_title.split(sep, 1)
|
||||
return (parts[0].strip(), parts[1].strip())
|
||||
return ("", raw_title)
|
||||
|
||||
|
||||
def _current_meta(bot) -> tuple[str, str]:
|
||||
"""Extract artist and title from the currently playing track.
|
||||
|
||||
@@ -103,15 +117,60 @@ def _current_meta(bot) -> tuple[str, str]:
|
||||
if current is None:
|
||||
return ("", "")
|
||||
raw_title = current.title or ""
|
||||
return _parse_title(raw_title)
|
||||
|
||||
# Try common "Artist - Title" patterns
|
||||
for sep in (" - ", " -- ", " | ", " ~ "):
|
||||
if sep in raw_title:
|
||||
parts = raw_title.split(sep, 1)
|
||||
return (parts[0].strip(), parts[1].strip())
|
||||
|
||||
# No separator -- treat whole thing as a search query
|
||||
return ("", raw_title)
|
||||
# -- Discovery orchestrator --------------------------------------------------
|
||||
|
||||
|
||||
async def discover_similar(bot, last_track_title: str) -> tuple[str, str] | None:
|
||||
"""Find a similar track via Last.fm or MusicBrainz fallback.
|
||||
|
||||
Returns ``(artist, title)`` or ``None`` if nothing found.
|
||||
"""
|
||||
artist, title = _parse_title(last_track_title)
|
||||
loop = asyncio.get_running_loop()
|
||||
|
||||
# -- Last.fm path --
|
||||
api_key = _get_api_key(bot)
|
||||
if api_key and artist:
|
||||
try:
|
||||
similar = await loop.run_in_executor(
|
||||
None, _get_similar_tracks, api_key, artist, title, 20,
|
||||
)
|
||||
if similar:
|
||||
pick = random.choice(similar)
|
||||
pick_artist = pick.get("artist", {}).get("name", "")
|
||||
pick_title = pick.get("name", "")
|
||||
if pick_artist and pick_title:
|
||||
return (pick_artist, pick_title)
|
||||
except Exception:
|
||||
log.warning("lastfm: discover via Last.fm failed", exc_info=True)
|
||||
|
||||
# -- MusicBrainz fallback --
|
||||
if artist:
|
||||
try:
|
||||
from plugins._musicbrainz import (
|
||||
mb_artist_tags,
|
||||
mb_find_similar_recordings,
|
||||
mb_search_artist,
|
||||
)
|
||||
|
||||
mbid = await loop.run_in_executor(None, mb_search_artist, artist)
|
||||
if mbid:
|
||||
tags = await loop.run_in_executor(None, mb_artist_tags, mbid)
|
||||
if tags:
|
||||
picks = await loop.run_in_executor(
|
||||
None, mb_find_similar_recordings, artist, tags, 20,
|
||||
)
|
||||
if picks:
|
||||
pick = random.choice(picks)
|
||||
return (pick["artist"], pick["title"])
|
||||
except Exception:
|
||||
log.warning("lastfm: discover via MusicBrainz failed",
|
||||
exc_info=True)
|
||||
|
||||
return None
|
||||
|
||||
|
||||
# -- Formatting --------------------------------------------------------------
|
||||
|
||||
Reference in New Issue
Block a user