diff --git a/patch_v2/ncm_app/REPORT.md b/patch_v2/ncm_app/REPORT.md index bb5ded9..f254161 100644 --- a/patch_v2/ncm_app/REPORT.md +++ b/patch_v2/ncm_app/REPORT.md @@ -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 (核心库 §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@:/usr/data/mnt/sd_0/MeowAPPs/dev.player.ncm/ncm.new && ssh root@ '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)。 diff --git a/patch_v2/ncm_app/app/logic/lyric.c b/patch_v2/ncm_app/app/logic/lyric.c index f48909f..da7dbbe 100644 --- a/patch_v2/ncm_app/app/logic/lyric.c +++ b/patch_v2/ncm_app/app/logic/lyric.c @@ -17,7 +17,7 @@ static char s_tl[NCM_LYRIC_LINES][LRC_MAX_TEXT]; static int s_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) { 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')) { dst[oi++] = ' '; 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 { dst[oi++] = src[si++]; } diff --git a/patch_v2/ncm_app/ncm b/patch_v2/ncm_app/ncm index a491459..d88469f 100755 Binary files a/patch_v2/ncm_app/ncm and b/patch_v2/ncm_app/ncm differ