# -*- coding: utf-8 -*-
"""
腾讯视频一键下载器
用法：
  python qq_dl.py 抓取结果.json                 # 下载全部"全片"集
  python qq_dl.py 抓取结果.json --eps 1-6      # 只下第1-6集
  python qq_dl.py 抓取结果.json --list         # 只打印直链不下载
  python qq_dl.py ep2_urls.json --name 第2集    # 直接给 m3u8 列表文件
输入兼容两种格式：
  1. 油猴脚本导出的 {episodes:[...], captured:{vid:{urls,td,pv,full}}}
  2. 纯 URL 列表（如 ep1_urls.json）
依赖：ffmpeg（主）或 yt-dlp（备）。直链有时效（1~2小时），抓到尽快下。
"""
import json
import os
import re
import subprocess
import sys

MIN = ["ffmpeg", "yt-dlp"]


def _ffmpeg_candidates():
    cands = ["ffmpeg"]
    local = os.environ.get("LOCALAPPDATA", "")
    if local:
        import glob
        cands += glob.glob(os.path.join(local, "Microsoft", "WinGet", "Links", "ffmpeg.exe"))
        cands += glob.glob(os.path.join(local, "Microsoft", "WinGet", "Packages", "Gyan.FFmpeg*",
                                        "ffmpeg*", "bin", "ffmpeg.exe"))
    return cands


def have(tool):
    if tool == "ffmpeg":
        for c in _ffmpeg_candidates():
            try:
                subprocess.run([c, "-version"], capture_output=True, timeout=10)
                globals()["_FF"] = c
                return True
            except Exception:
                continue
        return False
    try:
        subprocess.run([tool, "--version"], capture_output=True, timeout=10)
        return True
    except Exception:
        return False


def safe_name(s):
    return re.sub(r'[\\/:*?"<>|]', '_', s).strip()[:60] or "unnamed"


def load_entries(path):
    """返回 [(集名, url, 是否全片)]"""
    data = json.load(open(path, encoding="utf-8"))
    entries = []
    if isinstance(data, list):                      # 纯 URL 列表
        name = os.path.splitext(os.path.basename(path))[0]
        if data and isinstance(data[0], str):
            entries.append((name, data[0], True))
        else:                                       # [{vid,urls...}] 自定义
            for it in data:
                if it.get("urls"):
                    entries.append((it.get("vid", "ep"), it["urls"][0], it.get("full", True)))
    elif isinstance(data, dict):
        eps = {e["vid"]: e for e in data.get("episodes", [])}
        for vid, cap in (data.get("captured") or {}).items():
            if not cap.get("urls"):
                continue
            ep = eps.get(vid, {})
            label = ep.get("label") or vid
            title = ep.get("title") or ""
            name = label + ("_" + title if title else "")
            entries.append((name, cap["urls"][0], bool(cap.get("full"))))
    return entries


def pick_eps(entries, spec):
    if not spec:
        return entries
    want = set()
    for part in spec.split(","):
        if "-" in part:
            a, b = part.split("-")
            want.update(range(int(a), int(b) + 1))
        else:
            want.add(int(part))
    out = []
    for name, url, full in entries:
        m = re.match(r"(\d+)", name)
        if m and int(m.group(1)) in want:
            out.append((name, url, full))
    return out


def download_ffmpeg(url, out):
    return subprocess.run([globals().get("_FF", "ffmpeg"), "-y", "-referer", "https://v.qq.com/",
                           "-i", url, "-c", "copy", "-bsf:a", "aac_adtstoasc", out],
                          capture_output=True)


def download_ytdlp(url, out):
    return subprocess.run(["yt-dlp", "-o", out, url], capture_output=True)


def fix_container(path):
    """yt-dlp 无 ffmpeg 时产出的是 TS 数据但后缀 .mp4；检测后转封装成真 mp4"""
    with open(path, "rb") as f:
        head = f.read(4)
    if head[:1] != b"\x47":
        return                                    # 已是正常容器
    ff = globals().get("_FF") or ("ffmpeg" if have("ffmpeg") else None)
    if not ff:
        os.rename(path, path[:-4] + ".ts")
        print("  (无ffmpeg，保留为 .ts)")
        return
    tmp = path + ".fix.mp4"
    r = subprocess.run([ff, "-y", "-i", path, "-c", "copy", "-bsf:a", "aac_adtstoasc", tmp],
                       capture_output=True)
    if r.returncode == 0 and os.path.exists(tmp) and os.path.getsize(tmp) > 1048576:
        os.replace(tmp, path)
        print("  (TS→MP4 转封装完成)")
    else:
        os.rename(path, path[:-4] + ".ts")
        print("  (转封装失败，保留为 .ts)")


def main():
    ap = __import__("argparse").ArgumentParser()
    ap.add_argument("jsonfile")
    ap.add_argument("--eps", default="", help="如 1-6,9")
    ap.add_argument("--list", action="store_true", help="只打印直链")
    ap.add_argument("--name", default="", help="纯URL列表时的输出文件名")
    ap.add_argument("--include-preview", action="store_true", help="也下载预览片段")
    args = ap.parse_args()

    entries = load_entries(args.jsonfile)
    if args.name:
        entries = [(args.name, u, f) for n, u, f in entries]
    if not args.include_preview:
        full_only = [e for e in entries if e[2]]
        if full_only:
            entries = full_only
    if args.eps or args.list:
        entries = pick_eps(entries, args.eps) if args.eps else entries

    print("共 %d 个待下载：" % len(entries))
    for n, u, f in entries:
        print("  %s [%s] %s..." % (n, "全片" if f else "预览", u[:60]))

    if args.list:
        for _, u, _f in entries:
            print(u)
        return

    tool = "ffmpeg" if have("ffmpeg") else ("yt-dlp" if have("yt-dlp") else None)
    if not tool:
        sys.exit("未找到 ffmpeg / yt-dlp，请先安装：winget install Gyan.FFmpeg 或 pip install yt-dlp")
    print("使用 %s 下载\n" % tool)

    ok = fail = 0
    for name, url, _f in entries:
        out = safe_name(name) + ".mp4"
        if os.path.exists(out) and os.path.getsize(out) > 1048576:
            print("[跳过] %s 已存在" % out)
            ok += 1
            continue
        print("[下载] %s ..." % out)
        r = download_ffmpeg(url, out) if tool == "ffmpeg" else download_ytdlp(url, out)
        if r.returncode == 0 and os.path.exists(out):
            fix_container(out)
            print("  完成: %.1f MB" % (os.path.getsize(out) / 1048576))
            ok += 1
        else:
            print("  失败: %s" % (r.stderr[-200:] if r.stderr else "未知"))
            fail += 1
    print("\n完成 %d，失败 %d" % (ok, fail))


if __name__ == "__main__":
    main()
