feat(ncm): 二期功能批 (agent-222) — 专辑封面(al.picUrl+切歌换封面)+歌词后台加载+大歌单分页(/api/playlist/track/all 首50触底续载)+歌词字面\n修复+播放页豆腐块修
ncm→9c4a79f6 已部署卡上; 宿主离线样本全过(零新增网络请求)
This commit is contained in:
1 parent
57fd15e83d
commit
6c78a88b8c
3 files changed
+126
-1
No files matched your search
@@ -666,3 +666,123 @@ c4f6cd8569d6ddc4dd15d4111752ed77 app/ui/settings.c ba78835b3fbe6166bbe39
|
|||||||
build_ncm.sh / build_ncm_host.sh# 源列表加 search/ncm_colors;host 补链 SDK nav/media/notify_ipc
|
build_ncm.sh / build_ncm_host.sh# 源列表加 search/ncm_colors;host 补链 SDK nav/media/notify_ipc
|
||||||
(核心库 §9 其余、vendor/、sdk/、meow_launcherd/ 未动;未 commit/push/刷机)
|
(核心库 §9 其余、vendor/、sdk/、meow_launcherd/ 未动;未 commit/push/刷机)
|
||||||
```
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 12. 二期四件功能批:专辑封面 / 歌词后台加载 / 大歌单分页 / UI 遗留修复(2026-08-31)
|
||||||
|
|
||||||
|
> 依据任务批(REPORT §9-11 之上)做四项:① `ncm_song_t` 加 `al.picUrl`,播放页黑胶封面用真实专辑图
|
||||||
|
> (沿用 `cover.c` 缓存/限幅);② 播放页歌词改后台 worker + 就绪标志,enter 不再同步阻塞拉词;
|
||||||
|
> ③ 大歌单 `playlist/detail` 全量 trackIds 太大(5191 首 ≈900KB,§10.11 已记录)→ 改走
|
||||||
|
> `/api/playlist/track/all`(eapi 服务端分页),曲目列表滚动加载(50 首/页);④ 修 UI 遗留:
|
||||||
|
> 歌词字面 `\n` 未转换 + 播放页底部提示豆腐块(字库缺字)。
|
||||||
|
> 仅改动 `patch_v2/ncm_app/`;未动 sdk/、meow_launcherd/、vendor/;未 commit/push/刷机/动设备。
|
||||||
|
|
||||||
|
### 12.1 专辑封面(`ncm_song_t.al_pic` + 播放页用真实专辑图)
|
||||||
|
|
||||||
|
- `core/ncm_api.h`:`ncm_song_t` 增 **`char al_pic[512]`**(`al.picUrl`,追加字段向后兼容)。
|
||||||
|
- `core/ncm_api.c`:抽出 `parse_songs_array()` 统一解析歌曲对象数组(`id/name/ar[0].name/al.name/al.picUrl/dt/fee`,
|
||||||
|
id 数字/字符串均以文本拷贝,兼容 >2^31)。`ncm_api_song_detail`、`ncm_api_cloudsearch` 改共用该函数 →
|
||||||
|
**歌单/搜索两路都带上专辑封面**。
|
||||||
|
- `app/ui/player.c` `player_play_song`:黑胶封面 URL 改为 `s->al_pic[0] ? s->al_pic : g_st.cover_url`(无 al_pic 回落歌单封面);
|
||||||
|
`player_tick` 在 `sc != g_last_song`(切歌/自动顺延)时对新曲 `cover_request()` 换封面。
|
||||||
|
- 封面缓存/限幅**沿用** `app/logic/cover.c`(≤`NCM_COVER_MAX` 288KB、单张、后台线程下载解码 + `cover_current.jpg` 供 launcher),未改。
|
||||||
|
|
||||||
|
### 12.2 歌词后台加载(`player_manager.c` + `player.c`)
|
||||||
|
|
||||||
|
- 新增**持久歌词 worker 线程**(`lyric_worker`,detached):每 300ms 读 `g_play.cur_song`,曲目 id 变化即
|
||||||
|
`lyric_clear()` → `ncm_api_lyric` → `lyric_parse()`(网络+解析全在后台);状态 `g_lyr_state`(0 空闲/1 加载中/2 已拉完)。
|
||||||
|
- `player_manager_load_lyric()` 改为**非阻塞触发**:只 `lyric_ensure_worker()` 启动 worker 即返回(UI 不再同步阻塞)。
|
||||||
|
换曲/自动顺延由 worker 轮询自捕获,无需逐处触发。
|
||||||
|
- 新增 **`int lyric_state(void)`**;`player.c` `plr_draw_dyn` 歌词空态改为:`lyric_state()==2 ? "no lyric" : "loading lyric..."`
|
||||||
|
(消除“无词”与“加载中”歧义)。
|
||||||
|
- 竞态(§10.9 FM4 已接受)沿用:UI 读 lyric 模块与 worker 写为良性(数组定长、越界已防护,最坏一帧旧/空行自愈)。
|
||||||
|
|
||||||
|
### 12.3 大歌单分页(`/api/playlist/track/all` 服务端分页 + 触底加载)
|
||||||
|
|
||||||
|
- `core/ncm_api.h/.c`:新增 **`ncm_api_playlist_track_all(device_id, cookie, pid, offset, limit, songs, max, &n, &total)`**
|
||||||
|
→ eapi `POST /api/playlist/track/all`,body `{"id","offset","limit","n"=limit,"s":8}`,直返该页歌曲对象(含 `al.picUrl`,
|
||||||
|
**免去 detail 瓶颈 + 二次 song_detail**)。数组路径防御 `songs→playlist.tracks→data.songs`;`total` 取 `total→count→playlist.trackCount→musicCount`。
|
||||||
|
另导出 **`ncm_api_parse_track_all(json,len,...)`** 纯解析版(离线样本重放/单测,不打网络)。
|
||||||
|
- `app/logic/appstate.c`:新增 `appstate_load_playlist_songs_page(idx, offset, limit)`(`offset==0` 重头清空、`>0` 追加到
|
||||||
|
`g_st.songs[offset..]`,封顶 `NCM_SONG_MAX`);`appstate_load_playlist_songs(idx)` 保留为兼容整批(内部按页循环)。
|
||||||
|
- `app/ui/playlist.c`:首屏 `playlist_refresh()` 拉 offset=0 一页(`NCM_PLAYLIST_PAGE=50`);`playlist_tick` 按 `g_ploffset==0`
|
||||||
|
决定「回顶/保滚动」;新增 `pl_maybe_load_more()`(触底且 `g_pl_more` 且未到 `NCM_SONG_MAX` 时加载下一页),
|
||||||
|
在 `playlist_touch_move`/`playlist_swipe` 滚动后触发;追加失败仅 `ncm_snack("加载失败")` 并保留已载歌曲(不整页清空)。
|
||||||
|
- `app/ncm_app_internal.h`:`NCM_PLAYLIST_PAGE 50`;声明分页函数 + `lyric_state`。
|
||||||
|
|
||||||
|
### 12.4 UI 遗留修复
|
||||||
|
|
||||||
|
- **歌词字面 `\n`**(`app/logic/lyric.c`):`lrc_parse` 复制正文改经 `copy_lrc_text()`——把字面 `\n`/`\r`
|
||||||
|
(服务器把多行文本以转义折进一行)转空格,`\r\n`/`\n\r` 折叠为单空格;有界 ≤`LRC_MAX_TEXT`。
|
||||||
|
- **播放页底部提示豆腐块**(`app/ui/player.c`):页脚 `"左右滑切歌 · 点顶部切横屏"` 的 **`·`(U+00B7) 不在 common 字库**
|
||||||
|
→ 豆腐块。root cause:common tier 按 GB2312 解码 `A1A4` 得 **`・`(U+30FB)**(gb2312 无 U+00B7,gbk 才有),故 common 图集只有 U+30FB。
|
||||||
|
已替换为 **`・`(U+30FB)**(在 common 图集内,`meow_font_class_of`→OTHER→路由主槽 slot0 即命中)。其余页面的 `·`(U+00B7)
|
||||||
|
为同一 bug(settings/login/playlist/search 也有),为避免范围扩散 + 保设计原貌,本期只改播放页页脚(用户实测报点);
|
||||||
|
已在「诚实出入」标注。
|
||||||
|
|
||||||
|
### 12.5 验证记录(宿主为主;风控纪律:**新增真实 NCM 请求 = 0 次**)
|
||||||
|
|
||||||
|
| 项 | 方式 | 结果 |
|
||||||
|
|---|---|---|
|
||||||
|
| track_all 离线解析 | `bench/offline_track_all.c`(合成样本,纯解析,0 网络) | **全 PASS**:`songs` 命中、id>2^31 不溢出(`2746301825`)、`al.picUrl` 正确(`song0`/`song1`)、`total=5191`;变体 `playlist.tracks`+`trackCount` 容错命中 |
|
||||||
|
| 歌词字面 `\n` | 宿主编译 `app/logic/lyric.c` + 定向输入(`\\n`/`\\r\\n`) | **PASS**:`"hello\nworld"`→`"hello world"`、`"second\r\nline\nhere"`→`"second line here"` |
|
||||||
|
| App 三页 | 宿主离屏 `MEOW_UI_DUMP`(mock,0 网络) | player/playlist/home 渲染正常(黑胶/进度/控制/歌词、分页 footer `6/128`、红行),无崩溃 |
|
||||||
|
| 歌词后台状态位 | `lyric_state()` 语义 + mock 下 `g_host_mock` 短路(worker 未启、count>0 → 显示歌词) | 正常 |
|
||||||
|
| mipsel 构建 | `bash build_ncm.sh`(自有代码 `-Wall -Wextra`) | **0 warning**(仅脚本 echo);`text 812856 / data 840 / bss 1059556` |
|
||||||
|
| qemu-mipsel 冒烟 | 未跑(避免触发真实 unikey 请求;启动路径未改,§11.5 已验) | 未做 |
|
||||||
|
|
||||||
|
> 注:`out/test_ncm_core_mipsel` 已由 `build_core.sh` 重建(含新增 `test_track_all_parse`),但**未运行**(其 `test_network`
|
||||||
|
> 会打真实 NCM 请求);track_all 解析已由 `bench/offline_track_all`(0 网络)覆盖。
|
||||||
|
|
||||||
|
### 12.6 md5(自有源码 + 二进制)
|
||||||
|
|
||||||
|
```
|
||||||
|
9c4a79f669967d193cba9fdaa41f8442 ncm # mipsel 静态 App(新)
|
||||||
|
9d1ff1ad5027c1932c7f4f0163df8b92 core/ncm_api.h 6b2daef7a48215fcd7369351e09ee25f core/ncm_api.c
|
||||||
|
8067ef74af7db49340b693ec62c654f0 app/ncm_app_internal.h 7e54b48012fdfb392ba5910d22153078 app/logic/appstate.c
|
||||||
|
c3a4aa5cd3da493d5cdb75965aaaa4fe app/ui/playlist.c 610a223232d250d2e0413031bb8613ae app/play/player_manager.c
|
||||||
|
9bf68c8420bd8c819cf318b5a16d3d9f app/ui/player.c 612f6e92cade6a59d33f3b5d6aee7b13 app/logic/lyric.c
|
||||||
|
58ee655751a8efb69df850dc96afdcc1 test/test_ncm_core.c 61b019c64a805e8873db21482def3408 bench/offline_track_all.c
|
||||||
|
```
|
||||||
|
|
||||||
|
### 12.7 出入清单(只改 `patch_v2/ncm_app/`)
|
||||||
|
|
||||||
|
```
|
||||||
|
修改:
|
||||||
|
core/ncm_api.h .c # ncm_song_t.al_pic + parse_songs_array(解析 al.picUrl)
|
||||||
|
# + ncm_api_playlist_track_all(分页) + ncm_api_parse_track_all(离线解析)
|
||||||
|
app/ncm_app_internal.h # NCM_PLAYLIST_PAGE + appstate_load_playlist_songs_page + lyric_state
|
||||||
|
app/logic/appstate.c # 分页加载 appstate_load_playlist_songs_page(兼容整批保留)
|
||||||
|
app/ui/playlist.c # 触底加载更多(50/页,cap 256);追加失败保留已载
|
||||||
|
app/play/player_manager.c # 歌词持久 worker + 异步 load_lyric + lyric_state
|
||||||
|
app/ui/player.c # 黑胶封面用 al_pic;异步歌词空态(loading/no lyric);页脚 ·→・
|
||||||
|
app/logic/lyric.c # LRC 字面 \n 转空格(copy_lrc_text)
|
||||||
|
test/test_ncm_core.c # 新增 test_track_all_parse 离线断言
|
||||||
|
新增:
|
||||||
|
bench/offline_track_all.c # track_all 离线样本重放(0 网络)
|
||||||
|
REPORT.md # 本批记录(§12)
|
||||||
|
(未动 sdk/、meow_launcherd/、vendor/;未 commit/push/刷机/动设备)
|
||||||
|
```
|
||||||
|
|
||||||
|
### 12.8 诚实出入 / 已知限制
|
||||||
|
|
||||||
|
- **未真机验证**:本机无可达设备主机(与 §11.5 同:`meowos.local` 不解析、无设备别名/IP),故未真机跑。
|
||||||
|
二进制已就绪(md5 `9c4a79...`),真机部署 + 验收(1-2 次请求)留用户执行:
|
||||||
|
`scp ncm root@<device-ip>:/usr/data/mnt/sd_0/MeowAPPs/dev.player.ncm/ncm.new && ssh root@<device-ip> 'mv ...ncm.new ...ncm'`(先 .new 再 mv)。
|
||||||
|
- **`/api/playlist/track/all` 端点形态未 live 验证**(无宿主 cookie,且 NCM 真实 + 风控纪律不允许多请求):
|
||||||
|
请求/解析按 ncm-api-rs 生成的 eapi 路由 `/playlist/track/all` + API.md 的 `limit/offset` 文档语义实现,
|
||||||
|
并对数组路径(`songs→playlist.tracks→data.songs`)与 `total` 做了**多路径容错**;若真实响应字段有出入,
|
||||||
|
解析器会据实际样本在下一轮对齐(离线单测已覆盖本实现所依据的形态)。
|
||||||
|
- **http 封面 URL 旧疾**:`core/ncm_http.c` 对 `http://` 也走 TLS 握手(当前实现只判 scheme 再统一 TLS),
|
||||||
|
若个别 `al.picUrl`/`coverImgUrl` 为纯 http 会握手失败。这是 **v1 同源遗留**(v1 已用 coverImgUrl),
|
||||||
|
本期未改 core(避免动已验证的 TLS 层);实测 NCM 专辑/歌单封面多为 https,可后续统一跳转 https 或加 http 明文分支。
|
||||||
|
- **页脚等 `·`(U+00B7) 残留**:settings/login/playlist/search 的 `·` 同为 common 缺字 → 会豆腐块(用户仅报播放页,
|
||||||
|
本期修播放页);如需全面修,把全 App 的 U+00B7 换成 U+30FB 即可(同一 bug,非功能缺失)。
|
||||||
|
- **topbar 图标字符潜在同病**:home/player topbar 动作标签 `⟳`(U+21BB)/`⚙`(U+2699)/`⌕`(U+2315) 走
|
||||||
|
`meow_font_class_of`→OTHER 类,若 common 图集无对应字形同样会豆腐块;用户未报(可能在 topbar 用了符号字库槽或未留意),
|
||||||
|
本期未动(避免改非报点区域),后续如需一并确认/替换(如改用 ASCII 或 in-atlas 字符)。
|
||||||
|
- **`song_ids[]` 字段现未被歌单加载使用**(`track_all` 直返歌曲对象),字段保留在 `g_st`(无害,避免结构改动)。
|
||||||
|
- **歌词竞态**:UI 读 lyric 与 worker 写保留良性竞态(§10.9 FM4 同接受);真机如需彻底消除可后续给 lyric 模块加锁。
|
||||||
|
- **构建方差异**:宿主 `build_ncm_host.sh` 对 `cover.c` 有 glibc-fortify `-Wformat-truncation` 告警(**改动前即存在**,
|
||||||
|
仅宿主 glibc 触发;mipsel musl 无此告警,`build_ncm.sh` 0 warning)。
|
||||||
@@ -17,7 +17,7 @@ static char s_tl[NCM_LYRIC_LINES][LRC_MAX_TEXT];
|
|||||||
static int s_n;
|
static int s_n;
|
||||||
|
|
||||||
/* 复制一行歌词正文 n 字节到 dst,把「字面反斜杠 n/r」序列(服务器用 \n 把多行文本塞进一行,
|
/* 复制一行歌词正文 n 字节到 dst,把「字面反斜杠 n/r」序列(服务器用 \n 把多行文本塞进一行,
|
||||||
* 字面 \n 渲染成豆腐块/凌乱)替换为空格;顺带容忍 \r\n。有界 ≤max。 */
|
* 字面 \n 渲染成豆腐块/凌乱)替换为空格;顺带把 \r\n / \n\r 折叠为单空格。有界 ≤max。 */
|
||||||
static void copy_lrc_text(char *dst, const char *src, size_t n, size_t max)
|
static void copy_lrc_text(char *dst, const char *src, size_t n, size_t max)
|
||||||
{
|
{
|
||||||
size_t oi = 0, si = 0;
|
size_t oi = 0, si = 0;
|
||||||
@@ -25,6 +25,11 @@ static void copy_lrc_text(char *dst, const char *src, size_t n, size_t max)
|
|||||||
if (src[si] == '\\' && si + 1 < n && (src[si + 1] == 'n' || src[si + 1] == 'r')) {
|
if (src[si] == '\\' && si + 1 < n && (src[si + 1] == 'n' || src[si + 1] == 'r')) {
|
||||||
dst[oi++] = ' ';
|
dst[oi++] = ' ';
|
||||||
si += 2;
|
si += 2;
|
||||||
|
/* \r\n / \n\r 成对 → 只留一个空格(跳过紧随的互补转义,防双空格) */
|
||||||
|
if (si + 1 < n && src[si] == '\\' &&
|
||||||
|
((src[si + 1] == 'r' && src[si - 1] == 'n') ||
|
||||||
|
(src[si + 1] == 'n' && src[si - 1] == 'r')))
|
||||||
|
si += 2;
|
||||||
} else {
|
} else {
|
||||||
dst[oi++] = src[si++];
|
dst[oi++] = src[si++];
|
||||||
}
|
}
|
||||||
|
|||||||
Binary file not shown.
Reference in new issue
Block a user