| name | media-fetch |
| description | 本地媒体下载工具,按 URL 自动分流。Douyin 走 Playwright + headless Chromium 抓无水印 mp4;Apple Podcasts 走 iTunes Search API 抓 mp3/m4a 直链。落到 ~/Downloads/media-fetch/。Use when 用户提供抖音链接 / Apple Podcasts 链接说"下载"/"保存"/"准备转字",或作为 asr skill 的前置步骤。 |
media-fetch Skill
把媒体 URL 落成本地音视频 + 同名 .info.json,供后续 asr 或人工查阅复用。
文件结构
skills/media-fetch/
├── SKILL.md # 本文件
├── scripts/fetch.py # 下载逻辑(按 URL 分流)
└── README.md # 一次性安装说明
When to Use
- 用户提供 Douyin / Apple Podcasts URL 且需要本地副本(无论后续是否做 ASR)
asr skill 被调用但用户只给了 URL 没给本地文件——先触发本 skill 拿到 MEDIA_PATH,再传给 asr
支持平台
| 平台 | URL 形态 | 解析路径 | 输出 |
|---|
| Douyin | v.douyin.com/xxx / www.douyin.com/video/xxx | Playwright 启动 headless Chromium,监听真实 CDN(douyinvod.com / zjcdn.com / aweme.snssdk.com)的 mp4 网络响应 + DOM <video>.src 双路径;过滤 placeholder(uuu_265 H265 探测视频)。长视频自动识别 DASH 分流(media-video-* + media-audio-*),下载两路后用 ffmpeg 合并 | .mp4 无水印视频(含音轨) |
| Apple Podcasts | podcasts.apple.com/{country}/podcast/{slug}/id{NUM}[?i={trackId}] | iTunes Search API(lookup?id={podcast_id}&entity=podcastEpisode)拿 episode 元数据 + episodeUrl 直链 | .mp3 或 .m4a(保留原始扩展名) |
依赖(首次使用前一次性配置)
brew install ffmpeg
/opt/homebrew/bin/python3 -m pip install --break-system-packages playwright
/opt/homebrew/bin/python3 -m playwright install chromium
playwright 装在 Homebrew Python /opt/homebrew/bin/python3(3.14)。fetch.py 命中 Douyin 时若当前解释器缺 playwright 会自动重入装了依赖的解释器(见 skills/_shared/interp),故 python3 fetch.py ... 开箱即用;自动找不到时设 INVEST_WIKI_PY=/path/to/python3 覆盖。
Apple Podcasts 路径不需要 Playwright / ffmpeg——iTunes API 公开 + HTTP 直下,纯标准库,任意解释器(含 Xcode python)可跑、不触发重入。playwright 仅在命中 Douyin 时才检查/导入;ffmpeg 仅在抖音命中 DASH 分流时才被调用。
历史:2026-05 之前 douyin 用 douyin-tiktok-scraper,因抖音 API 签名/msToken 失效而废弃;改用 Playwright 让抖音页面自然加载并签出 cookies,再监听网络请求拿无水印 mp4 URL。
执行
python3 skills/media-fetch/scripts/fetch.py "<URL>" [--target DIR]
参数:
<URL>:Douyin 分享 URL 或 Apple Podcasts URL,自动按域名分流
--target:输出目录,默认 ~/Downloads/media-fetch/(不入 wiki);想直接入 wiki 时传 raw/media/_inbox/ 等仓库内路径
stdout 契约
成功时最后一行输出:
MEDIA_PATH=/absolute/path/to/<file>
下游 skill(如 asr)通过 grep 这行拿到文件路径。所有进度提示走 stderr 不污染契约。
落库产物
每次调用写两个文件,文件名格式 <YYYY-MM-DD>_<platform>_<id>.<ext>:
| 平台 | 文件命名示例 | id 构成 |
|---|
| Douyin | 2026-05-14_douyin_7638154883836824883.mp4 | aweme_id |
| Apple Podcast | 2026-05-14_applepodcast_1500662719_1000766866358.m4a | {podcast_id}_{episode_track_id} |
同名 .info.json 含:platform / id / title / source_url / media_url / candidates / extra / fetched_at。Apple Podcasts 的 extra 字段额外含 podcast_name / release_date / feed_url / track_time_millis / description(节目简介,对后续 wiki 摘要很有用)。
衔接 asr
如果用户的真实意图是"转文字",本 skill 完成后自动接 asr skill:
用户:转文字 https://podcasts.apple.com/cn/podcast/xxx/idNNN?i=MMM
↓
触发 media-fetch → MEDIA_PATH=~/Downloads/media-fetch/2026-05-14_applepodcast_NNN_MMM.m4a
↓
触发 asr 传入该路径 → TRANSCRIPT_PATH=~/Downloads/asr-output/2026-05-14_applepodcast_NNN_MMM.md
Error Handling
通用:
| 现象 | 可能原因 | 处理 |
|---|
不支持的 URL | 不是 Douyin / Apple Podcasts 域名 | 检查 URL;若是新平台,按本 skill 扩展模式新增 fetch_<platform>() |
Douyin 特有:
| 现象 | 可能原因 | 处理 |
|---|
未捕获到视频网络请求 | 视频已删除 / 是图文/直播 / 抖音页面结构变更 / 视频加载慢于 60s 等待窗口 | 浏览器手动打开 URL 确认可播;若可播仍失败,临时改 headed 模式(去掉 headless=True)人工查看;或延长 fetch_douyin() 的等待循环上限 |
| 拿到很小的 mp4(< 1MB / < 5 秒) | 抖音改了 placeholder 视频文件名(当前过滤 uuu_265 / douyinstatic.com) | 检查 info.json 的 media_url,确认是不是新 placeholder;更新 DOUYIN_PLACEHOLDER_KEYWORDS |
ffmpeg 提示 Output file does not contain any stream 在下游 asr 中 | DASH 视频流被识别但音频流没识别成功,合并未触发 → 输出文件无音轨 | 检查 info.json 里 audio_media_url 是否为 null;若是,看 candidates 列表里是否有 media-audio-* URL 没被分类——可能是抖音又改了 URL 模式 |
ffmpeg 合并失败 | ffmpeg 不在 PATH / 音视频 codec 不兼容 -c copy | 确认 brew install ffmpeg;codec 问题改成 -c:v copy -c:a aac 重编音轨 |
下载失败 403 | 防盗链 / mp4 URL 过期(CDN signed URL 通常分钟级有效) | 立即重试;超过几分钟需重新拉一次 |
ERROR: 缺少依赖 playwright | playwright 没装进可被自动发现的解释器 | /opt/homebrew/bin/python3 -m pip install --break-system-packages playwright;或设 INVEST_WIKI_PY=/path/to/python3 |
Executable doesn't exist | 装了 playwright 包但没装 chromium | python3 -m playwright install chromium |
Apple Podcasts 特有:
| 现象 | 可能原因 | 处理 |
|---|
iTunes API 未返回任何 episode | podcast_id 不存在 / 该地区下架 | 用 country=us 参数重试(脚本未暴露,需改代码);或换 Apple Podcasts 其他地区入口 URL |
episode_id 不在最近 200 集内 | iTunes lookup hard cap 200 集,老节目可能漏 | 短期:用节目首页 URL(不带 ?i=)拿最新一集;长期:fetch.py 加 RSS 回退路径(解析 feedUrl 的 XML) |
下载失败 | 媒体 CDN 临时不可达 / 节目下架 | 浏览器直接打开 info.json 里 media_url 验证;持续失败说明该集已下架 |
不做什么
- 不做 ASR / 转写(交给
asr skill)
- 不抓评论 / 点赞 / 互动数据
- Douyin 不下载图文 / 直播流(仅普通视频)
- Apple Podcasts 不批量下整档节目(单次只下一集;批量需用户脚本循环调用)
- 不维护持久化的 douyin cookie——每次都用全新 browser context 让抖音自己签发