歌单散落在两个平台,是很多人的日常:一首歌网易云没版权,就去 QQ 音乐找;QQ 音乐上的红心歌单又和网易云的对不上。两个客户端来回切,界面风格各异,广告和推荐流也越来越重。
于是我给自己写了一个 webmuic——一个只跑在本机的 WebUI 音乐播放器,把网易云音乐和 QQ 音乐放进同一个界面里搜、听、收藏。
一句话定位
本机自用、只绑 127.0.0.1、凭证绝不出本机。 它不是一个要部署到服务器上给别人用的服务,而是一个打开浏览器就能用的「私人唱片室」。
整体架构
浏览器 WebUI http://127.0.0.1:5173 (Vue 3 + Vite + Pinia) │ 同源 /api(Vite 代理) ▼网关 http://127.0.0.1:8787 (Express + TypeScript) ├── 网易云后端 http://127.0.0.1:3010 NeteaseCloudMusicApi └── QQ 音乐后端 http://127.0.0.1:3200 Rain120/qq-music-api(打了补丁)一共四个进程,pnpm dev 会按 网易云 → QQ → 网关 → 前端 的顺序依次拉起,并等每个端口真正就绪再启动下一个。Windows 上也准备了双击即用的 install-windows.bat / start-windows.bat。
为什么要有一个网关
最初的想法很简单:前端直接调两个开源的音乐 API 后端不就行了?真做起来发现中间必须有一层,理由有三个:
- 凭证隔离。 平台 Cookie 是账号的全部权限,绝不能交给浏览器。网关把它放在本地文件里(权限 600、已 gitignore),网易云的 cookie 由网关逐请求注入,QQ 的 cookie 写进一个文件让 QQ 后端热读取。对前端只暴露 SHA256 的前 8 位指纹,用来显示「已登录」。
- 两个平台的数据长得完全不一样。 网关里的适配器把两边的响应归一化成同一套
Track/Playlist/Album/Artist,曲目 ID 统一成平台:平台内ID,比如netease:2652820720、qq:0039MnYb0qxYhV。音质也抽象成standard / high / lossless / hires四档,由适配器映射到各自平台的参数。 - 音频要走代理。 平台 CDN 有防盗链,直链也不该暴露给前端。网关的
/stream只放行白名单里的 CDN 域名,只透传 Range、Content-Type 这类播放需要的响应头。
别把平台打急了:限速、合并与熔断
聚合搜索很容易一下子打出一串上游请求。网关里做了三件事:
- 令牌桶限速:网易云每秒 3 个、QQ 每秒 2 个,带少量突发。
- SingleFlight:同一时刻相同的请求只发一次,结果共享。
- 熔断冷却:上游连续 5 次异常就进入 3 分钟冷却,界面上会提示「某平台暂时不可用」,另一个平台照常工作。
播放地址解析结果缓存 30 分钟(LRU + TTL,纯内存)。如果 CDN 返回 403——地址过期了但缓存还没到期——就清掉这首歌所有音质档位的缓存,重新解析一次。
给上游 QQ 后端打补丁
QQ 音乐这边用的是开源的 Rain120/qq-music-api,但直接用有几个问题,所以项目里有一个 scripts/patch-qq-backend.mjs,自动打 6 处补丁:
- 强制只监听
127.0.0.1,端口可由环境变量配置; - 新增从本地文件热读取 cookie 的逻辑,并在请求上游时带上 Cookie 头,换 cookie 不用重启;
- uin 从 cookie 中动态提取;
- 把已经失效的搜索接口替换成可用的那个。
打完会留下一个 .webmuic-patched 标记,启动脚本会检查它。
前端:一些值得一提的细节
前端是 Vue 3 Composition API + Pinia,没有用任何大型 UI 库,样式全部手写。
- 同步歌词:毫秒级同步、自动滚动、点击任意一句跳转,支持双语翻译和逐字卡拉 OK 高亮。
- 一键跳到副歌:播放栏上有「高潮」按钮,进度条上标出副歌位置,赶时间的时候直接听最好听的那段。
- 均衡器:基于 Web Audio 的 9 段 EQ 加低音增强,带几组预设。这里有个坑:
createMediaElementSource对同一个<audio>只能调用一次,而且非同源、没有 CORS 的音源接进来之后会直接静音——这也是为什么音频必须走网关的同源代理。 - 离线缓存:喜欢的歌可以存进 IndexedDB,下次播放优先从本地读。
- Media Session 与快捷键:系统媒体键、锁屏控制都能用,还有一个画中画桌面歌词。
最近一次改版:奶白复古风
最早的界面是深色「录音棚」风,后来试过一版蓝紫色的「月光唱片室」。这次彻底换成了奶白复古风:
- 浅色是暖奶白的纸张底色,配赤陶色强调;深色是暖炭灰配浅赤陶;
- 标题用衬线体,界面上叠了一层很淡的纸张颗粒;
- 发现页的头图换成一幅复古条纹落日,四个榜单卡片用青、芥黄、橄榄、铁锈四种做旧色;
- 歌词全屏页换成羊皮纸色,没有封面时会显示一张黑胶唱片。
改版时顺手把散落在各个组件里的硬编码颜色全部收进了一个 theme.css 的设计令牌里,以后调色只改一个文件。
一次小小的性能翻车
改完一打开——好慢。量了一下,首屏要 2.4 秒,整页加载 8.7 秒。罪魁祸首有两个:
- 远程中文字体。 Google Fonts 会把思源黑体、思源宋体切成几十个分片,每片在我这儿要 5~6 秒。
- 网易云封面默认是原图,单张最大 845KB,首页一次要几十张。
解决办法都很朴素:中文字体改用本机已装的字体,只保留体积很小的英文衬线体;网易云封面按显示尺寸加上 ?param=96y96 这样的参数请求缩略图,列表小图从几百 KB 降到几 KB。改完首屏 1.0 秒,整页 2.5 秒。
关于安全的几条底线
因为要处理真实账号的 Cookie,项目从一开始就定了几条不能破的规矩:
- 所有进程只绑定
127.0.0.1,包括 Vite 开发服务器; - 凭证只存在网关的本地文件里,永远不下发前端,前端的 localStorage 里也不存任何凭证;
- 音频代理只允许已知 CDN 域名,防止被当成任意 URL 的跳板。
开源前我还对全部提交历史做了一次密钥扫描,确认没有任何真实 Cookie 或 token 进过仓库。
写在最后
webmuic 不是什么宏大的项目,它只是解决了我自己每天都会遇到的一点小麻烦:在一个干净、安静、属于自己的界面里,听两个平台上的歌。
如果你也有类似的困扰,欢迎去 GitHub 看看。
声明:本项目仅供个人学习与本机自用,音乐版权归各平台及版权方所有,请勿用于任何商业或分发用途。