抖音下载工具 dy-dl 的设计与实现
抖音下载工具 dy-dl 的设计与实现
dy-dl 是一个抖音视频/图集无水印下载工具,核心功能是粘贴分享链接后解析出最高画质的无水印视频或原图。
整体架构
前端用 Next.js + framer-motion 做交互动画。后端核心是一个 Puppeteer 实例,模拟浏览器访问抖音页面来绕过反爬限制。
用户流程:
1. 粘贴 v.douyin.com/xxx 格式的分享链接
2. 服务端解析真实 URL,提取视频/图集 ID
3. 通过抖音内部 API 获取最高画质的下载地址
4. 前端展示多画质选项,点击下载
链接解析
抖音的分享链接是短链格式 https://v.douyin.com/xxxxx/。访问这个短链有两种可能:
- HTTP 302 重定向到真实 URL
- 返回 HTML 页面,页面内包含一个带有真实 URL 的 标签
两种都需要处理。重定向直接读 Location 头,HTML 则用正则提取跳转链接。
拿到真实 URL 后,从路径中提取视频 ID(/video/{id})或笔记 ID(/note/{id})。这两种 ID 分别对应视频和图集(多图)。
数据获取
不使用抖音公开的 Web API(需要签名参数 _signature 或 X-Bogus,生成过程复杂),而是用 Puppeteer 加载抖音页面,拦截页面内部发起的 aweme/v1/web/aweme/detail/ 请求来获取原始 API 数据。
这个内部 API 返回的数据包含所有画质信息:
aweme_detail.video.bit_rate[]
├── play_addr.url_list[] (各 CDN 节点地址)
├── bit_rate (码率,单位 bps)
└── play_addr.width/height (分辨率)
Puppeteer 的启动配置需要绕过一些检测:
const browser = await puppeteer.launch({
headless: true,
args: [
"--no-sandbox",
"--disable-blink-features=AutomationControlled",
],
});
用 page.evaluateOnNewDocument 覆盖 navigator.webdriver 属性,防止抖音检测到自动化工具。
Cookie 的作用
不带 Cookie 访问也能拿到数据,但画质选项会少很多——最高只能拿到 720p。带上登录后的 Cookie 才能解锁 1080p 甚至 4K 画质。
因此设置页提供了两种方式:
- 手机扫码登录:通过二维码 session 自动获取 Cookie
- 手动粘贴:从浏览器开发者工具复制 Cookie 字符串
Cookie 存储在 localStorage 中,请求时通过 x-douyin-cookie 请求头发送到服务端。
文件下载
视频文件托管在抖音的 CDN(douyinvod.com)上,直接在前端发起下载会碰到 CORS 限制。解决方案是通过自己的服务端代理下载:
/api/download?url=
服务端收到请求后,带上 Referer: https://www.douyin.com/ 请求头去拉 CDN 文件,然后把流透传给前端。这样既绕过 CORS,又能用 Content-Disposition 控制下载文件名。
为了显示文件大小,在解析阶段会并行对每个视频源发 HEAD 请求读取 Content-Length。超时 4 秒没响应就跳过大小显示。
画质筛选逻辑
解析出来的视频源可能有十几个(不同 CDN 节点 + 不同码率),需要去重和排序:
1. 按 URL 去重(去掉同一个文件的不同镜像节点)
2. 过滤掉带水印的源
3. 按码率从高到低排序
4. 根据用户选择的分辨率筛选
"自动"模式会选择所有源中分辨率最高的那个,而不是显示所有可选项。手动模式则选择用户指定的分辨率。
动图识别
一个意料之外的情况——抖音的"动图"(live photo)本质上是一张静态图片配一段短视频,API 返回的类型是 images(图集),但实际内容却是视频。处理的逻辑是:如果 images 数组长度为 1 且包含 live_photo_type 和 video 字段,就按视频处理而非图集。
Puppeteer 的内存管理
Puppeteer 实例如果长时间不关闭,内存占用会持续增长(每次页面加载都会累积缓存)。做法是在每次解析后主动关闭页面 page.close(),但保留浏览器实例 browser 复用。浏览器实例在第一次请求时懒加载,后续请求复用同一个实例。
即使这样,运行几天后 Chrome 进程的内存占用还是会涨到 200MB 以上。折中方案是定期重启——在连续处理一定数量的请求后关闭浏览器实例重新创建。