Chrunos commited on
Commit
65eeb26
·
verified ·
1 Parent(s): b082976

Create app.py

Browse files
Files changed (1) hide show
  1. app.py +338 -0
app.py ADDED
@@ -0,0 +1,338 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import shutil
2
+ from fastapi import FastAPI, HTTPException, Request, Body
3
+ from deezspot.deezloader import DeeLogin
4
+ from deezspot.spotloader import SpoLogin
5
+ import requests
6
+ import os
7
+ import logging
8
+ import json
9
+ from typing import Optional
10
+ from fastapi.staticfiles import StaticFiles
11
+ from dotenv import load_dotenv
12
+ from pydantic import BaseModel, Field, HttpUrl
13
+ from urllib.parse import quote
14
+ from pathlib import Path
15
+ import uuid
16
+ from fastapi import BackgroundTasks
17
+ from collections import defaultdict
18
+ import time
19
+ from datetime import timedelta
20
+ import gc
21
+ from typing import Dict, Union, Literal
22
+ import urllib.parse
23
+ from fastapi.responses import JSONResponse
24
+
25
+ # Set up logging
26
+ logging.basicConfig(level=logging.INFO)
27
+ logger = logging.getLogger(__name__)
28
+
29
+ app = FastAPI(title="Deezer API")
30
+ # Load environment variables
31
+ load_dotenv()
32
+
33
+ # Mount a static files directory to serve downloaded files
34
+
35
+ os.makedirs("downloads", exist_ok=True)
36
+ app.mount("/downloads", StaticFiles(directory="downloads"), name="downloads")
37
+
38
+ # Deezer API base URL
39
+ DEEZER_API_URL = "https://api.deezer.com"
40
+
41
+ # Deezer ARL token (required for deezspot downloads)
42
+ ARL_TOKEN = os.getenv('ARL')
43
+
44
+
45
+ class DownloadRequest(BaseModel):
46
+ url: str
47
+ quality: str
48
+ arl: str
49
+
50
+
51
+ def convert_deezer_short_link_async(short_link: str) -> str:
52
+ try:
53
+ response = requests.get(short_link, allow_redirects=True)
54
+ return response.url
55
+ except requests.RequestException as e:
56
+ print(f"An error occurred: {e}")
57
+ return ""
58
+
59
+ @app.middleware("http")
60
+ async def log_errors_middleware(request: Request, call_next):
61
+ try:
62
+ return await call_next(request)
63
+ except Exception as e:
64
+ logger.error("An unhandled exception occurred!", exc_info=True)
65
+ return JSONResponse(status_code=500, content={'detail': 'Internal Server Error'})
66
+
67
+
68
+ @app.get("/")
69
+ def read_root():
70
+ return {"message": "running"}
71
+
72
+
73
+ # Helper function to get track info
74
+ def get_track_info(track_id: str):
75
+ try:
76
+ response = requests.get(f"{DEEZER_API_URL}/track/{track_id}")
77
+ if response.status_code != 200:
78
+ raise HTTPException(status_code=404, detail="Track not found")
79
+ return response.json()
80
+ except requests.exceptions.RequestException as e:
81
+ logger.error(f"Network error fetching track metadata: {e}")
82
+ raise HTTPException(status_code=500, detail=str(e))
83
+ except Exception as e:
84
+ logger.error(f"Error fetching track metadata: {e}")
85
+ raise HTTPException(status_code=500, detail=str(e))
86
+
87
+
88
+ # Fetch track metadata from Deezer API
89
+ @app.get("/track/{track_id}")
90
+ def get_track(track_id: str):
91
+ return get_track_info(track_id)
92
+
93
+
94
+ # Rate limiting dictionary
95
+ class RateLimiter:
96
+ def __init__(self, max_requests: int, time_window: timedelta):
97
+ self.max_requests = max_requests
98
+ self.time_window = time_window
99
+ self.requests: Dict[str, list] = defaultdict(list)
100
+
101
+ def _cleanup_old_requests(self, user_ip: str) -> None:
102
+ """Remove requests that are outside the time window."""
103
+ current_time = time.time()
104
+ self.requests[user_ip] = [
105
+ timestamp for timestamp in self.requests[user_ip]
106
+ if current_time - timestamp < self.time_window.total_seconds()
107
+ ]
108
+
109
+ def is_rate_limited(self, user_ip: str) -> bool:
110
+ """Check if the user has exceeded their rate limit."""
111
+ self._cleanup_old_requests(user_ip)
112
+
113
+ # Get current count after cleanup
114
+ current_count = len(self.requests[user_ip])
115
+
116
+ # Add current request timestamp (incrementing the count)
117
+ current_time = time.time()
118
+ self.requests[user_ip].append(current_time)
119
+
120
+ # Check if user has exceeded the maximum requests
121
+ return (current_count + 1) > self.max_requests
122
+
123
+ def get_current_count(self, user_ip: str) -> int:
124
+ """Get the current request count for an IP."""
125
+ self._cleanup_old_requests(user_ip)
126
+ return len(self.requests[user_ip])
127
+
128
+
129
+ # Initialize rate limiter with 25 requests per day
130
+ rate_limiter = RateLimiter(
131
+ max_requests=5,
132
+ time_window=timedelta(days=1)
133
+ )
134
+
135
+
136
+ def get_user_ip(request: Request) -> str:
137
+ """Helper function to get user's IP address."""
138
+ forwarded = request.headers.get("X-Forwarded-For")
139
+ if forwarded:
140
+ return forwarded.split(",")[0]
141
+ return request.client.host
142
+
143
+
144
+ # Download a track and return a download URL
145
+ @app.post("/download/track")
146
+ def download_track(request: Request, download_request: DownloadRequest):
147
+ try:
148
+ user_ip = get_user_ip(request)
149
+
150
+ if rate_limiter.is_rate_limited(user_ip):
151
+ current_count = rate_limiter.get_current_count(user_ip)
152
+ raise HTTPException(
153
+ status_code=429,
154
+ detail={
155
+ "detail": "You have exceeded the maximum number of requests per day. Please try again tomorrow or get the Premium version for unlimited downloads at https://chrunos.com/premium-shortcuts/.",
156
+ "help": "https://t.me/chrunoss"
157
+ }
158
+ )
159
+ if download_request.arl is None or download_request.arl.strip() == "":
160
+ ARL = ARL_TOKEN
161
+ else:
162
+ ARL = download_request.arl
163
+ logger.info(f'arl: {ARL}')
164
+ url = download_request.url
165
+ if 'dzr.page' in url or 'deezer.page' in url or 'link.deezer' in url:
166
+ url = convert_deezer_short_link_async(url)
167
+ quality = download_request.quality
168
+ dl = DeeLogin(arl=ARL)
169
+ logger.info(f'track_url: {url}')
170
+
171
+ if quality not in ["MP3_320", "MP3_128", "FLAC"]:
172
+ raise HTTPException(status_code=400, detail="Invalid quality specified")
173
+
174
+ # 提取 track_id (假设 url 格式为 https://api.deezer.com/track/{track_id})
175
+ track_id = url.split("/")[-1]
176
+
177
+ # Fetch track info
178
+ track_info = get_track_info(track_id)
179
+ track_link = track_info.get("link")
180
+ if not track_link:
181
+ raise HTTPException(status_code=404, detail="Track link not found")
182
+
183
+ # Sanitize filename
184
+ track_title = track_info.get("title", "track")
185
+ artist_name = track_info.get("artist", {}).get("name", "unknown")
186
+ file_extension = "flac" if quality == "FLAC" else "mp3"
187
+ expected_filename = f"{artist_name} - {track_title}.{file_extension}".replace("/", "_") # Sanitize filename
188
+
189
+ # Clear the downloads directory
190
+ for root, dirs, files in os.walk("downloads"):
191
+ for file in files:
192
+ os.remove(os.path.join(root, file))
193
+ for dir in dirs:
194
+ shutil.rmtree(os.path.join(root, dir))
195
+
196
+ # Download the track using deezspot
197
+ try:
198
+ # 下载文件的代码
199
+ dl.download_trackdee(
200
+ link_track=track_link,
201
+ output_dir="downloads",
202
+ quality_download=quality,
203
+ recursive_quality=False,
204
+ recursive_download=False
205
+ )
206
+ except Exception as e:
207
+ logger.error(f"Error downloading file: {e}")
208
+ raise HTTPException(status_code=500, detail="File download failed")
209
+
210
+ # Recursively search for the file in the downloads directory
211
+ filepath = None
212
+ for root, dirs, files in os.walk("downloads"):
213
+ for file in files:
214
+ if file.endswith(f'.{file_extension}'):
215
+ filepath = os.path.join(root, file)
216
+ break
217
+ if filepath:
218
+ break
219
+
220
+ if not filepath:
221
+ raise HTTPException(status_code=500, detail=f"{file_extension} file not found after download")
222
+ if filepath:
223
+ file_size = os.path.getsize(filepath)
224
+ logger.info(f"Downloaded file size: {file_size} bytes")
225
+
226
+ # Return the download URL
227
+ relative_path = quote(str(os.path.relpath(filepath, "downloads")))
228
+ # Remove spaces from the relative path
229
+ # Auto-detect base URL from request
230
+ base_url = str(request.base_url).rstrip('/')
231
+ download_url = f"{base_url}/downloads/{relative_path}"
232
+ logger.info(f"Download successful: {download_url}")
233
+ gc.collect()
234
+ return {"download_url": download_url, "requests_remaining": rate_limiter.max_requests - rate_limiter.get_current_count(user_ip)}
235
+ except Exception as e:
236
+ logger.error(f"Error downloading track: {e}")
237
+ raise HTTPException(status_code=500, detail=str(e))
238
+
239
+
240
+
241
+ # Pydantic model for album request
242
+ class AlbumRequest(BaseModel):
243
+ id: str
244
+
245
+
246
+ # Fetch album data
247
+ @app.post("/z_album")
248
+ def fetch_album(request: AlbumRequest):
249
+ album_id = request.id
250
+ try:
251
+ response = requests.get(f"{DEEZER_API_URL}/album/{album_id}")
252
+ response.raise_for_status()
253
+ album_data = response.json()
254
+ tracks = album_data.get("tracks", {}).get("data", [])
255
+ result = []
256
+ for track in tracks:
257
+ title = track.get("title")
258
+ link = track.get("link")
259
+ if title and link:
260
+ result.append({
261
+ "title": title,
262
+ "link": link
263
+ })
264
+ return result
265
+ except requests.exceptions.RequestException as e:
266
+ logger.error(f"Network error fetching album: {e}")
267
+ raise HTTPException(status_code=500, detail=str(e))
268
+ except Exception as e:
269
+ logger.error(f"Error fetching album: {e}")
270
+ raise HTTPException(status_code=500, detail=str(e))
271
+
272
+
273
+ # Pydantic model for playlist request
274
+ class PlaylistRequest(BaseModel):
275
+ id: str
276
+
277
+
278
+ # Fetch playlist data
279
+ @app.post("/z_playlist")
280
+ def fetch_playlist(request: PlaylistRequest):
281
+ playlist_id = request.id
282
+ try:
283
+ response = requests.get(f"{DEEZER_API_URL}/playlist/{playlist_id}")
284
+ response.raise_for_status()
285
+ playlist_data = response.json()
286
+ tracks = playlist_data.get("tracks", {}).get("data", [])
287
+ result = []
288
+ for track in tracks:
289
+ title = track.get("title")
290
+ link = track.get("link")
291
+ if title and link:
292
+ result.append({
293
+ "title": title,
294
+ "link": link
295
+ })
296
+ return result
297
+ except requests.exceptions.RequestException as e:
298
+ logger.error(f"Network error fetching album: {e}")
299
+ raise HTTPException(status_code=500, detail=str(e))
300
+ except Exception as e:
301
+ logger.error(f"Error fetching album: {e}")
302
+ raise HTTPException(status_code=500, detail=str(e))
303
+
304
+
305
+
306
+ # Search tracks using Deezer API
307
+ @app.get("/z_search")
308
+ def search_tracks(query: str, limit: Optional[int] = 10):
309
+ try:
310
+ response = requests.get(f"{DEEZER_API_URL}/search", params={"q": query, "limit": limit})
311
+ return response.json()
312
+ except requests.exceptions.RequestException as e:
313
+ logger.error(f"Network error searching tracks: {e}")
314
+ raise HTTPException(status_code=500, detail=str(e))
315
+ except Exception as e:
316
+ logger.error(f"Error searching tracks: {e}")
317
+ raise HTTPException(status_code=500, detail=str(e))
318
+
319
+
320
+ # --- Request Body Model ---
321
+ # Defines the expected structure and validation for the request body.
322
+ class SpotDlRequest(BaseModel):
323
+ url: HttpUrl = Field(..., description="The URL to be processed.")
324
+ quality: Literal["128", "320", "FLAC"] = Field(
325
+ ...,
326
+ description="The desired quality. Currently, only '128' is supported for link generation."
327
+ )
328
+
329
+ # --- Response Body Model ---
330
+ # Defines the structure for a successful response.
331
+ class SpotDlResponse(BaseModel):
332
+ download_url: str
333
+
334
+ # --- Error Response Model ---
335
+ # Defines the structure for an error response.
336
+ class ErrorResponse(BaseModel):
337
+ detail: str
338
+