M-hv1 commited on
Commit
7cf3e3b
ยท
verified ยท
1 Parent(s): b30c6a3

Update main.py

Browse files
Files changed (1) hide show
  1. main.py +150 -125
main.py CHANGED
@@ -84,7 +84,7 @@ cache = TTLCache(CACHE_TTL, MAX_CACHE_ITEMS)
84
  queue = Queue(MAX_CONCURRENT)
85
 
86
  # โ”€โ”€โ”€ yt-dlp helpers โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
87
- YDL_OPTS = {
88
  "quiet": True,
89
  "no_warnings": True,
90
  "skip_download": True,
@@ -102,187 +102,215 @@ def _size(b) -> str:
102
  b /= 1024
103
  return f"{b:.1f} GB"
104
 
105
- def _formats(fmts: list, webpage_url: str = "") -> list[dict]:
106
  """
107
- ุจู†ุงุก ู‚ุงุฆู…ุฉ ุฌูˆุฏุงุช ู†ุธูŠูุฉ ูˆู…ุฑุชุจุฉ.
108
- ู†ุฏู…ุฌ ุงู„ููŠุฏูŠูˆ ูˆุงู„ุตูˆุช ูŠุฏูˆูŠุงู‹ ู„ูˆ ู…ููŠุด ู…ุฏู…ุฌ.
109
  """
110
- result, seen = [], set()
 
111
 
112
- # ูุตู„ ุงู„ููˆุฑู…ุงุช
113
- video_only = [] # ููŠุฏูŠูˆ ุจุฏูˆู† ุตูˆุช
114
- audio_only = [] # ุตูˆุช ูู‚ุท
115
- merged = [] # ููŠุฏูŠูˆ + ุตูˆุช ู…ุฏู…ุฌ
116
 
117
  for f in fmts:
118
  vcodec = f.get("vcodec", "none")
119
  acodec = f.get("acodec", "none")
 
120
 
 
 
 
121
  is_video = vcodec not in ("none", None, "")
122
  is_audio = acodec not in ("none", None, "")
123
 
124
  if is_video and is_audio:
125
- merged.append(f)
 
126
  elif is_video:
127
  video_only.append(f)
128
  elif is_audio:
129
  audio_only.append(f)
130
 
131
- # โ”€โ”€โ”€ ุฃูˆู„ุงู‹: ู†ุถูŠู ุงู„ู€ merged (ุงู„ุฃูุถู„) โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
132
- for f in merged:
133
- url = f.get("url", "")
134
- if not url:
135
- continue
136
-
137
  height = f.get("height", 0) or 0
138
- ext = f.get("ext", "mp4")
139
- fps = f.get("fps", 0) or 0
140
- size_b = f.get("filesize") or f.get("filesize_approx")
141
- fmt_id = f.get("format_id", "")
142
 
143
- key = f"{height}_{ext}_merged"
144
- if key in seen:
145
  continue
146
- seen.add(key)
 
 
 
147
 
148
  lbl = f"{height}p" if height else "Video"
149
  if fps > 30:
150
  lbl += f" {int(fps)}fps"
151
 
152
  result.append({
153
- "id": fmt_id,
154
- "label": lbl,
155
- "type": "๐ŸŽฌ ููŠุฏูŠูˆ+ุตูˆุช",
156
- "ext": ext,
157
- "height": height,
158
- "has_audio": True,
159
- "is_merged": True,
160
- "size": _size(size_b),
161
- "size_bytes": size_b or 0,
162
- "url": url,
163
- "audio_url": None, # ู…ุฏู…ุฌุŒ ู…ููŠุด ุตูˆุช ู…ู†ูุตู„
 
164
  })
165
 
166
- # โ”€โ”€โ”€ ุซุงู†ูŠุงู‹: ู†ุฏู…ุฌ ููŠุฏูŠูˆ + ุตูˆุช ูŠุฏูˆูŠุงู‹ โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
167
- # ู†ุงุฎุฏ ุฃุญุณู† ุตูˆุช (ุฃุนู„ู‰ bitrate) ู†ุฏู…ุฌู‡ ู…ุน ูƒู„ ููŠุฏูŠูˆ
168
- if audio_only and video_only:
169
  best_audio = max(audio_only,
170
- key=lambda x: x.get("abr", 0) or x.get("asr", 0) or 0)
171
- audio_url = best_audio.get("url", "")
172
- audio_ext = best_audio.get("ext", "m4a")
 
173
 
174
  for f in video_only:
175
- url = f.get("url", "")
176
- if not url:
177
- continue
178
-
179
  height = f.get("height", 0) or 0
180
- ext = f.get("ext", "mp4")
181
- fps = f.get("fps", 0) or 0
182
- size_b = f.get("filesize") or f.get("filesize_approx") or 0
183
- fmt_id = f.get("format_id", "")
184
 
185
- key = f"{height}_{ext}_manual"
186
- if key in seen:
187
  continue
188
- seen.add(key)
189
 
190
- # ู†ุญุณุจ ุงู„ุญุฌู… ุงู„ุชู‚ุฑูŠุจูŠ ุงู„ู…ุฏู…ุฌ
191
- audio_size = best_audio.get("filesize") or best_audio.get("filesize_approx") or 0
192
- total_size = size_b + (audio_size * 0.8) # ุชู‚ุฏูŠุฑ: ุงู„ุตูˆุช ูŠุถูŠู ~80% ู…ู† ุญุฌู…ู‡
 
193
 
194
  lbl = f"{height}p" if height else "Video"
195
  if fps > 30:
196
  lbl += f" {int(fps)}fps"
197
 
198
  result.append({
199
- "id": f"{fmt_id}+{best_audio.get('format_id', '')}",
200
- "label": lbl,
201
- "type": "๐ŸŽฌ ููŠุฏูŠูˆ+ุตูˆุช",
202
- "ext": ext,
203
- "height": height,
204
- "has_audio": True,
205
- "is_merged": False, # ู‡ู†ุฏู…ุฌ ููŠ ุงู„ูˆุงุฌู‡ุฉ
206
- "size": _size(total_size),
207
- "size_bytes": total_size,
208
- "url": url, # ุฑุงุจุท ุงู„ููŠุฏูŠูˆ
209
- "audio_url": audio_url, # ุฑุงุจุท ุงู„ุตูˆุช
210
- "audio_ext": audio_ext,
 
211
  })
212
 
213
- # โ”€โ”€โ”€ ุซุงู„ุซุงู‹: ู†ุถูŠู ุงู„ุตูˆุช ุงู„ู…ู†ูุตู„ โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
 
 
 
214
  for f in audio_only:
215
  url = f.get("url", "")
216
  if not url:
217
  continue
218
 
 
 
 
 
 
219
  abr = f.get("abr", 0) or 0
220
  ext = f.get("ext", "m4a")
221
  size_b = f.get("filesize") or f.get("filesize_approx")
222
- fmt_id = f.get("format_id", "")
223
-
224
- lbl = f"{int(abr)}kbps" if abr else "Audio"
225
 
226
  result.append({
227
- "id": fmt_id,
228
- "label": lbl,
229
- "type": "๐ŸŽต ุตูˆุช ูู‚ุท",
230
- "ext": ext,
231
- "height": 0,
232
- "has_audio": True,
233
- "is_audio": True,
234
- "size": _size(size_b),
235
- "size_bytes": size_b or 0,
236
- "url": url,
237
- "audio_url": None,
 
238
  })
239
 
240
- # ุชุฑุชูŠุจ: ู…ุฏู…ุฌ ุฃูˆู„ุงู‹ โ† ุฃุนู„ู‰ ุฌูˆุฏุฉ
241
- result.sort(key=lambda x: (
242
- not x.get("is_merged", False), # ุงู„ู…ุฏู…ุฌ ุงู„ุญู‚ูŠู‚ูŠ ุฃูˆู„ุงู‹
243
- not x.get("has_audio", False), # ุงู„ู„ูŠ ู…ุนุงู‡ ุตูˆุช
244
- -x["height"], # ุฃุนู„ู‰ ุฌูˆุฏุฉ
245
- x.get("is_audio", False) # ุงู„ุตูˆุช ูู‚ุท ุขุฎุฑ
246
- ))
247
-
248
  return result
249
 
250
  def _extract_info(url: str) -> dict:
251
- """ุชุดุบูŠู„ yt-dlp (blocking โ€” ูŠูุดุบูŽู‘ู„ ููŠ thread executor)"""
252
 
253
- opts = YDL_OPTS.copy()
254
 
255
- # ู„ู„ู€ Facebook/Instagram: ู†ุฌูŠุจ ูƒู„ ุงู„ููˆุฑู…ุงุช ุนุดุงู† ู†ุฏู…ุฌ ูŠุฏูˆูŠุงู‹
256
- if any(x in url.lower() for x in ["facebook", "instagram", "fb.watch"]):
257
- opts["format"] = "all" # ู†ุฌูŠุจ ูƒู„ ุญุงุฌุฉ
 
 
 
 
 
 
 
 
 
 
258
 
259
  with yt_dlp.YoutubeDL(opts) as ydl:
260
  info = ydl.extract_info(url, download=False)
261
  if not info:
262
  raise ValueError("ู„ู… ูŠูุนุซุฑ ุนู„ู‰ ู…ุนู„ูˆู…ุงุช")
263
-
264
- # ุตูˆุฑุฉ ู…ุตุบุฑุฉ ุฃูุถู„ ุฌูˆุฏุฉ
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
265
  thumbs = info.get("thumbnails") or []
266
- thumb = (
267
  sorted(thumbs, key=lambda t: t.get("width", 0), reverse=True)[0]["url"]
268
  if thumbs else info.get("thumbnail", "")
269
  )
270
-
271
- fmts = _formats(info.get("formats", []), info.get("webpage_url", url))
 
272
  if not fmts:
273
  raise ValueError("ู„ุง ุชูˆุฌุฏ ุตูŠุบ ู‚ุงุจู„ุฉ ู„ู„ุชุญู…ูŠู„")
274
-
275
  return {
276
- "id": info.get("id",""),
277
- "title": info.get("title","ููŠุฏูŠูˆ"),
278
- "thumbnail": thumb,
279
- "duration": info.get("duration"),
280
- "duration_str": info.get("duration_string",""),
281
- "uploader": info.get("uploader","") or info.get("channel",""),
282
- "platform": info.get("extractor_key",""),
283
- "webpage_url": info.get("webpage_url", url),
284
- "view_count": info.get("view_count"),
285
- "formats": fmts,
286
  }
287
 
288
  # โ”€โ”€โ”€ FastAPI โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
@@ -314,31 +342,28 @@ class ExtractReq(BaseModel):
314
  @app.get("/health")
315
  async def health():
316
  return {
317
- "status": "online",
318
  "version": "1.0.0",
319
- "queue": queue.active,
320
- "max": MAX_CONCURRENT,
321
- "cache": await cache.stats(),
322
  }
323
 
324
  @app.post("/extract")
325
  @app.post("/extract/")
326
  async def extract(req: ExtractReq):
327
  url = req.url.strip()
328
- if not url.startswith(("http://","https://")):
329
  raise HTTPException(400, "ุฑุงุจุท ุบูŠุฑ ุตุงู„ุญ")
330
 
331
- # ุชุฃุฎูŠุฑ ุจุณูŠุท ู„ู„ู€ Instagram/Facebook ุนุดุงู† Rate Limit
332
  if any(x in url.lower() for x in ["instagram", "facebook", "fb.watch"]):
333
  await asyncio.sleep(0.5)
334
 
335
- # ูƒุงุด
336
- key = hashlib.sha256(url.encode()).hexdigest()
337
  cached = await cache.get(key)
338
  if cached:
339
  return {"success": True, "cached": True, "data": cached}
340
 
341
- # ุงุณุชุฎุฑุงุฌ
342
  async with queue.slot():
343
  log.info(f"๐Ÿ” [{queue.active}/{MAX_CONCURRENT}] {url[:80]}")
344
  loop = asyncio.get_event_loop()
@@ -351,17 +376,17 @@ async def extract(req: ExtractReq):
351
  raise HTTPException(408, "ุงู†ุชู‡ู‰ ูˆู‚ุช ุงู„ุงุณุชุฎุฑุงุฌ โ€” ุฌุฑู‘ุจ ู…ุฑุฉ ุฃุฎุฑู‰")
352
  except yt_dlp.utils.DownloadError as e:
353
  msg = str(e)
354
- if "Private video" in msg:
355
  raise HTTPException(403, "ุงู„ููŠุฏูŠูˆ ุฎุงุต")
356
- if "not available" in msg and "rate-limit" in msg.lower():
357
  raise HTTPException(429, "ุชู… ุชุฌุงูˆุฒ ุงู„ุญุฏ ุงู„ู…ุณู…ูˆุญ โ€” ุฌุฑู‘ุจ ุจุนุฏ ุดูˆูŠุฉ")
358
- if "not available" in msg:
359
  raise HTTPException(404, "ุงู„ููŠุฏูŠูˆ ุบูŠุฑ ู…ุชุงุญ ููŠ ู…ู†ุทู‚ุชูƒ")
360
- if "login required" in msg.lower():
361
  raise HTTPException(401, "ูŠุชุทู„ุจ ุชุณุฌูŠู„ ุฏุฎูˆู„")
362
- if "copyright" in msg.lower():
363
  raise HTTPException(451, "ู…ุญุฌูˆุจ ุจุณุจุจ ุญู‚ูˆู‚ ุงู„ู†ุดุฑ")
364
- if "Unable to extract" in msg:
365
  raise HTTPException(422, "ุงู„ู…ู†ุตุฉ ุญุธุฑุช ุงู„ุงุณุชุฎุฑุงุฌ โ€” ุฌุฑู‘ุจ ุฑุงุจุท ุชุงู†ูŠ")
366
  raise HTTPException(422, f"ู„ุง ูŠู…ูƒู† ุงุณุชุฎุฑุงุฌ ุงู„ุฑุงุจุท: {msg[:150]}")
367
  except ValueError as e:
 
84
  queue = Queue(MAX_CONCURRENT)
85
 
86
  # โ”€โ”€โ”€ yt-dlp helpers โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
87
+ YDL_OPTS_BASE = {
88
  "quiet": True,
89
  "no_warnings": True,
90
  "skip_download": True,
 
102
  b /= 1024
103
  return f"{b:.1f} GB"
104
 
105
+ def _formats(fmts: list) -> list[dict]:
106
  """
107
+ ู†ุฌูŠุจ ุงู„ููˆุฑู…ุงุช ุงู„ู…ุฏู…ุฌุฉ (ููŠุฏูŠูˆ+ุตูˆุช) ุฃูˆู„ุงู‹.
108
+ ู„ูˆ ู…ููŠุดุŒ ู†ุฏู…ุฌ ูŠุฏูˆูŠุงู‹.
109
  """
110
+ result = []
111
+ seen_heights = set()
112
 
113
+ # โ”€โ”€โ”€ ุงู„ุฎุทูˆุฉ 1: ู†ูุตู„ ุงู„ููˆุฑู…ุงุช โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
114
+ merged_formats = [] # ููŠุฏูŠูˆ+ุตูˆุช ู…ุฏู…ุฌ (ุงู„ุฃูุถู„)
115
+ video_only = [] # ููŠุฏูŠูˆ ุจุฏูˆู† ุตูˆุช
116
+ audio_only = [] # ุตูˆุช ูู‚ุท
117
 
118
  for f in fmts:
119
  vcodec = f.get("vcodec", "none")
120
  acodec = f.get("acodec", "none")
121
+ url = f.get("url", "")
122
 
123
+ if not url:
124
+ continue
125
+
126
  is_video = vcodec not in ("none", None, "")
127
  is_audio = acodec not in ("none", None, "")
128
 
129
  if is_video and is_audio:
130
+ # ู…ุฏู…ุฌ! ุฏู‡ ุงู„ู„ูŠ ุนุงูŠุฒูŠู†ู‡
131
+ merged_formats.append(f)
132
  elif is_video:
133
  video_only.append(f)
134
  elif is_audio:
135
  audio_only.append(f)
136
 
137
+ # โ”€โ”€โ”€ ุงู„ุฎุทูˆุฉ 2: ู†ุถูŠู ุงู„ู…ุฏู…ุฌ (ุงู„ุฃูุถู„) โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
138
+ # ู†ุฑุชุจู‡ู…: ุฃุนู„ู‰ ุฌูˆุฏุฉ ุฃูˆู„ุงู‹
139
+ merged_formats.sort(key=lambda x: x.get("height", 0) or 0, reverse=True)
140
+
141
+ for f in merged_formats:
 
142
  height = f.get("height", 0) or 0
143
+ ext = f.get("ext", "mp4")
 
 
 
144
 
145
+ # dedup ุจุงู„ู€ height
146
+ if height in seen_heights:
147
  continue
148
+ seen_heights.add(height)
149
+
150
+ fps = f.get("fps", 0) or 0
151
+ size_b = f.get("filesize") or f.get("filesize_approx")
152
 
153
  lbl = f"{height}p" if height else "Video"
154
  if fps > 30:
155
  lbl += f" {int(fps)}fps"
156
 
157
  result.append({
158
+ "id": f.get("format_id", ""),
159
+ "label": lbl,
160
+ "type": "๐ŸŽฌ ููŠุฏูŠูˆ+ุตูˆุช",
161
+ "ext": ext,
162
+ "height": height,
163
+ "has_audio": True,
164
+ "is_merged": True, # โ† ู…ู‡ู…: ุฏู‡ ู…ุฏู…ุฌ ุฃุตู„ุงู‹
165
+ "is_real_merged": True,
166
+ "size": _size(size_b),
167
+ "size_bytes": size_b or 0,
168
+ "url": f["url"],
169
+ "audio_url": None,
170
  })
171
 
172
+ # โ”€โ”€โ”€ ุงู„ุฎุทูˆุฉ 3: ู„ูˆ ู…ููŠุด ู…ุฏู…ุฌุŒ ู†ุฏู…ุฌ ูŠุฏูˆูŠุงู‹ โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
173
+ if not result and video_only and audio_only:
174
+ # ุฃุญุณู† ุตูˆุช
175
  best_audio = max(audio_only,
176
+ key=lambda x: (x.get("abr", 0) or 0, x.get("asr", 0) or 0))
177
+
178
+ # ู†ุงุฎุฏ ููŠุฏูŠูˆู‡ุงุช ู…ุฎุชู„ูุฉ ุงู„ุฌูˆุฏุงุช
179
+ video_only.sort(key=lambda x: x.get("height", 0) or 0, reverse=True)
180
 
181
  for f in video_only:
 
 
 
 
182
  height = f.get("height", 0) or 0
183
+ if height in seen_heights:
184
+ continue
185
+ seen_heights.add(height)
 
186
 
187
+ # ู†ุชุฎุทู‰ ุงู„ุฌูˆุฏุงุช ุงู„ุนุงู„ูŠุฉ ุฌุฏุงู‹ (4K) ุนุดุงู† ู…ููŠุด ุตูˆุช ู„ูŠู‡ุง ุบุงู„ุจุงู‹
188
+ if height > 1080 and len([x for x in result if x["height"] >= 720]) > 0:
189
  continue
 
190
 
191
+ ext = f.get("ext", "mp4")
192
+ fps = f.get("fps", 0) or 0
193
+ v_size = f.get("filesize") or f.get("filesize_approx") or 0
194
+ a_size = best_audio.get("filesize") or best_audio.get("filesize_approx") or 0
195
 
196
  lbl = f"{height}p" if height else "Video"
197
  if fps > 30:
198
  lbl += f" {int(fps)}fps"
199
 
200
  result.append({
201
+ "id": f"{f.get('format_id')}+{best_audio.get('format_id')}",
202
+ "label": lbl,
203
+ "type": "๐ŸŽฌ ููŠุฏูŠูˆ+ุตูˆุช",
204
+ "ext": ext,
205
+ "height": height,
206
+ "has_audio": True,
207
+ "is_merged": False, # โ† ู‡ู†ุฏู…ุฌ ูŠุฏูˆูŠุงู‹
208
+ "is_real_merged": False,
209
+ "size": _size(v_size + a_size),
210
+ "size_bytes": v_size + a_size,
211
+ "url": f["url"],
212
+ "audio_url": best_audio["url"],
213
+ "audio_ext": best_audio.get("ext", "m4a"),
214
  })
215
 
216
+ # โ”€โ”€โ”€ ุงู„ุฎุทูˆุฉ 4: ู†ุถูŠู ุงู„ุตูˆุช ุงู„ู…ู†ูุตู„ โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
217
+ audio_only.sort(key=lambda x: x.get("abr", 0) or 0, reverse=True)
218
+ best_audio_added = False
219
+
220
  for f in audio_only:
221
  url = f.get("url", "")
222
  if not url:
223
  continue
224
 
225
+ # ุฃุญุณู† ุตูˆุช ุจุณ
226
+ if best_audio_added:
227
+ continue
228
+ best_audio_added = True
229
+
230
  abr = f.get("abr", 0) or 0
231
  ext = f.get("ext", "m4a")
232
  size_b = f.get("filesize") or f.get("filesize_approx")
 
 
 
233
 
234
  result.append({
235
+ "id": f.get("format_id", ""),
236
+ "label": f"{int(abr)}kbps" if abr else "Audio",
237
+ "type": "๐ŸŽต ุตูˆุช ูู‚ุท",
238
+ "ext": ext,
239
+ "height": 0,
240
+ "has_audio": True,
241
+ "is_audio": True,
242
+ "is_merged": False,
243
+ "size": _size(size_b),
244
+ "size_bytes": size_b or 0,
245
+ "url": url,
246
+ "audio_url": None,
247
  })
248
 
 
 
 
 
 
 
 
 
249
  return result
250
 
251
  def _extract_info(url: str) -> dict:
252
+ """ุชุดุบูŠู„ yt-dlp"""
253
 
254
+ opts = YDL_OPTS_BASE.copy()
255
 
256
+ # โ”€โ”€โ”€ ู…ู‡ู…: ู†ุฌูŠุจ ุงู„ููˆุฑู…ุงุช ุงู„ู…ุฏู…ุฌุฉ ุฃูˆู„ุงู‹ โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
257
+ # ู†ุฌุฑุจ format specs ู…ุฎุชู„ูุฉ ุญุณุจ ุงู„ู…ู†ุตุฉ
258
+
259
+ if "youtube" in url.lower() or "youtu.be" in url.lower():
260
+ # YouTube: ู†ูุถู„ ุงู„ู…ุฏู…ุฌ
261
+ opts["format"] = "bestvideo[height<=1080][ext=mp4]+bestaudio[ext=m4a]/best[height<=1080][ext=mp4]/best"
262
+ elif "facebook" in url.lower() or "fb.watch" in url.lower():
263
+ # Facebook: ู†ุฌูŠุจ ูƒู„ ุญุงุฌุฉ
264
+ opts["format"] = "best"
265
+ elif "instagram" in url.lower():
266
+ opts["format"] = "best"
267
+ else:
268
+ opts["format"] = "best"
269
 
270
  with yt_dlp.YoutubeDL(opts) as ydl:
271
  info = ydl.extract_info(url, download=False)
272
  if not info:
273
  raise ValueError("ู„ู… ูŠูุนุซุฑ ุนู„ู‰ ู…ุนู„ูˆู…ุงุช")
274
+
275
+ # โ”€โ”€โ”€ ู…ู‡ู…: ู†ุฌูŠุจ ุงู„ู€ format ุงู„ู…ุฎุชุงุฑ ู…ู† yt-dlp โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
276
+ # ุฏู‡ ุจูŠูƒูˆู† ุฃุญุณู† ููŠุฏูŠูˆ+ุตูˆุช ู…ุฏู…ุฌ
277
+ requested_formats = info.get("requested_formats", [])
278
+ format_id = info.get("format_id", "")
279
+
280
+ # ู„ูˆ requested_formats ู…ูˆุฌูˆุฏุŒ ู†ุถูŠูู‡ู… ู„ู„ู€ formats
281
+ all_formats = info.get("formats", [])
282
+
283
+ # ู†ุถูŠู ุนู„ุงู…ุฉ ุนู„ู‰ ุงู„ู€ format ุงู„ู…ุฎุชุงุฑ
284
+ for f in all_formats:
285
+ if f.get("format_id") == format_id:
286
+ f["_selected"] = True
287
+ # ู„ูˆ ู‡ูˆ ุฌุฒุก ู…ู† requested_formats
288
+ if any(rf.get("format_id") == f.get("format_id") for rf in requested_formats):
289
+ f["_requested"] = True
290
+
291
+ # ุตูˆุฑุฉ ู…ุตุบุฑุฉ
292
  thumbs = info.get("thumbnails") or []
293
+ thumb = (
294
  sorted(thumbs, key=lambda t: t.get("width", 0), reverse=True)[0]["url"]
295
  if thumbs else info.get("thumbnail", "")
296
  )
297
+
298
+ fmts = _formats(all_formats)
299
+
300
  if not fmts:
301
  raise ValueError("ู„ุง ุชูˆุฌุฏ ุตูŠุบ ู‚ุงุจู„ุฉ ู„ู„ุชุญู…ูŠู„")
302
+
303
  return {
304
+ "id": info.get("id", ""),
305
+ "title": info.get("title", "ููŠุฏูŠูˆ"),
306
+ "thumbnail": thumb,
307
+ "duration": info.get("duration"),
308
+ "duration_str": info.get("duration_string", ""),
309
+ "uploader": info.get("uploader", "") or info.get("channel", ""),
310
+ "platform": info.get("extractor_key", ""),
311
+ "webpage_url": info.get("webpage_url", url),
312
+ "view_count": info.get("view_count"),
313
+ "formats": fmts,
314
  }
315
 
316
  # โ”€โ”€โ”€ FastAPI โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
 
342
  @app.get("/health")
343
  async def health():
344
  return {
345
+ "status": "online",
346
  "version": "1.0.0",
347
+ "queue": queue.active,
348
+ "max": MAX_CONCURRENT,
349
+ "cache": await cache.stats(),
350
  }
351
 
352
  @app.post("/extract")
353
  @app.post("/extract/")
354
  async def extract(req: ExtractReq):
355
  url = req.url.strip()
356
+ if not url.startswith(("http://", "https://")):
357
  raise HTTPException(400, "ุฑุงุจุท ุบูŠุฑ ุตุงู„ุญ")
358
 
 
359
  if any(x in url.lower() for x in ["instagram", "facebook", "fb.watch"]):
360
  await asyncio.sleep(0.5)
361
 
362
+ key = hashlib.sha256(url.encode()).hexdigest()
 
363
  cached = await cache.get(key)
364
  if cached:
365
  return {"success": True, "cached": True, "data": cached}
366
 
 
367
  async with queue.slot():
368
  log.info(f"๐Ÿ” [{queue.active}/{MAX_CONCURRENT}] {url[:80]}")
369
  loop = asyncio.get_event_loop()
 
376
  raise HTTPException(408, "ุงู†ุชู‡ู‰ ูˆู‚ุช ุงู„ุงุณุชุฎุฑุงุฌ โ€” ุฌุฑู‘ุจ ู…ุฑุฉ ุฃุฎุฑู‰")
377
  except yt_dlp.utils.DownloadError as e:
378
  msg = str(e)
379
+ if "Private video" in msg:
380
  raise HTTPException(403, "ุงู„ููŠุฏูŠูˆ ุฎุงุต")
381
+ if "not available" in msg and "rate-limit" in msg.lower():
382
  raise HTTPException(429, "ุชู… ุชุฌุงูˆุฒ ุงู„ุญุฏ ุงู„ู…ุณู…ูˆุญ โ€” ุฌุฑู‘ุจ ุจุนุฏ ุดูˆูŠุฉ")
383
+ if "not available" in msg:
384
  raise HTTPException(404, "ุงู„ููŠุฏูŠูˆ ุบูŠุฑ ู…ุชุงุญ ููŠ ู…ู†ุทู‚ุชูƒ")
385
+ if "login required" in msg.lower():
386
  raise HTTPException(401, "ูŠุชุทู„ุจ ุชุณุฌูŠู„ ุฏุฎูˆู„")
387
+ if "copyright" in msg.lower():
388
  raise HTTPException(451, "ู…ุญุฌูˆุจ ุจุณุจุจ ุญู‚ูˆู‚ ุงู„ู†ุดุฑ")
389
+ if "Unable to extract" in msg:
390
  raise HTTPException(422, "ุงู„ู…ู†ุตุฉ ุญุธุฑุช ุงู„ุงุณุชุฎุฑุงุฌ โ€” ุฌุฑู‘ุจ ุฑุงุจุท ุชุงู†ูŠ")
391
  raise HTTPException(422, f"ู„ุง ูŠู…ูƒู† ุงุณุชุฎุฑุงุฌ ุงู„ุฑุงุจุท: {msg[:150]}")
392
  except ValueError as e: