Files
bili-downloader-web/tools/e2e_download.py
T

166 lines
6.0 KiB
Python

# -*- coding: utf-8 -*-
"""
e2e_download.py —— 端到端验收:通过服务的 HTTP 接口真的下一次,并校验产物。
流程:playurl -> POST /api/download -> 订阅 SSE 收进度 -> 等完成 -> ffprobe 校验
"""
import json
import os
import subprocess
import sys
import threading
import time
import urllib.request
BASE = "http://127.0.0.1:8799"
BVID = sys.argv[1] if len(sys.argv) > 1 else "BV1HVBAYLEAo"
ok_count = 0
fail_count = 0
def check(name, ok, detail=""):
global ok_count, fail_count
if ok:
ok_count += 1
print(f" [OK] {name}" + (f" — {detail}" if detail else ""))
else:
fail_count += 1
print(f" [XX] {name}" + (f" — {detail}" if detail else ""))
def get(path):
with urllib.request.urlopen(BASE + path, timeout=60) as r:
return json.loads(r.read().decode("utf-8"))
def post(path, payload):
req = urllib.request.Request(
BASE + path, data=json.dumps(payload).encode("utf-8"),
headers={"Content-Type": "application/json"})
with urllib.request.urlopen(req, timeout=60) as r:
return json.loads(r.read().decode("utf-8"))
print("=" * 62)
print(f"端到端下载验收: {BVID}")
print("=" * 62)
video = get(f"/api/video?bvid={BVID}")
cid = video["pages"][0]["cid"]
print(f"\n视频: {video['title']}")
print(f"UP主: {video['owner']} 时长: {video['duration']}s")
play = get(f"/api/playurl?bvid={BVID}&cid={cid}")
check("拿到清晰度列表", len(play["qualities"]) > 0,
" / ".join(q["label"] for q in play["qualities"]))
quality = play["qualities"][0]["quality"]
print(f"选择清晰度: {quality} ({play['qualities'][0]['label']})")
# 先确认没有正在跑的任务
job0 = get("/api/job")
check("初始无进行中的任务", job0.get("state") != "running", str(job0.get("state")))
# 订阅 SSE
events = []
prog_samples = []
stop = threading.Event()
def listen():
try:
with urllib.request.urlopen(BASE + "/api/events", timeout=600) as r:
event = None
while not stop.is_set():
line = r.readline().decode("utf-8", "replace").strip()
if line.startswith("event:"):
event = line.split(":", 1)[1].strip()
elif line.startswith("data:"):
data = json.loads(line.split(":", 1)[1].strip())
events.append((event, data))
if event == "progress":
prog_samples.append(data)
except Exception as exc:
if not stop.is_set():
print(" SSE 读取结束:", exc)
t = threading.Thread(target=listen, daemon=True)
t.start()
time.sleep(0.8)
print("\n--- 开始下载 ---")
t0 = time.time()
job = post("/api/download", {"bvid": BVID, "cid": cid, "quality": quality,
"title": video["title"]})
check("已受理下载任务", job.get("state") == "running", str(job.get("state")))
deadline = time.time() + 300
last_state = None
while time.time() < deadline:
time.sleep(1)
cur = get("/api/job")
if cur.get("state") != last_state:
last_state = cur.get("state")
print(f" [{time.time() - t0:5.1f}s] state={last_state} stage={cur.get('stage')}")
if cur.get("state") in ("done", "error", "cancelled"):
break
elapsed = time.time() - t0
stop.set()
final = get("/api/job")
check("任务正常结束", final.get("state") == "done", str(final.get("state") or final.get("error")))
check("收到了进度事件", len(prog_samples) > 0, f"{len(prog_samples)} 条")
check("同时收到 job/stage 事件", len(events) > 0, f"{len(events)} 条")
out = final.get("outputPath") or ""
check("产物文件存在", bool(out) and os.path.exists(out), out)
if out and os.path.exists(out):
size = os.path.getsize(out)
check("产物大小合理", size > 100_000, f"{size / 1024 / 1024:.1f} MB")
ffprobe = None
import shutil
ffprobe = shutil.which("ffprobe") or r"C:\Users\Administrator\miniconda3\Library\bin\ffprobe.exe"
cp = subprocess.run(
[ffprobe, "-v", "error", "-show_entries",
"format=duration,size:stream=codec_type,codec_name,width,height",
"-of", "json", out],
capture_output=True, text=True)
if cp.returncode == 0:
info = json.loads(cp.stdout)
streams = info.get("streams", [])
kinds = {s["codec_type"]: s for s in streams}
check("产物含视频流", "video" in kinds,
f"{kinds.get('video', {}).get('codec_name')} "
f"{kinds.get('video', {}).get('width')}x{kinds.get('video', {}).get('height')}")
check("产物含音频流", "audio" in kinds, str(kinds.get("audio", {}).get("codec_name")))
dur = float(info["format"]["duration"])
check("时长与源基本一致", abs(dur - video["duration"]) < 5, f"{dur:.1f}s vs {video['duration']}s")
else:
check("ffprobe 可解析产物", False, cp.stderr.strip()[:120])
# 全片解码校验
ffmpeg = shutil.which("ffmpeg") or r"C:\Users\Administrator\miniconda3\Library\bin\ffmpeg.exe"
cp2 = subprocess.run([ffmpeg, "-v", "error", "-i", out, "-f", "null", "-"],
capture_output=True, text=True)
check("全片解码无错误", cp2.returncode == 0 and not cp2.stderr.strip(),
cp2.stderr.strip()[:160] or "clean")
# 临时文件应已清理
tmp_left = [f for f in os.listdir(os.path.dirname(out)) if f.endswith(".m4s")]
check("中间文件已清理", len(tmp_left) == 0, f"残留 {len(tmp_left)} 个" if tmp_left else "无残留")
# 并发保护:任务已结束,再来一次应能受理(说明状态机复位)
print(f"\n耗时 {elapsed:.1f}s")
print("\n进度采样(每 10 条取 1):")
for s in prog_samples[::max(1, len(prog_samples) // 10)][:10]:
print(f" {s['label']} {s['percent']:5.1f}% {s['downloaded']/1024/1024:6.1f}MB"
f"/{s['total']/1024/1024:.1f}MB {s['speed']/1024/1024:.2f}MB/s {s['threads']}线程")
print("\n" + "=" * 62)
print(f"结果: {ok_count} 通过 / {fail_count} 失败")
print("=" * 62)
sys.exit(1 if fail_count else 0)