项目定位

shayu-parser 是我把短视频解析能力从 MCP 工具里抽出来之后做的一层独立服务。它的目标很直接:用户丢进来一段分享文本或 URL,后端自动识别平台,然后返回标题、作者、封面、媒体地址、请求头和可播放/可下载的辅助信息。

它覆盖的平台比较多:抖音、B 站、小红书、快手、好看视频、微博、今日头条、豆包 AI、即梦 AI、最右、皮皮虾、皮皮搞笑,以及 Kuku 第三方兜底。

这个项目的边界也很明确:只处理公开分享页,不绕过登录、会员、DRM、付费内容或平台访问控制。很多直链本身有过期时间、Referer 约束和地域策略,所以服务返回的是“当下可用的播放材料”,不是永久资源。

架构

Static Frontend / Android / Other Client
      |
      | POST /api/v1/extract
      v
FastAPI main.py
      |
      v
services/extractor.py
      |
      v
services/registry.py
      |
      +--> platforms/douyin.py
      +--> platforms/bilibili.py
      +--> platforms/xiaohongshu.py
      +--> platforms/kuaishou.py
      +--> platforms/haokan.py
      +--> platforms/...

后端入口是 FastAPI。main.py 负责 CORS、健康检查、平台列表、单条解析、批量解析、媒体代理和 B 站 DASH 合并接口。

真正的调度逻辑在 services/extractor.py

  1. 清理输入文本。
  2. 如果是 auto 模式,按 host 自动识别平台。
  3. 查询短期内存缓存,避免同一链接反复打平台页面。
  4. 调用注册表中的平台解析器。
  5. 本地解析失败时,按配置走 Kuku 兜底。
  6. 统一补充 platform_nameplatform_capabilitiesmedia_urlscompliance_note

平台路由由 services/registry.py 管理。每个平台都是一个 PlatformParser,里面记录 key、名称、hosts、支持模式、是否需要 headers,以及实际 parser 函数。新增平台时不需要改 API 层,只要注册进去即可。

接口合同

核心接口是:

GET  /health
GET  /api/v1/platforms
POST /api/v1/extract
POST /api/v1/extract/batch
GET  /api/v1/media-proxy
GET  /api/v1/remux

单条解析请求大概是:

{
  "url": "https://v.douyin.com/xxxx/",
  "platform": "auto",
  "timeout_seconds": 20,
  "client": "web"
}

成功响应会尽量统一成这些字段:

{
  "success": true,
  "api_version": "v1",
  "request_id": "...",
  "duration_ms": 123.45,
  "platform": "douyin",
  "platform_name": "抖音",
  "mode": "video",
  "title": "...",
  "cover": "...",
  "author": "...",
  "video_url": "...",
  "audio_url": "",
  "images": [],
  "media_urls": ["..."],
  "request_headers": {},
  "source_type": "local_parser",
  "fallback_used": false,
  "cache_hit": false,
  "single_file": true
}

这个结构有两个好处。第一,Web、Android、MCP 或其它调用方可以先读通用字段,不需要知道每个平台的内部差异。第二,平台特殊字段仍然能保留下来,比如 B 站 DASH 的 audio_urlrequest_headers

平台解析器

每个平台解析器都保持同一个函数形态:

def parse_xxx_url(input_value: str, timeout_seconds: float) -> dict:
    ...

这样注册表只需要保存函数引用,API 层完全不关心平台细节。

抖音解析走公开分享页 SSR 数据,提取 video.play_addr token 后探测 aweme/v1/play。B 站优先尝试公开视频的合流 durl,拿不到时返回 DASH 音视频分离轨。小红书、快手、皮皮虾一类平台则要同时考虑视频、图文、实况图等模式。

如果某个平台本地解析失败,extractor 会把失败原因塞进 fallback_reason,再尝试 Kuku 兜底。这样调用方能知道结果来自本地 parser 还是第三方兜底。

媒体代理

浏览器直接播放解析到的直链时,经常会遇到两个问题:

  1. CDN 要求 Referer 或 User-Agent。
  2. 浏览器 <video> 标签没法给每个平台单独附加复杂请求头。

所以项目里有 /api/v1/media-proxy。它会:

  • 校验媒体 URL,只允许 http/https。
  • 拦截 localhost、内网 IP、回环地址、link-local 地址。
  • 对每一次 3xx 跳转继续校验目标地址。
  • 根据 CDN 域名推断 Referer。
  • 透传 Range,保证播放器拖动进度条时能按需加载。

这块是安全边界,不能只把用户传来的 URL 丢给 requests.get。否则公网部署时很容易变成 SSRF 入口。

B 站 remux

B 站公开网页经常只返回 DASH:视频 .m4s 和音频 .m4s 分开。Web 播放器能处理,但普通下载器或 Android 下载体验就不太好。

/api/v1/remux 做的事情是:先对 video_urlaudio_url 做和媒体代理一样的外部 URL 校验,然后调用 ffmpeg:

ffmpeg -hide_banner -loglevel error -y \
  -headers "User-Agent: ...\r\nReferer: ...\r\n" -i <video_url> \
  -headers "User-Agent: ...\r\nReferer: ...\r\n" -i <audio_url> \
  -map 0:v:0 -map 1:a:0 -c copy -movflags +faststart output.mp4

它不是重新编码,只是 stream copy,所以速度和画质都比较可控。但 remux 会占服务器带宽、临时磁盘和 CPU/IO,所以项目里加了进程内并发闸门,默认同时 2 个任务,超限返回 429 remux_busy

缓存和批量

解析成功结果有短期内存缓存,默认 TTL 120 秒,最多 256 条。这样同一个链接在 Web 页面里反复点、Android 重试、批量任务重复提交时,不会每次都去打平台页面。

批量接口 /api/v1/extract/batchasyncio.Semaphore 控制并发。每条失败不会拖垮整个批次,响应里会返回 success_countfailed_count 和每条结果。

我觉得这个项目有意思的点

shayu-parser 不是把一堆解析脚本简单堆起来,而是把“平台差异”压在 platforms/ 下面,把“产品接口”稳定在 /api/v1/* 上面。

真正可维护的地方在这几条:

  • 每个平台一份 parser,坏了只修自己。
  • 注册表描述平台能力,让前端和客户端可以动态展示。
  • 统一响应字段,让调用方不需要到处写平台判断。
  • 媒体代理和 remux 单独做安全校验,不把下载链路混进 parser。
  • 对外明确只支持公开内容,不把项目设计成绕过访问控制的工具。

这个项目后面最适合继续演进的方向,是给平台解析器补更多 fixture 测试,把 remux 做成异步任务队列,再给公网部署加认证、限流和日志脱敏。