yt-dlp 下载工具:从踩坑到最终方案

Posted on May 2, 2026 • 8 min read • 3,780 words
Share via
 
 
 
 
 
 

yt-dlp 自动下载及使用

yt-dlp 下载工具:从踩坑到最终方案
Photo by auggie yang

yt-dlp 下载工具:从踩坑到最终方案  

完整记录一个"看似简单"的视频批量下载工具的构建过程,以及在 GitHub Copilot 辅助下的工程实践与 token 消耗优化思路。


一、背景与目标  

早期为了学习英语付费购买了 VidJuice UniTube。后续电脑重装多次后,想重新激活,发现官网都打不开了。啊,这后续只能找客服咨询了。 网查发现 yt-dlp 还满热门的,想用其下载视频及字幕方便继续学习英语精听,作为替代方案。具体需求如下:

  • urls.txt 批量下载,高清画质(1080p+)
  • 自动获取字幕:原始语言 + 纯中文 + 双语合并
  • 字幕格式:ASS(支持样式),绿色显示,自适应分辨率
  • 双语字幕:英文第一行 + 中文第二行,统一在屏幕底部
  • Windows 11 环境,支持双击 .bat 和 bash 两种运行方式
  • Cookies 自动获取(不需要手动导出)

二、技术选型演变  

第一版:Go 脚本方案(失败)  

最初打算用 Go 写一个下载脚本,调用 os/exec 执行 yt-dlp 命令。

失败原因

  1. n challenge solving failed — Chocolatey 安装的 yt-dlp 属于第三方包,未捆绑 EJS 求解脚本,无法解 YouTube 的 JS 挑战
  2. android client does not support cookies — 同时指定了 android 客户端和 cookies,参数冲突
  3. 解决以上问题后,Go 脚本只是个 yt-dlp 的壳,没有附加价值

决策:放弃 Go,改用原生 yt-dlp + shell 脚本。删除 main.gogo.modytdl.exe

第二版:手动 cookies.txt(失败)  

尝试从浏览器导出 cookies.txt 交给 yt-dlp。

失败原因:YouTube 定期轮换 cookie,导出的文件很快失效,每次都要重新导出,维护成本高。

决策:改用 --cookies-from-browser,每次运行时直接从浏览器读取最新 cookies。

第三版:Chrome cookies 读取失败  

--cookies-from-browser chrome 时报 SQLite 数据库锁定错误。

根本原因:Chrome 使用 SQLite 排他锁,运行时其他进程无法读取;而 Firefox 使用 WAL 模式,打开时也可以被读取。

解决方案:脚本检测 Chrome 进程,若运行中则自动降级到 Firefox,Firefox 未登录 YouTube 则提示用户登录。

# Chrome 运行检测
if tasklist.exe 2>/dev/null | grep -qi "chrome.exe"; then
    CHROME_RUNNING=true
fi

最终方案:原生 yt-dlp + Python 字幕后处理  

toolkits/yt-dlp/
├── urls.txt          ← 每次只维护这个,每行一个 URL
├── yt-dlp.conf       ← 下载配置
├── 2download.sh      ← bash 运行(日常使用)
├── download.bat      ← Windows 双击运行
├── merge_subs.py     ← 字幕后处理(Python)
└── README.md

三、关键问题与解决过程  

问题 1:n-challenge 失败  

YouTube 用 JS 挑战(n-challenge)对抗爬虫,yt-dlp 需要运行 JS 来求解。Chocolatey 版 yt-dlp 不含这个脚本。

解决:配置从 GitHub 自动下载 EJS 求解器:

# yt-dlp.conf
--js-runtimes node
--remote-components ejs:github
--extractor-args youtube:player_client=tv,web

首次运行会从 GitHub 下载一次,之后缓存本地。

问题 2:输出路径错误(D:yt-dlpDown 缺少反斜杠)  

yt-dlp.conf 中写:

-P D:\yt-dlpDown   ← 错误!

yt-dlp 的 conf 解析器会把反斜杠当转义字符吃掉,导致路径变成相对路径 D:yt-dlpDown

解决:所有路径改用正斜杠:

-P D:/yt-dlpDown   ← 正确
-o %(title)s/%(title)s.%(ext)s

问题 3:所有文件堆在同一目录  

默认输出模板 -o %(title)s.%(ext)s 把所有视频和字幕文件堆在 D:\yt-dlpDown\ 根目录,乱成一锅粥。

解决:改为按视频标题建子目录:

-o %(title)s/%(title)s.%(ext)s

每个视频的所有文件(视频 + 各语言字幕)都在自己的目录下:

D:\yt-dlpDown\
└── 视频标题\
    ├── 视频标题.mp4
    ├── 视频标题.en.ass
    ├── 视频标题.zh-Hans.ass
    └── 视频标题.en-zh.ass

问题 4:Bilibili 视频「有字幕」却无法提取  

下载 Bilibili 视频后,播放器明明显示有字幕,但脚本报告:

[skip] no soft subtitle streams found (burned-in subtitles cannot be extracted)

根本原因:字幕类型决定能否提取:

类型 特征 能否提取
软字幕(内嵌轨道) MKV/MP4 容器里独立的字幕轨,播放器可开关 可以,ffmpeg 提取
烧录字幕(硬编码) 文字直接画在视频画面像素上,播放器无法关闭 不行,只能 OCR

Bilibili 二创/搬运视频几乎全是烧录字幕,播放器显示的只是画面本身,不是独立的字幕轨道。

关键教训:这个问题在写代码之前无法预知,必须下载真实视频才能触发,是典型的「用了才发现」问题。

问题 5:已处理目录重复触发内嵌字幕检测  

添加内嵌字幕提取功能后,每次运行都会对已处理过的目录(有 .ass.srt)重新走 ffprobe 检测:

[subtitle] Vibe Coding in VS Code...
  [info] no external subtitles, checking embedded streams...
  [skip] no soft subtitle streams found

原因:新功能改变了逻辑,但旧的「已处理」状态判断没有同步更新。

修复(一行):

if not srts:
    if list(d.glob("*.ass")):
        return  # 已处理过,跳过

这类 bug 只有实际跑多个视频目录时才会暴露,单次测试发现不了。


四、字幕系统设计  

为什么选 ASS 而不是 SRT?  

需求 SRT ASS
绿色字幕 <font color> 标签,多数播放器忽略 原生 PrimaryColour 属性,所有主流播放器渲染
自适应字体大小 完全由播放器决定 PlayResX/Y 定义参考分辨率,播放器等比缩放
双语位置控制 无法控制 Alignment 精确控制每行位置

自适应字号原理  

PlayResY: 720        ← 参考分辨率(基准)
Fontsize: 30         ← 基于 720p 的字号

播放器渲染时自动换算:

$$\text{实际字号} = 30 \times \frac{\text{实际窗口高度}}{720}$$

  • 720p 窗口 → 30pt
  • 1080p 窗口 → 45pt
  • 2K 1440p 窗口 → 60pt

无论什么分辨率,字幕始终占窗口高度约 5%(单行),双语合计约 10%。

双语字幕实现  

同一个 Dialogue 事件内用内联标签切换字体,英文第一行 + 中文第二行,统一在底部:

Dialogue: 0,0:00:01.00,0:00:03.00,BiBottom,,0,0,0,,{\fnArial}Hello World\N{\fnMicrosoft YaHei}你好世界

通过时间区间合并(boundary split)保证每个时间段的中英文严格对齐。

原始语言自动检测  

脚本自动识别视频原始语言,按规则生成字幕:

_ZH_LANGS = {"zh-Hans", "zh-Hant", "zh", "zh-CN", "zh-TW"}

# 逻辑:
# - 原始为中文 → 只生成 zh-Hans.ass
# - 原始为其他语言 → 生成 原文.ass + zh-Hans.ass + 原文-zh.ass
视频语言 生成文件
英语 base.en.ass + base.zh-Hans.ass + base.en-zh.ass
日语 base.ja.ass + base.zh-Hans.ass + base.ja-zh.ass
中文 base.zh-Hans.ass(仅一个)

五、最终配置说明  

yt-dlp.conf  

# 输出路径(必须正斜杠)
-P D:/yt-dlpDown
-o %(title)s/%(title)s.%(ext)s

# 最高画质
-f bestvideo[ext=mp4]+bestaudio[ext=m4a]/bestvideo+bestaudio/best
--merge-output-format mp4

# n-challenge 修复
--js-runtimes node
--remote-components ejs:github
--extractor-args youtube:player_client=tv,web

# 字幕(覆盖常见语言)
--write-sub
--write-auto-sub
--sub-langs zh-Hans,zh-Hant,zh,en,ja,ko,fr,de,es,pt,it,ru,ar,hi,th,vi
--convert-subs srt

--no-playlist
--retries 5
--fragment-retries 5
--file-access-retries 3

Cookies 优先级逻辑  

cookies.txt 存在?
  └─ 是 → 用 cookies.txt(手动维护模式)
  └─ 否 → Chrome 目录存在?
             └─ 是 → Chrome 正在运行?
                        └─ 否 → 用 Chrome cookies(最优)
                        └─ 是 → Firefox 目录存在?
                                   └─ 是 → 用 Firefox cookies(需已登录 YouTube)
                                   └─ 否 → 报错退出
             └─ 否 → 用 Edge cookies

六、工程实践:AI 辅助开发的 Token 消耗分析  

本次消耗分布  

整个项目消耗了约 11% 月度订阅额度,主要浪费来源:

原因 估算占比
字幕需求反复迭代 6 次(每次读文件+改+测试) ~30%
每次调试都重新从 YouTube 下载字幕 ~20%
没有 instructions 文件,每次重复交代背景 ~20%
路径、cookies 等问题多轮排查 ~20%
对话超长触发压缩,摘要本身占 token ~10%

如果重来,可以节省 40%+  

关键改进点:

1. 先写 .github/copilot-instructions.md  

一次性固化项目背景,后续每次对话 AI 自动加载,无需重复交代:

## 项目:yt-dlp 下载工具
- 路径约定:conf 文件必须用正斜杠
- 输出:D:/yt-dlpDown/<title>/<title>.mp4
- 字幕:merge_subs.py,PlayResY=720,fontsize=30,ASS格式
- Cookies:优先 Firefox(Chrome 运行时锁库)

2. 先出方案,人工确认,再写代码  

字幕格式迭代了 6 次,每次都是 AI 直接改代码,用户看效果再反馈。

正确流程

用户:"字幕改成底部双行,英文上中文下"
AI:"计划修改:
     1. 删除 EnTop 样式,新增 BiBottom 样式(Alignment=2)
     2. _merge_bilingual() 改为生成单条 Dialogue
     3. 内联 \fn 标签切换字体
     确认后执行?"
用户:"确认"
AI:一次完成所有修改

3. 用本地 mock 数据测试  

每次调试字幕脚本,都重新从 YouTube 下载 SRT,浪费时间和 token。

正确做法:项目目录放一份 20 条的测试 SRT,调试时本地跑:

python merge_subs.py ./test   # 0.1秒,不需要网络

4. 只粘关键错误行  

低效:粘贴 100 行完整 yt-dlp 输出

高效:只粘最后 ERROR 行:

ERROR: Sign in to confirm you're not a bot.

七、日常使用流程  

# 1. 编辑 urls.txt,每行一个 YouTube URL
vim urls.txt

# 2. 运行下载(bash 方式)
bash 2download.sh

# 3. 或者直接双击 download.bat(Windows)

下载完成后自动处理字幕,输出结构:

D:\yt-dlpDown\
└── 视频标题\
    ├── 视频标题.mp4
    ├── 视频标题.en.ass        ← 纯英文,绿色,自适应
    ├── 视频标题.zh-Hans.ass   ← 纯中文,绿色,自适应
    └── 视频标题.en-zh.ass     ← 双语(英上中下),底部,绿色

八、依赖安装  

# Windows,用 Chocolatey
choco install yt-dlp ffmpeg nodejs python

# 首次运行 yt-dlp 会自动从 GitHub 下载 EJS 求解脚本(需要网络)

Firefox 需提前登录 YouTube(一次性操作)。


九、总结  

这个项目本质上只是「配置 yt-dlp + 写一个字幕后处理脚本」,核心代码不超过 200 行。但整个过程消耗了大量时间和 token,教训分两类:

可以提前避免的消耗:

  1. 明确需求再动手:字幕位置、颜色、大小没有在最开始明确,导致反复迭代 6 次
  2. 理解工具特性:yt-dlp conf 的反斜杠问题、Chrome SQLite 锁,应该先查文档
  3. 测试数据本地化:依赖网络的测试拖慢了整个调试循环
  4. 用 instructions 文件固化背景:避免每次对话从零开始交代项目背景

本来就无法节省的消耗(探索成本):

  • 烧录字幕 vs 软字幕——不下真实视频不会知道 Bilibili 是烧录的
  • 新功能引入的状态判断 bug——只有跑多个真实目录才会暴露
  • 字幕在 2K 屏显示太大——不在目标屏幕上看不会发现
  • Chrome SQLite 锁——不在真实浏览器环境不会触发

这类「用了才发现」的问题是工程探索的正常成本,不必强求节省。

合理预期:优化前期准备可以节省 30-50% 的 token;运行时发现的问题,该花的还是要花。

AI 辅助开发的本质不是「让 AI 替你思考」,而是「把你的思考高效地转化为代码」。需求越清晰,AI 越高效,token 消耗越少。


项目代码:toolkits/yt-dlp/ 目录下所有文件


Comments

Follow me

I'm involved in Kubernetes coding and share developer memes.