feat: integrate custom yt-dl solver for YouTube downloads

This commit is contained in:
Renato
2026-07-02 16:54:35 +02:00
parent c7c3c4b74a
commit 5d8cfde527
+45 -112
View File
@@ -457,7 +457,6 @@ def get_video_resolution(video_path):
cap.release()
return width, height
def sanitize_filename(filename):
"""Remove invalid characters from filename."""
filename = re.sub(r'[<>:"/\\|?*#]', '', filename)
@@ -467,131 +466,65 @@ def sanitize_filename(filename):
def download_youtube_video(url, output_dir="."):
"""
Downloads a YouTube video using yt-dlp.
Returns the path to the downloaded video and the video title.
Downloads a YouTube video using the custom fixed yt-dl wrapper in /home/ren/yt/yt-dl.
"""
print(f"🔍 Debug: yt-dlp version: {yt_dlp.version.__version__}")
print("📥 Downloading video from YouTube...")
import subprocess
import json
import time
import sys
print("📥 Downloading video from YouTube using custom solver...")
step_start_time = time.time()
cookies_path = '/app/cookies.txt'
cookies_env = os.environ.get("YOUTUBE_COOKIES")
if cookies_env:
print("🍪 Found YOUTUBE_COOKIES env var, creating cookies file inside container...")
try:
with open(cookies_path, 'w') as f:
f.write(cookies_env)
if os.path.exists(cookies_path):
print(f" Debug: Cookies file created. Size: {os.path.getsize(cookies_path)} bytes")
with open(cookies_path, 'r') as f:
content = f.read(100)
print(f" Debug: First 100 chars of cookie file: {content}")
except Exception as e:
print(f"⚠️ Failed to write cookies file: {e}")
cookies_path = None
else:
cookies_path = None
print("⚠️ YOUTUBE_COOKIES env var not found.")
# Ensure output_dir is absolute path
abs_output_dir = os.path.abspath(output_dir)
# Common yt-dlp options to work around YouTube bot detection.
# extractor_args tries multiple player clients in order; tv_embed / android
# avoid the OAuth/PO-token checks that block server IPs.
_COMMON_YDL_OPTS = {
'quiet': False,
'verbose': True,
'no_warnings': False,
'cookiefile': cookies_path if cookies_path else None,
'socket_timeout': 30,
'retries': 10,
'fragment_retries': 10,
'nocheckcertificate': True,
'cachedir': False,
'extractor_args': {
'youtube': {
'player_client': ['tv_embed', 'android', 'mweb', 'web'],
'player_skip': ['webpage', 'configs'],
}
},
'http_headers': {
'User-Agent': (
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) '
'AppleWebKit/537.36 (KHTML, like Gecko) '
'Chrome/120.0.0.0 Safari/537.36'
),
},
}
# We run the command with --json to get structured output
cmd = [
"/home/ren/yt/venv/bin/python",
"/home/ren/yt/yt-dl",
"--cookies", "/home/ren/yt/cookies.txt",
"--json",
"-o", abs_output_dir,
url
]
print(f" Executing: {' '.join(cmd)}")
result = subprocess.run(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True)
if result.returncode != 0:
print("🚨 YOUTUBE DOWNLOAD ERROR 🚨", file=sys.stderr)
print(result.stderr, file=sys.stderr)
raise Exception(f"YouTube download failed: {result.stderr}")
with yt_dlp.YoutubeDL(_COMMON_YDL_OPTS) as ydl:
try:
info = ydl.extract_info(url, download=False)
video_title = info.get('title', 'youtube_video')
output_data = json.loads(result.stdout.strip())
downloaded_file = output_data['filepath']
video_title = output_data.get('title', 'youtube_video')
sanitized_title = sanitize_filename(video_title)
except Exception as e:
# Force print to stderr/stdout immediately so it's captured before crash
import sys
import traceback
# Print minimal error first to ensure something gets out
print("🚨 YOUTUBE DOWNLOAD ERROR 🚨", file=sys.stderr)
error_msg = f"""
❌ ================================================================= ❌
❌ FATAL ERROR: YOUTUBE DOWNLOAD FAILED
❌ ================================================================= ❌
REASON: YouTube has blocked the download request (Error 429/Unavailable).
This is likely a temporary IP ban on this server.
👇 SOLUTION FOR USER 👇
---------------------------------------------------------------------
1. Download the video manually to your computer.
2. Use the 'Upload Video' tab in this app to process it.
---------------------------------------------------------------------
Technical Details: {str(e)}
"""
# Print to both streams to ensure capture
print(error_msg, file=sys.stdout)
print(error_msg, file=sys.stderr)
# Force flush
sys.stdout.flush()
sys.stderr.flush()
# Wait a split second to allow buffer to drain before raising
time.sleep(0.5)
raise e
output_template = os.path.join(output_dir, f'{sanitized_title}.%(ext)s')
expected_file = os.path.join(output_dir, f'{sanitized_title}.mp4')
if os.path.exists(expected_file):
os.remove(expected_file)
print(f"🗑️ Removed existing file to re-download with H.264 codec")
ydl_opts = {
**_COMMON_YDL_OPTS,
'format': 'bestvideo[vcodec^=avc1][ext=mp4]+bestaudio[ext=m4a]/bestvideo[vcodec^=avc1]+bestaudio/best[ext=mp4]/best',
'outtmpl': output_template,
'merge_output_format': 'mp4',
'overwrites': True,
}
with yt_dlp.YoutubeDL(ydl_opts) as ydl:
ydl.download([url])
downloaded_file = os.path.join(output_dir, f'{sanitized_title}.mp4')
print(f"⚠️ JSON parsing failed, trying fallback detection. Error: {e}")
print(f" Raw stdout: {result.stdout}")
# Fallback parsing: if not JSON, the script prints the absolute filepath to stdout
downloaded_file = result.stdout.strip()
if not os.path.exists(downloaded_file):
for f in os.listdir(output_dir):
if f.startswith(sanitized_title) and f.endswith('.mp4'):
downloaded_file = os.path.join(output_dir, f)
# Search output_dir for files created recently
downloaded_file = None
for file in os.listdir(abs_output_dir):
if file.endswith('.mp4'):
full_path = os.path.join(abs_output_dir, file)
if time.time() - os.path.getmtime(full_path) < 60:
downloaded_file = full_path
break
if not downloaded_file or not os.path.exists(downloaded_file):
raise Exception("Failed to locate downloaded video file.")
sanitized_title = sanitize_filename(os.path.splitext(os.path.basename(downloaded_file))[0])
step_end_time = time.time()
print(f"✅ Video downloaded in {step_end_time - step_start_time:.2f}s: {downloaded_file}")
return downloaded_file, sanitized_title
def process_video_to_vertical(input_video, final_output_video, mute_ranges=None):