feat(ncm): 三期第一批 — 歌单真分页(取消256截断)+专辑封面ncm_cache+歌词后台加载补锁
[agent-239] 仅改 patch_v2/ncm_app/。 1. 歌单分页(REPORT §14.1): track/all 全 404(§13.1) 弃用, 回 detail(n=1)全量 trackIds + song_detail 分批(≤NCM_SONG_BATCH=100) 触底逐页加载。appstate_load_playlist_songs_page offset=0 重建 song_ids[]/track_ids_count 载第0页, offset>0 追加(失败保留已载); playlist.c g_pl_more=(song_count<track_ids_count && <NCM_SONG_MAX), 触底 g_ploffset=song_count 起下一页(保留滚动), 首屏失败整页空态+点按重试, 追加失败 snack+保留已载。NCM_SONG_MAX 256→1000(单页渲染上限, bss +~1MB), NCM_SONG_BATCH 64→100, 删 NCM_PLAYLIST_PAGE(track/all 遗留)。 2. 专辑封面(§14.2): cover.c 封面改走 ncm_cache(LRU, 键=djb2(url)→song_cov<h>.jpg 同池), 缓存命中免下载/未命中下载→open/write/commit 落盘, 仍写 cover_current.jpg 供 launcher; 顺带修真 bug: save_cover_raw 在已持 g_cm 临界区内再 lock(非递归死锁)→封面永不刷新, 改调用方持锁。 歌单列表/详情头块封面保持现状。 3. 歌词后台加载(§14.3): lyric_worker 已异步(§11)未改线程模型; 补 lyric.c g_lyric_mu 数据竞争锁, 写侧 lyric_parse 整段持锁(清空用 lyric_clear_nolock 免二次取锁), 读侧 lyric_count/line_at/ line_text/line_tl 逐次取锁, 消除 UI/IPC 与 worker 间数据竞争(原 §10.9 FM4 良性竞态)。 验证(0 真实 NCM 请求): bench/pagination_sim.c 离线 stub 驱动真实 appstate 分页函数全 PASS (130首读全程/5191首封顶1000/空/越界无操作); mipsel build_ncm.sh 0 warning(text 842820/bss 2052068); 宿主 build_ncm_host.sh+MEOW_UI_DUMP home/playlist/player/lands 渲染正常无崩溃; host SDK_SRC 补 meow_pinyin(拼音IME py_predict 漏编致链接断)。ncm md5 b34bc1ed, ncm_host md5 1a2327e0。 未动 sdk/(主会话改 meow_ui_ime.c)/meow_launcherd/meow_amsd/vendor/core; 未 commit 它们。
This commit is contained in:
1 parent
5fac5e380d
commit
ee104f1b18
10 files changed
+471
-87
No files matched your search
@@ -926,3 +926,127 @@ start → home: playlists rc=0 count=120 uid=408996274 → exit
|
|||||||
- **`al.picUrl` 封面保留**:song_detail 经 `parse_songs_array` 解析出 `al.picUrl`,§12.1 黑胶真专辑封面功能不退。
|
- **`al.picUrl` 封面保留**:song_detail 经 `parse_songs_array` 解析出 `al.picUrl`,§12.1 黑胶真专辑封面功能不退。
|
||||||
- **构建**:`build_ncm.sh` mipsel 自有代码 `-Wall -Wextra` 0 warning(本批改动文件逐文件复核无告警);vendor 豁免。
|
- **构建**:`build_ncm.sh` mipsel 自有代码 `-Wall -Wextra` 0 warning(本批改动文件逐文件复核无告警);vendor 豁免。
|
||||||
- **未动 sdk/、meow_launcherd/、vendor/,未 commit/push/刷机,未动设备系统/恢复区**;部署留用户后续验收。
|
- **未动 sdk/、meow_launcherd/、vendor/,未 commit/push/刷机,未动设备系统/恢复区**;部署留用户后续验收。
|
||||||
|
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 14. 三期第一批:歌单真分页 + 专辑封面 ncm_cache 化 + 歌词后台加载补锁(2026-08-31,agent-239)
|
||||||
|
|
||||||
|
> 范围:仅 `patch_v2/ncm_app/`(含 build 脚本与 REPORT 本文件)。未动 `sdk/`(主会话在改 `meow_ui_ime.c`)、
|
||||||
|
> `meow_launcherd/`、`vendor/`、`core/`。风控纪律:本批真实 NCM 请求 **0 次**(全部离线 stub/宿主导出/已落盘样本)。
|
||||||
|
> 部署/验收留用户(真机验收点见 §14.7)。
|
||||||
|
|
||||||
|
### 14.1 歌单分页(detail 全量 trackIds + song_detail 分批,取消 256 硬截断,分页驱动)
|
||||||
|
|
||||||
|
- **背景**:§13.1 真机确认 `/api/playlist/track/all`(eapi/weapi、v6/无v6)全 404,§12.3 的“服务端分页”不可行;
|
||||||
|
§13 回退到 detail(`n=1`)+ song_detail 但 `NCM_SONG_MAX=256` 硬截断、一次拉满(首屏 34.7s)。本期把
|
||||||
|
“256 截断 + 一次拉满”改为“**detail 全量 trackIds 驱动 + song_detail 分批 + 触底逐页加载**”。
|
||||||
|
- `app/logic/appstate.c` `appstate_load_playlist_songs_page(idx,offset,limit)` 重写:
|
||||||
|
- `offset==0`(重头):detail(`n=1`) 取全量 trackIds 入 `song_ids[]`(≤ `NCM_SONG_MAX`),记 `track_ids_count`;
|
||||||
|
`song_count`/`songs[]` 清空;只载**第 0 页**(song_detail 前 `limit` 首,其余待触底再拉,不冻 UI、不一次拉满)。
|
||||||
|
- `offset>0`(触底追加):从 `song_ids[offset..]` 载 song_detail,追加写 `songs[offset..]`,
|
||||||
|
`song_count = offset + 本页实际数`;**失败返回不修改 song_count**(保留已载页)。`offset>=track_ids_count` 或
|
||||||
|
`offset>=NCM_SONG_MAX` 时无操作返回。
|
||||||
|
- `track_ids_count` 为“是否还有下一页”唯一判据(`song_count < track_ids_count`)。
|
||||||
|
- `app/ui/playlist.c`:`playlist_worker` 按页(`NCM_SONG_BATCH`)调 `appstate_call`;`g_pl_more =
|
||||||
|
(rc==NCM_OK) && song_count<track_ids_count && song_count<NCM_SONG_MAX`;`pl_maybe_load_more()` 视口触底 →
|
||||||
|
`g_ploffset = g_st.song_count` 起下一页(append 成功保留滚动位置防跳顶);**首屏失败**整页空态 + 点按重试;
|
||||||
|
**追加失败** snack + 保留已载歌曲 + 重置 `g_pl_more`(可再触底/点击重试)。
|
||||||
|
- 加载/失败/空态三态:`LOADING 正在加载曲目...` / `LOAD FAILED·SESSION EXPIRED·NETWORK ERROR(点按重试)` / `NO TRACKS`。
|
||||||
|
点按重试:`playlist_on_tap` 在 `g_load_err!=0` 时命中内容区触发 `playlist_refresh()`。
|
||||||
|
- **上限:`NCM_SONG_MAX` 256→1000**(单页渲染上限 = 分页驱动;每首 `ncm_song_t` ≈1.32KB → 歌曲数组 ~1.37MB,
|
||||||
|
bss +~1MB,14MB 内安全);`NCM_SONG_BATCH` 64→100(≤500 安全窗内取保守 100;`clist[4096]` 装 100 首 ≈2KB,
|
||||||
|
如需 200 首需先扩 clist);删除 `NCM_PLAYLIST_PAGE`(track/all 遗留,已无引用)。footer 显示 `已载/总数`(总数取
|
||||||
|
`playlists[..].track_count`,如 5191 歌单读到 1000 时显示 `1000/5191`)。
|
||||||
|
|
||||||
|
### 14.2 专辑封面(黑胶用真实专辑图 + 缓存进 ncm_cache LRU + 修一个真死锁)
|
||||||
|
|
||||||
|
- `ncm_song_t.al_pic` + `parse_songs_array` 解析 `al.picUrl`(§12.1 已落地,本期未改 core);player 黑胶盘封面取自
|
||||||
|
`s->al_pic[0] ? s->al_pic : g_st.cover_url`(§12.1),**歌单列表/详情头块封面保持现状**(`cover_get()`,未改)。
|
||||||
|
- `app/logic/cover.c` 封面图改走 **ncm_cache**(LRU 口径沿用 ncm_cache):键 = djb2(封面 URL) → 文件
|
||||||
|
`song_cov<hex>.jpg`(与 mp3 `song_<id>.mp3` 同池,`ncm_cache_lookup`/`commit` 刷新 mtime=LRU)。
|
||||||
|
worker 流程:① 缓存命中直接解码(免下载、二次进入离线可看);② 未命中 → 下载解码 → `ncm_cache_open/write/commit`
|
||||||
|
落盘;仍写 `cover_current.jpg`(`save_cover_raw`)供 launcher 媒体卡片。
|
||||||
|
- **顺带修真 bug**:旧 `save_cover_raw` 在已持 `g_cm` 的临界区内再 `pthread_mutex_lock(&g_cm)`(非递归锁自锁死)→
|
||||||
|
cover worker 永久卡死、`g_gen` 永不 +1 → 黑胶封面**永不刷新**。改为“调用方持有 `g_cm`”(不再二次取锁);
|
||||||
|
封面写盘缓冲扩到 `NCM_CACHE_DIR_MAX+32`(顺带消宿主 glibc fortify `-Wformat-truncation` 告警,mipsel musl 本就无)。
|
||||||
|
|
||||||
|
### 14.3 歌词后台加载(确认已异步;补数据竞争锁)
|
||||||
|
|
||||||
|
- 歌词后台加载 §11(`player_manager.c` `lyric_worker`:轮询当前曲 → 异步 `ncm_api_lyric` → `lyric_parse`,
|
||||||
|
UI 不阻塞;`lyric_state()` 判「加载中/无词」)**已落地**,本批未改后台线程模型(确实不阻塞 UI)。
|
||||||
|
- 补 `app/logic/lyric.c` **数据竞争锁** `g_lyric_mu`:写侧 `lyric_parse` 整段持锁(清空用 `lyric_clear_nolock`
|
||||||
|
避免二次取锁自锁死);读侧 `lyric_count`/`lyric_line_at`/`lyric_line_text`/`lyric_line_tl` 每次取锁。
|
||||||
|
消除 UI 线程(player.c 歌词区)与 IPC 线程(ipc_media.c `lyric_count`)同 lyric worker 间的数据竞争
|
||||||
|
(早前为“良性竞态”§10.9 FM4;本期按“禁止数据竞争”要求收紧)。
|
||||||
|
- 与现有 `g_play.mu` 同模式(函数级锁、无嵌套)。已知残余:worker 读 `g_st.songs[idx].id` 无锁——
|
||||||
|
`songs[]` 写读竞态(§10.9 FM4 接受,与播放线程同源),本期未扩范围。
|
||||||
|
|
||||||
|
### 14.4 验证记录(0 真实 NCM 请求)
|
||||||
|
|
||||||
|
| 项 | 方式 | 结果 |
|
||||||
|
|---|---|---|
|
||||||
|
| 歌单分页状态机 | `bench/pagination_sim.c` 离线 stub(0 网络)驱动**真实** `appstate_load_playlist_songs_page` | **全 PASS**:130 首读全程(1 detail + 2 song_detail 页,`song_count=130/track_ids_count=130`,末首含 al_pic);5191 首封顶 `NCM_SONG_MAX`(1000,10 页,越界无操作);空歌单 0;越过尾部/上限再 load 均无副作用 |
|
||||||
|
| mipsel 构建 | `bash build_ncm.sh`(自有代码 `-Wall -Wextra`) | **0 warning**;`text 842820 / data 856 / bss 2052068`(bss +~1MB 来自 NCM_SONG_MAX 1000) |
|
||||||
|
| 宿主离屏渲染 | `build_ncm_host.sh` + `MEOW_UI_DUMP`(mock,0 网络) | home/playlist/player/lands 渲染正常(歌单 6 行 + footer `6/128`、黑胶 + 歌词当前行红色高亮),无崩溃 |
|
||||||
|
| 宿主链接 | `build_ncm_host.sh` | 补 `meow_pinyin.o`(主会话 229 拼音 IME 引入 `py_predict`,旧 host SDK_SRC 漏编 → 链接断);cover.c 截断告警已消 |
|
||||||
|
|
||||||
|
### 14.5 md5(本批改动文件 + 产物)
|
||||||
|
|
||||||
|
```
|
||||||
|
b34bc1ed2c3539f1ca80a005884833cd ncm # mipsel 静态 App(新)
|
||||||
|
1a2327e0ccd352d11bea88ebd35df549 ncm_host # 宿主离屏调试二进制
|
||||||
|
0b0a4dd5e208d7b5eb504befb374a29e app/logic/appstate.c
|
||||||
|
6d66eed99c556e301498c5823b43c294 app/logic/cover.c
|
||||||
|
ace0def0e8c3ec991013b0675957f269 app/logic/lyric.c
|
||||||
|
1a1871032c2ecae7a1cc80c7aa00381a app/ncm_app_internal.h
|
||||||
|
ab87bd5f74b82fd3d62c49a16a4c9fe5 app/ui/playlist.c
|
||||||
|
c73366373b3a372d96b2218e528c6381 build_ncm_host.sh
|
||||||
|
141e9098a7cabeade9aa6b442ba20910 bench/pagination_sim.c
|
||||||
|
```
|
||||||
|
|
||||||
|
### 14.6 出入清单(只改 `patch_v2/ncm_app/`)
|
||||||
|
|
||||||
|
```
|
||||||
|
修改:
|
||||||
|
app/logic/appstate.c # 歌单分页:offset=0 重建 trackIds 索引+载第0页;offset>0 追加;track_ids_count
|
||||||
|
app/ui/playlist.c # 触底逐页加载 + g_pl_more 判据 + 加载/失败/空态 + 点按重试 + 追加失败保留已载
|
||||||
|
app/ncm_app_internal.h # NCM_SONG_MAX 1000 / NCM_SONG_BATCH 100 / 删 NCM_PLAYLIST_PAGE + track_ids_count
|
||||||
|
app/logic/cover.c # 封面走 ncm_cache(LRU) + 修 save_cover_raw 自锁死 + 缓冲扩容消截断告警
|
||||||
|
app/logic/lyric.c # 加 g_lyric_mu(写 parse 整段持锁、读逐次持锁)消除数据竞争
|
||||||
|
build_ncm_host.sh # host SDK_SRC 补 meow_pinyin(拼音 IME py_predict 漏编→链接断)
|
||||||
|
ncm / ncm_host # 重编产物
|
||||||
|
新增:
|
||||||
|
bench/pagination_sim.c # 歌单分页离线状态机回归(stub 网络端点,0 请求)
|
||||||
|
REPORT.md # 本批记录(§14)
|
||||||
|
(未动 sdk/、meow_launcherd/、vendor/、core/;未 commit/push/刷机;未动设备系统/恢复区)
|
||||||
|
```
|
||||||
|
|
||||||
|
### 14.7 诚实出入 / 已知限制
|
||||||
|
|
||||||
|
- **真机未部署/未验**(风控:0 新增真实请求)。部署链路同 §13.5(`scp ncm root@<ip>:/usr/data/mnt/sd_0/MeowAPPs/dev.player.ncm/ncm.new`
|
||||||
|
→ `ssh root@<ip> '. /etc/profile; mv ...ncm.new ...ncm'`,先 `.new` 再 `mv` 防 ETXTBSY)。真机验收点:
|
||||||
|
① 小歌单全程可读;大歌单(如 5191)下滑触底逐页加载到 1000 封顶(footer `已载/总数`),不冻 UI;
|
||||||
|
② 黑胶显示真实专辑封面(首次下载→ncm_cache 落盘,二次进入免下载;第三方封面 `http://` 若握手失败回落歌单封面,
|
||||||
|
为 §12.8 既有“http 封面走 TLS”旧疾,本批未改 core 规避);③ 歌词后台加载不卡 UI、切歌刷新、下/上一首顺延。
|
||||||
|
- **大歌单仍有内存上限(NCM_SONG_MAX=1000)**:这是 14MB 硬约束下的取舍(每 +100 首 ≈ +132KB)。
|
||||||
|
若要全量 5000+ 首,需:a) 歌曲对象瘦身(`al_pic[512]` 每首占大头,可拆到按需加载),或 b) 列表滑动窗口
|
||||||
|
(`meow_listview` 需 O(1) `src2(idx)` 重映射,复杂度高)。footer「已载/总数」已向用户标明进度。
|
||||||
|
- **`ncm_cache_evict` 生产路径未显式触发超限淘汰**(§10.9 既有遗留:evict 仅 test 调用;song/cover commit 后不自动
|
||||||
|
触发)。封面走 ncm_cache 同池后同样受该路径约束——**超限淘汰非本批引入**,本批也未扩大行为(不主动加 evict 调用,
|
||||||
|
避免改已验证的缓存写入路径)。
|
||||||
|
- **歌词锁残余**:读侧函数取锁后返回存量指针(`s_txt/s_tl` 静态数组),解锁后可能被下一次 `lyric_parse` 覆盖;
|
||||||
|
写入仅发生在切歌瞬间(一次/歌),本地读在一两帧内完成,峰值可能显示一帧旧/空行(与 §10.9 FM4 同接受)。
|
||||||
|
严格消除需调用方持锁拷贝(本批未做,接口需改,避免扩大 diff)。
|
||||||
|
- **host 构建有 SDK `meow_ui_vol.c`「system 返回值忽略」告警**(SDK 源,非本 App;mipsel musl 无)。本 App 自有代码 0 warning。
|
||||||
|
- **分页触底加载与误触/点按为逻辑推断**(无法经 SSH 驱动真实触摸/滚动);误触修复(§13.3)未在分页下回归真机。
|
||||||
|
建议真机快速下滑到底验证逐页加载、点按重试、追加失败保留已载。
|
||||||
|
|
||||||
|
### 14.8 数值权衡(供后续微调)
|
||||||
|
|
||||||
|
| 常量 | 旧值 | 新值 | 说明 |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `NCM_SONG_MAX` | 256 | 1000 | 单页渲染上限(分页驱动),歌曲数组 ~1.32KB/首 → +100 ≈ +132KB bss |
|
||||||
|
| `NCM_SONG_BATCH` | 64 | 100 | song_detail 每批(≤500 安全窗内保守 100;`clist[4096]` 装 100 首 ≈2KB) |
|
||||||
|
| `NCM_PLAYLIST_PAGE` | 50 | 删除 | track/all 服务端分页遗留(该端点 404),改由 `NCM_SONG_BATCH` 承担页粒度 |
|
||||||
|
| `g_st.track_ids_count` | — | 新增 | detail 取回 trackIds 总数,驱动「还有下一页」判据 |
|
||||||
@@ -126,34 +126,44 @@ int appstate_load_playlist_songs_page(int idx, int offset, int limit)
|
|||||||
if (limit <= 0 || limit > NCM_SONG_BATCH) limit = NCM_SONG_BATCH;
|
if (limit <= 0 || limit > NCM_SONG_BATCH) limit = NCM_SONG_BATCH;
|
||||||
if (offset < 0) offset = 0;
|
if (offset < 0) offset = 0;
|
||||||
|
|
||||||
/* 二期曾改走 /api/playlist/track/all 服务端分页(REPORT §12.3),但真机实测该端点
|
/* REPORT §13.1:/api/playlist/track/all 服务端分页端点真机全 404(NCM 无此路由),弃用。
|
||||||
* (eapi/weapi、v6/无v6 四种路由)全部返回 code 404「接口未找到」——NCM 服务端无此路由,
|
* 回到 §10.11 已验证路径并做**真分页**:detail(n=1)取【全量】trackIds 入 song_ids[],
|
||||||
* 故回退到 REPORT §10.11 已验证的 /api/v6/playlist/detail(trackIds) + /api/v3/song/detail 分批。
|
* song_detail 按 ≤NCM_SONG_BATCH 分批拉歌曲对象(含 al.picUrl),offset 为「下一页」起始;
|
||||||
* detail 一次取回前 NCM_SONG_MAX 首 trackIds;song_detail 分批取歌曲对象(含 al.picUrl)。
|
* 列表触底驱动 offset 前进,song_count 逐页累加(不再一次性拉满,大歌单不冻 UI,
|
||||||
* offset==0 重头清空并整批载入;offset>0(触底“加页”)时 detail 已一次取满,无独立分页,
|
* 也不把「第 N 页之后的歌」挡在 256 之外——NCM_SONG_MAX 上限提升到 1000,见 §14)。 */
|
||||||
* 直接返回 NCM_OK(歌单数 ≤NCM_SONG_MAX 时已全部在列,song_count 不变)。 */
|
|
||||||
if (offset != 0) return NCM_OK;
|
|
||||||
|
|
||||||
memset(g_st.song_ids, 0, sizeof g_st.song_ids);
|
if (offset == 0) {
|
||||||
int idn = 0;
|
/* 重头:重建 trackIds 索引 + 清空已物化歌曲(song_count 无论成败都归零)。 */
|
||||||
int rc = ncm_api_playlist_detail(g_st.ctx.device_id, g_st.ctx.cookie, pl->id,
|
memset(g_st.song_ids, 0, sizeof g_st.song_ids);
|
||||||
NULL, 0, NULL, 0,
|
memset(g_st.songs, 0, sizeof g_st.songs);
|
||||||
g_st.song_ids, NCM_SONG_MAX, &idn);
|
g_st.song_count = 0;
|
||||||
if (rc != NCM_OK) { g_st.song_count = 0; return rc; }
|
g_st.track_ids_count = 0;
|
||||||
|
int idn = 0;
|
||||||
memset(g_st.songs, 0, sizeof g_st.songs);
|
int rc = ncm_api_playlist_detail(g_st.ctx.device_id, g_st.ctx.cookie, pl->id,
|
||||||
int outn = 0;
|
NULL, 0, NULL, 0,
|
||||||
for (int off = 0; off < idn; off += NCM_SONG_BATCH) {
|
g_st.song_ids, NCM_SONG_MAX, &idn);
|
||||||
int batch = idn - off;
|
if (rc != NCM_OK) return rc;
|
||||||
if (batch > NCM_SONG_BATCH) batch = NCM_SONG_BATCH;
|
g_st.track_ids_count = idn;
|
||||||
int bn = 0;
|
/* 首屏只需第 0 页:song_detail 只拉前 limit 首,其余待滚动触底再拉(分页驱动)。 */
|
||||||
rc = ncm_api_song_detail(g_st.ctx.device_id, g_st.ctx.cookie, &g_st.song_ids[off],
|
} else if (g_st.track_ids_count == 0) {
|
||||||
batch, &g_st.songs[off], NCM_SONG_MAX - off, &bn);
|
/* 防御:offset>0 但没有索引(不应发生;主路径先走 offset=0)。 */
|
||||||
if (rc != NCM_OK) break;
|
return NCM_ERR_ARG;
|
||||||
outn += bn;
|
|
||||||
if (bn < batch) break;
|
|
||||||
}
|
}
|
||||||
g_st.song_count = outn;
|
|
||||||
|
if (offset >= g_st.track_ids_count) return NCM_OK; /* 已到尾部/上限 */
|
||||||
|
if (offset >= NCM_SONG_MAX) return NCM_OK; /* 内存上限:不再物化更多 */
|
||||||
|
|
||||||
|
int can = g_st.track_ids_count - offset;
|
||||||
|
int room = NCM_SONG_MAX - offset;
|
||||||
|
if (can > limit) can = limit; /* 每页 ≤ limit(保守 100) */
|
||||||
|
if (can > room) can = room; /* 不越界 song_ids[]/songs[] */
|
||||||
|
|
||||||
|
int bn = 0;
|
||||||
|
int rc = ncm_api_song_detail(g_st.ctx.device_id, g_st.ctx.cookie, &g_st.song_ids[offset],
|
||||||
|
can, &g_st.songs[offset], NCM_SONG_MAX - offset, &bn);
|
||||||
|
if (rc != NCM_OK) return rc; /* 失败:song_count 不变(保留已载页) */
|
||||||
|
|
||||||
|
g_st.song_count = offset + bn; /* 成功:累加本页 */
|
||||||
appstate_apply_playlist_cover(idx);
|
appstate_apply_playlist_cover(idx);
|
||||||
return NCM_OK;
|
return NCM_OK;
|
||||||
}
|
}
|
||||||
@@ -161,15 +171,16 @@ int appstate_load_playlist_songs_page(int idx, int offset, int limit)
|
|||||||
int appstate_load_playlist_songs(int idx)
|
int appstate_load_playlist_songs(int idx)
|
||||||
{
|
{
|
||||||
/* 兼容入口:整批载入(内部按页循环到 NCM_SONG_MAX 或歌单尾部)。 */
|
/* 兼容入口:整批载入(内部按页循环到 NCM_SONG_MAX 或歌单尾部)。 */
|
||||||
int rc, loaded = 0;
|
int rc = appstate_load_playlist_songs_page(idx, 0, NCM_SONG_BATCH);
|
||||||
do {
|
if (rc != NCM_OK) return rc;
|
||||||
rc = appstate_load_playlist_songs_page(idx, loaded, NCM_PLAYLIST_PAGE);
|
int off = g_st.song_count;
|
||||||
|
while (off < g_st.track_ids_count && off < NCM_SONG_MAX) {
|
||||||
|
rc = appstate_load_playlist_songs_page(idx, off, NCM_SONG_BATCH);
|
||||||
if (rc != NCM_OK) return rc;
|
if (rc != NCM_OK) return rc;
|
||||||
int before = loaded;
|
int nxt = g_st.song_count;
|
||||||
loaded = g_st.song_count;
|
if (nxt <= off) break; /* 无进展:防死循环 */
|
||||||
if (loaded - before < NCM_PLAYLIST_PAGE) break; /* 尾部:已载完 */
|
off = nxt;
|
||||||
if (loaded >= NCM_SONG_MAX) break;
|
}
|
||||||
} while (1);
|
|
||||||
return NCM_OK;
|
return NCM_OK;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -10,6 +10,7 @@
|
|||||||
#include <string.h>
|
#include <string.h>
|
||||||
#include <stdlib.h>
|
#include <stdlib.h>
|
||||||
#include <unistd.h>
|
#include <unistd.h>
|
||||||
|
#include <sys/stat.h>
|
||||||
|
|
||||||
static pthread_mutex_t g_cm = PTHREAD_MUTEX_INITIALIZER;
|
static pthread_mutex_t g_cm = PTHREAD_MUTEX_INITIALIZER;
|
||||||
static meow_img *g_cover_img;
|
static meow_img *g_cover_img;
|
||||||
@@ -42,18 +43,39 @@ static meow_img *decode_cover(const char *url, size_t *out_len)
|
|||||||
return meow_img_load_mem((const void *)resp.body, (int)resp.body_len);
|
return meow_img_load_mem((const void *)resp.body, (int)resp.body_len);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 封面缓存键:URL 的 djb2 哈希(稳定性由 URL 决定——同一专辑封面 URL 名即可命中)。
|
||||||
|
* 走 ncm_cache(LRU 口径沿用 ncm_cache,见 REPORT §14.2):键进 ncm_cache_path => 文件
|
||||||
|
* `song_cov<hex>.jpg`,与 mp3(song_<id>.mp3)同池,lookup/commit 刷新 mtime(LRU)。 */
|
||||||
|
static void cover_cache_id(const char *url, char *out, size_t out_sz)
|
||||||
|
{
|
||||||
|
unsigned long h = 5381;
|
||||||
|
for (const char *p = url; *p; p++) h = ((h << 5) + h) + (unsigned char)*p;
|
||||||
|
snprintf(out, out_sz, "cov%lx", h);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 读整文件到 buf(< cap),成功返回 1,写入 *len。 */
|
||||||
|
static int read_file_into_buf(const char *path, unsigned char *buf, size_t cap, size_t *len)
|
||||||
|
{
|
||||||
|
FILE *f = fopen(path, "rb");
|
||||||
|
if (!f) return 0;
|
||||||
|
size_t n = fread(buf, 1, cap, f);
|
||||||
|
fclose(f);
|
||||||
|
if (n == 0) return 0;
|
||||||
|
if (len) *len = n;
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 写 cover_current.jpg 供 launcher 媒体卡片读(兼容保留)。调用方须已持有 g_cm
|
||||||
|
* (早前版本在此内再 lock g_cm → 非递归锁自锁死——cover worker 永久卡死、封面永不出现)。 */
|
||||||
static void save_cover_raw(const char *url, size_t len)
|
static void save_cover_raw(const char *url, size_t len)
|
||||||
{
|
{
|
||||||
if (!url || len == 0) return;
|
if (!url || len == 0) return;
|
||||||
if (g_st.cache_dir[0]) {
|
if (g_st.cache_dir[0]) {
|
||||||
char path[512];
|
char path[NCM_CACHE_DIR_MAX + 32]; /* >512:宿主 glibc fortify 对 %s 展开宽证明可放 */
|
||||||
snprintf(path, sizeof path, "%s/cover_current.jpg", g_st.cache_dir);
|
snprintf(path, sizeof path, "%s/cover_current.jpg", g_st.cache_dir);
|
||||||
ncm_store_write_file(path, g_buf, len);
|
ncm_store_write_file(path, g_buf, len);
|
||||||
pthread_mutex_lock(&g_cm);
|
|
||||||
ncm_strlcpy(g_cover_path, path, sizeof g_cover_path);
|
ncm_strlcpy(g_cover_path, path, sizeof g_cover_path);
|
||||||
pthread_mutex_unlock(&g_cm);
|
|
||||||
}
|
}
|
||||||
(void)url;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
static void *cover_worker(void *arg)
|
static void *cover_worker(void *arg)
|
||||||
@@ -66,9 +88,31 @@ static void *cover_worker(void *arg)
|
|||||||
ncm_strlcpy(url, g_tgt_url, sizeof url);
|
ncm_strlcpy(url, g_tgt_url, sizeof url);
|
||||||
pthread_mutex_unlock(&g_cm);
|
pthread_mutex_unlock(&g_cm);
|
||||||
|
|
||||||
|
char key[32];
|
||||||
|
cover_cache_id(url, key, sizeof key);
|
||||||
size_t blen = 0;
|
size_t blen = 0;
|
||||||
meow_img *img = decode_cover(url, &blen);
|
meow_img *img = NULL;
|
||||||
if (!img) { usleep(500 * 1000); continue; }
|
|
||||||
|
/* ① 缓存命中直接解码(免下载、离线可看;LRU:lookup 刷新 mtime) */
|
||||||
|
char cpath[512];
|
||||||
|
if (g_st.ctx.cache.dir[0] && ncm_cache_lookup(&g_st.ctx.cache, key, ".jpg",
|
||||||
|
cpath, sizeof cpath) == NCM_OK &&
|
||||||
|
read_file_into_buf(cpath, g_buf, sizeof g_buf, &blen)) {
|
||||||
|
img = meow_img_load_mem(g_buf, (int)blen);
|
||||||
|
}
|
||||||
|
/* ② 未命中 → 下载解码;成功后落进 ncm_cache(LRU 池,超限由 ncm_cache_evict 淘汰) */
|
||||||
|
if (!img) {
|
||||||
|
blen = 0;
|
||||||
|
img = decode_cover(url, &blen);
|
||||||
|
if (!img) { usleep(500 * 1000); continue; }
|
||||||
|
if (g_st.ctx.cache.dir[0]) {
|
||||||
|
ncm_cache_writer_t *w = NULL;
|
||||||
|
if (ncm_cache_open(&g_st.ctx.cache, key, ".jpg", &w) == NCM_OK) {
|
||||||
|
ncm_cache_write(w, g_buf, blen);
|
||||||
|
ncm_cache_commit(w);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pthread_mutex_lock(&g_cm);
|
pthread_mutex_lock(&g_cm);
|
||||||
/* 目标仍是 url 才换入(期间未提交更早) */
|
/* 目标仍是 url 才换入(期间未提交更早) */
|
||||||
|
|||||||
@@ -10,11 +10,24 @@
|
|||||||
#include <stdlib.h>
|
#include <stdlib.h>
|
||||||
#include <string.h>
|
#include <string.h>
|
||||||
|
|
||||||
|
/* 歌词数据由 lyric_worker(player_manager.c,后台线程)写(lyric_parse/lyric_clear)而 UI 线程
|
||||||
|
* 与 IPC 线程读(lyric_count/lyric_line_at/lyric_line_text/lyric_line_tl)。三期第一批(REPORT §14.3)
|
||||||
|
* 补一把锁消除**数据竞争**(早前为良性竞态 §10.9 FM4;本节要求禁止数据竞争)。锁粒度=函数级,
|
||||||
|
* 写侧在 lyric_parse 整段持锁、读侧每次访问持锁,无嵌套(清空用 nolock 版,避免二次取锁自锁死)。 */
|
||||||
#define LRC_MAX_TEXT 192
|
#define LRC_MAX_TEXT 192
|
||||||
static long s_t[NCM_LYRIC_LINES];
|
static long s_t[NCM_LYRIC_LINES];
|
||||||
static char s_txt[NCM_LYRIC_LINES][LRC_MAX_TEXT];
|
static char s_txt[NCM_LYRIC_LINES][LRC_MAX_TEXT];
|
||||||
static char s_tl[NCM_LYRIC_LINES][LRC_MAX_TEXT];
|
static char s_tl[NCM_LYRIC_LINES][LRC_MAX_TEXT];
|
||||||
static int s_n;
|
static int s_n;
|
||||||
|
static pthread_mutex_t g_lyric_mu = PTHREAD_MUTEX_INITIALIZER;
|
||||||
|
|
||||||
|
static void lyric_clear_nolock(void)
|
||||||
|
{
|
||||||
|
s_n = 0;
|
||||||
|
memset(s_t, 0, sizeof(s_t));
|
||||||
|
memset(s_txt, 0, sizeof(s_txt));
|
||||||
|
memset(s_tl, 0, sizeof(s_tl));
|
||||||
|
}
|
||||||
|
|
||||||
/* 复制一行歌词正文 n 字节到 dst,把「字面反斜杠 n/r」序列(服务器用 \n 把多行文本塞进一行,
|
/* 复制一行歌词正文 n 字节到 dst,把「字面反斜杠 n/r」序列(服务器用 \n 把多行文本塞进一行,
|
||||||
* 字面 \n 渲染成豆腐块/凌乱)替换为空格;顺带把 \r\n / \n\r 折叠为单空格。有界 ≤max。 */
|
* 字面 \n 渲染成豆腐块/凌乱)替换为空格;顺带把 \r\n / \n\r 折叠为单空格。有界 ≤max。 */
|
||||||
@@ -104,57 +117,74 @@ static int cmp_lines(const void *a, const void *b)
|
|||||||
|
|
||||||
int lyric_parse(const char *lrc, const char *tlrc)
|
int lyric_parse(const char *lrc, const char *tlrc)
|
||||||
{
|
{
|
||||||
lyric_clear();
|
pthread_mutex_lock(&g_lyric_mu);
|
||||||
|
lyric_clear_nolock();
|
||||||
|
int *idx = NULL;
|
||||||
|
int result = 0;
|
||||||
lrc_parse(lrc, 0);
|
lrc_parse(lrc, 0);
|
||||||
lrc_parse(tlrc, 1);
|
lrc_parse(tlrc, 1);
|
||||||
/* 按时间排序(用索引数组) */
|
/* 按时间排序(用索引数组) */
|
||||||
int *idx = (int *)malloc(sizeof(int) * (size_t)(s_n ? s_n : 1));
|
idx = (int *)malloc(sizeof(int) * (size_t)(s_n ? s_n : 1));
|
||||||
if (!idx) return s_n;
|
if (idx) {
|
||||||
for (int i = 0; i < s_n; i++) idx[i] = i;
|
for (int i = 0; i < s_n; i++) idx[i] = i;
|
||||||
qsort(idx, (size_t)s_n, sizeof(int), cmp_lines);
|
qsort(idx, (size_t)s_n, sizeof(int), cmp_lines);
|
||||||
long ttmp[NCM_LYRIC_LINES];
|
long ttmp[NCM_LYRIC_LINES];
|
||||||
char txt[NCM_LYRIC_LINES][LRC_MAX_TEXT];
|
char txt[NCM_LYRIC_LINES][LRC_MAX_TEXT];
|
||||||
char tl[NCM_LYRIC_LINES][LRC_MAX_TEXT];
|
char tl[NCM_LYRIC_LINES][LRC_MAX_TEXT];
|
||||||
memcpy(ttmp, s_t, sizeof(s_t));
|
memcpy(ttmp, s_t, sizeof(s_t));
|
||||||
memcpy(txt, s_txt, sizeof(s_txt));
|
memcpy(txt, s_txt, sizeof(s_txt));
|
||||||
memcpy(tl, s_tl, sizeof(s_tl));
|
memcpy(tl, s_tl, sizeof(s_tl));
|
||||||
for (int i = 0; i < s_n; i++) {
|
for (int i = 0; i < s_n; i++) {
|
||||||
s_t[i] = ttmp[idx[i]];
|
s_t[i] = ttmp[idx[i]];
|
||||||
memcpy(s_txt[i], txt[idx[i]], LRC_MAX_TEXT);
|
memcpy(s_txt[i], txt[idx[i]], LRC_MAX_TEXT);
|
||||||
memcpy(s_tl[i], tl[idx[i]], LRC_MAX_TEXT);
|
memcpy(s_tl[i], tl[idx[i]], LRC_MAX_TEXT);
|
||||||
|
}
|
||||||
|
free(idx);
|
||||||
}
|
}
|
||||||
free(idx);
|
result = s_n;
|
||||||
return s_n;
|
pthread_mutex_unlock(&g_lyric_mu);
|
||||||
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
int lyric_line_at(int64_t ms)
|
int lyric_line_at(int64_t ms)
|
||||||
{
|
{
|
||||||
|
pthread_mutex_lock(&g_lyric_mu);
|
||||||
int line = 0;
|
int line = 0;
|
||||||
for (int i = 0; i < s_n; i++) {
|
for (int i = 0; i < s_n; i++) {
|
||||||
if (s_t[i] <= ms) line = i;
|
if (s_t[i] <= ms) line = i;
|
||||||
else break;
|
else break;
|
||||||
}
|
}
|
||||||
|
pthread_mutex_unlock(&g_lyric_mu);
|
||||||
return line;
|
return line;
|
||||||
}
|
}
|
||||||
|
|
||||||
const char *lyric_line_text(int i)
|
const char *lyric_line_text(int i)
|
||||||
{
|
{
|
||||||
if (i < 0 || i >= s_n) return "";
|
pthread_mutex_lock(&g_lyric_mu);
|
||||||
return s_txt[i];
|
const char *p = (i < 0 || i >= s_n) ? "" : s_txt[i];
|
||||||
|
pthread_mutex_unlock(&g_lyric_mu);
|
||||||
|
return p;
|
||||||
}
|
}
|
||||||
|
|
||||||
const char *lyric_line_tl(int i)
|
const char *lyric_line_tl(int i)
|
||||||
{
|
{
|
||||||
if (i < 0 || i >= s_n) return "";
|
pthread_mutex_lock(&g_lyric_mu);
|
||||||
return s_tl[i];
|
const char *p = (i < 0 || i >= s_n) ? "" : s_tl[i];
|
||||||
|
pthread_mutex_unlock(&g_lyric_mu);
|
||||||
|
return p;
|
||||||
}
|
}
|
||||||
|
|
||||||
int lyric_count(void) { return s_n; }
|
int lyric_count(void)
|
||||||
|
{
|
||||||
|
pthread_mutex_lock(&g_lyric_mu);
|
||||||
|
int n = s_n;
|
||||||
|
pthread_mutex_unlock(&g_lyric_mu);
|
||||||
|
return n;
|
||||||
|
}
|
||||||
|
|
||||||
void lyric_clear(void)
|
void lyric_clear(void)
|
||||||
{
|
{
|
||||||
s_n = 0;
|
pthread_mutex_lock(&g_lyric_mu);
|
||||||
memset(s_t, 0, sizeof(s_t));
|
lyric_clear_nolock();
|
||||||
memset(s_txt, 0, sizeof(s_txt));
|
pthread_mutex_unlock(&g_lyric_mu);
|
||||||
memset(s_tl, 0, sizeof(s_tl));
|
|
||||||
}
|
}
|
||||||
@@ -39,11 +39,17 @@
|
|||||||
#define APP_OP_W 48 /* 行右侧操作区触达宽 */
|
#define APP_OP_W 48 /* 行右侧操作区触达宽 */
|
||||||
#define APP_FOOTER_H 26
|
#define APP_FOOTER_H 26
|
||||||
|
|
||||||
/* ---- 数据规模(内存纪律:歌单/歌曲大列表虚拟化显示,数组有界)---- */
|
/* ---- 数据规模(内存纪律:歌单/歌曲大列表虚拟化显示,数组有界;硬约束 R3P2 ~14MB)---- */
|
||||||
#define NCM_PLAYLIST_MAX 120 /* 我的歌单上限 */
|
#define NCM_PLAYLIST_MAX 120 /* 我的歌单上限 */
|
||||||
#define NCM_SONG_MAX 256 /* 单个歌单曲目上限(大歌单截断前 256 条) */
|
/* 单个歌单曲目上限 = 分页驱动下的「单页渲染上限」(REPORT §14,三期第一批):
|
||||||
#define NCM_SONG_BATCH 64 /* song_detail 分批每批条数 */
|
* detail(/v6/playlist/detail, n=1)取回全量 trackIds 入 song_ids[](≤ NCM_SONG_MAX);
|
||||||
#define NCM_PLAYLIST_PAGE 50 /* 歌单曲目服务端分页每页条数(/api/playlist/track/all) */
|
* song_detail 按 NCM_SONG_BATCH 分批拉歌词对象,列表触底自动加载下一页,直到
|
||||||
|
* song_count == track_ids_count(歌单尾部)或 == NCM_SONG_MAX(内存上限,footer 显示 x/总数)。
|
||||||
|
* 不再硬截断 256(NCM_SONG_MAX 从 256 提升到 1000,约 1.32KB/首 → 歌曲数组 ~1.37MB,14MB 内安全)。
|
||||||
|
* 数值权衡见 REPORT §14.8(可再上调,代价是 bss 线性增长:每 +100 首 ≈ +132KB)。 */
|
||||||
|
#define NCM_SONG_MAX 1000 /* 歌单曲目/歌曲对象上限(append-only 分页加载) */
|
||||||
|
#define NCM_SONG_BATCH 100 /* song_detail 分批每批条数(≤500 安全窗内取保守 100;
|
||||||
|
* 同时作「下一页」加载粒度;c 数组缓冲 clist[4096] 装 100 首 ≈2KB */
|
||||||
#define NCM_LYRIC_LINES 600 /* 歌词行上限 */
|
#define NCM_LYRIC_LINES 600 /* 歌词行上限 */
|
||||||
#define NCM_COVER_MAX 288*1024 /* 封面下载限幅 ~288KB */
|
#define NCM_COVER_MAX 288*1024 /* 封面下载限幅 ~288KB */
|
||||||
#define NCM_SEARCH_MAX 32 /* 搜索结果上限(v1 只显示前 32 条,无大缓冲) */
|
#define NCM_SEARCH_MAX 32 /* 搜索结果上限(v1 只显示前 32 条,无大缓冲) */
|
||||||
@@ -106,10 +112,15 @@ typedef struct ncm_app_state {
|
|||||||
int playlist_count;
|
int playlist_count;
|
||||||
int active_playlist; /* 当前打开歌单的下标;-1=无 */
|
int active_playlist; /* 当前打开歌单的下标;-1=无 */
|
||||||
|
|
||||||
/* 当前歌单曲目(id 用 int64_t:网易云歌曲 id 可 >2^31,MIPS32 long 溢出) */
|
/* 当前歌单曲目(id 用 int64_t:网易云歌曲 id 可 >2^31,MIPS32 long 溢出)。
|
||||||
|
* song_ids[] = detail 返回的全量 trackIds(≤ NCM_SONG_MAX,分页索引);
|
||||||
|
* songs[] = 已物化的歌曲对象(append-only,与 song_ids[] 前 song_count 首一一对应);
|
||||||
|
* song_count = 已物化数(列表 count = song_count);track_ids_count = trackIds 总数(≥ song_count,
|
||||||
|
* 其差值 = 仍有「下一页」待 song_detail 分批拉取)。 */
|
||||||
int64_t song_ids[NCM_SONG_MAX];
|
int64_t song_ids[NCM_SONG_MAX];
|
||||||
ncm_song_t songs[NCM_SONG_MAX];
|
ncm_song_t songs[NCM_SONG_MAX];
|
||||||
int song_count;
|
int song_count;
|
||||||
|
int track_ids_count; /* detail 取回的 trackIds 总数(≤ NCM_SONG_MAX) */
|
||||||
|
|
||||||
/* 播放页视图态 */
|
/* 播放页视图态 */
|
||||||
int playing_song; /* 正在播放歌曲在 songs[] 下标(UI 侧缓存);-1=无 */
|
int playing_song; /* 正在播放歌曲在 songs[] 下标(UI 侧缓存);-1=无 */
|
||||||
@@ -249,8 +260,10 @@ int appstate_ensure_uid(void); /* 若 uid 缺失则 login_status_check 取
|
|||||||
int appstate_load_playlists(void); /* 拉我的歌单 */
|
int appstate_load_playlists(void); /* 拉我的歌单 */
|
||||||
int appstate_load_playlist_songs(int idx); /* 兼容:整批载入(内部走分页循环) */
|
int appstate_load_playlist_songs(int idx); /* 兼容:整批载入(内部走分页循环) */
|
||||||
int appstate_load_playlist_songs_page(int idx, int offset, int limit);
|
int appstate_load_playlist_songs_page(int idx, int offset, int limit);
|
||||||
/* 服务端分页载入第 offset 页(offset=0 重头填,
|
/* detail(全量 trackIds)后按页拉 song_detail:
|
||||||
* offset>0 追加到 g_st.songs[offset..]);写 g_st.song_count */
|
* offset=0 重头(重建 song_ids[]/track_ids_count,清空后载第 0 页);
|
||||||
|
* offset>0 追加第 offset 页到 g_st.songs[offset..];
|
||||||
|
* 写 g_st.song_count(=offset+本页实际数)。 */
|
||||||
void appstate_apply_playlist_cover(int idx);
|
void appstate_apply_playlist_cover(int idx);
|
||||||
void appstate_set_playing_song(int idx, const char *cover_url);
|
void appstate_set_playing_song(int idx, const char *cover_url);
|
||||||
int appstate_current_playing(void); /* 返回当前播放歌曲下标(UI) */
|
int appstate_current_playing(void); /* 返回当前播放歌曲下标(UI) */
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ static int g_placked;
|
|||||||
static volatile int g_plloading;
|
static volatile int g_plloading;
|
||||||
static volatile int g_plloaded_idx;
|
static volatile int g_plloaded_idx;
|
||||||
static volatile int g_ploffset; /* 本页起始下标(0=首屏重头;>0=触底追加) */
|
static volatile int g_ploffset; /* 本页起始下标(0=首屏重头;>0=触底追加) */
|
||||||
static volatile int g_pl_more; /* 1=尚有下一页可加载(页满且未到 NCM_SONG_MAX) */
|
static volatile int g_pl_more; /* 1=尚有下一页可加载(song_count < track_ids_count 且未到 NCM_SONG_MAX) */
|
||||||
|
|
||||||
static void pl_drag_reset(void)
|
static void pl_drag_reset(void)
|
||||||
{
|
{
|
||||||
@@ -157,7 +157,7 @@ static void pl_draw_empty(void *ctx)
|
|||||||
if (g_load_err == 0 && g_st.song_count != 0) return;
|
if (g_load_err == 0 && g_st.song_count != 0) return;
|
||||||
if (g_plloading) { ncm_draw_empty(r_list, "LOADING", "正在加载曲目..."); return; }
|
if (g_plloading) { ncm_draw_empty(r_list, "LOADING", "正在加载曲目..."); return; }
|
||||||
if (g_load_err == NCM_ERR_AUTH) ncm_draw_empty(r_list, "SESSION EXPIRED", "请重新登录");
|
if (g_load_err == NCM_ERR_AUTH) ncm_draw_empty(r_list, "SESSION EXPIRED", "请重新登录");
|
||||||
else if (g_load_err) ncm_draw_empty(r_list, "LOAD FAILED", "返回后重试");
|
else if (g_load_err) ncm_draw_empty(r_list, "LOAD FAILED", "点按重试");
|
||||||
else ncm_draw_empty(r_list, "NO TRACKS", "这个歌单是空的");
|
else ncm_draw_empty(r_list, "NO TRACKS", "这个歌单是空的");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -197,12 +197,11 @@ static void *playlist_worker(void *arg)
|
|||||||
int rc;
|
int rc;
|
||||||
if (g_host_mock) rc = 0;
|
if (g_host_mock) rc = 0;
|
||||||
else if (my_idx < 0) rc = NCM_ERR_ARG;
|
else if (my_idx < 0) rc = NCM_ERR_ARG;
|
||||||
else rc = appstate_load_playlist_songs_page(my_idx, off, NCM_PLAYLIST_PAGE);
|
else rc = appstate_load_playlist_songs_page(my_idx, off, NCM_SONG_BATCH);
|
||||||
pthread_mutex_lock(&g_plmu);
|
pthread_mutex_lock(&g_plmu);
|
||||||
g_load_err = rc;
|
g_load_err = rc;
|
||||||
/* 页满(==NCM_PLAYLIST_PAGE)且未到记忆上限 → 还有下一页;否则到屏尾 */
|
/* 本页后仍有多余 trackIds(tail 未到)且未到内存上限 → 还有下一页;否则到屏尾。 */
|
||||||
int loaded = (rc == NCM_OK) ? (g_st.song_count - off) : 0;
|
g_pl_more = (rc == NCM_OK) && (g_st.song_count < g_st.track_ids_count) &&
|
||||||
g_pl_more = (rc == NCM_OK) && (loaded >= NCM_PLAYLIST_PAGE) &&
|
|
||||||
(g_st.song_count < NCM_SONG_MAX);
|
(g_st.song_count < NCM_SONG_MAX);
|
||||||
g_plloading = 0;
|
g_plloading = 0;
|
||||||
g_plloaded_idx = my_idx;
|
g_plloaded_idx = my_idx;
|
||||||
@@ -211,10 +210,11 @@ static void *playlist_worker(void *arg)
|
|||||||
return NULL;
|
return NULL;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 触底加载下一页:仅当仍有下一页、未在载、且未到 NCM_SONG_MAX 时发。 */
|
/* 触底加载下一页:仅当仍有下一页、未在载、未失败、未到 NCM_SONG_MAX 时发。 */
|
||||||
static void pl_maybe_load_more(void)
|
static void pl_maybe_load_more(void)
|
||||||
{
|
{
|
||||||
if (g_plthr_on || g_load_err != 0 || g_host_mock) return;
|
if (g_plthr_on || g_host_mock) return;
|
||||||
|
if (g_load_err != 0) return; /* 失败态:走点按重试,不自动循环 */
|
||||||
if (!g_pl_more) return;
|
if (!g_pl_more) return;
|
||||||
if (g_st.song_count >= NCM_SONG_MAX) return;
|
if (g_st.song_count >= NCM_SONG_MAX) return;
|
||||||
/* 视口底接近列表底(最后一行即将可见即预取) */
|
/* 视口底接近列表底(最后一行即将可见即预取) */
|
||||||
@@ -222,11 +222,11 @@ static void pl_maybe_load_more(void)
|
|||||||
int max_scroll = list_px - r_list.h;
|
int max_scroll = list_px - r_list.h;
|
||||||
if (max_scroll < 0) max_scroll = 0;
|
if (max_scroll < 0) max_scroll = 0;
|
||||||
if (meow_listview_scroll(g_list) < max_scroll - APP_ROW_H * 2) return;
|
if (meow_listview_scroll(g_list) < max_scroll - APP_ROW_H * 2) return;
|
||||||
g_ploffset = g_st.song_count;
|
g_ploffset = g_st.song_count; /* 下一页起始 = 已物化数 */
|
||||||
g_plloading = 1;
|
g_plloading = 1;
|
||||||
g_plthr_on = 1;
|
g_plthr_on = 1;
|
||||||
if (pthread_create(&g_plthr, NULL, playlist_worker, NULL) != 0) {
|
if (pthread_create(&g_plthr, NULL, playlist_worker, NULL) != 0) {
|
||||||
g_plthr_on = 0; g_plloading = 0; g_load_err = NCM_ERR_IO;
|
g_plthr_on = 0; g_plloading = 0; ncm_snack("加载失败");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -236,6 +236,7 @@ void playlist_refresh(void)
|
|||||||
if (g_plthr_on) return;
|
if (g_plthr_on) return;
|
||||||
g_load_err = 0;
|
g_load_err = 0;
|
||||||
g_st.song_count = 0;
|
g_st.song_count = 0;
|
||||||
|
g_st.track_ids_count = 0;
|
||||||
g_ploffset = 0;
|
g_ploffset = 0;
|
||||||
g_pl_more = 0;
|
g_pl_more = 0;
|
||||||
g_plloading = 1;
|
g_plloading = 1;
|
||||||
@@ -256,8 +257,18 @@ void playlist_tick(uint64_t now)
|
|||||||
g_plloading = 0;
|
g_plloading = 0;
|
||||||
if (g_plloaded_idx != g_st.active_playlist) { playlist_refresh(); return; }
|
if (g_plloaded_idx != g_st.active_playlist) { playlist_refresh(); return; }
|
||||||
if (g_load_err != 0) {
|
if (g_load_err != 0) {
|
||||||
if (g_ploffset == 0) g_st.song_count = 0; /* 首屏失败 → 整页空态 */
|
if (g_ploffset == 0) {
|
||||||
else { ncm_snack("加载失败"); g_load_err = 0; } /* 追加失败 → 保留已载歌曲 */
|
g_st.song_count = 0; /* 首屏失败 → 整页空态(点按重试) */
|
||||||
|
pl_apply(1);
|
||||||
|
} else {
|
||||||
|
/* 追加失败 → 保留已载歌曲 + 重置 g_pl_more(触底/点击可重试) */
|
||||||
|
ncm_snack("加载失败");
|
||||||
|
g_load_err = 0;
|
||||||
|
g_pl_more = (g_st.song_count < g_st.track_ids_count) &&
|
||||||
|
(g_st.song_count < NCM_SONG_MAX);
|
||||||
|
pl_apply(0);
|
||||||
|
}
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
pl_apply(g_ploffset == 0); /* 首屏回顶;触底追加保持滚动 */
|
pl_apply(g_ploffset == 0); /* 首屏回顶;触底追加保持滚动 */
|
||||||
}
|
}
|
||||||
@@ -289,7 +300,14 @@ int playlist_on_tap(int x, int y)
|
|||||||
if (x >= r_cmt.x && x < r_cmt.x + r_cmt.w && y >= r_cmt.y && y < r_cmt.y + r_cmt.h) {
|
if (x >= r_cmt.x && x < r_cmt.x + r_cmt.w && y >= r_cmt.y && y < r_cmt.y + r_cmt.h) {
|
||||||
pl_drag_reset(); ncm_snack("评论功能开发中"); return 1;
|
pl_drag_reset(); ncm_snack("评论功能开发中"); return 1;
|
||||||
}
|
}
|
||||||
if (g_load_err != 0 || g_st.song_count == 0) return 0;
|
if (g_load_err != 0) {
|
||||||
|
/* 失败态(LOAD FAILED / SESSION EXPIRED / NETWORK ERROR):点按内容区重试。 */
|
||||||
|
pl_drag_reset();
|
||||||
|
meow_ui_invalidate(r_list.x, r_list.y, r_list.w, r_list.h);
|
||||||
|
playlist_refresh();
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
if (g_st.song_count == 0) return 0; /* 空歌单(无失败):不重试 */
|
||||||
if (meow_widget_hit(g_list, x, y)) {
|
if (meow_widget_hit(g_list, x, y)) {
|
||||||
/* 误触修复:小拖动(<SWIPE_THRESH)会先滚动列表、release 又被 SDK 判成 TAP,旧的
|
/* 误触修复:小拖动(<SWIPE_THRESH)会先滚动列表、release 又被 SDK 判成 TAP,旧的
|
||||||
* meow_listview_hit(x,y) 用“释放点 + 更新后的 scroll”命中 → 打到“已移动到手指下”的行,
|
* meow_listview_hit(x,y) 用“释放点 + 更新后的 scroll”命中 → 打到“已移动到手指下”的行,
|
||||||
|
|||||||
@@ -0,0 +1,144 @@
|
|||||||
|
/* bench/pagination_sim.c — 三期第一批 歌单分页 离线状态机验证(0 网络请求)。
|
||||||
|
*
|
||||||
|
* 直接驱动 App 的真实函数 appstate_load_playlist_songs_page()(app/logic/appstate.c),
|
||||||
|
* 仅把两个网络端点(ncm_api_playlist_detail / ncm_api_song_detail)替换成本地 stub
|
||||||
|
* (合成 trackIds / 合成歌曲对象),验证 detail 后按页 song_detail 的累加/封顶/尾部语义:
|
||||||
|
* ① offset=0 重建索引(song_ids[]←全量 trackIds,track_ids_count)并载第 0 页;
|
||||||
|
* ② 逐页 offset 前进,song_count 单调累加;
|
||||||
|
* ③ 尾部(song_count==track_ids_count)不再载下一页;④ NCM_SONG_MAX 封顶不越界。
|
||||||
|
* 风控纪律:本验证 0 次真实 NCM 请求(离线 stub)。歌曲上限 = NCM_SONG_MAX(内存封顶);
|
||||||
|
* 本模拟用两种歌单规模:(a) 130 首(< 上限,读全程);(b) 5191 首(> 上限,读满 NCM_SONG_MAX 截断)。
|
||||||
|
*
|
||||||
|
* 编译(宿主):$CC_HOST -O2 -ffunction-sections -DJSMN_HEADER -I. -I../sdk -I../meow_launcherd \
|
||||||
|
* -Icore -I../vendor/jsmn -c bench/pagination_sim.c -o /tmp/ps.o
|
||||||
|
* $CC_HOST -O2 -ffunction-sections -DJSMN_HEADER -I. -I../sdk -I../meow_launcherd \
|
||||||
|
* -Icore -I../vendor/jsmn -c app/logic/appstate.c -o /tmp/ps_app.o
|
||||||
|
* $CC_HOST -O2 -Wl,--gc-sections -o pagination_sim /tmp/ps.o /tmp/ps_app.o -lpthread
|
||||||
|
* 运行:./pagination_sim (0 网络;PASS/FAIL 退出码)
|
||||||
|
*/
|
||||||
|
#include "../app/ncm_app_internal.h"
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <string.h>
|
||||||
|
#include <stdlib.h>
|
||||||
|
|
||||||
|
/* appstate.c 依赖的全局(app.c 提供;本 bench 只测分页路径,仅 g_st 被用到) */
|
||||||
|
ncm_app_state_t g_st;
|
||||||
|
ncm_play_run_t g_play = { .mu = PTHREAD_MUTEX_INITIALIZER, .state = NCM_PC_STOPPED, .cur_song = -1 };
|
||||||
|
volatile int g_app_quit = 0;
|
||||||
|
volatile int g_net_ready = 0;
|
||||||
|
int g_host_mock = 0;
|
||||||
|
|
||||||
|
/* ncm_strlcpy:appstate_apply_playlist_cover 用到(从 core ncm_common 借;此处最小内联实现) */
|
||||||
|
size_t ncm_strlcpy(char *dst, const char *src, size_t sz)
|
||||||
|
{
|
||||||
|
size_t n = 0;
|
||||||
|
if (sz) {
|
||||||
|
while (src[n] && n < sz - 1) { dst[n] = src[n]; n++; }
|
||||||
|
dst[n] = 0;
|
||||||
|
}
|
||||||
|
while (src[n]) n++;
|
||||||
|
return n;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---- 网络端点 stub(离线,合成数据) ---- */
|
||||||
|
static int g_pid_total; /* detail 应返回的 trackIds 总数(供测试注入) */
|
||||||
|
static int g_detail_calls;
|
||||||
|
static int g_song_calls;
|
||||||
|
|
||||||
|
int ncm_api_playlist_detail(const char *device_id, const char *cookie, const char *playlist_id,
|
||||||
|
char *name, size_t name_sz, char *cover, size_t cover_sz,
|
||||||
|
int64_t *ids, int id_cap, int *out_id_n)
|
||||||
|
{
|
||||||
|
(void)device_id; (void)cookie; (void)cover_sz;
|
||||||
|
g_detail_calls++;
|
||||||
|
if (name) snprintf(name, name_sz, "Sim Playlist");
|
||||||
|
if (cover) cover[0] = 0;
|
||||||
|
int n = (g_pid_total < id_cap) ? g_pid_total : id_cap;
|
||||||
|
for (int i = 0; i < n; i++) ids[i] = 1000000LL + i; /* 合成连续 id */
|
||||||
|
*out_id_n = n;
|
||||||
|
return NCM_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
int ncm_api_song_detail(const char *device_id, const char *cookie, const int64_t *ids, int n,
|
||||||
|
ncm_song_t *out, int max, int *out_n)
|
||||||
|
{
|
||||||
|
(void)device_id; (void)cookie;
|
||||||
|
g_song_calls++;
|
||||||
|
int m = (n < max) ? n : max;
|
||||||
|
for (int i = 0; i < m; i++) {
|
||||||
|
snprintf(out[i].id, sizeof out[i].id, "%lld", (long long)ids[i]);
|
||||||
|
snprintf(out[i].name, sizeof out[i].name, "Song%lld", (long long)ids[i]);
|
||||||
|
snprintf(out[i].art_name, sizeof out[i].art_name, "Artist%lld", (long long)ids[i]);
|
||||||
|
snprintf(out[i].al_pic, sizeof out[i].al_pic, "http://p.music.126.net/cov%lld.jpg", (long long)ids[i]);
|
||||||
|
}
|
||||||
|
*out_n = m;
|
||||||
|
return NCM_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---- 驱动:整批载入到尾部/上限,返回最终 song_count ---- */
|
||||||
|
static int load_all(int pid_total, int *detail_calls, int *song_calls)
|
||||||
|
{
|
||||||
|
memset(&g_st, 0, sizeof g_st);
|
||||||
|
g_st.playlist_count = 1;
|
||||||
|
snprintf(g_st.playlists[0].id, sizeof g_st.playlists[0].id, "pl0");
|
||||||
|
g_st.playlists[0].track_count = pid_total;
|
||||||
|
g_pid_total = pid_total;
|
||||||
|
g_detail_calls = 0; g_song_calls = 0;
|
||||||
|
|
||||||
|
int rc = appstate_load_playlist_songs_page(0, 0, NCM_SONG_BATCH);
|
||||||
|
if (rc != NCM_OK) { printf(" page0 rc=%d\n", rc); return -1; }
|
||||||
|
int off = g_st.song_count;
|
||||||
|
while (off < g_st.track_ids_count && off < NCM_SONG_MAX) {
|
||||||
|
rc = appstate_load_playlist_songs_page(0, off, NCM_SONG_BATCH);
|
||||||
|
if (rc != NCM_OK) { printf(" page(%d) rc=%d\n", off, rc); return -1; }
|
||||||
|
int nxt = g_st.song_count;
|
||||||
|
if (nxt <= off) break;
|
||||||
|
off = nxt;
|
||||||
|
}
|
||||||
|
if (detail_calls) *detail_calls = g_detail_calls;
|
||||||
|
if (song_calls) *song_calls = g_song_calls;
|
||||||
|
return g_st.song_count;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int g_fail = 0;
|
||||||
|
#define CHECK(cond, fmt, ...) do { \
|
||||||
|
if (cond) printf(" PASS " fmt "\n", ##__VA_ARGS__); \
|
||||||
|
else { printf(" FAIL " fmt "\n", ##__VA_ARGS__); g_fail = 1; } \
|
||||||
|
} while (0)
|
||||||
|
|
||||||
|
int main(void)
|
||||||
|
{
|
||||||
|
/* (a) 130 首(< NCM_SONG_MAX):应读全程,track_ids_count=130,song_count=130,尾部停。 */
|
||||||
|
printf("== case A: 130 songs (reads to tail) ==\n");
|
||||||
|
int dc = 0, sc = 0;
|
||||||
|
int n = load_all(130, &dc, &sc);
|
||||||
|
CHECK(n == 130, "song_count=%d (expect 130)", n);
|
||||||
|
CHECK(g_st.track_ids_count == 130, "track_ids_count=%d (expect 130)", g_st.track_ids_count);
|
||||||
|
CHECK(dc == 1, "detail calls=%d (expect 1, index fetched once)", dc);
|
||||||
|
CHECK(sc == 2, "song_detail calls=%d (expect 2 = 100 + 30 pages)", sc);
|
||||||
|
CHECK(strcmp(g_st.songs[129].name, "Song1000129") == 0, "last song name='%s'", g_st.songs[129].name);
|
||||||
|
CHECK(g_st.songs[129].al_pic[0] != 0, "last song has al_pic='%s'", g_st.songs[129].al_pic);
|
||||||
|
/* 越过尾部再 load:应无操作(不越界) */
|
||||||
|
int rc = appstate_load_playlist_songs_page(0, 130, NCM_SONG_BATCH);
|
||||||
|
CHECK(rc == NCM_OK && g_st.song_count == 130, "load past tail: rc=%d song_count=%d", rc, g_st.song_count);
|
||||||
|
|
||||||
|
/* (b) 5191 首(> NCM_SONG_MAX):索引封顶 NCM_SONG_MAX,读满封顶截断。 */
|
||||||
|
printf("== case B: 5191 songs (capped at NCM_SONG_MAX) ==\n");
|
||||||
|
dc = sc = 0;
|
||||||
|
n = load_all(5191, &dc, &sc);
|
||||||
|
CHECK(n == NCM_SONG_MAX, "song_count=%d (expect NCM_SONG_MAX=%d)", n, NCM_SONG_MAX);
|
||||||
|
CHECK(g_st.track_ids_count == NCM_SONG_MAX, "track_ids_count=%d (index capped)", g_st.track_ids_count);
|
||||||
|
CHECK(dc == 1, "detail calls=%d", dc);
|
||||||
|
CHECK(sc == NCM_SONG_MAX / NCM_SONG_BATCH, "song_detail calls=%d (expect %d full pages)", sc, NCM_SONG_MAX / NCM_SONG_BATCH);
|
||||||
|
/* 越界再 load:应 NCM_OK 但不动 song_count */
|
||||||
|
rc = appstate_load_playlist_songs_page(0, NCM_SONG_MAX, NCM_SONG_BATCH);
|
||||||
|
CHECK(rc == NCM_OK && g_st.song_count == NCM_SONG_MAX, "load past cap: rc=%d song_count=%d", rc, g_st.song_count);
|
||||||
|
|
||||||
|
/* (c) 空歌单(0 首) */
|
||||||
|
printf("== case C: empty playlist ==\n");
|
||||||
|
n = load_all(0, &dc, &sc);
|
||||||
|
CHECK(n == 0 && g_st.track_ids_count == 0, "empty: song_count=%d track_ids_count=%d", n, g_st.track_ids_count);
|
||||||
|
|
||||||
|
printf("%s\n", g_fail ? "RESULT: FAIL" : "RESULT: PASS");
|
||||||
|
return g_fail ? 1 : 0;
|
||||||
|
}
|
||||||
@@ -19,7 +19,7 @@ INC="-I. -I../sdk -I../meow_launcherd -Icore \
|
|||||||
echo "==> host SDK objects"
|
echo "==> host SDK objects"
|
||||||
SDK_SRC="meow_ui_gfx meow_ui_sys meow_ui_theme meow_ui_vol meow_ui_log meow_ui_widget \
|
SDK_SRC="meow_ui_gfx meow_ui_sys meow_ui_theme meow_ui_vol meow_ui_log meow_ui_widget \
|
||||||
meow_ui_input meow_ui_activity meow_ui_dialog meow_ui_ime meow_ui_draw meow_ui_view \
|
meow_ui_input meow_ui_activity meow_ui_dialog meow_ui_ime meow_ui_draw meow_ui_view \
|
||||||
meow_ui_img meow_ui_config meow_ui_nav meow_ui_media meow_notify_ipc"
|
meow_ui_img meow_ui_config meow_ui_nav meow_ui_media meow_notify_ipc meow_pinyin"
|
||||||
for f in $SDK_SRC; do
|
for f in $SDK_SRC; do
|
||||||
$CC_HOST -O2 -c -DMEOW_UI_HOST_TEST -I"$SDK_DIR" -I"$ROOT/patch_v2/meow_launcherd" \
|
$CC_HOST -O2 -c -DMEOW_UI_HOST_TEST -I"$SDK_DIR" -I"$ROOT/patch_v2/meow_launcherd" \
|
||||||
"$SDK_DIR/$f.c" -o "$OUT/host/sdk_$f.o" || { echo "sdk $f FAILED"; exit 1; }
|
"$SDK_DIR/$f.c" -o "$OUT/host/sdk_$f.o" || { echo "sdk $f FAILED"; exit 1; }
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
Reference in new issue
Block a user