"""YouTube 开播前网络预检(参照 d2ypp2 network_preflight,Windows/Electron 适配)。""" from __future__ import annotations import os from typing import Any, Optional import httpx from web2_network import ( CONNECT_TIMEOUT, DOUYIN_CHECK_URL, GOOGLE_CHECK_URL, READ_TIMEOUT, _latency_get, ) from web2_proxy_config import read_proxy_config YOUTUBE_CHECK_URL = "https://www.youtube.com/generate_204" _PROBE_TARGETS: tuple[dict[str, str], ...] = ( {"id": "google", "label": "Google", "url": GOOGLE_CHECK_URL, "role": "global"}, {"id": "youtube", "label": "YouTube", "url": YOUTUBE_CHECK_URL, "role": "global"}, {"id": "douyin", "label": "抖音源站", "url": DOUYIN_CHECK_URL, "role": "source"}, ) def _preflight_disabled() -> bool: raw = os.environ.get("YOUTUBE_NETWORK_PREFLIGHT", "1").strip().lower() return raw in {"0", "false", "no", "off"} def _global_ok(probes: list[dict[str, Any]]) -> bool: return any(p.get("ok") for p in probes if p.get("role") == "global") def _douyin_ok(probes: list[dict[str, Any]]) -> bool: return any(p.get("ok") for p in probes if p.get("id") == "douyin") async def _probe_all(client: httpx.AsyncClient) -> list[dict[str, Any]]: rows: list[dict[str, Any]] = [] for target in _PROBE_TARGETS: ok, ms, err = await _latency_get(client, target["url"]) rows.append( { **target, "ok": ok, "latency_ms": round(ms, 1) if ms >= 0 else None, "error": err, } ) return rows async def run_youtube_preflight() -> dict[str, Any]: """返回完整预检报告,供 /youtube/preflight 与开播拦截共用。""" if _preflight_disabled(): return { "ok": True, "mode": "disabled", "message": "网络预检已关闭(YOUTUBE_NETWORK_PREFLIGHT=0)", "checks": {}, "direct": [], "proxy": [], "proxy_config": read_proxy_config(), } timeout = httpx.Timeout(READ_TIMEOUT, connect=CONNECT_TIMEOUT) proxy_cfg = read_proxy_config() async with httpx.AsyncClient(timeout=timeout, follow_redirects=True) as client: direct = await _probe_all(client) proxy_rows: list[dict[str, Any]] = [] proxy_http = (proxy_cfg.get("http") or "").strip() if proxy_cfg.get("enabled") and proxy_http: try: async with httpx.AsyncClient( timeout=timeout, follow_redirects=True, proxy=proxy_http, ) as client: proxy_rows = await _probe_all(client) except Exception as e: proxy_rows = [ {**t, "ok": False, "latency_ms": None, "error": str(e)} for t in _PROBE_TARGETS ] global_direct = _global_ok(direct) global_proxy = _global_ok(proxy_rows) douyin_direct = _douyin_ok(direct) ok = bool(global_direct or global_proxy) mode = "direct" if global_direct else ("proxy" if global_proxy else "failed") checks = { "google": next((p for p in direct if p.get("id") == "google"), {}).get("ok", False), "youtube": next((p for p in direct if p.get("id") == "youtube"), {}).get("ok", False), "douyin": douyin_direct, "global_direct": global_direct, "global_proxy": global_proxy, "proxy_configured": bool(proxy_cfg.get("enabled") and proxy_http), } if ok: if global_direct: message = "直连可达 Google/YouTube,可尝试开播" else: message = f"直连失败,但录制代理 {proxy_http} 可达 Google/YouTube" if not douyin_direct: message += ";抖音源站当前不可达,可能无法拉流" else: parts = [] if not global_direct: parts.append("Google/YouTube 直连不可达") if proxy_cfg.get("enabled"): if not global_proxy: parts.append("录制代理也未打通 Google/YouTube") else: parts.append("未启用录制代理") if not douyin_direct: parts.append("抖音源站不可达") message = ";".join(parts) blocking_message: Optional[str] = None if not ok: blocking_message = ( f"{message}。请先在「网络」页检测并配置代理,或在下方「录制代理」中填写 Clash 混合端口后再开播。" ) return { "ok": ok, "mode": mode, "message": message, "blocking_message": blocking_message, "checks": checks, "direct": direct, "proxy": proxy_rows, "proxy_config": proxy_cfg, "douyin_direct_ok": douyin_direct, } async def youtube_start_preflight_message() -> str | None: result = await run_youtube_preflight() if result.get("ok"): return None return str(result.get("blocking_message") or result.get("message") or "网络预检未通过")