fix: replace deprecated recommendations/related-artists with search-based discovery

Spotify deprecated /recommendations and /artists/{id}/related-artists.
All three discovery tools now use genre-based search:
- suggest: extracts genres from top artists, searches with mood keywords
- discover_artist: finds artists sharing same genres
- fresh_finds: genre search with known-artist filtering for freshness
This commit is contained in:
Alex Kazaiev
2026-04-06 20:13:59 -05:00
parent 4d3d2b4ddb
commit 065dd35ceb
2 changed files with 110 additions and 45 deletions

View File

@@ -114,30 +114,62 @@ class FreshFindsInput(BaseModel):
async def suggest_music(params: SuggestInput) -> str: async def suggest_music(params: SuggestInput) -> str:
""" """
Suggest music based on your recent listening history and optional mood filter. Suggest music based on your recent listening history and optional mood filter.
Automatically seeds from your top tracks — no manual input needed. Uses genre-based search seeded from your top artists' genres.
""" """
sp = get_spotify_client() sp = get_spotify_client()
# Get top tracks as seeds (mix of recent and medium-term)
top_short = sp.current_user_top_tracks(limit=5, time_range="short_term")
top_medium = sp.current_user_top_tracks(limit=5, time_range="medium_term")
seed_ids = [] # Get top artists to extract genres
for t in (top_short["items"][:3] + top_medium["items"][:2]): top_artists = sp.current_user_top_artists(limit=10, time_range="short_term")
if t["id"] not in seed_ids:
seed_ids.append(t["id"])
seed_ids = seed_ids[:5] # Spotify max 5 seeds
# Build recommendation kwargs # Collect genres from top artists
kwargs = {"seed_tracks": seed_ids, "limit": params.count} genres = []
if params.mood and params.mood.value in MOOD_FEATURES: for a in top_artists["items"]:
kwargs.update(MOOD_FEATURES[params.mood.value]) genres.extend(a.get("genres", []))
results = sp.recommendations(**kwargs) # Deduplicate and pick top genres
genre_count = {}
for g in genres:
genre_count[g] = genre_count.get(g, 0) + 1
sorted_genres = sorted(genre_count.items(), key=lambda x: -x[1])
top_genres = [g for g, _ in sorted_genres[:3]]
if not top_genres:
return "Could not determine your genres. Listen to more music first!"
# Add mood keywords to search
mood_keywords = {
"chill": "chill ambient",
"energetic": "energetic upbeat",
"melancholy": "melancholy sad",
"focused": "instrumental focus",
"dreamy": "dreamy ethereal",
"dark": "dark atmospheric",
"uplifting": "uplifting bright",
"intense": "intense powerful",
}
# Build search query from genres + mood
query_parts = top_genres[:2]
if params.mood and params.mood.value in mood_keywords:
query_parts.append(mood_keywords[params.mood.value])
query = " ".join(query_parts)
# Search for tracks
results = sp.search(q=query, type="track", limit=params.count)
# Filter out tracks by artists already in top list
top_artist_ids = {a["id"] for a in top_artists["items"]}
tracks = [t for t in results["tracks"]["items"]
if not any(a["id"] in top_artist_ids for a in t["artists"])]
# If too few new tracks, include some known artists
if len(tracks) < params.count // 2:
tracks = results["tracks"]["items"][:params.count]
lines = [f"🎵 **MusicTail Suggestions**" + (f" — mood: {params.mood.value}" if params.mood else "")] lines = [f"🎵 **MusicTail Suggestions**" + (f" — mood: {params.mood.value}" if params.mood else "")]
lines.append(f"Seeded from your top tracks\n") lines.append(f"Based on your genres: {', '.join(top_genres)}\n")
for i, track in enumerate(results["tracks"], 1): for i, track in enumerate(tracks[:params.count], 1):
artists = ", ".join(a["name"] for a in track["artists"]) artists = ", ".join(a["name"] for a in track["artists"])
album = track["album"]["name"] album = track["album"]["name"]
tid = track["id"] tid = track["id"]
@@ -157,7 +189,7 @@ async def suggest_music(params: SuggestInput) -> str:
async def discover_artist(params: DiscoverInput) -> str: async def discover_artist(params: DiscoverInput) -> str:
""" """
Find artists similar to one you like, with their top tracks. Find artists similar to one you like, with their top tracks.
Great for expanding from known favorites into new territory. Uses genre-based search to find artists in the same musical space.
""" """
sp = get_spotify_client() sp = get_spotify_client()
@@ -167,15 +199,28 @@ async def discover_artist(params: DiscoverInput) -> str:
return f"Could not find artist: {params.artist_name}" return f"Could not find artist: {params.artist_name}"
artist = search["artists"]["items"][0] artist = search["artists"]["items"][0]
artist_id = artist["id"] artist_genres = artist.get("genres", [])
genres = ", ".join(artist.get("genres", [])[:5]) or "no genres listed" genres_str = ", ".join(artist_genres[:5]) or "no genres listed"
# Get related artists if not artist_genres:
related = sp.artist_related_artists(artist_id) return f"**{artist['name']}** has no genres listed on Spotify — can't find similar artists without genre data."
similar = related["artists"][:params.count]
# Search for artists in the same genres
seen_ids = {artist["id"]}
similar = []
for genre in artist_genres[:3]:
if len(similar) >= params.count:
break
genre_search = sp.search(q=f"genre:\"{genre}\"", type="artist", limit=20)
for a in genre_search["artists"]["items"]:
if a["id"] not in seen_ids and len(similar) < params.count:
seen_ids.add(a["id"])
similar.append(a)
lines = [f"🔍 **Artists similar to {artist['name']}**"] lines = [f"🔍 **Artists similar to {artist['name']}**"]
lines.append(f"Genres: {genres}\n") lines.append(f"Genres: {genres_str}\n")
for i, sim in enumerate(similar, 1): for i, sim in enumerate(similar, 1):
sim_genres = ", ".join(sim.get("genres", [])[:3]) or "" sim_genres = ", ".join(sim.get("genres", [])[:3]) or ""
popularity = sim.get("popularity", 0) popularity = sim.get("popularity", 0)
@@ -192,6 +237,9 @@ async def discover_artist(params: DiscoverInput) -> str:
lines.append(f" Play ID: `{top_tracks[0]['id']}`") lines.append(f" Play ID: `{top_tracks[0]['id']}`")
lines.append("") lines.append("")
if not similar:
lines.append("No similar artists found in these genres.")
return "\n".join(lines) return "\n".join(lines)
@@ -259,31 +307,48 @@ async def fresh_finds(params: FreshFindsInput) -> str:
""" """
sp = get_spotify_client() sp = get_spotify_client()
if params.adventurous: # Get genres from top artists
# Use long-term + different seed strategy time_range = "long_term" if params.adventurous else "short_term"
top = sp.current_user_top_artists(limit=10, time_range="long_term") top_artists = sp.current_user_top_artists(limit=15, time_range=time_range)
# Pick less-obvious artists (positions 5-10 instead of 1-5)
seed_artists = [a["id"] for a in top["items"][4:9]][:5] # Collect and rank genres
kwargs = { genre_count = {}
"seed_artists": seed_artists, for a in top_artists["items"]:
"limit": params.count, for g in a.get("genres", []):
"min_popularity": 5, genre_count[g] = genre_count.get(g, 0) + 1
"max_popularity": 50, # Push toward obscure sorted_genres = sorted(genre_count.items(), key=lambda x: -x[1])
}
# Adventurous = less common genres; Normal = top genres
if params.adventurous and len(sorted_genres) > 3:
pick_genres = [g for g, _ in sorted_genres[3:6]] # Less dominant genres
query_extra = "new"
else: else:
# Use recent tracks as seeds pick_genres = [g for g, _ in sorted_genres[:3]]
top = sp.current_user_top_tracks(limit=10, time_range="short_term") query_extra = ""
seed_tracks = [t["id"] for t in top["items"][:5]]
kwargs = { if not pick_genres:
"seed_tracks": seed_tracks, return "Could not determine your genres. Listen to more music first!"
"limit": params.count,
} # Search for tracks in those genres
results = sp.recommendations(**kwargs) query = " ".join(pick_genres[:2])
if query_extra:
query += f" {query_extra}"
results = sp.search(q=query, type="track", limit=min(params.count * 2, 50))
# Filter out tracks by known top artists for freshness
top_artist_ids = {a["id"] for a in top_artists["items"]}
fresh = [t for t in results["tracks"]["items"]
if not any(a["id"] in top_artist_ids for a in t["artists"])]
# Fall back to all results if too few fresh ones
tracks = fresh[:params.count] if len(fresh) >= params.count // 2 else results["tracks"]["items"][:params.count]
mode = "🗺️ Adventurous" if params.adventurous else "🏠 Comfort Zone" mode = "🗺️ Adventurous" if params.adventurous else "🏠 Comfort Zone"
lines = [f"🎵 **MusicTail Fresh Finds** — {mode}\n"] lines = [f"🎵 **MusicTail Fresh Finds** — {mode}"]
lines.append(f"Searching: {', '.join(pick_genres)}\n")
for i, track in enumerate(results["tracks"], 1): for i, track in enumerate(tracks[:params.count], 1):
artists = ", ".join(a["name"] for a in track["artists"]) artists = ", ".join(a["name"] for a in track["artists"])
album = track["album"]["name"] album = track["album"]["name"]
pop = track.get("popularity", 0) pop = track.get("popularity", 0)