This commit is contained in:
2026-02-06 23:34:20 -08:00
parent d806ebad85
commit af54acda3c
5 changed files with 498 additions and 392 deletions

View File

@@ -376,31 +376,14 @@ def _run_op(op: str, data: Any) -> Dict[str, Any]:
"table": None,
}
# Fast gate: only for streaming URLs yt-dlp knows about.
try:
from tool.ytdlp import is_url_supported_by_ytdlp # noqa: WPS433
if not is_url_supported_by_ytdlp(url):
return {
"success": False,
"stdout": "",
"stderr": "",
"error": "URL not supported by yt-dlp",
"table": None,
}
except Exception:
# If probing support fails, still attempt extraction and let yt-dlp decide.
pass
try:
import yt_dlp # type: ignore
from tool.ytdlp import list_formats, is_browseable_format # noqa: WPS433
except Exception as exc:
return {
"success": False,
"stdout": "",
"stderr": "",
"error":
f"yt-dlp module not available: {type(exc).__name__}: {exc}",
"error": f"yt-dlp tool unavailable: {type(exc).__name__}: {exc}",
"table": None,
}
@@ -415,96 +398,27 @@ def _run_op(op: str, data: Any) -> Dict[str, Any]:
except Exception:
cookiefile = None
ydl_opts: Dict[str,
Any] = {
"quiet": True,
"no_warnings": True,
"socket_timeout": 20,
"retries": 2,
"skip_download": True,
# Avoid accidentally expanding huge playlists on load.
"noplaylist": True,
"noprogress": True,
}
if cookiefile:
ydl_opts["cookiefile"] = cookiefile
def _format_bytes(n: Any) -> str:
"""Format bytes using centralized utility."""
return format_bytes(n)
with yt_dlp.YoutubeDL(ydl_opts) as ydl: # type: ignore[attr-defined]
info = ydl.extract_info(url, download=False)
formats = list_formats(
url,
no_playlist=True,
cookiefile=cookiefile,
timeout_seconds=25,
)
# Debug: dump a short summary of the format list to the helper log.
try:
formats_any = info.get("formats") if isinstance(info, dict) else None
count = len(formats_any) if isinstance(formats_any, list) else 0
_append_helper_log(
f"[ytdlp-formats] extracted formats count={count} url={url}"
)
if isinstance(formats_any, list) and formats_any:
limit = 60
for i, f in enumerate(formats_any[:limit], start=1):
if not isinstance(f, dict):
continue
fid = str(f.get("format_id") or "")
ext = str(f.get("ext") or "")
note = f.get("format_note") or f.get("format") or ""
vcodec = str(f.get("vcodec") or "")
acodec = str(f.get("acodec") or "")
size = f.get("filesize") or f.get("filesize_approx")
res = str(f.get("resolution") or "")
if not res:
try:
w = f.get("width")
h = f.get("height")
if w and h:
res = f"{int(w)}x{int(h)}"
elif h:
res = f"{int(h)}p"
except Exception:
res = ""
_append_helper_log(
f"[ytdlp-format {i:02d}] id={fid} ext={ext} res={res} note={note} codecs={vcodec}/{acodec} size={size}"
)
if count > limit:
_append_helper_log(
f"[ytdlp-formats] (truncated; total={count})"
)
except Exception:
pass
# Optional: dump the full extracted JSON for inspection.
try:
dump = os.environ.get("MEDEIA_MPV_YTDLP_DUMP", "").strip()
if dump and dump != "0" and isinstance(info, dict):
h = hashlib.sha1(url.encode("utf-8",
errors="replace")).hexdigest()[:10]
out_path = _repo_root() / "Log" / f"ytdlp-probe-{h}.json"
out_path.write_text(
json.dumps(info,
ensure_ascii=False,
indent=2),
encoding="utf-8",
errors="replace",
)
_append_helper_log(f"[ytdlp-formats] wrote probe json: {out_path}")
except Exception:
pass
if not isinstance(info, dict):
if formats is None:
return {
"success": False,
"stdout": "",
"stderr": "",
"error": "yt-dlp returned non-dict info",
"error": "yt-dlp format probe failed or timed out",
"table": None,
}
formats = info.get("formats")
if not isinstance(formats, list) or not formats:
if not formats:
return {
"success": True,
"stdout": "",
@@ -516,6 +430,48 @@ def _run_op(op: str, data: Any) -> Dict[str, Any]:
},
}
browseable = [f for f in formats if is_browseable_format(f)]
if browseable:
formats = browseable
# Debug: dump a short summary of the format list to the helper log.
try:
count = len(formats)
_append_helper_log(
f"[ytdlp-formats] extracted formats count={count} url={url}"
)
limit = 60
for i, f in enumerate(formats[:limit], start=1):
if not isinstance(f, dict):
continue
fid = str(f.get("format_id") or "")
ext = str(f.get("ext") or "")
note = f.get("format_note") or f.get("format") or ""
vcodec = str(f.get("vcodec") or "")
acodec = str(f.get("acodec") or "")
size = f.get("filesize") or f.get("filesize_approx")
res = str(f.get("resolution") or "")
if not res:
try:
w = f.get("width")
h = f.get("height")
if w and h:
res = f"{int(w)}x{int(h)}"
elif h:
res = f"{int(h)}p"
except Exception:
res = ""
_append_helper_log(
f"[ytdlp-format {i:02d}] id={fid} ext={ext} res={res} note={note} codecs={vcodec}/{acodec} size={size}"
)
if count > limit:
_append_helper_log(
f"[ytdlp-formats] (truncated; total={count})"
)
except Exception:
pass
rows = []
for fmt in formats:
if not isinstance(fmt, dict):
@@ -540,8 +496,15 @@ def _run_op(op: str, data: Any) -> Dict[str, Any]:
ext = str(fmt.get("ext") or "").strip()
size = _format_bytes(fmt.get("filesize") or fmt.get("filesize_approx"))
vcodec = str(fmt.get("vcodec") or "none")
acodec = str(fmt.get("acodec") or "none")
selection_id = format_id
if vcodec != "none" and acodec == "none":
selection_id = f"{format_id}+ba"
# Build selection args compatible with MPV Lua picker.
selection_args = ["-query", f"format:{format_id}"]
# Use -format instead of -query so Lua can extract the ID easily.
selection_args = ["-format", selection_id]
rows.append(
{
@@ -962,6 +925,28 @@ def main(argv: Optional[list[str]] = None) -> int:
f"[helper] failed to publish config temp: {type(exc).__name__}: {exc}"
)
# Publish yt-dlp supported domains for Lua menu filtering
try:
from tool.ytdlp import _build_supported_domains
domains = sorted(list(_build_supported_domains()))
if domains:
# We join them into a space-separated string for Lua to parse easily
domains_str = " ".join(domains)
client.send_command_no_wait(
[
"set_property_string",
"user-data/medeia-ytdlp-domains-cached",
domains_str
]
)
_append_helper_log(
f"[helper] published {len(domains)} ytdlp domains for Lua menu filtering"
)
except Exception as exc:
_append_helper_log(
f"[helper] failed to publish ytdlp domains: {type(exc).__name__}: {exc}"
)
last_seen_id: Optional[str] = None
try: