从 YouTube Music 到 Navidrome:我搭建了一套属于自己的音乐库

从 YouTube Music 到 Navidrome:我搭建了一套属于自己的音乐库

免责声明:本文涉及的技术方案(如使用 yt-dlp 等工具下载网络资源)仅用于个人学习、技术研究与个人音源备份,请勿用于任何商业用途或侵权传播。尊重版权,请支持正版音乐。

这次折腾下来,我最大的收获反而不是记住了多少命令,而是逐渐理解了一件事情:一个真正好用的个人音乐库,不应该追求绝对完美,而应该追求能够长期、低成本地维护。


为什么要搭建

最开始的原因其实特别直接:我不想再一直为各个平台的音乐会员买单了。

能少花钱,同时又能通过折腾这套系统提升自己解决问题的能力,何乐而不为呢?而且我之前用 Apple Music 养成了按“专辑”听歌的习惯,很喜欢从一张完整专辑里去发现歌曲,而不是听算法推荐的单曲碎片。

但这又带来了一个很棘手的问题:去哪里找一个靠谱、稳定、内容又全的音源?

我实际使用下来发现,不同音乐平台的覆盖完全不同。比如 HiFiTi 的资源确实很丰富,很多经典 CD Rip 和无损都能找到,但它依赖社区用户上传,并不是“要啥有啥”。

比如之前看韩剧《机智医生生活》,我超级超级喜欢里面的一首插曲 《Let’s Forget It》。曹政奭那种带着一丝沧桑感的嗓音一出来,一下子就把我拉回到了金隽婠和李翼纯那段让人上头的甜蜜爱情里。当时我兴冲冲地跑去 HiFiTi 搜,结果整部剧的 OST 居然一首都没有!而且这种第三方分享网站随时可能关停跑路,作为长期依靠的音源,心里终究不踏实。

navid1.png|587

那种急着想听歌却找不到资源(而且是长期稳定资源)的挫败感,让我开始寻找替代方案——最后发现 YouTube Music 上几乎一应俱全。

这是当时在 YouTube Music 上搜索《机智医生生活OST》,得到的一系列结果:

navid2.png|585

点进插曲 《Let’s Forget It》的详细界面,发现音乐的 Title、Artist、Album、Release Date 等元数据非常完整,甚至还附带封面缩略图。

navid3.png|603

这让我意识到:YouTube Music 是一个很不错的长期音源储备。

我不再执着于找到一个“万能”的音乐网站,而是开始搭一个可以接受不同来源的音乐库,最后统一交给 Navidrome 来播放。我需要的不是另一个“音乐商店”,而是一个能把自己的音乐文件变成个人流媒体服务的服务器。

除了 YouTube Music ,我的 Navidrome 音乐库还可以接受不同的音乐来源:

navidraw1.png


怎么搭建系统

搭着搭着,我发现一个很重要的原则:不要找一个软件包办所有事情,而是让每个工具只负责它最擅长的事情。

整体架构

先看整套系统的运作流程:

yt-dlp 负责下载音频 -> beets 负责匹配 MusicBrainz 整理、刮削封面、拉取同步歌词 -> 下载整理好的歌曲文件存入 Navidrome 音乐文件夹 -> 客户端通过 IPv6 / tailscale 访问服务器对应端口,进行播放音乐
navidraw2.png

真正让我觉得这套架构舒服的地方,是每个环节可以独立出问题、独立排查。


前置准备:工具依赖与代理配置

在正式开始搭建前,先说明一下整套系统依赖的核心工具与安装方法。

强烈推荐使用 Windows 官方包管理器 winget,Win11 自带,Win10 不一定自带,但可以很方便地装上。它可以极大地简化命令行工具的安装与更新,避免手忙脚乱地去官网寻找解压包和手动配置环境变量。如果你的系统还没有安装 winget,可以通过 Microsoft Store 快速获取,非常值得安装的一款小工具!

通过 winget,只需在终端中运行以下命令即可完成 yt-dlp 的安装:

1
winget install yt-dlp

关于 FFmpeg 的说明:yt-dlp 进行音频提取和转码需要依赖 FFmpeg。好消息是,通过 winget install yt-dlp 安装时,会自动将 ffmpeg、ffprobe 等依赖一并安装到位,无需额外操作。

此外,整套方案还有一个很现实的前提:网络必须能够正常访问 YouTube。 整个获取链路的入口是 yt-dlp,如果 YouTube 无法访问,后面的 beets、MusicBrainz、封面和歌词都没有意义。

我在 Windows 上使用 v2rayN,本地代理监听在 10808 端口(注意:不同软件的端口不同,需按实际情况修改)。为了避免每次下载都输入代理参数,首先找到 yt-dlp 的 config 文件的位置:

在文件资源管理器输入以下命令:

1
%APPDATA%\yt-dlp\

把代理写进了配置文件:

1
--proxy http://127.0.0.1:10808

这样以后只需要运行:

1
yt-dlp "YouTube Music URL"

就可以自动走代理下载。

关于代理的问题,在这里不多说。


yt-dlp:负责“拿到音乐”

我常用的下载命令:

1
2
3
4
5
6
7
8
yt-dlp `
--no-playlist `
-x `
--audio-format opus `
--embed-metadata `
--embed-thumbnail `
-o "E:\yt-dlp\%(artist)s - %(title)s.%(ext)s" `
"YouTube Music URL"

这里有一个很有价值的技巧:--no-playlist 参数控制的是只下载当前单曲。如果想下载整个歌单或专辑,只要去掉 --no-playlist 即可。

单曲适合“发现一首收一首”,Playlist 和专辑适合“发现一个喜欢的集合,就一次性处理”。把 Playlist 的 URL 交给 yt-dlp,原本需要重复几十次的操作就变成了一次批处理。

日志输出示例:

1
2
3
4
5
6
[info] Downloading 1 format(s): 251
[info] Downloading video thumbnail ...
[download] Destination: E:\yt-dlp\鳳凰傳奇 - 全是愛.webm
[ExtractAudio] Destination: E:\yt-dlp\鳳凰傳奇 - 全是愛.opus
[Metadata] Adding metadata
[EmbedThumbnail] mutagen: Adding thumbnail

beets:负责“整理与补全”的核心大脑

beets 是整个系统里最像“大脑”的部分。需要说明的是,fetchart 和 lyrics 并不是独立的第三方软件,它们全都是 beets 官方自带的强大扩展插件。

安装与验证

在 Windows 环境下,通过 pip 安装 beets:

1
python -m pip install --user beets

安装完成后,在 PowerShell 中验证版本及默认插件加载情况:

1
2
3
4
PS C:\Users\DEL> beet version
beets version 2.13.1
Python version 3.12.7
plugins: embedart, fetchart, lyrics, musicbrainz

beets 的数据库文件位于 %APPDATA%\beets\library.db
配置文件位于 %APPDATA%\beets\config.yaml

需要注意的是:数据库本身体积非常小,因为它保存的是索引和音轨元数据,而不是复制一份真实的音频文件。

我的 beets 配置

需要注意的是,首次安装 beets 后,配置文件默认并不存在。如果文件夹或文件不存在,可以通过以下 PowerShell 命令安全创建(若文件已存在则跳过,不会覆盖现有配置):

1
if (-not (Test-Path "$env:APPDATA\beets\config.yaml")) { New-Item -ItemType File -Path "$env:APPDATA\beets\config.yaml" -Force }

下面是我整理后的 config.yaml 配置文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
# =========================
# Beets / Navidrome 基础设置
# =========================
directory: E:/navidrome/music
library: C:/Users/DEL/AppData/Roaming/beets/library.db

# 原始发行日期优先
original_date: yes

# =========================
# 导入与匹配策略 (Import)
# =========================
import:
# 把 beets 处理后的标签写回文件
write: yes
# 导入成功后自动移动文件到正式音乐库
move: yes
copy: no
# 自动尝试 MusicBrainz 匹配
autotag: yes
# 同一个目录里允许按单曲分组
group_albums: yes
# 保留已有标签作为匹配基础
from_scratch: no
# 发生无法高置信度匹配时不要自动乱套
quiet_fallback: no
# 导入时显示详细的匹配对比
detail: yes
# 继续之前中断的导入
resume: yes
# 只有一个候选时不要盲目自动选中
singletons: no

# =========================
# 路径结构 (Paths)
# =========================
paths:
default: $albumartist/$album/$track - $title
singleton: $albumartist/$album/$title

# =========================
# 插件启用 (Plugins)
# =========================
plugins:
- musicbrainz
- fetchart
- embedart
- lyrics

# =========================
# 插件详细配置
# =========================
musicbrainz:
search_limit: 5
search_query_ascii: no
genres: yes
genres_tag: genre
external_ids:
discogs: no
bandcamp: no
spotify: no
deezer: no
tidal: no
aliases_as_credits: no

fetchart:
auto: yes
minwidth: 500
maxwidth: 1200
sources:
- filesystem
- coverart
- itunes
- albumart
art_filename: cover.jpg

embedart:
auto: yes

lyrics:
auto: yes
synced: yes
keep_synced: yes
dist_thresh: 0.11
sources:
- lrclib
- lrcmux
- genius
force: no

插件协同

在配置 config.yaml 开启插件后,beets 便化身为全能中枢:

  • MusicBrainz(辅助识别):
    负责识别歌曲、确认身份与标准命名。最开始我以为:MusicBrainz 既然是权威数据库,那匹配之后照着改就行了。后来发现完全不是这样。
    例如 YouTube Music 上的艺人名和歌名是英文 Mido and Falasol - Me to You, You to Me,而 MusicBrainz 匹配到的全韩文 미도와 파라솔 - 너에게 난, 나에게 넌,虽然是同一首歌,但全韩文不符合我的检索习惯;又比如戴佩妮与 GAI 周延合唱的《明知山》,MusicBrainz 甚至可能给出一个完全不相关的候选。

    所以我形成了自己的原则:MusicBrainz 负责确认“这是什么”,但不一定决定“我应该怎么显示它”。

    1. 匹配明显正确:执行 Apply
    2. 明显错误或语言不合习惯:执行 Use as-is(保持原样)
    3. 模棱两可:人工看一眼对比结果再决定
  • fetchart & embedart 插件(正方形封面补全与嵌入):
    自动解决封面问题。yt-dlp 抓到的 YouTube 封面往往是 16:9 的视频缩略图,在播放器里很不美观。fetchart 会根据 Artist / Album 自动拉取高质量正方形专辑封面,并由 embedart 嵌进音频文件本身中。

YouTube 封面和 fetchart 封面的对比:

  • lyrics 插件(同步歌词拉取):
    自动通过 lrclib、lrcmux 等源拉取 LRC 同步歌词并嵌入绑定。

一次单曲导入流程

当通过 yt-dlp 下载好一首单曲后,我们运行 beet import 命令将其打标并归档。下面是一次真实的终端导入演示:

一般通过以下命令导入单曲:

1
beet import "音乐文件所在的路径"
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
PS C:\Users\DEL> beet import "E:\yt-dlp\伍佰 & China Blue - 挪威的森林.opus"

E:\yt-dlp\伍佰 & China Blue - 挪威的森林.opus (1 items)

Finding tags for album "伍佰 & China Blue - 愛情的盡頭".
Candidates:
1. 46.8% 伍佰 & China Blue - 愛情的盡頭
≠ year, missing tracks
MusicBrainz, CD, 1996, TW, 魔岩唱片, None, None
2. 43.2% 伍佰 & China Blue - 愛情的盡頭
≠ year, tracks, missing tracks
MusicBrainz, CD, 1996, TW, 魔岩唱片股份有限公司, MSD-018, None
3. 34.4% 伍佰 - 愛情的盡頭
≠ artist, year, missing tracks
MusicBrainz, None, 1996, TW, None, None, None
4. 25.9% 伍佰 & China Blue - 无尽闪亮的哀愁
≠ album, year, tracks, ...
MusicBrainz, CD, 2013, TW, Universal Music, 889 676-9, None
5. 24.9% 伍佰 & China Blue - 无尽闪亮的哀愁
≠ album, year, tracks, ...
MusicBrainz, Digital Media, 2013, CN, 環球音樂, None, None
➜ # selection (default 1), Skip, Use as-is, as Tracks, Group albums,
Enter search, enter Id, aBort? 1

Match (46.8%):
伍佰 & China Blue - 愛情的盡頭
≠ year, missing tracks
MusicBrainz, CD, 1996, TW, 魔岩唱片, None, None
https://musicbrainz.org/release/f8eaef16-383a-415d-ac9d-a4795c9e0cd2
* Artist: 伍佰 & China Blue
* Album: 愛情的盡頭
≠ (#0) 挪威的森林 (6:31) -> (#9) 挪威的森林 (6:31)
Missing tracks (9/10 - 90.0%):
! 夏夜晚風 (#1) (3:34)
! 愛情的盡頭 (#2) (6:15)
! 親愛的,你喝醉了 (#3) (3:48)
! 變 (#4) (3:31)
! 七彩燈光 (#5) (5:53)
! Last Dance (#6) (4:31)
! 青春 (#7) (3:30)
! 人生一場夢 (#8) (4:00)
! 隨風而去 (#10) (3:12)
➜ Apply, More candidates, Skip, Use as-is, as Tracks, Group albums,
Enter search, enter Id, aBort? a

在这段交互中,非常形象地展示了 beets 工作的细节与坑点应对:

  1. 匹配机制与报警:beets 默认会优先把单曲当成完整专辑来匹配,因此列出了候选专辑并警报 Missing tracks (9/10 - 90.0%)。
  2. 正确应对选项:面对提示 ➜ Apply, More candidates, Skip, Use as-is, as Tracks... 时,按下键盘 a(即 Apply),确认接受当前专辑匹配结果,选定后,beets 就会以此为准抓取封面歌词并归入专辑档案,自动整理归档入 E:\\\\navidrome\\\\music 正式音乐库:

574

不过这里的选择因人而异:如果你本来就想按专辑归档管理,选 Apply 没问题;但如果你只是想收藏某一首单曲(甚至这首歌压根没有所属专辑),那更合适的做法是在导入时加上 -s 参数,直接以单曲模式导入,跳过专辑匹配。具体用法见后文「补充技巧-单曲的识别」部分。

专辑导入流程补充

专辑可以从 YouTube music 下载获取,也可以从其他渠道获得,在这里以网络上下载的 wav 未分轨的专辑文件为例子说明:

首先,需要下载一个分轨工具:CUETools

至于你问为什么要分轨,因为如果不分轨,一张专辑都在一个 wav 里面,beets是无法识别的。

navid9.png|588

进入 CUETools 进行整轨文件的分轨:

navid7.png|536

可以看到,歌曲分轨成功,就是这么舒服,就是这么简单!

navid8.png|596

剩下的导入流程和单曲导入大差不差,唯一的差别在于 beet import 后面接的路径需要是整张专辑歌曲所在的路径,注意一下这一点应该就没有什么问题了。


Navidrome 的部署可以在 Windows 本地部署,也可以通过 Docker 部署(适合后续迁移)。在 Windows 下,Navidrome 原生支持通过自身命令将其注册为 Windows 后台服务(Navidrome.exe service execute 机制),实现开机自启与后台无感运行。

修改主文件库配置时没有图形交互界面,需要直接修改安装路径下的 ini 配置文件。

首先进入安装目录:

1
C:\Program Files\Navidrome

打开 navidrome.ini,通过更改 MusicFolder 的值来指定 Navidrome 的主音乐文件库:

1
2
3
4
5
[default]
Port='4533'
MusicFolder='E:\navidrome\music'
DataFolder='C:\ProgramData\Navidrome\'
FFmpegPath='C:\Program Files\Navidrome\ffmpeg.exe'

更改完成后,通过 PowerShell 以管理员权限重启 Navidrome 服务即可生效:

1
Restart-Service Navidrome

或者使用服务控制命令:

1
2
net stop Navidrome
net start Navidrome

日常管理小技巧:

  • 查看服务状态:Get-Service Navidrome
  • 实时查看运行日志:Get-Content "C:\ProgramData\Navidrome\navidrome.log" -Wait

重启服务后,进入后台管理界面 http://localhost:4533,观察音乐库路径更改是否生效:

navid10.png|597


客户端访问

关于客户端的选用,可以看看官方的网站:Client Apps | Navidrome。

我日常使用的比较多的是 iOS 端的 Amcfy Music,选择它的理由有很多,比如有实时的息屏歌词显示,而且有小组件可以使用,缺点是界面和 Apple Music 相比不够苹果化,界面没有那么圆润流畅。

服务搭建好后,在局域网内直接访问 服务端 IP 就能听歌。但如果出门在外,手机客户端该如何连接后端的 Navidrome 呢?

目前我有两种成熟的连接策略:

  1. IPv6 直连访问:如果家里宽带带有公网 IPv6 地址,配置好 DDNS 动态域名解析与端口转发,就能以极低延迟直连家里的音乐服务。具体配置流程可以参考我之前写的这篇文章:基于原生 IPv6 与 NPM 实现 Win11 本地服务低延迟公网暴露 。
  2. Tailscale 虚拟局域网:如果不具备公网 IP 环境,或者不想将服务暴露在公网上,直接开启 Tailscale 构建异地虚拟局域网是最方便的安全选择。详细走通与实战流程可以看我的另一篇记录:老毛子 Padavan 路由器 IPv6 踩坑全记录与 Tailscale 直连优化,这种方法延迟可能会比较大,但是如果客户端和服务器连接在同一个网络的情况下,通常可以做到 direct 直连。

长期维护

有人可能会问:Navidrome 本身或其它第三方客户端难道不能直接自动刮削封面和歌词吗?为什么还要费劲用 beets 在本地处理好再导入?

对我来说,这种数据与服务解耦的做法有着极其关键的意义:

  1. 封面与歌词质量完全可控:不用忍受服务端刮削错乱的低清图片或匹配失败的歌词;
  2. 元数据直接写入音频文件,随时自由迁移:所有 Metadata、封面和 LRC 歌词全部内嵌在文件本身中。以后哪怕我换掉 Navidrome,换成 Plex、Jellyfin 甚至移动到云端,所有音乐数据依然完好无损,绝不会因为服务器迁移而丢失;

navidraw4.png

  1. “前置自动化整理”的架构:我将“下载区”与“正式库”彻底分离:
    • 下载区:E:\yt-dlp(临时暂存)
    • 正式库:E:\navidrome\music(只存放经过 beets 筛选整理后的音乐)

这样的架构让各个模块彻底解耦:换下载工具不影响 Navidrome,换歌词/封面插件不影响音频存储,换播放服务器不影响数据资产。


补充技巧

  • 下载报 HTTP Error 403: Forbidden 异常的避坑经验:
    如果运行 yt-dlp 下载时提示 403 错误或被 YouTube 识别为“未登录机器人流量拦截”,往往是因为手动下载的可执行文件版本过老,触发了 YouTube 接口的风控机制。此时最快最省心的解决方法不是繁琐地配置 Cookie,而是直接使用包管理器重新覆盖安装最新版:

    1
    winget install yt-dlp --force

    或者通过 yt-dlp -U 强制更新到最新版本,问题即可迎刃而解!

  • 如果想强制重新获取某一首歌的歌词,可以执行:

1
beet lyrics -f title:"月亮"

对于已有同步歌词的歌曲,提示 Keeping synced lyrics 说明原有的歌词已被妥善保留。

  • 单曲的识别,只需要在命令里加一个小小的 -s 参数:
1
beet import -s "E:\test\2015-清风徐来[单曲]"

这是为了明确告诉 beets:“这是一首独立的单曲,不需要去找整张专辑”。


未来展望

目前这套系统保存在我Windows 11 系统的笔记本电脑里,受限于内网环境,虽然有 IPv6 和 Tailscale 加持,但面对一些复杂网络场景(比如网络波动或主机未开机),依然无法做到 100% 随时随地瞬间响应。

因此,我后续有两个很明确的升级和探索小计划:

  1. 迁移至公网云服务器:购买一个带有固定公网 IP 的云服务器,直接把 Navidrome 部署到云端。得益于此前做好的“数据解耦”设计,到时候我只需要把 E:\navidrome\music 整个音乐库打包同步上传,就能以极低的成本完成迁移,实现毫无顾虑的“随时随地无缝听歌”;

  2. 开发并开源“一键自动化导入工具”:我打算把整套流程封装,作为我在 GitHub 上的第一个个人项目发布!它将提供傻瓜式环境一键配置(自动处理代理环境),用户只需要输入歌名或 URL,系统便全自动完成获取、匹配、正方形封面刮削、歌词拉取并写入目录。

工具的流程图如下:

navidraw3.png

我最终想建立的,不是一个“全世界最高音质”的展示柜,而是一套属于我自己、可以不断积累、可以随时访问、可以逐步升级,而且多年以后依然愿意维护的音乐系统。

如果这篇文章对你有帮助,请我喝杯咖啡吧 ☕