Fred808 commited on
Commit
9483663
·
verified ·
1 Parent(s): 5ae2bb8

Upload main.py

Browse files
Files changed (1) hide show
  1. main.py +342 -135
main.py CHANGED
@@ -1,7 +1,7 @@
1
  #!/usr/bin/env python3
2
  """
3
- Cookie-Enhanced FastAPI YouTube Video Downloader
4
- Addresses common cookie issues and provides robust cookie handling
5
  """
6
 
7
  import os
@@ -13,6 +13,7 @@ import random
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
@@ -33,12 +34,23 @@ class VideoInfoRequest(BaseModel):
33
  url: HttpUrl
34
  use_cookies: bool = False
35
 
 
 
 
 
36
  class DownloadRequest(BaseModel):
37
  url: HttpUrl
38
  quality: str = "best"
39
  audio_only: bool = False
40
  use_cookies: bool = False
41
 
 
 
 
 
 
 
 
42
  class VideoInfo(BaseModel):
43
  title: str
44
  duration: int
@@ -51,6 +63,12 @@ class VideoInfo(BaseModel):
51
  thumbnail: str
52
  webpage_url: str
53
 
 
 
 
 
 
 
54
  class DownloadResponse(BaseModel):
55
  success: bool
56
  message: str
@@ -59,18 +77,38 @@ class DownloadResponse(BaseModel):
59
  video_info: Optional[VideoInfo] = None
60
  download_path: Optional[str] = None
61
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
62
  class HealthResponse(BaseModel):
63
  status: str
64
  yt_dlp_available: bool
65
  timestamp: str
66
  cookie_file_exists: bool
67
  strategies_enabled: List[str]
 
68
 
69
  # Initialize FastAPI app
70
  app = FastAPI(
71
- title="Cookie-Enhanced YouTube Video Downloader",
72
- description="Download YouTube videos with proper cookie support and troubleshooting",
73
- version="3.0.0",
74
  docs_url="/docs",
75
  redoc_url="/redoc"
76
  )
@@ -85,7 +123,10 @@ app.add_middleware(
85
  )
86
 
87
  # Thread pool for background tasks
88
- executor = ThreadPoolExecutor(max_workers=2)
 
 
 
89
 
90
  class CookieManager:
91
  """Manages cookie files and validation"""
@@ -104,22 +145,17 @@ class CookieManager:
104
  def save_cookies(self, cookie_content: str) -> bool:
105
  """Save cookie content to file with validation"""
106
  try:
107
- # Basic validation - check if it looks like Netscape format
108
  lines = cookie_content.strip().split('\n')
109
-
110
- # Remove comments and empty lines for validation
111
  data_lines = [line for line in lines if line.strip() and not line.startswith('#')]
112
 
113
  if not data_lines:
114
  logger.error("Cookie file appears to be empty or contains only comments")
115
  return False
116
 
117
- # Check if any line contains youtube.com
118
  has_youtube = any('youtube.com' in line for line in data_lines)
119
  if not has_youtube:
120
  logger.warning("Cookie file doesn't contain youtube.com entries")
121
 
122
- # Save the file
123
  with open(self.cookie_file, 'w', encoding='utf-8') as f:
124
  f.write(cookie_content)
125
 
@@ -145,10 +181,7 @@ class CookieManager:
145
  if not data_lines:
146
  return {"valid": False, "reason": "Cookie file is empty or contains only comments"}
147
 
148
- # Check for YouTube cookies
149
  youtube_cookies = [line for line in data_lines if 'youtube.com' in line]
150
-
151
- # Check for essential cookies
152
  essential_cookies = ['VISITOR_INFO1_LIVE', 'YSC', 'CONSENT']
153
  found_essential = []
154
 
@@ -176,8 +209,8 @@ class CookieManager:
176
  return str(self.cookie_file)
177
  return None
178
 
179
- class EnhancedYouTubeDownloader:
180
- """Enhanced YouTube downloader with cookie support and troubleshooting"""
181
 
182
  def __init__(self, download_dir: str = None):
183
  if download_dir is None:
@@ -190,7 +223,6 @@ class EnhancedYouTubeDownloader:
190
  self.download_dir.mkdir(parents=True, exist_ok=True)
191
  self.cookie_manager = CookieManager()
192
 
193
- # User agents for rotation
194
  self.user_agents = [
195
  'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
196
  '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',
@@ -198,14 +230,12 @@ class EnhancedYouTubeDownloader:
198
  'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:121.0) Gecko/20100101 Firefox/121.0',
199
  ]
200
 
201
- # Ensure yt-dlp is available
202
  self._ensure_ytdlp_available()
203
 
204
  def _ensure_ytdlp_available(self):
205
  """Ensure yt-dlp is available, install if necessary"""
206
  try:
207
- subprocess.run(['yt-dlp', '--version'],
208
- capture_output=True, check=True)
209
  logger.info("yt-dlp is available")
210
  except (subprocess.CalledProcessError, FileNotFoundError):
211
  logger.info("Installing yt-dlp...")
@@ -221,10 +251,8 @@ class EnhancedYouTubeDownloader:
221
  """Build yt-dlp command with proper options"""
222
  cmd = base_cmd.copy()
223
 
224
- # Add user agent
225
  cmd.extend(['--user-agent', random.choice(self.user_agents)])
226
 
227
- # Cookie handling
228
  if use_cookies:
229
  cookie_path = self.cookie_manager.get_cookie_path()
230
  if cookie_path:
@@ -236,7 +264,6 @@ class EnhancedYouTubeDownloader:
236
  else:
237
  cmd.extend(['--no-cookies'])
238
 
239
- # Enhanced options for better success rate
240
  cmd.extend([
241
  '--sleep-interval', str(random.randint(1, 3)),
242
  '--retries', '5',
@@ -259,13 +286,7 @@ class EnhancedYouTubeDownloader:
259
  max_retries = 3
260
 
261
  try:
262
- base_cmd = [
263
- 'yt-dlp',
264
- '--dump-json',
265
- '--no-download',
266
- str(url)
267
- ]
268
-
269
  cmd = self._build_command(base_cmd, use_cookies)
270
 
271
  logger.info(f"Getting video info (attempt {retry_count + 1}, cookies: {use_cookies})")
@@ -289,7 +310,6 @@ class EnhancedYouTubeDownloader:
289
  except subprocess.CalledProcessError as e:
290
  error_msg = e.stderr.lower() if e.stderr else ""
291
 
292
- # Handle specific error cases
293
  if "429" in error_msg or "too many requests" in error_msg:
294
  if retry_count < max_retries:
295
  wait_time = (retry_count + 1) * 30
@@ -307,13 +327,6 @@ class EnhancedYouTubeDownloader:
307
  time.sleep(wait_time)
308
  return self.get_video_info(url, use_cookies, retry_count + 1)
309
 
310
- elif "cookies" in error_msg and use_cookies:
311
- logger.error("Cookie-related error - cookies may be expired or invalid")
312
- # Try without cookies as fallback
313
- if retry_count == 0:
314
- logger.info("Retrying without cookies as fallback")
315
- return self.get_video_info(url, False, retry_count + 1)
316
-
317
  logger.error(f"Failed to get video info: {e.stderr}")
318
  return None
319
 
@@ -333,11 +346,9 @@ class EnhancedYouTubeDownloader:
333
  try:
334
  base_cmd = ['yt-dlp']
335
 
336
- # Set output directory and filename template
337
  output_template = str(self.download_dir / "%(title)s.%(ext)s")
338
  base_cmd.extend(['-o', output_template])
339
 
340
- # Set format/quality
341
  if audio_only:
342
  base_cmd.extend(['-f', 'bestaudio/best'])
343
  else:
@@ -349,7 +360,6 @@ class EnhancedYouTubeDownloader:
349
  base_cmd.extend(['-f', quality])
350
 
351
  base_cmd.append(str(url))
352
-
353
  cmd = self._build_command(base_cmd, use_cookies)
354
 
355
  logger.info(f"Downloading video (attempt {retry_count + 1}, cookies: {use_cookies})")
@@ -358,7 +368,6 @@ class EnhancedYouTubeDownloader:
358
 
359
  logger.info("Download completed successfully")
360
 
361
- # Find the downloaded file
362
  downloaded_files = [f for f in self.download_dir.glob("*") if f.is_file()]
363
  if downloaded_files:
364
  latest_file = max(downloaded_files, key=os.path.getctime)
@@ -389,20 +398,161 @@ class EnhancedYouTubeDownloader:
389
  if retry_count < max_retries:
390
  return self.download_video(url, quality, audio_only, use_cookies, retry_count + 1)
391
  return None
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
392
 
393
  # Global downloader instance
394
- downloader = EnhancedYouTubeDownloader()
395
 
396
  @app.get("/", response_class=HTMLResponse)
397
  async def read_root():
398
- """Serve the main HTML interface with cookie upload"""
399
  html_content = """
400
  <!DOCTYPE html>
401
  <html lang="en">
402
  <head>
403
  <meta charset="UTF-8">
404
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
405
- <title>Cookie-Enhanced YouTube Downloader</title>
406
  <style>
407
  body {
408
  font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
@@ -419,7 +569,7 @@ async def read_root():
419
  border-radius: 15px;
420
  box-shadow: 0 20px 40px rgba(0,0,0,0.1);
421
  padding: 40px;
422
- max-width: 900px;
423
  width: 100%;
424
  }
425
  .header {
@@ -437,13 +587,6 @@ async def read_root():
437
  padding: 20px;
438
  margin: 20px 0;
439
  }
440
- .upload-area {
441
- border: 2px dashed #dee2e6;
442
- border-radius: 8px;
443
- padding: 20px;
444
- text-align: center;
445
- margin: 15px 0;
446
- }
447
  .btn {
448
  background: linear-gradient(135deg, #667eea, #764ba2);
449
  color: white;
@@ -459,103 +602,79 @@ async def read_root():
459
  .btn:hover {
460
  transform: translateY(-2px);
461
  }
462
- .warning {
463
- background: #fff3cd;
464
- border: 1px solid #ffeaa7;
465
- border-radius: 5px;
466
- padding: 15px;
467
- margin: 20px 0;
468
- color: #856404;
469
- }
470
- .success {
471
- background: #d4edda;
472
- border: 1px solid #c3e6cb;
473
  border-radius: 5px;
474
  padding: 15px;
475
- margin: 20px 0;
476
- color: #155724;
477
- }
478
- input[type="file"] {
479
  margin: 10px 0;
480
  }
 
 
 
 
481
  </style>
482
  </head>
483
  <body>
484
  <div class="container">
485
  <div class="header">
486
- <h1>🍪 Cookie-Enhanced YouTube Downloader</h1>
487
- <p>Upload your YouTube cookies for better success rates</p>
488
- </div>
489
-
490
- <div class="warning">
491
- <strong>⚠️ Cookie Issues?</strong> If your cookies aren't working, they might be:
492
- <ul>
493
- <li>Expired (YouTube cookies expire frequently)</li>
494
- <li>Wrong format (must be Netscape format)</li>
495
- <li>Missing essential cookies</li>
496
- <li>From a different IP/location</li>
497
- </ul>
498
  </div>
499
 
500
  <div class="section">
501
- <h3>📤 Upload Cookie File</h3>
502
- <div class="upload-area">
503
- <form id="cookieForm" enctype="multipart/form-data">
504
- <p>Select your YouTube cookies.txt file (Netscape format):</p>
505
- <input type="file" id="cookieFile" name="cookie_file" accept=".txt" required>
506
- <br>
507
- <button type="submit" class="btn">Upload Cookies</button>
508
- </form>
 
 
 
 
 
 
 
 
 
 
 
 
509
  </div>
510
- <div id="uploadResult"></div>
511
  </div>
512
 
513
  <div class="section">
514
- <h3>🔍 How to Export Cookies</h3>
515
- <ol>
516
- <li>Install a cookie export extension (like "Get cookies.txt LOCALLY")</li>
517
- <li>Go to YouTube and make sure you're logged in</li>
518
- <li>Use the extension to export cookies in Netscape format</li>
519
- <li>Save the file and upload it here</li>
520
- </ol>
 
 
 
 
 
 
 
 
 
 
 
 
 
521
  </div>
522
 
523
  <div style="text-align: center;">
524
- <a href="/docs" class="btn">📖 API Documentation</a>
525
  <a href="/health" class="btn">🏥 Health Check</a>
526
  <a href="/cookie-status" class="btn">🍪 Cookie Status</a>
527
  </div>
528
  </div>
529
-
530
- <script>
531
- document.getElementById('cookieForm').addEventListener('submit', async function(e) {
532
- e.preventDefault();
533
-
534
- const formData = new FormData();
535
- const fileInput = document.getElementById('cookieFile');
536
- formData.append('cookie_file', fileInput.files[0]);
537
-
538
- const resultDiv = document.getElementById('uploadResult');
539
- resultDiv.innerHTML = '<p>Uploading...</p>';
540
-
541
- try {
542
- const response = await fetch('/upload-cookies', {
543
- method: 'POST',
544
- body: formData
545
- });
546
-
547
- const result = await response.json();
548
-
549
- if (result.success) {
550
- resultDiv.innerHTML = '<div class="success"><strong>✅ Success!</strong> ' + result.message + '</div>';
551
- } else {
552
- resultDiv.innerHTML = '<div class="warning"><strong>❌ Error:</strong> ' + result.message + '</div>';
553
- }
554
- } catch (error) {
555
- resultDiv.innerHTML = '<div class="warning"><strong>❌ Error:</strong> Failed to upload cookies</div>';
556
- }
557
- });
558
- </script>
559
  </body>
560
  </html>
561
  """
@@ -604,7 +723,7 @@ async def cookie_status():
604
 
605
  @app.get("/health", response_model=HealthResponse)
606
  async def health_check():
607
- """Enhanced health check with cookie information"""
608
  try:
609
  subprocess.run(['yt-dlp', '--version'], capture_output=True, check=True)
610
  yt_dlp_available = True
@@ -612,12 +731,13 @@ async def health_check():
612
  yt_dlp_available = False
613
 
614
  strategies = [
 
615
  "Cookie Support",
616
  "User-Agent Rotation",
617
  "Smart Retry Logic",
618
  "Enhanced Headers",
619
- "Timeout Handling",
620
- "Automatic Cookie Fallback"
621
  ]
622
 
623
  return HealthResponse(
@@ -625,9 +745,11 @@ async def health_check():
625
  yt_dlp_available=yt_dlp_available,
626
  timestamp=datetime.now().isoformat(),
627
  cookie_file_exists=downloader.cookie_manager.cookie_file.exists(),
628
- strategies_enabled=strategies
 
629
  )
630
 
 
631
  @app.post("/video/info", response_model=Dict[str, Any])
632
  async def get_video_info(request: VideoInfoRequest):
633
  """Get video information with cookie support"""
@@ -666,7 +788,6 @@ async def download_video(request: DownloadRequest, background_tasks: BackgroundT
666
  if not any(domain in url_str for domain in ['youtube.com', 'youtu.be']):
667
  raise HTTPException(status_code=400, detail="Invalid YouTube URL")
668
 
669
- # Get video info first
670
  loop = asyncio.get_event_loop()
671
  info = await loop.run_in_executor(
672
  executor,
@@ -680,7 +801,6 @@ async def download_video(request: DownloadRequest, background_tasks: BackgroundT
680
  detail="Failed to get video information. Try uploading fresh cookies."
681
  )
682
 
683
- # Download the video
684
  downloaded_file = await loop.run_in_executor(
685
  executor,
686
  downloader.download_video,
@@ -694,12 +814,11 @@ async def download_video(request: DownloadRequest, background_tasks: BackgroundT
694
  file_size = os.path.getsize(downloaded_file)
695
  filename = os.path.basename(downloaded_file)
696
 
697
- # Schedule cleanup after 2 hours
698
  background_tasks.add_task(cleanup_file, downloaded_file, delay=7200)
699
 
700
  return DownloadResponse(
701
  success=True,
702
- message="Video downloaded successfully with cookie support",
703
  filename=filename,
704
  file_size=file_size,
705
  video_info=VideoInfo(**info),
@@ -717,6 +836,94 @@ async def download_video(request: DownloadRequest, background_tasks: BackgroundT
717
  logger.error(f"Error downloading video: {e}")
718
  raise HTTPException(status_code=500, detail=str(e))
719
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
720
  @app.get("/video/file/{filename}")
721
  async def download_file(filename: str):
722
  """Serve downloaded files"""
 
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
  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
 
34
  url: HttpUrl
35
  use_cookies: bool = False
36
 
37
+ class BatchVideoInfoRequest(BaseModel):
38
+ urls: List[HttpUrl]
39
+ use_cookies: bool = False
40
+
41
  class DownloadRequest(BaseModel):
42
  url: HttpUrl
43
  quality: str = "best"
44
  audio_only: bool = False
45
  use_cookies: bool = False
46
 
47
+ class BatchDownloadRequest(BaseModel):
48
+ urls: List[HttpUrl]
49
+ quality: str = "best"
50
+ audio_only: bool = False
51
+ use_cookies: bool = False
52
+ max_concurrent: int = 2 # Limit concurrent downloads
53
+
54
  class VideoInfo(BaseModel):
55
  title: str
56
  duration: int
 
63
  thumbnail: str
64
  webpage_url: str
65
 
66
+ class BatchVideoInfo(BaseModel):
67
+ url: str
68
+ success: bool
69
+ info: Optional[VideoInfo] = None
70
+ error: Optional[str] = None
71
+
72
  class DownloadResponse(BaseModel):
73
  success: bool
74
  message: str
 
77
  video_info: Optional[VideoInfo] = None
78
  download_path: Optional[str] = None
79
 
80
+ class BatchDownloadResponse(BaseModel):
81
+ batch_id: str
82
+ total_urls: int
83
+ status: str
84
+ completed: int
85
+ failed: int
86
+ results: List[Dict[str, Any]]
87
+
88
+ class BatchStatus(BaseModel):
89
+ batch_id: str
90
+ status: str
91
+ total_urls: int
92
+ completed: int
93
+ failed: int
94
+ in_progress: int
95
+ results: List[Dict[str, Any]]
96
+ created_at: str
97
+ updated_at: str
98
+
99
  class HealthResponse(BaseModel):
100
  status: str
101
  yt_dlp_available: bool
102
  timestamp: str
103
  cookie_file_exists: bool
104
  strategies_enabled: List[str]
105
+ batch_support: bool
106
 
107
  # Initialize FastAPI app
108
  app = FastAPI(
109
+ title="Batch YouTube Video Downloader",
110
+ description="Download YouTube videos individually or in batches with cookie support",
111
+ version="4.0.0",
112
  docs_url="/docs",
113
  redoc_url="/redoc"
114
  )
 
123
  )
124
 
125
  # Thread pool for background tasks
126
+ executor = ThreadPoolExecutor(max_workers=4)
127
+
128
+ # Global batch status storage (in production, use Redis or database)
129
+ batch_status_store = {}
130
 
131
  class CookieManager:
132
  """Manages cookie files and validation"""
 
145
  def save_cookies(self, cookie_content: str) -> bool:
146
  """Save cookie content to file with validation"""
147
  try:
 
148
  lines = cookie_content.strip().split('\n')
 
 
149
  data_lines = [line for line in lines if line.strip() and not line.startswith('#')]
150
 
151
  if not data_lines:
152
  logger.error("Cookie file appears to be empty or contains only comments")
153
  return False
154
 
 
155
  has_youtube = any('youtube.com' in line for line in data_lines)
156
  if not has_youtube:
157
  logger.warning("Cookie file doesn't contain youtube.com entries")
158
 
 
159
  with open(self.cookie_file, 'w', encoding='utf-8') as f:
160
  f.write(cookie_content)
161
 
 
181
  if not data_lines:
182
  return {"valid": False, "reason": "Cookie file is empty or contains only comments"}
183
 
 
184
  youtube_cookies = [line for line in data_lines if 'youtube.com' in line]
 
 
185
  essential_cookies = ['VISITOR_INFO1_LIVE', 'YSC', 'CONSENT']
186
  found_essential = []
187
 
 
209
  return str(self.cookie_file)
210
  return None
211
 
212
+ class BatchYouTubeDownloader:
213
+ """Enhanced YouTube downloader with batch support and cookie handling"""
214
 
215
  def __init__(self, download_dir: str = None):
216
  if download_dir is None:
 
223
  self.download_dir.mkdir(parents=True, exist_ok=True)
224
  self.cookie_manager = CookieManager()
225
 
 
226
  self.user_agents = [
227
  'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
228
  '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 (Windows NT 10.0; Win64; x64; rv:121.0) Gecko/20100101 Firefox/121.0',
231
  ]
232
 
 
233
  self._ensure_ytdlp_available()
234
 
235
  def _ensure_ytdlp_available(self):
236
  """Ensure yt-dlp is available, install if necessary"""
237
  try:
238
+ subprocess.run(['yt-dlp', '--version'], capture_output=True, check=True)
 
239
  logger.info("yt-dlp is available")
240
  except (subprocess.CalledProcessError, FileNotFoundError):
241
  logger.info("Installing yt-dlp...")
 
251
  """Build yt-dlp command with proper options"""
252
  cmd = base_cmd.copy()
253
 
 
254
  cmd.extend(['--user-agent', random.choice(self.user_agents)])
255
 
 
256
  if use_cookies:
257
  cookie_path = self.cookie_manager.get_cookie_path()
258
  if cookie_path:
 
264
  else:
265
  cmd.extend(['--no-cookies'])
266
 
 
267
  cmd.extend([
268
  '--sleep-interval', str(random.randint(1, 3)),
269
  '--retries', '5',
 
286
  max_retries = 3
287
 
288
  try:
289
+ base_cmd = ['yt-dlp', '--dump-json', '--no-download', str(url)]
 
 
 
 
 
 
290
  cmd = self._build_command(base_cmd, use_cookies)
291
 
292
  logger.info(f"Getting video info (attempt {retry_count + 1}, cookies: {use_cookies})")
 
310
  except subprocess.CalledProcessError as e:
311
  error_msg = e.stderr.lower() if e.stderr else ""
312
 
 
313
  if "429" in error_msg or "too many requests" in error_msg:
314
  if retry_count < max_retries:
315
  wait_time = (retry_count + 1) * 30
 
327
  time.sleep(wait_time)
328
  return self.get_video_info(url, use_cookies, retry_count + 1)
329
 
 
 
 
 
 
 
 
330
  logger.error(f"Failed to get video info: {e.stderr}")
331
  return None
332
 
 
346
  try:
347
  base_cmd = ['yt-dlp']
348
 
 
349
  output_template = str(self.download_dir / "%(title)s.%(ext)s")
350
  base_cmd.extend(['-o', output_template])
351
 
 
352
  if audio_only:
353
  base_cmd.extend(['-f', 'bestaudio/best'])
354
  else:
 
360
  base_cmd.extend(['-f', quality])
361
 
362
  base_cmd.append(str(url))
 
363
  cmd = self._build_command(base_cmd, use_cookies)
364
 
365
  logger.info(f"Downloading video (attempt {retry_count + 1}, cookies: {use_cookies})")
 
368
 
369
  logger.info("Download completed successfully")
370
 
 
371
  downloaded_files = [f for f in self.download_dir.glob("*") if f.is_file()]
372
  if downloaded_files:
373
  latest_file = max(downloaded_files, key=os.path.getctime)
 
398
  if retry_count < max_retries:
399
  return self.download_video(url, quality, audio_only, use_cookies, retry_count + 1)
400
  return None
401
+
402
+ async def batch_get_info(self, urls: List[str], use_cookies: bool = False) -> List[BatchVideoInfo]:
403
+ """Get info for multiple videos concurrently"""
404
+ results = []
405
+
406
+ async def get_single_info(url: str) -> BatchVideoInfo:
407
+ try:
408
+ loop = asyncio.get_event_loop()
409
+ info = await loop.run_in_executor(executor, self.get_video_info, url, use_cookies)
410
+
411
+ if info:
412
+ return BatchVideoInfo(
413
+ url=url,
414
+ success=True,
415
+ info=VideoInfo(**info)
416
+ )
417
+ else:
418
+ return BatchVideoInfo(
419
+ url=url,
420
+ success=False,
421
+ error="Failed to get video information"
422
+ )
423
+ except Exception as e:
424
+ return BatchVideoInfo(
425
+ url=url,
426
+ success=False,
427
+ error=str(e)
428
+ )
429
+
430
+ # Process URLs concurrently with a limit
431
+ semaphore = asyncio.Semaphore(3) # Limit concurrent requests
432
+
433
+ async def limited_get_info(url: str) -> BatchVideoInfo:
434
+ async with semaphore:
435
+ return await get_single_info(url)
436
+
437
+ tasks = [limited_get_info(url) for url in urls]
438
+ results = await asyncio.gather(*tasks)
439
+
440
+ return results
441
+
442
+ async def batch_download(self, batch_id: str, urls: List[str], quality: str = "best",
443
+ audio_only: bool = False, use_cookies: bool = False,
444
+ max_concurrent: int = 2) -> None:
445
+ """Download multiple videos with progress tracking"""
446
+
447
+ # Initialize batch status
448
+ batch_status_store[batch_id] = {
449
+ "batch_id": batch_id,
450
+ "status": "in_progress",
451
+ "total_urls": len(urls),
452
+ "completed": 0,
453
+ "failed": 0,
454
+ "in_progress": 0,
455
+ "results": [],
456
+ "created_at": datetime.now().isoformat(),
457
+ "updated_at": datetime.now().isoformat()
458
+ }
459
+
460
+ semaphore = asyncio.Semaphore(max_concurrent)
461
+
462
+ async def download_single(url: str) -> Dict[str, Any]:
463
+ async with semaphore:
464
+ batch_status_store[batch_id]["in_progress"] += 1
465
+ batch_status_store[batch_id]["updated_at"] = datetime.now().isoformat()
466
+
467
+ try:
468
+ loop = asyncio.get_event_loop()
469
+
470
+ # Get video info first
471
+ info = await loop.run_in_executor(
472
+ executor, self.get_video_info, url, use_cookies
473
+ )
474
+
475
+ if not info:
476
+ result = {
477
+ "url": url,
478
+ "success": False,
479
+ "error": "Failed to get video information",
480
+ "completed_at": datetime.now().isoformat()
481
+ }
482
+ batch_status_store[batch_id]["failed"] += 1
483
+ batch_status_store[batch_id]["in_progress"] -= 1
484
+ batch_status_store[batch_id]["results"].append(result)
485
+ batch_status_store[batch_id]["updated_at"] = datetime.now().isoformat()
486
+ return result
487
+
488
+ # Download the video
489
+ downloaded_file = await loop.run_in_executor(
490
+ executor, self.download_video, url, quality, audio_only, use_cookies
491
+ )
492
+
493
+ if downloaded_file:
494
+ file_size = os.path.getsize(downloaded_file)
495
+ filename = os.path.basename(downloaded_file)
496
+
497
+ result = {
498
+ "url": url,
499
+ "success": True,
500
+ "filename": filename,
501
+ "file_size": file_size,
502
+ "video_info": info,
503
+ "download_path": downloaded_file,
504
+ "completed_at": datetime.now().isoformat()
505
+ }
506
+ batch_status_store[batch_id]["completed"] += 1
507
+ else:
508
+ result = {
509
+ "url": url,
510
+ "success": False,
511
+ "error": "Failed to download video",
512
+ "completed_at": datetime.now().isoformat()
513
+ }
514
+ batch_status_store[batch_id]["failed"] += 1
515
+
516
+ batch_status_store[batch_id]["in_progress"] -= 1
517
+ batch_status_store[batch_id]["results"].append(result)
518
+ batch_status_store[batch_id]["updated_at"] = datetime.now().isoformat()
519
+
520
+ return result
521
+
522
+ except Exception as e:
523
+ result = {
524
+ "url": url,
525
+ "success": False,
526
+ "error": str(e),
527
+ "completed_at": datetime.now().isoformat()
528
+ }
529
+ batch_status_store[batch_id]["failed"] += 1
530
+ batch_status_store[batch_id]["in_progress"] -= 1
531
+ batch_status_store[batch_id]["results"].append(result)
532
+ batch_status_store[batch_id]["updated_at"] = datetime.now().isoformat()
533
+ return result
534
+
535
+ # Process all downloads
536
+ tasks = [download_single(url) for url in urls]
537
+ await asyncio.gather(*tasks)
538
+
539
+ # Mark batch as completed
540
+ batch_status_store[batch_id]["status"] = "completed"
541
+ batch_status_store[batch_id]["updated_at"] = datetime.now().isoformat()
542
 
543
  # Global downloader instance
544
+ downloader = BatchYouTubeDownloader()
545
 
546
  @app.get("/", response_class=HTMLResponse)
547
  async def read_root():
548
+ """Serve the main HTML interface with batch support"""
549
  html_content = """
550
  <!DOCTYPE html>
551
  <html lang="en">
552
  <head>
553
  <meta charset="UTF-8">
554
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
555
+ <title>Batch YouTube Video Downloader</title>
556
  <style>
557
  body {
558
  font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
 
569
  border-radius: 15px;
570
  box-shadow: 0 20px 40px rgba(0,0,0,0.1);
571
  padding: 40px;
572
+ max-width: 1000px;
573
  width: 100%;
574
  }
575
  .header {
 
587
  padding: 20px;
588
  margin: 20px 0;
589
  }
 
 
 
 
 
 
 
590
  .btn {
591
  background: linear-gradient(135deg, #667eea, #764ba2);
592
  color: white;
 
602
  .btn:hover {
603
  transform: translateY(-2px);
604
  }
605
+ .feature {
606
+ background: white;
607
+ border: 1px solid #dee2e6;
 
 
 
 
 
 
 
 
608
  border-radius: 5px;
609
  padding: 15px;
 
 
 
 
610
  margin: 10px 0;
611
  }
612
+ .new-feature {
613
+ background: #e8f5e8;
614
+ border: 1px solid #4caf50;
615
+ }
616
  </style>
617
  </head>
618
  <body>
619
  <div class="container">
620
  <div class="header">
621
+ <h1>📦 Batch YouTube Video Downloader</h1>
622
+ <p>Download single videos or process multiple URLs in batches</p>
 
 
 
 
 
 
 
 
 
 
623
  </div>
624
 
625
  <div class="section">
626
+ <h3>🚀 Features</h3>
627
+
628
+ <div class="feature new-feature">
629
+ <strong>🆕 Batch Downloads:</strong> Process multiple YouTube URLs simultaneously
630
+ </div>
631
+
632
+ <div class="feature new-feature">
633
+ <strong>🆕 Progress Tracking:</strong> Monitor batch download progress in real-time
634
+ </div>
635
+
636
+ <div class="feature">
637
+ <strong>🍪 Cookie Support:</strong> Upload cookies for better success rates
638
+ </div>
639
+
640
+ <div class="feature">
641
+ <strong>🔄 Smart Retry Logic:</strong> Automatic retries with exponential backoff
642
+ </div>
643
+
644
+ <div class="feature">
645
+ <strong>⚡ Concurrent Processing:</strong> Configurable concurrent download limits
646
  </div>
 
647
  </div>
648
 
649
  <div class="section">
650
+ <h3>📋 API Endpoints</h3>
651
+
652
+ <h4>Single Video Operations:</h4>
653
+ <ul>
654
+ <li><code>POST /video/info</code> - Get single video information</li>
655
+ <li><code>POST /video/download</code> - Download single video</li>
656
+ </ul>
657
+
658
+ <h4>Batch Operations:</h4>
659
+ <ul>
660
+ <li><code>POST /batch/info</code> - Get info for multiple videos</li>
661
+ <li><code>POST /batch/download</code> - Start batch download</li>
662
+ <li><code>GET /batch/status/{batch_id}</code> - Check batch progress</li>
663
+ </ul>
664
+
665
+ <h4>Cookie Management:</h4>
666
+ <ul>
667
+ <li><code>POST /upload-cookies</code> - Upload cookie file</li>
668
+ <li><code>GET /cookie-status</code> - Check cookie status</li>
669
+ </ul>
670
  </div>
671
 
672
  <div style="text-align: center;">
673
+ <a href="/docs" class="btn">📖 Interactive API Docs</a>
674
  <a href="/health" class="btn">🏥 Health Check</a>
675
  <a href="/cookie-status" class="btn">🍪 Cookie Status</a>
676
  </div>
677
  </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
678
  </body>
679
  </html>
680
  """
 
723
 
724
  @app.get("/health", response_model=HealthResponse)
725
  async def health_check():
726
+ """Enhanced health check with batch support information"""
727
  try:
728
  subprocess.run(['yt-dlp', '--version'], capture_output=True, check=True)
729
  yt_dlp_available = True
 
731
  yt_dlp_available = False
732
 
733
  strategies = [
734
+ "Batch Processing",
735
  "Cookie Support",
736
  "User-Agent Rotation",
737
  "Smart Retry Logic",
738
  "Enhanced Headers",
739
+ "Concurrent Downloads",
740
+ "Progress Tracking"
741
  ]
742
 
743
  return HealthResponse(
 
745
  yt_dlp_available=yt_dlp_available,
746
  timestamp=datetime.now().isoformat(),
747
  cookie_file_exists=downloader.cookie_manager.cookie_file.exists(),
748
+ strategies_enabled=strategies,
749
+ batch_support=True
750
  )
751
 
752
+ # Single video endpoints
753
  @app.post("/video/info", response_model=Dict[str, Any])
754
  async def get_video_info(request: VideoInfoRequest):
755
  """Get video information with cookie support"""
 
788
  if not any(domain in url_str for domain in ['youtube.com', 'youtu.be']):
789
  raise HTTPException(status_code=400, detail="Invalid YouTube URL")
790
 
 
791
  loop = asyncio.get_event_loop()
792
  info = await loop.run_in_executor(
793
  executor,
 
801
  detail="Failed to get video information. Try uploading fresh cookies."
802
  )
803
 
 
804
  downloaded_file = await loop.run_in_executor(
805
  executor,
806
  downloader.download_video,
 
814
  file_size = os.path.getsize(downloaded_file)
815
  filename = os.path.basename(downloaded_file)
816
 
 
817
  background_tasks.add_task(cleanup_file, downloaded_file, delay=7200)
818
 
819
  return DownloadResponse(
820
  success=True,
821
+ message="Video downloaded successfully",
822
  filename=filename,
823
  file_size=file_size,
824
  video_info=VideoInfo(**info),
 
836
  logger.error(f"Error downloading video: {e}")
837
  raise HTTPException(status_code=500, detail=str(e))
838
 
839
+ # Batch endpoints
840
+ @app.post("/batch/info")
841
+ async def batch_get_video_info(request: BatchVideoInfoRequest):
842
+ """Get information for multiple videos"""
843
+ try:
844
+ if len(request.urls) > 50: # Limit batch size
845
+ raise HTTPException(status_code=400, detail="Maximum 50 URLs allowed per batch")
846
+
847
+ urls = [str(url) for url in request.urls]
848
+
849
+ # Validate URLs
850
+ for url in urls:
851
+ if not any(domain in url for domain in ['youtube.com', 'youtu.be']):
852
+ raise HTTPException(status_code=400, detail=f"Invalid YouTube URL: {url}")
853
+
854
+ results = await downloader.batch_get_info(urls, request.use_cookies)
855
+
856
+ success_count = sum(1 for r in results if r.success)
857
+
858
+ return {
859
+ "success": True,
860
+ "total_urls": len(urls),
861
+ "successful": success_count,
862
+ "failed": len(urls) - success_count,
863
+ "results": [r.dict() for r in results]
864
+ }
865
+
866
+ except HTTPException:
867
+ raise
868
+ except Exception as e:
869
+ logger.error(f"Error in batch info: {e}")
870
+ raise HTTPException(status_code=500, detail=str(e))
871
+
872
+ @app.post("/batch/download", response_model=BatchDownloadResponse)
873
+ async def batch_download_videos(request: BatchDownloadRequest, background_tasks: BackgroundTasks):
874
+ """Start batch download of multiple videos"""
875
+ try:
876
+ if len(request.urls) > 20: # Limit batch size for downloads
877
+ raise HTTPException(status_code=400, detail="Maximum 20 URLs allowed per batch download")
878
+
879
+ if request.max_concurrent > 5: # Limit concurrent downloads
880
+ raise HTTPException(status_code=400, detail="Maximum 5 concurrent downloads allowed")
881
+
882
+ urls = [str(url) for url in request.urls]
883
+
884
+ # Validate URLs
885
+ for url in urls:
886
+ if not any(domain in url for domain in ['youtube.com', 'youtu.be']):
887
+ raise HTTPException(status_code=400, detail=f"Invalid YouTube URL: {url}")
888
+
889
+ # Generate batch ID
890
+ batch_id = str(uuid.uuid4())
891
+
892
+ # Start batch download in background
893
+ background_tasks.add_task(
894
+ downloader.batch_download,
895
+ batch_id,
896
+ urls,
897
+ request.quality,
898
+ request.audio_only,
899
+ request.use_cookies,
900
+ request.max_concurrent
901
+ )
902
+
903
+ return BatchDownloadResponse(
904
+ batch_id=batch_id,
905
+ total_urls=len(urls),
906
+ status="started",
907
+ completed=0,
908
+ failed=0,
909
+ results=[]
910
+ )
911
+
912
+ except HTTPException:
913
+ raise
914
+ except Exception as e:
915
+ logger.error(f"Error starting batch download: {e}")
916
+ raise HTTPException(status_code=500, detail=str(e))
917
+
918
+ @app.get("/batch/status/{batch_id}", response_model=BatchStatus)
919
+ async def get_batch_status(batch_id: str):
920
+ """Get status of a batch download"""
921
+ if batch_id not in batch_status_store:
922
+ raise HTTPException(status_code=404, detail="Batch not found")
923
+
924
+ status = batch_status_store[batch_id]
925
+ return BatchStatus(**status)
926
+
927
  @app.get("/video/file/{filename}")
928
  async def download_file(filename: str):
929
  """Serve downloaded files"""