43 KiB
split_cue.py 使用手册
批量用 CUE 文件把"整轨大文件 + CUE"式专辑切成分轨的 Python 脚本,支持断点续跑、CJK 编码 CUE、多种源格式,以 shntool + cuetools 作为切割/标签后端。
设计上是 transcode_music.py 的前置伴生工具:先跑 split_cue.py 把整轨拆开,再跑 transcode_music.py 转 MP3,两者组合无缝衔接。
目录
- 1. 简介
- 2. 系统要求
- 3. 快速上手
- 4. 命令行参数详解
- 5. 常用使用场景
- 6. Manifest 文件说明
- 7. 支持的源格式与依赖矩阵
- 8. CUE 检测与匹配逻辑
- 9. 分轨命名与元数据
- 10. 与 transcode_music.py 的组合工作流
- 11. 错误处理与恢复
- 12. 已知局限
- 13. FAQ
1. 简介
这个工具解决什么问题?
你有很多"整轨型"专辑:一张 CD 打包成一个大 FLAC / WAV / APE 文件,配一个 CUE 索引,例如:
王菲 - 唱游 [FLAC+CUE]/
├── CDImage.flac (整张 CD 的一个 40 分钟大文件)
└── CDImage.cue (12 个 TRACK 条目, 标注每首歌的起止)
这类文件不方便用普通播放器逐首播放(除非播放器专门支持 CUE),也不方便直接转成 MP3(transcode_music.py 会检测出这种情况并报警跳过)。
split_cue.py 就是用来把这类专辑批量拆成分轨的:扫描整个源目录树,找出所有"整轨 CUE"型专辑,用 shnsplit 按 CD 帧边界(1/75 秒精度)切成 01 - 红豆.flac、02 - 催眠.flac 这样的分轨文件,用 cuetag 写入 CUE 里的标题/艺人/专辑等元数据,全部原地切割、保留原文件不动。
核心特性:
| 特性 | 说明 |
|---|---|
| 原地切割 | 分轨文件写在原专辑目录里,与整轨大文件并存 |
| 保留原文件 | 整轨的 CDImage.flac 从不被删改,可事后自己决定去留 |
| 保持源格式 | FLAC → FLAC 无损、WAV → WAV、APE → FLAC(ffmpeg 无 APE 编码器) |
| Sample-exact 切割 | shntool 按 CD 帧对齐,每首歌起止精确到 1/75 秒 |
| 完整元数据 | cuetag 自动写入 TITLE / ALBUM / ARTIST / track / TRACKTOTAL / DATE / GENRE |
| CJK 支持 | CUE 编码自动识别(utf-8/utf-8-sig/gbk/big5/shift_jis/cp936),中日韩音源都能处理 |
| 断点续跑 | .split_cue_manifest.json 记录状态,任意时刻中断都能从上次位置继续 |
| 幂等 | 已完成的专辑再次运行秒跳过;新增专辑增量处理 |
| 自动排除已分轨 | 若目录里已有分轨文件(situation C),自动 SKIP 不误切 |
| 依赖自适应 | 启动时 probe 依赖,缺 mac 时把 APE 源标 UNSUPPORTED 而非崩溃 |
2. 系统要求
| 组件 | 版本 | 必需/可选 | 说明 |
|---|---|---|---|
| Python | 3.10+ | 必需 | 只用标准库,无需 pip install |
| shntool | 3.0+ | 必需 | 提供 shnsplit 切割命令 |
| cuetools | 1.4+ | 必需 | 提供 cuetag 元数据写入命令 |
| flac | 1.3+ | 处理 FLAC 时必需 | shntool 编码 FLAC 输出时调用 |
| mac (Monkey's Audio) | 4.x+ | 处理 APE 时必需 | shntool 解码 APE 输入时调用 |
| ffprobe | 6.0+ | 可选 | 仅用于源文件元数据展示(时长、采样率),不影响切割 |
检查环境:
python3 --version
shnsplit -v | head -1
cuetag --help 2>&1 | head -1
flac --version | head -1
mac 2>&1 | head -1 # 无输出/命令不存在 = APE 源将标 UNSUPPORTED
ffprobe -version | head -1
安装依赖(Ubuntu / Debian / WSL):
sudo apt install shntool cuetools flac
# APE 支持(可选):Ubuntu 官方仓库通常没有,需要第三方 PPA 或源码编译
sudo apt install monkeys-audio # 如果仓库有的话
macOS:
brew install shntool cuetools flac
# APE:需从 https://monkeysaudio.com 下载或用其他方式
Windows:直接跑不推荐(有 os.fsync(dir_fd) 的 POSIX 依赖),建议 WSL2。
依赖缺失时的行为:
启动时脚本会 probe 所有依赖并打印状态。缺 shnsplit 或 cuetag 直接退出(exit code 3)。缺 mac 或 flac 只影响对应格式,其他格式仍能处理。
3. 快速上手
最常用的三个命令:
# 1. 先分析看看哪些专辑需要切割 (不实际切)
python3 split_cue.py /mnt/z/我的音乐 --analyze-only
# 2. 确认没问题后正式切割 (中断可再跑, 自动 resume)
python3 split_cue.py /mnt/z/我的音乐
# 3. 切完了继续转 MP3 (transcode_music.py 会自动跳过整轨大文件)
python3 transcode_music.py /mnt/z/我的音乐 /mnt/x/mp3输出
就这么简单。整轨型专辑切成分轨后,transcode_music.py 会自动识别为"situation C"(CUE 引用整轨但存在分轨),跳过整轨文件只转分轨——两个脚本天然衔接。
4. 命令行参数详解
完整语法
split_cue.py [SOURCE] [选项...]
或
split_cue.py --source SOURCE [选项...]
参数列表
| 参数 | 短选项 | 类型 | 是否必需 | 默认 | 说明 |
|---|---|---|---|---|---|
SOURCE |
— | 位置参数 | 必需¹ | — | 源目录,含整轨型专辑的位置 |
--source |
-s |
字符串 | 必需¹ | — | 源目录(与位置参数 SOURCE 等价) |
--analyze-only |
— | 开关 | 可选 | 关 | 只分析生成 manifest,不实际切割 |
--force |
— | 开关 | 可选 | 关 | 忽略现有 manifest,全部重新分析并覆盖已有分轨 |
--verbose |
-v |
开关 | 可选 | 关 | 显示 DEBUG 级别日志(含跳过项) |
--shnsplit |
— | 字符串 | 可选 | shnsplit |
shnsplit 可执行路径 |
--cuetag |
— | 字符串 | 可选 | cuetag |
cuetag 可执行路径 |
--ffprobe |
— | 字符串 | 可选 | ffprobe |
ffprobe 可执行路径 |
--split-timeout |
— | 整数 | 可选 | 1800 |
单个专辑 shnsplit 超时秒数 |
--help |
-h |
开关 | — | — | 显示帮助并退出 |
¹ 源目录必须提供,可用位置参数或 flag 两种形式:
# 位置参数 (更简洁)
split_cue.py /mnt/z/music
# flag 长形式
split_cue.py --source /mnt/z/music
# flag 短形式
split_cue.py -s /mnt/z/music
注意:split_cue.py 不需要目标目录,因为它是原地切割——分轨文件直接写到原专辑目录里。这与 transcode_music.py 需要 -s / -t 两个参数不同。
参数详细说明
SOURCE / --source / -s
源目录。脚本会递归扫描此目录及所有子目录,找出每个"整轨 CUE"型专辑处理。
- 支持含空格、中文、特殊字符的路径
- 网盘挂载(
/mnt/z/、/mnt/x/等)都可以 - Manifest 文件
.split_cue_manifest.json写入此目录
特殊约定:如果 SOURCE 本身就是一个专辑目录(而不是一堆专辑的父目录),也能正常工作。脚本会把源目录自身也作为分析候选。
--analyze-only
只分析,不切割。用途:
- 首次跑一个新的大目录时,先看看总共有几个专辑需要切、切成几轨
- 检查 CUE 解析是否有问题(编码、缺 FILE 引用等)
- 生成 manifest 后可以手动查看/编辑
强烈推荐每次对新目录都先跑一次 --analyze-only。切割是原地写文件的操作,先看清楚计划再动手更安全。
--force
强制重跑。忽略现有 manifest,重新分析并覆盖已存在的分轨文件。
什么时候用:
- 之前切错了想重来
- CUE 修改过了,想重新按新 CUE 切
- 想清理并重新开始
警告:--force 会覆盖已存在的同名分轨文件。如果之前有你手动编辑过的分轨,会丢失。不确定时先备份。
边界情况:如果专辑的 status 已经因为"situation C"被判为 skipped(因为目录里已有分轨),--force 也不会自动 un-skip。因为脚本无法可靠区分"我上次生成的分轨"和"用户已有的分轨"——安全起见不动。想强制重切,请先手动删掉旧的分轨文件再跑。
--verbose / -v
打开 DEBUG 级别日志。默认 INFO 级别只显示每首切割和错误。加上 -v 会额外显示:
[已完成 skip] ...每个跳过的专辑[SKIP (无需切割): ...]每个 skipped 目录[已存在, 保留] ...每个已有分轨文件
大量输出,通常只在排查问题时用。
--shnsplit / --cuetag / --ffprobe
指定可执行文件路径。默认从 PATH 查找。
什么时候用:
- 装了多个版本想指定用哪个
- 二进制不在标准 PATH 里
- 用自己编译的版本
示例:
split_cue.py /src --shnsplit /opt/shntool-3.0.10/bin/shnsplit
--split-timeout
单个专辑 shnsplit 命令的超时时间(秒),默认 1800(30 分钟)。超时算失败,写入 manifest 后继续下一个。
通常不需要调。除非你有超长专辑(比如一个 3 小时的整轨 audiobook)在慢网盘上,可以调大到 3600 或更多。
注意:shnsplit 是单次调用切完整个专辑的,不像 ffmpeg 是逐轨调用,所以超时值应该覆盖整个专辑的切割时间。
5. 常用使用场景
场景 1:第一次处理新目录(推荐流程)
假设你刚下载了一堆 [FLAC+CUE] 到 /mnt/z/新收藏/。
第一步:先分析
python3 split_cue.py /mnt/z/新收藏 --analyze-only
输出会告诉你:
- 总共扫了几个目录
- 有几个专辑属于"整轨型"需要切割(
analyzed) - 有几个源格式不支持(
unsupported) - 有几个已经是分轨的(
skipped)
第二步:查看切割计划
python3 -c "
import json
d = json.load(open('/mnt/z/新收藏/.split_cue_manifest.json'))
for rel, a in d['albums'].items():
if a['status'] == 'analyzed':
print(f'{rel}: {a[\"source_audio\"]} → {len(a[\"tracks\"])} 轨')
for t in a['tracks'][:3]:
print(f' · {t[\"target_name\"]}')
if len(a['tracks']) > 3:
print(f' ... 及另外 {len(a[\"tracks\"]) - 3} 轨')
"
第三步:确认无误后正式切割
python3 split_cue.py /mnt/z/新收藏
已经 analyzed 的专辑会开始切割,skipped / unsupported 的自动跳过。
第四步:接着跑 transcode_music.py 转 MP3
python3 transcode_music.py /mnt/z/新收藏 /mnt/x/music/新收藏
transcode_music.py 会自动识别切出来的分轨(situation C),跳过整轨大文件只转分轨。
场景 2:中断后恢复
切到一半按 Ctrl+C 中断(或断电、脚本崩溃)。只需要再跑同样的命令:
python3 split_cue.py /mnt/z/新收藏
脚本会读取 manifest,跳过所有 completed 状态的专辑,从上次中断的地方继续。不需要任何特殊参数。
场景 3:增量同步(新增了整轨专辑)
以后又下载了几个 [FLAC+CUE] 到 /mnt/z/新收藏/:
python3 split_cue.py /mnt/z/新收藏
已完成的秒跳过,只有新目录会被分析和切割。最舒服的用法。
场景 4:修复失败的专辑
如果某个专辑因为 CUE 解析错误、shnsplit 崩溃等失败了(status: failed),修好后:
# 直接再跑, failed 的会被自动重试
python3 split_cue.py /mnt/z/新收藏
已成功的不动,只重试失败的。
场景 5:只切一个专辑
比如只想切某一张:
python3 split_cue.py "/mnt/z/新收藏/王菲 - 唱游 [FLAC+CUE]"
Manifest 会写到那个专辑目录里,其他专辑不受影响。
场景 6:想重新切某个已完成的专辑
比如切完发现 CUE 元数据有错,改了 CUE 想重切:
方法一:删掉分轨文件 + 编辑 manifest
删除该专辑目录里的分轨文件(01 - Title.flac 等),然后编辑 .split_cue_manifest.json,把该专辑的 status 改成 analyzed、每个 track 的 status 改成 pending。再跑一次即可。
方法二:全局 --force
python3 split_cue.py /mnt/z/新收藏 --force
会重新分析所有专辑并覆盖分轨。但注意:如果目录里已有旧分轨且脚本无法区分,可能会被判为 situation C 而 skip。最保险的做法还是先手动删旧分轨。
场景 7:排查某个专辑的问题
某个专辑 status=failed,用 verbose 模式跑:
python3 split_cue.py "/mnt/z/新收藏/有问题的专辑" -v
输出会显示 shnsplit 的完整错误、cuetag 警告等。
场景 8:切完不满意,回滚
因为原文件从不被删改,回滚很简单:
# 删掉所有分轨文件, 只留原整轨 + CUE
cd "/mnt/z/新收藏/王菲 - 唱游"
rm -f *.flac # 删所有 flac
# 找回原整轨 (脚本没删过它, 只是你上面 rm 一起删了; 如果只想删分轨:)
# 或更精确: 只删符合 "NN - Title.flac" 命名的
更精确的清理:
# 只删 "01 - ...", "02 - ..." 这种格式的文件
find "/mnt/z/新收藏/王菲 - 唱游" -maxdepth 1 -regex '.*/[0-9]\{2\} - .*\.\(flac\|wav\)$' -delete
# 编辑 manifest 把该专辑 status 改回 analyzed
6. Manifest 文件说明
位置
Manifest 保存在源目录下,文件名 .split_cue_manifest.json(隐藏文件)。
顶层结构
{
"manifest_version": 1,
"created_at": "2026-09-06T13:41:03+08:00",
"updated_at": "2026-09-06T13:41:04+08:00",
"source_root": "/mnt/z/新收藏",
"stats": {
"total_albums": 4,
"by_album_status": {"completed": 1, "skipped": 3},
"by_track_status": {"completed": 3},
"total_tracks": 3
},
"albums": {
"专辑相对路径1": { ... },
"专辑相对路径2": { ... }
}
}
单个专辑 (album) 结构
{
"status": "completed",
"source_audio": "CDImage.flac",
"cue": "CDImage.cue",
"album_title": "唱游",
"album_performer": "王菲",
"album_date": "1998",
"album_genre": "Chinese Pop",
"source_codec": "flac",
"source_sample_rate": 44100,
"source_channels": 2,
"duration_sec": 40.0,
"output_format": "flac",
"tracks": [
{
"num": 1,
"shnsplit_index": 1,
"title": "红豆",
"performer": "王菲",
"target_name": "01 - 红豆.flac",
"total_tracks": 3,
"status": "completed",
"error": null,
"converted_at": "2026-09-06T13:41:04+08:00"
},
{ "num": 2, "shnsplit_index": 2, ... },
{ "num": 3, "shnsplit_index": 3, ... }
],
"issues": [],
"started_at": "2026-09-06T13:41:03+08:00",
"completed_at": "2026-09-06T13:41:04+08:00"
}
字段含义:
| 字段 | 含义 |
|---|---|
status |
专辑级状态,见下表 |
source_audio |
检测到的整轨大文件名(相对路径) |
cue |
匹配的 CUE 文件名 |
album_title / album_performer |
从 CUE 的顶级 TITLE / PERFORMER 解析 |
album_date / album_genre |
从 CUE 的 REM DATE / REM GENRE 解析 |
source_codec / source_sample_rate / source_channels / duration_sec |
由 ffprobe 探测(仅用于展示) |
output_format |
shnsplit -o 参数(flac 或 wav) |
tracks[] |
切割计划,每首歌一项 |
tracks[].num |
CUE 里声明的 TRACK 编号 |
tracks[].shnsplit_index |
shnsplit 内部计数器(1-based,用于对应 tmp 文件名) |
tracks[].title / tracks[].performer |
从 CUE 的 TRACK 级 TITLE / PERFORMER 解析 |
tracks[].target_name |
最终文件名(已 sanitize) |
tracks[].status |
分轨级状态 |
issues[] |
人类可读的警告/说明 |
状态值 (status)
专辑级状态:
| status | 说明 | 下次运行时行为 |
|---|---|---|
pending |
初始状态(一般用不到) | 重新分析 |
analyzed |
已识别为整轨 CUE 型,切割计划就绪 | 执行切割 |
processing |
正在切割(若看到,说明上次中断了) | 重新切割整个专辑 |
completed |
所有分轨都成功输出 | 秒跳过 |
failed |
切割失败(shnsplit / cuetag / 重命名任一失败) | 重试整个专辑 |
skipped |
无需切割(无 CUE / 已分轨 situation C / CUE 引用多文件 / 引用不存在) | 跳过 |
unsupported |
源格式当前依赖无法处理(如 APE 但 mac 未装、DSF) |
跳过 |
分轨级状态(tracks[] 里):
| status | 说明 |
|---|---|
pending |
待处理 |
completed |
已完成 |
failed |
失败(error 字段有原因) |
手动干预 manifest
Manifest 是纯 JSON,可以用文本编辑器直接改。
例子 1:让某个专辑重新切割
"status": "completed" → "status": "analyzed"
同时把该专辑内所有 track 的 status 从 completed 改成 pending,删掉现有分轨文件,再跑脚本。
例子 2:跳过某个有问题的专辑
"status": "analyzed" → "status": "skipped"
例子 3:删除某个专辑的记录重新扫描
删除对应的 album 条目,下次运行会自动重新扫描添加。
改完保存直接跑:
python3 split_cue.py /mnt/z/新收藏
Manifest 损坏怎么办?
如果 JSON 损坏(编辑错、盘故障),脚本启动时会自动检测,把坏的 manifest 重命名为 .split_cue_manifest.json.corrupt-<时间戳>,然后重新生成新的 manifest。
已切出来的分轨文件不会丢,但脚本会重新扫描分析。因为已切分的目录会呈现为 situation C(有分轨 + 有整轨),会自动 SKIP,不会重复切割。幂等性保护。
7. 支持的源格式与依赖矩阵
支持矩阵
| 源格式 | 输出格式 | 依赖 | 备注 |
|---|---|---|---|
.flac |
.flac |
shntool + flac |
无损 re-encode,sample-exact |
.wav |
.wav |
shntool 原生 |
无外部依赖,PCM 样本直切 |
.ape |
.flac |
shntool + mac + flac |
mac 解码 APE,flac 编码输出(ffmpeg 无 APE 编码器) |
.dsf / .dff |
— | 无 | 不支持 (DSD)。请先手动转 FLAC 再切 |
依赖启动时 probe
启动时会打印所有依赖状态:
未找到 mac (APE 解码器) → APE 源将标记 UNSUPPORTED. 安装可选: apt install monkeys-audio
支持源格式: ['.flac', '.wav']
缺 shnsplit 或 cuetag:直接退出(exit 3)。这是硬依赖。
缺 flac:FLAC 源会被标为 UNSUPPORTED。只有 WAV 源能处理。
缺 mac:APE 源会被标为 UNSUPPORTED,其他格式不受影响。
缺 ffprobe:警告但不影响切割,只是 manifest 里 source_codec / duration_sec 等字段是空的。
为什么不支持 DSD?
shntool 本身不支持 DSD 格式。理论上可以用 ffmpeg 走另一条路,但:
- DSD 打包成整轨很少见(DSD 音源通常出货就是分轨的)
- DSD 解码涉及低通滤波、降采样等,输出到 FLAC 会显著改变数据
- 处理逻辑复杂度不值得
如果你确实有 DSD 整轨 + CUE,建议先手动用 ffmpeg 转成 FLAC,再跑 split_cue.py:
ffmpeg -i album.dsf -c:a flac -ar 88200 album.flac
# 然后修改 CUE 里的 FILE 指向 album.flac
split_cue.py /path/to/album
8. CUE 检测与匹配逻辑
什么样的目录会被识别为"整轨 CUE 型"?
严格判定条件(全部满足才算):
- 目录里至少有 1 个 CUE 文件
- 目录里至少有 1 个无损音频文件(FLAC/WAV/APE/DSF/DFF)
- 某个 CUE 里
FILE "xxx" WAVE条目恰好 1 个 - 这个 FILE 引用能匹配到目录里的音频文件
- 目录里没有其他无损音频文件(除了这个被引用的)
- CUE 里有 TRACK 条目(不能是空 CUE)
只要有一项不满足,就不会被切割:
| 情况 | 判定 | 原因 |
|---|---|---|
| 无 CUE | skipped |
没有切割依据 |
| 无无损音频 | skipped |
没东西可切 |
| CUE 无 FILE 条目 | skipped + issues 记录 |
CUE 无效 |
| CUE 有多个 FILE (>1) | skipped |
已经是分轨 CUE 索引 |
| CUE 引用的文件不存在 | skipped + issues 记录 |
可能是错配的 CUE |
| CUE 引用整轨但同时存在分轨 | skipped (situation C) |
已切过或用户手动分轨了 |
| 匹配成功但源格式依赖缺失 | unsupported |
例如 APE 但 mac 未装 |
CUE FILE 引用的匹配策略
CUE 里的 FILE "album.wav" WAVE 未必和实际文件名完全一致。脚本用三级 fallback:
- 精确匹配:文件名完全一致
- 不区分大小写:
ALBUM.FLAC匹配album.flac - 忽略扩展名:
FILE "album.wav" WAVE匹配到album.flac(假设目录里只有一个album.*无损文件)
三级都不匹配才判为"引用不存在"。
CUE 编码识别
CUE 文件常见的编码:
utf-8-sig(有 BOM 的 UTF-8)utf-8gbk(简体中文)big5(繁体中文)shift_jis(日文)cp936(GBK 别名)latin1(最后兜底)
脚本按顺序尝试解码,第一个成功的即采用。中日韩音源基本都能识别。
CUE 里 TITLE / PERFORMER 的语义
CUE 里 TITLE "..." 和 PERFORMER "..." 可能出现在两个位置:
- 顶级(在任何 TRACK 之前):整张专辑的标题和艺人
- TRACK 块内:这一首歌的标题和艺人
脚本区分这两种上下文,分别写入 album_title / album_performer 和每首歌的 title / performer。
REM 元数据
CUE 里 REM DATE "1998" 和 REM GENRE "Chinese Pop" 在顶级出现时,会被解析为专辑的年份和流派,写入 manifest 并通过 cuetag 传给分轨的 tag。
CUE 与 transcode_music.py 的区别
transcode_music.py 也有 CUE 检测逻辑(在其文档第 8 节详述了三种情况 A/B/C)。它的目的是报警"这个专辑需要 CUE split",而 split_cue.py 是执行这个 split。
两者对 CUE 的看法一致:
| 情况 | transcode_music.py 视角 | split_cue.py 视角 |
|---|---|---|
| A: CUE + 整轨大文件 | needs_cue_split 报警跳过 |
analyzed,执行切割 |
| B: CUE + 分轨 | 正常转码 | skipped |
| C: CUE 引用整轨 + 分轨都在 | 跳过整轨只转分轨 | skipped |
这就是为什么它们能天然衔接:split_cue.py 把 A 转成 C,然后 transcode_music.py 处理 C。
9. 分轨命名与元数据
文件命名规则
分轨文件名格式:NN - Title.ext
- NN:CUE 里 TRACK 声明的编号,零填充到至少 2 位(如果总轨数 ≥ 100 会用 3 位)
- Title:CUE 里 TRACK 块内的 TITLE,经过文件名净化
- ext:由源格式决定的输出扩展名(见第 7 节表格)
Title 净化规则:
- 替换文件系统非法字符
<>:"/\|?*以及 ASCII 控制字符(\x00-\x1f)为_ - 折叠连续空白为单个空格
- 去掉结尾的点号和空格(Windows 要求)
- 截断到最多 200 字符(避免文件系统限制)
- 净化后为空则用
untitled兜底
示例:
| CUE 里的 TITLE | 输出文件名(假设 num=2) |
|---|---|
红豆 |
02 - 红豆.flac |
催眠 / 静夜 |
02 - 催眠 _ 静夜.flac(/ → _) |
"Track: A" |
02 - _Track_ A_.flac(" 和 : → _) |
| (无 TITLE) | 02 - Track 02.flac(兜底) |
注意:文件名里的非法字符被替换,但元数据 tag(写入文件内部)保留 CUE 里的原样。所以 催眠 / 静夜.flac 里的 TITLE tag 仍然是 催眠 / 静夜(含 /),播放器显示正常。
命名冲突处理
极少数情况下多首歌的净化后名字撞车(比如两首歌都叫 未命名 而且 num 不同但被截断)。脚本会在冲突时追加 (2)、(3):
05 - 未命名.flac06 - 未命名 (2).flac
元数据 (Tag) 映射
由 cuetag 自动写入。对 FLAC 输出,写的是 Vorbis Comment tags:
| Tag | 来源 |
|---|---|
TITLE |
CUE 里 TRACK 块内的 TITLE |
ARTIST / PERFORMER |
CUE 里 TRACK 块内的 PERFORMER(若无则用专辑 PERFORMER) |
ALBUM |
CUE 里顶级 TITLE |
track |
CUE 里 TRACK 编号(补零至 2 位,如 01) |
TRACKTOTAL |
CUE 里总 TRACK 数 |
DATE |
CUE 里顶级 REM DATE |
GENRE |
CUE 里顶级 REM GENRE |
注意:cuetag 的行为是按位置对应:cuetag album.cue file1.flac file2.flac file3.flac 会把 CUE 的 TRACK 1 写到 file1,TRACK 2 写到 file2,以此类推。脚本按 shnsplit_index 严格排序传参,保证对应正确。
cuetag 失败的后果
如果 cuetag 因某种原因失败(罕见),脚本会警告但不算专辑失败——分轨音频文件本身是完整的,只是没有元数据 tag。可以事后手动跑一次:
cd "专辑目录"
cuetag *.cue 01*.flac 02*.flac 03*.flac # 按顺序传
10. 与 transcode_music.py 的组合工作流
典型完整流程
# ┌─ 前期整理 (split_cue.py)
python3 split_cue.py /mnt/z/新收藏 --analyze-only
python3 split_cue.py /mnt/z/新收藏
# └─ 转码到便携格式 (transcode_music.py)
python3 transcode_music.py /mnt/z/新收藏 /mnt/x/mp3输出 --analyze-only
python3 transcode_music.py /mnt/z/新收藏 /mnt/x/mp3输出
两个脚本对同一个专辑的看法
举例:王菲 - 唱游 [FLAC+CUE]/ 目录里有 CDImage.flac + CDImage.cue(12 轨)。
只跑 transcode_music.py:
[!] 1 个专辑需先手动 CUE split (已跳过转码):
• 王菲 - 唱游 [FLAC+CUE]
CDImage.cue: 整轨型 (单文件 'CDImage.flac', 12 轨) — 需先手动 CUE split 再转码
先跑 split_cue.py 再跑 transcode_music.py:
[split_cue.py]
[1/1] 切割: 王菲 - 唱游 [FLAC+CUE]
源: CDImage.flac → 12 轨 (flac)
✓ [01/12] 01 - 红豆.flac
✓ [02/12] 02 - 催眠.flac
...
[transcode_music.py]
CDImage.cue: 引用 'CDImage.flac' 但存在 12 个分轨 → 跳过 'CDImage.flac' 只转分轨
转码: 01 - 红豆.flac (flac, 44100Hz)
转码: 02 - 催眠.flac (flac, 44100Hz)
...
transcode_music.py 自动检测到 situation C,只转 12 个分轨、不动整轨,产生 12 个 MP3。
Manifest 隔离
两个脚本的 manifest 文件名不同:
split_cue.py→.split_cue_manifest.jsontranscode_music.py→.transcode_manifest.json
互不干扰。可以随时来回跑。
输出结构对比
运行前:
/mnt/z/新收藏/
└── 王菲 - 唱游 [FLAC+CUE]/
├── CDImage.flac (整张 CD 一个大文件)
└── CDImage.cue
运行 split_cue.py 后:
/mnt/z/新收藏/ ← 源目录, 分轨写这里
├── .split_cue_manifest.json ← split_cue 状态
└── 王菲 - 唱游 [FLAC+CUE]/
├── CDImage.flac ← 原文件保留
├── CDImage.cue ← 保留
├── 01 - 红豆.flac ← 新: split_cue 输出
├── 02 - 催眠.flac
└── ... (12 个分轨)
接着运行 transcode_music.py 后:
/mnt/z/新收藏/
├── .split_cue_manifest.json
├── .transcode_manifest.json ← transcode_music 状态
└── 王菲 - 唱游 [FLAC+CUE]/ (未变)
/mnt/x/mp3输出/ ← 目标目录
└── 王菲 - 唱游 [FLAC+CUE]/
├── CDImage.cue ← CUE 也复制过去
├── 01 - 红豆.mp3 ← 320k CBR
├── 02 - 催眠.mp3
└── ... (12 个 MP3)
注意:CDImage.flac 不会转成 MP3(因为 situation C 检测跳过);CDImage.cue 会被复制到目标目录(作为资源)。
关于 CUE 的一个细节
切完后,CDImage.cue 里的 FILE "CDImage.flac" WAVE 仍指向原整轨。如果用户在目标目录里想用 CUE 播放分轨 MP3,这个 CUE 是不正确的——它指向整轨、但目标目录里没有那个整轨。
如果你实际会用 CUE 索引,考虑:
- 手动编辑复制到目标的 CUE,把 FILE 指向 MP3(但 CUE 传统上不支持多 FILE 分轨映射,改起来麻烦)
- 直接删除目标目录里的 CUE(分轨 MP3 有 tag,播放器不需要 CUE 也能显示正确信息)
推荐做法:直接删。分轨 MP3 的 ID3 tag 已经包含所有 CUE 元数据。
11. 错误处理与恢复
错误捕获原则
单个专辑失败不影响其他。shnsplit / cuetag / 重命名任一失败时:
- 该专辑内所有未完成的 track 状态设为
failed,错误消息写入error字段 - 该专辑整体状态设为
failed issues[]追加人类可读的说明- 继续处理下一个专辑
- 最后在报告里列出所有失败项
再次运行时
failed 状态的专辑会自动重试(整个专辑重切,因为 shnsplit 是全轨一次调用的原子操作,无法只重切失败的分轨)。
# 修好源文件后, 直接再跑
python3 split_cue.py /mnt/z/新收藏
常见错误
"shnsplit 失败: unknown format"
CUE 里 FILE "xxx" TYPE 的 TYPE 值 shntool 不认。常见于 FLAC / WAVE / MP3 / BINARY,其他值可能有问题。检查 CUE 内容并手动修正。
"shnsplit 失败: cannot open file"
CUE 的 FILE 引用文件名和实际文件名不一致(尽管脚本会 fallback 匹配到实际文件,但 shnsplit 用的是 CUE 里的原字符串)。可能因为脚本 fallback 匹配到了但 shntool 找不到。手动把 CUE 里的 FILE 名改成实际文件名,再跑。
"cuetag 失败, 音频仍可用但可能无标签"
cuetag 有时对某些 CUE 或 FLAC 版本行为异常。不影响切割本身,可事后手动跑 cuetag 或用别的 tagger(Picard、mp3tag 等)补 tag。
"预期临时文件缺失/过小"
shnsplit 返回 0 但实际没写全所有分轨。极罕见,通常是磁盘满或权限问题。检查目标目录空间和权限。
"临时目录创建失败"
无法在专辑目录里创建 .split_cue_tmp_<pid>。通常是目录只读或磁盘满。
"源文件消失"
分析阶段之后、切割阶段之前源文件被删/移动。删除对应专辑 manifest 条目重新扫描即可。
中断(Ctrl+C)
脚本捕获 SIGINT,保存 manifest 后退出。中断时临时目录 .split_cue_tmp_<pid> 里的 shnsplit 中间产物会保留(因为 finally 清理只在 split_album 函数正常返回时执行)——下次跑会重建。中断专辑的状态可能停留在 processing,重跑时会被视为需要重切。
如果发现有遗留的 .split_cue_tmp_* 目录(比如脚本被 kill -9 强杀),可以手动清理:
find /mnt/z/新收藏 -maxdepth 2 -type d -name '.split_cue_tmp_*' -exec rm -rf {} +
查看失败列表
# 用 jq 快速查看失败的专辑
jq -r '.albums | to_entries[] | select(.value.status=="failed") | .key' \
/mnt/z/新收藏/.split_cue_manifest.json
# 查看某专辑的具体失败原因
jq '.albums["专辑名"] | {status, issues, tracks: [.tracks[] | select(.status=="failed") | {num, error}]}' \
/mnt/z/新收藏/.split_cue_manifest.json
关于 unsupported
unsupported 状态的专辑不算失败,是主动跳过。原因写在 issues[] 里,例如:
缺少依赖: mac (APE 解码器)→ 装mac后再跑,会自动切shntool 不支持 DSD, 请先手动转 FLAC→ 手动预处理
装好依赖后,需要用 --force 或删掉对应 manifest 条目才会重新分析。
12. 已知局限
- 不支持 DSD(
.dsf/.dff):shntool 不识别 DSD。若有整轨 DSD + CUE,需先手动 ffmpeg 转 FLAC 再切。 - shnsplit 是一次调用切完整个专辑:无法只重切失败的某一首歌。失败时整个专辑重切(幂等,无副作用)。
- 不做 replaygain / 章节标记 / 高级 tag:只做 cuetag 支持的标准 tag(TITLE/ARTIST/ALBUM/DATE/GENRE/track/TRACKTOTAL)。
- 不支持并行切割:为保证 manifest 状态一致性,专辑串行处理。可对不同子目录同时跑多个脚本实例。
- CUE 里 track 编号必须连续从 1 开始:极少数非标准 CUE(比如从 TRACK 03 开始)会导致 shnsplit 内部计数器和 CUE 编号错位。目前未特殊处理,可能造成 tag 错乱。
- 网盘 mtime 不能保留:分轨是新建文件,mtime 是当下时间。原文件不动。
- --force 不会自动清理旧分轨:如果目录里已有旧分轨且脚本判为 situation C 而 skip,
--force也不会强制切。需手动删旧分轨。 - shnsplit 依赖
flac二进制来编码 FLAC:这是 shntool 3.x 的架构选择(它调用外部 encoder)。装了flac才能切 FLAC 源;装了mac才能切 APE 源。 - APE 输出为 FLAC,不能原格式:ffmpeg 和 shntool 都没 APE 编码器(APE 是私有格式)。所以 APE → FLAC 是唯一路径。
- Windows 直接跑未测试:脚本用了 POSIX
os.fsync(dir_fd),Windows 上会 best-effort。理论可用,实际推荐 Linux / WSL2 / macOS。
13. FAQ
Q: 为什么要单独一个 split_cue.py?直接让 transcode_music.py 顺便切了不好吗?
A: 分离关注点。切割是修改源目录的操作(写新文件),转码是读源目录写目标目录的操作。两者错误影响面不同:切错了改源,转错了改目标。分开可以:
- 先充分 review 切割计划再动手(
--analyze-only) - 切割失败不牵连转码,反之亦然
- 用户可以只切不转(本地播放器直接消费分轨 FLAC)
- 用户可以直接手动切好再让 transcode_music 转(跳过 split_cue)
Q: 切完 CUE 文件删不删?
A: 保留。原文件(CDImage.flac)也保留。用户想删的话手动来,脚本不做破坏性操作。删原文件前建议:
- 先播放几首分轨确认没问题
- 用
metaflac -l 01*.flac确认 tag 完整 - 备份 CUE(换新 CUE 想重切时有用)
Q: 切割精度和音质怎么样?
A: shntool 是按 CD 帧(1/75 秒)对齐切的,与原始 CUE INDEX 完全一致。对 FLAC → FLAC,音频数据无损(decode + re-encode 是数学等价的 PCM 转换)。对 WAV → WAV 是纯样本操作,字节级精确。对 APE → FLAC,音频数据也无损(FLAC 压缩是无损的),只是容器格式变了。
Q: 分轨没有 gapless 播放会不会有咔嗒声?
A: 用了 shntool 的默认 --append-gaps 模式,pregap 追加到上一轨结尾。相邻曲目之间的过渡应该 sample-exact 无缝,播放器如支持 gapless 就无咔嗒声。
Q: 切完后音频总长度对不上原文件?
A: 应该是精确对上的(每首歌的 duration 加起来 = 原文件 duration)。如果偏差 < 1 秒,可能是取整误差(ffprobe 显示的 duration 有精度限制)。偏差大就是有 bug,请提 issue。
Q: 中文 / 日文 / 韩文的 CUE 处理有问题吗?
A: 应该没问题。脚本会按 utf-8 / gbk / big5 / shift_jis / cp936 / latin1 顺序尝试解码,绝大部分 CJK CUE 都能识别。切割输出的 tag(Vorbis Comment)总是 UTF-8。如果发现某个 CUE 解码错乱,可以用别的工具(比如 iconv 或文本编辑器)先把 CUE 转成 UTF-8 再跑脚本。
Q: cuetag 输出 warning 说什么 metaflac 不 exist?
A: 有些 cuetools 版本的 cuetag 是 Python 或 shell 脚本,内部调用 metaflac(来自 flac 包)。装了 flac 就有 metaflac。如果 cuetag 抱怨 metaflac 不存在,装 flac 包即可(sudo apt install flac)。
Q: 我的 CUE 有奇怪的编码/结构 shntool 处理不了怎么办?
A: 先用文本编辑器把 CUE 转成 UTF-8 无 BOM,去掉不必要的 REM 行,看 TRACK / FILE / INDEX 结构是否规范(每个 TRACK 至少要有 INDEX 01 MM:SS:FF)。规范化后重试。
Q: 能不能自定义分轨文件名格式?比如 "%p - %t.flac"?
A: 目前不能。命名格式固定为 NN - Title.ext。这是刻意的:
- 保证目录内文件按 track 顺序排列
- 简化文件名,避免复杂替换的 bug
- 元数据 tag 里已经有完整信息(艺人、专辑等),不需要塞进文件名
Q: 支持 track 里带 gap(HTOA / hidden track)吗?
A: 部分支持。shnsplit 默认 --append-gaps 会把 gap 归到前一轨末尾。TRACK 01 之前的 pregap(HTOA)目前算作 TRACK 01 的开头。如果你需要独立提取 HTOA,得手动调 shnsplit 参数(脚本目前不暴露此选项)。
Q: 切错了想撤回?
A: 因为原文件从不被删改,撤回很简单:找出符合 NN - Title.ext 格式的新生成分轨全部删除即可。示例见场景 8。
Q: 支持嵌套目录(比如多碟 CD1/CD2/)吗?
A: 支持。脚本会 os.walk 全递归,每一层子目录独立分析。多碟专辑的每个 CDx 目录都会被视为一个独立的专辑单元。
Q: 我可以在 Docker 里跑吗?
A: 可以。写个简单 Dockerfile:
FROM debian:stable-slim
RUN apt-get update && apt-get install -y \
python3 shntool cuetools flac ffmpeg \
&& rm -rf /var/lib/apt/lists/*
COPY split_cue.py /usr/local/bin/
ENTRYPOINT ["python3", "/usr/local/bin/split_cue.py"]
然后:
docker run --rm -v /mnt/z/music:/music my-split-cue /music --analyze-only
Q: 我想只切某一个格式(比如只 FLAC 不 APE)怎么办?
A: 目前没提供 filter 参数。可以用 shell 循环:
find /mnt/z/新收藏 -type d | while read d; do
if ls "$d"/*.flac >/dev/null 2>&1; then
python3 split_cue.py "$d"
fi
done
Q: 生成的 manifest 会很大吗?
A: 一般不大。50 个专辑的 manifest 约 30 KB。上千个专辑估计几 MB。不影响使用。
Q: transcode_music.py 和 split_cue.py 能同时跑吗?
A: 不建议。两者都会写源目录(split_cue 写分轨、transcode_music 写自己的 manifest)。虽然写的是不同文件不会冲突,但 transcode_music 分析时看到 split_cue 正在切的中间状态可能判断错误。串行跑更安全:split_cue 先跑完,transcode_music 再跑。
附录 A:完整命令示例集
# 分析 (不切割)
python3 split_cue.py /mnt/z/music --analyze-only
# 正式切割 (自动 resume)
python3 split_cue.py /mnt/z/music
# 详细日志
python3 split_cue.py /mnt/z/music -v
# 强制重跑 (覆盖已有分轨)
python3 split_cue.py /mnt/z/music --force
# 使用长参数
python3 split_cue.py --source /mnt/z/music
# 使用短参数
python3 split_cue.py -s /mnt/z/music
# 组合: 位置 + 选项
python3 split_cue.py /mnt/z/music -v --force
# 指定 shnsplit / cuetag 路径
python3 split_cue.py /mnt/z/music \
--shnsplit /opt/shntool-3/bin/shnsplit \
--cuetag /opt/cuetools/bin/cuetag
# 加大超时 (超长专辑)
python3 split_cue.py /mnt/z/music --split-timeout 3600
# 只处理某一个专辑
python3 split_cue.py "/mnt/z/music/王菲 - 唱游 [FLAC+CUE]"
# 完整工作流: split 然后 transcode
python3 split_cue.py /mnt/z/music && \
python3 transcode_music.py /mnt/z/music /mnt/x/mp3
附录 B:文件结构示例
运行前(典型的整轨型音乐盘):
/mnt/z/新收藏/
├── 王菲 - 唱游 [FLAC+CUE]/
│ ├── CDImage.flac ← 整张 CD 一个大文件, 40 分钟
│ └── CDImage.cue ← 12 个 TRACK
├── 蔡琴 - 老歌 [FLAC+CUE]/
│ ├── album.flac ← 整张 CD 一个大文件
│ ├── album.cue
│ ├── cover.jpg
│ └── booklet.pdf
├── 张学友精选 [已分轨]/ ← 已经是分轨的, 会 SKIP
│ ├── 01 吻别.flac
│ ├── 02 一路上有你.flac
│ ├── 03 情网.flac
│ └── (无 CUE)
└── Beatles - White Album [WAV+CUE]/ ← WAV 源, 会切成 WAV
├── disc1.wav
└── disc1.cue
运行 split_cue.py /mnt/z/新收藏 后:
/mnt/z/新收藏/
├── .split_cue_manifest.json ← 状态记录
├── 王菲 - 唱游 [FLAC+CUE]/
│ ├── CDImage.flac ← 保留不动
│ ├── CDImage.cue ← 保留
│ ├── 01 - 红豆.flac ← 新: 分轨输出 (含 tag)
│ ├── 02 - 催眠.flac
│ ├── 03 - 无常.flac
│ ├── 04 - 童 (国语).flac
│ ├── ... (共 12 个)
│ └── 12 - 精彩.flac
├── 蔡琴 - 老歌 [FLAC+CUE]/
│ ├── album.flac ← 保留
│ ├── album.cue ← 保留
│ ├── cover.jpg ← 保留
│ ├── booklet.pdf ← 保留
│ ├── 01 - 恰似你的温柔.flac ← 新: 分轨
│ ├── 02 - 你的眼神.flac
│ └── ... (12 个)
├── 张学友精选 [已分轨]/ ← 无变化 (SKIP)
│ ├── 01 吻别.flac
│ ├── 02 一路上有你.flac
│ └── 03 情网.flac
└── Beatles - White Album [WAV+CUE]/
├── disc1.wav ← 保留 WAV 整轨
├── disc1.cue ← 保留
├── 01 - Back In The U.S.S.R..wav ← 新: WAV 分轨
├── 02 - Dear Prudence.wav
└── ... (30 个)
接着运行 transcode_music.py /mnt/z/新收藏 /mnt/x/mp3:
/mnt/z/新收藏/ ← 未变
├── .split_cue_manifest.json
├── .transcode_manifest.json ← 新: transcode 状态
└── ... (专辑目录未变)
/mnt/x/mp3/ ← 目标目录
├── 王菲 - 唱游 [FLAC+CUE]/
│ ├── CDImage.cue ← CUE 复制过来
│ ├── 01 - 红豆.mp3 ← 320k CBR
│ ├── 02 - 催眠.mp3
│ └── ... (12 个 MP3)
├── 蔡琴 - 老歌 [FLAC+CUE]/
│ ├── album.cue
│ ├── cover.jpg
│ ├── booklet.pdf
│ ├── 01 - 恰似你的温柔.mp3
│ └── ...
├── 张学友精选 [已分轨]/
│ ├── 01 吻别.mp3
│ ├── 02 一路上有你.mp3
│ └── 03 情网.mp3
└── Beatles - White Album [WAV+CUE]/
├── disc1.cue
├── 01 - Back In The U.S.S.R..mp3
└── ... (30 个 MP3)
注意:
- 整轨大文件(
CDImage.flac、album.flac、disc1.wav)不会被转成 MP3,因为 transcode_music.py 检测到 situation C 会跳过它们 - 所有 CUE 文件会被复制到目标目录(作为资源文件)
- 分轨 MP3 保留完整 ID3 tag(继承自分轨 FLAC 的 Vorbis Comment)
Manifest 位置:
/mnt/z/新收藏/.split_cue_manifest.json ← split_cue 的状态
/mnt/z/新收藏/.transcode_manifest.json ← transcode_music 的状态
两个 manifest 都在源目录里,文件名不同,互不冲突。