Fred808 commited on
Commit
07760d5
Β·
verified Β·
1 Parent(s): 9e08d56

Update main.py

Browse files
Files changed (1) hide show
  1. main.py +322 -172
main.py CHANGED
@@ -1,7 +1,7 @@
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
@@ -18,7 +18,7 @@ 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
@@ -31,11 +31,13 @@ logger = logging.getLogger(__name__)
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
@@ -61,13 +63,14 @@ 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,88 +85,99 @@ app.add_middleware(
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,7 +188,15 @@ class EnhancedYouTubeDownloader:
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()
@@ -195,28 +217,58 @@ class EnhancedYouTubeDownloader:
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)
@@ -240,23 +292,27 @@ class EnhancedYouTubeDownloader:
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,19 +321,15 @@ class EnhancedYouTubeDownloader:
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
 
@@ -296,12 +348,11 @@ class EnhancedYouTubeDownloader:
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
 
@@ -320,11 +371,15 @@ class EnhancedYouTubeDownloader:
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,7 +387,7 @@ class EnhancedYouTubeDownloader:
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
@@ -340,14 +395,14 @@ 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,7 +419,7 @@ async def read_root():
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,22 +431,18 @@ async def read_root():
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,6 +454,7 @@ async def read_root():
403
  display: inline-block;
404
  margin: 10px 5px;
405
  transition: transform 0.2s ease;
 
406
  }
407
  .btn:hover {
408
  transform: translateY(-2px);
@@ -415,57 +467,144 @@ async def read_root():
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,39 +612,44 @@ async def health_check():
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,7 +660,7 @@ async def get_video_info(request: VideoInfoRequest):
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']):
@@ -524,11 +668,16 @@ async def download_video(request: DownloadRequest, background_tasks: BackgroundT
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
@@ -537,19 +686,20 @@ async def download_video(request: DownloadRequest, background_tasks: BackgroundT
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,7 +708,7 @@ async def download_video(request: DownloadRequest, background_tasks: BackgroundT
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:
 
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
 
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
 
31
  # Pydantic models
32
  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
 
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
  )
86
 
87
  # Thread pool for background tasks
88
+ executor = ThreadPoolExecutor(max_workers=2)
89
 
90
+ class CookieManager:
91
+ """Manages cookie files and validation"""
92
 
93
+ def __init__(self, cookie_dir: str = None):
94
+ if cookie_dir is None:
95
+ if os.path.exists('/data'):
96
+ cookie_dir = '/data/cookies'
97
+ else:
98
+ cookie_dir = '/tmp/cookies'
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
99
 
100
+ self.cookie_dir = Path(cookie_dir)
101
+ self.cookie_dir.mkdir(parents=True, exist_ok=True)
102
+ self.cookie_file = self.cookie_dir / "youtube_cookies.txt"
103
 
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
+
126
+ logger.info(f"Cookies saved to {self.cookie_file}")
127
  return True
128
+
129
+ except Exception as e:
130
+ logger.error(f"Failed to save cookies: {e}")
131
+ return False
132
 
133
+ def validate_cookies(self) -> Dict[str, Any]:
134
+ """Validate existing cookie file"""
135
+ if not self.cookie_file.exists():
136
+ return {"valid": False, "reason": "Cookie file does not exist"}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
137
 
138
+ try:
139
+ with open(self.cookie_file, 'r', encoding='utf-8') as f:
140
+ content = f.read()
141
+
142
+ lines = content.strip().split('\n')
143
+ data_lines = [line for line in lines if line.strip() and not line.startswith('#')]
144
+
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
+
155
+ for line in data_lines:
156
+ for cookie in essential_cookies:
157
+ if cookie in line:
158
+ found_essential.append(cookie)
159
+
160
+ return {
161
+ "valid": True,
162
+ "total_lines": len(data_lines),
163
+ "youtube_cookies": len(youtube_cookies),
164
+ "essential_cookies": found_essential,
165
+ "file_size": self.cookie_file.stat().st_size,
166
+ "last_modified": datetime.fromtimestamp(self.cookie_file.stat().st_mtime).isoformat()
167
+ }
168
+
169
+ except Exception as e:
170
+ return {"valid": False, "reason": f"Error reading cookie file: {e}"}
171
+
172
+ def get_cookie_path(self) -> Optional[str]:
173
+ """Get path to cookie file if it exists and is valid"""
174
+ validation = self.validate_cookies()
175
+ if validation["valid"]:
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:
 
188
 
189
  self.download_dir = Path(download_dir)
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',
197
+ 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
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()
 
217
  logger.error(f"Failed to install yt-dlp: {e}")
218
  raise RuntimeError("Could not install yt-dlp")
219
 
220
+ def _build_command(self, base_cmd: List[str], use_cookies: bool = False) -> List[str]:
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:
231
+ cmd.extend(['--cookies', cookie_path])
232
+ logger.info("Using cookie file for authentication")
233
+ else:
234
+ logger.warning("Cookies requested but no valid cookie file found")
235
+ cmd.extend(['--no-cookies'])
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',
243
+ '--fragment-retries', '5',
244
+ '--socket-timeout', '30',
245
+ '--no-check-certificates',
246
+ '--geo-bypass',
247
+ '--add-header', 'Accept:text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
248
+ '--add-header', 'Accept-Language:en-US,en;q=0.5',
249
+ '--add-header', 'Accept-Encoding:gzip, deflate',
250
+ '--add-header', 'DNT:1',
251
+ '--add-header', 'Connection:keep-alive',
252
+ '--add-header', 'Upgrade-Insecure-Requests:1',
253
+ ])
254
+
255
+ return cmd
256
+
257
+ def get_video_info(self, url: str, use_cookies: bool = False, retry_count: int = 0) -> Optional[Dict[str, Any]]:
258
+ """Get video information with cookie support"""
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})")
272
 
273
  result = subprocess.run(cmd, capture_output=True, text=True, check=True, timeout=60)
274
  video_info = json.loads(result.stdout)
 
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
296
+ logger.warning(f"Rate limited, waiting {wait_time}s before retry {retry_count + 1}")
297
  time.sleep(wait_time)
298
+ return self.get_video_info(url, use_cookies, retry_count + 1)
 
 
 
299
 
300
  elif "sign in" in error_msg or "bot" in error_msg:
301
+ if not use_cookies and retry_count == 0:
302
+ logger.warning("Bot detection triggered, retrying with cookies if available")
303
+ return self.get_video_info(url, True, retry_count + 1)
304
+ elif retry_count < max_retries:
305
+ wait_time = (retry_count + 1) * 60
306
+ logger.warning(f"Bot detection, waiting {wait_time}s before retry {retry_count + 1}")
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
 
321
  logger.error(f"Error processing video info: {e}")
322
  if retry_count < max_retries:
323
  time.sleep(10)
324
+ return self.get_video_info(url, use_cookies, retry_count + 1)
325
  return None
326
 
327
  def download_video(self, url: str, quality: str = "best",
328
+ audio_only: bool = False, use_cookies: bool = False,
329
+ retry_count: int = 0) -> Optional[str]:
330
+ """Download video with cookie support"""
331
  max_retries = 2
332
 
 
 
 
 
 
333
  try:
334
  base_cmd = ['yt-dlp']
335
 
 
348
  else:
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})")
356
 
357
  result = subprocess.run(cmd, capture_output=True, text=True, check=True, timeout=300)
358
 
 
371
 
372
  if ("429" in error_msg or "too many requests" in error_msg or
373
  "sign in" in error_msg or "bot" in error_msg):
374
+
375
+ if not use_cookies and retry_count == 0:
376
+ logger.warning("Download blocked, retrying with cookies if available")
377
+ return self.download_video(url, quality, audio_only, True, retry_count + 1)
378
+ elif retry_count < max_retries:
379
  wait_time = (retry_count + 1) * 60
380
  logger.warning(f"Download blocked, waiting {wait_time}s before retry {retry_count + 1}")
381
  time.sleep(wait_time)
382
+ return self.download_video(url, quality, audio_only, use_cookies, retry_count + 1)
383
 
384
  logger.error(f"Download failed: {e.stderr}")
385
  return None
 
387
  except subprocess.TimeoutExpired:
388
  logger.error("Download timeout")
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
 
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
  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 {
 
431
  margin-bottom: 10px;
432
  font-size: 2.5em;
433
  }
434
+ .section {
 
 
 
 
435
  background: #f8f9fa;
436
  border-radius: 8px;
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);
 
454
  display: inline-block;
455
  margin: 10px 5px;
456
  transition: transform 0.2s ease;
457
+ cursor: pointer;
458
  }
459
  .btn:hover {
460
  transform: translateY(-2px);
 
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
  """
562
  return HTMLResponse(content=html_content)
563
 
564
+ @app.post("/upload-cookies")
565
+ async def upload_cookies(cookie_file: UploadFile = File(...)):
566
+ """Upload and validate cookie file"""
567
+ try:
568
+ if not cookie_file.filename.endswith('.txt'):
569
+ raise HTTPException(status_code=400, detail="Cookie file must be a .txt file")
570
+
571
+ content = await cookie_file.read()
572
+ cookie_content = content.decode('utf-8')
573
+
574
+ success = downloader.cookie_manager.save_cookies(cookie_content)
575
+
576
+ if success:
577
+ validation = downloader.cookie_manager.validate_cookies()
578
+ return {
579
+ "success": True,
580
+ "message": f"Cookies uploaded successfully. Found {validation.get('youtube_cookies', 0)} YouTube cookies.",
581
+ "validation": validation
582
+ }
583
+ else:
584
+ return {
585
+ "success": False,
586
+ "message": "Failed to save cookie file. Please check the format."
587
+ }
588
+
589
+ except Exception as e:
590
+ logger.error(f"Error uploading cookies: {e}")
591
+ return {
592
+ "success": False,
593
+ "message": f"Error processing cookie file: {str(e)}"
594
+ }
595
+
596
+ @app.get("/cookie-status")
597
+ async def cookie_status():
598
+ """Get current cookie status"""
599
+ validation = downloader.cookie_manager.validate_cookies()
600
+ return {
601
+ "cookie_file_exists": downloader.cookie_manager.cookie_file.exists(),
602
+ "validation": validation
603
+ }
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
  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(
624
  status="healthy" if yt_dlp_available else "unhealthy",
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"""
634
  try:
635
  url_str = str(request.url)
636
  if not any(domain in url_str for domain in ['youtube.com', 'youtu.be']):
637
  raise HTTPException(status_code=400, detail="Invalid YouTube URL")
638
 
 
639
  loop = asyncio.get_event_loop()
640
+ info = await loop.run_in_executor(
641
+ executor,
642
+ downloader.get_video_info,
643
+ url_str,
644
+ request.use_cookies
645
+ )
646
 
647
  if info:
648
  return {"success": True, "info": info}
649
  else:
650
  raise HTTPException(
651
  status_code=503,
652
+ detail="Failed to get video information. Try uploading fresh cookies or wait before retrying."
653
  )
654
 
655
  except HTTPException:
 
660
 
661
  @app.post("/video/download", response_model=DownloadResponse)
662
  async def download_video(request: DownloadRequest, background_tasks: BackgroundTasks):
663
+ """Download video with cookie support"""
664
  try:
665
  url_str = str(request.url)
666
  if not any(domain in url_str for domain in ['youtube.com', 'youtu.be']):
 
668
 
669
  # Get video info first
670
  loop = asyncio.get_event_loop()
671
+ info = await loop.run_in_executor(
672
+ executor,
673
+ downloader.get_video_info,
674
+ url_str,
675
+ request.use_cookies
676
+ )
677
  if not info:
678
  raise HTTPException(
679
  status_code=503,
680
+ detail="Failed to get video information. Try uploading fresh cookies."
681
  )
682
 
683
  # Download the video
 
686
  downloader.download_video,
687
  url_str,
688
  request.quality,
689
+ request.audio_only,
690
+ request.use_cookies
691
  )
692
 
693
  if downloaded_file:
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),
 
708
  else:
709
  raise HTTPException(
710
  status_code=503,
711
+ detail="Failed to download video. Try uploading fresh cookies or wait before retrying."
712
  )
713
 
714
  except HTTPException: