Repository navigation
Releases: deltrivx/ToneCore
Release list
v0.34.0
ToneCore v0.34.0
实现「我的网易云歌单」(补 UI + 修字段映射);Subsonic 补 getMusicDirectory。
一、我的网易云歌单
真因:缺的是 UI,不是接口
排查时发现一件事值得说明:服务端接口(/api/netease/playlists、/playlist/:id/tracks)
早在 v0.32.0 就已经落地了。此前你看到的「未实现我的歌单」,
是因为前端没有任何界面去消费它 —— 接口一直都在,只是没人调用。
所以这次的工作是:补 UI + 修好数据映射。
实现
参考飞牛的 proxy/nmplaylists.py(只读虚拟歌单):
- 曲库页新增「我的网易云歌单」卡片墙
- 点开看曲目,可播放 / 加入队列 / 入库
- 只读:在 ToneCore 里加歌、移歌、删除都不回写网易(与飞牛一致)
- 未登录时区块自动隐藏,不显示空白
修了一个真 bug:歌手全空、封面全无
第一版 UI 出来后实测:歌单读到了 100 首,但每首都没有歌手、没有封面。
根因是字段名。网易 song/detail 返回的真实字段是:
| 我按这个取 | 实际是这个 |
|---|---|
artists |
ar |
album |
al(封面在 al.picUrl) |
duration |
dt(毫秒) |
修法:抽出 normalizeNeteaseSong() 统一映射,并兼容两种写法,
避免以后上游换字段再犯同样的错。
二、Subsonic 补 getMusicDirectory
端点数 35 → 36。
为什么补它:绝大多数 Subsonic 客户端(DSub / Substreamer / 音流等)
拉目录树的第一个调用就是 getMusicDirectory,而此前只实现了 getIndexes。
缺它时客户端往往直接报「无法读取目录」而连不上。
id 语义:1(根/歌手) → ar-(专辑) → al-(曲目) → 曲目详情。
关于「新增 API 连接方式」
现有连接层已是两套:
| 协议 | 端点数 | 用途 |
|---|---|---|
SongLoft /api/v1/* |
约 28 | 自有 Web / 兼容层 |
Subsonic /rest/* |
36 | 第三方播放器(DSub、音流等) |
本次补的是 Subsonic 的目录浏览缺口。如果你指的是另一套新协议
(比如 Navidrome 扩展、Jellyfin/Emby 音频、或 MPD),告诉我具体是哪个客户端,
我按它的协议补。
验证
npm run build ✅ web + server 均通过
npm test ✅ 15 项全过
package-lock ✅ 未漂移
需要你实测
你现在已登录网易云(昵称 DeltrivX),所以直接能看到效果:
- 曲库页 →「我的网易云歌单」→ 应有「DeltrivX喜欢的音乐」
- 点开看曲目:应有歌手名和封面(此前是全空)
- 点一首播放,确认能出声
待办
- 音源状态显示口径
- 锁屏动态歌词:浏览器能力边界,做不到
v0.33.1
ToneCore v0.33.1
修复封面补齐的匹配顺序 bug(先归一化把括号删了,导致「截掉版本后缀」失效),覆盖率 15% → 85~100%。
这是个什么 bug
v0.33.0 里我加了「按主标题匹配」来对齐酷我的版本后缀 —— 例如把
稻香 (完整版|DJ Ray版)、稻 香(堵桥版) 都归一到 稻香,好去匹配网易那边干净的 稻香。
但实现写成了:
const base = (s) => {
const t = norm(s); // ← 先归一化
const i = t.search(/[((]/); // ← 再找括号
...
};norm() 会把 ()() 当标点一并删掉。所以等它执行完,字符串里已经一个括号都没有了,
后面那句 search 永远找不到 —— base() 退回返回整个字符串。
结果:改进写进去了,但完全没生效。
修正
顺序反过来 —— 先截括号,再归一化:
const base = (s) => {
const raw = String(s || '');
const i = raw.search(/[((]/);
return norm(i > 0 ? raw.slice(0, i) : raw);
};实测覆盖率(同一批真实搜索数据)
| 关键词 | v0.33.0(实际未生效) | v0.33.1 |
|---|---|---|
| 稻香 | 3/20 (15%) | 17/20 (85%) |
| 孤勇者 | 3/20 (15%) | 20/20 (100%) |
| 起风了 | 5/20 (25%) | 18/20 (90%) |
| 晴天 | 7/20 (35%) | 17/20 (85%) |
(统计口径:一次搜索里,除网易外其它平台曲目中能补到封面的比例。)
教训
v0.33.0 是改完就发、没有验证覆盖率。而新旧逻辑跑出来的数字完全一样(都是 15%)
——这种「改了等于没改」,只有拿真实数据做前后对比才能发现。
这次补了验证:用 4 个关键词各 20 首真实数据跑新旧对照,确认提升后才发。
验证
npm run build ✅ server 通过
npm test ✅ 15 项全过
package-lock ✅ 未漂移
需要你实测
搜一首歌(比如「稻香」),看酷我那组结果是不是几乎都有封面了 ——
此前是清一色灰度占位方块。
待办
- 音源状态显示口径
- 锁屏动态歌词:浏览器能力边界,做不到
v0.32.0
ToneCore v0.32.0
网易云完整接入(eapi + 扫码登录):个人歌单、个性化每日推荐、登录后高音质取链。
为什么这版要换通道:weapi 在本项目里是死的
上一版做的是免登录接入。哥哥要求「参考飞牛音乐扩展实现完整接入,而不是免登录」,
于是我先去实现扫码登录 —— 但踩到一个硬事实。
weapi 三条路全部返回空(实测,不是猜的)
| 尝试 | 结果 |
|---|---|
/weapi/login/qrcode/unikey(裸请求) |
HTTP 200 · 体长 0 |
| 注入匿名设备 cookie 后再试 | HTTP 200 · 体长 0 |
/weapi/register/anonimous(拿 MUSIC_A) |
HTTP 200 · 体长 0 |
三次都是「200 但没有响应体」。如果我只看到状态码 200 就以为通了,会一路做下去然后全部落空。
eapi 则是全通的
换到 interface.music.163.com/eapi/*(飞牛的 NEMbox 取链/歌单实际也走 eapi):
| 端点 | 结果 |
|---|---|
| 取链 v1 | ✅ 拿到真实 URL(br 320000) |
| 歌单详情 | ✅ 热歌榜 200 首 |
login/qrcode/unikey |
✅ 拿到真实 unikey |
| 扫码轮询 | ✅ 正确返回 801「等待扫码」 |
决策:登录、歌单、取链统一走 eapi。
做了什么
1. 扫码登录
POST /api/netease/qr 取二维码(服务端用 qrcode 直接出 dataUrl,前端不必再引库),
前端每 3 秒轮询:
| code | 含义 | 处理 |
|---|---|---|
| 801 | 等待扫码 | 继续等 |
| 802 | 已扫码待确认 | 提示去手机确认 |
| 800 | 二维码过期 | 自动换新,不用再点一次 |
| 803 | 成功 | 吸收 cookie 并落盘 |
设置页新增「网易云账号」卡片。离开页面会停掉轮询(否则定时器泄漏、后台一直打接口)。
2. 个人歌单
/api/netease/playlists + /api/netease/playlist/:id/tracks,需登录。
3. 个性化每日推荐
/api/netease/recommend,返回 personalized 标志标明是否个性化。
(未登录时是全网通用版 —— 上一版就说明了这点,登录后才真正个性化。)
4. 登录后取链
走 eapi 的 song/enhance/player/url/v1,登录后有机会拿到更高音质。
两条必须说清楚的事实
① 未登录时要无损会被静默降级
请求 lossless,响应回来是 exhigh(320k) —— 不是报错,是静默降级。
好在 br/level 会如实反映,代码回读实际值判定音质,所以界面显示的是真实音质,不谎报。
② 凭据不外泄
cookie 含 MUSIC_U 等凭据,落盘在 data/netease-cookie.json。
所有接口只回「是否登录 / 昵称 / 歌单元数据」,绝不吐 cookie 原文,也不写进日志。
验证
npm run build ✅ web + server 均通过
npm test ✅ 15 项全过
新增依赖 qrcode@1.5.4(生产依赖,能扛住 Dockerfile 的 npm prune --omit=dev)+
@types/qrcode(开发依赖)。
需要你实测
- 设置 → 网易云账号 → 扫码登录(这是本次核心,得你拿手机扫)
- 登录成功后确认显示昵称
- 之后每日推荐应变为个性化内容
待办
- 酷我(kw)在线搜索封面:该平台不返回可用封面地址(实测 404)
- 音源状态显示口径
- 锁屏动态歌词:浏览器能力边界,做不到
v0.31.0
ToneCore v0.31.0
网易云音源自研直连(不依赖第三方脚本);入库改为按精确曲目 ID(避免入库到翻唱版);曲库页功能收纳。
一、网易云音源自研直连
为什么必须补
v0.30.0 把音源并入 RoMusic 与飞牛后,实测 23 份脚本里 5 份加载失败
(六音音源、聚合API接口、野花、野草、长青SVIP,均为联网初始化超时)。
而当时的自研直连只覆盖酷我(fetchKwUrl)。网易云是搜索质量最好的主力平台,
却只能靠第三方脚本 —— 脚本一失效,主力平台就没了。这是明显的能力缺口,
所以照酷我的范式补上 wy-url.ts。
实测结论(不是猜的)
| 接口 | 结果 |
|---|---|
/api/song/enhance/player/url?ids=[id]&br=<码率> |
✅ 免登录,直接返回真实播放地址 |
/api/song/enhance/player/url/v1?level=exhigh |
❌ 参数错误 code:400(需额外加密参数,故不用) |
一个必须说清楚的事实:无损会被静默降级
实测同一首歌(id 1973665667)各档位:
br=128000 → type=mp3 br=128000 ✅
br=192000 → type=mp3 br=192000 ✅
br=320000 → type=mp3 br=320000 ✅
br=999000 → type=mp3 br=320000 ⚠️ 实际只给到 320k!
也就是无 VIP 时要无损也会拿到 320k。好消息是响应里的 br 会如实反映,
所以代码回读实际 br/type 来判定音质 —— 界面显示的是真实音质,不会谎报「无损」,
落盘扩展名也与内容一致。
二、修复:入库可能落到翻唱版(重点)
这是哥哥说「仔细思考还缺少什么」时我发现的真问题,跟「歌词提前跑完」是同一个坑。
根因
/api/play 只吃 keyword + artist,会把推荐位和搜索结果自带的精确平台 ID 丢掉,
退化成「按歌名重新搜一遍」。
问题在于:打分再准,也抵不过再搜一次带来的不确定性。用户在推荐位看到的
明明是原唱,点「入库」后服务端重新搜索,很可能落到翻唱版 —— 用户完全无从察觉。
修法
新增 POST /api/fetch-by-id(platform + songId),直接按 ID 取链,
保住用户看到的那一首。
前端两处入库都改走它:
- 曲库页推荐位(
saveOne) - 在线搜索(
saveOne+saveBatch)
批量入库此前有同一个问题,一并修正了。批量现在是「入队」语义
(逐条入下载队列),文案也相应改为「已入队」,不再声称「已入库」。
三、曲库页功能收纳
改之前的状态很不一致:
| 位置 | 改前 |
|---|---|
| 曲库页推荐行 | 完全没有操作按钮,只能点整行播放 |
| 在线搜索行 | 3 个按钮平铺(播放 / 入队 / 入库) |
| 主页列表行 | 已用 RowActions 收纳(v0.28.0) |
现在统一按主页 RowActions 范式:
- 列表视图:播放留行内(高频操作),其余收进「更多」菜单
—— 播放 / 加入队列 / 下载入库 / 复制曲目信息 - 网格视图:hover 浮出,避免小卡片被按钮挤满
- 以后新增功能只需在
rowActions()里加一项,不用再挤布局
验证
npm run build ✅ web + server 均通过
npm test ✅ 15 项全过
package-lock ✅ 未漂移
需要你实测确认
- 曲库页:推荐行右侧应出现「更多」菜单,含 下载入库 / 复制曲目信息
- 入库正确性(本次重点):从推荐位挑一首入库,到主页比对是否是原唱
—— 这是这次修的核心问题,值得验一下 - 音质:非 VIP 账号入库无损时,实际会得到 320k(界面会如实显示,不是 bug)
待办
- 酷我(kw)在线搜索封面:该平台不返回可用封面地址(实测 404)
- 音源状态显示口径
- 锁屏动态歌词:浏览器能力边界,做不到
v0.30.0
ToneCore v0.30.0
音源并入 RoMusic 与飞牛音乐扩展(按内容去重);曲库在线推荐改为网易云每日推荐 + 全量榜单。
一、音乐源:并入 RoMusic 与飞牛音乐扩展
三处脚本本就是同一套格式
先做了真实比对,而不是假设:三处音源(ToneCore 本地 / Unraid RoMusic / 飞牛 fnmusic-ext)
都是洛雪(LX)音源脚本格式 —— 以「六音音源」为例,三处的头部注释与 globalThis.lx
通信方式完全一致。所以可以直接共用,不需要任何转换层。
去重:52 个文件其实只有 23 份内容
| 目录 | 文件数 |
|---|---|
sources(本地) |
9 |
sources-romusic(RoMusic) |
20 |
sources-fnos(飞牛) |
23 |
| 合计 | 52 |
| 按内容 sha1 去重后 | 23 |
去重按内容 sha1 而不是文件名:实测不存在「同名不同内容」的情况,但按内容去重
在两种情况下都正确,按文件名去重会误杀同名不同版本的脚本。
去重保留靠前目录的版本 —— 主目录的本地副本永远优先于外部副本。
加载:必须并发
脚本 init 需要联网拉配置,单份最坏等 12s。串行加载 23 份最坏 276 秒,
启动慢到像卡死。改为 6 路并发后最坏耗时约为 1/6。
不需要改容器模板
新增 extraSourcesDirs 配置(env EXTRA_SOURCES_DIRS,冒号分隔),
但未显式配置时会自动发现 /data/sources-romusic、/data/sources-fnos ——
目录存在即生效。所以这次并入外部音源没有动容器模板。
界面上现在能看出每个脚本来自哪套仓库(本地 / RoMusic / 飞牛)。
二、曲库推荐:改为网易云
参考飞牛音乐扩展(proxy/recommend.py)的接入方案,采用同样的两级降级:
| 级别 | 接口 | 说明 |
|---|---|---|
netease-daily |
/api/v3/discovery/recommend/songs |
每日推荐,优先 |
netease-charts |
/api/toplist + /api/playlist/detail |
榜单,免登录兜底 |
分级的原因:推荐位不该因为单个源挂掉就整块空掉。
榜单不再硬编码 4 个
原来写死 4 个榜单(热歌/新歌/原创/飙升)。实测 /api/toplist 返回 63 个榜单
(飙升、新歌、热歌、原创、古典、电音、说唱、UK、Billboard…),
改为动态取前若干个,取不到清单时才回退内置的那 4 个。
三、两条必须说清楚的实测事实
1. 每日推荐不是「猜你喜欢」
该接口未登录也能返回 32 首真数据,但我连着请求两次,曲目 ID 完全一致:
第1次前5个ID: [458725081, 297748, 1866165360, 2638372289, 483378334]
第2次前5个ID: [458725081, 297748, 1866165360, 2638372289, 483378334]
说明它是全网通用的每日推荐,不是个性化推荐。真正个性化必须带登录态(cookie),
ToneCore 不做账号体系,所以拿到的就是通用版。
它依然比固定榜单新鲜(每天轮换),所以作为首选 —— 但不要宣传成「猜你喜欢」。
界面上我只标了「每日更新」,没有过度承诺。
2. 飞牛的网易云服务调不到,无法复用
飞牛的 musicbox(proxy/app.py 里 FNMUSIC_MUSICBOX_URL,默认 http://127.0.0.1:8770)
含真正的登录态与个性化能力。但 compose 里映射是 127.0.0.1:8770:8002 —— 只监听本机。
从 Unraid(192.168.31.2)实测:
curl http://192.168.31.5:8770/api/v1/toplist → HTTP 000(不可达)
所以本项目不能接飞牛的服务,只能照它的方案自己接网易云公开接口。
(改监听范围要动 FnOS 上的 compose,属你的系统配置,我没碰。)
验证
npm run build ✅ web + server 均通过
npm test ✅ 15 项全过
需要你实测确认
- 曲库页在线推荐:应出现「每日推荐」板块(带「每日更新」标),下面是若干榜单
- 设置页音源列表:脚本数应从 9 增至最多 23,且能看出来源
待办
- 酷我(kw)在线搜索封面:该平台不返回可用封面地址(实测 404)
- 音源状态显示口径
- 锁屏动态歌词:浏览器能力边界,做不到
v0.29.0
ToneCore v0.29.0
修复歌词「提前跑完」:在线歌词改为按音频真实时长挑版本;顺带补齐曲库时长。
修复:歌词「提前跑完」
这个问题从 v0.26.0 就悬着,当时只测出了现象、没敢改。这次把根因链补齐了。
根因
- 在线歌词按歌名去搜,候选里混着翻唱 / 剪辑版;
- 旧逻辑是「遍历候选,取第一个有歌词的」—— 极易命中翻唱版;
- 翻唱版的时间轴与本地音频天然对不上。
实测数据(末行时间 ÷ 真实时长):
| 来源 | 曲目 | 比值 | 判定 |
|---|---|---|---|
| online | 黯然销魂 | 0.83 | 提前跑完 |
| online | 思念漫过海岸线 | 0.83 | 提前跑完 |
| lrc-file / embedded | 天堂岛之歌、要不然我们就这样一万年 | 0.91~0.94 | 正常 |
| online | 悬溺、三生三世 | 0.97~1.00 | 正常 |
跑偏的全部来自 online,这也解释了「只有个别歌正确」——
那些歌恰好命中了原版。
修法
fetchLyrics() 新增 targetDuration 参数:
- 给了时长 → 收集全部候选的歌词,按「末行时间与音频长度的偏差」排序,取最优版本,
而不是第一个碰巧有歌词的; - 偏差超过 25%(
ratio < 0.75或> 1.35)→ 判定这批候选里没有匹配版本,宁可不用,也不把明显错位的歌词放上去; - 不传时长 → 退化为旧行为,兼容其它调用方。
LyricsService.probeDuration():用 music-metadata 读本地音频真实时长,
在走在线兜底前先取到,传给 fetchLyrics。
为什么不对本地来源做时长校验
同名 .lrc 与音频内嵌标签本就来自本地文件,时间轴应当可信。
给它们加校验只会让本可直接用的歌词被误弃 —— 所以校验只作用于在线兜底那一层。
变更:补齐曲库时长
上面那个修复需要一个前提:得知道音频的真实时长。而实测发现曲库 duration 全是 NULL。
原因是扫描入库时只顾着快速建索引,duration 直接写了 NULL。后果有两个:
- 界面时长一直显示
--:--; - 歌词校验失去了参照(所以必须先修这个)。
新增 Library.backfillDurations(),在扫描后回填真实时长。要点:
- 只处理
duration IS NULL的行; - 解析失败保持 NULL,下轮扫描再试 —— 不写 0,否则 0 会被当成有效值而永远不再重试。
验证
npm run build ✅ web + server 均通过
npm test ✅ 15 项全过
需要你实测确认
这个修复依赖真实网络候选,本地没法自动断言,请在界面上验一首以前跑偏的歌(比如「黯然销魂」):
- 播放该曲 → 歌词应当跟着当前句走
- 若仍然错位,先看「歌词校准」里的偏移值是不是合理
如果有个别歌反而从正确变成没有歌词,说明这批候选里确实没有匹配版本(此时会返回空),
属于预期内的保守行为 —— 把歌名告诉我,我单独查。
待办
- 酷我(kw)在线搜索封面:该平台不返回封面地址
- 音源状态显示口径
- 锁屏不显示动态歌词:Media Session API 不提供歌词接口,属浏览器能力边界,非 bug
v0.28.0
ToneCore v0.28.0
功能性按钮收纳进「更多」菜单(方便以后拓展);删除改为彻底删除(不走回收站)。
变更
1. 功能按钮收纳进「更多」菜单
新增可复用组件 RowActions(apps/web/src/components/RowActions.vue)。
为什么收纳:此前每个功能各占一个按钮(详情 / 删除 / 刮削…),
列表行已经被挤满,再加功能就没地方放了。
收纳后的好处:以后新增功能只需在 rowActions() 里加一项,
不用再改每一处的布局 —— 这正是你说的"方便以后功能性拓展"。
菜单当前包含:查看详情 / 加入队列 / 重新刮削 / 彻底删除。
细节处理:
- 点击外部、按 Esc、页面滚动时都会关闭 —— 滚动不关的话,
弹层会脱离原行浮在别处,挡住后面的行 - 危险项(彻底删除)用红色标注,与常规项区分
- 「加入队列」是高频操作,仍留在行内,不收进菜单
组件用法:
<RowActions :items="[{ label: '详情', icon: 'info', run: () => {} }]" />2. 删除改为彻底删除
按你的要求:不走回收站,直接删。
服务端:Library.remove(relPath, hard) 新增 hard 参数
| hard | 行为 |
|---|---|
true |
直接 unlinkSync 从磁盘删除(不可恢复) |
false |
移入 <dataDir>/_trash/(保留,默认) |
两种模式共用同一套前置校验(目录穿越 / 文件不存在 / 索引清理),
只有最终「怎么处理文件」这一步不同,避免两处逻辑各自漂移。
接口:POST /api/library/delete 接受 hard;前端 libraryDelete(path, hard)。
关于二次确认 —— 我保留了,请看完这段再决定要不要去掉:
彻底删除是不可逆的真删盘操作。一次误点就永久丢失,代价远大于多一次确认。
所以我保留了确认弹窗,文案也写清了后果:
彻底删除「歌名」?
将直接从磁盘删除音频文件(及同名 .lrc),不可恢复。
确定要继续吗?
如果你确实想连确认也不要,告诉我,我去掉。
同步修正文案:详情弹窗按钮由「移入回收站」改为「彻底删除」——
行为变了而文案没变,会让用户以为还能找回。
验证
npm run build ✅ web + server 均通过
npm test ✅ 15 项全过
硬删的手工验证方式(未加自动测试)
现有 5 个测试均为纯函数单测(tags / player / queue / cover),
不碰数据库与真实音乐目录;而 remove() 依赖 loadConfig() 与真实 SQLite,
写自动测试会动到你的真实曲库 —— 风险大于收益,所以没加。
需要验证时按这个步骤(用一首无关紧要的测试曲目):
- 记下测试曲目的路径与磁盘上的文件
- 主页 → 该曲右侧「更多」→ 彻底删除 → 确认
- 确认:列表里该曲消失、统计数 -1、磁盘上音频文件确实没有了
/data/_trash/下不应出现该文件(这是与旧软删的关键区别)
未改动(仍是待办)
- 歌词「提前跑完」:v0.26.0 实测过,跑偏的集中在
online来源
(在线按歌名匹配,易命中翻唱版,时间轴天然对不上本地文件)。
修法是按真实时长校验候选、命中率低就回退本地/内嵌来源,需更多样本验证。 - 锁屏不显示动态歌词:Media Session API 不提供歌词接口,属浏览器能力边界,非 bug。
- 酷我(kw)在线搜索封面;音源状态显示口径。
v0.27.0
ToneCore v0.27.0
曲库支持列表 / 小图 / 大图三种显示方式;图标重新设计;主页本地音乐管理补齐(补回被误删的功能)。
变更
1. 曲库支持三种显示方式
列表 / 小图 / 大图,覆盖曲库页两处内容:
- 云端搜索结果(
OnlineSearch) - 在线推荐榜单(
Library)
三处——主页本地列表、推荐榜单、搜索结果——共用同一个偏好 key tc.home.view。
因为它们本质是同一类内容(封面墙 or 列表),在一处调过显示方式,其余不必再调一遍。
网格视图下的操作按钮(加入队列 / 入库)默认隐藏、hover 浮出,避免小卡片被按钮挤满。
2. 主页本地音乐管理补齐
这里要先说一个我自己的失误:v0.26.0 重写曲库页时,我把单曲详情 /
删除 / 重新刮削整个删掉了,却忘了迁到主页 —— 结果本地歌曲只能播、不能管。
这是功能倒退,本版补回并扩展:
- 列表行悬停新增「详情」「移入回收站」入口
- 网格卡片新增常驻「详情」按钮
- 详情弹窗:封面 / 标题 / 歌手 / 专辑 / 时长 / 格式 / 文件路径,并提供
播放、加入队列、重新刮削、移入回收站
移动端适配(顺带修的隐性坑):此前操作按钮是 opacity-0 + group-hover 才显形,
但触屏设备没有 hover —— 等于这些功能在手机上根本不存在。
现改为小屏常驻、大屏才收进 hover:
opacity-60 md:opacity-0 md:group-hover:opacity-100
3. 图标重新设计
v0.19.0 之后再没改过,在 Unraid 面板和浏览器标签页的小尺寸下辨识度偏低。
- 底:深色径向渐变
#2E3846 → #0E1218,深/浅两种主题下都不发灰 - 声波:由两道弧改为三层同心弧环(青蓝 → 紫渐变),左右严格镜像
(各以 0° / 180° 为中心 ±35°),语义是「声音由内向外扩散」,小尺寸下层次依然清晰 - 中心保留发光音符(青蓝竖向渐变),仍是第一眼识别点
assets/icon.svg与assets/icon.png已同步
排查记录(避免以后重复踩)
「Unraid 图标是旧版」其实是个误判
我一开始以为是部署没同步或缓存旧版,于是做了 crypto-brute 式的排查。实测结论相反:
| 位置 | md5 |
|---|---|
仓库 HEAD:assets/icon.png blob |
4ba287a6… |
/mnt/user/icons/ToneCore.png |
4ba287a6… |
/var/local/emhttp/…/images/ToneCore-icon.png |
4ba287a6… |
容器内 /app/public/icon.png |
4ba287a6… |
四份完全一致,且与仓库当前文件是同一个 blob ——
根本不是缓存问题,而是这个图标自 v0.19.0 起就没再改过。
所以真正该做的是重做设计,清缓存、重新 tag 镜像都是白费功夫。
SVG 弧线端点必须落在圆上
手写 SVG arc 的端点若不落在对应半径的圆上,渲染器会自动缩放弧半径,
结果与 PNG 对不上。我第一版手写端点偏差达 12–26px,弧会明显变形。
现已按 r 与角度重算校验(左弧 145°→215°、右弧 -35°→35°,sweep=1)。
验证
npm run build ✅ web + server 均通过
npm test ✅ 15 项全过
悬空引用检查 ✅ 无 recent / home.albums / home.artists / filterBy 残留
package-lock ✅ 未漂移
未改动(仍是待办)
- 歌词「提前跑完」:v0.26.0 实测过,跑偏的集中在
online来源
(在线按歌名匹配,易命中翻唱版,时间轴天然对不上本地文件)。
修法是按真实时长校验候选、命中率低就回退本地/内嵌来源,需更多样本验证。 - 锁屏不显示动态歌词:Media Session API 不提供歌词接口,属浏览器能力边界,
非 bug —— 专辑图 / 歌名 / 歌手能正常显示即说明该层已通。 - 酷我(kw)在线搜索封面;音源状态显示口径。
v0.26.0
ToneCore v0.26.0
主页专管本地歌曲、曲库专管推荐与下载;取消主页专辑/歌手分类。附:歌词来源质量实测。
变更:两个页面职责分开
按用户要求重新划分:
| 页面 | 职责 |
|---|---|
| 主页 | 显示并处理本地歌曲(列表 + 检索 + 曲库管理) |
| 曲库 | 在线推荐 + 云端搜歌入库 |
此前两页功能交叉:曲库页有一份本地列表和一堆本地管理工具,
主页又摆「最近入库 / 专辑 / 歌手」,同一个东西从几个角度重复摆,
反而不如直接看到歌本身。
主页
- 取消顶部「专辑」「歌手」分类:它们只是把本地歌曲换个角度再摆一遍,
挡住了本地歌曲本体。 - 本地曲库管理迁到列表后方:扫描、元数据审计、补全标签、
清理失效曲目、统计卡 —— 从曲库页整体搬过来,放在本地列表之后。
逻辑上也更顺:先看到歌,再决定要不要维护。
曲库
- 移除本地列表与全部本地管理工具(377 行 → 105 行)。
- 只保留:云端搜索 + 在线推荐。
关于歌词「提前跑完」(实测,本版未改代码)
用户反馈:除个别歌外,其他都提前跑完歌词。
用「歌词末行时间 ÷ 真实时长」实测 8 首(时长用容器内 ffprobe 取真实值):
| 来源 | 曲目 | 末行/时长 |
|---|---|---|
| lrc-file | 天堂岛之歌 | 0.92 |
| lrc-file | 落 | 0.94 |
| embedded | 要不然我们就这样一万年 | 0.91 |
| online | 悬溺 | 1.00 |
| online | 秋天不回来 | 0.89 |
| online | 黯然销魂 | 0.83 |
| online | 思念漫过海岸线 | 0.83 |
| online | 三生三世 | 0.97 |
提前跑完的集中在 online 来源 —— 在线歌词按歌名去搜,
容易命中翻唱或别的版本,时间轴天然对不上本地文件。
这也解释了「只有个别歌正确」:那些歌恰好命中了原版。
本版不做代码改动,因为修法(按真实时长校验候选、命中率低就回退本地来源)
需要更多样本验证,贸然改可能让原本正确的歌也错。列为待办。
关于锁屏不显示动态歌词
浏览器的 Media Session API 只提供 metadata(标题/歌手/专辑图)、
playbackState、positionState,没有任何歌词接口。
锁屏滚动歌词是各家音乐 App 用私有 SDK 实现的,网页拿不到。
所以这是能力边界,不是 bug。专辑图/歌名/歌手能正常显示,
说明 Media Session 这一层已经通了。
验证
npm run build ✅ web + server 均通过
npm test ✅ 15 项全过
页面结构检查:主页与曲库均无悬空引用(filterBy / boards /
home.albums / home.artists 等死引用已清理干净)。
待办
- 歌词:在线来源按真实时长校验候选,过低则回退本地/内嵌来源
- 酷我(kw)在线搜索封面:该平台不返回封面地址
- 音源状态显示口径
v0.25.0
ToneCore v0.25.0
修锁屏媒体控件(作用域错误致 metadata 从未上报);修移动端歌词显示区域;推荐移入曲库;取消主页最近入库。
修复一:锁屏媒体控件从未生效(真因)
现象
加了 Media Session 之后,锁屏/通知栏依然只显示软件名「ToneCore 音乐中枢」,
没有专辑图、没有歌名歌手、也没有播放控制。
上一版误判
v0.23.0 我判断是 artwork 的 MIME 类型问题(一律写 image/png,而曲库多是 jpg)。
那个问题真实存在且已修,但不是主因 —— 修完锁屏依然没变。
真因(本次定位)
media 被声明在 usePlayer() 内部:
export function usePlayer() {
...
const media = useMediaSession({...}); // ← 函数作用域
}而 applyState() 是模块级函数,它末尾调用:
function applyState(r) { // ← 模块级
...
media.update(); // ← 引用不到内部的 const
}JS 里这会抛 ReferenceError: media is not defined。
而 applyState() 是每一次播放状态更新的必经之路 ——
于是 metadata 一次都没有成功上报,锁屏只能拿 <title> 兜底。
修法
把句柄提到模块级,调用处用可选链兜住未初始化:
let media = null; // 模块级
function applyState(r) { ... media?.update(); } // 安全
export function usePlayer() { ... media = useMediaSession({...}); }教训:跨作用域调用必须在真实运行时验证,不能只看编译通过。
构建成功不代表运行时引用合法。
修复二:移动端歌词显示的是后面的歌词
用户点破了方向
我一直按「歌词不同步」在查(调 offset、加一键对齐),
用户指出:不是同步问题,是显示区域不足。确认后确实如此,且有两个叠加成因。
成因 1:滚动定位算错了基准
const target = el.offsetTop - box.clientHeight / 2 + el.clientHeight / 2;offsetTop 相对最近的 positioned 祖先计算,
而 lyricBox 自身没有 position: relative ——
于是它相对更外层祖先算,把上方大碟的高度也算了进去。
移动端大碟在正上方(约 300px ≈ 9 行歌词),所以滚动严重过头;
桌面端是左右布局,横向不叠加,误差只有几十 px ——
这解释了为什么「只有移动端错」。
改用 rect 差值,只关心「目标行中心」与「可视区中心」的差,与定位上下文无关:
const boxRect = box.getBoundingClientRect();
const elRect = el.getBoundingClientRect();
const delta = (elRect.top + elRect.height / 2) - (boxRect.top + boxRect.height / 2);
box.scrollTo({ top: Math.max(0, box.scrollTop + delta), behavior: 'smooth' });成因 2:移动端歌词区本身被挤没了
竖向预算(iPhone 14,视口 844px):
| 区块 | 高度 |
|---|---|
| 顶栏 | 58px |
| 底部控制区 | ≈150px |
大碟(min(42vh,300px),shrink-0) |
300px |
| 曲目信息 | ≈70px |
| 歌词区剩余 | ≈266px(≈6.7 行) |
大碟带 shrink-0,flex 压缩不了它,只能压歌词区。
把移动端大碟缩到 min(30vh,200px) 后,歌词区 ≈366px(≈9.2 行)。
桌面端保持 min(46vh,340px) 不变。
变更:在线推荐移入曲库页
按用户要求,从主页搬到曲库页(曲目列表之上)。
同时取消首页「最近入库」 —— 它与本地曲库列表完全重复,
首页一份、曲库页一份,等于把同样的歌摆两遍。
清理了主页脚本里的 recent / playHomeSong / 推荐相关死引用。
验证
npm run build ✅ web + server 均通过
npm test ✅ 15 项全过
待办
- 锁屏需真机验证:Media Session 需先播起来才激活,且要求安全上下文
(HTTPS 或 localhost)。当前访问若是http://192.168.31.x:8090,
浏览器会直接禁用 Media Session —— 需确认访问方式。 - 酷我(kw)在线搜索封面:该平台不返回封面地址
- 音源状态显示口径