Fred808 commited on
Commit
5a2b03e
·
verified ·
1 Parent(s): 0cc8a49

Upload main.py

Browse files
Files changed (1) hide show
  1. main.py +188 -761
main.py CHANGED
@@ -1,7 +1,7 @@
1
  #!/usr/bin/env python3
2
  """
3
- Batch-Enhanced FastAPI YouTube Video Downloader
4
- Supports both single and batch downloads with cookie support
5
  """
6
 
7
  import os
@@ -13,15 +13,13 @@ import random
13
  import time
14
  import asyncio
15
  import logging
16
- import uuid
17
  from pathlib import Path
18
  from typing import Optional, Dict, Any, List
19
  from datetime import datetime
20
  from concurrent.futures import ThreadPoolExecutor
21
- import io
22
 
23
- from fastapi import FastAPI, HTTPException, BackgroundTasks, Request, UploadFile, File, Form
24
- from fastapi.responses import FileResponse, HTMLResponse, StreamingResponse
25
  from fastapi.middleware.cors import CORSMiddleware
26
  from pydantic import BaseModel, HttpUrl
27
  import uvicorn
@@ -33,24 +31,11 @@ logger = logging.getLogger(__name__)
33
  # Pydantic models
34
  class VideoInfoRequest(BaseModel):
35
  url: HttpUrl
36
- use_cookies: Optional[bool] = None
37
-
38
- class BatchVideoInfoRequest(BaseModel):
39
- urls: List[HttpUrl]
40
- use_cookies: Optional[bool] = None
41
 
42
  class DownloadRequest(BaseModel):
43
  url: HttpUrl
44
  quality: str = "best"
45
  audio_only: bool = False
46
- use_cookies: Optional[bool] = None
47
-
48
- class BatchDownloadRequest(BaseModel):
49
- urls: List[HttpUrl]
50
- quality: str = "best"
51
- audio_only: bool = False
52
- use_cookies: Optional[bool] = None
53
- max_concurrent: int = 2 # Limit concurrent downloads
54
 
55
  class VideoInfo(BaseModel):
56
  title: str
@@ -64,12 +49,6 @@ class VideoInfo(BaseModel):
64
  thumbnail: str
65
  webpage_url: str
66
 
67
- class BatchVideoInfo(BaseModel):
68
- url: str
69
- success: bool
70
- info: Optional[VideoInfo] = None
71
- error: Optional[str] = None
72
-
73
  class DownloadResponse(BaseModel):
74
  success: bool
75
  message: str
@@ -78,38 +57,17 @@ class DownloadResponse(BaseModel):
78
  video_info: Optional[VideoInfo] = None
79
  download_path: Optional[str] = None
80
 
81
- class BatchDownloadResponse(BaseModel):
82
- batch_id: str
83
- total_urls: int
84
- status: str
85
- completed: int
86
- failed: int
87
- results: List[Dict[str, Any]]
88
-
89
- class BatchStatus(BaseModel):
90
- batch_id: str
91
- status: str
92
- total_urls: int
93
- completed: int
94
- failed: int
95
- in_progress: int
96
- results: List[Dict[str, Any]]
97
- created_at: str
98
- updated_at: str
99
-
100
  class HealthResponse(BaseModel):
101
  status: str
102
  yt_dlp_available: bool
103
  timestamp: str
104
- cookie_file_exists: bool
105
  strategies_enabled: List[str]
106
- batch_support: bool
107
 
108
  # Initialize FastAPI app
109
  app = FastAPI(
110
- title="Batch YouTube Video Downloader",
111
- description="Download YouTube videos individually or in batches with cookie support",
112
- version="4.0.0",
113
  docs_url="/docs",
114
  redoc_url="/redoc"
115
  )
@@ -124,94 +82,88 @@ app.add_middleware(
124
  )
125
 
126
  # Thread pool for background tasks
127
- executor = ThreadPoolExecutor(max_workers=4)
128
-
129
- # Global batch status storage (in production, use Redis or database)
130
- batch_status_store = {}
131
 
132
- class CookieManager:
133
- """Manages cookie files and validation"""
134
 
135
- def __init__(self, cookie_dir: str = None):
136
- if cookie_dir is None:
137
- if os.path.exists('/data'):
138
- cookie_dir = '/data/cookies'
139
- else:
140
- cookie_dir = '/tmp/cookies'
 
 
 
 
 
141
 
142
- self.cookie_dir = Path(cookie_dir)
143
- self.cookie_dir.mkdir(parents=True, exist_ok=True)
144
- self.cookie_file = self.cookie_dir / "youtube_cookies.txt"
 
145
 
146
- def save_cookies(self, cookie_content: str) -> bool:
147
- """Save cookie content to file with validation"""
148
- try:
149
- lines = cookie_content.strip().split('\n')
150
- data_lines = [line for line in lines if line.strip() and not line.startswith('#')]
151
-
152
- if not data_lines:
153
- logger.error("Cookie file appears to be empty or contains only comments")
154
- return False
155
-
156
- has_youtube = any('youtube.com' in line for line in data_lines)
157
- if not has_youtube:
158
- logger.warning("Cookie file doesn't contain youtube.com entries")
159
-
160
- with open(self.cookie_file, 'w', encoding='utf-8') as f:
161
- f.write(cookie_content)
162
-
163
- logger.info(f"Cookies saved to {self.cookie_file}")
164
- return True
165
-
166
- except Exception as e:
167
- logger.error(f"Failed to save cookies: {e}")
168
- return False
169
 
170
- def validate_cookies(self) -> Dict[str, Any]:
171
- """Validate existing cookie file"""
172
- if not self.cookie_file.exists():
173
- return {"valid": False, "reason": "Cookie file does not exist"}
174
 
175
- try:
176
- with open(self.cookie_file, 'r', encoding='utf-8') as f:
177
- content = f.read()
178
-
179
- lines = content.strip().split('\n')
180
- data_lines = [line for line in lines if line.strip() and not line.startswith('#')]
181
-
182
- if not data_lines:
183
- return {"valid": False, "reason": "Cookie file is empty or contains only comments"}
184
-
185
- youtube_cookies = [line for line in data_lines if 'youtube.com' in line]
186
- essential_cookies = ['VISITOR_INFO1_LIVE', 'YSC', 'CONSENT']
187
- found_essential = []
188
-
189
- for line in data_lines:
190
- for cookie in essential_cookies:
191
- if cookie in line:
192
- found_essential.append(cookie)
193
-
194
- return {
195
- "valid": True,
196
- "total_lines": len(data_lines),
197
- "youtube_cookies": len(youtube_cookies),
198
- "essential_cookies": found_essential,
199
- "file_size": self.cookie_file.stat().st_size,
200
- "last_modified": datetime.fromtimestamp(self.cookie_file.stat().st_mtime).isoformat()
201
- }
202
-
203
- except Exception as e:
204
- return {"valid": False, "reason": f"Error reading cookie file: {e}"}
205
 
206
- def get_cookie_path(self) -> Optional[str]:
207
- """Get path to cookie file if it exists and is valid"""
208
- validation = self.validate_cookies()
209
- if validation["valid"]:
210
- return str(self.cookie_file)
211
- return None
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
212
 
213
- class BatchYouTubeDownloader:
214
- """Enhanced YouTube downloader with batch support and cookie handling"""
215
 
216
  def __init__(self, download_dir: str = None):
217
  if download_dir is None:
@@ -222,21 +174,16 @@ class BatchYouTubeDownloader:
222
 
223
  self.download_dir = Path(download_dir)
224
  self.download_dir.mkdir(parents=True, exist_ok=True)
225
- self.cookie_manager = CookieManager()
226
-
227
- self.user_agents = [
228
- 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
229
- 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
230
- 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
231
- 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:121.0) Gecko/20100101 Firefox/121.0',
232
- ]
233
 
 
234
  self._ensure_ytdlp_available()
235
 
236
  def _ensure_ytdlp_available(self):
237
  """Ensure yt-dlp is available, install if necessary"""
238
  try:
239
- subprocess.run(['yt-dlp', '--version'], capture_output=True, check=True)
 
240
  logger.info("yt-dlp is available")
241
  except (subprocess.CalledProcessError, FileNotFoundError):
242
  logger.info("Installing yt-dlp...")
@@ -248,52 +195,28 @@ class BatchYouTubeDownloader:
248
  logger.error(f"Failed to install yt-dlp: {e}")
249
  raise RuntimeError("Could not install yt-dlp")
250
 
251
- def _build_command(self, base_cmd: List[str], use_cookies: bool = False) -> List[str]:
252
- """Build yt-dlp command with proper options"""
253
- cmd = base_cmd.copy()
254
-
255
- cmd.extend(['--user-agent', random.choice(self.user_agents)])
256
-
257
- if use_cookies:
258
- cookie_path = self.cookie_manager.get_cookie_path()
259
- if cookie_path:
260
- cmd.extend(['--cookies', cookie_path])
261
- logger.info("Using cookie file for authentication")
262
- else:
263
- logger.warning("Cookies requested but no valid cookie file found")
264
- cmd.extend(['--no-cookies'])
265
- else:
266
- cmd.extend(['--no-cookies'])
267
-
268
- cmd.extend([
269
- '--sleep-interval', str(random.randint(1, 3)),
270
- '--retries', '5',
271
- '--fragment-retries', '5',
272
- '--socket-timeout', '30',
273
- '--no-check-certificates',
274
- '--geo-bypass',
275
- '--add-header', 'Accept:text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
276
- '--add-header', 'Accept-Language:en-US,en;q=0.5',
277
- '--add-header', 'Accept-Encoding:gzip, deflate',
278
- '--add-header', 'DNT:1',
279
- '--add-header', 'Connection:keep-alive',
280
- '--add-header', 'Upgrade-Insecure-Requests:1',
281
- ])
282
-
283
- return cmd
284
-
285
- def get_video_info(self, url: str, use_cookies: Optional[bool] = None, retry_count: int = 0) -> Optional[Dict[str, Any]]:
286
- """Get video information with cookie support"""
287
  max_retries = 3
288
 
289
- # Determine actual use_cookies value
290
- actual_use_cookies = use_cookies if use_cookies is not None else self.cookie_manager.get_cookie_path() is not None
 
 
291
 
292
  try:
293
- base_cmd = ["yt-dlp", "--dump-json", "--no-download", str(url)]
294
- cmd = self._build_command(base_cmd, actual_use_cookies)
 
 
 
 
 
 
 
 
295
 
296
- logger.info(f"Getting video info (attempt {retry_count + 1}, cookies: {actual_use_cookies})")
297
 
298
  result = subprocess.run(cmd, capture_output=True, text=True, check=True, timeout=60)
299
  video_info = json.loads(result.stdout)
@@ -314,22 +237,26 @@ class BatchYouTubeDownloader:
314
  except subprocess.CalledProcessError as e:
315
  error_msg = e.stderr.lower() if e.stderr else ""
316
 
 
317
  if "429" in error_msg or "too many requests" in error_msg:
318
  if retry_count < max_retries:
319
- wait_time = (retry_count + 1) * 30
320
- logger.warning(f"Rate limited, waiting {wait_time}s before retry {retry_count + 1}")
321
  time.sleep(wait_time)
322
- return self.get_video_info(url, use_cookies, retry_count + 1)
 
 
 
323
 
324
  elif "sign in" in error_msg or "bot" in error_msg:
325
- if not use_cookies and retry_count == 0:
326
- logger.warning("Bot detection triggered, retrying with cookies if available")
327
- return self.get_video_info(url, True, retry_count + 1)
328
- elif retry_count < max_retries:
329
- wait_time = (retry_count + 1) * 60
330
- logger.warning(f"Bot detection, waiting {wait_time}s before retry {retry_count + 1}")
331
  time.sleep(wait_time)
332
- return self.get_video_info(url, use_cookies, retry_count + 1)
 
 
 
333
 
334
  logger.error(f"Failed to get video info: {e.stderr}")
335
  return None
@@ -338,43 +265,49 @@ class BatchYouTubeDownloader:
338
  logger.error(f"Error processing video info: {e}")
339
  if retry_count < max_retries:
340
  time.sleep(10)
341
- return self.get_video_info(url, use_cookies, retry_count + 1)
342
  return None
343
 
344
  def download_video(self, url: str, quality: str = "best",
345
- audio_only: bool = False, use_cookies: Optional[bool] = None,
346
- retry_count: int = 0) -> Optional[str]:
347
- """Download video with cookie support"""
348
  max_retries = 2
349
 
350
- # Determine actual use_cookies value
351
- actual_use_cookies = use_cookies if use_cookies is not None else self.cookie_manager.get_cookie_path() is not None
 
 
352
 
353
  try:
354
- base_cmd = ["yt-dlp"]
355
 
356
- output_template = str(self.download_dir / "%(title)s_%(id)s.%(ext)s")
357
- base_cmd.extend(["-o", output_template])
 
358
 
 
359
  if audio_only:
360
- base_cmd.extend(["-f", "bestaudio/best"])
361
  else:
362
  if quality == "best":
363
- base_cmd.extend(["-f", "best[height<=720]"])
364
  elif quality == "worst":
365
- base_cmd.extend(["-f", "worst"])
366
  else:
367
- base_cmd.extend(["-f", quality])
 
 
368
 
369
- base_cmd.append(str(url))
370
- cmd = self._build_command(base_cmd, actual_use_cookies)
371
 
372
- logger.info(f"Downloading video (attempt {retry_count + 1}, cookies: {actual_use_cookies})")
373
 
374
  result = subprocess.run(cmd, capture_output=True, text=True, check=True, timeout=300)
375
 
376
  logger.info("Download completed successfully")
377
 
 
378
  downloaded_files = [f for f in self.download_dir.glob("*") if f.is_file()]
379
  if downloaded_files:
380
  latest_file = max(downloaded_files, key=os.path.getctime)
@@ -387,15 +320,11 @@ class BatchYouTubeDownloader:
387
 
388
  if ("429" in error_msg or "too many requests" in error_msg or
389
  "sign in" in error_msg or "bot" in error_msg):
390
-
391
- if not use_cookies and retry_count == 0:
392
- logger.warning("Download blocked, retrying with cookies if available")
393
- return self.download_video(url, quality, audio_only, True, retry_count + 1)
394
- elif retry_count < max_retries:
395
  wait_time = (retry_count + 1) * 60
396
  logger.warning(f"Download blocked, waiting {wait_time}s before retry {retry_count + 1}")
397
  time.sleep(wait_time)
398
- return self.download_video(url, quality, audio_only, use_cookies, retry_count + 1)
399
 
400
  logger.error(f"Download failed: {e.stderr}")
401
  return None
@@ -403,196 +332,22 @@ class BatchYouTubeDownloader:
403
  except subprocess.TimeoutExpired:
404
  logger.error("Download timeout")
405
  if retry_count < max_retries:
406
- return self.download_video(url, quality, audio_only, use_cookies, retry_count + 1)
407
  return None
408
-
409
- async def batch_get_info(self, urls: List[str], use_cookies: bool = False) -> List[BatchVideoInfo]:
410
- """Get info for multiple videos concurrently"""
411
- results = []
412
-
413
- async def get_single_info(url: str) -> BatchVideoInfo:
414
- try:
415
- loop = asyncio.get_event_loop()
416
- info = await loop.run_in_executor(executor, self.get_video_info, url, use_cookies)
417
-
418
- if info:
419
- return BatchVideoInfo(
420
- url=url,
421
- success=True,
422
- info=VideoInfo(**info)
423
- )
424
- else:
425
- return BatchVideoInfo(
426
- url=url,
427
- success=False,
428
- error="Failed to get video information"
429
- )
430
- except Exception as e:
431
- return BatchVideoInfo(
432
- url=url,
433
- success=False,
434
- error=str(e)
435
- )
436
-
437
- # Process URLs concurrently with a limit
438
- semaphore = asyncio.Semaphore(3) # Limit concurrent requests
439
-
440
- async def limited_get_info(url: str) -> BatchVideoInfo:
441
- async with semaphore:
442
- return await get_single_info(url)
443
-
444
- tasks = [limited_get_info(url) for url in urls]
445
- results = await asyncio.gather(*tasks)
446
-
447
- return results
448
-
449
- async def batch_download(self, batch_id: str, urls: List[str], quality: str = "best",
450
- audio_only: bool = False, use_cookies: bool = False,
451
- max_concurrent: int = 2) -> None:
452
- """Download multiple videos with progress tracking"""
453
-
454
- # Initialize batch status
455
- batch_status_store[batch_id] = {
456
- "batch_id": batch_id,
457
- "status": "in_progress",
458
- "total_urls": len(urls),
459
- "completed": 0,
460
- "failed": 0,
461
- "in_progress": 0,
462
- "results": [],
463
- "created_at": datetime.now().isoformat(),
464
- "updated_at": datetime.now().isoformat()
465
- }
466
-
467
- semaphore = asyncio.Semaphore(max_concurrent)
468
-
469
- async def download_single(url: str) -> Dict[str, Any]:
470
- async with semaphore:
471
- batch_status_store[batch_id]["in_progress"] += 1
472
- batch_status_store[batch_id]["updated_at"] = datetime.now().isoformat()
473
-
474
- try:
475
- loop = asyncio.get_event_loop()
476
-
477
- # Get video info first
478
- info = await loop.run_in_executor(
479
- executor, self.get_video_info, url, use_cookies
480
- )
481
-
482
- if not info:
483
- result = {
484
- "url": url,
485
- "success": False,
486
- "error": "Failed to get video information",
487
- "completed_at": datetime.now().isoformat()
488
- }
489
- batch_status_store[batch_id]["failed"] += 1
490
- batch_status_store[batch_id]["in_progress"] -= 1
491
- batch_status_store[batch_id]["results"].append(result)
492
- batch_status_store[batch_id]["updated_at"] = datetime.now().isoformat()
493
- return result
494
-
495
- # Download the video
496
- downloaded_file = await loop.run_in_executor(
497
- executor, self.download_video, url, quality, audio_only, use_cookies
498
- )
499
-
500
- if downloaded_file:
501
- file_size = os.path.getsize(downloaded_file)
502
- filename = os.path.basename(downloaded_file)
503
-
504
- result = {
505
- "url": url,
506
- "success": True,
507
- "filename": filename,
508
- "file_size": file_size,
509
- "video_info": info,
510
- "download_path": downloaded_file,
511
- "completed_at": datetime.now().isoformat()
512
- }
513
- batch_status_store[batch_id]["completed"] += 1
514
- else:
515
- result = {
516
- "url": url,
517
- "success": False,
518
- "error": "Failed to download video",
519
- "completed_at": datetime.now().isoformat()
520
- }
521
- batch_status_store[batch_id]["failed"] += 1
522
-
523
- batch_status_store[batch_id]["in_progress"] -= 1
524
- batch_status_store[batch_id]["results"].append(result)
525
- batch_status_store[batch_id]["updated_at"] = datetime.now().isoformat()
526
-
527
- return result
528
-
529
- except Exception as e:
530
- result = {
531
- "url": url,
532
- "success": False,
533
- "error": str(e),
534
- "completed_at": datetime.now().isoformat()
535
- }
536
- batch_status_store[batch_id]["failed"] += 1
537
- batch_status_store[batch_id]["in_progress"] -= 1
538
- batch_status_store[batch_id]["results"].append(result)
539
- batch_status_store[batch_id]["updated_at"] = datetime.now().isoformat()
540
- return result
541
-
542
- # Process all downloads
543
- tasks = [download_single(url) for url in urls]
544
- await asyncio.gather(*tasks)
545
-
546
- # Mark batch as completed
547
- batch_status_store[batch_id]["status"] = "completed"
548
- batch_status_store[batch_id]["updated_at"] = datetime.now().isoformat()
549
 
550
  # Global downloader instance
551
- downloader = BatchYouTubeDownloader()
552
-
553
- def generate_multipart_response(file_paths: List[str], boundary: str = None):
554
- """Generate a multipart response with multiple files"""
555
- if boundary is None:
556
- boundary = f"----formdata-{uuid.uuid4().hex}"
557
-
558
- def file_generator():
559
- for file_path in file_paths:
560
- if os.path.exists(file_path):
561
- filename = os.path.basename(file_path)
562
- file_size = os.path.getsize(file_path)
563
-
564
- # Write multipart boundary and headers
565
- yield f"--{boundary}\r\n".encode()
566
- yield f"Content-Disposition: form-data; name=\"file\"; filename=\"{filename}\"\r\n".encode()
567
- yield f"Content-Type: application/octet-stream\r\n".encode()
568
- yield f"Content-Length: {file_size}\r\n".encode()
569
- yield "\r\n".encode()
570
-
571
- # Stream file content
572
- with open(file_path, 'rb') as f:
573
- while True:
574
- chunk = f.read(8192)
575
- if not chunk:
576
- break
577
- yield chunk
578
-
579
- yield "\r\n".encode()
580
-
581
- # Final boundary
582
- yield f"--{boundary}--\r\n".encode()
583
-
584
- return file_generator(), boundary
585
 
586
  @app.get("/", response_class=HTMLResponse)
587
  async def read_root():
588
- """Serve the main HTML interface with batch support and cookie upload"""
589
  html_content = """
590
  <!DOCTYPE html>
591
  <html lang="en">
592
  <head>
593
  <meta charset="UTF-8">
594
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
595
- <title>Batch YouTube Video Downloader</title>
596
  <style>
597
  body {
598
  font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
@@ -609,7 +364,7 @@ async def read_root():
609
  border-radius: 15px;
610
  box-shadow: 0 20px 40px rgba(0,0,0,0.1);
611
  padding: 40px;
612
- max-width: 1000px;
613
  width: 100%;
614
  }
615
  .header {
@@ -621,18 +376,22 @@ async def read_root():
621
  margin-bottom: 10px;
622
  font-size: 2.5em;
623
  }
624
- .section {
 
 
 
 
625
  background: #f8f9fa;
626
  border-radius: 8px;
627
  padding: 20px;
628
  margin: 20px 0;
629
  }
630
- .upload-area {
631
- border: 2px dashed #dee2e6;
632
- border-radius: 8px;
633
- padding: 20px;
634
- text-align: center;
635
- margin: 15px 0;
636
  }
637
  .btn {
638
  background: linear-gradient(135deg, #667eea, #764ba2);
@@ -644,22 +403,10 @@ async def read_root():
644
  display: inline-block;
645
  margin: 10px 5px;
646
  transition: transform 0.2s ease;
647
- cursor: pointer;
648
  }
649
  .btn:hover {
650
  transform: translateY(-2px);
651
  }
652
- .feature {
653
- background: white;
654
- border: 1px solid #dee2e6;
655
- border-radius: 5px;
656
- padding: 15px;
657
- margin: 10px 0;
658
- }
659
- .new-feature {
660
- background: #e8f5e8;
661
- border: 1px solid #4caf50;
662
- }
663
  .warning {
664
  background: #fff3cd;
665
  border: 1px solid #ffeaa7;
@@ -668,197 +415,57 @@ async def read_root():
668
  margin: 20px 0;
669
  color: #856404;
670
  }
671
- .success {
672
- background: #d4edda;
673
- border: 1px solid #c3e6cb;
674
- border-radius: 5px;
675
- padding: 15px;
676
- margin: 20px 0;
677
- color: #155724;
678
- }
679
- input[type="file"] {
680
- margin: 10px 0;
681
- }
682
  </style>
683
  </head>
684
  <body>
685
  <div class="container">
686
  <div class="header">
687
- <h1>📦 Batch YouTube Video Downloader</h1>
688
- <p>Download single videos or process multiple URLs in batches with cookie support</p>
689
  </div>
690
 
691
  <div class="warning">
692
- <strong>⚠️ Cookie Issues?</strong> If your cookies aren't working, they might be:
693
- <ul>
694
- <li>Expired (YouTube cookies expire frequently)</li>
695
- <li>Wrong format (must be Netscape format)</li>
696
- <li>Missing essential cookies</li>
697
- <li>From a different IP/location</li>
698
- </ul>
699
  </div>
700
 
701
- <div class="section">
702
- <h3>📤 Upload Cookie File</h3>
703
- <div class="upload-area">
704
- <form id="cookieForm" enctype="multipart/form-data">
705
- <p>Select your YouTube cookies.txt file (Netscape format):</p>
706
- <input type="file" id="cookieFile" name="cookie_file" accept=".txt" required>
707
- <br>
708
- <button type="submit" class="btn">Upload Cookies</button>
709
- </form>
710
- </div>
711
- <div id="uploadResult"></div>
712
- </div>
713
-
714
- <div class="section">
715
- <h3>🚀 Features</h3>
716
 
717
- <div class="feature new-feature">
718
- <strong>🆕 Batch Downloads:</strong> Process multiple YouTube URLs simultaneously
719
- </div>
720
-
721
- <div class="feature new-feature">
722
- <strong>🆕 Progress Tracking:</strong> Monitor batch download progress in real-time
723
  </div>
724
 
725
- <div class="feature new-feature">
726
- <strong>🆕 Bulk File Download:</strong> Download all files from a batch in one request
727
  </div>
728
 
729
  <div class="feature">
730
- <strong>🍪 Cookie Support:</strong> Upload cookies for better success rates
731
  </div>
732
 
733
  <div class="feature">
734
- <strong>🔄 Smart Retry Logic:</strong> Automatic retries with exponential backoff
735
  </div>
736
 
737
  <div class="feature">
738
- <strong>⚡ Concurrent Processing:</strong> Configurable concurrent download limits
739
  </div>
740
  </div>
741
 
742
- <div class="section">
743
- <h3>📋 API Endpoints</h3>
744
-
745
- <h4>Cookie Management:</h4>
746
- <ul>
747
- <li><code>POST /upload-cookies</code> - Upload cookie file</li>
748
- <li><code>GET /cookie-status</code> - Check cookie status</li>
749
- </ul>
750
-
751
- <h4>Single Video Operations:</h4>
752
- <ul>
753
- <li><code>POST /video/info</code> - Get single video information</li>
754
- <li><code>POST /video/download</code> - Download single video</li>
755
- </ul>
756
-
757
- <h4>Batch Operations:</h4>
758
- <ul>
759
- <li><code>POST /batch/info</code> - Get info for multiple videos</li>
760
- <li><code>POST /batch/download</code> - Start batch download</li>
761
- <li><code>GET /batch/status/{batch_id}</code> - Check batch progress</li>
762
- <li><code>GET /batch/download-all/{batch_id}</code> - Download all files from batch</li>
763
- <li><code>GET /download-all</code> - Download ALL files from server</li>
764
- </ul>
765
- </div>
766
-
767
- <div class="section">
768
- <h3>🔍 How to Export Cookies</h3>
769
- <ol>
770
- <li>Install a cookie export extension (like "Get cookies.txt LOCALLY")</li>
771
- <li>Go to YouTube and make sure you're logged in</li>
772
- <li>Use the extension to export cookies in Netscape format</li>
773
- <li>Save the file and upload it here</li>
774
- </ol>
775
- </div>
776
-
777
  <div style="text-align: center;">
778
- <a href="/docs" class="btn">📖 Interactive API Docs</a>
779
  <a href="/health" class="btn">🏥 Health Check</a>
780
- <a href="/cookie-status" class="btn">🍪 Cookie Status</a>
781
  </div>
782
  </div>
783
-
784
- <script>
785
- document.getElementById('cookieForm').addEventListener('submit', async function(e) {
786
- e.preventDefault();
787
-
788
- const formData = new FormData();
789
- const fileInput = document.getElementById('cookieFile');
790
- formData.append('cookie_file', fileInput.files[0]);
791
-
792
- const resultDiv = document.getElementById('uploadResult');
793
- resultDiv.innerHTML = '<p>Uploading...</p>';
794
-
795
- try {
796
- const response = await fetch('/upload-cookies', {
797
- method: 'POST',
798
- body: formData
799
- });
800
-
801
- const result = await response.json();
802
-
803
- if (result.success) {
804
- resultDiv.innerHTML = '<div class="success"><strong>✅ Success!</strong> ' + result.message + '</div>';
805
- } else {
806
- resultDiv.innerHTML = '<div class="warning"><strong>❌ Error:</strong> ' + result.message + '</div>';
807
- }
808
- } catch (error) {
809
- resultDiv.innerHTML = '<div class="warning"><strong>❌ Error:</strong> Failed to upload cookies</div>';
810
- }
811
- });
812
- </script>
813
  </body>
814
  </html>
815
  """
816
  return HTMLResponse(content=html_content)
817
 
818
- @app.post("/upload-cookies")
819
- async def upload_cookies(cookie_file: UploadFile = File(...)):
820
- """Upload and validate cookie file"""
821
- try:
822
- if not cookie_file.filename.endswith('.txt'):
823
- raise HTTPException(status_code=400, detail="Cookie file must be a .txt file")
824
-
825
- content = await cookie_file.read()
826
- cookie_content = content.decode('utf-8')
827
-
828
- success = downloader.cookie_manager.save_cookies(cookie_content)
829
-
830
- if success:
831
- validation = downloader.cookie_manager.validate_cookies()
832
- return {
833
- "success": True,
834
- "message": f"Cookies uploaded successfully. Found {validation.get('youtube_cookies', 0)} YouTube cookies.",
835
- "validation": validation
836
- }
837
- else:
838
- return {
839
- "success": False,
840
- "message": "Failed to save cookie file. Please check the format."
841
- }
842
-
843
- except Exception as e:
844
- logger.error(f"Error uploading cookies: {e}")
845
- return {
846
- "success": False,
847
- "message": f"Error processing cookie file: {str(e)}"
848
- }
849
-
850
- @app.get("/cookie-status")
851
- async def cookie_status():
852
- """Get current cookie status"""
853
- validation = downloader.cookie_manager.validate_cookies()
854
- return {
855
- "cookie_file_exists": downloader.cookie_manager.cookie_file.exists(),
856
- "validation": validation
857
- }
858
-
859
  @app.get("/health", response_model=HealthResponse)
860
  async def health_check():
861
- """Enhanced health check with batch support information"""
862
  try:
863
  subprocess.run(['yt-dlp', '--version'], capture_output=True, check=True)
864
  yt_dlp_available = True
@@ -866,48 +473,39 @@ async def health_check():
866
  yt_dlp_available = False
867
 
868
  strategies = [
869
- "Batch Processing",
870
- "Cookie Support",
871
  "User-Agent Rotation",
 
872
  "Smart Retry Logic",
873
  "Enhanced Headers",
874
- "Concurrent Downloads",
875
- "Progress Tracking",
876
- "Bulk File Download"
877
  ]
878
 
879
  return HealthResponse(
880
  status="healthy" if yt_dlp_available else "unhealthy",
881
  yt_dlp_available=yt_dlp_available,
882
  timestamp=datetime.now().isoformat(),
883
- cookie_file_exists=downloader.cookie_manager.cookie_file.exists(),
884
- strategies_enabled=strategies,
885
- batch_support=True
886
  )
887
 
888
- # Single video endpoints
889
  @app.post("/video/info", response_model=Dict[str, Any])
890
  async def get_video_info(request: VideoInfoRequest):
891
- """Get video information with cookie support"""
892
  try:
893
  url_str = str(request.url)
894
  if not any(domain in url_str for domain in ['youtube.com', 'youtu.be']):
895
  raise HTTPException(status_code=400, detail="Invalid YouTube URL")
896
 
 
897
  loop = asyncio.get_event_loop()
898
- info = await loop.run_in_executor(
899
- executor,
900
- downloader.get_video_info,
901
- url_str,
902
- request.use_cookies
903
- )
904
 
905
  if info:
906
  return {"success": True, "info": info}
907
  else:
908
  raise HTTPException(
909
  status_code=503,
910
- detail="Failed to get video information. Try uploading fresh cookies or wait before retrying."
911
  )
912
 
913
  except HTTPException:
@@ -918,43 +516,40 @@ async def get_video_info(request: VideoInfoRequest):
918
 
919
  @app.post("/video/download", response_model=DownloadResponse)
920
  async def download_video(request: DownloadRequest, background_tasks: BackgroundTasks):
921
- """Download video with cookie support"""
922
  try:
923
  url_str = str(request.url)
924
  if not any(domain in url_str for domain in ['youtube.com', 'youtu.be']):
925
  raise HTTPException(status_code=400, detail="Invalid YouTube URL")
926
 
 
927
  loop = asyncio.get_event_loop()
928
- info = await loop.run_in_executor(
929
- executor,
930
- downloader.get_video_info,
931
- url_str,
932
- request.use_cookies
933
- )
934
  if not info:
935
  raise HTTPException(
936
  status_code=503,
937
- detail="Failed to get video information. Try uploading fresh cookies."
938
  )
939
 
 
940
  downloaded_file = await loop.run_in_executor(
941
  executor,
942
  downloader.download_video,
943
  url_str,
944
  request.quality,
945
- request.audio_only,
946
- request.use_cookies
947
  )
948
 
949
  if downloaded_file:
950
  file_size = os.path.getsize(downloaded_file)
951
  filename = os.path.basename(downloaded_file)
952
 
 
953
  background_tasks.add_task(cleanup_file, downloaded_file, delay=7200)
954
 
955
  return DownloadResponse(
956
  success=True,
957
- message="Video downloaded successfully",
958
  filename=filename,
959
  file_size=file_size,
960
  video_info=VideoInfo(**info),
@@ -963,7 +558,7 @@ async def download_video(request: DownloadRequest, background_tasks: BackgroundT
963
  else:
964
  raise HTTPException(
965
  status_code=503,
966
- detail="Failed to download video. Try uploading fresh cookies or wait before retrying."
967
  )
968
 
969
  except HTTPException:
@@ -972,174 +567,6 @@ async def download_video(request: DownloadRequest, background_tasks: BackgroundT
972
  logger.error(f"Error downloading video: {e}")
973
  raise HTTPException(status_code=500, detail=str(e))
974
 
975
- # Batch endpoints
976
- @app.post("/batch/info")
977
- async def batch_get_video_info(request: BatchVideoInfoRequest):
978
- """Get information for multiple videos"""
979
- try:
980
- if len(request.urls) > 50: # Limit batch size
981
- raise HTTPException(status_code=400, detail="Maximum 50 URLs allowed per batch")
982
-
983
- urls = [str(url) for url in request.urls]
984
-
985
- # Validate URLs
986
- for url in urls:
987
- if not any(domain in url for domain in ['youtube.com', 'youtu.be']):
988
- raise HTTPException(status_code=400, detail=f"Invalid YouTube URL: {url}")
989
-
990
- results = await downloader.batch_get_info(urls, request.use_cookies)
991
-
992
- success_count = sum(1 for r in results if r.success)
993
-
994
- return {
995
- "success": True,
996
- "total_urls": len(urls),
997
- "successful": success_count,
998
- "failed": len(urls) - success_count,
999
- "results": [r.dict() for r in results]
1000
- }
1001
-
1002
- except HTTPException:
1003
- raise
1004
- except Exception as e:
1005
- logger.error(f"Error in batch info: {e}")
1006
- raise HTTPException(status_code=500, detail=str(e))
1007
-
1008
- @app.post("/batch/download", response_model=BatchDownloadResponse)
1009
- async def batch_download_videos(request: BatchDownloadRequest, background_tasks: BackgroundTasks):
1010
- """Start batch download of multiple videos"""
1011
- try:
1012
- if len(request.urls) > 20: # Limit batch size for downloads
1013
- raise HTTPException(status_code=400, detail="Maximum 20 URLs allowed per batch download")
1014
-
1015
- if request.max_concurrent > 5: # Limit concurrent downloads
1016
- raise HTTPException(status_code=400, detail="Maximum 5 concurrent downloads allowed")
1017
-
1018
- urls = [str(url) for url in request.urls]
1019
-
1020
- # Validate URLs
1021
- for url in urls:
1022
- if not any(domain in url for domain in ['youtube.com', 'youtu.be']):
1023
- raise HTTPException(status_code=400, detail=f"Invalid YouTube URL: {url}")
1024
-
1025
- # Generate batch ID
1026
- batch_id = str(uuid.uuid4())
1027
-
1028
- # Start batch download in background
1029
- background_tasks.add_task(
1030
- downloader.batch_download,
1031
- batch_id,
1032
- urls,
1033
- request.quality,
1034
- request.audio_only,
1035
- request.use_cookies,
1036
- request.max_concurrent
1037
- )
1038
-
1039
- return BatchDownloadResponse(
1040
- batch_id=batch_id,
1041
- total_urls=len(urls),
1042
- status="started",
1043
- completed=0,
1044
- failed=0,
1045
- results=[]
1046
- )
1047
-
1048
- except HTTPException:
1049
- raise
1050
- except Exception as e:
1051
- logger.error(f"Error starting batch download: {e}")
1052
- raise HTTPException(status_code=500, detail=str(e))
1053
-
1054
- @app.get("/batch/status/{batch_id}", response_model=BatchStatus)
1055
- async def get_batch_status(batch_id: str):
1056
- """Get status of a batch download"""
1057
- if batch_id not in batch_status_store:
1058
- raise HTTPException(status_code=404, detail="Batch not found")
1059
-
1060
- status = batch_status_store[batch_id]
1061
- return BatchStatus(**status)
1062
-
1063
- @app.get("/batch/download-all/{batch_id}")
1064
- async def download_all_batch_files(batch_id: str):
1065
- """Download all files from a completed batch as multipart response"""
1066
- try:
1067
- if batch_id not in batch_status_store:
1068
- raise HTTPException(status_code=404, detail="Batch not found")
1069
-
1070
- batch_status = batch_status_store[batch_id]
1071
-
1072
- if batch_status["status"] != "completed":
1073
- raise HTTPException(
1074
- status_code=400,
1075
- detail=f"Batch is not completed yet. Current status: {batch_status['status']}"
1076
- )
1077
-
1078
- # Get all successfully downloaded files
1079
- file_paths = []
1080
- for result in batch_status["results"]:
1081
- if result.get("success") and result.get("download_path"):
1082
- file_path = result["download_path"]
1083
- if os.path.exists(file_path):
1084
- file_paths.append(file_path)
1085
-
1086
- if not file_paths:
1087
- raise HTTPException(status_code=404, detail="No files found for this batch")
1088
-
1089
- # Generate multipart response
1090
- file_generator, boundary = generate_multipart_response(file_paths)
1091
-
1092
- headers = {
1093
- "Content-Type": f"multipart/form-data; boundary={boundary}",
1094
- "Content-Disposition": f"attachment; filename=\"batch_{batch_id}_files.multipart\""
1095
- }
1096
-
1097
- return StreamingResponse(
1098
- file_generator,
1099
- headers=headers,
1100
- media_type=f"multipart/form-data; boundary={boundary}"
1101
- )
1102
-
1103
- except HTTPException:
1104
- raise
1105
- except Exception as e:
1106
- logger.error(f"Error downloading batch files: {e}")
1107
- raise HTTPException(status_code=500, detail=str(e))
1108
-
1109
- @app.get("/download-all")
1110
- async def download_all_files():
1111
- """Download all files from the server as multipart response"""
1112
- try:
1113
- # Get all files from the download directory
1114
- download_dir = Path(downloader.download_dir)
1115
- all_files = [f for f in download_dir.glob("*") if f.is_file()]
1116
-
1117
- if not all_files:
1118
- raise HTTPException(status_code=404, detail="No files found on server")
1119
-
1120
- # Convert to string paths
1121
- file_paths = [str(f) for f in all_files]
1122
-
1123
- # Generate multipart response
1124
- file_generator, boundary = generate_multipart_response(file_paths)
1125
-
1126
- headers = {
1127
- "Content-Type": f"multipart/form-data; boundary={boundary}",
1128
- "Content-Disposition": f"attachment; filename=\"all_server_files.multipart\""
1129
- }
1130
-
1131
- return StreamingResponse(
1132
- file_generator,
1133
- headers=headers,
1134
- media_type=f"multipart/form-data; boundary={boundary}"
1135
- )
1136
-
1137
- except HTTPException:
1138
- raise
1139
- except Exception as e:
1140
- logger.error(f"Error downloading all files: {e}")
1141
- raise HTTPException(status_code=500, detail=str(e))
1142
-
1143
  @app.get("/video/file/{filename}")
1144
  async def download_file(filename: str):
1145
  """Serve downloaded files"""
 
1
  #!/usr/bin/env python3
2
  """
3
+ Enhanced FastAPI YouTube Video Downloader with Anti-Bot Measures
4
+ Implements strategies to bypass YouTube's rate limiting and bot detection
5
  """
6
 
7
  import os
 
13
  import time
14
  import asyncio
15
  import logging
 
16
  from pathlib import Path
17
  from typing import Optional, Dict, Any, List
18
  from datetime import datetime
19
  from concurrent.futures import ThreadPoolExecutor
 
20
 
21
+ from fastapi import FastAPI, HTTPException, BackgroundTasks, Request
22
+ from fastapi.responses import FileResponse, HTMLResponse
23
  from fastapi.middleware.cors import CORSMiddleware
24
  from pydantic import BaseModel, HttpUrl
25
  import uvicorn
 
31
  # Pydantic models
32
  class VideoInfoRequest(BaseModel):
33
  url: HttpUrl
 
 
 
 
 
34
 
35
  class DownloadRequest(BaseModel):
36
  url: HttpUrl
37
  quality: str = "best"
38
  audio_only: bool = False
 
 
 
 
 
 
 
 
39
 
40
  class VideoInfo(BaseModel):
41
  title: str
 
49
  thumbnail: str
50
  webpage_url: str
51
 
 
 
 
 
 
 
52
  class DownloadResponse(BaseModel):
53
  success: bool
54
  message: str
 
57
  video_info: Optional[VideoInfo] = None
58
  download_path: Optional[str] = None
59
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
60
  class HealthResponse(BaseModel):
61
  status: str
62
  yt_dlp_available: bool
63
  timestamp: str
 
64
  strategies_enabled: List[str]
 
65
 
66
  # Initialize FastAPI app
67
  app = FastAPI(
68
+ title="Enhanced YouTube Video Downloader",
69
+ description="Download YouTube videos with anti-bot measures and rate limiting bypass",
70
+ version="2.0.0",
71
  docs_url="/docs",
72
  redoc_url="/redoc"
73
  )
 
82
  )
83
 
84
  # Thread pool for background tasks
85
+ executor = ThreadPoolExecutor(max_workers=2) # Reduced to avoid overwhelming YouTube
 
 
 
86
 
87
+ class AntiDetectionManager:
88
+ """Manages anti-detection strategies for YouTube downloading"""
89
 
90
+ def __init__(self):
91
+ self.user_agents = [
92
+ 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
93
+ 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
94
+ 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
95
+ 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:121.0) Gecko/20100101 Firefox/121.0',
96
+ 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:121.0) Gecko/20100101 Firefox/121.0',
97
+ 'Mozilla/5.0 (X11; Linux x86_64; rv:121.0) Gecko/20100101 Firefox/121.0',
98
+ 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Edge/120.0.0.0 Safari/537.36',
99
+ 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.2 Safari/605.1.15'
100
+ ]
101
 
102
+ self.sleep_intervals = [1, 2, 3, 5] # Random sleep intervals
103
+ self.last_request_time = 0
104
+ self.request_count = 0
105
+ self.max_requests_per_minute = 10
106
 
107
+ def get_random_user_agent(self) -> str:
108
+ """Get a random user agent"""
109
+ return random.choice(self.user_agents)
110
+
111
+ def get_sleep_interval(self) -> int:
112
+ """Get a random sleep interval"""
113
+ return random.choice(self.sleep_intervals)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
114
 
115
+ def should_rate_limit(self) -> bool:
116
+ """Check if we should rate limit requests"""
117
+ current_time = time.time()
 
118
 
119
+ # Reset counter every minute
120
+ if current_time - self.last_request_time > 60:
121
+ self.request_count = 0
122
+ self.last_request_time = current_time
123
+
124
+ self.request_count += 1
125
+
126
+ # Rate limit if too many requests
127
+ if self.request_count > self.max_requests_per_minute:
128
+ return True
129
+
130
+ return False
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
131
 
132
+ def get_enhanced_command(self, base_cmd: List[str]) -> List[str]:
133
+ """Enhance yt-dlp command with anti-detection measures"""
134
+ enhanced_cmd = base_cmd.copy()
135
+
136
+ # Add user agent
137
+ enhanced_cmd.extend(['--user-agent', self.get_random_user_agent()])
138
+
139
+ # Add sleep interval
140
+ enhanced_cmd.extend(['--sleep-interval', str(self.get_sleep_interval())])
141
+
142
+ # Add retry options
143
+ enhanced_cmd.extend(['--retries', '3'])
144
+ enhanced_cmd.extend(['--fragment-retries', '3'])
145
+
146
+ # Add socket timeout
147
+ enhanced_cmd.extend(['--socket-timeout', '30'])
148
+
149
+ # Disable certificate checking (sometimes helps)
150
+ enhanced_cmd.extend(['--no-check-certificates'])
151
+
152
+ # Add geo bypass
153
+ enhanced_cmd.extend(['--geo-bypass'])
154
+
155
+ # Add additional headers to look more like a browser
156
+ enhanced_cmd.extend(['--add-header', 'Accept:text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8'])
157
+ enhanced_cmd.extend(['--add-header', 'Accept-Language:en-US,en;q=0.5'])
158
+ enhanced_cmd.extend(['--add-header', 'Accept-Encoding:gzip, deflate'])
159
+ enhanced_cmd.extend(['--add-header', 'DNT:1'])
160
+ enhanced_cmd.extend(['--add-header', 'Connection:keep-alive'])
161
+ enhanced_cmd.extend(['--add-header', 'Upgrade-Insecure-Requests:1'])
162
+
163
+ return enhanced_cmd
164
 
165
+ class EnhancedYouTubeDownloader:
166
+ """Enhanced YouTube downloader with anti-detection measures"""
167
 
168
  def __init__(self, download_dir: str = None):
169
  if download_dir is None:
 
174
 
175
  self.download_dir = Path(download_dir)
176
  self.download_dir.mkdir(parents=True, exist_ok=True)
177
+ self.anti_detection = AntiDetectionManager()
 
 
 
 
 
 
 
178
 
179
+ # Ensure yt-dlp is available
180
  self._ensure_ytdlp_available()
181
 
182
  def _ensure_ytdlp_available(self):
183
  """Ensure yt-dlp is available, install if necessary"""
184
  try:
185
+ subprocess.run(['yt-dlp', '--version'],
186
+ capture_output=True, check=True)
187
  logger.info("yt-dlp is available")
188
  except (subprocess.CalledProcessError, FileNotFoundError):
189
  logger.info("Installing yt-dlp...")
 
195
  logger.error(f"Failed to install yt-dlp: {e}")
196
  raise RuntimeError("Could not install yt-dlp")
197
 
198
+ def get_video_info(self, url: str, retry_count: int = 0) -> Optional[Dict[str, Any]]:
199
+ """Get video information with anti-detection measures"""
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
200
  max_retries = 3
201
 
202
+ # Check rate limiting
203
+ if self.anti_detection.should_rate_limit():
204
+ logger.warning("Rate limiting applied - waiting before request")
205
+ time.sleep(30) # Wait 30 seconds if rate limited
206
 
207
  try:
208
+ base_cmd = [
209
+ 'yt-dlp',
210
+ '--dump-json',
211
+ '--no-download',
212
+ '--no-cookies',
213
+ str(url)
214
+ ]
215
+
216
+ # Enhance command with anti-detection measures
217
+ cmd = self.anti_detection.get_enhanced_command(base_cmd)
218
 
219
+ logger.info(f"Executing command (attempt {retry_count + 1}): {' '.join(cmd[:5])}...")
220
 
221
  result = subprocess.run(cmd, capture_output=True, text=True, check=True, timeout=60)
222
  video_info = json.loads(result.stdout)
 
237
  except subprocess.CalledProcessError as e:
238
  error_msg = e.stderr.lower() if e.stderr else ""
239
 
240
+ # Handle specific error cases
241
  if "429" in error_msg or "too many requests" in error_msg:
242
  if retry_count < max_retries:
243
+ wait_time = (retry_count + 1) * 30 # Exponential backoff
244
+ logger.warning(f"Rate limited (429), waiting {wait_time}s before retry {retry_count + 1}")
245
  time.sleep(wait_time)
246
+ return self.get_video_info(url, retry_count + 1)
247
+ else:
248
+ logger.error("Max retries exceeded for rate limiting")
249
+ return None
250
 
251
  elif "sign in" in error_msg or "bot" in error_msg:
252
+ if retry_count < max_retries:
253
+ wait_time = (retry_count + 1) * 60 # Longer wait for bot detection
254
+ logger.warning(f"Bot detection triggered, waiting {wait_time}s before retry {retry_count + 1}")
 
 
 
255
  time.sleep(wait_time)
256
+ return self.get_video_info(url, retry_count + 1)
257
+ else:
258
+ logger.error("Max retries exceeded for bot detection")
259
+ return None
260
 
261
  logger.error(f"Failed to get video info: {e.stderr}")
262
  return None
 
265
  logger.error(f"Error processing video info: {e}")
266
  if retry_count < max_retries:
267
  time.sleep(10)
268
+ return self.get_video_info(url, retry_count + 1)
269
  return None
270
 
271
  def download_video(self, url: str, quality: str = "best",
272
+ audio_only: bool = False, retry_count: int = 0) -> Optional[str]:
273
+ """Download video with anti-detection measures"""
 
274
  max_retries = 2
275
 
276
+ # Check rate limiting
277
+ if self.anti_detection.should_rate_limit():
278
+ logger.warning("Rate limiting applied - waiting before download")
279
+ time.sleep(30)
280
 
281
  try:
282
+ base_cmd = ['yt-dlp']
283
 
284
+ # Set output directory and filename template
285
+ output_template = str(self.download_dir / "%(title)s.%(ext)s")
286
+ base_cmd.extend(['-o', output_template])
287
 
288
+ # Set format/quality
289
  if audio_only:
290
+ base_cmd.extend(['-f', 'bestaudio/best'])
291
  else:
292
  if quality == "best":
293
+ base_cmd.extend(['-f', 'best[height<=720]'])
294
  elif quality == "worst":
295
+ base_cmd.extend(['-f', 'worst'])
296
  else:
297
+ base_cmd.extend(['-f', quality])
298
+
299
+ base_cmd.extend(['--no-cookies', str(url)])
300
 
301
+ # Enhance command with anti-detection measures
302
+ cmd = self.anti_detection.get_enhanced_command(base_cmd)
303
 
304
+ logger.info(f"Downloading video (attempt {retry_count + 1}): {url}")
305
 
306
  result = subprocess.run(cmd, capture_output=True, text=True, check=True, timeout=300)
307
 
308
  logger.info("Download completed successfully")
309
 
310
+ # Find the downloaded file
311
  downloaded_files = [f for f in self.download_dir.glob("*") if f.is_file()]
312
  if downloaded_files:
313
  latest_file = max(downloaded_files, key=os.path.getctime)
 
320
 
321
  if ("429" in error_msg or "too many requests" in error_msg or
322
  "sign in" in error_msg or "bot" in error_msg):
323
+ if retry_count < max_retries:
 
 
 
 
324
  wait_time = (retry_count + 1) * 60
325
  logger.warning(f"Download blocked, waiting {wait_time}s before retry {retry_count + 1}")
326
  time.sleep(wait_time)
327
+ return self.download_video(url, quality, audio_only, retry_count + 1)
328
 
329
  logger.error(f"Download failed: {e.stderr}")
330
  return None
 
332
  except subprocess.TimeoutExpired:
333
  logger.error("Download timeout")
334
  if retry_count < max_retries:
335
+ return self.download_video(url, quality, audio_only, retry_count + 1)
336
  return None
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
337
 
338
  # Global downloader instance
339
+ downloader = EnhancedYouTubeDownloader()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
340
 
341
  @app.get("/", response_class=HTMLResponse)
342
  async def read_root():
343
+ """Serve the main HTML interface"""
344
  html_content = """
345
  <!DOCTYPE html>
346
  <html lang="en">
347
  <head>
348
  <meta charset="UTF-8">
349
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
350
+ <title>Enhanced YouTube Video Downloader</title>
351
  <style>
352
  body {
353
  font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
 
364
  border-radius: 15px;
365
  box-shadow: 0 20px 40px rgba(0,0,0,0.1);
366
  padding: 40px;
367
+ max-width: 800px;
368
  width: 100%;
369
  }
370
  .header {
 
376
  margin-bottom: 10px;
377
  font-size: 2.5em;
378
  }
379
+ .header p {
380
+ color: #666;
381
+ font-size: 1.1em;
382
+ }
383
+ .features {
384
  background: #f8f9fa;
385
  border-radius: 8px;
386
  padding: 20px;
387
  margin: 20px 0;
388
  }
389
+ .feature {
390
+ background: white;
391
+ border: 1px solid #dee2e6;
392
+ border-radius: 5px;
393
+ padding: 15px;
394
+ margin: 10px 0;
395
  }
396
  .btn {
397
  background: linear-gradient(135deg, #667eea, #764ba2);
 
403
  display: inline-block;
404
  margin: 10px 5px;
405
  transition: transform 0.2s ease;
 
406
  }
407
  .btn:hover {
408
  transform: translateY(-2px);
409
  }
 
 
 
 
 
 
 
 
 
 
 
410
  .warning {
411
  background: #fff3cd;
412
  border: 1px solid #ffeaa7;
 
415
  margin: 20px 0;
416
  color: #856404;
417
  }
 
 
 
 
 
 
 
 
 
 
 
418
  </style>
419
  </head>
420
  <body>
421
  <div class="container">
422
  <div class="header">
423
+ <h1>🛡️ Enhanced YouTube Downloader</h1>
424
+ <p>Advanced anti-detection measures for reliable video downloading</p>
425
  </div>
426
 
427
  <div class="warning">
428
+ <strong>⚠️ Important:</strong> This enhanced version includes anti-bot measures to handle YouTube's restrictions.
429
+ Downloads may take longer due to rate limiting and retry mechanisms.
 
 
 
 
 
430
  </div>
431
 
432
+ <div class="features">
433
+ <h3>🚀 Enhanced Features</h3>
 
 
 
 
 
 
 
 
 
 
 
 
 
434
 
435
+ <div class="feature">
436
+ <strong>🔄 Smart Retry Logic:</strong> Automatically retries failed requests with exponential backoff
 
 
 
 
437
  </div>
438
 
439
+ <div class="feature">
440
+ <strong>🎭 User-Agent Rotation:</strong> Randomizes browser signatures to avoid detection
441
  </div>
442
 
443
  <div class="feature">
444
+ <strong>⏱️ Rate Limiting:</strong> Intelligent request spacing to prevent 429 errors
445
  </div>
446
 
447
  <div class="feature">
448
+ <strong>🌐 Enhanced Headers:</strong> Mimics real browser behavior with proper headers
449
  </div>
450
 
451
  <div class="feature">
452
+ <strong>🔧 Timeout Handling:</strong> Robust timeout and error recovery mechanisms
453
  </div>
454
  </div>
455
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
456
  <div style="text-align: center;">
457
+ <a href="/docs" class="btn">📖 API Documentation</a>
458
  <a href="/health" class="btn">🏥 Health Check</a>
 
459
  </div>
460
  </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
461
  </body>
462
  </html>
463
  """
464
  return HTMLResponse(content=html_content)
465
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
466
  @app.get("/health", response_model=HealthResponse)
467
  async def health_check():
468
+ """Enhanced health check with strategy information"""
469
  try:
470
  subprocess.run(['yt-dlp', '--version'], capture_output=True, check=True)
471
  yt_dlp_available = True
 
473
  yt_dlp_available = False
474
 
475
  strategies = [
 
 
476
  "User-Agent Rotation",
477
+ "Rate Limiting",
478
  "Smart Retry Logic",
479
  "Enhanced Headers",
480
+ "Timeout Handling",
481
+ "Exponential Backoff"
 
482
  ]
483
 
484
  return HealthResponse(
485
  status="healthy" if yt_dlp_available else "unhealthy",
486
  yt_dlp_available=yt_dlp_available,
487
  timestamp=datetime.now().isoformat(),
488
+ strategies_enabled=strategies
 
 
489
  )
490
 
 
491
  @app.post("/video/info", response_model=Dict[str, Any])
492
  async def get_video_info(request: VideoInfoRequest):
493
+ """Get video information with enhanced anti-detection"""
494
  try:
495
  url_str = str(request.url)
496
  if not any(domain in url_str for domain in ['youtube.com', 'youtu.be']):
497
  raise HTTPException(status_code=400, detail="Invalid YouTube URL")
498
 
499
+ # Get video info in thread pool with enhanced measures
500
  loop = asyncio.get_event_loop()
501
+ info = await loop.run_in_executor(executor, downloader.get_video_info, url_str)
 
 
 
 
 
502
 
503
  if info:
504
  return {"success": True, "info": info}
505
  else:
506
  raise HTTPException(
507
  status_code=503,
508
+ detail="Failed to get video information. YouTube may be blocking requests. Please try again later."
509
  )
510
 
511
  except HTTPException:
 
516
 
517
  @app.post("/video/download", response_model=DownloadResponse)
518
  async def download_video(request: DownloadRequest, background_tasks: BackgroundTasks):
519
+ """Download video with enhanced anti-detection"""
520
  try:
521
  url_str = str(request.url)
522
  if not any(domain in url_str for domain in ['youtube.com', 'youtu.be']):
523
  raise HTTPException(status_code=400, detail="Invalid YouTube URL")
524
 
525
+ # Get video info first
526
  loop = asyncio.get_event_loop()
527
+ info = await loop.run_in_executor(executor, downloader.get_video_info, url_str)
 
 
 
 
 
528
  if not info:
529
  raise HTTPException(
530
  status_code=503,
531
+ detail="Failed to get video information. YouTube may be blocking requests."
532
  )
533
 
534
+ # Download the video
535
  downloaded_file = await loop.run_in_executor(
536
  executor,
537
  downloader.download_video,
538
  url_str,
539
  request.quality,
540
+ request.audio_only
 
541
  )
542
 
543
  if downloaded_file:
544
  file_size = os.path.getsize(downloaded_file)
545
  filename = os.path.basename(downloaded_file)
546
 
547
+ # Schedule cleanup after 2 hours (longer due to potential delays)
548
  background_tasks.add_task(cleanup_file, downloaded_file, delay=7200)
549
 
550
  return DownloadResponse(
551
  success=True,
552
+ message="Video downloaded successfully with anti-detection measures",
553
  filename=filename,
554
  file_size=file_size,
555
  video_info=VideoInfo(**info),
 
558
  else:
559
  raise HTTPException(
560
  status_code=503,
561
+ detail="Failed to download video. YouTube may be blocking requests. Please try again later."
562
  )
563
 
564
  except HTTPException:
 
567
  logger.error(f"Error downloading video: {e}")
568
  raise HTTPException(status_code=500, detail=str(e))
569
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
570
  @app.get("/video/file/{filename}")
571
  async def download_file(filename: str):
572
  """Serve downloaded files"""