背景
我的黑群晖(Xpenology,机型 SA6400,DSM 7.2)里存了 7 万多首音乐,中文为主、少量英文。问题是:很多歌没有歌词,一部分没有专辑封面。播放器用的是 Navidrome。
网上都推荐 music-tag-web 这个 Docker,装了之后发现——它是逐首手动匹配的 GUI。7 万首逐首点?不可能。核心诉求其实是「批量、自动」,而不是又一个手动工具。
于是自己写脚本解决。目标定得很清楚:
- 只补 歌词 和 封面
- 适配 Navidrome
- 绝不破坏原文件(因为这些音乐还在 PT 做种,改动会掉种)
本文时间线:2026 年 7 月 1 日晚 ~ 2 日。文中域名、IP、账号均已脱敏。
一、远程访问就踩了两个坑
人不在家,先要能连上 NAS。这里连着栽了两跟头。
坑 1:Cloudflare 代理的域名不能 SSH
NAS 的管理域名(形如 dsm.example.com)套了 Cloudflare 代理。结果:
443网页能通22SSH 握手直接超时
因为 Cloudflare 的橙云只转发 HTTP/HTTPS,不转发裸 TCP 的 SSH。用 nc 测端口还会给"开放"的假象(那是代理层接受了连接),但真正的 SSH banner 永远收不到。
坑 2:Tailscale 的地址段被另一条路由抢占
改用 Tailscale 连内网 IP(100.x.x.x)。tailscale status 显示 NAS 在线,tailscale ping 也通(走 DERP 中继),但普通 ssh / nc 全部超时。
排查路由表发现问题:
# route -n get 100.x.x.x → interface: en0, gateway: 192.168.64.x ← 错的!
100.64/10 192.168.64.x UGSc en0 ← 另一条路由抢了 Tailscale 的地址段
100.64/10 link#29 UCSI utun8 ← Tailscale 自己的接口
Tailscale 的 CGNAT 段是 100.64.0.0/10,本机上另一个虚拟网卡也声明了同段路由,且被系统优先选中,导致发往 NAS 的包没走 Tailscale。tailscale ping 用的是它自己的用户态通道(绕过系统路由表)所以能通,普通程序走系统路由就死。
解决:给 NAS 单独加一条 /32 主机路由(比 /10 更精确,优先命中),指向 Tailscale 的 utun8:
sudo route -n add -host 100.x.x.x -interface utun8
加完 SSH 秒连。(收尾时 route -n delete 即可,可逆。)
二、方案设计:非破坏是第一原则
因为要保护做种,方案定为只新增伴随文件,绝不碰音频:
| 类型 | 做法 | Navidrome 识别 |
|---|---|---|
| 歌词 | 在每首歌旁生成同名 .lrc(song.flac → song.lrc) |
原生读取外部 .lrc |
| 封面 | 在专辑文件夹里放 cover.jpg |
原生读取外部封面 |
为什么不嵌进音频标签? 嵌入 = 重写文件 = 改变哈希/mtime = 掉种。而生成伴随文件对种子来说是"额外文件",BT 校验时直接忽略。
后来还专门验证过:处理完的音频 .flac 的修改时间纹丝不动(还停在几个月前),只有旁边新增的 .lrc 是新时间戳。做种零影响。
歌词/封面源:实测 NAS 出网是**“国内通、国外不通”——music.163.com(网易云)通,lrclib.net 等国外源超时。所以主用网易云**,对中文歌覆盖极好,英文歌网易云也大多有。
三、环境搭建的三个小坑
坑 3:系统 Python 没有 pip
DSM 自带 python3(3.8),但 import pip 报没有模块。好在 ensurepip 可用,能离线引导出 pip,再用清华镜像装依赖:
python3 -m venv venv # ensurepip 会离线塞好 pip
./venv/bin/pip install -i https://pypi.tuna.tsinghua.edu.cn/simple mutagen
坑 4:scp 被登录 shell 的警告污染
该账号没有家目录,每次登录 shell 都吐一句 Could not chdir to home directory ...。这句话会污染 scp 的二进制协议,导致 scp: Connection closed。
解决:改用 base64 走 stdin 传文件,绕开 scp:
base64 < local.py | ssh user@nas 'base64 -d > /path/remote.py'
坑 5:SSH ControlMaster 复用连接
走 DERP 中继延迟高,每条命令都重新认证太慢。开一个 ControlMaster 长连接复用(注意 socket 路径要短,Unix domain socket 有 104 字节上限,别放太深的目录)。
四、歌词脚本核心
思路:遍历音频 → 读标签(缺了用文件名兜底)→ 网易云搜索按「标题/歌手/时长」打分匹配 → 只写带时间轴的 LRC → 已存在 .lrc 自动跳过(可断点续跑)。
关键的两个网易云接口(带 User-Agent / Referer / Cookie: os=pc 头):
搜索: GET https://music.163.com/api/search/get/?s=<关键词>&type=1&limit=5
歌词: GET https://music.163.com/api/song/lyric?os=pc&id=<id>&lv=-1&kv=-1&tv=-1
歌词接口返回的 lrc.lyric 就是带 [mm:ss.xx] 时间轴的 LRC,tlyric.lyric 是中文翻译。外语歌开启双语:把翻译按相同时间轴插进 LRC,形成中外对照。
匹配打分(避免张冠李戴):
score = 标题相似度*0.62 + 歌手相似度*0.26 + 时长接近度*0.12
# score >= 0.80 记为 ok,0.60~0.80 记为 low(仍写入),< 0.60 放弃
五、封面脚本核心(坑最多)
先搞清楚"到底缺多少"
全库 7877 个专辑文件夹,扫描分类:
- 已有外部封面(cover/folder/front…):3280
- 音频内嵌封面(Navidrome 也认):2509
- 真正缺图(外部、内嵌都没有):2088
所以逻辑是:外部无封面 且 无内嵌图,才去联网抓。用 mutagen 检测内嵌图(FLAC 的 pictures、ID3 的 APIC、MP4 的 covr)。
坑 6:搜歌返回的 album 对象没有封面 URL
一开始想"搜歌 → 取歌所属专辑封面",结果 type=1(搜歌)返回的 album 字段不含 picUrl,永远拿不到图。
解决:改用 type=10(搜专辑)直接拿 picUrl;或者由歌曲的 album.id 再查一次专辑详情 api/album/{id} 取封面。
坑 7:真缺图的往往正是"没标签"的
真缺封面的这 2088 个,很多当年就没打标签(album/artist 全空),只能靠文件夹名。而 郭静 - 2020 - 两个人的秘密 这种带年份的名字会把搜索搞懵。
解决:做三级兜底,取分最高者:
- 标签的
album + artist→ 搜专辑 - 代表曲目的文件名解析出「歌名 - 歌手」→ 搜歌 → 取专辑封面(并把"歌名/歌手"正反都打分,兼容英文库的
Title - Artist顺序) - 清洗文件夹名(去年份、去
[FLAC]/SACD/2CD等噪声,拆出歌手+专辑)→ 搜专辑
命中率从最初的 ~48% 提到 ~96%(测试样本)。封面统一下载 1000×1000,存为 cover.jpg。
六、坑 8:多任务并发会被网易云限流
歌词全量任务在跑时,我又并发测试封面,结果封面请求大面积返回空——触发了网易云的 -460(反爬)。歌词和封面不能并发,要错峰。
做法:用一个链式脚本 setsid 挂后台,轮询等歌词任务的 PID 退出,再自动接力跑封面。
七、结果
- 歌词:音乐库实际生成 56,068 个
.lrc(约占全库 77% ;未命中的主要是纯器乐、说唱、以及标签太烂匹配不到的冷门歌) - 封面:真缺口 2088 个里补上 1827 个,剩 203 个网易云确实没有
- 全程零破坏,做种不受任何影响
八、做成每日自动化
以后新下载的音乐要能自动补。NAS 上没有 inotify 工具,真·实时不现实,于是用每日定时 + 增量:
- 增量:用
last_run时间戳标记 +find -newer,只处理"上次运行后新增/改动"的文件。避免每天重复联网去查那批网易云本就没有的歌(否则每天上万次无效请求,还容易被限流)。 --min-age:跳过 10 分钟内改动的文件,避免处理还没下载完的音乐。- 原子锁:
mkdir锁防止任务重叠,超 12 小时的陈旧锁自动清除。 - 幂等:已有
.lrc/cover一律跳过,重复跑无副作用。
调度用 DSM 控制面板 → 任务计划 → 用户定义脚本,每天凌晨 4 点执行封装脚本 auto_process.sh。(DSM 任务计划比裸 crontab 更抗系统更新和重启。)
九、刷新 Navidrome
批量补完后让 Navidrome 重新识别。因为新增文件改变了文件夹 mtime,快速扫描通常就能认出。变更量大时用完整扫描最稳(Subsonic 兼容 API):
curl "http://localhost:4533/rest/startScan?u=账号&p=密码&v=1.16.1&c=curl&f=json&fullScan=true"
十、待办 & 经验
待办:对未命中的那批(网易云没有 / 匹配分过低)做二轮兜底,接入 QQ 音乐 / 酷狗 多源。教训是——别在一棵树上吊死,多源并行覆盖更全。(QQ 的 u.y.qq.com / c.y.qq.com 接口反爬较严,需要正确的 Referer 和签名,留待后续。)
几条经验:
- 非破坏优先:能用伴随文件(
.lrc/cover.jpg)就别改原文件,尤其是做种/有备份洁癖的库。 - 先验证命门再动手:动工前先手测"网易云 API 在 NAS 上真能返回带轴歌词",一条命令验证,避免白搭一套。
- 限流是真实存在的:单一公共 API 别开太高并发,任务之间要错峰。
- 自动化要做增量:全量重扫历史遗漏 = 每天给自己制造上万次无效请求。
- 网络诊断要看路由决策:
tailscale ping通不代表系统能路由过去,route -n get <ip>才是真相。