Podcast Download Split — 播客下载切分器
从小宇宙 (xiaoyuzhoufm.com) 下载播客音频与 Show Notes,自动转为 MP3,
并按 shownotes 时间轴把一集长播客切分成多个短音频片段。
Quick Start
python scripts/download.py "https://www.xiaoyuzhoufm.com/episode/abc123def456ghi789jklmno"
> 默认行为:下载 → 切分 → 清理。
> download.py 跑完会接着调用 split.py,按 shownotes 时间轴把长音频切成短片段,
> 落到 E:\下载\<播客名>-<单集标题>\;切分成功后自动删掉整期音频,
> 并把 Show Notes 一并归档到切分目录。
> 想保留整期文件加 set CLEAN_SRC=false;想整个跳过切分加 set AUTO_SPLIT=false。
Output
默认「下载 → 切分 → 清理」跑完后,一集只留一个文件夹(整期音频切完即删,不产生第二个副本):
E:\下载\PodcastName-EpisodeTitle\
|- 01_00m00s_开场.mp3
|- 02_02m18s_xxxx.mp3
|- ...
|- EpisodeTitle.md (Show Notes)
`- _时间轴.md (索引表:文件 / 时间点 / 内容)
Workflow
- 解析页面 — 读取单集页
__NEXT_DATA__中的props.pageProps.episode - 下载 m4a — 从
enclosure字段取 CDN 直链下载 - 转 MP3 — 骨传导耳机等设备需要 MP3 格式
- 删除 m4a — 节省磁盘(
KEEP_M4A=true可保留) - 保存 Show Notes — 把
shownotes的 HTML 片段转成 Markdown,落成同名.md - 自动切分 — 默认继续按 shownotes 时间轴把长音频切成短片段(
AUTO_SPLIT=true,默认开) - 写索引 — 在切分目录里生成
_时间轴.md,列出「文件 / 时间点 / 内容」 - 清理整期 — 切分成功后删掉整期音频,Show Notes 归档进切分目录(
CLEAN_SRC=true,默认开)
Requirements
| 依赖 | 必需 | 说明 |
|---|---|---|
| Python 3.8+ | 是 | 脚本运行环境,仅用标准库,无需 pip 安装 |
| ffmpeg | 推荐 | 转 MP3 用;缺失时自动降级为只保留 m4a |
本机(Windows,用户名 SUYONG)ffmpeg 已装好,脚本自动探测,无需配置:
C:\Users\SUYONG\.workbuddy\binaries\ffmpeg\bin\ffmpeg.exe (7.1, 含 libmp3lame)
> 这是本机的路径记录。换电脑请按下方「方式一」装一次,脚本会自动找到它。
查找顺序:FFMPEG 环境变量 → PATH → ~/.workbuddy/binaries/ffmpeg/bin/
→ WinGet Links/Packages → C:\ffmpeg\bin\
换机器或 ffmpeg 缺失时的安装方法
# 方式一(推荐,走国内镜像,约 1 分钟)
pip install imageio-ffmpeg -i https://mirrors.aliyun.com/pypi/simple/
python -c "import imageio_ffmpeg,shutil,os;d=os.path.expanduser('~/.workbuddy/binaries/ffmpeg/bin');os.makedirs(d,exist_ok=True);shutil.copy2(imageio_ffmpeg.get_ffmpeg_exe(),os.path.join(d,'ffmpeg.exe'))"
# 方式二(注意:下载源在 GitHub,国内可能长时间卡住甚至下不来)
winget install --id Gyan.FFmpeg -e
> 实测记录:winget 方式 11 分钟仍未下完(GitHub 直连被限速);
> 阿里云 PyPI 镜像 56 秒下完 31 MB wheel,解出 ffmpeg 7.1 静态包。优先走方式一。
Environment Variables
| Variable | Default | Description |
|---|---|---|
PODCAST_DIR |
自动 | 整期音频输出目录。默认:Windows 有 E 盘 → E:\下载,没有 → ~/Downloads |
AUDIO_QUALITY |
0 |
MP3 质量(0=最好,2=好,4=普通) |
KEEP_M4A |
false |
保留原始 m4a 文件 |
FFMPEG |
自动探测 | 手动指定 ffmpeg 可执行文件路径 |
AUTO_SPLIT |
true |
下载后自动按时间轴切分;设 false 可关闭 |
CLEAN_SRC |
true |
切分成功后删掉整期音频;设 false 可保留 |
SPLIT_DIR |
同 PODCAST_DIR |
切分产物的输出根目录 |
Windows 设置环境变量(当前窗口生效):
set PODCAST_DIR=D:\播客
set AUDIO_QUALITY=2
python scripts/download.py <URL>
Quick Reference
| Task | Command |
|---|---|
| 下载单集 | python scripts/download.py <单集URL> |
| 下载整个播客 | python scripts/download.py <播客主页URL> |
| 批量下载 | python scripts/download.py links.txt |
| 按时间轴切分 | python scripts/split.py <单集URL> |
| 自定义音质 | set AUDIO_QUALITY=2 && python scripts/download.py <URL> |
| 保留 m4a | set KEEP_M4A=true && python scripts/download.py <URL> |
| 只要整期、不切分 | set AUTO_SPLIT=false && python scripts/download.py <URL> |
| 切完保留整期文件 | set CLEAN_SRC=false && python scripts/download.py <URL> |
按时间轴切分(scripts/split.py)
把一集长播客按 shownotes 里的时间轴切成多个短音频,片段命名为
「序号_时间点_内容」,收纳进以「播客名-单集标题」命名的文件夹。
python scripts/split.py "https://www.xiaoyuzhoufm.com/episode/685986762a38b4d97928da10"
python scripts/split.py "<URL>" "D:/播客切片" # 自定义输出根目录
输出结构(根目录默认 Windows 为 E:\下载)
E:\下载\天真不天真-vol.30 对谈邵亦波:一个学霸的“学会爱自己”之路\
|- 01_00m00s_开场.mp3
|- 02_02m18s_20年前的邵亦波....mp3
|- ...
|- 19_01h12m08s_个人成长是终身学习....mp3
`- _时间轴.md (索引表:文件 / 时间点 / 内容)
切分模式 SPLIT_MODE
| 值 | 行为 |
|---|---|
auto(默认) |
探测源编码:MP3 源走 copy 秒切,其他源自动重编码 |
copy |
只重封装不重编码,秒级完成,仅对 MP3 源有效 |
encode |
重编码为 MP3,耗时约等于音频总长 |
> 实测坑:小宇宙的音频大多是 m4a/AAC,此时 -c copy 会报
> Invalid audio stream. Exactly one MP3 audio stream is required,
> 必须重编码才能得到 mp3 片段。auto 模式已自动处理这种情况。
其他说明
- 第一个时间点之前的部分会单独切成
01_00m00s_开场.mp3(前提是超过 5 秒) - 整期音频默认切完即删;所以二次重切时若整期已不在,会自动重新下载。想留着反复切设
CLEAN_SRC=false - 已存在的片段自动跳过,可中断后续跑
- 时间轴优先取
<a data-timestamp="秒数">;纯文本时间轴(每行HH:MM 内容)也能解析 - 该单集若没有可解析的时间轴,脚本会明确报错退出,不会瞎切
实测性能(1 小时 16 分单集,18 个时间节点 → 19 段)
下载 105.9 MB m4a → 重编码切分 19 段 = 56.5 MB,全程 21 秒
各段时长误差 < 0.05 秒(02 段 3:47.04 / 10 段 10:10.03 / 19 段 4:21.02)
支持的输入格式
- 单集链接
https://www.xiaoyuzhoufm.com/episode/<24位id> - 播客主页
https://www.xiaoyuzhoufm.com/podcast/<pid>(下载该页列出的单集) - 裸 24 位 id
- 带
?s=参数的分享链接 - 文本文件:每行一条上述内容,
#开头为注释
注意
- 默认「下载 → 切分 → 清理」:切分成功后整期音频会被自动删除,Show Notes 挪进切分目录,
PODCAST_DIR里不留残留。要保留整期文件设CLEAN_SRC=false。 - 切分失败时(如该集没有可解析的时间轴)不会删任何文件,整期音频原样保留。
KEEP_M4A=true时只删转码产物 mp3,原始 m4a 会保留。- 单集页原始 HTML 里
<audio src="">是空的(音频地址由客户端 JS 注入),
音频直链只能从__NEXT_DATA__的enclosure字段取。不要试图从<audio>标签取地址。 - 播客主页是滚动加载,单次只渲染最近约 15 集;需要更早的单集时,
在该页持续下拉后用浏览器插件批量收集链接再写入列表文件。 - 已存在的音频文件会自动跳过,可中断后续跑。
- 下载内容仅供个人离线收听与学习,请勿二次分发或商用。
移植到其他电脑
不要从 SkillHub 重新安装。 市场上发布的那个版本(v1.0.1)是空壳 ——
只有 SKILL.md 和图标,缺 scripts/,所有命令都跑不起来。
本技能真正能用的实现全部在本地文件里(本文件 + scripts/download.py + scripts/split.py)。
正确做法:直接拷贝整个目录
源(本机): %USERPROFILE%\.workbuddy\skills\podcast-downloader__skillhub\
目标(新机):%USERPROFILE%\.workbuddy\skills\podcast-downloader__skillhub\
拷过去后只需补一件事:装一次 ffmpeg(见上方 Requirements 的「方式一」,走阿里云源)。
其余脚本已做跨机适配,无需改任何代码:
| 项目 | 换机后的行为 |
|---|---|
| 输出目录 | 自动判断:Windows 有 E 盘 → E:\下载;没有 → ~/Downloads。也可用 PODCAST_DIR / SPLIT_DIR 指定 |
| ffmpeg | 自动探测多处位置;缺失时降级为只保留 m4a,不会报错中断 |
| 用户名差异 | 代码里无硬编码用户名。SKILL.md 中出现的 C:\Users\SUYONG\... 只是本机记录,不影响运行 |
| 系统差异 | 非 Windows 自动走 ~/Downloads;脚本只用 Python 标准库,无 pip 依赖 |
> _skillhub_meta.json 里的 iconLocalPath 是绝对路径,换机后技能图标可能不显示 ——
> 纯显示问题,不影响任何功能。
Troubleshooting
| 错误现象 | 可能原因 | 处理方式 |
|---|---|---|
| 未取到音频直链 | 页面结构变动或网络异常 | 单独打开该单集页确认可播放;重试一次 |
| ffmpeg 转换失败 | ffmpeg 缺失或路径不对 | 见上方 Requirements 的安装方式,或用 FFMPEG 变量指定路径 |
| 只生成了 m4a | 未检测到 ffmpeg | 属正常降级;装好 ffmpeg 后重跑即可 |
| Show Notes 为空 | 该单集本身没有节目说明 | 正常情况,.md 中会标注 |
| 文件名乱码 | 终端编码非 UTF-8 | 脚本已内置 stdout UTF-8 重配置;仍异常时改用 PowerShell 运行 |
Files
SKILL.md— 本文件scripts/download.py— 下载主脚本(纯标准库)scripts/split.py— 按时间轴切分脚本(复用 download.py 的解析逻辑)
来源与许可
本技能基于 SkillHub 上 @zcwl/podcast-downloader(发布者:智创未来,MIT 许可)重构而成。
原作者发布的版本为 MIT 许可,但不含实现 —— 目录里只有 SKILL.md 和图标,
缺少 scripts/ 与 reference.md,文档中所有命令都无法执行。
本版本保留其文档骨架,重写了全部实现,并新增:
- 按 shownotes 时间轴切分长音频(
scripts/split.py,支持data-timestamp与纯文本两种时间轴) - 下载后自动切分 + 清理整期音频(
AUTO_SPLIT/CLEAN_SRC,均为默认开启) - 输出目录跨机自适应、ffmpeg 多路径探测、请求自动重试
- Show Notes 的 HTML → Markdown 转换(含时间轴、图片、链接)
- 切分失败时保护整期文件不被误删
沿用 MIT 许可发布,保留原作者的版权与出处说明。