import os import sys import io import random import math import requests import spotipy import gradio as gr import matplotlib.pyplot as plt import pandas as pd from spotipy.oauth2 import SpotifyClientCredentials from PIL import Image # Spotify credentials from environment variables (fallback) ENV_SPOTIFY_CLIENT_ID = os.getenv("SPOTIFY_CLIENT_ID") ENV_SPOTIFY_CLIENT_SECRET = os.getenv("SPOTIFY_CLIENT_SECRET") if not ENV_SPOTIFY_CLIENT_ID or not ENV_SPOTIFY_CLIENT_SECRET: print("Error: Spotify credentials not set.") sys.exit(1) global_sp = spotipy.Spotify(client_credentials_manager=SpotifyClientCredentials( client_id=ENV_SPOTIFY_CLIENT_ID, client_secret=ENV_SPOTIFY_CLIENT_SECRET )) def get_musicbrainz_genre(artist_name): search_url = "https://musicbrainz.org/ws/2/artist/" headers = {"User-Agent": "SpotifyAnalyzer/1.0 (your-email@example.com)"} params = {"query": artist_name, "fmt": "json"} try: search_response = requests.get(search_url, params=params, headers=headers) search_data = search_response.json() if "artists" in search_data and search_data["artists"]: best_artist = None best_score = 0 for artist in search_data["artists"]: name = artist.get("name", "") score = int(artist.get("score", 0)) if name.lower() == artist_name.lower(): best_artist = artist break if score > best_score: best_score = score best_artist = artist if best_artist: mbid = best_artist.get("id") if mbid: lookup_url = f"https://musicbrainz.org/ws/2/artist/{mbid}" lookup_params = {"inc": "tags+genres", "fmt": "json"} lookup_response = requests.get(lookup_url, params=lookup_params, headers=headers) lookup_data = lookup_response.json() official_genres = lookup_data.get("genres", []) if official_genres: return official_genres[0].get("name", "Unknown") tags = lookup_data.get("tags", []) if tags: sorted_tags = sorted(tags, key=lambda t: t.get("count", 0), reverse=True) return sorted_tags[0].get("name", "Unknown") except Exception: pass return "Unknown" def get_audiodb_genre(artist_name): url = "https://theaudiodb.com/api/v1/json/1/search.php" params = {"s": artist_name} try: response = requests.get(url, params=params) if response.ok: data = response.json() if data and data.get("artists"): artist_data = data["artists"][0] genre = artist_data.get("strGenre", "") if genre: return genre except Exception: pass return "Unknown" def extract_playlist_id(url: str) -> str: if "playlist" not in url: return "" parts = url.split("/") try: idx = parts.index("playlist") return parts[idx + 1].split("?")[0] except (ValueError, IndexError): return "" def get_playlist_tracks(playlist_id: str, spotify_client) -> list: tracks = [] try: results = spotify_client.playlist_tracks(playlist_id) tracks.extend(results["items"]) while results["next"]: results = spotify_client.next(results) tracks.extend(results["items"]) except spotipy.SpotifyException: return [] return tracks def analyze_playlist(playlist_url: str, spotify_client_id: str, spotify_client_secret: str): if spotify_client_id.strip() and spotify_client_secret.strip(): local_sp = spotipy.Spotify(client_credentials_manager=SpotifyClientCredentials( client_id=spotify_client_id.strip(), client_secret=spotify_client_secret.strip() )) else: local_sp = global_sp playlist_id = extract_playlist_id(playlist_url.strip()) if not playlist_id: return ("Invalid playlist URL.", None, None, None, None, "") tracks = get_playlist_tracks(playlist_id, local_sp) if not tracks: return ("No tracks found or playlist is private.", None, None, None, None, "") genre_count = {} artist_cache = {} tracks_table = [] for item in tracks: track = item.get("track") if not track: continue track_name = track.get("name", "Unknown Track") artists = track.get("artists", []) if not artists: continue artist_info = artists[0] artist_name = artist_info.get("name", "Unknown Artist") artist_id = artist_info.get("id") # Try Spotify genres; if empty, fallback to MusicBrainz then AudioDB. if artist_id: if artist_id in artist_cache: genres = artist_cache[artist_id] else: try: artist_data = local_sp.artist(artist_id) genres = artist_data.get("genres", []) except spotipy.SpotifyException: genres = [] if not genres: mb_genre = get_musicbrainz_genre(artist_name) if mb_genre == "Unknown": audiodb_genre = get_audiodb_genre(artist_name) if audiodb_genre != "Unknown": genres = [audiodb_genre] else: genres = [mb_genre] artist_cache[artist_id] = genres else: genres = [] if genres: for g in genres: genre_count[g] = genre_count.get(g, 0) + 1 primary_genre = genres[0] if genres else "Unknown" spotify_url = track.get("external_urls", {}).get("spotify", "#") query = f"{track_name} {artist_name}" yt_link = f'YouTube Music' tracks_table.append([track_name, artist_name, primary_genre, f'Listen on Spotify', yt_link]) total_occurrences = sum(genre_count.values()) genres_table_data = [[genre, count, f"{(count / total_occurrences * 100):.2f}%"] for genre, count in genre_count.items()] genres_table_data.sort(key=lambda x: x[1], reverse=True) genres_df = pd.DataFrame(genres_table_data, columns=["Genre", "Count", "Percentage"]) top15 = genres_df.head(15) plt.figure(figsize=(10, 6)) plt.bar(top15["Genre"], top15["Count"], color='skyblue') plt.xticks(rotation=45, ha="right") plt.xlabel("Genre") plt.ylabel("Count") plt.title("Top 15 Genres") plt.tight_layout() buf = io.BytesIO() plt.savefig(buf, format='png') plt.close() buf.seek(0) chart_image = Image.open(buf).convert("RGB") tracks_df = pd.DataFrame(tracks_table, columns=["Song Name", "Artist", "Genre", "Spotify Link", "YouTube Music Link"]) table_style = """ """ tracks_html = table_style + tracks_df.to_html(escape=False, index=False, classes="nice-table") # Prepare state for recommendations: exclude "Unknown" genres. top_genres = [genre for genre in genres_df["Genre"].head(15).tolist() if genre.lower() != "unknown"] if not top_genres: top_genres = ["pop"] num_genres = len(top_genres) rec_per_genre = math.ceil(75 / num_genres) existing_tracks = set((t[0].strip().lower(), t[1].strip().lower()) for t in tracks_table) analysis_state = { "top_genres": top_genres, "existing_tracks": existing_tracks, "sp_client_id": spotify_client_id, "sp_client_secret": spotify_client_secret, "rec_per_genre": rec_per_genre } recommended_html = generate_recommendations(analysis_state, local_sp, table_style) processed_info = f"Processed {len(tracks_table)} songs from the playlist." return (genres_df, chart_image, tracks_html, recommended_html, analysis_state, processed_info) def generate_recommendations(state, local_sp, table_style): rec_tracks = [] recommended_artists = set() for genre in state["top_genres"]: try: search_result = local_sp.search(q=f'genre:"{genre}"', type="track", limit=state["rec_per_genre"]) total = search_result.get("tracks", {}).get("total", 0) if total > state["rec_per_genre"]: offset = random.randint(0, min(total - state["rec_per_genre"], 100)) search_result = local_sp.search(q=f'genre:"{genre}"', type="track", limit=state["rec_per_genre"], offset=offset) items = search_result.get("tracks", {}).get("items", []) for t in items: track_name = t.get("name", "Unknown Track") artists_list = [a.get("name", "") for a in t.get("artists", [])] if not artists_list: continue artist_str = ", ".join(artists_list) first_artist = artists_list[0].strip().lower() if (track_name.strip().lower(), first_artist) in state["existing_tracks"] or first_artist in recommended_artists: continue spotify_url = t.get("external_urls", {}).get("spotify", "#") query = f"{track_name} {artist_str}" yt_link = f'YouTube Music' rec_tracks.append([f"{track_name} by {artist_str}", genre, f'Listen on Spotify', yt_link]) recommended_artists.add(first_artist) except Exception: continue if len(rec_tracks) > 75: rec_tracks = random.sample(rec_tracks, 75) rec_tracks_df = pd.DataFrame(rec_tracks, columns=["Title + Author", "Genre", "Spotify Link", "YouTube Music Link"]) return table_style + rec_tracks_df.to_html(escape=False, index=False, classes="nice-table") def refresh_recommendations(state): if state["sp_client_id"].strip() and state["sp_client_secret"].strip(): local_sp = spotipy.Spotify(client_credentials_manager=SpotifyClientCredentials( client_id=state["sp_client_id"].strip(), client_secret=state["sp_client_secret"].strip() )) else: local_sp = global_sp table_style = """ """ return generate_recommendations(state, local_sp, table_style) # Main interface description and disclaimer description_text = ( "This agent analyzes a public Spotify playlist (must be user-shared; providing a playlist uploaded by Spotify will result in an error) " "by generating a genre distribution, a track list (with direct Spotify and YouTube Music search links), and a table of recommended tracks " "based on the top genres found in the playlist. API keys are not stored. Use the 'Refresh recommendations' button to get a new set of recommendations." ) disclaimer_text = ( "Disclaimer: This tool works best for playlists with around 100-200 songs (30-60s). For larger playlists, processing may take multiple minutes. " "A default API key is provided, but if you reach the limits, you can supply your own API keys, which you can quickly obtain from " "Spotify Developer.
" "Note: If the agent is processing for too long, check the logs. If you see a message like 'Your application has reached a rate/request limit', " "it means that the provided Spotify API key has reached its limits. Please generate your own API keys and add them." ) with gr.Blocks() as demo: gr.Markdown("# Spotify Playlist Analyzer & Recommendations + YouTube Music Links") gr.Markdown(disclaimer_text) gr.Markdown(description_text) with gr.Row(): playlist_url = gr.Textbox( label="Spotify Playlist URL", placeholder="e.g. https://open.spotify.com/playlist/1zgenIMomxFp4irGwgW4Rb" ) with gr.Row(): sp_client_id = gr.Textbox(label="Spotify Client ID (optional)") sp_client_secret = gr.Textbox(label="Spotify Client Secret (optional)") analyze_button = gr.Button("Analyze Playlist") with gr.Tab("Analysis Results"): output_genres = gr.Dataframe(label="Genre Distribution Table") output_chart = gr.Image(label="Top 15 Genre Chart") output_tracks_html = gr.HTML(label="Playlist Tracks Table") output_processed = gr.HTML(label="Processing Info") with gr.Tab("Recommended Tracks"): refresh_button = gr.Button("Refresh recommendations") recommended_html_output = gr.HTML(label="Recommended Tracks Table") state_out = gr.State() # Will hold the analysis state def run_analysis(playlist_url, sp_client_id, sp_client_secret): result = analyze_playlist(playlist_url, sp_client_id, sp_client_secret) # result: (genres_df, chart_image, tracks_html, recommended_html, analysis_state, processed_info) return result analyze_button.click( fn=run_analysis, inputs=[playlist_url, sp_client_id, sp_client_secret], outputs=[output_genres, output_chart, output_tracks_html, recommended_html_output, state_out, output_processed] ) refresh_button.click( fn=refresh_recommendations, inputs=[state_out], outputs=[recommended_html_output] ) if __name__ == "__main__": demo.launch(share=True)