在线音乐播放器满地都是,GitHub上一搜一大堆,我为什么还要自己写一个?
原因特别简单:我受不了“随机播放”了。
三百首歌的播放列表,点开随机播放,听二十几首就能撞到重复的——“生日悖论”,23 首之内出现重复的概率就超过一半。实际上体感感觉很多歌都重复放,想听的歌又一直放不到。
于是我干脆自己整了一个,大概长这样:

在线体验
一、介绍#
这个项目不是从零开始写的,它基于 HeoMusic(Apache-2.0 协议)魔改而来。
HeoMusic 为张洪大佬写的一个播放器,本身就是一个很优秀的静态播放器了(基于 APlayer + MetingJS),开箱即用、配置简单。不过经过我用 Vibe Coding 魔改之后 原项目架构已经被我改得不成样了。
上游的 index.html 只有 1 KB、config.js 是 570 字节的示例文件;我这边 index.html 18 KB、config.js 16 KB,player.js 写到 2400 行、modern.css 1907 行。基本上除了“这是个音乐播放器”这件事没变,其他都大改了一遍。
致谢:感谢 @张洪Heo 和 HeoMusic 的贡献者们,没有这个项目打底,我大概会一直停留在“想做一个播放器”的阶段。
二、功能与界面#
2.1 主界面:沉浸式的一屏#
打开就是一屏,没有多余的东西。背景为封面的模糊图,封面为主,侧边栏为辅,控制台在封面下方有进度条和控制按钮等。
2.2 歌词:逐行高亮#
右侧面板可自行切换歌词栏。当前行高亮加粗,上下行渐隐,唱到哪亮到哪:

支持逐行 + 逐字两种歌词格式,桌面端自动滚动到当前行居中。
2.3 播放列表:可搜可点#
「列表」页签是完整歌单,带序号、缩略图、歌名、歌手,顶部还有一个列表内搜索框:

我目前的歌单是 340 首(网易云歌单)+ 50 首本地音乐混合播放。本地音乐可以直接填直链(mp3 / flac),也可以单独配封面和歌词文件。
2.4 在线搜索点播#
想听什么直接搜,搜到就能点播,不用切到网易云再复制链接:

搜出来的结果会直接插进当前播放队列,听完继续回到原来的顺序
鸣谢:由GD音乐台提供的API
2.5 音效#
点击“滑块”图标,会弹出音效菜单:

关闭 / 3D环绕 / 沉浸 / 现场感 / 迷幻 / 流行 / 重低音,由 Web Audio API 实时处理。
当时弄这个音效整了挺久的,主要问题在于CORS跨域的问题,想要整音效,首先得整到音乐文件,而音乐文件得从网易云CDN爬取,这就麻烦了,网易云的有些CDN没有CORS跨域支持,就无法下载,当时折腾了挺久。
2.6 账户:播放记录云端同步#
游客模式可以完整使用(数据存在浏览器 localStorage),但如果你想换台设备继续听,可以注册个账户:

登录之后播放次数会同步到云端,换浏览器、换手机都不会丢了 这个数据同时也是下面「平均随机」算法的依据——听得越多,越不容易被重复抽到
2.7 移动端交互#
手机上的布局不是把桌面版压扁,而是重新做了一套:

底部导航四个入口(播放 / 歌词 / 列表 / 搜索),歌词可以全屏展开:

三、两个设计#
3.1 「平均随机」#
这是整个项目的起点,也是我最满意的一块。
问题在于:真随机 ≠ 用户想要的随机。用户要的不是“每次独立均匀抽样”,而是“这一轮里别重复,长期下来每首歌播放次数差不多”。
所以我用的是加权无放回抽样(Gumbel-top):
每首歌的 key = -ln(random) / (PRIOR + α × 播放次数)
按 key 降序排列 = 本轮播放队列这一个公式同时给了三个性质:
- 轮内零重复——排列天然保证,根治生日悖论
- 播放次数少的歌期望排前——长期收敛到“人人平等”
- 新歌不会被无限播——新歌按"已播 4 次"起步参与排序(贝叶斯平滑),每播一次让一步,播满 4 次后就和普通歌平权。解决了“新歌计数为 0,权重无限大,被反复抽中”的问题
两个参数都在 config.js 里,可以调:shufflePrior(越大,新歌特权越小)、shuffleAlpha(0 = 纯均匀洗牌,1 = 线性偏置,推荐 1)。
顺带还有两件小事:手动点播的歌会从本轮队列移除(同一轮不会二次抽到),歌单增删改序后播放计数也不丢(用 ne:{id} / lo:{url} 这种稳定标识,而不是数组下标)。
3.2 用户数据云写入#
账户同步这块踩过一个坑。
最初的协议是"全量替换":本地计数变了就把整份数据 PUT 上去。问题是服务端实现是 DELETE 全部 + INSERT 全部——一次同步写大约 2N 行(N = 该用户的计数行数)。我实测是 384 行,也就是一次同步写 768 行。
而我用的Cloudflare D1 写入额度比较少,10 万行/天——折算下来全站所有用户加起来,每天只能同步大约 131 次。两个真实用户各听 65 首就能把额度打穿。
后来改成了增量合并 + 写入节流:
播放 → 本地计数 +1(localStorage 永远不丢)
→ 变化的 key 记入 pending(落盘持久化)
→ 节流:30 秒防抖 + 60 秒最小间隔 + 3 分钟最长等待保底
→ POST 只发变化项,服务端 UPSERT 取 MAX(云端, 客户端)写行数从 768 降到 13 行,约 768 倍降幅,读请求也归零了(MAX 天然幂等,多设备互不覆盖,不再需要"先读再写回")。
再加一个 countPlayAfterSec(默认 20 秒)——实际播放满 20 秒才计入播放次数。快速切歌、试听不算数,既减少了写入,也让洗牌权重,不被"点开就跳过"的歌污染。
代价是上报有 30~60 秒延迟,但 pending 是落盘的,崩溃、强杀、关页都有兜底补传,数据不会丢。
四、自己部署一个#
整个前端是纯静态、零构建的——没有 npm install,没有打包步骤,把文件丢到平台上就能跑。
4.1 Fork 我的仓库#
Fork 666mingqing6/Music 到你自己的账号。
4.2 改 config.js#
config.js 是全站唯一的配置入口,所有东西都在这个文件里:
var userId = "12675886878"; // 你的网易云歌单 ID
var userServer = "netease"; // 服务商:netease / tencent / kugou ...
var userType = "playlist"; // playlist / song / album / artist
// Meting API 基地址(自建 Cloudflare Worker)
var metingApiBase = "https://meting-api.646474.xyz";
// 随机播放参数
var shufflePrior = 4; // 新歌冷启动平滑底数
var shuffleAlpha = 1; // 播放次数偏置强度
// 播放满多少秒才计入播放次数
var countPlayAfterSec = 20;
// 本地音乐(可选,支持 mp3 / flac 直链)
var localMusic = [
{
name: '歌名',
artist: '歌手',
url: 'https://example.com/song.mp3',
cover: 'https://example.com/cover.jpg',
lrc: 'https://example.com/song.lrc'
}
];至少要改的:
userId— 换成你自己的歌单 ID。从歌单分享链接里取,比如https://music.163.com/playlist?id=12675886878,ID 就是12675886878metingApiBase— 可以换成你自己的 API 地址(见下一步)localMusic可以留空数组,只用在线歌单也行
4.3 后端 API#
音乐数据(歌单、搜索、歌词、封面)需要一个 Meting API。我把自建的API项目也开源了:666mingqing6/meting-api,Cloudflare Workers + D1,部署即可。
4.4 部署到 Cloudflare Pages#
改完 config.js,剩下就是选个静态托管平台。我采用的是Cloudflare Pages 免费方案:
- 登录 Cloudflare Dashboard → Workers & Pages → Create → Pages → Connect to Git
- 选中你 fork 的仓库
- 其他啥也不用管
- 部署,等它跑完即可。
当然,因为它就是个纯静态站,Vercel、Netlify、GitHub Pages…… 都能部署,看你习惯哪个。
五、结语#
回头看,这个项目真正花时间的部分,几乎都不是"写功能"。
写一个能播歌的页面用AI可能一两小时就够了。剩下的时间都花在:音效的 CORS 深坑来回改了六版、随机算法从"均匀随机"推到加权洗牌、同步协议为了不把云配额打穿重构了三次、以及一堆"点开就跳过算不算播放"这种只有真用起来才会发现的问题。
这可能就是 Vibe Coding 的问题:让 AI 把第一版写出来很快,但把它变成一个真正能用、经得起日常使用的东西,还是得靠一次次踩坑和打磨
不过至少现在,我打开自己的音乐馆,按下播放,我就可以愉快地去,边听边享受,去干别的事了,只需要开着标签页就行。
这就够了。


