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.
155
musictail_mcp.py
155
musictail_mcp.py
@@ -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)
|
||||||
|
|||||||
Reference in New Issue
Block a user