Chrunos commited on
Commit
298d016
·
verified ·
1 Parent(s): 4f7c5a0

Create app.py

Browse files
Files changed (1) hide show
  1. app.py +512 -0
app.py ADDED
@@ -0,0 +1,512 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import base64
2
+ import logging
3
+ from typing import Optional, Dict, Any
4
+ from fastapi import FastAPI, HTTPException, Request
5
+ import requests
6
+ from bs4 import BeautifulSoup
7
+ import os
8
+ from datetime import datetime, timedelta
9
+ import time
10
+ from requests.adapters import HTTPAdapter
11
+ from urllib3.util.retry import Retry
12
+ import asyncio
13
+ from typing import Optional, Dict, Tuple
14
+ import urllib.parse
15
+ from fastapi.responses import JSONResponse
16
+
17
+ # Configure logging
18
+ logging.basicConfig(
19
+ level=logging.INFO,
20
+ format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
21
+ handlers=[
22
+ logging.StreamHandler(),
23
+ logging.FileHandler('spotify_api.log')
24
+ ]
25
+ )
26
+ logger = logging.getLogger(__name__)
27
+
28
+ # Define Pydantic model for request body validation (optional but recommended)
29
+ from pydantic import BaseModel, HttpUrl
30
+
31
+ class TrackDlRequest(BaseModel):
32
+ spotify_url: str # Keep as str for flexibility, could use HttpUrl if strict validation needed
33
+ album_cover_url: Optional[str] = None # Make cover optional
34
+
35
+ app = FastAPI(title="Spotify Track API",
36
+ description="API for retrieving Spotify track information and download URLs")
37
+
38
+ # Constants
39
+ SPOTIFY_API_URL = "https://api.spotify.com/v1"
40
+ SPOTIFY_CLIENT_ID = os.getenv("SPOTIFY_CLIENT_ID")
41
+ SPOTIFY_CLIENT_SECRET = os.getenv("SPOTIFY_CLIENT_SECRET")
42
+ TOKEN_EXPIRY = 3500 # Slightly less than 1 hour to ensure token refresh before expiration
43
+
44
+ # Token cache
45
+ class TokenCache:
46
+ def __init__(self):
47
+ self.token: Optional[str] = None
48
+ self.expiry_time: Optional[datetime] = None
49
+
50
+ def set_token(self, token: str):
51
+ self.token = token
52
+ self.expiry_time = datetime.now() + timedelta(seconds=TOKEN_EXPIRY)
53
+
54
+ def get_token(self) -> Optional[str]:
55
+ if not self.token or not self.expiry_time or datetime.now() >= self.expiry_time:
56
+ return None
57
+ return self.token
58
+
59
+ def is_expired(self) -> bool:
60
+ return not self.token or not self.expiry_time or datetime.now() >= self.expiry_time
61
+
62
+
63
+ token_cache = TokenCache()
64
+
65
+ # Custom exception for Spotify API errors
66
+ class SpotifyAPIError(Exception):
67
+ pass
68
+
69
+
70
+ def get_spotify_token() -> str:
71
+ """
72
+ Get Spotify access token with expiration handling.
73
+ Returns a valid token, either from cache or by requesting a new one.
74
+ """
75
+ try:
76
+ # Check if we have a valid cached token
77
+ cached_token = token_cache.get_token()
78
+ if cached_token:
79
+ logger.info("Using cached Spotify token")
80
+ return cached_token
81
+
82
+ logger.info("Requesting new Spotify access token")
83
+ start_time = time.time()
84
+
85
+ if not SPOTIFY_CLIENT_ID or not SPOTIFY_CLIENT_SECRET:
86
+ raise SpotifyAPIError("Spotify credentials not configured")
87
+
88
+ auth_string = f"{SPOTIFY_CLIENT_ID}:{SPOTIFY_CLIENT_SECRET}"
89
+ auth_bytes = base64.b64encode(auth_string.encode()).decode()
90
+
91
+ auth_response = requests.post(
92
+ 'https://accounts.spotify.com/api/token',
93
+ data={'grant_type': 'client_credentials'},
94
+ headers={'Authorization': f'Basic {auth_bytes}'},
95
+ timeout=10
96
+ )
97
+
98
+ if auth_response.status_code != 200:
99
+ raise SpotifyAPIError(f"Failed to get token: {auth_response.text}")
100
+
101
+ new_token = auth_response.json()['access_token']
102
+ token_cache.set_token(new_token)
103
+
104
+ logger.info(f"New token obtained successfully in {time.time() - start_time:.2f}s")
105
+ return new_token
106
+
107
+ except requests.exceptions.RequestException as e:
108
+ logger.error(f"Network error during token request: {str(e)}")
109
+ raise HTTPException(status_code=503, detail="Spotify authentication service unavailable")
110
+ except Exception as e:
111
+ logger.error(f"Unexpected error during token request: {str(e)}")
112
+ raise HTTPException(status_code=500, detail="Internal server error")
113
+
114
+ def extract_album_id(album_url: str) -> str:
115
+ """Extract album ID from Spotify URL."""
116
+ try:
117
+ return album_url.split("/")[-1].split("?")[0]
118
+ except Exception as e:
119
+ logger.error(f"Failed to extract album ID from URL {album_url}: {str(e)}")
120
+ raise HTTPException(status_code=400, detail="Invalid Spotify album URL format")
121
+
122
+
123
+ @app.post("/album")
124
+ async def get_album_data(request: Request):
125
+ try:
126
+ # Get the JSON data from the request
127
+ data = await request.json()
128
+ album_url = data.get('album_url')
129
+ if not album_url:
130
+ raise HTTPException(status_code=400, detail="Missing 'album_url' in JSON data")
131
+
132
+ # Extract the album ID from the URL
133
+ album_id = extract_album_id(album_url)
134
+
135
+ # Get the Spotify access token
136
+ access_token = get_spotify_token()
137
+
138
+ # Make a request to the Spotify API to get album data
139
+ headers = {
140
+ 'Authorization': f'Bearer {access_token}'
141
+ }
142
+ album_api_url = f"{SPOTIFY_API_URL}/albums/{album_id}"
143
+ response = requests.get(album_api_url, headers=headers, timeout=10)
144
+
145
+ if response.status_code != 200:
146
+ raise SpotifyAPIError(f"Failed to get album data: {response.text}")
147
+
148
+ album_data = response.json()
149
+ return album_data
150
+
151
+ except SpotifyAPIError as e:
152
+ logger.error(f"Spotify API error: {str(e)}")
153
+ raise HTTPException(status_code=500, detail=str(e))
154
+ except Exception as e:
155
+ logger.error(f"Unexpected error: {str(e)}")
156
+ raise HTTPException(status_code=500, detail="Internal server error")
157
+
158
+
159
+ def extract_playlist_id(playlist_url: str) -> str:
160
+ """Extract playlist ID from Spotify URL."""
161
+ try:
162
+ return playlist_url.split("/")[-1].split("?")[0]
163
+
164
+ except Exception as e:
165
+ logger.error(f"Failed to extract playlist ID from URL {playlist_url}: {str(e)}")
166
+ raise HTTPException(status_code=400, detail="Invalid Spotify playlist URL format")
167
+
168
+
169
+ @app.post("/playlist")
170
+ async def get_playlist_data(request: Request):
171
+ try:
172
+ # Get the JSON data from the request
173
+ data = await request.json()
174
+ playlist_url = data.get('playlist_url')
175
+ if not playlist_url:
176
+ raise HTTPException(status_code=400, detail="Missing 'playlist_url' in JSON data")
177
+
178
+ # Extract the playlist ID from the URL
179
+ playlist_id = extract_playlist_id(playlist_url)
180
+ logger.info(f"Extracted playlist ID: {playlist_id}")
181
+
182
+ # Get the Spotify access token
183
+ access_token = get_spotify_token()
184
+
185
+ # Make a request to the Spotify API to get playlist data
186
+ headers = {
187
+ 'Authorization': f'Bearer {access_token}'
188
+ }
189
+ playlist_api_url = f"{SPOTIFY_API_URL}/playlists/{playlist_id}/tracks"
190
+ response = requests.get(playlist_api_url, headers=headers, timeout=10)
191
+
192
+ if response.status_code != 200:
193
+ raise SpotifyAPIError(f"Failed to get playlist data: {response.text}")
194
+
195
+ playlist_data = response.json()
196
+ return playlist_data
197
+
198
+ except SpotifyAPIError as e:
199
+ logger.error(f"Spotify API error: {str(e)}")
200
+ raise HTTPException(status_code=500, detail=str(e))
201
+ except Exception as e:
202
+ logger.error(f"Unexpected error: {str(e)}")
203
+ raise HTTPException(status_code=500, detail="Internal server error")
204
+
205
+
206
+ def extract_track_id(track_url: str) -> str:
207
+ """Extract track ID from Spotify URL."""
208
+ try:
209
+ return track_url.split("/")[-1].split("?")[0]
210
+ except Exception as e:
211
+ logger.error(f"Failed to extract track ID from URL {track_url}: {str(e)}")
212
+ raise HTTPException(status_code=400, detail="Invalid Spotify URL format")
213
+
214
+
215
+
216
+
217
+
218
+ # --- MODIFIED Function to get CSRF token and Session from Spowload ---
219
+ def get_spowload_session_and_token() -> Optional[Tuple[requests.Session, str]]:
220
+ """
221
+ Creates a requests session, fetches the spowload.com homepage,
222
+ extracts the CSRF token, and returns both the session and the token.
223
+
224
+ Returns:
225
+ A tuple containing the (requests.Session, csrf_token_string) if successful,
226
+ otherwise None.
227
+ """
228
+ spowload_url = "https://spowload.com" # Use https for security
229
+ headers = {
230
+ # Mimic a common browser user-agent
231
+ 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36',
232
+ 'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8',
233
+ 'Accept-Language': 'en-US,en;q=0.9',
234
+ 'Connection': 'keep-alive',
235
+ }
236
+ # Create a session object to persist cookies
237
+ session = requests.Session()
238
+ session.headers.update(headers) # Set default headers for the session
239
+
240
+ try:
241
+ logger.info(f"Attempting to fetch CSRF token and session cookies from {spowload_url}")
242
+ # Use the session to make the GET request
243
+ response = session.get(spowload_url, timeout=15) # Use session.get
244
+ response.raise_for_status() # Raise an exception for bad status codes (4xx or 5xx)
245
+
246
+ # Parse the HTML content
247
+ soup = BeautifulSoup(response.text, 'html.parser')
248
+
249
+ # Find the meta tag with name="csrf-token"
250
+ meta_tag = soup.find('meta', attrs={'name': 'csrf-token'})
251
+
252
+ if meta_tag and 'content' in meta_tag.attrs:
253
+ csrf_token = meta_tag['content']
254
+ logger.info(f"Successfully extracted CSRF token and established session.")
255
+ # Return the session object AND the token
256
+ return session, csrf_token
257
+ else:
258
+ logger.warning(f"Could not find meta tag with name='csrf-token' or 'content' attribute at {spowload_url}")
259
+ return None
260
+
261
+ except requests.exceptions.Timeout:
262
+ logger.error(f"Request timed out while trying to reach {spowload_url}")
263
+ return None
264
+ except requests.exceptions.RequestException as e:
265
+ logger.error(f"Error fetching {spowload_url}: {str(e)}")
266
+ return None
267
+ except Exception as e:
268
+ # Catch potential BeautifulSoup errors or other unexpected issues
269
+ logger.exception(f"An unexpected error occurred while getting CSRF token/session: {str(e)}")
270
+ return None
271
+
272
+
273
+ # --- /track ENDPOINT (from previous version, CSRF call removed for clarity) ---
274
+ @app.post("/track", response_model=Dict)
275
+ async def get_track_data(request: Request):
276
+ """
277
+ Retrieves specific track information from Spotify based on a track URL.
278
+
279
+ Expects JSON body: {"track_url": "spotify-track-url"}
280
+
281
+ Returns:
282
+ JSON response with track details:
283
+ {
284
+ "id": str,
285
+ "title": str,
286
+ "artists": list[str],
287
+ "album_cover_url": str | None,
288
+ "duration_ms": int,
289
+ "spotify_url": str
290
+ }
291
+ or an HTTP error response.
292
+ """
293
+ try:
294
+ # 1. Get data from request
295
+ try:
296
+ data = await request.json()
297
+ track_url = data.get('track_url')
298
+ except Exception:
299
+ logger.error("Failed to parse request JSON body.")
300
+ raise HTTPException(status_code=400, detail="Invalid JSON body.")
301
+
302
+ if not track_url:
303
+ logger.warning("Request received without 'track_url'.")
304
+ raise HTTPException(status_code=400, detail="Missing 'track_url' in JSON data.")
305
+
306
+ # 2. Extract Track ID
307
+ track_id = extract_track_id(track_url)
308
+ if not track_id:
309
+ logger.warning(f"Failed to extract track ID from URL: {track_url}")
310
+ raise HTTPException(status_code=400, detail="Invalid Spotify track URL format or unable to extract ID.")
311
+
312
+ logger.info(f"Processing request for track ID: {track_id}")
313
+
314
+ # 3. Get Spotify Token
315
+ try:
316
+ access_token = get_spotify_token() # Use the existing function
317
+ except HTTPException as he: # Propagate HTTP exceptions from token function
318
+ raise he
319
+ except Exception as e:
320
+ logger.error(f"Unexpected error getting Spotify token: {str(e)}")
321
+ raise HTTPException(status_code=500, detail="Internal error obtaining Spotify access token.")
322
+
323
+
324
+ # 4. Call Spotify API for Track Info
325
+ headers = {
326
+ 'Authorization': f'Bearer {access_token}'
327
+ }
328
+ # Ensure SPOTIFY_API_URL is the correct base URL (e.g., "https://api.spotify.com/v1")
329
+ track_api_url = f"{SPOTIFY_API_URL}/tracks/{track_id}"
330
+ logger.info(f"Requesting track data from Spotify API: {track_api_url}")
331
+
332
+ try:
333
+ response = requests.get(track_api_url, headers=headers, timeout=15) # Increased timeout slightly
334
+
335
+ # 5. Handle Potential Token Expiry (Retry logic)
336
+ if response.status_code == 401:
337
+ logger.warning("Spotify token likely expired or invalid (received 401). Requesting new one and retrying.")
338
+ try:
339
+ access_token = get_spotify_token() # Force refresh
340
+ except HTTPException as he:
341
+ raise he # Propagate HTTP exceptions from token function
342
+ except Exception as e:
343
+ logger.error(f"Unexpected error getting fresh Spotify token during retry: {str(e)}")
344
+ raise HTTPException(status_code=500, detail="Internal error obtaining fresh Spotify access token.")
345
+
346
+ headers['Authorization'] = f'Bearer {access_token}' # Update header
347
+ response = requests.get(track_api_url, headers=headers, timeout=15) # Retry the request
348
+
349
+ # 6. Handle API Errors after potential retry
350
+ if response.status_code != 200:
351
+ error_detail = f"Spotify API request failed. Status: {response.status_code}, URL: {track_api_url}, Response: {response.text[:200]}..." # Limit response length in log
352
+ logger.error(error_detail)
353
+ # Map Spotify errors to appropriate HTTP status codes
354
+ if response.status_code == 400:
355
+ raise HTTPException(status_code=400, detail=f"Bad request to Spotify API (check track ID format?).")
356
+ elif response.status_code == 404:
357
+ raise HTTPException(status_code=404, detail=f"Track ID '{track_id}' not found on Spotify.")
358
+ else:
359
+ # Use 502 Bad Gateway for upstream errors
360
+ raise HTTPException(status_code=502, detail=f"Failed to retrieve data from Spotify (Status: {response.status_code}).")
361
+
362
+ # 7. Process and Format Response
363
+ track_data = response.json()
364
+
365
+ # Extract desired information safely using .get() with defaults
366
+ artists = [artist.get("name") for artist in track_data.get("artists", []) if artist.get("name")]
367
+ album_images = track_data.get("album", {}).get("images", [])
368
+ cover_url = None
369
+ if len(album_images) > 1:
370
+ cover_url = album_images[1].get("url") # Prefer medium image (index 1)
371
+ elif len(album_images) > 0:
372
+ cover_url = album_images[0].get("url") # Fallback to largest (index 0)
373
+
374
+ track_info = {
375
+ "id": track_data.get("id"),
376
+ "title": track_data.get("name"),
377
+ "artists": artists,
378
+ "album_cover_url": cover_url,
379
+ "duration_ms": track_data.get("duration_ms"),
380
+ "spotify_url": track_data.get("external_urls", {}).get("spotify")
381
+ # Removed spowload_csrf_token from this endpoint's response
382
+ }
383
+
384
+ logger.info(f"Successfully retrieved data for track ID: {track_id}")
385
+ return JSONResponse(content=track_info)
386
+
387
+ except requests.exceptions.RequestException as e:
388
+ logger.error(f"Network error contacting Spotify API at {track_api_url}: {str(e)}")
389
+ raise HTTPException(status_code=504, detail=f"Gateway timeout or network error contacting Spotify.")
390
+ except Exception as e: # Catch potential JSON parsing errors or other issues
391
+ logger.exception(f"Error processing Spotify response or formatting data for track {track_id}: {str(e)}")
392
+ raise HTTPException(status_code=500, detail="Internal server error processing track data.")
393
+
394
+
395
+ # 8. General Exception Handling (Catchall)
396
+ except HTTPException as e:
397
+ # Re-raise FastAPI/manual HTTP exceptions so FastAPI handles them
398
+ raise e
399
+ except Exception as e:
400
+ # Log any unexpected errors that weren't caught above
401
+ logger.exception(f"An unexpected critical error occurred in /track endpoint: {str(e)}") # Log full traceback
402
+ raise HTTPException(status_code=500, detail="An unexpected internal server error occurred.")
403
+
404
+
405
+ # --- MODIFIED /track_dl ENDPOINT ---
406
+ @app.post("/track_dl", response_model=Dict[str, Any]) # Use Dict[str, Any] for flexible response
407
+ async def get_track_download_info(payload: TrackDlRequest):
408
+ """
409
+ Attempts to get download information for a Spotify track via spowload.com,
410
+ using a persistent session for CSRF handling.
411
+
412
+ Expects JSON body: {"spotify_url": "...", "album_cover_url": "..."}
413
+
414
+ Returns:
415
+ The JSON response from spowload.com/convert if successful,
416
+ otherwise an HTTP error response.
417
+ """
418
+ logger.info(f"Received request for /track_dl for URL: {payload.spotify_url}")
419
+
420
+ # 1. Get Session and CSRF Token from Spowload
421
+ session_data = get_spowload_session_and_token()
422
+ if not session_data:
423
+ logger.error("Failed to retrieve session and CSRF token from spowload.com.")
424
+ raise HTTPException(status_code=503, detail="Could not get necessary session/token from the download service.")
425
+
426
+ # Unpack the session and token
427
+ spowload_session, csrf_token = session_data
428
+
429
+ # 2. Prepare request for spowload.com/convert
430
+ convert_url = "https://spowload.com/convert"
431
+ # Headers are now mostly set on the session, but we need to add the CSRF token
432
+ # and ensure Content-Type is set for this specific POST request.
433
+ headers = {
434
+ 'Content-Type': 'application/json',
435
+ 'X-CSRF-Token': csrf_token,
436
+ 'Accept': 'application/json, text/plain, */*', # Override default session Accept for API call
437
+ 'Referer': 'https://spowload.com/', # Keep Referer
438
+ 'Origin': 'https://spowload.com', # Keep Origin
439
+ }
440
+ # Construct the body exactly as specified
441
+ body = {
442
+ "urls": payload.spotify_url,
443
+ "cover": payload.album_cover_url # Use the provided cover URL
444
+ }
445
+
446
+ logger.info(f"Sending request to {convert_url} for Spotify URL: {payload.spotify_url} using established session.")
447
+
448
+ # 3. Make the POST request to spowload.com/convert USING THE SESSION
449
+ try:
450
+ # Use the session object obtained earlier to make the POST request
451
+ # It will automatically send cookies associated with the session
452
+ response = spowload_session.post(convert_url, headers=headers, json=body, timeout=30) # Use session.post
453
+ response.raise_for_status() # Check for 4xx/5xx errors
454
+
455
+ # 4. Process the response
456
+ try:
457
+ result = response.json()
458
+ logger.info(f"Successfully received response from {convert_url}.")
459
+ # Add basic check if the result seems valid (depends on spowload's response structure)
460
+ if isinstance(result, dict) and result.get("success"): # Example check
461
+ logger.info("Spowload response indicates success.")
462
+ elif isinstance(result, dict):
463
+ logger.warning(f"Spowload response received but may indicate failure: {result}")
464
+ return JSONResponse(content=result)
465
+ except json.JSONDecodeError:
466
+ logger.error(f"Failed to decode JSON response from {convert_url}. Response text: {response.text[:200]}...")
467
+ raise HTTPException(status_code=502, detail="Received invalid response format from the download service.")
468
+
469
+ except requests.exceptions.Timeout:
470
+ logger.error(f"Request timed out while contacting {convert_url}")
471
+ raise HTTPException(status_code=504, detail="Download service timed out.")
472
+ except requests.exceptions.HTTPError as e:
473
+ # Log specific HTTP errors from spowload
474
+ # Check for 419 specifically now
475
+ if e.response.status_code == 419:
476
+ logger.error(f"Received 419 Page Expired error from {convert_url}. CSRF token or session likely invalid despite using session.")
477
+ raise HTTPException(status_code=419, detail="Download service reported 'Page Expired' (CSRF/Session issue).")
478
+ logger.error(f"HTTP error {e.response.status_code} received from {convert_url}. Response: {e.response.text[:200]}...")
479
+ # Pass a more specific error back to the client if possible
480
+ if e.response.status_code == 429: # Too Many Requests
481
+ raise HTTPException(status_code=429, detail="Rate limited by the download service. Try again later.")
482
+ elif e.response.status_code == 403: # Forbidden
483
+ raise HTTPException(status_code=403, detail="Request forbidden by the download service.")
484
+ else:
485
+ raise HTTPException(status_code=502, detail=f"Download service returned an error (Status: {e.response.status_code}).")
486
+ except requests.exceptions.RequestException as e:
487
+ logger.error(f"Network error contacting {convert_url}: {str(e)}")
488
+ raise HTTPException(status_code=502, detail="Network error communicating with the download service.")
489
+ except Exception as e:
490
+ logger.exception(f"An unexpected error occurred during /track_dl processing: {str(e)}")
491
+ raise HTTPException(status_code=500, detail="An unexpected internal server error occurred.")
492
+ finally:
493
+ # Close the session to release resources (optional but good practice)
494
+ if 'spowload_session' in locals():
495
+ spowload_session.close()
496
+
497
+
498
+
499
+ @app.get("/")
500
+ async def health_check():
501
+ """Health check endpoint."""
502
+ try:
503
+ # Test Spotify API token generation
504
+ token = get_spotify_token()
505
+ return {
506
+ "status": "healthy",
507
+ "spotify_auth": "ok",
508
+ "token_expires_in": token_cache.expiry_time.timestamp() - datetime.now().timestamp() if token_cache.expiry_time else None
509
+ }
510
+ except Exception as e:
511
+ logger.error(f"Health check failed: {str(e)}")
512
+ return {"status": "unhealthy", "error": str(e)}