Spaces:
Running
Running
File size: 14,607 Bytes
3ce445b 1a5f803 1614441 308dee3 1a5f803 3ce445b f916083 1a5f803 8d7f55f 3ce445b 8d7f55f edad03c 8d7f55f f916083 3ce445b edad03c 3ce445b fb93808 1007d5f 13367cb 1007d5f fb93808 1007d5f 1e7fd76 3ce445b f916083 3ce445b f916083 1a5f803 f916083 9b5b26a edad03c 98e479e 9b5b26a 1007d5f f916083 98e479e 1007d5f 98e479e 1007d5f 98e479e d79880a edad03c bd7c9f5 3a3a87e edad03c bd7c9f5 3a3a87e bd7c9f5 13367cb 1007d5f f916083 bd7c9f5 1a5f803 1007d5f 1a5f803 053e2a6 1a5f803 d79880a 3a3a87e 8d7f55f 053e2a6 100f1e5 1a5f803 3a3a87e 1a5f803 3430f6c 3a3a87e d79880a c3a69a1 8d7f55f c3a69a1 3a3a87e 1007d5f 053e2a6 ecba72d 053e2a6 308dee3 053e2a6 308dee3 3a3a87e 308dee3 3a3a87e 308dee3 3a3a87e 100f1e5 308dee3 f45089c 308dee3 f45089c 1007d5f 1614441 308dee3 1007d5f d93cb29 8d7f55f 100f1e5 308dee3 100f1e5 8d7f55f 100f1e5 1614441 100f1e5 8d7f55f 100f1e5 f45089c d93cb29 308dee3 f45089c 308dee3 f45089c 3a3a87e 1a5f803 1007d5f 13367cb 3a3a87e 1007d5f 3a3a87e f45089c bc5e932 1007d5f f45089c 8d7f55f f45089c 8d7f55f f45089c 8d7f55f 3a3a87e f45089c 8d7f55f 308dee3 8d7f55f f45089c 308dee3 d79880a 308dee3 3a3a87e 308dee3 f45089c 8d7f55f d79880a 3a3a87e 308dee3 f45089c 9b5b26a 3ce445b 99fb280 |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 |
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 ([email protected])"}
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'<a href="https://music.youtube.com/search?q={requests.utils.quote(query)}" target="_blank">YouTube Music</a>'
tracks_table.append([track_name, artist_name, primary_genre,
f'<a href="{spotify_url}" target="_blank">Listen on Spotify</a>', 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 = """
<style>
.nice-table { width: 100%; border-collapse: collapse; margin-top: 1em; }
.nice-table th, .nice-table td { border: 1px solid #ccc; padding: 8px; }
.nice-table th { background-color: #f9f9f9; font-weight: bold; }
.nice-table tr:nth-child(even) { background-color: #f2f2f2; }
.nice-table a { color: #007bff; text-decoration: none; }
.nice-table a:hover { text-decoration: underline; }
</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'<a href="https://music.youtube.com/search?q={requests.utils.quote(query)}" target="_blank">YouTube Music</a>'
rec_tracks.append([f"{track_name} by {artist_str}", genre,
f'<a href="{spotify_url}" target="_blank">Listen on Spotify</a>',
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 = """
<style>
.nice-table { width: 100%; border-collapse: collapse; margin-top: 1em; }
.nice-table th, .nice-table td { border: 1px solid #ccc; padding: 8px; }
.nice-table th { background-color: #f9f9f9; font-weight: bold; }
.nice-table tr:nth-child(even) { background-color: #f2f2f2; }
.nice-table a { color: #007bff; text-decoration: none; }
.nice-table a:hover { text-decoration: underline; }
</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 = (
"<b>Disclaimer:</b> 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 "
"<a href='https://developer.spotify.com/' target='_blank'>Spotify Developer</a>.<br>"
"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")
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)
|