Mthrfkr commited on
Commit
994c145
·
verified ·
1 Parent(s): 4d76c8d

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +216 -77
app.py CHANGED
@@ -6,123 +6,262 @@ import shutil
6
  import os
7
  from tempfile import NamedTemporaryFile
8
 
9
- # Spotify API credentials from environment variables
10
  client_ids = os.getenv("SPOTIFY_CLIENT_IDS", "").split(',')
11
  client_secrets = os.getenv("SPOTIFY_CLIENT_SECRETS", "").split(',')
12
 
13
  if not client_ids or not client_secrets:
14
- raise ValueError("SPOTIFY_CLIENT_IDS or SPOTIFY_CLIENT_SECRETS environment variables not set.")
15
 
16
- # Token rotation management
17
- current_api_index = 0
18
  request_counter = 0
19
- MAX_REQUESTS_PER_CLIENT = 100 # Rotar cliente después de X peticiones
20
 
21
  def rotate_client():
22
- global current_api_index, request_counter
23
- current_api_index = (current_api_index + 1) % len(client_ids)
24
  request_counter = 0
25
- print(f"Rotando a cliente Spotify #{current_api_index + 1}")
26
 
27
- def get_token():
28
- global current_api_index
29
  for _ in range(len(client_ids)):
30
- client_id = client_ids[current_api_index]
31
- client_secret = client_secrets[current_api_index]
32
- url = 'https://accounts.spotify.com/api/token'
33
- response = requests.post(url,
 
34
  headers={'Content-Type': 'application/x-www-form-urlencoded'},
35
  data={'grant_type': 'client_credentials'},
36
- auth=(client_id, client_secret))
37
 
38
  if response.status_code == 200:
39
  return response.json().get('access_token')
40
- else:
41
- print(f"Error con cliente {current_api_index}: {response.text}")
42
- rotate_client()
43
 
44
- raise Exception("Todos los clientes Spotify fallaron")
45
 
46
- def make_request_with_retry(url, headers, params=None):
 
47
  global request_counter
48
- for _ in range(3): # 3 intentos por cliente
 
 
 
49
  response = requests.get(url, headers=headers, params=params)
50
  request_counter += 1
51
 
52
  if response.status_code == 429:
53
- print(f"Rate limit alcanzado. Cliente actual: {current_api_index}")
54
  rotate_client()
55
- headers['Authorization'] = f'Bearer {get_token()}'
56
- time.sleep(int(response.headers.get('Retry-After', 10)))
57
  continue
58
 
59
  if response.status_code == 200:
60
  if request_counter >= MAX_REQUESTS_PER_CLIENT:
61
  rotate_client()
62
- headers['Authorization'] = f'Bearer {get_token()}' # Actualizar token
63
- return response
64
-
65
- print(f"Error {response.status_code}: {response.text}")
66
  time.sleep(2)
67
 
68
  return None
69
 
70
- # ... (keep your existing functions like get_playlist_tracks, get_album_tracks, etc.) ...
 
 
 
 
 
 
 
 
 
 
 
 
 
71
 
72
- def interface(project_name, spotify_urls, include_all_info=True):
73
- global current_api_index, request_counter
74
- current_api_index = 0
75
- request_counter = 0
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
76
 
77
- # Validación de URLs mejorada
78
- urls_list = []
79
- for url in spotify_urls.strip().split('\n'):
80
- url = url.strip()
81
- if not url: continue
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
82
 
83
- # Extraer tipo correctamente
84
- if "spotify.com" in url:
85
- if "/playlist/" in url:
86
- url_type = "playlist"
87
- elif "/album/" in url:
88
- url_type = "album"
89
- elif "/track/" in url:
90
- url_type = "track"
91
- else:
92
- print(f"URL no soportada: {url}")
93
- continue
94
- urls_list.append((url, url_type))
95
-
96
- if not urls_list:
97
- return gr.Dataframe(value=pd.DataFrame({"Error": ["No valid URLs provided"]})), None
98
-
99
- token = get_token()
100
  all_tracks = []
101
 
102
- for url, url_type in urls_list:
103
  try:
104
- if url_type == "playlist":
105
- print(f"Procesando playlist: {url}")
106
- tracks = get_playlist_tracks(token, url)
107
- all_tracks.extend(tracks)
108
-
109
- elif url_type == "album":
110
- print(f"Procesando álbum: {url}")
111
- tracks = get_album_tracks(token, url)
112
- all_tracks.extend(tracks)
113
-
114
- elif url_type == "track":
115
- print(f"Procesando track: {url}")
116
- track = get_track_info(token, url)
117
- if track:
118
- all_tracks.extend(track)
119
-
120
  except Exception as e:
121
- print(f"Error procesando {url}: {str(e)}")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
122
  continue
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
123
 
124
- # ... (resto del procesamiento igual) ...
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
125
 
126
- # Mantén la interfaz de Gradio igual
127
- iface = gr.Interface(...)
128
- iface.launch()
 
6
  import os
7
  from tempfile import NamedTemporaryFile
8
 
9
+ # Configuración de Credenciales
10
  client_ids = os.getenv("SPOTIFY_CLIENT_IDS", "").split(',')
11
  client_secrets = os.getenv("SPOTIFY_CLIENT_SECRETS", "").split(',')
12
 
13
  if not client_ids or not client_secrets:
14
+ raise ValueError("Faltan variables de entorno SPOTIFY_CLIENT_IDS o SPOTIFY_CLIENT_SECRETS")
15
 
16
+ # Gestión de Rotación de Clientes
17
+ current_client_index = 0
18
  request_counter = 0
19
+ MAX_REQUESTS_PER_CLIENT = 95 # Rotar antes de llegar al límite de 100
20
 
21
  def rotate_client():
22
+ global current_client_index, request_counter
23
+ current_client_index = (current_client_index + 1) % len(client_ids)
24
  request_counter = 0
25
+ print(f"🔁 Rotando a cliente Spotify #{current_client_index + 1}")
26
 
27
+ def get_spotify_token():
28
+ global current_client_index
29
  for _ in range(len(client_ids)):
30
+ client_id = client_ids[current_client_index]
31
+ client_secret = client_secrets[current_client_index]
32
+
33
+ response = requests.post(
34
+ 'https://accounts.spotify.com/api/token',
35
  headers={'Content-Type': 'application/x-www-form-urlencoded'},
36
  data={'grant_type': 'client_credentials'},
37
+ auth=(client_id, client_secret)
38
 
39
  if response.status_code == 200:
40
  return response.json().get('access_token')
41
+
42
+ print(f"Error con cliente {current_client_index}: {response.text}")
43
+ rotate_client()
44
 
45
+ raise Exception("🚨 Todos los clientes fallaron al obtener token")
46
 
47
+ # Funciones Principales
48
+ def make_spotify_request(url, params=None):
49
  global request_counter
50
+ token = get_spotify_token()
51
+ headers = {'Authorization': f'Bearer {token}'}
52
+
53
+ for _ in range(3): # Reintentos máximos
54
  response = requests.get(url, headers=headers, params=params)
55
  request_counter += 1
56
 
57
  if response.status_code == 429:
58
+ print(f"⚠️ Límite de tasa alcanzado. Cliente: {current_client_index}")
59
  rotate_client()
60
+ time.sleep(int(response.headers.get('Retry-After', 10)) # Espera mínima de 10 segundos
 
61
  continue
62
 
63
  if response.status_code == 200:
64
  if request_counter >= MAX_REQUESTS_PER_CLIENT:
65
  rotate_client()
66
+ return response.json()
67
+
68
+ print(f"❌ Error {response.status_code}: {response.text}")
 
69
  time.sleep(2)
70
 
71
  return None
72
 
73
+ def extract_spotify_id(url, type_keyword):
74
+ """Extrae ID de diferentes formatos de URLs de Spotify"""
75
+ if type_keyword in url:
76
+ parts = url.split(f"/{type_keyword}/")
77
+ if len(parts) > 1:
78
+ return parts[1].split("?")[0].split("/")[0]
79
+
80
+ # Manejo de URLs complejas
81
+ parts = [p for p in url.split("/") if p]
82
+ for i, part in enumerate(parts):
83
+ if part == type_keyword and i < len(parts)-1:
84
+ return parts[i+1].split("?")[0]
85
+
86
+ return url.split("/")[-1].split("?")[0]
87
 
88
+ def get_playlist_tracks(playlist_url):
89
+ playlist_id = extract_spotify_id(playlist_url, "playlist")
90
+ print(f"🎧 Obteniendo playlist: {playlist_id}")
91
+
92
+ all_tracks = []
93
+ url = f'https://api.spotify.com/v1/playlists/{playlist_id}/tracks'
94
+
95
+ while url:
96
+ data = make_spotify_request(url)
97
+ if not data:
98
+ break
99
+
100
+ all_tracks.extend([item['track'] for item in data.get('items', []) if item.get('track')])
101
+ url = data.get('next')
102
+
103
+ print(f"✅ Encontrados {len(all_tracks)} tracks")
104
+ return all_tracks
105
+
106
+ def get_album_tracks(album_url):
107
+ album_id = extract_spotify_id(album_url, "album")
108
+ print(f"💿 Obteniendo álbum: {album_id}")
109
 
110
+ # Obtener información básica del álbum
111
+ album_data = make_spotify_request(f'https://api.spotify.com/v1/albums/{album_id}')
112
+ if not album_data:
113
+ return []
114
+
115
+ all_tracks = []
116
+ url = f'https://api.spotify.com/v1/albums/{album_id}/tracks'
117
+
118
+ while url:
119
+ data = make_spotify_request(url)
120
+ if not data:
121
+ break
122
+
123
+ # Obtener detalles completos de cada track
124
+ for item in data.get('items', []):
125
+ track_data = make_spotify_request(f"https://api.spotify.com/v1/tracks/{item['id']}")
126
+ if track_data:
127
+ track_data['album'] = { # Añadir info del álbum
128
+ 'name': album_data.get('name'),
129
+ 'release_date': album_data.get('release_date'),
130
+ 'id': album_id
131
+ }
132
+ all_tracks.append(track_data)
133
 
134
+ url = data.get('next')
135
+
136
+ print(f" Encontrados {len(all_tracks)} tracks")
137
+ return all_tracks
138
+
139
+ def process_tracks(urls):
 
 
 
 
 
 
 
 
 
 
 
140
  all_tracks = []
141
 
142
+ for url in urls:
143
  try:
144
+ if "playlist" in url:
145
+ all_tracks.extend(get_playlist_tracks(url))
146
+ elif "album" in url:
147
+ all_tracks.extend(get_album_tracks(url))
148
+ elif "track" in url:
149
+ track_id = extract_spotify_id(url, "track")
150
+ track_data = make_spotify_request(f"https://api.spotify.com/v1/tracks/{track_id}")
151
+ if track_data:
152
+ all_tracks.append(track_data)
 
 
 
 
 
 
 
153
  except Exception as e:
154
+ print(f"⚠️ Error procesando {url}: {str(e)}")
155
+
156
+ return all_tracks
157
+
158
+ # Procesamiento de Datos
159
+ def create_final_dataframe(tracks, include_all_info=True):
160
+ artist_cache = {}
161
+
162
+ def get_artist_details(artist_id):
163
+ if artist_id not in artist_cache:
164
+ data = make_spotify_request(f"https://api.spotify.com/v1/artists/{artist_id}")
165
+ artist_cache[artist_id] = {
166
+ 'genres': data.get('genres', []) if data else [],
167
+ 'followers': data.get('followers', {}).get('total', 0) if data else 0,
168
+ 'popularity': data.get('popularity', 0) if data else 0
169
+ }
170
+ return artist_cache[artist_id]
171
+
172
+ processed = []
173
+ for track in tracks:
174
+ if not track:
175
  continue
176
+
177
+ main_artist = track['artists'][0] if track.get('artists') else {}
178
+ artist_info = get_artist_details(main_artist.get('id')) if main_artist.get('id') else {}
179
+
180
+ duration_min = f"{track['duration_ms']//60000}:{str(track['duration_ms']%60000//1000).zfill(2)}"
181
+
182
+ track_data = {
183
+ 'Artista': main_artist.get('name', 'Desconocido'),
184
+ 'Título': track.get('name', 'Sin título'),
185
+ 'Álbum': track.get('album', {}).get('name', 'Sin álbum'),
186
+ 'ISRC': track.get('external_ids', {}).get('isrc', 'No disponible'),
187
+ 'Popularidad': track.get('popularity', 0),
188
+ 'Duración': duration_min,
189
+ 'Fecha Lanzamiento': track.get('album', {}).get('release_date', 'No disponible'),
190
+ 'Enlace Spotify': track.get('external_urls', {}).get('spotify', '')
191
+ }
192
+
193
+ if include_all_info:
194
+ track_data.update({
195
+ 'Géneros': ', '.join(artist_info.get('genres', [])),
196
+ 'Seguidores Artista': artist_info.get('followers', 0),
197
+ 'Popularidad Artista': artist_info.get('popularity', 0),
198
+ 'Explicit': 'Sí' if track.get('explicit') else 'No',
199
+ 'ID Track': track.get('id'),
200
+ 'ID Álbum': track.get('album', {}).get('id')
201
+ })
202
+
203
+ processed.append(track_data)
204
 
205
+ df = pd.DataFrame(processed)
206
+
207
+ # Eliminar duplicados usando ISRC o combinación artista-título
208
+ df['dup_key'] = df.apply(
209
+ lambda x: x['ISRC'] if x['ISRC'] != 'No disponible' else f"{x['Artista']}|{x['Título']}",
210
+ axis=1
211
+ )
212
+ df = df.drop_duplicates('dup_key').drop(columns='dup_key')
213
+
214
+ return df
215
+
216
+ # Interfaz Gradio
217
+ def main_interface(project_name, spotify_urls, include_all_info=True):
218
+ urls = [url.strip() for url in spotify_urls.strip().split('\n') if url.strip()]
219
+
220
+ if not urls:
221
+ return pd.DataFrame({"Error": ["Ingresa al menos una URL válida"]}), None
222
+
223
+ print("⏳ Procesando URLs...")
224
+ tracks = process_tracks(urls)
225
+
226
+ if not tracks:
227
+ return pd.DataFrame({"Error": ["No se encontraron tracks"]}), None
228
+
229
+ print("📊 Creando dataframe...")
230
+ df = create_final_dataframe(tracks, include_all_info)
231
+
232
+ with NamedTemporaryFile(delete=False, suffix='.xlsx') as tmp:
233
+ df.to_excel(tmp.name, index=False)
234
+ final_filename = f"{project_name}.xlsx" if project_name else "spotify_tracks.xlsx"
235
+ shutil.move(tmp.name, final_filename)
236
+
237
+ print("✅ Proceso completado")
238
+ return df, final_filename
239
+
240
+ # Configuración de la UI
241
+ iface = gr.Interface(
242
+ fn=main_interface,
243
+ inputs=[
244
+ gr.Textbox(label="Nombre del Proyecto", placeholder="Mi Colección Musical"),
245
+ gr.Textbox(
246
+ label="URLs de Spotify (1 por línea)",
247
+ placeholder="Pega aquí URLs de playlists, álbumes o canciones...",
248
+ lines=5
249
+ ),
250
+ gr.Checkbox(label="Incluir toda la información", value=True)
251
+ ],
252
+ outputs=[
253
+ gr.Dataframe(label="Resultados"),
254
+ gr.File(label="Descargar Excel")
255
+ ],
256
+ title="🟢 Spotify Track Collector PRO",
257
+ description="Extrae información detallada de múltiples URLs de Spotify (playlists, álbumes, canciones) y genera un Excel unificado.",
258
+ examples=[
259
+ ["Mi Playlist", "https://open.spotify.com/playlist/37i9dQZF1DXcBWIGoYBM5M", True],
260
+ ["Álbum Clásico", "https://open.spotify.com/album/1R5BORZZxNUg8QMgbqt0nd", False]
261
+ ],
262
+ allow_flagging="never",
263
+ theme=gr.themes.Soft()
264
+ )
265
 
266
+ if __name__ == "__main__":
267
+ iface.launch(server_name="0.0.0.0", server_port=7860)