网页版个人音乐播放器

客户端的每一个请求都不可信任,必须假定是恶意请求。



一个网页版个人音乐播放器:
music_ice

线上地址:
https://yuqianglianshou.com/music_ice/

运行界面

桌面端:

Music Icee 桌面端播放界面

移动端:

Music Icee 移动端歌单界面 Music Icee 移动端播放详情界面



它不是一个音乐平台,也不是一个要接入账号、会员、推荐算法的产品。

它更像是一个很小的私人播放器:

把自己喜欢的歌整理好,
把歌词、封面、分类放进项目里,
然后用一个网页打开它。

有时候我越来越觉得,工具不一定要复杂。
真正能长期使用的东西,往往是边界很清楚的东西。


一、这个项目解决什么问题

我最初想要的东西很简单:

  1. 有一个干净的网页播放器。
  2. 能按分类管理歌曲。
  3. 能展示封面、歌词、播放进度、音量、播放模式。
  4. 能适配桌面端和移动端。
  5. 添加歌曲不要太痛苦。
  6. 不依赖后端服务,静态部署即可访问。

所以 music_ice 的定位很明确:

一个可以部署在 GitHub Pages 或任意静态服务器上的个人音乐播放器。

它的数据不在数据库里,而是在项目文件里。

歌曲文件、歌词文件、封面文件放在 media/ 目录;
列表缩略图放在 assets/covers/music-thumbs/
歌单数据按分类拆分在 catalog/
页面逻辑集中在 app/

整个项目的结构大概是这样:

music_ice
├── index.html
├── app/
│   ├── app.js
│   ├── config.js
│   ├── lyrics-manager.js
│   ├── music-store.js
│   ├── song-importer.js
│   ├── theme-manager.js
│   └── utils.js
├── catalog/
│   ├── index.js
│   ├── qingyinyue.js
│   ├── shangganzhiyu.js
│   ├── liuxingjingdian.js
│   └── ...
├── assets/
├── css/
├── media/
├── scripts/
└── tools/
    └── import-song.html

这个结构的好处是直观。

播放器是播放器,数据是数据,资源是资源,导入工具是导入工具。

以后想加歌,主要就是维护 media/catalog/


二、怎么使用

最简单的使用方式是直接打开线上地址:

https://yuqianglianshou.com/music_ice/

进入页面后,可以做这些事情:

  1. 切换不同分类的歌单。
  2. 点击歌曲播放。
  3. 切换上一首、下一首。
  4. 调整音量。
  5. 切换播放模式:列表循环、随机播放、单曲循环。
  6. 查看歌词同步滚动。
  7. 双击歌词跳转到对应播放时间。
  8. 在移动端打开播放面板,像一个小型音乐 App 一样使用。

如果只是听歌,到这里就够了。

如果要维护自己的歌单,就需要看项目内部的数据组织。


三、歌曲数据是怎么组织的

catalog/ 目录按分类拆分歌单。

比如:

catalog/qingyinyue.js      轻音乐
catalog/shangganzhiyu.js   伤感治愈
catalog/liuxingjingdian.js 流行经典
catalog/shishipeiyue.js    史诗配乐
catalog/jiezoulvdong.js    节奏律动
catalog/dongmanyingshi.js  动漫影视
catalog/huaijiujinqv.js    怀旧金曲
catalog/minyao.js          民谣

然后由 catalog/index.js 统一导出。

播放器启动时,app/music-store.js 会把这些分类数据汇总起来,并提供几个能力:

  1. 获取全部歌曲。
  2. 按分类获取歌曲。
  3. 搜索歌曲。
  4. 获取随机歌曲。
  5. 校验歌曲数据是否合法。
  6. 把歌单同步到 localStorage 缓存。

这里有一个比较实用的小设计:

项目不是手动维护一个固定版本号,而是根据歌单内容生成签名。

catalog/ 里的歌曲内容发生变化时,签名就会变化,播放器会自动刷新本地缓存。

这解决了一个静态站点常见的问题:

页面部署后,用户浏览器里可能还留着旧数据。

如果没有缓存刷新机制,明明已经加了新歌,用户打开页面却还是旧歌单,就会很困惑。


四、播放逻辑

播放器核心在 app/app.js

它没有引入复杂框架,而是直接使用浏览器原生的 Audio 对象。

核心状态大概包括:

currentMusicIndex; // 当前歌曲索引
playMode; // 播放模式
isPlaying; // 是否正在播放
currentMusicList; // 当前分类歌单
displayMusicList; // 当前展示歌单

当选择一首歌时,播放器会根据歌曲数据拼出音频路径:

./media/分类目录/歌曲文件

然后更新 audio.src

播放过程里监听这些事件:

  1. loadedmetadata:拿到歌曲总时长。
  2. durationchange:更新进度条范围。
  3. timeupdate:更新当前播放时间和歌词。
  4. ended:根据播放模式决定下一首。
  5. error:展示播放错误。

这套逻辑很朴素,但足够稳定。

浏览器已经提供了成熟的音频能力,项目要做的事情主要是:

把状态维护清楚,
把 UI 更新清楚,
把各种边界处理好。


五、歌词系统

歌词管理在 app/lyrics-manager.js

它负责四件事:

  1. 加载歌词文件。
  2. 解析 LRC 时间轴。
  3. 渲染歌词 DOM。
  4. 根据播放进度滚动和高亮当前行。

歌词文件支持类似下面的时间格式:

[00:29.299]歌词内容
[00:15:50]歌词内容

解析之后,会得到两个数组:

timerArray      歌词时间点
currentLyrics   歌词文本

播放时,根据当前 currentTime 找到应该高亮的歌词行。

这里用了一个小优化:

不是每次从头到尾扫描歌词,而是用二分查找定位当前行。

歌词短的时候看不出差异,
但这个思路是对的。

因为播放器这类页面会频繁触发 timeupdate,任何可以减少重复 DOM 查询和重复计算的地方,都值得处理一下。

另外,歌词渲染时也做了 HTML 转义。

这点很重要。

因为歌词文件虽然看起来是“自己的文件”,但只要内容最终进入 DOM,就不能默认它安全。

所以文章开头那句话,在这里也成立:

客户端的每一个请求都不可信任,必须假定是恶意请求。

更准确地说:

不仅请求不可信,输入文件、歌词内容、歌单字段,也都不应该被无脑塞进页面。


六、本地导入工具

这个项目里我比较喜欢的一部分,是 tools/import-song.html

它是一个本地导入工具。

打开方式:

tools/import-song.html

不过要注意,它需要浏览器的 File System Access API,所以最好通过本地 localhost 打开。

使用流程:

  1. 选择 music_ice 项目根目录。
  2. 选择歌曲分类。
  3. 选择歌曲文件。
  4. 选择歌词文件,可选。
  5. 选择封面文件,可选。
  6. 填写歌曲名、作者、时长、描述。
  7. 点击“添加歌曲”。

工具会自动完成这些事情:

  1. 把歌曲写入 media/对应分类/
  2. 把歌词写入 media/对应分类/
  3. 把封面写入 media/对应分类/
  4. 生成列表缩略图到 assets/covers/music-thumbs/
  5. 把歌曲条目追加到对应的 catalog/*.js
  6. 写入后再校验文件和歌单条目。
  7. 通过 BroadcastChannellocalStorage 通知播放器刷新歌单。

这解决了手动维护静态歌单时最麻烦的问题。

以前加一首歌,要自己复制文件、改路径、写对象、处理封面缩略图,很容易写错。

现在导入工具把这些动作收束成一个表单。

这就是小工具的价值:

不是为了炫技,
而是减少重复劳动,减少出错机会。


七、封面与缩略图

播放器有两类封面:

  1. 默认封面。
  2. 歌曲自己的封面。

如果歌曲没有封面,导入工具会使用默认封面。

如果歌曲有封面,导入工具会生成列表缩略图。

项目里还提供了脚本:

node scripts/generate-thumbnails.mjs

这个脚本会读取 catalog/*.js 中的 img_file 字段,自动生成缺失或过期的缩略图。

为什么要单独做缩略图?

因为列表里不应该直接加载大图。

音乐播放器的列表会出现很多歌曲,如果每一项都加载原始封面,会影响首屏速度和滚动性能。

大图留给播放详情,
小图服务于列表展示。

这是很普通但很有效的性能优化。


八、这个项目适合怎么部署

因为它是纯前端项目,所以部署很简单。

可以放在:

  1. GitHub Pages。
  2. 自己的静态服务器。
  3. Nginx 静态目录。
  4. 任意支持静态文件托管的平台。

部署时需要注意两点:

第一,资源路径要保持稳定。

项目默认使用相对路径,比如:

./media/
./assets/
./catalog/

所以部署到子目录时也能工作,比如:

/music_ice/

第二,音频文件通常比较大。

如果歌曲很多,仓库体积会迅速变大。

这也是这个项目目前最明显的边界:

它适合个人收藏,不适合做公开音乐平台。


九、技术上的优点

我觉得这个项目目前有几个地方是比较舒服的。

第一,边界清楚。

播放、歌词、主题、存储、导入工具都有独立文件,不是全部堆在一个脚本里。

第二,数据组织直观。

歌单按分类拆分,后期维护比一个超大数组更轻松。

第三,纯静态部署。

没有数据库、没有接口服务、没有登录系统,复杂度被控制住了。

第四,导入工具补齐了维护体验。

一个静态项目如果只能手改数据,时间久了就会变成负担。
导入工具让它更像一个可以长期使用的个人工具。

第五,歌词体验做得比较完整。

同步滚动、当前行高亮、双击跳转、滚轮临时暂停自动滚动,这些细节会让播放器更像“真正能用”的东西。


十、还可以继续优化什么

当然,它也还有很多可以继续做的地方。

  1. 增加播放历史。
  2. 增加收藏列表。
  3. 增加最近播放。
  4. 增加关键词搜索入口。
  5. 增加键盘快捷键说明。
  6. 增加 PWA,让它更像一个本地 App。
  7. 增加音频文件缺失检测。
  8. 增加 catalog 数据格式校验脚本。
  9. 增加更明确的版权和私有使用说明。

这里我最想强调的是版权问题。

这个项目更适合管理自己的本地音乐收藏。
如果公开部署并放入大量受版权保护的歌曲,就会有明显风险。

技术上能做,不代表现实中就应该随便做。


结语

music_ice 不是一个大项目。

它没有复杂架构,也没有宏大的产品设想。

但它很实用。

它把一个个人需求拆成了几个清楚的问题:

怎么播放?
怎么展示?
怎么分类?
怎么维护?
怎么部署?

然后分别解决掉。

我现在越来越喜欢这种项目。

它不一定惊艳,
但它真的能用。

一个能长期留在自己生活里的工具,
有时候比一个看起来很厉害、但很快就不会再打开的项目,更有意义。