From 065dd35cebbb585f04c6b4e58115ccdbd7b5819e Mon Sep 17 00:00:00 2001 From: Alex Kazaiev Date: Mon, 6 Apr 2026 20:13:59 -0500 Subject: [PATCH] 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 --- __pycache__/musictail_mcp.cpython-311.pyc | Bin 18347 -> 22823 bytes musictail_mcp.py | 155 +++++++++++++++------- 2 files changed, 110 insertions(+), 45 deletions(-) diff --git a/__pycache__/musictail_mcp.cpython-311.pyc b/__pycache__/musictail_mcp.cpython-311.pyc index 36bf912148f44a4d03a0b0eecb2acb86f4a73dbe..969b4089bdb90d89a953d3c74526b6c9f06b0903 100644 GIT binary patch delta 8684 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 JdhaLv{{ps4mUjRE delta 4971 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 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)