背景与目标
三块盘上存了一批音乐,结构是历年下载攒出来的,越攒越乱:
L:\CloudMusic(14 GB)——主库,顶层混着「歌手名」「专辑名」,还有散落在根目录的单曲M:\CDs(4 GB)——EAC 抓轨存档,多套一层EAC/目录F:\MUSIC(8 GB)——存储卡,以前用的MP3用的,结构停留在旧版
目标很朴素:找出可删的重复文件,再把三块盘统一成规范的 歌手/专辑/曲目 结构。听起来像"跑个查重脚本 + 挪挪文件"的小活,实际做下来是一次不小的工程——中间还因为一个想当然的判断,把 9 首歌删错了版本。
技术选型
| 环节 | 选择 | 理由 |
|---|---|---|
| 查重 | 字节级 MD5(按文件大小预筛) | 证明"完全相同"最直接,预筛后只需对少量候选算哈希 |
| 元数据 | ffprobe 批量读 tag + 时长 | 拿 ARTIST/ALBUM/TITLE/duration,用于识别"同曲不同文件" |
| 删除 | send2trash(回收站) | 可反悔,比 rm -rf 稳妥 |
| 整理 | Python 脚本(walk + rename/move) | 结构化判断"目录名是歌手还是专辑",文本比不过脚本 |
方法:分层递进,而不是一上来就删
先盘点,再分类,最后才动手。 顺序是:
- 格式直方图——先数一遍 flac/mp3/m4a/wav 各多少、共多大,判断有没有 ALAC 双编码这种麻烦事,再决定要不要投 ffprobe 的时间成本。
- 字节级查重——按文件大小分组,只有同大小的才去算 MD5。上千个文件一筛,真正要比的没剩几个,MD5 足够(要证明的是字节一致,不是防篡改)。
- 元数据分组——按 (歌手, 标题, 时长±2s) 分组,把"同曲不同文件"的候选捞出来。
- 流签名分类——对每个候选组跑
ffprobe -select_streams a:0看sample_rate / bits_per_raw_sample / codec:完全相同=纯重复;FLAC+ALAC 同规格=同一无损源双编码;规格不同=不同母带/母带不同版本,不删,列为待决策。 - 结构整理——walk 出每个顶层目录,读元数据判断它到底是"歌手名"还是"专辑名"(靠 ARTIST/ALBUM 和目录名比对),专辑名的挪到歌手下,散单曲按元数据归位。
这套下来,三块盘的结构统一了,L 和 F 的重复也理清了。
踩过的坑
MSYS 路径喂给 Windows 原生 Python = 静默扫出 0 个文件。 脚本里 walk
/l/CloudMusic,但跑在打包的python.exe(不是 MSYS python)下,返回 0 文件还"干净退出"——不是报错,是安静地什么都没扫。必须翻译成盘符形式L:/CloudMusic。同理$TMPDIR展开成/tmp,原生 Python 解析成不存在的C:\tmp——用$LOCALAPPDATA/Temp。批量哈希超时。 242 个配对、约 8GB,一口气跑直接撞上 300 秒工具上限,整个 kernel 被杀、状态全丢。改成分批持久化:每批 35–50 个,把中间结果写盘,跨调用累积。慢,但不会一夜回到解放前。
归一化标题时把括号内容剥掉,会人为制造"重复"。
Song (inst ver.)被剥成Song,乐器版就和人声版并成一组了;(remix)、(live)、(2019 remaster)同理。匹配键里保留括号,或者凡是带括号的配对都人工过一眼,别自动合并。同标题 + 同时长 ≠ 同一首录音。 它是合理的候选筛选条件,不是判决——真正让判定安全的是第 4 步的流签名。乐器版、采样研究里的"处理前 vs 处理后"对比对,都能通过标题+时长的筛选,绝不能删。
send2trash要 Windows 原生反斜杠路径。 正斜杠路径L:/CloudMusic/x.zip在旧版 Windows 后端会因为短路径查找失败报错,得os.path.abspath(p).replace("/", "\\")。另外回收站清空前不释放空间——删完立刻读剩余空间是不变的,得说清楚可回收总量,否则用户会以为"没删成功"。
最痛的一课:同曲 ≠ 同版本
失误发生在这:跨盘同步时,规则定成"M 盘优先"——F 盘的歌若与 M/L 不同,就用 M 盘那份覆盖。听着很合理,直到它把 9 首具島直子的原专辑版本,覆盖成了 M 盘《Magic Wave》精选辑的版本。
问题出在匹配只看了标题和格式。精选辑是重新压片的:同一首歌,母带不同、时长差 1–3 秒(原专辑 340.5s,精选辑 337.7s)、文件反而更大。标题一样、都是 flac,脚本就认定"同曲",M 盘优先一覆盖——错版了。
能找回来,纯属运气好:L 盘还留着原专辑版本,没被这次同步碰过。于是照着 L 盘一份份换回去,MD5 逐一对齐,才恢复原状。
真正的教训写进了 skill,核心是三条:
- 盘符优先级 ≠ 版本优先级。覆盖前先确认候选文件的 ALBUM 标签和被替换文件一致——“同一专辑"比"哪块盘"更重要。
- 时长差 1–3 秒、或专辑标签不符,就是"不同版本"的信号。遇到就保留两者 / 列为待决策,绝不自动覆盖。
- 别拿低规格去"升级"高规格。24bit 的原件,永远不该被 16bit 的精选辑或 CD 抓轨覆盖。
结果
- ✅ 三块盘统一成
歌手/专辑/曲目结构,文件总数逐盘核对无丢失(L 376 / M 137 / F 244) - ✅ 可删的纯重复、双编码副本、废弃测试渲染都清了(回收站可反悔)
- ✅ 24bit hi-res 原件、乐器版、采样研究文件都保住了
- ✅ 9 首误换的具島直子已全部换回原专辑版本,MD5 与 L 盘一致
- ✅ 教训沉淀进了
windows-disk-cleanupskill 的两个 reference,下次跨盘同步会被提前拦住
下次可以做得更好
- 跨盘替换前,先保留一份原件快照,或者至少确认"要覆盖的那份,在别处还有副本”。这次能救回来是因为 L 盘没动;如果三盘一起同步,9 首原专辑版本就永久没了。
- 匹配记录里要保留 ALBUM 标签。当时只存了"同曲",没存"来自哪张专辑",事后想回答"这份是哪来的"都得重新扫。
- 别把"盘符优先级"当"质量优先级"。谁优先、谁覆盖,本质是"哪个版本更该留"的判断,得看专辑来源和规格,不是看盘符顺位。
- 时长差异要当成一等信号,而不是噪声。1–3 秒的漂移在"同曲不同母带"里几乎必然出现,看到就该警觉。
一句话总结:“同一首歌"是个技术判定,不是"文件一样”。跨盘同步省事的代价,就是把版本判断藏进了默认规则里——而版本判断,恰恰是最不该省的那一步。