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:
Binary file not shown.
153
musictail_mcp.py
153
musictail_mcp.py
@@ -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")
|
||||
|
||||
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)
|
||||
|
||||
Reference in New Issue
Block a user