5
1

Halo Fuwari 音乐馆重构:本地音乐库、NCM 转换、自动封面歌词

2026-05-12
2026-05-12

Halo Fuwari 音乐馆重构:本地音乐库、NCM 转换、自动封面歌词与点歌心愿单

前言

上一篇记录里,我把 Fuwari 主题从直接修改 templates/*.html 的方式迁移到了 Astro 源码开发流程。视频放映室、侧边栏归档、图库瀑布流都被拆成了更可维护的组件。

这次继续沿着这个方向,把“音乐馆”做成一个真正可长期使用的页面:我只需要整理音乐文件,脚本会自动转换、读取元数据、生成封面歌词和 songs.json,前台页面负责播放、筛选、搜索和点歌。

一、目标

这次音乐馆要解决几个实际问题:

  • 不再手动维护巨大的 JSON 歌单。
  • 支持服务器上的本地歌曲在线播放。
  • 支持按歌手、专辑、分区筛选。
  • 支持搜索歌名、歌手、专辑。
  • 自动读取 MP3/FLAC 等音频文件里的标题、歌手、专辑、音轨号、封面、歌词。
  • 支持网易云 .ncm 文件先转换再入库。
  • 音乐页不显示博客侧边栏,更像一个独立播放器页面。
  • 用户可以在音乐馆底部留言点歌。

二、页面架构

音乐馆不是单独写一个 /music.astro 路由,而是沿用 Halo 单页机制:

Halo 后台新建单页,别名 music
        ↓
templates/page.html
        ↓
singlePage.spec.slug == 'music'
        ↓
渲染 MusicLibrary.astro

这样做的好处是和视频放映室保持一致,不会被 Halo 的自定义页面路由卡住,也方便后台菜单直接指向 /music

核心入口在 src/pages/page.astro

<div
  th:if="${singlePage.metadata.name == 'music' or singlePage.spec.slug == 'music'}"
>
  <MusicLibrary />
</div>

同时我给 MainGridLayout.astro 加了一个 wideExpression,当页面是 music 时隐藏侧边栏:

<div
  id="main-grid"
  th:with={`layoutWide=${wideExpression}`}
  th:classappend="${layoutWide ? ' layout-wide' : ''}"
>
  <div class="contents" th:if="${not layoutWide}">
    <SideBar />
  </div>
</div>

于是音乐馆页面不会再挤在博客侧边栏旁边,而是使用完整主区域。

三、歌曲数据来源

最开始我考虑过把歌曲列表放进 settings.yaml,但这很快会变成维护灾难。几十首歌的标题、歌手、专辑、封面、歌词、URL 全塞进主题设置表单,后台会非常难用。

现在改为外部 JSON:

[
  {
    "title": "The Root of All Evil",
    "artist": "Dream Theater",
    "album": "Octavarium",
    "category": "ROCK",
    "track": 1,
    "cover": "/music-library/covers/Dream-Theater-The-Root-of-All-Evil.jpg",
    "url": "/music-library/songs/Dream-Theater-The-Root-of-All-Evil.mp3",
    "lrc": "/music-library/lyrics/Dream-Theater-The-Root-of-All-Evil.lrc",
    "duration": "8:25"
  }
]

主题设置里只需要填一个地址:

/music-library/songs.json

歌曲、封面、歌词都放在服务器的静态目录中,不再跟主题包一起上传。

四、服务器目录设计

我现在的目录大致是这样:

/opt/music-library/
├── ncm-source/              # 原始 ncm 文件
│   └── ROCK/
│       └── Dream Theater/
│           └── Octavarium/
│               ├── 01 - The Root of All Evil.ncm
│               └── 01 - The Root of All Evil.lrc
├── source/                  # 转换后的 mp3/flac
└── public/                  # 对外访问的静态目录
    ├── songs.json
    ├── songs/
    ├── covers/
    └── lyrics/

Nginx 暴露静态目录:

location /music-library/ {
    alias /opt/music-library/public/;
    add_header Access-Control-Allow-Origin * always;
}

访问:

https://blog.fallrain0905.top/music-library/songs.json

五、NCM 转换流程

.ncm 转换使用 ncmdump-go。主题项目里只写了一个包装脚本,它不下载音乐,也不处理账号,只转换我本地或服务器上已经存在的文件。

转换命令:

cd /opt/1panel/apps/halo/halo/data/themes/theme-fuwari

node scripts/convert-ncm-library.mjs \
  --input /opt/music-library/ncm-source \
  --output /opt/music-library/source

脚本实际调用:

ncmdump-go -d /opt/music-library/ncm-source -o /opt/music-library/source -r

-r 会递归保留目录结构,所以:

ncm-source/ROCK/Dream Theater/Octavarium/song.ncm

会转换为:

source/ROCK/Dream Theater/Octavarium/song.mp3

同目录下的 .lrc.txt.jpg.png 等旁挂文件也会一起复制到 source

六、自动生成 songs.json

转换完成后运行生成脚本:

node scripts/generate-music-library.mjs \
  --input /opt/music-library/source \
  --output /opt/music-library/public \
  --public-base /music-library \
  --category-depth 1 \
  --clean

脚本会扫描这些格式:

.mp3 .flac .m4a .ogg .wav

并自动读取:

  • 标题
  • 歌手
  • 专辑
  • 音轨号
  • 时长
  • 内嵌封面
  • 内嵌歌词
  • 同名 .lrc 歌词
  • 同名封面图片

--category-depth 用来控制页面分区来自第几层目录。

例如当前结构:

source/ROCK/Dream Theater/Octavarium/song.mp3

使用:

--category-depth 1

页面分区就是 ROCK

如果想用专辑作为分区,可以改成:

--category-depth 3

如果不想按文件夹分区:

--category-depth 0

七、前台音乐馆功能

MusicLibrary.astro 负责前台播放体验:

  • 顶部搜索框:搜索歌名、歌手、专辑。
  • 歌手筛选。
  • 专辑筛选。
  • 分区筛选,例如 ROCKACGPOP
  • 当前播放封面。
  • 播放、暂停、上一首、下一首。
  • 顺序播放、单曲循环、随机播放。
  • 音量控制。
  • 歌词显示。

歌词滚动这里踩过一个坑:一开始使用 scrollIntoView 让当前歌词居中,但它会带动整个页面自动跳到播放器底部。现在改成只滚动歌词容器:

ui.lyrics.scrollTo({
  top:
    activeLine.offsetTop -
    ui.lyrics.clientHeight / 2 +
    activeLine.clientHeight / 2,
  behavior: "smooth",
});

这样歌词可以跟随播放滚动,但不会强制改变页面位置。

八、图标显示修复

播放按钮、上一首、下一首一开始用的是 astro-icon 输出的 SVG。部分设备上按钮功能正常,但图标只剩一个空圆圈。

后来改成主题现有的 Iconify CSS 图标写法:

<span class="icon-[material-symbols--play-arrow-rounded]"></span>
<span class="icon-[material-symbols--skip-previous-rounded]"></span>
<span class="icon-[material-symbols--skip-next-rounded]"></span>

这和 Fuwari 主题其他位置的图标风格一致,也更稳定。

九、点歌心愿单

音乐馆底部新增了一个“点歌心愿单”。它没有额外写后端,而是复用 Halo 评论组件:

<halo:comment
  group="content.halo.run"
  kind="SinglePage"
  th:attr="name=${singlePage.metadata.name}"></halo:comment>

用户可以在音乐馆页面留言想听的歌,我在 Halo 后台评论管理里就能看到。

如果评论功能没启用,这个区域会自动隐藏。

十、部署与日常使用

主题代码更新后,需要本地构建并上传主题:

pnpm.cmd build:only

scp -P 2595 -r .\templates .\settings.yaml .\i18n .\theme.yaml .\scripts .\docs .\package.json .\pnpm-lock.yaml root@64.90.20.245:/opt/1panel/apps/halo/halo/data/themes/theme-fuwari/

然后重启 Halo:

docker restart 1Panel-halo-XnHB

日常加歌不需要重新构建主题,只需要:

cd /opt/1panel/apps/halo/halo/data/themes/theme-fuwari

node scripts/convert-ncm-library.mjs \
  --input /opt/music-library/ncm-source \
  --output /opt/music-library/source

node scripts/generate-music-library.mjs \
  --input /opt/music-library/source \
  --output /opt/music-library/public \
  --public-base /music-library \
  --category-depth 1 \
  --clean

然后刷新 /music 页面即可。

十一、关于缓存和旧文件

如果整理过目录结构,一定要注意旧的 source 目录可能还留着之前转换出来的歌曲。比如同时存在:

source/Dream Theater/Octavarium/song.mp3
source/ROCK/Dream Theater/Octavarium/song.mp3

那么生成出来的 songs.json 里就会出现两个分区:Dream TheaterROCK

最干净的做法是重建一次:

mv /opt/music-library/source /opt/music-library/source.bak-$(date +%Y%m%d-%H%M%S)
mkdir -p /opt/music-library/source

再重新转换和生成 JSON。

如果浏览器仍然显示旧数据,可以临时给主题设置里的 JSON 地址加版本号:

/music-library/songs.json?v=20260512-1

总结

这次音乐馆改造之后,维护方式从“手写 JSON + 手动传文件”变成了:

整理音乐目录 -> 转换 ncm -> 读取元数据 -> 生成 songs.json -> 前台自动展示

主题只负责页面和播放器,音乐资源独立放在服务器静态目录里。这样既不会让主题包越来越大,也不会每次加歌都重新上传几 GB 文件。

更重要的是,整个流程已经变成可重复的脚本化工作。以后加一个专辑,只需要把文件放到正确目录,跑两条命令,就能在音乐馆里看到它。

评论