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:
"""
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()
# 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")
sp = get_spotify_client()
seed_ids = []
for t in (top_short["items"][:3] + top_medium["items"][:2]):
if t["id"] not in seed_ids:
seed_ids.append(t["id"])
seed_ids = seed_ids[:5] # Spotify max 5 seeds
# Get top artists to extract genres
top_artists = sp.current_user_top_artists(limit=10, time_range="short_term")
# Build recommendation kwargs
kwargs = {"seed_tracks": seed_ids, "limit": params.count}
if params.mood and params.mood.value in MOOD_FEATURES:
kwargs.update(MOOD_FEATURES[params.mood.value])
# Collect genres from top artists
genres = []
for a in top_artists["items"]:
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.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"])
album = track["album"]["name"]
tid = track["id"]
@@ -157,7 +189,7 @@ async def suggest_music(params: SuggestInput) -> str:
async def discover_artist(params: DiscoverInput) -> str:
"""
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()
@@ -167,15 +199,28 @@ async def discover_artist(params: DiscoverInput) -> str:
return f"Could not find artist: {params.artist_name}"
artist = search["artists"]["items"][0]
artist_id = artist["id"]
genres = ", ".join(artist.get("genres", [])[:5]) or "no genres listed"
artist_genres = artist.get("genres", [])
genres_str = ", ".join(artist_genres[:5]) or "no genres listed"
# Get related artists
related = sp.artist_related_artists(artist_id)
similar = related["artists"][:params.count]
if not artist_genres:
return f"**{artist['name']}** has no genres listed on Spotify — can't find similar artists without genre data."
# 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.append(f"Genres: {genres}\n")
lines.append(f"Genres: {genres_str}\n")
for i, sim in enumerate(similar, 1):
sim_genres = ", ".join(sim.get("genres", [])[:3]) or ""
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("")
if not similar:
lines.append("No similar artists found in these genres.")
return "\n".join(lines)
@@ -259,31 +307,48 @@ async def fresh_finds(params: FreshFindsInput) -> str:
"""
sp = get_spotify_client()
if params.adventurous:
# Use long-term + different seed strategy
top = sp.current_user_top_artists(limit=10, time_range="long_term")
# Pick less-obvious artists (positions 5-10 instead of 1-5)
seed_artists = [a["id"] for a in top["items"][4:9]][:5]
kwargs = {
"seed_artists": seed_artists,
"limit": params.count,
"min_popularity": 5,
"max_popularity": 50, # Push toward obscure
}
# Get genres from top artists
time_range = "long_term" if params.adventurous else "short_term"
top_artists = sp.current_user_top_artists(limit=15, time_range=time_range)
# Collect and rank genres
genre_count = {}
for a in top_artists["items"]:
for g in a.get("genres", []):
genre_count[g] = genre_count.get(g, 0) + 1
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:
# Use recent tracks as seeds
top = sp.current_user_top_tracks(limit=10, time_range="short_term")
seed_tracks = [t["id"] for t in top["items"][:5]]
kwargs = {
"seed_tracks": seed_tracks,
"limit": params.count,
}
results = sp.recommendations(**kwargs)
pick_genres = [g for g, _ in sorted_genres[:3]]
query_extra = ""
if not pick_genres:
return "Could not determine your genres. Listen to more music first!"
# Search for tracks in those genres
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"
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"])
album = track["album"]["name"]
pop = track.get("popularity", 0)