vocal-separation / youtube.py
mcuo's picture
Handle YouTube verification and support private proxy
9143e25 verified
Raw History Blame Contribute Delete
4.4 kB
"""Download a single YouTube video's audio for the separation UI."""
from html import escape
import os
from pathlib import Path
import tempfile
from urllib.parse import urlparse
import gradio as gr
import yt_dlp
FORMATS = {
"Best available": "bestaudio/best",
"M4A (if available)": "bestaudio[ext=m4a]/bestaudio/best",
"Opus/WebM (if available)": "bestaudio[ext=webm]/bestaudio/best",
}
def _validate_url(url: str) -> str:
url = (url or "").strip()
parsed = urlparse(url)
host = (parsed.hostname or "").lower()
if parsed.scheme not in ("http", "https") or host not in {
"youtube.com", "www.youtube.com", "m.youtube.com",
"music.youtube.com", "youtu.be", "www.youtu.be",
}:
raise gr.Error("Enter a valid YouTube video URL.")
return url
def _options(audio_format: str = "Best available") -> dict:
if audio_format not in FORMATS:
raise gr.Error("Select a valid audio format.")
options = {
"format": FORMATS[audio_format],
"noplaylist": True,
"max_filesize": 300 * 1024 * 1024,
"source_address": "0.0.0.0",
"quiet": True,
"no_warnings": True,
}
proxy = os.environ.get("YTDLP_PROXY", "").strip()
if proxy:
parsed_proxy = urlparse(proxy)
if parsed_proxy.scheme not in ("http", "https", "socks5", "socks5h") or not parsed_proxy.hostname:
raise gr.Error("YTDLP_PROXY must be an HTTP(S) or SOCKS5 proxy URL.")
options["proxy"] = proxy
return options
def _download_error(exc: Exception, action: str) -> gr.Error:
if "Sign in to confirm you鈥檙e not a bot" in str(exc) or "Sign in to confirm you're not a bot" in str(exc):
return gr.Error(
"YouTube is requiring verification from this Space's network. "
"Use a trusted proxy configured as the YTDLP_PROXY Space Secret, "
"or upload an audio file instead."
)
return gr.Error(f"Could not {action} the YouTube video. Check the URL and try again.")
def _extract(url: str, options: dict, download: bool):
"""Retry with a YouTube client that does not require a PO Token."""
last_error = None
for client in (None, "android_vr"):
attempt = options.copy()
if client:
attempt["extractor_args"] = {"youtube": {"player_client": [client]}}
try:
with yt_dlp.YoutubeDL(attempt) as ydl:
info = ydl.extract_info(url, download=download)
return info, ydl
except yt_dlp.utils.DownloadError as exc:
last_error = exc
raise last_error
def inspect_youtube(url: str) -> str:
"""Show video details without downloading the media."""
url = _validate_url(url)
try:
info, _ = _extract(url, _options(), download=False)
except yt_dlp.utils.DownloadError as exc:
raise _download_error(exc, "inspect") from exc
if not info or info.get("_type") == "playlist":
raise gr.Error("Select a single video from the playlist.")
title = escape(str(info.get("title") or "Untitled"))
channel = escape(str(info.get("channel") or info.get("uploader") or "Unknown"))
duration = info.get("duration")
length = f"{int(duration) // 60}:{int(duration) % 60:02d}" if duration else "Unknown"
return f"**{title}** 路 {channel} 路 {length}"
def youtube(url: str, audio_format: str = "Best available") -> str:
"""Keep the selected source audio without an unnecessary MP3 conversion."""
url = _validate_url(url)
directory = Path(tempfile.mkdtemp(prefix="vocal-separation-yt-"))
options = _options(audio_format)
options["outtmpl"] = str(directory / f"{directory.name}.%(ext)s")
try:
info, ydl = _extract(url, options, download=True)
if not info or info.get("_type") == "playlist":
raise gr.Error("Select a single video from the playlist.")
downloads = info.get("requested_downloads") or []
candidate = downloads[0].get("filepath") if downloads else None
path = Path(candidate or ydl.prepare_filename(info))
if not path.is_file():
raise gr.Error("The downloaded audio file was not found.")
return str(path)
except yt_dlp.utils.DownloadError as exc:
raise _download_error(exc, "download") from exc