Warning
该项目正在重构整改,接口与行为可能随时调整,接入前请先确认最新实现。
短视频解析 API:给一条分享链接,返回无水印视频 / 图集直链。v2.1 现支持 9 个平台:抖音、小红书、快手、B 站、微博、X/Twitter、YouTube、Instagram Reels、TikTok。默认返回直链(客户端请求时不带 Referer 即可下载),服务器零带宽;/media 中转作为可选增强。
- 9 个平台统一 JSON 信封,直链无水印
- 纯 HTTP 主通道,默认零浏览器依赖;风控自动降级浏览器兜底
- 缓存、限流、结构化日志、Prometheus 指标开箱即用
- 一条命令 Docker 部署,镜像 495MB 起
Note
本项目只做链接解析:收一条分享链接,返回直链地址,不存储、不转存任何内容。请仅解析自己有权访问的内容(自己的作品或已获授权的素材),使用行为由使用者自行负责。文中平台名称与商标归其各自所有者。
前置:装好 Bun(1.2+)。
git clone https://github.com/hey-yx/short-video-extractor.git
cd short-video-extractor
bun install
bun dev # 启动开发服务,默认端口 7777
curl http://127.0.0.1:7777/health # 服务起来了会返回 {"status":"ok",...}拿一条分享链接就能解析:
curl -X POST http://127.0.0.1:7777/parse \
-H 'Content-Type: application/json' \
-d '{"url": "<分享链接>"}'返回统一信封(见 API),data.assets[].url 就是无水印直链,直接下载即可(不带 Referer)。不想装环境可以直接 docker compose up -d,见部署。
更多场景(选清晰度、图集、YouTube 中转、错误码处置、限流)见 docs/调用案例.md。
请求流程:match(识别平台)→ 缓存 → dispatch(短链展开)→ fetch(平台适配层)→ parse(纯函数)→ 统一信封返回。
src/
├── index.ts # 入口
├── bootstrap/{app,server}.ts # createApp()(测试可直接调)、监听与优雅关闭
├── routes/
│ ├── parse.routes.ts # POST|GET /parse(限流、缓存、指标打点)
│ ├── media.routes.ts # GET|HEAD /media(可选流式中转)
│ └── ops.routes.ts # /health /ready /platforms /metrics
├── services/
│ ├── parse.service.ts # 编排:match → 缓存 → dispatch → fetch → parse
│ ├── cache.ts # LRU + expiresAt 语义 + 负缓存 + singleflight
│ ├── limiter.ts # 按 IP 令牌桶限流
│ └── media-proxy.ts # HMAC 签名 + SSRF 白名单 + 手动跟 302 + Range 透传
├── platforms/ # 插件式平台层:加平台 = 加目录 + app.ts 注册一行
│ ├── douyin/ xhs/ kuaishou/ bilibili/ weibo/ twitter/ youtube/ instagram/ tiktok/
│ └── registry.ts hosts.ts
├── core/ # 错误码、统一信封、URL 工具
├── infra/ # pino 日志、指标、Puppeteer 浏览器网关
└── types/platform.ts # 统一数据模型(MediaAsset / ParseResult / FetchCtx)
平台层三条硬约束:platforms/** 不 import Elysia、不读 env、不 console.log;parse() 是纯函数(同输入同输出),用 fixture 离线回归;IO 只经 fetch() 和注入的 FetchCtx。平台改版时表现是「单测先红」,不是线上悄悄返回空结果。
技术栈:Bun + Elysia。运行时依赖只有 elysia、@elysiajs/cors、lru-cache、pino、puppeteer(仅浏览器兜底路径需要:抖音风控降级、小红书降级兜底;纯 HTTP 主通道零浏览器依赖)。
- 抖音三级降级链(L1 → L2 → L3):默认纯 HTTP L1(零签名 detail 接口);L1 被风控(403
ArgusSecurityPlugin)时先降级 L2 纯 HTTP 签名通道(标准指纹参数 +uifid/timestamp/x-secsdk-web-signature签名 +ttwidcookie,需配DOUYIN_UIFID,配方与实测见docs/research/01§12),L2 也失效(403 / 200 空 body)才进 L3 浏览器兜底:打开作品页截页面自己发出的 detail XHR JSON。via字段如实标注http/http-l2/browser,降级计数见channel_downgrade_total(from/to 按http-l1→http-l2→browser逐级标注)。L1 风控信号(RISK_CONTROL)与 L2 签名失效(UPSTREAM_CHANGED)在错误码与打点上分开,便于区分「口子被堵」和「签名过期要重铸 uifid」。 - 熔断粘性:HTTP 通道连续 2 次命中风控后,该平台 60s 内固定走浏览器,不再每个请求先吃一次 403 再冷启动浏览器(省 1-3s 延迟,也不加重风控画像);60s 探针窗口到点放一个请求回 HTTP 试探,试探成功即回到纯 HTTP。状态见
channel_breaker_open{platform}gauge。 - 风控止血:无浏览器兜底可用(或
BROWSER_FALLBACK=off)时,最终风控会让该平台进入 45s 止血窗口——窗口内所有请求直接 503,不再全额撞上游(重试风控=加重风控);缓存命中与负缓存不受窗口影响。有兜底时降级在 adapter 内部消化,不进窗口;开关切回 on 后窗口立即失效,恢复降级链。 - 纯 HTTP 部署开关:
BROWSER_FALLBACK=off时不注入浏览器能力,风控如实 503;适合不想跑 Chrome 的部署(省 400-600 MB 常驻内存,puppeteer 依赖可移除)。 - 瞬态重试:
UPSTREAM_TIMEOUT/UPSTREAM_ERROR(网络层与上游 5xx)在编排层重试 1 次,指数退避(500ms→1s),总耗时受请求 15s 上限约束;RISK_CONTROL、内容类与参数类错误一律不重试(重试风控=加重风控)。计数见parse_retry_total。 - 浏览器并发闸:所有浏览器通道共享一个信号量,最多 2 个并发 page,超出的排队,排队期间随请求取消(AbortSignal)立即退出,防止并发打满内存(Chrome 每页约 30-110 MB)。
- 风控如实分类:小红书
error_code=300012(IP at risk)等错误页归RISK_CONTROL(503,带可读文案),不冒充改版。
9 个平台全部接入统一 ParseResult 信封;通道与能力以各 adapter 的 capabilities 声明 + docs/research/ 实测记录为准:
| 平台 | 通道 | 支持内容 | 清晰度档位 | 直链绑 IP |
|---|---|---|---|---|
| 抖音 | 三级降级链:L1 零签名 detail 接口(主)→ L2 纯 HTTP 签名通道(配 DOUYIN_UIFID 后启用)→ 浏览器截 detail XHR(兜底);play 直链 |
视频、图集、实况照片 | 540p/720p/1080p/原片四档(另附 720p 水印档) | 否(play 直链空 Referer 可下) |
| 小红书 | 纯 HTTP 主通道(请求头三件套放行,见下)+ 风控/登录墙自动降级浏览器兜底 | 笔记视频、图集图片 | 视频出 master 流(h264 优先);图片原图 | 否 |
| 快手 | 纯 HTTP(短链 302 取 photoId + 移动端分享页 INIT_STATE,零 cookie) | 视频、图集 | 服务端返回的全部分辨率(按高度排序,最高档为主 URL) | 否 |
| B 站 | 纯 HTTP(view + playurl try_look=1) | 视频(多 P 逐个出 asset)、DASH 音视频分离 | 360P-4K 按 DASH id 映射(120=4K、116=1080P60、80=1080P…) | 否 |
| 微博 | 纯 HTTP(statuses/show 匿名 JSON;视频直链页 fid 换 mid;t.cn 短链 302 展开) | 视频、图集 | 1080P/720P/480P 等服务端给的档位(component 档位比 show 全) | 否 |
| X/Twitter | 纯 HTTP(cdn.syndication.twimg.com/tweet-result,token=0 免签名) | 单条推文视频、GIF、图集 | mp4 按 bitrate 多档 + HLS 备选 | 否(twimg 直链不校验出口 IP) |
| YouTube / Shorts | 纯 API(innertube /player,ANDROID_VR→IOS 失败自动换 client,免 PO Token 免 cookie) | 视频、Shorts | 144p-2160p 按源返回(ANDROID_VR 可到 4K),DASH 音视频分离 + 合并档 | 是(googlevideo 直链绑解析出口 IP,约 6h 过期,直链须经 /media 服务端中转,中转与解析走同一代理出口) |
| Instagram Reels | 纯 HTTP(匿名 GraphQL doc_id 口子,免登录免 cookie,需配 IG_GRAPHQL_DOC_ID) |
Reels、Post、IGTV 视频、图文图集 | mp4 按源返回 | 否(fbcdn 直链无 ip= 绑定) |
| TikTok | 纯 HTTP(tikwm 公开接口:无水印直链/图集/音乐;解析出站走代理) | 视频、图集、音乐 | 无水印 play/hdplay 双档 | 否(tiktokcdn 直链不绑 IP) |
代理:国内平台直连不走代理;海外平台(YouTube 等)经 PROXY_<PLATFORM> / HTTPS_PROXY 出站代理(见环境变量)。
所有 JSON 响应走统一信封:
{ "success": true, "data": { ... } }
{ "success": false, "data": null, "error": { "code": "RISK_CONTROL", "message": "..." } }主接口。Content-Type: application/json,body {"url": "<分享链接>"}。支持分享短链(v.douyin.com、xhslink.com、v.kuaishou.com、b23.tv、t.cn、youtu.be 等,自动展开)。
data(ParseResult)核心字段:
| 字段 | 说明 |
|---|---|
platform / id / kind |
平台、平台内 ID、video | album |
title / description / tags / author |
元数据 |
assets[] |
资源列表,见下 |
expiresAt |
所有直链里最早过期的时刻(Unix 秒),缓存与重取依据 |
via |
解析通道:http 或 browser(取数语义,全平台统一) |
assets[](MediaAsset):type(video/image/audio/cover)、url(直连地址,客户端请求时不要带 Referer;YouTube 除外:googlevideo 直链绑解析出口 IP,必须经 /media 服务端中转下载)、variants[](同资源的其他档位,抖音给全四档)、width / height / durationMs / size、backups[](备用 CDN)、liveClipUrl(实况照片的 5s 视频)。B 站与 YouTube 视频另带 audioUrl(DASH 音轨,需与视频分别拉取后合成)。
错误码与 HTTP 状态:INVALID_URL(400)、UNSUPPORTED_PLATFORM(422)、CONTENT_NOT_FOUND(404)、CONTENT_RESTRICTED(403)、RISK_CONTROL(503)、UPSTREAM_ERROR / UPSTREAM_CHANGED(502)、UPSTREAM_TIMEOUT(504)、RATE_LIMITED(429)、MEDIA_*(见下)、INTERNAL(500)。
流式中转,三种用途:强制下载文件名(Content-Disposition)、兜底会强制带 Referer 的容器(微信内置浏览器等)、链接过期后透明重取。
调用 /parse 的客户端用不到这个接口:正常情况直链拿来就能用,无需任何配置。它只服务两类场景:YouTube 直链绑 IP 必须经服务端中转(见平台状态);自部署的维护者想要上面三种增强能力。
Note
配置 PROXY_SECRET 后启用(未配置返回 501,直链主路径不受影响)。
sig= HMAC-SHA256(PROXY_SECRET,"<url>\n<exp>") 前 32 位十六进制,有效期 600 秒;过期 410、篡改 403。- 上游域名白名单(平台分享域 + CDN 域),302 每跳重新校验(SSRF 防护);并发上限 2;支持 Range / HEAD。
存活探针:进程活着即 200 {"status":"ok","uptime":...},不查外部依赖(防外部抖动引发连环重启)。Docker HEALTHCHECK 打这里。
就绪探针:平台适配器注册齐才 200,否则 503。
已注册平台自描述(名称、说明、识别的域名、能力声明)。
Prometheus 文本格式(自写渲染,未引 prom-client):
| 指标 | 类型 | 说明 |
|---|---|---|
parse_total{platform,code} |
counter | 解析计数,code 为 ok 或业务错误码 |
parse_duration_ms |
histogram | 解析耗时分布(毫秒,按 platform) |
cache_hit_total |
counter | 缓存命中次数 |
upstream_error_total{platform,reason} |
counter | 上游失败/风控次数 |
channel_downgrade_total{platform,from,to} |
counter | 解析通道自动降级次数(抖音/小红书 http→browser) |
parse_retry_total{platform} |
counter | 瞬态错误(超时/5xx)重试次数 |
channel_breaker_open{platform} |
gauge | 通道熔断粘性状态(1 = 该平台 HTTP 通道熔断中,固定走浏览器) |
- 结构化日志:pino,一行一请求 JSON。每请求生成/透传
x-request-id(响应头回写,日志字段reqId),访问日志含 method、path、status、耗时,平台解析事件另带 platform / id / 耗时。 - 敏感信息脱敏:所有日志输出经深度 redact,URL 的 query 值整体打码(防
xsec_token等凭据进日志),消息文本里的内联链接同样处理,Error 实例与 detail 递归脱敏。参数名保留,值一律REDACTED。 - 日志级别由
LOG_LEVEL控制(默认info)。
bun install
bun dev # 开发(--watch),默认端口 7777
bun test # 单测(全离线 fixture 回归)
bun run test:status # 一条命令看全状态:格式 / 类型 / 单测 / 覆盖率 / CI
bun run typecheckdocs/status.html 是零依赖检测站:状态总览、服务健康、平台状态、使用计数与解析演示一页全有。file:// 直接打开(手动填服务地址),部署后浏览器访问 http://<服务器>:7777/status 即看。
Tip
浏览器兜底路径(抖音风控降级、小红书降级兜底)需要本机有 Chrome(Puppeteer 自动管理);纯 HTTP 通道无此要求。
参考 .env.example。生产 .env 放服务器本地(chmod 600),不进仓库。
多阶段构建:oven/bun:alpine 编译单二进制 → alpine:3.22 + chromium 运行时(浏览器兜底路径需要真实浏览器内核)。非 root(uid 10001)运行,tini 收僵尸进程,HEALTHCHECK 打 /health。实测体积:arm64 495MB、amd64 527MB(初始 830MB);裁剪明细、构建后验收清单与 apk 镜像源切换见 docs/image-build-notes.md。apk 源默认走阿里云镜像(官方源实测仅 14KB/s),换官方源:docker build --build-arg ALPINE_MIRROR=dl-cdn.alpinelinux.org .
docker build -t short-video-extractor:v2 .
docker run -d --name short-video-extractor -p 7777:7777 \
--env-file .env --memory 800m --restart unless-stopped short-video-extractor:v2
curl http://127.0.0.1:7777/health或直接 compose(7777:7777、restart: unless-stopped、mem_limit 800m、env_file: .env、日志轮转 10m×3):
docker compose up -d重新构建镜像后 docker compose up -d,确认 /health、/ready 与每平台一条真实链接解析通过即完成,回滚就是换回旧镜像 tag。偶尔跑一次 docker image prune 清构建缓存。
服务自带状态页:浏览器打开 http://<服务器>:7777/status,一眼看到服务是否正常、各平台状态、解析成功/失败计数与最近的平台侧故障,每 10 秒自动刷新(可停)。同一张页 file:// 直开也能用(手动填服务地址)。页面内嵌在服务二进制里,没有额外依赖,也不引入监控组件。
仓库用 GitHub Actions 做持续集成:push 到 main 或提交 PR 时,GitHub 自动拉起干净环境运行 bun run typecheck 和全部单测,README 顶部的 CI 徽章显示结果(绿色 = 通过)。配置在 .github/workflows/test.yml,其中 PUPPETEER_SKIP_DOWNLOAD: "true" 避免 CI 拉浏览器。
docs/调用案例.md:面向调用方的场景示例(最简调用、图集、选清晰度、DASH 合成、YouTube 中转签名、错误码处置、限流)docs/开发者指南.md:本地开发、目录导览、代码约定、加平台步骤、测试写法与维护手册(改代码前从这里开始)docs/research/:平台 API 调研与实测记录(01 抖音/TikTok API、02 国内平台、03 架构、04 小红书 HTTP 通道、05 海外平台与出站代理)docs/image-build-notes.md:镜像裁剪明细与构建验收清单

