This commit is contained in:
2026-02-10 15:57:33 -08:00
parent 2fd13a6b3f
commit c2449d0ba7
3 changed files with 220 additions and 10 deletions

View File

@@ -15,6 +15,7 @@ import time
import traceback
import re
import os
import json
from typing import Optional, Dict, Any, Callable, List, Union
from pathlib import Path
from urllib.parse import unquote, urlparse, parse_qs
@@ -464,14 +465,41 @@ class HTTPClient:
def _preview(value: Any, *, limit: int = 2000) -> str:
if value is None:
return "<null>"
return "<not provided>"
try:
# File-like objects (uploads/streams) - show compact summary instead of raw contents
if hasattr(value, "read") or hasattr(value, "fileno"):
name = getattr(value, "name", None)
total = getattr(value, "_total", None)
label = getattr(value, "_label", None)
try:
pos = value.tell() if hasattr(value, "tell") else None
except Exception:
pos = None
parts = []
if name is not None:
parts.append(f"name={name!r}")
if total is not None:
parts.append(f"total={total}")
if label is not None:
parts.append(f"label={label!r}")
if pos is not None:
parts.append(f"pos={pos}")
summary = " ".join(parts) if parts else None
return f"<file-like {summary}>" if summary else "<file-like>"
if isinstance(value, (dict, list, tuple)):
text = json.dumps(value, ensure_ascii=False)
# Use default=str to avoid failing on non-serializable objects (e.g., file-like)
text = json.dumps(value, ensure_ascii=False, default=str)
elif isinstance(value, (bytes, bytearray)):
text = value.decode("utf-8", errors="replace")
else:
text = str(value)
except Exception:
try:
text = repr(value)
except Exception:
text = "<unprintable>"
if len(text) > limit:
return text[:limit] + "..."
return text
@@ -497,6 +525,7 @@ class HTTPClient:
("data", _preview(kwargs.get("data"))),
("json", _preview(kwargs.get("json"))),
("content", _preview(kwargs.get("content"))),
("files", _preview(kwargs.get("files"))),
],
)
try:

View File

@@ -1177,7 +1177,7 @@ class Download_File(Cmdlet):
return f"https://www.youtube.com/watch?v={entry_id.strip()}"
return None
table = Table()
table = Table(preserve_order=True)
safe_url = str(url or "").strip()
table.title = f'download-file -url "{safe_url}"' if safe_url else "download-file"
if table_type:
@@ -1477,7 +1477,7 @@ class Download_File(Cmdlet):
actual_playlist_items = None
if mode == "audio" and not actual_format:
actual_format = "bestaudio"
actual_format = "bestaudio/best"
if mode == "video" and not actual_format:
configured = (ytdlp_tool.default_format("video") or "").strip()
@@ -1493,6 +1493,20 @@ class Download_File(Cmdlet):
):
actual_format = forced_single_format_id
forced_single_applied = True
# Proactive fallback for single audio formats which might be unstable
if (
actual_format
and isinstance(actual_format, str)
and mode == "audio"
and "/" not in actual_format
and "+" not in actual_format
and not forced_single_applied
and actual_format not in {"best", "bestaudio", "bw", "ba"}
):
debug(f"Appending fallback to specific audio format: {actual_format} -> {actual_format}/bestaudio/best")
actual_format = f"{actual_format}/bestaudio/best"
if (
actual_format
and isinstance(actual_format, str)
@@ -1521,7 +1535,14 @@ class Download_File(Cmdlet):
except Exception as e:
pass
debug(
"[download-file] Resolved format for download: "
f"mode={mode}, format={actual_format or 'default'}, playlist_items={actual_playlist_items}"
)
attempted_single_format_fallback = False
attempted_audio_fallback_specific = False
attempted_audio_fallback_generic = False
while True:
try:
opts = DownloadOptions(
@@ -1551,7 +1572,79 @@ class Download_File(Cmdlet):
except Exception:
detail = ""
if ("requested format is not available" in (detail or "").lower()) and mode != "audio":
detail_lc = (detail or "").lower()
msg_lc = ""
try:
msg_lc = str(e or "").lower()
except Exception:
msg_lc = ""
requested_format_unavailable = (
"requested format is not available" in detail_lc
or "requested format is not available" in msg_lc
)
if requested_format_unavailable and mode == "audio":
# Level 1: Try to find a specific audio-only format from the list that hopefully works
if not attempted_audio_fallback_specific:
attempted_audio_fallback_specific = True
audio_format_id = None
try:
formats = self._list_formats_cached(
url,
playlist_items_value=actual_playlist_items,
formats_cache=formats_cache,
ytdlp_tool=ytdlp_tool,
)
if formats:
audio_candidates = []
for fmt in formats:
if not isinstance(fmt, dict):
continue
vcodec = str(fmt.get("vcodec", "none"))
acodec = str(fmt.get("acodec", "none"))
if acodec != "none" and vcodec == "none":
audio_candidates.append(fmt)
if audio_candidates:
def _score_audio(fmt: Dict[str, Any]) -> float:
score = 0.0
# Penalize DRC formats heavily
fid = str(fmt.get("format_id") or "").lower()
if "drc" in fid:
score -= 1000.0
for key in ("abr", "tbr", "filesize", "filesize_approx"):
try:
val = fmt.get(key)
if isinstance(val, (int, float)):
score += float(val)
break # Use the first valid metric found
if isinstance(val, str) and val.strip().isdigit():
score += float(val)
break
except Exception:
continue
return score
audio_candidates.sort(key=_score_audio, reverse=True)
audio_format_id = str(audio_candidates[0].get("format_id") or "").strip() or None
except Exception:
audio_format_id = None
if audio_format_id:
actual_format = audio_format_id
debug(f"Requested audio format not available; retrying with best specific audio format: {actual_format}")
continue
# Level 2: Fallback to generic 'bestaudio/best' if specific selection failed or wasn't found
if not attempted_audio_fallback_generic:
attempted_audio_fallback_generic = True
if actual_format != "bestaudio/best":
actual_format = "bestaudio/best"
debug("Requested audio format not available; retrying with generic fallback: bestaudio/best")
continue
if requested_format_unavailable and mode != "audio":
if (
forced_single_format_for_batch
and forced_single_format_id
@@ -1953,8 +2046,8 @@ class Download_File(Cmdlet):
except Exception:
height_selector = None
if query_wants_audio:
# Explicit audio request should map to best-audio-only selector
ytdl_format = "bestaudio"
# Explicit audio request should map to the configured audio selector (usually '251/140/bestaudio')
ytdl_format = ytdlp_tool.default_format("audio")
elif height_selector:
ytdl_format = height_selector
elif query_format:
@@ -2045,6 +2138,51 @@ class Download_File(Cmdlet):
if early_ret is not None:
return int(early_ret)
# Auto-detect audio-only sources (e.g., Bandcamp albums) when no explicit format is requested.
if mode == "video" and not ytdl_format and not query_format and not query_wants_audio:
try:
sample_url = str(supported_url[0]) if supported_url else ""
fmts = self._list_formats_cached(
sample_url,
playlist_items_value=playlist_items,
formats_cache=formats_cache,
ytdlp_tool=ytdlp_tool,
)
if fmts:
has_video = any(str(f.get("vcodec", "none")) != "none" for f in fmts if isinstance(f, dict))
has_audio = any(str(f.get("acodec", "none")) != "none" for f in fmts if isinstance(f, dict))
if has_audio and not has_video:
mode = "audio"
ytdl_format = ytdlp_tool.default_format("audio")
debug("[download-file] No video formats detected; switching to audio mode")
else:
if "bandcamp.com/album/" in sample_url:
mode = "audio"
ytdl_format = ytdlp_tool.default_format("audio")
debug("[download-file] Bandcamp album detected; switching to audio mode")
except Exception as e:
debug(f"[download-file] Audio-only detection error: {e}")
# Auto-detect audio mode for explicit format IDs
if len(supported_url) == 1 and ytdl_format and mode != "audio":
try:
candidates = self._list_formats_cached(
supported_url[0],
playlist_items_value=playlist_items,
formats_cache=formats_cache,
ytdlp_tool=ytdlp_tool,
)
if candidates:
match = next((f for f in candidates if str(f.get("format_id", "")) == str(ytdl_format)), None)
if match:
vcodec = str(match.get("vcodec", "none"))
acodec = str(match.get("acodec", "none"))
if vcodec == "none" and acodec != "none":
debug(f"[download-file] Requested format {ytdl_format} is audio-only; switching mode to audio")
mode = "audio"
except Exception as e:
debug(f"[download-file] Error validating format mode: {e}")
timeout_seconds = 300
try:
override = config.get("_pipeobject_timeout_seconds") if isinstance(config, dict) else None

View File

@@ -726,7 +726,7 @@ class YtDlpDefaults:
format: str = "best"
video_format: str = "bestvideo+bestaudio/best"
audio_format: str = "251/140/bestaudio"
audio_format: str = "bestaudio/best"
format_sort: Optional[List[str]] = None
cookies_from_browser: Optional[str] = None
@@ -958,6 +958,36 @@ class YtDlpTool:
# Add browser cookies support "just in case" if no file found (best effort)
_add_browser_cookies_if_available(base_options)
# YouTube hardening: prefer browser cookies + mobile/web clients when available
try:
netloc = urlparse(opts.url).netloc.lower()
except Exception:
netloc = ""
is_youtube = ("youtube.com" in netloc) or ("youtu.be" in netloc)
if is_youtube:
# Prefer browser cookies over a potentially stale cookiefile when available
pref = (self.defaults.cookies_from_browser or "").lower().strip()
if pref not in {"none", "off", "false"} and _has_browser_cookie_database():
_add_browser_cookies_if_available(
base_options,
preferred_browser=None if pref in {"", "auto", "detect"} else pref,
)
if base_options.get("cookiesfrombrowser"):
base_options.pop("cookiefile", None)
debug("[ytdlp] Using browser cookies for YouTube; ignoring cookiefile")
extractor_args = base_options.get("extractor_args")
if not isinstance(extractor_args, dict):
extractor_args = {}
youtube_args = extractor_args.get("youtube")
if not isinstance(youtube_args, dict):
youtube_args = {}
if "player_client" not in youtube_args:
youtube_args["player_client"] = ["android", "web"]
debug("[ytdlp] Using YouTube player_client override: android, web")
extractor_args["youtube"] = youtube_args
base_options["extractor_args"] = extractor_args
# Special handling for format keywords explicitly passed in via options
if opts.ytdl_format == "audio":
try:
@@ -1014,7 +1044,14 @@ class YtDlpTool:
if resolved:
ytdl_format = resolved
fmt = ytdl_format or self.default_format(opts.mode)
default_fmt = self.default_format(opts.mode)
fmt = ytdl_format or default_fmt
debug(
"[ytdlp] build options: "
f"mode={opts.mode}, ytdl_format={ytdl_format}, default_format={default_fmt}, final_format={fmt}, "
f"cookiefile={base_options.get('cookiefile')}, cookiesfrombrowser={base_options.get('cookiesfrombrowser')}, "
f"player_client={((base_options.get('extractor_args') or {}).get('youtube') or {}).get('player_client')}"
)
base_options["format"] = fmt
if opts.mode == "audio":
@@ -1870,6 +1907,12 @@ def download_media(opts: DownloadOptions, *, config: Optional[Dict[str, Any]] =
extractor_args["youtube"] = youtube_args
fallback_options["extractor_args"] = extractor_args
debug(
"[ytdlp] retry options: "
f"cookiefile={fallback_options.get('cookiefile')}, cookiesfrombrowser={fallback_options.get('cookiesfrombrowser')}, "
f"player_client={((fallback_options.get('extractor_args') or {}).get('youtube') or {}).get('player_client')}"
)
with yt_dlp.YoutubeDL(fallback_options) as ydl: # type: ignore[arg-type]
info = cast(Dict[str, Any], ydl.extract_info(opts.url, download=True))
except Exception as exc2: