Fred808 commited on
Commit
3062d44
·
verified ·
1 Parent(s): 545e6d0

Update main.py

Browse files
Files changed (1) hide show
  1. main.py +508 -122
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,13 +13,15 @@ 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
19
  from concurrent.futures import ThreadPoolExecutor
 
20
 
21
  from fastapi import FastAPI, HTTPException, BackgroundTasks, Request, UploadFile, File, Form
22
- from fastapi.responses import FileResponse, HTMLResponse
23
  from fastapi.middleware.cors import CORSMiddleware
24
  from pydantic import BaseModel, HttpUrl
25
  import uvicorn
@@ -31,13 +33,24 @@ logger = logging.getLogger(__name__)
31
  # Pydantic models
32
  class VideoInfoRequest(BaseModel):
33
  url: HttpUrl
34
- use_cookies: bool = None # Changed to None to allow auto-detection
 
 
 
 
35
 
36
  class DownloadRequest(BaseModel):
37
  url: HttpUrl
38
  quality: str = "best"
39
  audio_only: bool = False
40
- use_cookies: bool = None # Changed to None to allow auto-detection
 
 
 
 
 
 
 
41
 
42
  class VideoInfo(BaseModel):
43
  title: str
@@ -51,6 +64,12 @@ class VideoInfo(BaseModel):
51
  thumbnail: str
52
  webpage_url: str
53
 
 
 
 
 
 
 
54
  class DownloadResponse(BaseModel):
55
  success: bool
56
  message: str
@@ -59,18 +78,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.1.0",
74
  docs_url="/docs",
75
  redoc_url="/redoc"
76
  )
@@ -85,7 +124,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 +146,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 +182,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
 
@@ -175,13 +209,9 @@ class CookieManager:
175
  if validation["valid"]:
176
  return str(self.cookie_file)
177
  return None
178
-
179
- def has_valid_cookies(self) -> bool:
180
- """Check if valid cookies are available"""
181
- return self.get_cookie_path() is not None
182
 
183
- class EnhancedYouTubeDownloader:
184
- """Enhanced YouTube downloader with cookie support and troubleshooting"""
185
 
186
  def __init__(self, download_dir: str = None):
187
  if download_dir is None:
@@ -194,7 +224,6 @@ class EnhancedYouTubeDownloader:
194
  self.download_dir.mkdir(parents=True, exist_ok=True)
195
  self.cookie_manager = CookieManager()
196
 
197
- # User agents for rotation
198
  self.user_agents = [
199
  'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
200
  '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',
@@ -202,14 +231,12 @@ class EnhancedYouTubeDownloader:
202
  'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:121.0) Gecko/20100101 Firefox/121.0',
203
  ]
204
 
205
- # Ensure yt-dlp is available
206
  self._ensure_ytdlp_available()
207
 
208
  def _ensure_ytdlp_available(self):
209
  """Ensure yt-dlp is available, install if necessary"""
210
  try:
211
- subprocess.run(['yt-dlp', '--version'],
212
- capture_output=True, check=True)
213
  logger.info("yt-dlp is available")
214
  except (subprocess.CalledProcessError, FileNotFoundError):
215
  logger.info("Installing yt-dlp...")
@@ -221,24 +248,13 @@ class EnhancedYouTubeDownloader:
221
  logger.error(f"Failed to install yt-dlp: {e}")
222
  raise RuntimeError("Could not install yt-dlp")
223
 
224
- def _should_use_cookies(self, use_cookies: Optional[bool]) -> bool:
225
- """Determine whether to use cookies based on request and availability"""
226
- if use_cookies is None:
227
- # Auto-detect: use cookies if available
228
- return self.cookie_manager.has_valid_cookies()
229
- return use_cookies and self.cookie_manager.has_valid_cookies()
230
-
231
- def _build_command(self, base_cmd: List[str], use_cookies: Optional[bool] = None) -> List[str]:
232
  """Build yt-dlp command with proper options"""
233
  cmd = base_cmd.copy()
234
 
235
- # Add user agent
236
  cmd.extend(['--user-agent', random.choice(self.user_agents)])
237
 
238
- # Cookie handling with improved logic
239
- should_use_cookies = self._should_use_cookies(use_cookies)
240
-
241
- if should_use_cookies:
242
  cookie_path = self.cookie_manager.get_cookie_path()
243
  if cookie_path:
244
  cmd.extend(['--cookies', cookie_path])
@@ -249,7 +265,6 @@ class EnhancedYouTubeDownloader:
249
  else:
250
  cmd.extend(['--no-cookies'])
251
 
252
- # Enhanced options for better success rate
253
  cmd.extend([
254
  '--sleep-interval', str(random.randint(1, 3)),
255
  '--retries', '5',
@@ -265,23 +280,20 @@ class EnhancedYouTubeDownloader:
265
  '--add-header', 'Upgrade-Insecure-Requests:1',
266
  ])
267
 
268
- return cmd, should_use_cookies
269
 
270
  def get_video_info(self, url: str, use_cookies: Optional[bool] = None, retry_count: int = 0) -> Optional[Dict[str, Any]]:
271
  """Get video information with cookie support"""
272
  max_retries = 3
273
 
 
 
 
274
  try:
275
- base_cmd = [
276
- 'yt-dlp',
277
- '--dump-json',
278
- '--no-download',
279
- str(url)
280
- ]
281
-
282
- cmd, actual_cookie_usage = self._build_command(base_cmd, use_cookies)
283
 
284
- logger.info(f"Getting video info (attempt {retry_count + 1}, cookies: {actual_cookie_usage})")
285
 
286
  result = subprocess.run(cmd, capture_output=True, text=True, check=True, timeout=60)
287
  video_info = json.loads(result.stdout)
@@ -302,7 +314,6 @@ class EnhancedYouTubeDownloader:
302
  except subprocess.CalledProcessError as e:
303
  error_msg = e.stderr.lower() if e.stderr else ""
304
 
305
- # Handle specific error cases
306
  if "429" in error_msg or "too many requests" in error_msg:
307
  if retry_count < max_retries:
308
  wait_time = (retry_count + 1) * 30
@@ -311,9 +322,8 @@ class EnhancedYouTubeDownloader:
311
  return self.get_video_info(url, use_cookies, retry_count + 1)
312
 
313
  elif "sign in" in error_msg or "bot" in error_msg:
314
- # If not using cookies and we have them available, try with cookies
315
- if not self._should_use_cookies(use_cookies) and self.cookie_manager.has_valid_cookies() and retry_count == 0:
316
- logger.warning("Bot detection triggered, retrying with cookies")
317
  return self.get_video_info(url, True, retry_count + 1)
318
  elif retry_count < max_retries:
319
  wait_time = (retry_count + 1) * 60
@@ -321,13 +331,6 @@ class EnhancedYouTubeDownloader:
321
  time.sleep(wait_time)
322
  return self.get_video_info(url, use_cookies, retry_count + 1)
323
 
324
- elif "cookies" in error_msg and self._should_use_cookies(use_cookies):
325
- logger.error("Cookie-related error - cookies may be expired or invalid")
326
- # Try without cookies as fallback
327
- if retry_count == 0:
328
- logger.info("Retrying without cookies as fallback")
329
- return self.get_video_info(url, False, retry_count + 1)
330
-
331
  logger.error(f"Failed to get video info: {e.stderr}")
332
  return None
333
 
@@ -344,35 +347,34 @@ class EnhancedYouTubeDownloader:
344
  """Download video with cookie support"""
345
  max_retries = 2
346
 
 
 
 
347
  try:
348
- base_cmd = ['yt-dlp']
349
 
350
- # Set output directory and filename template
351
- output_template = str(self.download_dir / "%(title)s.%(ext)s")
352
- base_cmd.extend(['-o', output_template])
353
 
354
- # Set format/quality
355
  if audio_only:
356
- base_cmd.extend(['-f', 'bestaudio/best'])
357
  else:
358
  if quality == "best":
359
- base_cmd.extend(['-f', 'best[height<=720]'])
360
  elif quality == "worst":
361
- base_cmd.extend(['-f', 'worst'])
362
  else:
363
- base_cmd.extend(['-f', quality])
364
 
365
  base_cmd.append(str(url))
 
366
 
367
- cmd, actual_cookie_usage = self._build_command(base_cmd, use_cookies)
368
-
369
- logger.info(f"Downloading video (attempt {retry_count + 1}, cookies: {actual_cookie_usage})")
370
 
371
  result = subprocess.run(cmd, capture_output=True, text=True, check=True, timeout=300)
372
 
373
  logger.info("Download completed successfully")
374
 
375
- # Find the downloaded file
376
  downloaded_files = [f for f in self.download_dir.glob("*") if f.is_file()]
377
  if downloaded_files:
378
  latest_file = max(downloaded_files, key=os.path.getctime)
@@ -386,9 +388,8 @@ class EnhancedYouTubeDownloader:
386
  if ("429" in error_msg or "too many requests" in error_msg or
387
  "sign in" in error_msg or "bot" in error_msg):
388
 
389
- # If not using cookies and we have them available, try with cookies
390
- if not self._should_use_cookies(use_cookies) and self.cookie_manager.has_valid_cookies() and retry_count == 0:
391
- logger.warning("Download blocked, retrying with cookies")
392
  return self.download_video(url, quality, audio_only, True, retry_count + 1)
393
  elif retry_count < max_retries:
394
  wait_time = (retry_count + 1) * 60
@@ -404,20 +405,194 @@ class EnhancedYouTubeDownloader:
404
  if retry_count < max_retries:
405
  return self.download_video(url, quality, audio_only, use_cookies, retry_count + 1)
406
  return None
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
407
 
408
  # Global downloader instance
409
- downloader = EnhancedYouTubeDownloader()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
410
 
411
  @app.get("/", response_class=HTMLResponse)
412
  async def read_root():
413
- """Serve the main HTML interface with cookie upload"""
414
  html_content = """
415
  <!DOCTYPE html>
416
  <html lang="en">
417
  <head>
418
  <meta charset="UTF-8">
419
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
420
- <title>Cookie-Enhanced YouTube Downloader</title>
421
  <style>
422
  body {
423
  font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
@@ -434,7 +609,7 @@ async def read_root():
434
  border-radius: 15px;
435
  box-shadow: 0 20px 40px rgba(0,0,0,0.1);
436
  padding: 40px;
437
- max-width: 900px;
438
  width: 100%;
439
  }
440
  .header {
@@ -474,6 +649,17 @@ async def read_root():
474
  .btn:hover {
475
  transform: translateY(-2px);
476
  }
 
 
 
 
 
 
 
 
 
 
 
477
  .warning {
478
  background: #fff3cd;
479
  border: 1px solid #ffeaa7;
@@ -490,14 +676,6 @@ async def read_root():
490
  margin: 20px 0;
491
  color: #155724;
492
  }
493
- .info {
494
- background: #d1ecf1;
495
- border: 1px solid #bee5eb;
496
- border-radius: 5px;
497
- padding: 15px;
498
- margin: 20px 0;
499
- color: #0c5460;
500
- }
501
  input[type="file"] {
502
  margin: 10px 0;
503
  }
@@ -506,13 +684,8 @@ async def read_root():
506
  <body>
507
  <div class="container">
508
  <div class="header">
509
- <h1>🍪 Cookie-Enhanced YouTube Downloader</h1>
510
- <p>Upload your YouTube cookies for better success rates</p>
511
- </div>
512
-
513
- <div class="info">
514
- <strong>🔄 Auto-Cookie Detection:</strong> This version automatically uses cookies when available.
515
- You no longer need to manually enable cookies in API requests!
516
  </div>
517
 
518
  <div class="warning">
@@ -538,6 +711,59 @@ async def read_root():
538
  <div id="uploadResult"></div>
539
  </div>
540
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
541
  <div class="section">
542
  <h3>🔍 How to Export Cookies</h3>
543
  <ol>
@@ -549,7 +775,7 @@ async def read_root():
549
  </div>
550
 
551
  <div style="text-align: center;">
552
- <a href="/docs" class="btn">📖 API Documentation</a>
553
  <a href="/health" class="btn">🏥 Health Check</a>
554
  <a href="/cookie-status" class="btn">🍪 Cookie Status</a>
555
  </div>
@@ -605,7 +831,7 @@ async def upload_cookies(cookie_file: UploadFile = File(...)):
605
  validation = downloader.cookie_manager.validate_cookies()
606
  return {
607
  "success": True,
608
- "message": f"Cookies uploaded successfully. Found {validation.get('youtube_cookies', 0)} YouTube cookies. Auto-detection enabled.",
609
  "validation": validation
610
  }
611
  else:
@@ -627,14 +853,12 @@ async def cookie_status():
627
  validation = downloader.cookie_manager.validate_cookies()
628
  return {
629
  "cookie_file_exists": downloader.cookie_manager.cookie_file.exists(),
630
- "has_valid_cookies": downloader.cookie_manager.has_valid_cookies(),
631
- "auto_detection_enabled": True,
632
  "validation": validation
633
  }
634
 
635
  @app.get("/health", response_model=HealthResponse)
636
  async def health_check():
637
- """Enhanced health check with cookie information"""
638
  try:
639
  subprocess.run(['yt-dlp', '--version'], capture_output=True, check=True)
640
  yt_dlp_available = True
@@ -642,13 +866,14 @@ async def health_check():
642
  yt_dlp_available = False
643
 
644
  strategies = [
645
- "Auto Cookie Detection",
646
  "Cookie Support",
647
  "User-Agent Rotation",
648
  "Smart Retry Logic",
649
  "Enhanced Headers",
650
- "Timeout Handling",
651
- "Automatic Cookie Fallback"
 
652
  ]
653
 
654
  return HealthResponse(
@@ -656,12 +881,14 @@ async def health_check():
656
  yt_dlp_available=yt_dlp_available,
657
  timestamp=datetime.now().isoformat(),
658
  cookie_file_exists=downloader.cookie_manager.cookie_file.exists(),
659
- strategies_enabled=strategies
 
660
  )
661
 
 
662
  @app.post("/video/info", response_model=Dict[str, Any])
663
  async def get_video_info(request: VideoInfoRequest):
664
- """Get video information with automatic cookie detection"""
665
  try:
666
  url_str = str(request.url)
667
  if not any(domain in url_str for domain in ['youtube.com', 'youtu.be']):
@@ -672,15 +899,11 @@ async def get_video_info(request: VideoInfoRequest):
672
  executor,
673
  downloader.get_video_info,
674
  url_str,
675
- request.use_cookies # This can be None for auto-detection
676
  )
677
 
678
  if info:
679
- return {
680
- "success": True,
681
- "info": info,
682
- "cookies_used": downloader._should_use_cookies(request.use_cookies)
683
- }
684
  else:
685
  raise HTTPException(
686
  status_code=503,
@@ -695,19 +918,18 @@ async def get_video_info(request: VideoInfoRequest):
695
 
696
  @app.post("/video/download", response_model=DownloadResponse)
697
  async def download_video(request: DownloadRequest, background_tasks: BackgroundTasks):
698
- """Download video with automatic cookie detection"""
699
  try:
700
  url_str = str(request.url)
701
  if not any(domain in url_str for domain in ['youtube.com', 'youtu.be']):
702
  raise HTTPException(status_code=400, detail="Invalid YouTube URL")
703
 
704
- # Get video info first
705
  loop = asyncio.get_event_loop()
706
  info = await loop.run_in_executor(
707
  executor,
708
  downloader.get_video_info,
709
  url_str,
710
- request.use_cookies # This can be None for auto-detection
711
  )
712
  if not info:
713
  raise HTTPException(
@@ -715,28 +937,24 @@ async def download_video(request: DownloadRequest, background_tasks: BackgroundT
715
  detail="Failed to get video information. Try uploading fresh cookies."
716
  )
717
 
718
- # Download the video
719
  downloaded_file = await loop.run_in_executor(
720
  executor,
721
  downloader.download_video,
722
  url_str,
723
  request.quality,
724
  request.audio_only,
725
- request.use_cookies # This can be None for auto-detection
726
  )
727
 
728
  if downloaded_file:
729
  file_size = os.path.getsize(downloaded_file)
730
  filename = os.path.basename(downloaded_file)
731
 
732
- # Schedule cleanup after 2 hours
733
  background_tasks.add_task(cleanup_file, downloaded_file, delay=7200)
734
 
735
- cookies_used = downloader._should_use_cookies(request.use_cookies)
736
-
737
  return DownloadResponse(
738
  success=True,
739
- message=f"Video downloaded successfully {'with' if cookies_used else 'without'} cookies",
740
  filename=filename,
741
  file_size=file_size,
742
  video_info=VideoInfo(**info),
@@ -754,6 +972,174 @@ async def download_video(request: DownloadRequest, background_tasks: BackgroundT
754
  logger.error(f"Error downloading video: {e}")
755
  raise HTTPException(status_code=500, detail=str(e))
756
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
757
  @app.get("/video/file/{filename}")
758
  async def download_file(filename: str):
759
  """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
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
  # 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
  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
  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
  )
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"""
 
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
 
 
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
 
 
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:
 
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',
 
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
  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])
 
265
  else:
266
  cmd.extend(['--no-cookies'])
267
 
 
268
  cmd.extend([
269
  '--sleep-interval', str(random.randint(1, 3)),
270
  '--retries', '5',
 
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
  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
 
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
 
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
336
 
 
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)
 
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
 
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
  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 {
 
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;
 
676
  margin: 20px 0;
677
  color: #155724;
678
  }
 
 
 
 
 
 
 
 
679
  input[type="file"] {
680
  margin: 10px 0;
681
  }
 
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">
 
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>
 
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>
 
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:
 
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
  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(
 
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']):
 
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,
 
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(
 
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),
 
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"""