Spaces:
Running
Running
Commit ·
ee2a4a8
1
Parent(s): 132f09c
Fix YouTube download SSL/EOF error using multi-attempt fallback loop
Browse files
app.py
CHANGED
|
@@ -298,42 +298,100 @@ def clip_youtube_video():
|
|
| 298 |
except ImportError:
|
| 299 |
has_curl_cffi = False
|
| 300 |
|
| 301 |
-
|
| 302 |
-
|
| 303 |
-
|
| 304 |
-
|
| 305 |
-
"--merge-output-format", "mp4",
|
| 306 |
-
# Player client: ios/android work for most videos without embedding restrictions
|
| 307 |
-
# tv_embedded is intentionally excluded — it fails for non-embeddable videos
|
| 308 |
-
"--extractor-args", "youtube:player_client=ios,android,mweb,web",
|
| 309 |
-
# Geo-bypass
|
| 310 |
-
"--geo-bypass",
|
| 311 |
-
# SSL resilience
|
| 312 |
-
"--no-check-certificates",
|
| 313 |
-
"--socket-timeout", "60",
|
| 314 |
-
# Retry logic
|
| 315 |
-
"--retries", "10",
|
| 316 |
-
"--fragment-retries", "10",
|
| 317 |
-
"--retry-sleep", "exp=1:30",
|
| 318 |
-
# Output
|
| 319 |
-
"-o", raw_download_path,
|
| 320 |
-
url
|
| 321 |
-
]
|
| 322 |
-
|
| 323 |
-
# Only add --impersonate if curl-cffi is installed
|
| 324 |
if has_curl_cffi:
|
| 325 |
-
|
| 326 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 327 |
|
| 328 |
-
|
| 329 |
-
stderr_text =
|
| 330 |
|
| 331 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 332 |
# Check impersonation error FIRST before generic "unavailable"
|
| 333 |
if 'impersonate' in stderr_text.lower() or ('curl' in stderr_text.lower() and 'cffi' in stderr_text.lower()):
|
| 334 |
-
err = 'Browser impersonation library missing.
|
| 335 |
elif 'SSL' in stderr_text or 'EOF' in stderr_text:
|
| 336 |
-
err = 'YouTube SSL error
|
| 337 |
elif 'Private' in stderr_text or 'members-only' in stderr_text:
|
| 338 |
err = 'This video is private or members-only.'
|
| 339 |
elif 'Sign in' in stderr_text:
|
|
@@ -341,12 +399,13 @@ def clip_youtube_video():
|
|
| 341 |
elif 'removed' in stderr_text or 'unavailable' in stderr_text or 'not available' in stderr_text:
|
| 342 |
err = 'This video is unavailable or has been removed from YouTube.'
|
| 343 |
elif 'bot' in stderr_text.lower() or 'detected' in stderr_text.lower():
|
| 344 |
-
err = 'YouTube detected bot activity. Try a
|
| 345 |
else:
|
| 346 |
# Show raw error so we can diagnose
|
| 347 |
err = f'yt-dlp error: {stderr_text[-800:]}' if stderr_text else 'Unknown download error'
|
| 348 |
return jsonify({'success': False, 'error': f'Download failed: {err}'}), 500
|
| 349 |
|
|
|
|
| 350 |
|
| 351 |
# 2. Slice downloaded video
|
| 352 |
temp_clips_dir = os.path.join(UPLOAD_FOLDER, f"{job_id}_slices")
|
|
|
|
| 298 |
except ImportError:
|
| 299 |
has_curl_cffi = False
|
| 300 |
|
| 301 |
+
# Define download attempts with different fallback strategies
|
| 302 |
+
attempts = []
|
| 303 |
+
|
| 304 |
+
# Attempt 1: Impersonate chrome with ios/android/mweb/web player client (if curl_cffi is available)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 305 |
if has_curl_cffi:
|
| 306 |
+
attempts.append({
|
| 307 |
+
"impersonate": "chrome",
|
| 308 |
+
"player_client": "ios,android,mweb,web"
|
| 309 |
+
})
|
| 310 |
+
|
| 311 |
+
# Attempt 2: No impersonate (standard urllib/requests), with player_client=default,-tv
|
| 312 |
+
attempts.append({
|
| 313 |
+
"impersonate": None,
|
| 314 |
+
"player_client": "default,-tv"
|
| 315 |
+
})
|
| 316 |
+
|
| 317 |
+
# Attempt 3: No impersonate, standard ios/android/mweb/web player client
|
| 318 |
+
attempts.append({
|
| 319 |
+
"impersonate": None,
|
| 320 |
+
"player_client": "ios,android,mweb,web"
|
| 321 |
+
})
|
| 322 |
+
|
| 323 |
+
# Attempt 4: Absolute fallback (default yt-dlp behaviour)
|
| 324 |
+
attempts.append({
|
| 325 |
+
"impersonate": None,
|
| 326 |
+
"player_client": None
|
| 327 |
+
})
|
| 328 |
|
| 329 |
+
success = False
|
| 330 |
+
stderr_text = ""
|
| 331 |
|
| 332 |
+
for idx, attempt in enumerate(attempts, 1):
|
| 333 |
+
print(f"Download attempt {idx}/{len(attempts)}: impersonate={attempt['impersonate']}, player_client={attempt['player_client']}")
|
| 334 |
+
|
| 335 |
+
# Clean up partial download files from previous attempts
|
| 336 |
+
if os.path.exists(raw_download_path):
|
| 337 |
+
try:
|
| 338 |
+
os.remove(raw_download_path)
|
| 339 |
+
except Exception:
|
| 340 |
+
pass
|
| 341 |
+
partial_path = raw_download_path + ".part"
|
| 342 |
+
if os.path.exists(partial_path):
|
| 343 |
+
try:
|
| 344 |
+
os.remove(partial_path)
|
| 345 |
+
except Exception:
|
| 346 |
+
pass
|
| 347 |
+
|
| 348 |
+
cmd = [
|
| 349 |
+
ytdlp_bin,
|
| 350 |
+
"-f", "bestvideo[height<=720][ext=mp4]+bestaudio[ext=m4a]/bestvideo[height<=720]+bestaudio/best[height<=720]/best",
|
| 351 |
+
"--merge-output-format", "mp4",
|
| 352 |
+
"--geo-bypass",
|
| 353 |
+
"--no-check-certificates",
|
| 354 |
+
"--socket-timeout", "40",
|
| 355 |
+
"--retries", "3",
|
| 356 |
+
"--fragment-retries", "5",
|
| 357 |
+
"-o", raw_download_path
|
| 358 |
+
]
|
| 359 |
+
|
| 360 |
+
if attempt["impersonate"]:
|
| 361 |
+
cmd.extend(["--impersonate", attempt["impersonate"]])
|
| 362 |
+
|
| 363 |
+
if attempt["player_client"]:
|
| 364 |
+
cmd.extend(["--extractor-args", f"youtube:player_client={attempt['player_client']}"])
|
| 365 |
+
|
| 366 |
+
cmd.append(url)
|
| 367 |
+
|
| 368 |
+
try:
|
| 369 |
+
# Run the command with 5 minutes (300s) timeout per attempt
|
| 370 |
+
result = subprocess.run(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, timeout=300)
|
| 371 |
+
stderr_text = result.stderr.decode('utf-8', errors='ignore')
|
| 372 |
+
|
| 373 |
+
# Check if download succeeded and file exists and has size
|
| 374 |
+
if result.returncode == 0 and os.path.exists(raw_download_path) and os.path.getsize(raw_download_path) > 0:
|
| 375 |
+
success = True
|
| 376 |
+
print(f"Download succeeded on attempt {idx}!")
|
| 377 |
+
break
|
| 378 |
+
else:
|
| 379 |
+
print(f"Attempt {idx} failed with return code {result.returncode}. Stderr: {stderr_text[-300:]}")
|
| 380 |
+
except subprocess.TimeoutExpired:
|
| 381 |
+
print(f"Attempt {idx} timed out.")
|
| 382 |
+
stderr_text = "Download timed out."
|
| 383 |
+
continue
|
| 384 |
+
except Exception as e:
|
| 385 |
+
print(f"Attempt {idx} encountered exception: {e}")
|
| 386 |
+
stderr_text = str(e)
|
| 387 |
+
continue
|
| 388 |
+
|
| 389 |
+
if not success:
|
| 390 |
# Check impersonation error FIRST before generic "unavailable"
|
| 391 |
if 'impersonate' in stderr_text.lower() or ('curl' in stderr_text.lower() and 'cffi' in stderr_text.lower()):
|
| 392 |
+
err = 'Browser impersonation library missing. Please try a different video or link.'
|
| 393 |
elif 'SSL' in stderr_text or 'EOF' in stderr_text:
|
| 394 |
+
err = 'YouTube SSL/Connection error (often caused by YouTube rate limits on hosting servers). Try again or try a different video.'
|
| 395 |
elif 'Private' in stderr_text or 'members-only' in stderr_text:
|
| 396 |
err = 'This video is private or members-only.'
|
| 397 |
elif 'Sign in' in stderr_text:
|
|
|
|
| 399 |
elif 'removed' in stderr_text or 'unavailable' in stderr_text or 'not available' in stderr_text:
|
| 400 |
err = 'This video is unavailable or has been removed from YouTube.'
|
| 401 |
elif 'bot' in stderr_text.lower() or 'detected' in stderr_text.lower():
|
| 402 |
+
err = 'YouTube detected bot activity from the hosting server. Try again in a few minutes.'
|
| 403 |
else:
|
| 404 |
# Show raw error so we can diagnose
|
| 405 |
err = f'yt-dlp error: {stderr_text[-800:]}' if stderr_text else 'Unknown download error'
|
| 406 |
return jsonify({'success': False, 'error': f'Download failed: {err}'}), 500
|
| 407 |
|
| 408 |
+
|
| 409 |
|
| 410 |
# 2. Slice downloaded video
|
| 411 |
temp_clips_dir = os.path.join(UPLOAD_FOLDER, f"{job_id}_slices")
|