d
This commit is contained in:
117
tool/ytdlp.py
117
tool/ytdlp.py
@@ -145,6 +145,7 @@ def list_formats(
|
||||
no_playlist: bool = False,
|
||||
playlist_items: Optional[str] = None,
|
||||
cookiefile: Optional[str] = None,
|
||||
timeout_seconds: int = 20,
|
||||
) -> Optional[List[Dict[str, Any]]]:
|
||||
"""Get available formats for a URL.
|
||||
|
||||
@@ -154,47 +155,67 @@ def list_formats(
|
||||
if not is_url_supported_by_ytdlp(url):
|
||||
return None
|
||||
|
||||
ensure_yt_dlp_ready()
|
||||
assert yt_dlp is not None
|
||||
result_container: List[Optional[Any]] = [None, None] # [result, error]
|
||||
|
||||
ydl_opts: Dict[str, Any] = {
|
||||
"quiet": True,
|
||||
"no_warnings": True,
|
||||
"skip_download": True,
|
||||
"noprogress": True,
|
||||
}
|
||||
def _do_list() -> None:
|
||||
try:
|
||||
ensure_yt_dlp_ready()
|
||||
assert yt_dlp is not None
|
||||
|
||||
if cookiefile:
|
||||
ydl_opts["cookiefile"] = str(cookiefile)
|
||||
else:
|
||||
# Best effort attempt to use browser cookies if no file is explicitly passed
|
||||
ydl_opts["cookiesfrombrowser"] = "chrome"
|
||||
ydl_opts: Dict[str, Any] = {
|
||||
"quiet": True,
|
||||
"no_warnings": True,
|
||||
"skip_download": True,
|
||||
"noprogress": True,
|
||||
"socket_timeout": min(10, max(1, int(timeout_seconds))),
|
||||
"retries": 2,
|
||||
}
|
||||
|
||||
if no_playlist:
|
||||
ydl_opts["noplaylist"] = True
|
||||
if playlist_items:
|
||||
ydl_opts["playlist_items"] = str(playlist_items)
|
||||
if cookiefile:
|
||||
ydl_opts["cookiefile"] = str(cookiefile)
|
||||
else:
|
||||
# Best effort attempt to use browser cookies if no file is explicitly passed
|
||||
ydl_opts["cookiesfrombrowser"] = "chrome"
|
||||
|
||||
try:
|
||||
with yt_dlp.YoutubeDL(ydl_opts) as ydl: # type: ignore[arg-type]
|
||||
info = ydl.extract_info(url, download=False)
|
||||
except Exception as exc:
|
||||
debug(f"yt-dlp format probe failed for {url}: {exc}")
|
||||
if no_playlist:
|
||||
ydl_opts["noplaylist"] = True
|
||||
if playlist_items:
|
||||
ydl_opts["playlist_items"] = str(playlist_items)
|
||||
|
||||
with yt_dlp.YoutubeDL(ydl_opts) as ydl: # type: ignore[arg-type]
|
||||
info = ydl.extract_info(url, download=False)
|
||||
|
||||
if not isinstance(info, dict):
|
||||
result_container[0] = None
|
||||
return
|
||||
|
||||
formats = info.get("formats")
|
||||
if not isinstance(formats, list):
|
||||
result_container[0] = None
|
||||
return
|
||||
|
||||
out: List[Dict[str, Any]] = []
|
||||
for fmt in formats:
|
||||
if isinstance(fmt, dict):
|
||||
out.append(fmt)
|
||||
result_container[0] = out
|
||||
except Exception as exc:
|
||||
debug(f"yt-dlp format probe failed for {url}: {exc}")
|
||||
result_container[1] = exc
|
||||
|
||||
# Use daemon=True so a hung thread doesn't block process exit
|
||||
thread = threading.Thread(target=_do_list, daemon=True)
|
||||
thread.start()
|
||||
thread.join(timeout=max(1, int(timeout_seconds)))
|
||||
|
||||
if thread.is_alive():
|
||||
debug(f"yt-dlp format probe timed out for {url} (>={timeout_seconds}s)")
|
||||
return None
|
||||
|
||||
if not isinstance(info, dict):
|
||||
if result_container[1] is not None:
|
||||
return None
|
||||
|
||||
formats = info.get("formats")
|
||||
if not isinstance(formats, list):
|
||||
return None
|
||||
|
||||
out: List[Dict[str, Any]] = []
|
||||
for fmt in formats:
|
||||
if isinstance(fmt, dict):
|
||||
out.append(fmt)
|
||||
|
||||
return out
|
||||
return cast(Optional[List[Dict[str, Any]]], result_container[0])
|
||||
|
||||
|
||||
def probe_url(
|
||||
@@ -216,6 +237,7 @@ def probe_url(
|
||||
|
||||
def _do_probe() -> None:
|
||||
try:
|
||||
debug(f"[probe] Starting probe for {url}")
|
||||
ensure_yt_dlp_ready()
|
||||
|
||||
assert yt_dlp is not None
|
||||
@@ -235,7 +257,9 @@ def probe_url(
|
||||
ydl_opts["noplaylist"] = True
|
||||
|
||||
with yt_dlp.YoutubeDL(ydl_opts) as ydl: # type: ignore[arg-type]
|
||||
debug(f"[probe] ytdlp extract_info (download=False) start: {url}")
|
||||
info = ydl.extract_info(url, download=False)
|
||||
debug(f"[probe] ytdlp extract_info (download=False) done: {url}")
|
||||
|
||||
if not isinstance(info, dict):
|
||||
result_container[0] = None
|
||||
@@ -258,7 +282,8 @@ def probe_url(
|
||||
debug(f"Probe error for {url}: {exc}")
|
||||
result_container[1] = exc
|
||||
|
||||
thread = threading.Thread(target=_do_probe, daemon=False)
|
||||
# Use daemon=True so a hung probe doesn't block the process
|
||||
thread = threading.Thread(target=_do_probe, daemon=True)
|
||||
thread.start()
|
||||
thread.join(timeout=timeout_seconds)
|
||||
|
||||
@@ -1194,6 +1219,7 @@ except ImportError:
|
||||
def download_media(opts: DownloadOptions, *, debug_logger: Optional[DebugLogger] = None) -> Any:
|
||||
"""Download streaming media exclusively via yt-dlp."""
|
||||
|
||||
debug(f"[download_media] start: {opts.url}")
|
||||
try:
|
||||
netloc = urlparse(opts.url).netloc.lower()
|
||||
except Exception:
|
||||
@@ -1536,20 +1562,37 @@ def _download_with_timeout(opts: DownloadOptions, timeout_seconds: int = 300) ->
|
||||
except Exception as exc:
|
||||
result_container[1] = exc
|
||||
|
||||
thread = threading.Thread(target=_do_download, daemon=False)
|
||||
# Use daemon=True so a hung download doesn't block process exit if the wall timeout hits.
|
||||
thread = threading.Thread(target=_do_download, daemon=True)
|
||||
thread.start()
|
||||
start_time = time.monotonic()
|
||||
|
||||
# We use two timeouts:
|
||||
# 1. Activity timeout (no progress updates for X seconds)
|
||||
# 2. Hard wall-clock timeout (total time for this URL)
|
||||
# The wall-clock timeout is slightly larger than the activity timeout
|
||||
# to allow for slow-but-steady progress, up to a hard cap (e.g. 10 minutes).
|
||||
wall_timeout = max(timeout_seconds * 2, 600)
|
||||
|
||||
_record_progress_activity(start_time)
|
||||
try:
|
||||
while thread.is_alive():
|
||||
thread.join(1)
|
||||
if not thread.is_alive():
|
||||
break
|
||||
|
||||
now = time.monotonic()
|
||||
|
||||
# Check activity timeout
|
||||
last_activity = _get_last_progress_activity()
|
||||
if last_activity <= 0:
|
||||
last_activity = start_time
|
||||
if time.monotonic() - last_activity > timeout_seconds:
|
||||
raise DownloadError(f"Download timeout after {timeout_seconds} seconds for {opts.url}")
|
||||
if now - last_activity > timeout_seconds:
|
||||
raise DownloadError(f"Download activity timeout after {timeout_seconds} seconds for {opts.url}")
|
||||
|
||||
# Check hard wall-clock timeout
|
||||
if now - start_time > wall_timeout:
|
||||
raise DownloadError(f"Download hard timeout after {wall_timeout} seconds for {opts.url}")
|
||||
finally:
|
||||
_clear_progress_activity()
|
||||
|
||||
|
||||
Reference in New Issue
Block a user