YouTube 下载 1080p:yt-dlp 实战与自建服务
本文讲的是用开源工具
yt-dlp在本地下载,以及把它包成一个只给自己用的小服务(在线聚合下载站为什么不再推荐,见下一节)。
部署时需要的两件事另见:代理怎么搭、Docker 怎么装。适用范围仅限你有权下载的内容:自己上传的视频、CC 授权作品、权利人明确允许下载的素材、以及所在司法辖区允许的个人存档。抓取受版权保护的内容并再分发是侵权;绕过平台的付费/DRM 限制通常同时违反服务条款。越过上面这两条线,后果由你自己承担。
为什么不再推荐在线下载站
原来的做法是把视频链接粘给某个在线聚合下载站,让它在服务器上解析好再丢给你一个文件。这套玩法的代价在这几年逐渐显现:
- 可用性极差:这类站点的解析依赖 YouTube 的接口细节,平台一改就集体挂掉,域名换得比修得快。
- 注入式广告与跳转:免费站的变现方式通常是弹窗、套娃跳转、伪装成「下载」按钮的推广;解析页正是挂马和钓鱼的重灾区,因为访问者天然准备点「下载」。
- 隐私代价:你粘贴的是完整 URL(含视频 ID),部分站点还要求登录或装浏览器扩展。对「下载一个视频」这件事来说,这个代价不成比例。
- 分辨率虚标:页面上写着支持 4K,实际给的是合并好的 720p 单流(带音轨)。原因见下节。
- 随时可能消失:站点关停后,你之前的收藏、订阅、队列全部归零。
yt-dlp 把这些环节放回本地:解析规则是开源、可审计、社区维护的,下载的文件不经过第三方服务器,分辨率由你按格式表自己挑。
装好三件东西
yt-dlp 本体
1 | # macOS(Homebrew 提供的是自带更新能力的可执行文件) |
官方文档把发布渠道分成三条:stable(大致按月发布)、nightly(有代码变更当晚发布)、master。README 明确写着 stable 往往滞后且容易被外部变化打断,nightly 才是对普通用户的推荐渠道。理由是这个工具维护的解析逻辑对手每天都在改:
1 | yt-dlp -U # 更新到当前渠道的最新版 |
ffmpeg
1080p 几乎一定是「视频流 + 音频流分离」的 DASH 格式,合并必须靠 ffmpeg,缺了它你只能拿到无声画面或低清单流。
1 | brew install ffmpeg # macOS |
JavaScript 运行时(新的硬性依赖)
这一条是近两年才出现的:YouTube 的播放器挑战需要用 JS 引擎来解算,yt-dlp 现在把这段逻辑放在 yt-dlp-ejs 里执行,没有可用的 JS 运行时,某些客户端会被整体跳过。官方支持的运行时按优先级为 deno(推荐,默认启用)、node、quickjs、bun:
1 | brew install deno # 推荐:默认启用的那个 |
日常使用:从格式表开始
先看能拿到什么
任何「为什么只有 720p」的问题,第一步都是看格式表:
1 | yt-dlp -F "https://www.youtube.com/watch?v=VIDEO_ID" |
输出的是一张表:每行一个格式,带 ID、EXT、分辨率、码率、编码、协议、是否有音轨。两类格式要分清:
- 带音轨的单流(多是
18、22这类):下载即可播放,但分辨率通常封顶在 720p,且码率较低; - 纯视频流(
137、248之类,acodec显示为none):分辨率高,但必须和纯音频流(140等)合并。
选格式:优先用 -S 而不是硬编码 -f
不传任何参数时,yt-dlp 的默认选择等价于 -f bestvideo*+bestaudio/best,也就是尽力拿最好的画质并合并。真要限制,推荐用排序(-S)而不是过滤(-f):过滤写死了具体格式 ID,一旦这个 ID 在该视频上不存在,整条命令直接失败;排序则是在候选里挑最符合你偏好的那个。
1 | # 想要「不超过 1080p 的最佳画质」,用排序表达偏好 |
几个约定俗成的写法:bv* 是「最好的含视频流(可含音频)」,ba 是「最好的纯音频」,b 是「最好的单流」。/ 表示候选优先级,从左到右。合并容器可以指定:
1 | yt-dlp -S "res:1080" --merge-output-format mp4 "URL" |
输出命名与目录
默认文件名会把标题原样拼进去,中文和特殊字符容易出问题。模板变量用 -o 控制:
1 | yt-dlp -S "res:1080" \ |
%(title).80s 表示标题截断到 80 字符,%(upload_date>%Y-%m-%d)s 是日期格式转换,%(id)s 保留视频 ID 便于去重。音频单独抽出来(不需要 ffmpeg 也能跑,但要转码成 mp3 仍然需要):
1 | yt-dlp -x --audio-format mp3 "URL" |
字幕与元数据
1 | # 下载人工字幕(中文优先,其次英文),并嵌入视频 |
1080p 为什么经常拿不到
原因在 YouTube 侧:几道门槛叠在一起。
第一道:客户端与格式形态。 yt-dlp 通过不同的「客户端」身份去取格式表,不同客户端能看到的格式完全不同。默认值是 visionos,web;而 web 客户端目前只会提供 SABR 形态的流。如果环境里没有 JS 运行时,web 会被直接跳过——这就是「装了 yt-dlp 却拿不到高清」最常见的原因,装个 deno 往往立刻见效。想手动指定客户端:
1 | yt-dlp --extractor-args "youtube:player_client=tv,web_safari" -F "URL" |
web_safari 会提供 HLS 格式,tv 通常不需要 PO Token(但登录态缺失时格式可能带 DRM)。可选的客户端还有 web_embedded、mweb、ios、android_vr、tv_simply 等,各自的能力和限制不同,值得在拿不到格式时逐个试试。
第二道:PO Token。 部分客户端(如 web、mweb 的流媒体请求)要求请求里带一个能证明「请求来自真实客户端」的令牌。缺令牌的表现就是 403,或者干脆拿不到某些格式。这个令牌与视频 ID 绑定,也就是说每个视频都要新生成一个——所以官方已经不推荐手工抓取,而是推荐装一个 PO Token Provider 插件自动生成:
1 | # 示例:bgutil 的 provider 插件(由 yt-dlp 维护者维护,需按其文档部署本地服务) |
第三道:登录态。 年龄限制、会员专属、部分地区限制的内容需要账号 cookie:
1 | yt-dlp --cookies-from-browser chrome "URL" |
注意两点:运行时要先退出浏览器(cookie 数据库被锁会读取失败);用账号 cookie 下载会把这些行为记到你的账号上,别拿主账号跑大批量任务。
403 与限流的排查顺序
按这个顺序试,基本能覆盖绝大多数失败:
- 先更新到 nightly:
yt-dlp --update-to nightly。YouTube 的改动按周计,stable 经常已经过期。 - 确认 JS 运行时可用:
deno --version、node -v能跑通,且deno在PATH里。 - 打印请求过程看失败在哪一步:
yt-dlp -v "URL",关注是取格式表失败,还是取媒体流 403。 - 换客户端:
--extractor-args "youtube:player_client=tv,web_safari"。 - 加 cookie:
--cookies-from-browser firefox(参见上面的注意事项)。 - 上 PO Token Provider 插件:走到这一步基本能确定是令牌问题。
- 降速:被限流时给请求之间加间隔、限制并发分片数。
1 | yt-dlp --sleep-requests 1 --min-sleep-interval 2 --max-sleep-interval 5 \ |
-N 是并发下载的分片数(默认 1),调高能提速,但也更容易触发限流;--limit-rate 限制速率,长任务里能显著降低被封的概率。
包成一个只给自己用的小服务
命令行适合自己用,但如果你有 NAS、家庭服务器,或者希望手机也能用,把它包成一个 HTTP 服务更顺手。关键设计原则是不要裸奔在公网上——一个开放的 YouTube 代理会被迅速扫到并滥用,最后是出口 IP 被封。
1 | # app.py —— 最小可用的下载服务:单用户、带令牌鉴权、串行队列 |
几点说明:
- 鉴权是必须的,且令牌从环境变量读、没有默认值——避免「忘了配就裸奔」。
- 串行队列是刻意设计:并发下载会放大被限流的概率,家用场景下排队完全够用。
- URL 白名单只放行 YouTube 域名,防止这个服务被当成通用 SSRF 跳板。
- 想再收紧一层,用
docker run --cpus 1 -m 512m限制资源,并在前面套一层带 HTTPS 的反向代理。
1 | # deno 用官方静态二进制镜像取:yt-dlp 提取 YouTube 需要 JS 运行时,且默认只认 deno |
同目录再放一个 .dockerignore,否则你下载到 ./data 的视频会被当成构建上下文整包发给 Docker 守护进程:
1 | data/ |
1 | # 容器以 uid 10001 运行,宿主目录属主要对得上 |
几个和「能跑起来」直接相关的点:
- JS 运行时不是可选项。yt-dlp 从 PyPI 装的时候,
[default]这个附加依赖才会带上yt-dlp-ejs与解析挑战所需的依赖;裸写pip install yt-dlp装不上 ejs,容器里也没有任何 JS 引擎,YouTube 的提取会退化或失败。上面三条docker exec就是用来确认这件事的。 --restart unless-stopped:进程崩了或机器重启会自动拉起,手动stop之后不会被强行拉回。已经在跑的容器可以用docker update --restart unless-stopped ytdlp补上。- 日志要设上限:
json-file驱动默认不限制大小,而这个服务会持续往 stdout 写下载日志。 - 不要写
VOLUME ["/data"]:它只会在你忘记挂载时悄悄建一个匿名卷,数据藏在那里很难找。-v是必须的。 -p 127.0.0.1:8000:8000只监听本机回环地址,需要外网访问时通过反向代理加认证暴露,不要直接-p 8000:8000。- 容器里的 yt-dlp 随镜像固化,升级方式就是重建镜像:改上面的版本号(或把
YTDLP_SPEC换成 nightly 的--pre安装方式)再docker build。YouTube 侧的改动按周计,建议每月重建一次。 - 资源限制不是可选项。这个服务会替别人下载文件、调用外部命令,一旦被滥用(有人扫到端口、或者一个超长视频占满内存),代价是整台机器:
--memory与--memory-swap相等表示禁用 swap,--cpus限核数,--pids-limit防 fork bomb。设备紧张时还可以加--read-only --tmpfs /tmp与--security-opt no-new-privileges。 - 代理有三层,别只看一层:守护进程拉镜像、容器内进程出网、
docker build的RUN步骤。上面第二条docker run解决的是中间那层,另外两层的配置见 sing-box 代理与 Docker 那节。 - 数据都在
-v挂载的宿主目录里,备份直接 tar 这个目录即可;如果改用命名卷,回收前先看docker volume ls,匿名卷要用docker volume prune才清得掉。
替代方案
- cobalt:另一个开源下载工具,前端 + API 一体,有公开实例,也支持自托管(官方仓库的
docs/run-an-instance.md有部署与「如何保护实例」的说明)。它的定位是「下载自由且公开可访问的内容」,服务端不缓存文件、相当于一个带解析能力的代理。想要开箱即用又不想自建解析逻辑,它是目前体验最接近「粘链接、拿文件」的方案。 - NewPipe(Android):手机端开源客户端,内置下载与后台播放,不需要 Google 服务框架。
- JDownloader:老牌桌面下载管理器,站点支持面广,适合批量任务与网盘,代价是界面和依赖都偏重。
维护提示
这类工具的成本主要在持续更新:YouTube 侧的改动是常态,nightly 渠道的存在就是为了跟上它。建议把更新做成例行任务(比如每周 yt-dlp --update-to nightly),或者在自建服务里直接用镜像重建来承担更新。遇到突然大批量 403,先更新再排查。
总结
- 在线聚合下载站是「可用性、隐私、安全」三重折价,能用开源工具就别用它们。
- yt-dlp 装三件套:本体(建议 nightly)+ ffmpeg(合并音视频)+ JS 运行时(deno,新的硬性依赖)。
- 选格式用
-S "res:1080"这类排序表达偏好,只有必须卡死条件时才用-f过滤;-F是排障第一步。 - 拿不到 1080p 通常是撞在客户端、PO Token、登录态三道门槛之一。
- 要长期用就自建一个带鉴权、只监听回环、串行队列的小服务,别把解析能力裸奔在公网上。
参考资料
系列索引:网络与自建服务




