#transcode #music #readme #document #guide #mp3 #wav #flac #ffmpeg #cue # `transcode_music.py` 使用手册 批量将无损音乐(FLAC / WAV / DSF / DFF / APE)转码为 MP3 320k CBR 的 Python 脚本,支持断点续跑、错误恢复、CUE 整轨检测、DSD 转码。 --- ## 目录 - [1. 简介](#1-简介) - [2. 系统要求](#2-系统要求) - [3. 快速上手](#3-快速上手) - [4. 命令行参数详解](#4-命令行参数详解) - [5. 常用使用场景](#5-常用使用场景) - [6. Manifest 文件说明](#6-manifest-文件说明) - [7. 文件分类规则](#7-文件分类规则) - [8. CUE 整轨检测逻辑](#8-cue-整轨检测逻辑) - [9. 采样率与 DSD 处理](#9-采样率与-dsd-处理) - [10. 错误处理与恢复](#10-错误处理与恢复) - [11. 已知局限](#11-已知局限) - [12. FAQ](#12-faq) --- ## 1. 简介 **这个工具解决什么问题?** 你有一大堆无损音乐(比如 `/mnt/z/` 网盘上几百 GB 的 FLAC / WAV / DSF),想批量转成体积更小的 MP3 320k 存到手机/车载/移动设备,同时**保持原有的目录结构和文件名**,并且要能**中途暂停、下次继续**。 **核心特性**: | 特性 | 说明 | |---|---| | 保持目录结构 | 目标目录树与源目录一一对应,多碟专辑 (CD1/CD2) 分开处理 | | 保留所有资源 | 封面图片 (jpg/png/…)、说明 TXT、EAC log、CUE 都会复制过去 | | 断点续跑 | 转码状态记录在源目录的 `.transcode_manifest.json`,任意时刻中断都能从上次位置继续 | | CUE 整轨检测 | 分析阶段自动识别 "CUE + 单个大 flac 未分轨" 的专辑,报警并跳过等你手动 split | | CUE 已分轨识别 | 若 CUE 引用的是整轨但同时存在分轨文件,自动跳过冗余的整轨文件 | | 高采样率处理 | 24bit / 96kHz / 192kHz 输入自动重采样到 44.1kHz (符合 MP3 规范) | | DSD 支持 | DSF/DFF (DSD64 / DSD128) 自动解码降采样后编码 MP3 | | 保留封面 | 内嵌于 FLAC/DSF 的封面会写入 MP3 的 ID3v2 tag | | 错误隔离 | 单个文件失败不中断整体流程,错误写入 manifest 供后续排查 | | 幂等 | 已完成的文件/专辑,再次运行时秒跳过 | --- ## 2. 系统要求 | 组件 | 版本 | 说明 | |---|---|---| | Python | 3.10+ | 只用标准库,无需 pip install 任何依赖 | | ffmpeg | 6.0+ | 需支持 `libmp3lame` 编码器,`dsd_lsbf/msbf` 解码器 (DSD 用) | | ffprobe | 6.0+ | 通常随 ffmpeg 一起安装 | **检查环境**: ```bash python3 --version ffmpeg -version | head -1 ffprobe -version | head -1 # 确认支持 mp3 编码 ffmpeg -codecs 2>/dev/null | grep libmp3lame # 确认支持 DSD 解码 (如果你有 DSF 文件) ffmpeg -codecs 2>/dev/null | grep -i dsd ``` **安装 ffmpeg**(如果没有): ```bash # Ubuntu/Debian/WSL sudo apt install ffmpeg # macOS brew install ffmpeg ``` --- ## 3. 快速上手 **最常用的三个命令**: ```bash # 1. 先分析看看有没有问题 (不实际转码) python3 transcode_music.py /mnt/z/我的音乐 /mnt/x/mp3输出 --analyze-only # 2. 确认没问题后, 正式转码 (中断可再跑, 自动 resume) python3 transcode_music.py /mnt/z/我的音乐 /mnt/x/mp3输出 python3 transcode_music.py /mnt/d/music/ /mnt/x/music/20260823 # 3. 后来加了新专辑, 再跑一次 (已完成的会自动跳过, 只处理新加的) python3 transcode_music.py /mnt/z/我的音乐 /mnt/x/mp3输出 ``` ``` shell # 4. Macmini上的命令路径(NAS上 /volume2/navidrome已经成功mount): python3 transcode_music.py ~/MyMusic ~/mnt/volume2/navidrome/music/20260823 ``` [[Mac Mini NAS 挂载配置指南]] 就这么简单。下面是完整的参数说明和更多用法。 --- ## 4. 命令行参数详解 ### 完整语法 ``` transcode_music.py [SOURCE] [TARGET] [选项...] ``` 或 ``` transcode_music.py --source SOURCE --target TARGET [选项...] ``` ### 参数列表 | 参数 | 短选项 | 类型 | 是否必需 | 默认 | 说明 | |---|---|---|---|---|---| | `SOURCE` | — | 位置参数 | 必需¹ | — | 源目录,无损音乐所在位置 | | `TARGET` | — | 位置参数 | 必需¹ | — | 目标目录,MP3 输出位置 | | `--source` | `-s` | 字符串 | 必需¹ | — | 源目录(与位置参数 SOURCE 等价) | | `--target` | `-t` | 字符串 | 必需¹ | — | 目标目录(与位置参数 TARGET 等价) | | `--bitrate` | `-b` | 字符串 | 可选 | `320k` | MP3 CBR 比特率。可选 `320k` / `256k` / `192k` / `128k` 等 | | `--analyze-only` | — | 开关 | 可选 | 关 | 只分析生成 manifest,不实际转码 | | `--force` | — | 开关 | 可选 | 关 | 忽略现有 manifest,全部重新分析并转码 | | `--verbose` | `-v` | 开关 | 可选 | 关 | 显示 debug 级别日志(包括跳过的项) | | `--ffmpeg` | — | 字符串 | 可选 | `ffmpeg` | ffmpeg 可执行文件路径(默认从 PATH 查找) | | `--ffprobe` | — | 字符串 | 可选 | `ffprobe` | ffprobe 可执行文件路径 | | `--timeout` | — | 整数 | 可选 | `3600` | 单文件转码超时秒数(超时算失败) | | `--sample-rate-when-high` | — | 整数 | 可选 | `44100` | 输入采样率 > 48kHz 时的输出采样率,可选 `44100` / `48000` | | `--help` | `-h` | 开关 | — | — | 显示帮助并退出 | ¹ **源目录 和 目标目录 必须提供,但可以用位置参数或 flag 两种形式之一**: ```bash # 位置参数 (更简洁, 推荐) transcode_music.py /mnt/z/music /mnt/x/mp3 # flag 长形式 transcode_music.py --source /mnt/z/music --target /mnt/x/mp3 # flag 短形式 transcode_music.py -s /mnt/z/music -t /mnt/x/mp3 # 混合也可以 transcode_music.py /mnt/z/music -t /mnt/x/mp3 ``` ### 参数详细说明 #### `SOURCE` / `--source` / `-s` **源目录**。脚本会递归扫描此目录下所有子目录,对每个包含无损音频的子目录识别为一个"专辑单元"处理。 - 支持含空格、中文、特殊字符的路径 - 网盘挂载(`/mnt/z/`、`/mnt/x/` 等)都可以 - Manifest 文件 `.transcode_manifest.json` 会写入此目录 #### `TARGET` / `--target` / `-t` **目标目录**。MP3 输出位置。目录结构会与源目录保持一致。 - 目录会自动创建(含中间层) - 已存在的目标文件如果 manifest 记录 `completed` 会自动跳过 #### `--bitrate` / `-b` MP3 输出比特率(CBR,Constant Bit Rate)。默认 `320k`。 **推荐取值**: | 值 | 说明 | |---|---| | `320k` | 最高质量,几乎无损感知,推荐(默认) | | `256k` | 高质量,体积减小约 20% | | `192k` | 良好质量,体积减小约 40% | | `128k` | 一般质量,仅在存储极度紧张时使用 | **注意**: 一旦确定比特率跑过一次,配置写入 manifest。**中途改比特率**不会导致已完成的文件重新转码(脚本以文件 completed 状态为准)。想改需配合 `--force`。 #### `--analyze-only` **只分析,不转码**。用途: - 首次跑一个新的大目录时,先看看总专辑数、CUE 整轨的有没有 - 探测有没有解析错误的文件 - 生成 manifest 后可以手动查看/编辑 **推荐**每次对新目录都先跑一次 `--analyze-only`。 #### `--force` **强制重跑**。忽略源目录现有的 manifest,重新分析并转码所有文件。 **什么时候用**: - 换了比特率想全部重新转 - 怀疑之前的输出有问题 - 想清理并重新开始 **警告**:会覆盖已存在的 MP3。之前的 manifest 不会删除但会被覆盖。 #### `--verbose` / `-v` 打开 DEBUG 级别日志。默认 INFO 级别只显示正在处理和错误。加上 `-v` 会额外显示: - `[SKIP (已完成): ...]` 每个跳过的专辑 - `[SKIP (空目录): ...]` 跳过的空目录 - ffprobe/ffmpeg 的调试信息 大量输出,通常只在排查问题时用。 #### `--ffmpeg` / `--ffprobe` 指定 ffmpeg / ffprobe 的路径。默认从 `PATH` 查找。 **什么时候用**: - 系统装了多个 ffmpeg 版本,想指定用哪个 - ffmpeg 不在标准 PATH 里 示例: ```bash transcode_music.py /src /dst --ffmpeg /opt/ffmpeg-6.1/bin/ffmpeg ``` #### `--timeout` 单个文件转码的超时时间(秒),默认 3600(1 小时)。超时算失败,写入 manifest 后继续处理下一个。 **通常不需要调整**。除非你有超长的音频文件(比如整轨 3 小时的 wav)在慢网盘上转码,可以调大。 #### `--sample-rate-when-high` 高采样率输入(> 48kHz)降到多少 kHz。默认 `44100`(CD 标准)。 - `44100`:CD 标准,最兼容 - `48000`:视频/DVD 标准 对于 24bit/96kHz、24bit/192kHz、DSD(352.8k / 705.6k)等高采样率源,MP3 编码器不支持 > 48kHz,必须降采样。默认 44.1kHz 是 CD 标准,兼容性最好。 如果你的音乐主要来源是从视频/电影音轨提取的,用 48000 可能更保留原有采样率。 --- ## 5. 常用使用场景 ### 场景 1: 第一次处理新目录(推荐流程) 假设你刚下载了一大堆音乐到 `/mnt/z/新收藏/`,想转到 `/mnt/x/music/新收藏/`。 **第一步:先分析** ```bash python3 transcode_music.py /mnt/z/新收藏 /mnt/x/music/新收藏 --analyze-only ``` 输出会告诉你: - 总共多少个专辑 - 多少个音频文件、多少资源 - **有没有需要 CUE split 的专辑(`needs_cue_split`)** **第二步:处理 needs_cue_split 的专辑**(如果有) 对于报警的整轨专辑,你需要用别的工具(比如 `shntool`、`CUETools`、`XLD`)手动 split 成分轨,然后再回来跑脚本。 **第三步:正式转码** ```bash python3 transcode_music.py /mnt/z/新收藏 /mnt/x/music/新收藏 ``` 已经 `analyzed` 的专辑会开始转码,`needs_cue_split` 的会自动跳过(不影响其他)。 ### 场景 2: 中断后恢复 假设转到一半按 Ctrl+C 中断了(或者断电、脚本崩溃了)。 只需要**再跑同样的命令**: ```bash python3 transcode_music.py /mnt/z/新收藏 /mnt/x/music/新收藏 ``` 脚本会读取 manifest,跳过所有 `completed` 状态的文件和专辑,从上次中断的地方继续。**不需要任何特殊参数**。 ### 场景 3: 增量同步(新增了专辑) 以后你又下载了几个新专辑到 `/mnt/z/新收藏/`,直接跑: ```bash python3 transcode_music.py /mnt/z/新收藏 /mnt/x/music/新收藏 ``` 已完成的专辑秒跳过,只有新目录会被分析和转码。这是最舒服的用法。 ### 场景 4: 修复失败的文件 如果某个专辑因为源文件损坏等原因失败了(`status: failed`),你修复源文件后: ```bash # 直接再跑一次, failed 的会被自动重试 python3 transcode_music.py /mnt/z/新收藏 /mnt/x/music/新收藏 ``` 已成功的不会重跑,只有失败的会重试。 ### 场景 5: 换比特率重新转码 想把之前的 320k 全部改成 256k: ```bash # 加 --force 强制重跑 python3 transcode_music.py /mnt/z/新收藏 /mnt/x/music/新收藏 -b 256k --force ``` ### 场景 6: 只转一小部分做质量测试 比如你想先转一个专辑试听下音质,别一下转几百 GB。 **方法一**:直接指定那个专辑的父目录 ```bash # 只处理 "李宗盛精选" 这一个专辑 python3 transcode_music.py "/mnt/z/新收藏/李宗盛精选" /mnt/x/music/李宗盛精选 ``` **方法二**:跑 `--analyze-only` 看总数,觉得数量能接受再跑正式的。 ### 场景 7: 只想复制资源不想转码 不太常见。可以用位置参数让源和目标一致(虽然实际上 target 是别的,但 --analyze-only 后手动处理): ```bash # 只分析生成 manifest, 不转码 python3 transcode_music.py /mnt/z/music /mnt/x/mp3 --analyze-only ``` 然后手动编辑 manifest 把所有音频状态改成 `skipped` 再跑。**不推荐**,通常你就是想转码。 ### 场景 8: 排查某个专辑的问题 某个专辑失败了或者行为奇怪,用 verbose 模式跑: ```bash python3 transcode_music.py "/mnt/z/新收藏/有问题的专辑" /mnt/x/music/test -v ``` 输出会更详细,包括 ffprobe 探测细节、跳过原因等。 ### 场景 9: 用不同 ffmpeg 版本 ```bash python3 transcode_music.py /mnt/z/music /mnt/x/mp3 \ --ffmpeg /opt/ffmpeg-7.0/bin/ffmpeg \ --ffprobe /opt/ffmpeg-7.0/bin/ffprobe ``` --- ## 6. Manifest 文件说明 ### 位置 Manifest 保存在**源目录**下,文件名 `.transcode_manifest.json`(隐藏文件,前缀有点)。 ### 顶层结构 ```json { "manifest_version": 1, "created_at": "2026-08-16T15:05:03+08:00", "updated_at": "2026-08-16T15:19:06+08:00", "source_root": "/mnt/z/test", "target_root": "/mnt/x/music/test", "config": { "bitrate": "320k", "id3v2_version": "3", "output_sample_rate_when_high": 44100 }, "stats": { "total_albums": 5, "by_album_status": {"completed": 5}, "by_audio_status": {"completed": 50}, "by_asset_status": {"completed": 7}, "total_audio_files": 50, "total_asset_files": 7 }, "albums": { "专辑相对路径1": { ... }, "专辑相对路径2": { ... } } } ``` ### 单个专辑 (album) 结构 ```json { "status": "completed", "issues": [], "audio_files": { "01 - 北风.flac": { "type": "flac", "size": 29671831, "sample_rate": 44100, "channels": 2, "duration_sec": 297.36, "bits_per_sample": 16, "codec": "flac", "status": "completed", "target_rel": "01 - 北风.mp3", "error": null, "converted_at": "2026-08-16T15:18:04+08:00" } }, "asset_files": { "cover.jpg": { "type": "image", "size": 234567, "status": "completed", "target_rel": "cover.jpg", "error": null, "copied_at": "2026-08-16T15:18:05+08:00" } }, "ignored_files": ["Thumbs.db"], "has_audio": true, "started_at": "2026-08-16T15:18:04+08:00", "completed_at": "2026-08-16T15:18:06+08:00", "error": null } ``` ### 状态值 (status) **专辑级状态**: | status | 说明 | 下次运行时行为 | |---|---|---| | `pending` | 初始状态(一般用不到) | 会重新分析 | | `analyzed` | 已分析,等待转码 | 继续转码 | | `processing` | 正在处理(若看到,说明上次中断了) | 会重新处理未完成部分 | | `needs_cue_split` | 是 CUE + 整轨大文件,需先手动 split | **跳过**,不处理 | | `completed` | 所有文件都成功了 | **秒跳过** | | `failed` | 有文件失败 | 会重试失败的部分 | | `skipped` | 空目录,无音频且无资源 | 跳过 | **文件级状态**(audio_files 和 asset_files 里): | status | 说明 | |---|---| | `pending` | 待处理 | | `completed` | 已完成 | | `failed` | 失败(`error` 字段有原因) | | `skipped` | 主动跳过(如 CUE 引用的整轨已有分轨) | ### 手动干预 manifest Manifest 是纯 JSON,你可以用文本编辑器直接改: **例子 1:让某个专辑重新处理** ```json "status": "completed" → "status": "analyzed" ``` 把该专辑内的音频文件也改成: ```json "status": "completed" → "status": "pending" ``` **例子 2:跳过某个损坏的文件** ```json "01 - 有问题.flac": { ... "status": "skipped", "error": "手动跳过,源文件有损坏" } ``` **例子 3:删除某个专辑的记录,让它从头分析** 删除对应的 album 条目,下次运行会自动重新扫描添加。 **改完保存直接跑**: ```bash python3 transcode_music.py /mnt/z/music /mnt/x/mp3 ``` ### Manifest 损坏怎么办? 如果 JSON 损坏(编辑错了、盘故障),脚本启动时会自动检测,将坏 manifest 重命名为 `.transcode_manifest.json.corrupt-<时间戳>`,然后**重新生成新的 manifest**。已完成的 MP3 文件不会丢,但脚本会重新扫描所有文件(对已有 MP3 的处理由目标文件是否存在来判断,见"文件级幂等")。 **手动恢复方法**:如果损坏的 manifest 里有大量已完成状态,可以试着修复 JSON 语法后重命名回去。 --- ## 7. 文件分类规则 脚本会根据扩展名把文件分到几类: ### 无损音频(会转码为 MP3) `.flac` `.wav` `.dsf` `.dff` `.ape` ### 有损音频(当作资源直接复制,不转码) `.mp3` `.m4a` `.aac` `.ogg` `.opus` `.wma` `.mp2` **为什么不再压?** 因为它们已经是有损,再压一次损失更多。 ### 图片(复制) `.jpg` `.jpeg` `.png` `.bmp` `.gif` `.webp` `.tif` `.tiff` ### 文档(复制) `.txt` `.log` `.pdf` `.md` `.nfo` `.doc` `.docx` `.rtf` ### CUE(复制) `.cue` ### 忽略的文件(不处理) - `.ds_store`、`thumbs.db`、`desktop.ini`、`.directory`(系统文件) - 以 `._` 开头的(macOS resource fork) - `.transcode_manifest.json` 及其 tmp 中间文件 ### 其他扩展名 未列出的扩展名会被当作"other"资源直接复制,不会丢。 --- ## 8. CUE 整轨检测逻辑 CUE 文件常见有三种搭配: ### 情况 A:CUE + 单个整轨大文件(需先 split) ``` album.cue (指向 album.flac, 里面 12 个 TRACK 条目) album.flac (整张 CD 打包成一个大文件) ``` **处理**:脚本标记专辑状态为 `needs_cue_split`,跳过转码,在报告里警告: ``` [!] 1 个专辑需先手动 CUE split (已跳过转码): • album名 album.cue: 整轨型 (单文件 'album.flac', 12 轨) — 需先手动 CUE split 再转码 ``` **处理方式**:用其他工具(`shntool split`、`CUETools`、`XLD`、`foobar2000`)split 成分轨 flac 后,删除大整轨文件(或保留但用 CUE 引用改指向分轨),然后再跑脚本。 ### 情况 B:CUE + 分轨文件(已 split,直接转码) ``` album.cue (指向 track01.flac, track02.flac...) track01.flac track02.flac ... ``` **处理**:脚本正常转码所有分轨 flac,CUE 文件也作为资源复制过去。 ### 情况 C:CUE 引用整轨 + 但同时存在分轨(跳过冗余) ``` CDImage.cue (指向 CDImage.flac) CDImage.flac (整轨大文件, 冗余) (01) 曲名.flac (已经手动 split 出来的分轨) (02) 曲名.flac ... ``` **处理**:脚本自动跳过 `CDImage.flac`(避免重复转码),只转分轨,同时复制 CUE。报告里会说明: ``` CDImage.cue: 引用 'CDImage.flac' 但存在 20 个分轨 → 跳过 'CDImage.flac' 只转分轨 ``` ### 检测算法(技术细节) 对每个 CUE 文件: 1. 解析出所有 `FILE "xxx" WAVE` 条目(可能有多个) 2. 解析出所有 `TRACK NN AUDIO` 条目 3. 判断: - 若 CUE 只引用 1 个文件,且这个文件是目录里唯一的音频 → 情况 A(整轨) - 若 CUE 只引用 1 个文件,但目录里还有其他分轨音频 → 情况 C(跳过整轨) - 若 CUE 引用多个文件(>= 2)→ 情况 B(分轨) CUE 文件的编码会尝试多种(`utf-8-sig` / `utf-8` / `gbk` / `big5` / `shift_jis` / `cp936` / `latin1`),中日韩音源基本都能识别。 --- ## 9. 采样率与 DSD 处理 ### MP3 采样率约束 MP3 规范只允许这些采样率:8 / 11.025 / 12 / 16 / 22.05 / 24 / 32 / 44.1 / 48 kHz。 超过 48kHz 的输入**必须重采样**,脚本自动处理。 ### 具体规则 | 输入采样率 | 输出采样率 | 备注 | |---|---|---| | 44100 Hz | 44100 Hz | 透传,CD 标准 | | 48000 Hz | 48000 Hz | 透传 | | 32000 Hz 及其他 MP3 支持值 | 保持不变 | 透传 | | 88200 / 96000 Hz | 44100 Hz | 降采样(默认,可通过 `--sample-rate-when-high` 改为 48000) | | 176400 / 192000 Hz | 44100 Hz | 降采样 | | 352800 Hz (DSD64 解码后) | 44100 Hz | 精确 8:1 降采样(DSD64 特性)| | 705600 Hz (DSD128 解码后) | 44100 Hz | 精确 16:1 降采样 | ### DSD (DSF / DFF) 处理 DSD 是 1-bit 高频(2.8 或 5.6 MHz)采样格式,与传统 PCM 不同。脚本流程: 1. **ffprobe** 探测:报告 `codec_name=dsd_lsbf_planar` 或 `dsd_msbf_planar`,`sample_rate=352800` 或 `705600` 2. **ffmpeg 解码**:DSD → 高采样率 PCM(自带低通滤波器,96 tap,通带平坦到 48kHz,阻带抑制 ~160dB) 3. **重采样**:降到 44100 Hz 4. **编码**:libmp3lame 320k CBR DSD 内嵌的封面(通常是 mjpeg)会被识别为视频流并保留到 MP3 的 ID3v2 tag。 ### 位深处理 - 16-bit 输入:直接编码 - 24-bit 输入:libmp3lame 内部用 32-bit float 处理,等效于 24-bit 精度参与编码 - DSD (1-bit):解码为 24-bit PCM 后编码 --- ## 10. 错误处理与恢复 ### 错误捕获原则 **单个文件失败不影响其他**。转码或复制某个文件出错时: 1. 该文件状态设为 `failed`,错误消息写入 `error` 字段 2. 该专辑状态设为 `failed` 3. **继续处理**下一个文件、下一个专辑 4. 最后报告里列出所有失败项 ### 再次运行时 `failed` 状态的文件会**自动重试**。已经 `completed` 的不会重跑。 ```bash # 修好源文件后, 直接再跑 python3 transcode_music.py /mnt/z/music /mnt/x/mp3 ``` ### 常见错误 **"Error opening output files: Invalid argument"** 通常是 ffmpeg 无法识别输出格式或权限问题。脚本内部已加 `-f mp3` 强制指定 muxer 避免此问题。若仍出现: - 检查目标目录是否可写 - 检查文件名是否有极端特殊字符 **"Operation not permitted"(复制时)** 网盘挂载(9p / SMB / NFS)不允许改文件权限。脚本已经改用 `copyfile` + best-effort `utime`,通常应该没问题。若出现: - 检查目标目录挂载参数 - 尝试 `mount -o rw,noatime ...` 之类的选项 **ffprobe 失败 / 无法探测采样率** 源文件可能损坏。检查 `manifest.audio_files.<文件>.error` 里的具体消息。 **ffmpeg 超时** 默认 3600 秒。若真有超大文件慢网盘的组合,用 `--timeout 7200` 调大。 ### 中断(Ctrl+C) 脚本捕获 SIGINT,会保存 manifest 后退出。**中断后当前正在转的那个文件的 `.mp3.tmp` 中间文件会被清理**,不会留下坏的 MP3。 ### 用什么方法查看失败列表 ```bash # 用 jq 快速查看失败的专辑 jq -r '.albums | to_entries[] | select(.value.status=="failed") | .key' \ /mnt/z/music/.transcode_manifest.json # 查看某个专辑里失败的文件 jq '.albums["专辑名"].audio_files | with_entries(select(.value.status=="failed"))' \ /mnt/z/music/.transcode_manifest.json ``` ### 关于 needs_cue_split `needs_cue_split` 状态的专辑**不算失败**,是主动跳过等你手动处理。若不需要转这些,可以: **选项 1:修 manifest 忽略它们** 把状态改成 `skipped`: ```json "status": "needs_cue_split" → "status": "skipped" ``` **选项 2:手动 CUE split** 用其他工具 split 成分轨,然后: ```bash # 让脚本重新分析 rm /mnt/z/music/.transcode_manifest.json python3 transcode_music.py /mnt/z/music /mnt/x/mp3 --analyze-only ``` --- ## 11. 已知局限 - **不支持并行转码**。是单进程顺序处理。CPU 密集型(LAME 编码)加网盘 IO 是主要瓶颈。多进程可以显著加速,但为保证 manifest 状态一致性没有实现。若性能是问题,可以对不同子目录同时跑多个脚本实例。 - **不支持 VBR MP3**。只做 CBR。原因:CBR 兼容性更好,且 320k CBR 与 V0 VBR 音质差异对绝大多数人不可闻。 - **不做 replaygain / ID3v1 补齐 / 章节标记**。只做基本的 ID3v2.3 元数据映射。 - **CUE split 不会自动做**。检测出整轨专辑后只是报警跳过,不代替你调用外部工具去 split。设计如此以避免不可控的副作用。 - **不支持 ALAC (.m4a) 转 MP3**。因为 `.m4a` 容器里既可能是 ALAC 无损也可能是 AAC 有损,无法从扩展名区分。这些文件会被当作有损直接复制。若你有确定是 ALAC 的 `.m4a`,需要先用 ffmpeg 手动转成 flac。 - **网盘 mtime 不能保留**。9p / SMB 挂载不允许改 mtime,所以复制后的资源文件时间戳是当下时间(内容完全一致)。转码后的 MP3 也是同样情况。 - **不做重复检测**。若源目录有两个不同路径下的同名同内容文件,会各自转码两份。 --- ## 12. FAQ **Q: 我可以中途改比特率吗?** A: 已经 `completed` 的文件不会重转,即使你改了 `-b`。想全部按新比特率重跑就加 `--force`。 **Q: 手机不支持 320k 会怎样?** A: 所有正常的 MP3 播放器都支持 320k CBR,几十年的老设备也没问题。 **Q: 转码后 MP3 音质怎么样?** A: LAME 320k CBR 是"透明比特率"级别,即绝大多数听者在双盲测试中无法区分 320k MP3 和原始 FLAC。除非你有非常好的耳机和听音训练,几乎听不出差别。 **Q: DSD 转 MP3 有没有意义?** A: DSD 主要意义在于极高频响应(超过人耳范围)和低失真。转成 MP3 后这些优势基本没了。但 MP3 320k 仍能保留 DSD 音源的音乐性和大部分动态。若你就是想要个手机能播的版本,DSD → MP3 是合理的。想要"无损感"应该保留 FLAC。 **Q: 支持 Windows 直接跑吗?** A: 脚本用了 POSIX 的 `os.fsync(dir_fd)` 做原子写,Windows 上会 best-effort(fsync dir 会 fail 但不影响正确性)。理论上可以跑,但没实测。**推荐 Linux / WSL2 / macOS**。 **Q: 网盘断连了怎么办?** A: 单文件失败会记录到 manifest 并继续。重新连上后再跑一次,失败的会自动重试。 **Q: 生成的 manifest 会不会很大?** A: 一般不大。50 个专辑(我的测试目录)的 manifest 约 26 KB。上千个专辑估计几 MB。不影响使用。 **Q: 我想只转某几个专辑怎么办?** A: 最简单的办法是让 SOURCE 指向那个专辑的父目录: ```bash python3 transcode_music.py "/mnt/z/music/张学友精选" /mnt/x/mp3/张学友精选 ``` 或者临时把想转的目录复制/软链接到一个专用目录,跑完再删。 **Q: 能不能顺便做元数据整理(比如加艺人、专辑年份)?** A: 目前不做。脚本只做 `-map_metadata 0` 把源文件的 tag 原样映射过去。想批量整理元数据推荐 `beets`、`Picard`。 **Q: 转码完想验证质量怎么办?** A: 抽几首用 `ffprobe` 看输出的 `bit_rate` 应该是 320000 左右,`sample_rate` 是 44100 或 48000。 ```bash ffprobe -v error -show_entries "stream=codec_name,sample_rate,bit_rate" \ -show_entries "format=bit_rate,duration" \ "/mnt/x/mp3/xxx.mp3" ``` 或用播放器听一下确认。 --- ## 附录 A:完整命令示例集 ```bash # 分析(不转码) python3 transcode_music.py /mnt/z/music /mnt/x/mp3 --analyze-only # 正式转码(自动 resume) python3 transcode_music.py /mnt/z/music /mnt/x/mp3 # 详细日志 python3 transcode_music.py /mnt/z/music /mnt/x/mp3 -v # 使用 256k 比特率 python3 transcode_music.py /mnt/z/music /mnt/x/mp3 -b 256k # 强制重跑 python3 transcode_music.py /mnt/z/music /mnt/x/mp3 --force # 使用 48kHz 输出(视频 / DVD 场景) python3 transcode_music.py /mnt/z/music /mnt/x/mp3 --sample-rate-when-high 48000 # 使用长参数 python3 transcode_music.py --source /mnt/z/music --target /mnt/x/mp3 # 使用短参数 python3 transcode_music.py -s /mnt/z/music -t /mnt/x/mp3 # 组合:位置 + 选项 python3 transcode_music.py /mnt/z/music /mnt/x/mp3 -b 256k -v --force # 单独指定 ffmpeg 路径 python3 transcode_music.py /mnt/z/music /mnt/x/mp3 \ --ffmpeg /opt/ffmpeg-7/bin/ffmpeg \ --ffprobe /opt/ffmpeg-7/bin/ffprobe # 单文件超长的场景,加大超时 python3 transcode_music.py /mnt/z/music /mnt/x/mp3 --timeout 7200 ``` ## 附录 B:文件结构示例 **源目录**(真实的音乐盘结构示例): ``` /mnt/z/新收藏/ ├── 张学友精选/ │ ├── 01 - 吻别.flac ← 转成 mp3 │ ├── 02 - 一路上有你.flac ← 转成 mp3 │ ├── cover.jpg ← 复制 │ └── 专辑说明.txt ← 复制 ├── 王菲 - 天空 (24bit-192kHz)/ │ ├── 01. 天空.flac ← 24/192 → 降采样后转 mp3 │ ├── 02. 誓言.flac │ └── booklet.jpg ├── 蔡琴 2CD 精选/ │ ├── back.jpg ← 顶层封面, 复制 │ ├── cover.jpg │ ├── 目录.txt │ ├── CD1/ ← 独立处理 │ │ ├── 01 恰似你的温柔.wav ← 转成 mp3 │ │ └── 02 你的眼神.wav │ └── CD2/ │ └── 01 出塞曲.wav ├── DSD 专辑/ │ └── 谭咏麟-一生中最爱.dsf ← DSD 解码后转 mp3 └── 整轨专辑 [FLAC+CUE]/ ← ⚠ needs_cue_split ├── CDImage.flac ← 整个 CD 一个大文件 └── CDImage.cue ← CUE 索引 ``` **目标目录**(脚本处理后): ``` /mnt/x/mp3/新收藏/ ├── 张学友精选/ │ ├── 01 - 吻别.mp3 ← 320k CBR │ ├── 02 - 一路上有你.mp3 │ ├── cover.jpg │ └── 专辑说明.txt ├── 王菲 - 天空 (24bit-192kHz)/ │ ├── 01. 天空.mp3 ← 44.1kHz 320k │ ├── 02. 誓言.mp3 │ └── booklet.jpg ├── 蔡琴 2CD 精选/ │ ├── back.jpg │ ├── cover.jpg │ ├── 目录.txt │ ├── CD1/ │ │ ├── 01 恰似你的温柔.mp3 │ │ └── 02 你的眼神.mp3 │ └── CD2/ │ └── 01 出塞曲.mp3 ├── DSD 专辑/ │ └── 谭咏麟-一生中最爱.mp3 ← 320k └── 整轨专辑 [FLAC+CUE]/ ← 空(因为 needs_cue_split 被跳过) ``` **Manifest**(在源目录): ``` /mnt/z/新收藏/.transcode_manifest.json ```