diff --git a/__pycache__/musictail_mcp.cpython-311.pyc b/__pycache__/musictail_mcp.cpython-311.pyc index 36bf912..969b408 100644 Binary files a/__pycache__/musictail_mcp.cpython-311.pyc and b/__pycache__/musictail_mcp.cpython-311.pyc differ diff --git a/musictail_mcp.py b/musictail_mcp.py index e183663..ad879b7 100644 --- a/musictail_mcp.py +++ b/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") + 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)