Chrunos commited on
Commit
33eac3d
·
verified ·
1 Parent(s): 2a58ed0

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +195 -153
app.py CHANGED
@@ -7,11 +7,12 @@ from bs4 import BeautifulSoup
7
  import os
8
  from datetime import datetime, timedelta
9
  import time
10
- import requests
11
  from requests.adapters import HTTPAdapter
12
  from urllib3.util.retry import Retry
13
  import asyncio
14
  from typing import Optional, Dict, Tuple
 
 
15
 
16
  # Configure logging
17
  logging.basicConfig(
@@ -25,7 +26,7 @@ logging.basicConfig(
25
  logger = logging.getLogger(__name__)
26
 
27
  app = FastAPI(title="Spotify Track API",
28
- description="API for retrieving Spotify track information and download URLs")
29
 
30
  # Constants
31
  SPOTIFY_API_URL = "https://api.spotify.com/v1"
@@ -38,25 +39,27 @@ class TokenCache:
38
  def __init__(self):
39
  self.token: Optional[str] = None
40
  self.expiry_time: Optional[datetime] = None
41
-
42
  def set_token(self, token: str):
43
  self.token = token
44
  self.expiry_time = datetime.now() + timedelta(seconds=TOKEN_EXPIRY)
45
-
46
  def get_token(self) -> Optional[str]:
47
  if not self.token or not self.expiry_time or datetime.now() >= self.expiry_time:
48
  return None
49
  return self.token
50
-
51
  def is_expired(self) -> bool:
52
  return not self.token or not self.expiry_time or datetime.now() >= self.expiry_time
53
 
 
54
  token_cache = TokenCache()
55
 
56
  # Custom exception for Spotify API errors
57
  class SpotifyAPIError(Exception):
58
  pass
59
 
 
60
  def get_spotify_token() -> str:
61
  """
62
  Get Spotify access token with expiration handling.
@@ -71,29 +74,29 @@ def get_spotify_token() -> str:
71
 
72
  logger.info("Requesting new Spotify access token")
73
  start_time = time.time()
74
-
75
  if not SPOTIFY_CLIENT_ID or not SPOTIFY_CLIENT_SECRET:
76
  raise SpotifyAPIError("Spotify credentials not configured")
77
-
78
  auth_string = f"{SPOTIFY_CLIENT_ID}:{SPOTIFY_CLIENT_SECRET}"
79
  auth_bytes = base64.b64encode(auth_string.encode()).decode()
80
-
81
  auth_response = requests.post(
82
  'https://accounts.spotify.com/api/token',
83
  data={'grant_type': 'client_credentials'},
84
  headers={'Authorization': f'Basic {auth_bytes}'},
85
  timeout=10
86
  )
87
-
88
  if auth_response.status_code != 200:
89
  raise SpotifyAPIError(f"Failed to get token: {auth_response.text}")
90
-
91
  new_token = auth_response.json()['access_token']
92
  token_cache.set_token(new_token)
93
-
94
  logger.info(f"New token obtained successfully in {time.time() - start_time:.2f}s")
95
  return new_token
96
-
97
  except requests.exceptions.RequestException as e:
98
  logger.error(f"Network error during token request: {str(e)}")
99
  raise HTTPException(status_code=503, detail="Spotify authentication service unavailable")
@@ -101,6 +104,98 @@ def get_spotify_token() -> str:
101
  logger.error(f"Unexpected error during token request: {str(e)}")
102
  raise HTTPException(status_code=500, detail="Internal server error")
103
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
104
  def extract_track_id(track_url: str) -> str:
105
  """Extract track ID from Spotify URL."""
106
  try:
@@ -109,83 +204,71 @@ def extract_track_id(track_url: str) -> str:
109
  logger.error(f"Failed to extract track ID from URL {track_url}: {str(e)}")
110
  raise HTTPException(status_code=400, detail="Invalid Spotify URL format")
111
 
112
- # Constants
113
- DOWNLOAD_TIMEOUT = 30 # Increased timeout for download URLs
114
- MAX_RETRIES = 3
115
- BACKOFF_FACTOR = 0.5 # Will sleep for [0.5, 1.0, 2.0] seconds between retries
116
-
117
- # Create a session with retry strategy
118
- def create_request_session() -> requests.Session:
119
- session = requests.Session()
120
- retry_strategy = Retry(
121
- total=MAX_RETRIES,
122
- backoff_factor=BACKOFF_FACTOR,
123
- status_forcelist=[408, 429, 500, 502, 503, 504],
124
- )
125
- adapter = HTTPAdapter(max_retries=retry_strategy)
126
- session.mount("http://", adapter)
127
- session.mount("https://", adapter)
128
- return session
129
-
130
- # Create a session to be reused
131
- session = create_request_session()
132
-
133
- async def get_track_download_url(track_id: str) -> Tuple[str, str]:
134
- """
135
- Get download URL with fallback and retry logic.
136
- Returns tuple of (url, api_version) where api_version is 'v1' or 'v2'
137
- """
138
- apis = [
139
- ("https://spotisongdownloader.vercel.app/", "v2"),
140
- ("https://spotisongdownloader.vercel.app/", "v1")
141
- ]
142
-
143
- errors = []
144
-
145
- for api_base, version in apis:
146
- api_url = f"{api_base}{track_id}"
147
- try:
148
- logger.info(f"Attempting to get download URL from: {api_url}")
149
-
150
- # Make the request with increased timeout and session
151
- response = session.get(
152
- api_url,
153
- timeout=DOWNLOAD_TIMEOUT,
154
- headers={'User-Agent': 'Mozilla/5.0'} # Adding user agent to prevent some blocks
155
- )
156
-
157
- if response.status_code == 200:
158
- download_url = response.json().get("url")
159
- if download_url:
160
- logger.info(f"Successfully obtained download URL from {api_url}")
161
- return download_url, version
162
- else:
163
- logger.warning(f"No URL in response from {api_url}")
164
- errors.append(f"{version}: Empty URL in response")
165
- else:
166
- logger.warning(f"Failed response from {api_url}: {response.status_code}")
167
- errors.append(f"{version}: Status {response.status_code}")
168
-
169
- except requests.exceptions.Timeout as e:
170
- logger.warning(f"Timeout for {api_url}: {str(e)}")
171
- errors.append(f"{version}: Timeout after {DOWNLOAD_TIMEOUT}s")
172
- continue
173
- except requests.exceptions.RequestException as e:
174
- logger.warning(f"Failed to get download URL from {api_url}: {str(e)}")
175
- errors.append(f"{version}: {str(e)}")
176
- continue
177
- except Exception as e:
178
- logger.error(f"Unexpected error for {api_url}: {str(e)}")
179
- errors.append(f"{version}: Unexpected error - {str(e)}")
180
- continue
181
-
182
- # If we get here, both APIs failed
183
- error_msg = " | ".join(errors)
184
- logger.error(f"All download URL attempts failed for track {track_id}: {error_msg}")
185
- raise HTTPException(
186
- status_code=404,
187
- detail=f"Download URL not found. Errors: {error_msg}"
188
- )
189
 
190
  @app.get("/get-track")
191
  async def get_track(
@@ -194,10 +277,8 @@ async def get_track(
194
  track_url: Optional[str] = None
195
  ):
196
  """Get track information and download URL."""
197
- request_id = datetime.now().strftime('%Y%m%d%H%M%S%f')
198
- logger.info(f"Request {request_id} started - Track ID: {track_id}, URL: {track_url}")
199
 
200
- start_time = time.time()
201
 
202
  try:
203
  # Input validation
@@ -207,68 +288,29 @@ async def get_track(
207
  # Extract track ID from URL if provided
208
  if track_url:
209
  track_id = extract_track_id(track_url)
210
-
211
- # Get Spotify API token
212
- access_token = get_spotify_token()
213
-
214
- # Get track metadata from Spotify
215
- logger.info(f"Request {request_id} - Fetching track metadata for {track_id}")
216
- response = requests.get(
217
- f"{SPOTIFY_API_URL}/tracks/{track_id}",
218
- headers={"Authorization": f"Bearer {access_token}"},
219
- timeout=10
220
- )
221
-
222
- # Handle token expiration
223
- if response.status_code == 401:
224
- logger.info("Token expired, requesting new token")
225
- token_cache.token = None # Clear expired token
226
- access_token = get_spotify_token()
227
- # Retry the request with new token
228
- response = requests.get(
229
- f"{SPOTIFY_API_URL}/tracks/{track_id}",
230
- headers={"Authorization": f"Bearer {access_token}"},
231
- timeout=10
232
- )
233
-
234
- if response.status_code != 200:
235
- logger.error(f"Request {request_id} - Spotify API error: {response.text}")
236
- raise HTTPException(
237
- status_code=response.status_code,
238
- detail="Failed to fetch track information from Spotify"
239
- )
240
-
241
- track_data = response.json()
242
-
243
- # Get track metadata from Spotify
244
- spotify_response = track_data
245
-
246
- # Get download URL with version info
247
- download_url, api_version = await get_track_download_url(track_id)
248
-
249
- # Prepare response
250
- result = {
251
- "track_id": track_id,
252
- "name": spotify_response["name"],
253
- "album": spotify_response["album"]["name"],
254
- "artist": spotify_response["artists"][0]["name"],
255
- "release_date": spotify_response["album"]["release_date"],
256
- "duration_ms": spotify_response["duration_ms"],
257
- "url": download_url,
258
- "download_api_version": api_version
259
- }
260
-
261
- duration = time.time() - start_time
262
- logger.info(f"Request {request_id} completed successfully in {duration:.2f}s using API {api_version}")
263
- return result
264
-
265
- except HTTPException:
266
- raise
267
  except Exception as e:
268
- logger.error(f"Request {request_id} failed with error: {str(e)}")
269
- raise HTTPException(status_code=500, detail="Internal server error")
 
 
 
 
270
 
271
- # ... (rest of the code remains the same)
272
  @app.get("/")
273
  async def health_check():
274
  """Health check endpoint."""
@@ -282,4 +324,4 @@ async def health_check():
282
  }
283
  except Exception as e:
284
  logger.error(f"Health check failed: {str(e)}")
285
- return {"status": "unhealthy", "error": str(e)}
 
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(
 
26
  logger = logging.getLogger(__name__)
27
 
28
  app = FastAPI(title="Spotify Track API",
29
+ description="API for retrieving Spotify track information and download URLs")
30
 
31
  # Constants
32
  SPOTIFY_API_URL = "https://api.spotify.com/v1"
 
39
  def __init__(self):
40
  self.token: Optional[str] = None
41
  self.expiry_time: Optional[datetime] = None
42
+
43
  def set_token(self, token: str):
44
  self.token = token
45
  self.expiry_time = datetime.now() + timedelta(seconds=TOKEN_EXPIRY)
46
+
47
  def get_token(self) -> Optional[str]:
48
  if not self.token or not self.expiry_time or datetime.now() >= self.expiry_time:
49
  return None
50
  return self.token
51
+
52
  def is_expired(self) -> bool:
53
  return not self.token or not self.expiry_time or datetime.now() >= self.expiry_time
54
 
55
+
56
  token_cache = TokenCache()
57
 
58
  # Custom exception for Spotify API errors
59
  class SpotifyAPIError(Exception):
60
  pass
61
 
62
+
63
  def get_spotify_token() -> str:
64
  """
65
  Get Spotify access token with expiration handling.
 
74
 
75
  logger.info("Requesting new Spotify access token")
76
  start_time = time.time()
77
+
78
  if not SPOTIFY_CLIENT_ID or not SPOTIFY_CLIENT_SECRET:
79
  raise SpotifyAPIError("Spotify credentials not configured")
80
+
81
  auth_string = f"{SPOTIFY_CLIENT_ID}:{SPOTIFY_CLIENT_SECRET}"
82
  auth_bytes = base64.b64encode(auth_string.encode()).decode()
83
+
84
  auth_response = requests.post(
85
  'https://accounts.spotify.com/api/token',
86
  data={'grant_type': 'client_credentials'},
87
  headers={'Authorization': f'Basic {auth_bytes}'},
88
  timeout=10
89
  )
90
+
91
  if auth_response.status_code != 200:
92
  raise SpotifyAPIError(f"Failed to get token: {auth_response.text}")
93
+
94
  new_token = auth_response.json()['access_token']
95
  token_cache.set_token(new_token)
96
+
97
  logger.info(f"New token obtained successfully in {time.time() - start_time:.2f}s")
98
  return new_token
99
+
100
  except requests.exceptions.RequestException as e:
101
  logger.error(f"Network error during token request: {str(e)}")
102
  raise HTTPException(status_code=503, detail="Spotify authentication service unavailable")
 
104
  logger.error(f"Unexpected error during token request: {str(e)}")
105
  raise HTTPException(status_code=500, detail="Internal server error")
106
 
107
+ def extract_album_id(album_url: str) -> str:
108
+ """Extract album ID from Spotify URL."""
109
+ try:
110
+ return album_url.split("/")[-1].split("?")[0]
111
+ except Exception as e:
112
+ logger.error(f"Failed to extract album ID from URL {album_url}: {str(e)}")
113
+ raise HTTPException(status_code=400, detail="Invalid Spotify album URL format")
114
+
115
+
116
+ @app.post("/album")
117
+ async def get_album_data(request: Request):
118
+ try:
119
+ # Get the JSON data from the request
120
+ data = await request.json()
121
+ album_url = data.get('album_url')
122
+ if not album_url:
123
+ raise HTTPException(status_code=400, detail="Missing 'album_url' in JSON data")
124
+
125
+ # Extract the album ID from the URL
126
+ album_id = extract_album_id(album_url)
127
+
128
+ # Get the Spotify access token
129
+ access_token = get_spotify_token()
130
+
131
+ # Make a request to the Spotify API to get album data
132
+ headers = {
133
+ 'Authorization': f'Bearer {access_token}'
134
+ }
135
+ album_api_url = f"{SPOTIFY_API_URL}/albums/{album_id}"
136
+ response = requests.get(album_api_url, headers=headers, timeout=10)
137
+
138
+ if response.status_code != 200:
139
+ raise SpotifyAPIError(f"Failed to get album data: {response.text}")
140
+
141
+ album_data = response.json()
142
+ return album_data
143
+
144
+ except SpotifyAPIError as e:
145
+ logger.error(f"Spotify API error: {str(e)}")
146
+ raise HTTPException(status_code=500, detail=str(e))
147
+ except Exception as e:
148
+ logger.error(f"Unexpected error: {str(e)}")
149
+ raise HTTPException(status_code=500, detail="Internal server error")
150
+
151
+
152
+ def extract_playlist_id(playlist_url: str) -> str:
153
+ """Extract playlist ID from Spotify URL."""
154
+ try:
155
+ return playlist_url.split("/")[-1].split("?")[0]
156
+
157
+ except Exception as e:
158
+ logger.error(f"Failed to extract playlist ID from URL {playlist_url}: {str(e)}")
159
+ raise HTTPException(status_code=400, detail="Invalid Spotify playlist URL format")
160
+
161
+
162
+ @app.post("/playlist")
163
+ async def get_playlist_data(request: Request):
164
+ try:
165
+ # Get the JSON data from the request
166
+ data = await request.json()
167
+ playlist_url = data.get('playlist_url')
168
+ if not playlist_url:
169
+ raise HTTPException(status_code=400, detail="Missing 'playlist_url' in JSON data")
170
+
171
+ # Extract the playlist ID from the URL
172
+ playlist_id = extract_playlist_id(playlist_url)
173
+ logger.info(f"Extracted playlist ID: {playlist_id}")
174
+
175
+ # Get the Spotify access token
176
+ access_token = get_spotify_token()
177
+
178
+ # Make a request to the Spotify API to get playlist data
179
+ headers = {
180
+ 'Authorization': f'Bearer {access_token}'
181
+ }
182
+ playlist_api_url = f"{SPOTIFY_API_URL}/playlists/{playlist_id}/tracks"
183
+ response = requests.get(playlist_api_url, headers=headers, timeout=10)
184
+
185
+ if response.status_code != 200:
186
+ raise SpotifyAPIError(f"Failed to get playlist data: {response.text}")
187
+
188
+ playlist_data = response.json()
189
+ return playlist_data
190
+
191
+ except SpotifyAPIError as e:
192
+ logger.error(f"Spotify API error: {str(e)}")
193
+ raise HTTPException(status_code=500, detail=str(e))
194
+ except Exception as e:
195
+ logger.error(f"Unexpected error: {str(e)}")
196
+ raise HTTPException(status_code=500, detail="Internal server error")
197
+
198
+
199
  def extract_track_id(track_url: str) -> str:
200
  """Extract track ID from Spotify URL."""
201
  try:
 
204
  logger.error(f"Failed to extract track ID from URL {track_url}: {str(e)}")
205
  raise HTTPException(status_code=400, detail="Invalid Spotify URL format")
206
 
207
+
208
+
209
+
210
+
211
+
212
+ def get_cookie():
213
+ headers = {
214
+ 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.0.0 Safari/537.36'
215
+ }
216
+
217
+ try:
218
+ session = requests.Session()
219
+ response = session.get('https://spotisongdownloader.to/', headers=headers)
220
+ response.raise_for_status()
221
+ cookies = session.cookies.get_dict()
222
+ return f"PHPSESSID={cookies['PHPSESSID']}; quality=m4a"
223
+
224
+ except requests.exceptions.RequestException as e:
225
+ print(f"Error getting cookie: {e}")
226
+ return None
227
+
228
+
229
+ def get_data(track_id):
230
+ link = f"https://open.spotify.com/track/{track_id}"
231
+ try:
232
+ response = requests.get(
233
+ 'https://spotisongdownloader.to/api/composer/spotify/xsingle_track.php',
234
+ params={'url': link}
235
+ )
236
+ return response.json()
237
+
238
+ except Exception as error:
239
+ print(f'Error getting track data: {error}')
240
+ return None
241
+
242
+
243
+ def get_url(track_data, cookie):
244
+ url = 'https://spotisongdownloader.to/api/composer/spotify/wertyuht3456.php'
245
+
246
+ payload = {
247
+ 'song_name': track_data['song_name'],
248
+ 'artist_name': track_data['artist'],
249
+ 'url': track_data['url']
250
+ }
251
+
252
+ headers = {
253
+ 'Accept': 'application/json, text/javascript, */*; q=0.01',
254
+ 'Cookie': cookie,
255
+ 'Origin': 'https://spotisongdownloader.to',
256
+ 'Referer': 'https://spotisongdownloader.to/track.php',
257
+ 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36'
258
+ }
259
+
260
+ try:
261
+ response = requests.post(url, data=payload, headers=headers)
262
+ response.raise_for_status()
263
+ download_data = response.json()
264
+
265
+ encoded_link = urllib.parse.quote(download_data['dlink'], safe=':/?=')
266
+ return encoded_link
267
+
268
+ except requests.RequestException as e:
269
+ print(f'Error getting download URL: {e}')
270
+ return None
271
+
 
 
 
 
 
 
 
 
 
 
 
 
272
 
273
  @app.get("/get-track")
274
  async def get_track(
 
277
  track_url: Optional[str] = None
278
  ):
279
  """Get track information and download URL."""
280
+ logger.info(f"Request started - Track ID: {track_id}, URL: {track_url}")
 
281
 
 
282
 
283
  try:
284
  # Input validation
 
288
  # Extract track ID from URL if provided
289
  if track_url:
290
  track_id = extract_track_id(track_url)
291
+ logger.info(f'track_id: {track_id}')
292
+ cookie = get_cookie()
293
+ if not cookie:
294
+ return JSONResponse(content={"error": "Failed to get session cookie"}, status_code=500)
295
+
296
+ track_data = get_data(track_id)
297
+ logger.info(track_data)
298
+ if not track_data:
299
+ return JSONResponse(content={"error": "Failed to get track data"}, status_code=404)
300
+
301
+ download_link = get_url(track_data, cookie)
302
+ if not download_link:
303
+ return JSONResponse(content={"error": "Failed to get download URL"}, status_code=500)
304
+
305
+ return JSONResponse(content={"url": download_link})
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
306
  except Exception as e:
307
+ # 可以根据具体情况记录日志或者返回不同的错误信息
308
+ return JSONResponse(content={"error": str(e)}, status_code=500)
309
+
310
+
311
+
312
+
313
 
 
314
  @app.get("/")
315
  async def health_check():
316
  """Health check endpoint."""
 
324
  }
325
  except Exception as e:
326
  logger.error(f"Health check failed: {str(e)}")
327
+ return {"status": "unhealthy", "error": str(e)}