f
This commit is contained in:
35
API/HTTP.py
35
API/HTTP.py
@@ -15,6 +15,7 @@ import time
|
|||||||
import traceback
|
import traceback
|
||||||
import re
|
import re
|
||||||
import os
|
import os
|
||||||
|
import json
|
||||||
from typing import Optional, Dict, Any, Callable, List, Union
|
from typing import Optional, Dict, Any, Callable, List, Union
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from urllib.parse import unquote, urlparse, parse_qs
|
from urllib.parse import unquote, urlparse, parse_qs
|
||||||
@@ -464,14 +465,41 @@ class HTTPClient:
|
|||||||
|
|
||||||
def _preview(value: Any, *, limit: int = 2000) -> str:
|
def _preview(value: Any, *, limit: int = 2000) -> str:
|
||||||
if value is None:
|
if value is None:
|
||||||
return "<null>"
|
return "<not provided>"
|
||||||
try:
|
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)):
|
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:
|
else:
|
||||||
text = str(value)
|
text = str(value)
|
||||||
except Exception:
|
except Exception:
|
||||||
text = repr(value)
|
try:
|
||||||
|
text = repr(value)
|
||||||
|
except Exception:
|
||||||
|
text = "<unprintable>"
|
||||||
if len(text) > limit:
|
if len(text) > limit:
|
||||||
return text[:limit] + "..."
|
return text[:limit] + "..."
|
||||||
return text
|
return text
|
||||||
@@ -497,6 +525,7 @@ class HTTPClient:
|
|||||||
("data", _preview(kwargs.get("data"))),
|
("data", _preview(kwargs.get("data"))),
|
||||||
("json", _preview(kwargs.get("json"))),
|
("json", _preview(kwargs.get("json"))),
|
||||||
("content", _preview(kwargs.get("content"))),
|
("content", _preview(kwargs.get("content"))),
|
||||||
|
("files", _preview(kwargs.get("files"))),
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
try:
|
try:
|
||||||
|
|||||||
@@ -1177,7 +1177,7 @@ class Download_File(Cmdlet):
|
|||||||
return f"https://www.youtube.com/watch?v={entry_id.strip()}"
|
return f"https://www.youtube.com/watch?v={entry_id.strip()}"
|
||||||
return None
|
return None
|
||||||
|
|
||||||
table = Table()
|
table = Table(preserve_order=True)
|
||||||
safe_url = str(url or "").strip()
|
safe_url = str(url or "").strip()
|
||||||
table.title = f'download-file -url "{safe_url}"' if safe_url else "download-file"
|
table.title = f'download-file -url "{safe_url}"' if safe_url else "download-file"
|
||||||
if table_type:
|
if table_type:
|
||||||
@@ -1477,7 +1477,7 @@ class Download_File(Cmdlet):
|
|||||||
actual_playlist_items = None
|
actual_playlist_items = None
|
||||||
|
|
||||||
if mode == "audio" and not actual_format:
|
if mode == "audio" and not actual_format:
|
||||||
actual_format = "bestaudio"
|
actual_format = "bestaudio/best"
|
||||||
|
|
||||||
if mode == "video" and not actual_format:
|
if mode == "video" and not actual_format:
|
||||||
configured = (ytdlp_tool.default_format("video") or "").strip()
|
configured = (ytdlp_tool.default_format("video") or "").strip()
|
||||||
@@ -1493,6 +1493,20 @@ class Download_File(Cmdlet):
|
|||||||
):
|
):
|
||||||
actual_format = forced_single_format_id
|
actual_format = forced_single_format_id
|
||||||
forced_single_applied = True
|
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 (
|
if (
|
||||||
actual_format
|
actual_format
|
||||||
and isinstance(actual_format, str)
|
and isinstance(actual_format, str)
|
||||||
@@ -1521,7 +1535,14 @@ class Download_File(Cmdlet):
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
pass
|
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_single_format_fallback = False
|
||||||
|
attempted_audio_fallback_specific = False
|
||||||
|
attempted_audio_fallback_generic = False
|
||||||
while True:
|
while True:
|
||||||
try:
|
try:
|
||||||
opts = DownloadOptions(
|
opts = DownloadOptions(
|
||||||
@@ -1551,7 +1572,79 @@ class Download_File(Cmdlet):
|
|||||||
except Exception:
|
except Exception:
|
||||||
detail = ""
|
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 (
|
if (
|
||||||
forced_single_format_for_batch
|
forced_single_format_for_batch
|
||||||
and forced_single_format_id
|
and forced_single_format_id
|
||||||
@@ -1953,8 +2046,8 @@ class Download_File(Cmdlet):
|
|||||||
except Exception:
|
except Exception:
|
||||||
height_selector = None
|
height_selector = None
|
||||||
if query_wants_audio:
|
if query_wants_audio:
|
||||||
# Explicit audio request should map to best-audio-only selector
|
# Explicit audio request should map to the configured audio selector (usually '251/140/bestaudio')
|
||||||
ytdl_format = "bestaudio"
|
ytdl_format = ytdlp_tool.default_format("audio")
|
||||||
elif height_selector:
|
elif height_selector:
|
||||||
ytdl_format = height_selector
|
ytdl_format = height_selector
|
||||||
elif query_format:
|
elif query_format:
|
||||||
@@ -2045,6 +2138,51 @@ class Download_File(Cmdlet):
|
|||||||
if early_ret is not None:
|
if early_ret is not None:
|
||||||
return int(early_ret)
|
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
|
timeout_seconds = 300
|
||||||
try:
|
try:
|
||||||
override = config.get("_pipeobject_timeout_seconds") if isinstance(config, dict) else None
|
override = config.get("_pipeobject_timeout_seconds") if isinstance(config, dict) else None
|
||||||
|
|||||||
@@ -726,7 +726,7 @@ class YtDlpDefaults:
|
|||||||
|
|
||||||
format: str = "best"
|
format: str = "best"
|
||||||
video_format: str = "bestvideo+bestaudio/best"
|
video_format: str = "bestvideo+bestaudio/best"
|
||||||
audio_format: str = "251/140/bestaudio"
|
audio_format: str = "bestaudio/best"
|
||||||
format_sort: Optional[List[str]] = None
|
format_sort: Optional[List[str]] = None
|
||||||
cookies_from_browser: Optional[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 support "just in case" if no file found (best effort)
|
||||||
_add_browser_cookies_if_available(base_options)
|
_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
|
# Special handling for format keywords explicitly passed in via options
|
||||||
if opts.ytdl_format == "audio":
|
if opts.ytdl_format == "audio":
|
||||||
try:
|
try:
|
||||||
@@ -1014,7 +1044,14 @@ class YtDlpTool:
|
|||||||
if resolved:
|
if resolved:
|
||||||
ytdl_format = 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
|
base_options["format"] = fmt
|
||||||
|
|
||||||
if opts.mode == "audio":
|
if opts.mode == "audio":
|
||||||
@@ -1870,6 +1907,12 @@ def download_media(opts: DownloadOptions, *, config: Optional[Dict[str, Any]] =
|
|||||||
extractor_args["youtube"] = youtube_args
|
extractor_args["youtube"] = youtube_args
|
||||||
fallback_options["extractor_args"] = extractor_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]
|
with yt_dlp.YoutubeDL(fallback_options) as ydl: # type: ignore[arg-type]
|
||||||
info = cast(Dict[str, Any], ydl.extract_info(opts.url, download=True))
|
info = cast(Dict[str, Any], ydl.extract_info(opts.url, download=True))
|
||||||
except Exception as exc2:
|
except Exception as exc2:
|
||||||
|
|||||||
Reference in New Issue
Block a user