huuyfytryr commited on
Commit
ee2a4a8
·
1 Parent(s): 132f09c

Fix YouTube download SSL/EOF error using multi-attempt fallback loop

Browse files
Files changed (1) hide show
  1. app.py +90 -31
app.py CHANGED
@@ -298,42 +298,100 @@ def clip_youtube_video():
298
  except ImportError:
299
  has_curl_cffi = False
300
 
301
- download_cmd = [
302
- ytdlp_bin,
303
- # Quality: max 720p
304
- "-f", "bestvideo[height<=720][ext=mp4]+bestaudio[ext=m4a]/bestvideo[height<=720]+bestaudio/best[height<=720]/best",
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
- download_cmd.insert(3, "chrome")
326
- download_cmd.insert(3, "--impersonate")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
327
 
328
- result = subprocess.run(download_cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, timeout=480)
329
- stderr_text = result.stderr.decode('utf-8', errors='ignore')
330
 
331
- if result.returncode != 0 or not os.path.exists(raw_download_path) or os.path.getsize(raw_download_path) == 0:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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. Try again (it will retry without impersonation).'
335
  elif 'SSL' in stderr_text or 'EOF' in stderr_text:
336
- err = 'YouTube SSL error. Try a different video or try again in a few seconds.'
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 different video.'
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")