feat(ncm): 三期第二批 — 中文搜索接拼音IME+设置页做好(音质全档/缓存上限可设数字+清理缓存/关于)+缓存LRU淘汰接生产路径(写前写后+手动调低上限)
- 搜索页输入框 MEOW_IME_ASCII→MEOW_IME_PINYIN: 候选上屏UTF-8经 on_change 实时回写 g_st.search_query, 键盘「拼/英」「中」切换, 英文零回归; 搜索仍仅在点「搜索」时发 cloudsearch(风控) - 设置页 5 行: 音质(Hi-Res 默认,Lossless/Exhigh/Higher/Standard 全档,core 既有降级链)/缓存上限(1~100GB 预设+「自定义」NUM IME 数字输入,默认10上限100)/清理缓存(显示当前占用,ncm_cache_clear)/退出登录/关于行 - 缓存淘汰接线: ncm_cache_evict 进生产路径 — player_manager(下载写前/写后)+cover(写前/写后)+appstate_save_settings(手动调低上限立即淘汰); 封面与 mp3 同池; 上限读设置值 - core 新增 ncm_cache_clear; 设置持久化走 meow_config_* → MeowData/dev.player.ncm/data - bench/cache_evict_sim.c 离线回归(0网络) 6 组 PASS; pagination_sim/core(host+mipsel/qemu)/6页宿主渲染 全过; mipsel -Wall -Wextra 0 warning (未动 sdk/、meow_launcherd/、vendor/;0 真实 NCM 请求;真机部署留用户)
This commit is contained in:
1 parent
1a239c5eab
commit
f23720f934
13 files changed
+469
-33
No files matched your search
@@ -1050,3 +1050,123 @@ c73366373b3a372d96b2218e528c6381 build_ncm_host.sh
|
|||||||
| `NCM_SONG_BATCH` | 64 | 100 | song_detail 每批(≤500 安全窗内保守 100;`clist[4096]` 装 100 首 ≈2KB) |
|
| `NCM_SONG_BATCH` | 64 | 100 | song_detail 每批(≤500 安全窗内保守 100;`clist[4096]` 装 100 首 ≈2KB) |
|
||||||
| `NCM_PLAYLIST_PAGE` | 50 | 删除 | track/all 服务端分页遗留(该端点 404),改由 `NCM_SONG_BATCH` 承担页粒度 |
|
| `NCM_PLAYLIST_PAGE` | 50 | 删除 | track/all 服务端分页遗留(该端点 404),改由 `NCM_SONG_BATCH` 承担页粒度 |
|
||||||
| `g_st.track_ids_count` | — | 新增 | detail 取回 trackIds 总数,驱动「还有下一页」判据 |
|
| `g_st.track_ids_count` | — | 新增 | detail 取回 trackIds 总数,驱动「还有下一页」判据 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 15. 三期第二批:中文搜索接入拼音 IME + 设置页做好 + 缓存淘汰接线(2026-08-31,agent-241)
|
||||||
|
|
||||||
|
> 范围:仅 `patch_v2/ncm_app/`(含 `core/`)。未动 `sdk/`(主会话在改 `meow_ui_theme.c`/测试,另有 agent 在改
|
||||||
|
> `meow_launcherd/` 主题)与 `meow_launcherd/`。风控纪律:本批真实 NCM 请求 **0 次**(全部离线 stub/宿主 mock/
|
||||||
|
> 已落盘样本)。部署/验收留用户(真机验收点见 §15.7)。上一批(歌单分页/封面/歌词锁)已 commit `ee104f1`,
|
||||||
|
> 本批在其上做;提交见 git。
|
||||||
|
|
||||||
|
### 15.1 中文搜索接入拼音 IME
|
||||||
|
|
||||||
|
- 搜索页(`app/ui/search.c`)输入框 `meow_textfield_create(..., MEOW_IME_ASCII)` 改 **`MEOW_IME_PINYIN`**。
|
||||||
|
SDK `MEOW_IME_PINYIN`(M2.88 agent-229 落地)打开后为 4 行字母键 + 组合条 + 候选栏(≤6 候选/页,翻页 +
|
||||||
|
「清」+「拼/英」键),候选上屏走 `py_commit` UTF-8 写进字段并触发 `on_change`(TextField 的
|
||||||
|
`textfield_change_cb` 实时回写 `g_st.search_query` + `invalidate`)。
|
||||||
|
- **候选上屏/清空/退格语义**均沿用 SDK IME(`py_commit`/`py_clear`/`py_back_combo`+`py_back_field`,组合态
|
||||||
|
UTF-8 安全退格 = 删末字;组合非空时退格删组合末字母),App 侧零改动。
|
||||||
|
- **英文输入零回归**:拼音键盘含「拼/英」键(`K_SWITCH`→ASCII)与 ASCII 键盘顶条「中」热区(→拼音),
|
||||||
|
ASCII/NUM 不随本批改动;搜索仍只在用户点「搜索」按钮时发 `cloudsearch`(风控:进页不自动搜,不逐键发)。
|
||||||
|
- 保留 `search_submit`/`search_worker`(后台线程 cloudsearch,UI 不卡);`ncm_api_cloudsearch` 的 keyword
|
||||||
|
为 UTF-8 字符串,中文关键词直接进 weapi 加密体(无需改造)。
|
||||||
|
|
||||||
|
### 15.2 设置页做好(用户定案:音质/缓存上限可设数字+清理缓存+关于)
|
||||||
|
|
||||||
|
`app/ui/settings.c` 重做为 **5 行**:音质 / 缓存上限 / 清理缓存 / 退出登录 / 关于网易云。
|
||||||
|
|
||||||
|
- **音质**:sheet 列出全 5 档(Hi-Res/Lossless/Exhigh/Higher/Standard,默认 Hi-Res)。单曲不可用时
|
||||||
|
**沿用 core 既有降级链**(`ncm_api_song_url_auto`:hires→lossless→exhigh→higher→standard,`ncm_api.c:260`),
|
||||||
|
本批未改 core 降级逻辑。
|
||||||
|
- **缓存上限**:sheet 预设(1/2/5/10/20/30/50/100 GB)+「自定义...」→ INPUT 弹窗 + **NUM IME 数字输入**
|
||||||
|
(`input_cache_limit_gb()`,atoi + 钳制 1~100)。默认 10GB,上限 100GB。持久化走既有
|
||||||
|
`appstate_save_settings`(`meow_config_*` → `<env MEOW_DATA_DIR>/ncm.cache_limit_gb.cfg`,即
|
||||||
|
`MeowData/dev.player.ncm/data/`,与 core `ncm_cache_load_setting`/`ncm_store_read_cfg_int` 同目录同键)。
|
||||||
|
- **清理缓存(新)**:显示当前占用(`ncm_cache_total_bytes`,进页时算一次存 `g_cache_used`,清理后清零);
|
||||||
|
tap → CONFIRM 弹窗 → `ncm_cache_clear`(新 core API)→ snack「缓存已清理」。占用用整数运算格式化
|
||||||
|
(`fmt_bytes`,GB/MB/KB,不用浮点/软浮点)。
|
||||||
|
- **退出登录 / 关于/版本行**:保留;关于 tap 改为 snack「网易云音乐 v0.1 · MeowOS 移植版」。
|
||||||
|
|
||||||
|
### 15.3 缓存淘汰接线(ncm_cache_evict 接进生产路径)
|
||||||
|
|
||||||
|
- **core 新增 `ncm_cache_clear`**(`core/ncm_cache.c/.h`):删除 `song_*` 全部(mp3 与封面同池,含残留
|
||||||
|
`.tmp`),供「清理缓存」与测试用;`ncm_cache_evict`(既有)保持 LRU=按 mtime 升序删最旧直到 `<= limit`。
|
||||||
|
- **mp3 下载路径**(`app/play/player_manager.c song_local_file`):**写前**(`ncm_cache_open` 前)与**写后**
|
||||||
|
(`ncm_cache_commit` 后)各调一次 `ncm_cache_evict(&g_st.ctx.cache)`——写前腾空间(此时本线程 temp 尚未建,
|
||||||
|
无在途文件被误删风险),写后清「新文件推过上限」的增量。刚写的文件 mtime 最新,LRU 淘汰时存活。
|
||||||
|
- **封面路径**(`app/logic/cover.c` cover_worker 未命中写盘分支):写前 + 写后同样各调一次 `ncm_cache_evict`
|
||||||
|
(封面 `song_cov<hash>..jpg` 与 mp3 同池)。
|
||||||
|
- **手动调低上限**:`appstate_save_settings` 末尾加一次 `ncm_cache_evict`,用户从 50GB 降到 1GB 立即淘汰。
|
||||||
|
- 上限读设置值:`ncm_cache_set_limit_gb(&g_st.ctx.cache, g_st.cache_limit_gb)`(`player_manager_set_quality`
|
||||||
|
与 `appstate_save_settings` 均已同步,继 `appstate_load_settings`/`ncm_cache_load_setting`)。
|
||||||
|
|
||||||
|
### 15.4 验证记录(0 真实 NCM 请求)
|
||||||
|
|
||||||
|
| 项 | 方式 | 结果 |
|
||||||
|
|---|---|---|
|
||||||
|
| 缓存 LRU 淘汰/清空 | `bench/cache_evict_sim.c` 离线(0 网络,稀疏文件构造,直接驱动 core) | **全 PASS**:①超上限删最旧(700+300+400MB→删 700 停于 700MB)②删 2 个最旧到 ≤1GB(4×500MB→留 2)③未超上限无操作 ④`lookup` 刷新 mtime → 被访问的 A 存活、B/C 被淘汰 ⑤`set_limit_gb` 钳制(0→1、200→100)⑥`clear` 删 mp3+封面+残留 .tmp(total=0)|
|
||||||
|
| 歌单分页回归 | `pagination_sim`(0 网络) | **全 PASS**(130/5191/空 三态无回归;`appstate.c` 加 `ncm_cache_evict` 未破坏分页——`--gc-sections` 丢弃不可达的 `save_settings`,链接不受影响) |
|
||||||
|
| core 宿主测试 | `test_ncm_core_host`(重建 core-host) | **PASS** |
|
||||||
|
| core mipsel 测试 | `qemu-mipsel ./out/test_ncm_core_mipsel` | **PASS** |
|
||||||
|
| mipsel 构建 | `bash build_ncm.sh`(自有代码 `-Wall -Wextra`) | **0 warning**;`text 844640 / data 856 / bss 2052068` |
|
||||||
|
| 宿主离屏渲染 | `build_ncm_host.sh` + `MEOW_UI_DUMP`(mock,0 网络) | settings(明/暗 5 行 + 占用 + 版本 + 品牌红脚注)/search(输入框+搜索钮+footer)/home/playlist/player 均正常渲染,无崩溃 |
|
||||||
|
|
||||||
|
### 15.5 md5(本批改动文件 + 产物)
|
||||||
|
|
||||||
|
```
|
||||||
|
8a313d27dc7d05fa3a78ce7772436b04 ncm # mipsel 静态 App(新)
|
||||||
|
dc66bdcfb29ba5717b42c72ecbf45049 ncm_host # 宿主离屏调试二进制(重编)
|
||||||
|
be3d906edce4d90ebd9c09d3b7faf814 core/ncm_cache.c # 新增 ncm_cache_clear
|
||||||
|
f5810d23709080f4fa39b2fbc242c9e0 core/ncm_cache.h
|
||||||
|
41ae3f40014f68f7855fed3a0be1558c app/ui/settings.c # 设置页 5 行 + 自定义数字 + 清理缓存
|
||||||
|
9811af8f7150b325225e067748ca50a3 app/ui/search.c # 输入框 MEOW_IME_PINYIN
|
||||||
|
2350622237e9fb5d05a5497dd8b87e39 app/play/player_manager.c # mp3 写前/写后淘汰
|
||||||
|
fb1a74a68950b26a2a4f42281725f37d app/logic/cover.c # 封面写前/写后淘汰
|
||||||
|
a51b6d8477bde5692089caf511eafca1 app/logic/appstate.c # save_settings 末触发淘汰
|
||||||
|
ed2f71c52563e812a04fb8a7cc05148d app/test/test_ncm_app.c # settings mock 初始化缓存目录
|
||||||
|
2134ad23a6ee4d1b37120ee8d2e44337 bench/cache_evict_sim.c # 新增 LRU/清空离线回归
|
||||||
|
```
|
||||||
|
|
||||||
|
### 15.6 出入清单(只改 `patch_v2/ncm_app/`)
|
||||||
|
|
||||||
|
```
|
||||||
|
修改:
|
||||||
|
app/ui/settings.c # 设置页 5 行:音质(全5档)/缓存上限(预设+自定义数字)/清理缓存(显示占用)/退出/关于
|
||||||
|
app/ui/search.c # 搜索输入框 MEOW_IME_ASCII → MEOW_IME_PINYIN(中文搜索,英文零回归)
|
||||||
|
app/play/player_manager.c # song_local_file 写前/写后 ncm_cache_evict
|
||||||
|
app/logic/cover.c # cover_worker 未命中写盘分支 写前/写后 ncm_cache_evict
|
||||||
|
app/logic/appstate.c # appstate_save_settings 末尾触发 ncm_cache_evict(手动调低上限立即淘汰)
|
||||||
|
core/ncm_cache.c/.h # 新增 ncm_cache_clear(删 song_* 全部)
|
||||||
|
app/test/test_ncm_app.c # settings mock 初始化 ctx.cache(「占用」行读真实目录)
|
||||||
|
ncm / ncm_host / test/test_ncm_core_host # 重编产物
|
||||||
|
新增:
|
||||||
|
bench/cache_evict_sim.c # 缓存 LRU 淘汰/清空 离线回归(0 网络)
|
||||||
|
REPORT.md # 本批记录(§15)
|
||||||
|
(未动 sdk/、meow_launcherd/、vendor/;未动设备系统/恢复区;未 killall hiby_player / 未重启设备)
|
||||||
|
```
|
||||||
|
|
||||||
|
### 15.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'`)。真机验收点:
|
||||||
|
① 搜索页输入拼音→候选上屏中文→点「搜索」出中文结果(键盘「拼/英」「中」「退格」「清」语义);
|
||||||
|
② 设置页缓存上限「自定义」数字键盘输入 1~100、改上限后立即 LRU 淘汰;③ 清理缓存显示真实占用、(超上限
|
||||||
|
磁盘占用下降);④ 音质默认 Hi-Res、VIP/版权曲逐档降级播放。
|
||||||
|
- **`song_<id>..mp3` 双点命名是 core 既有约定**(`ncm_cache_path` 用 `"song_%s.%s"` + ext 带前导点 `.mp3`)
|
||||||
|
——本批未改(改动它需动 core 缓存写入路径,风险大于收益);`cache_evict_sim.c` 已按此约定构造文件,
|
||||||
|
验证的正是真实生产路径。
|
||||||
|
- **跨线程淘汰的极小竞态**:cover 线程写盘时,player 线程的下载 temp(`song_<id>..mp3.tmp`)也匹配
|
||||||
|
`song_` 前缀;但 in-progress .tmp 的 mtime 是「刚写」=最新,LRU 升序下排最末,正常淘汰只删最旧,
|
||||||
|
不会先删它。仅在「池严重超上限、删净旧文件后仍超」的极端情形才可能触及其(上限 ≥1GB,现实中几乎不
|
||||||
|
发生);沿用既有「双线程同池无锁」模型(本批未扩范围,未引入新锁)。
|
||||||
|
- **`ncm_cache_evict` 上限读 `&g_st.ctx.cache`**:宿主 settings mock 未 `ncm_ctx_init`(只 init cache dir),
|
||||||
|
`ctx.cache.dir` 为空 → 占用行显示 0(渲染验证用);真机 `appstate_init`→`ncm_ctx_init` 后为真实目录。
|
||||||
|
|
||||||
|
### 15.8 数值权衡(供后续微调)
|
||||||
|
|
||||||
|
| 常量 | 旧值 | 新值 | 说明 |
|
||||||
|
|---|---|---|---|
|
||||||
|
| 设置页行数 | 4 | 5 | 新增「清理缓存」行并在「缓存上限」接「自定义」数字输入 |
|
||||||
|
| 音质 sheet 档数 | 3(Hi-Res/Higher/Standard) | 5(+Lossless/Exhigh) | 对齐 core `NCM_LEVEL_*` 全档 + 降级链起点 |
|
||||||
|
| `ncm_cache_clear` | — | 新增 | 删除 `song_*` 全部(mp3/封面/.tmp),供「清理缓存」与测试 |
|
||||||
@@ -57,6 +57,8 @@ void appstate_save_settings(void)
|
|||||||
meow_config_set("ncm.level", b);
|
meow_config_set("ncm.level", b);
|
||||||
ncm_cache_set_limit_gb(&g_st.ctx.cache, g_st.cache_limit_gb);
|
ncm_cache_set_limit_gb(&g_st.ctx.cache, g_st.cache_limit_gb);
|
||||||
g_st.ctx.default_level = g_st.level;
|
g_st.ctx.default_level = g_st.level;
|
||||||
|
/* 手动调低上限时立即按 LRU 淘汰(超上限删最旧);未超限则为无操作 */
|
||||||
|
ncm_cache_evict(&g_st.ctx.cache);
|
||||||
}
|
}
|
||||||
|
|
||||||
void appstate_set_logged_in(const char *uid, const char *nickname)
|
void appstate_set_logged_in(const char *uid, const char *nickname)
|
||||||
|
|||||||
@@ -106,10 +106,14 @@ static void *cover_worker(void *arg)
|
|||||||
img = decode_cover(url, &blen);
|
img = decode_cover(url, &blen);
|
||||||
if (!img) { usleep(500 * 1000); continue; }
|
if (!img) { usleep(500 * 1000); continue; }
|
||||||
if (g_st.ctx.cache.dir[0]) {
|
if (g_st.ctx.cache.dir[0]) {
|
||||||
|
/* 写前检查:池大小超限先按 LRU 淘汰(与 mp3 同池;封面约 200KB,先腾空间) */
|
||||||
|
ncm_cache_evict(&g_st.ctx.cache);
|
||||||
ncm_cache_writer_t *w = NULL;
|
ncm_cache_writer_t *w = NULL;
|
||||||
if (ncm_cache_open(&g_st.ctx.cache, key, ".jpg", &w) == NCM_OK) {
|
if (ncm_cache_open(&g_st.ctx.cache, key, ".jpg", &w) == NCM_OK) {
|
||||||
ncm_cache_write(w, g_buf, blen);
|
ncm_cache_write(w, g_buf, blen);
|
||||||
ncm_cache_commit(w);
|
ncm_cache_commit(w);
|
||||||
|
/* 写后检查:新封面可能把池推过上限 → 再按 LRU 淘汰最旧 */
|
||||||
|
ncm_cache_evict(&g_st.ctx.cache);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -120,6 +120,9 @@ static int song_local_file(int idx, char *path, size_t psz, int64_t *bytes)
|
|||||||
if (rc != NCM_OK) return rc;
|
if (rc != NCM_OK) return rc;
|
||||||
if (!u.url[0]) return NCM_ERR_NOTFOUND;
|
if (!u.url[0]) return NCM_ERR_NOTFOUND;
|
||||||
|
|
||||||
|
/* 写前检查:池大小超限先按 LRU 淘汰(此线程尚未开 temp,无在途文件被误删风险) */
|
||||||
|
ncm_cache_evict(&g_st.ctx.cache);
|
||||||
|
|
||||||
/* 流式下载到缓存 temp → commit(稳妥者:先落盘再播) */
|
/* 流式下载到缓存 temp → commit(稳妥者:先落盘再播) */
|
||||||
ncm_cache_writer_t *w = NULL;
|
ncm_cache_writer_t *w = NULL;
|
||||||
rc = ncm_cache_open(&g_st.ctx.cache, id, ".mp3", &w);
|
rc = ncm_cache_open(&g_st.ctx.cache, id, ".mp3", &w);
|
||||||
@@ -143,6 +146,8 @@ static int song_local_file(int idx, char *path, size_t psz, int64_t *bytes)
|
|||||||
}
|
}
|
||||||
rc = ncm_cache_commit(w);
|
rc = ncm_cache_commit(w);
|
||||||
if (rc != 0) return rc;
|
if (rc != 0) return rc;
|
||||||
|
/* 写后检查:新落盘可能把池推过上限 → 再按 LRU 淘汰最旧(刚写的 mtime 最新,存活) */
|
||||||
|
ncm_cache_evict(&g_st.ctx.cache);
|
||||||
if (ncm_cache_lookup(&g_st.ctx.cache, id, ".mp3", path, psz) != NCM_OK)
|
if (ncm_cache_lookup(&g_st.ctx.cache, id, ".mp3", path, psz) != NCM_OK)
|
||||||
return NCM_ERR_NOENT;
|
return NCM_ERR_NOENT;
|
||||||
struct stat st;
|
struct stat st;
|
||||||
|
|||||||
@@ -145,6 +145,8 @@ int main(int argc, char **argv)
|
|||||||
ncm_strlcpy(g_st.nickname, "Tester", sizeof g_st.nickname);
|
ncm_strlcpy(g_st.nickname, "Tester", sizeof g_st.nickname);
|
||||||
g_st.level = NCM_LEVEL_HIRES;
|
g_st.level = NCM_LEVEL_HIRES;
|
||||||
g_st.cache_limit_gb = 10;
|
g_st.cache_limit_gb = 10;
|
||||||
|
ncm_cache_init(&g_st.ctx.cache, g_st.cache_dir); /* 初始化缓存目录(「占用」行读取)*/
|
||||||
|
ncm_cache_set_limit_gb(&g_st.ctx.cache, g_st.cache_limit_gb);
|
||||||
} else if (strcmp(name, "search") == 0) {
|
} else if (strcmp(name, "search") == 0) {
|
||||||
mock_playlists();
|
mock_playlists();
|
||||||
static const char *sn[] = { "Song One", "Song Two", "Song Three" };
|
static const char *sn[] = { "Song One", "Song Two", "Song Three" };
|
||||||
|
|||||||
@@ -235,9 +235,11 @@ static void sr_page_create(meow_view *v, void *ctx)
|
|||||||
meow_ui_activity_push(ACT_SEARCH); /* owner=ACT_SEARCH */
|
meow_ui_activity_push(ACT_SEARCH); /* owner=ACT_SEARCH */
|
||||||
g_tb = ncm_topbar_create("搜索", 1, NULL);
|
g_tb = ncm_topbar_create("搜索", 1, NULL);
|
||||||
if (!g_st.search_query[0]) ncm_strlcpy(g_st.search_query, "", sizeof g_st.search_query);
|
if (!g_st.search_query[0]) ncm_strlcpy(g_st.search_query, "", sizeof g_st.search_query);
|
||||||
|
/* 中文搜索:IME 切 MEOW_IME_PINYIN(拼音候选上屏 UTF-8 到 g_st.search_query,经 on_change 实时回写;
|
||||||
|
* 键盘「拼/英」键可切回 ASCII 输英文,「中」热区回拼音——英文零回归)。 */
|
||||||
g_field = meow_textfield_create(field.x, field.y, field.w, field.h,
|
g_field = meow_textfield_create(field.x, field.y, field.w, field.h,
|
||||||
"输入歌曲 / 歌手", g_st.search_query,
|
"输入歌曲 / 歌手", g_st.search_query,
|
||||||
sizeof g_st.search_query, MEOW_IME_ASCII);
|
sizeof g_st.search_query, MEOW_IME_PINYIN);
|
||||||
g_list = meow_listview_create(r_list.x, r_list.y, r_list.w, r_list.h, 0,
|
g_list = meow_listview_create(r_list.x, r_list.y, r_list.w, r_list.h, 0,
|
||||||
sr_src2, NULL, MEOW_LIST_SEL_NONE);
|
sr_src2, NULL, MEOW_LIST_SEL_NONE);
|
||||||
meow_listview_set_item_h(g_list, APP_ROW_H);
|
meow_listview_set_item_h(g_list, APP_ROW_H);
|
||||||
|
|||||||
@@ -1,14 +1,17 @@
|
|||||||
/* ui/settings.c — 设置页(ACT_SETTINGS)。
|
/* ui/settings.c — 设置页(ACT_SETTINGS)。
|
||||||
*
|
*
|
||||||
* 行项:音质(Hi-res/较高/标准,默认 Hi-res)+ 缓存上限(1~100GB,默认 10GB)+ 退出登录。
|
* 行项:音质(Hi-res 默认,单曲不可用时 core ncm_api_song_url_auto 逐档降级)+ 缓存上限
|
||||||
* 设置持久化到 MeowData/dev.player.ncm/data(meow_config_*;缓存上限键 ncm.cache_limit_gb 与核心库
|
* (默认 10GB,上限 100GB,支持预设与「自定义」数字输入)+ 清理缓存(显示当前占用)+
|
||||||
* ncm_cache_load_setting 对齐)。音质/上限经 BottomSheet 选择;退出登录经 confirm 弹窗。
|
* 退出登录 + 关于/版本。设置持久化到 MeowData/dev.player.ncm/data(meow_config_*,与 core
|
||||||
* 空态/绘制用 custom 绘制物 + 手动行命中(仅 3 行,无需列表滚动)。
|
* ncm_store_read_cfg_int 同目录同键,ncm.cache_limit_gb/ncm.level)。超上限触发 ncm_cache LRU
|
||||||
|
* 淘汰(写缓存前后 + 手动调低上限时,见 player_manager.c/cover.c/appstate.c)。
|
||||||
|
* 空态/绘制用 custom 绘制物 + 手动行命中(仅 5 行,无需列表滚动)。
|
||||||
*/
|
*/
|
||||||
#include "../ncm_app_internal.h"
|
#include "../ncm_app_internal.h"
|
||||||
#include <stdio.h>
|
#include <stdio.h>
|
||||||
#include <string.h>
|
#include <string.h>
|
||||||
#include <stdlib.h>
|
#include <stdlib.h>
|
||||||
|
#include <stdint.h>
|
||||||
|
|
||||||
typedef struct { meow_view *view; int active; } settings_ctrl;
|
typedef struct { meow_view *view; int active; } settings_ctrl;
|
||||||
static settings_ctrl g_sc;
|
static settings_ctrl g_sc;
|
||||||
@@ -17,6 +20,9 @@ static meow_widget *g_tb;
|
|||||||
static ncm_rect_t r_cont;
|
static ncm_rect_t r_cont;
|
||||||
static int g_custom_id;
|
static int g_custom_id;
|
||||||
static int g_row_h; /* 行高 */
|
static int g_row_h; /* 行高 */
|
||||||
|
static uint64_t g_cache_used; /* 缓存占用字节(进设置页时算一次,清理后清零) */
|
||||||
|
|
||||||
|
#define SETTINGS_ROWS 5
|
||||||
|
|
||||||
static const char *level_name(int lv)
|
static const char *level_name(int lv)
|
||||||
{
|
{
|
||||||
@@ -29,6 +35,30 @@ static const char *level_name(int lv)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 缓存占用字节 → 人类可读(整数运算,不用浮点/软浮点)。buf ≥ 24。 */
|
||||||
|
static void fmt_bytes(uint64_t b, char *buf, size_t sz)
|
||||||
|
{
|
||||||
|
const uint64_t GB = 1024ULL * 1024 * 1024;
|
||||||
|
const uint64_t MB = 1024ULL * 1024;
|
||||||
|
if (b >= GB) {
|
||||||
|
uint64_t g = b / GB, fr = (b % GB) / (GB / 10);
|
||||||
|
snprintf(buf, sz, "%llu.%llu GB", (unsigned long long)g, (unsigned long long)fr);
|
||||||
|
} else if (b >= MB) {
|
||||||
|
uint64_t m = b / MB, fr = (b % MB) / (MB / 10);
|
||||||
|
snprintf(buf, sz, "%llu.%llu MB", (unsigned long long)m, (unsigned long long)fr);
|
||||||
|
} else if (b >= 1024) {
|
||||||
|
snprintf(buf, sz, "%llu KB", (unsigned long long)(b / 1024));
|
||||||
|
} else {
|
||||||
|
snprintf(buf, sz, "%llu B", (unsigned long long)b);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 刷新缓存占用缓存(进设置页/清理后调用)。 */
|
||||||
|
static void refresh_cache_used(void)
|
||||||
|
{
|
||||||
|
g_cache_used = ncm_cache_total_bytes(&g_st.ctx.cache);
|
||||||
|
}
|
||||||
|
|
||||||
static void settings_draw(void *ctx)
|
static void settings_draw(void *ctx)
|
||||||
{
|
{
|
||||||
(void)ctx;
|
(void)ctx;
|
||||||
@@ -40,18 +70,23 @@ static void settings_draw(void *ctx)
|
|||||||
int w = r_cont.w - 2 * APP_PAD;
|
int w = r_cont.w - 2 * APP_PAD;
|
||||||
int x = APP_PAD;
|
int x = APP_PAD;
|
||||||
|
|
||||||
/* 行:音质 / 缓存上限 / 退出登录 / 关于(色 = 语义色;退出 = error)。 */
|
char usetxt[24];
|
||||||
struct { const char *title; char sub[160]; int red; } rows[4];
|
fmt_bytes(g_cache_used, usetxt, sizeof usetxt);
|
||||||
rows[0].title = "音质"; rows[0].red = 0;
|
|
||||||
rows[1].title = "缓存上限"; rows[1].red = 0;
|
|
||||||
rows[2].title = "退出登录"; rows[2].red = 1;
|
|
||||||
rows[3].title = "关于网易云"; rows[3].red = 0;
|
|
||||||
snprintf(rows[0].sub, sizeof rows[0].sub, "当前: %s", level_name(g_st.level));
|
|
||||||
snprintf(rows[1].sub, sizeof rows[1].sub, "当前: %d GB", g_st.cache_limit_gb);
|
|
||||||
snprintf(rows[2].sub, sizeof rows[2].sub, "%s", g_st.nickname[0] ? g_st.nickname : "退出");
|
|
||||||
snprintf(rows[3].sub, sizeof rows[3].sub, "%s", "v0.1 · MeowOS");
|
|
||||||
|
|
||||||
for (int i = 0; i < 4; i++) {
|
/* 行:音质 / 缓存上限 / 清理缓存 / 退出登录 / 关于。 */
|
||||||
|
struct { const char *title; char sub[160]; int red; } rows[SETTINGS_ROWS];
|
||||||
|
rows[0].title = "音质"; rows[0].red = 0;
|
||||||
|
rows[1].title = "缓存上限"; rows[1].red = 0;
|
||||||
|
rows[2].title = "清理缓存"; rows[2].red = 0;
|
||||||
|
rows[3].title = "退出登录"; rows[3].red = 1;
|
||||||
|
rows[4].title = "关于网易云"; rows[4].red = 0;
|
||||||
|
snprintf(rows[0].sub, sizeof rows[0].sub, "当前: %s", level_name(g_st.level));
|
||||||
|
snprintf(rows[1].sub, sizeof rows[1].sub, "当前: %d GB · 上限 100", g_st.cache_limit_gb);
|
||||||
|
snprintf(rows[2].sub, sizeof rows[2].sub, "占用: %s", usetxt);
|
||||||
|
snprintf(rows[3].sub, sizeof rows[3].sub, "%s", g_st.nickname[0] ? g_st.nickname : "退出");
|
||||||
|
snprintf(rows[4].sub, sizeof rows[4].sub, "%s", "v0.1 · MeowOS");
|
||||||
|
|
||||||
|
for (int i = 0; i < SETTINGS_ROWS; i++) {
|
||||||
int yy = y + i * (g_row_h + 8);
|
int yy = y + i * (g_row_h + 8);
|
||||||
ncm_fill_round(x, yy, w, g_row_h, 12, th->surfaceContainer);
|
ncm_fill_round(x, yy, w, g_row_h, 12, th->surfaceContainer);
|
||||||
uint32_t tcol = rows[i].red ? th->error : th->onSurface;
|
uint32_t tcol = rows[i].red ? th->error : th->onSurface;
|
||||||
@@ -61,9 +96,9 @@ static void settings_draw(void *ctx)
|
|||||||
meow_ui_text_right(x + w - APP_PAD, yy + 18, ">", 28, rows[i].red ? th->error : th->onSurfaceVariant);
|
meow_ui_text_right(x + w - APP_PAD, yy + 18, ">", 28, rows[i].red ? th->error : th->onSurfaceVariant);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 分组说明(占位)+ 品牌红小字。 */
|
/* 品牌红页脚小字。 */
|
||||||
int ny = y + 4 * (g_row_h + 8) + 4;
|
int ny = y + SETTINGS_ROWS * (g_row_h + 8) + 4;
|
||||||
meow_ui_text(x, ny, "以下为占位,v1 不启用", 20, th->onSurfaceVariant);
|
meow_ui_text(x, ny, "解锁完整音质请登录网易云黑胶会员", 20, th->onSurfaceVariant);
|
||||||
meow_ui_text(x, ny + 24, "品牌红 © MeowOS · NCM", 20, pa->red);
|
meow_ui_text(x, ny + 24, "品牌红 © MeowOS · NCM", 20, pa->red);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -71,13 +106,36 @@ static int row_at(int x, int y)
|
|||||||
{
|
{
|
||||||
(void)x;
|
(void)x;
|
||||||
int r = r_cont.y + 16;
|
int r = r_cont.y + 16;
|
||||||
for (int i = 0; i < 4; i++) {
|
for (int i = 0; i < SETTINGS_ROWS; i++) {
|
||||||
int yy = r + i * (g_row_h + 8);
|
int yy = r + i * (g_row_h + 8);
|
||||||
if (y >= yy && y <= yy + g_row_h) return i;
|
if (y >= yy && y <= yy + g_row_h) return i;
|
||||||
}
|
}
|
||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 「自定义」缓存上限:INPUT 弹窗 + NUM IME(数字键盘输入 1~100,越界钳制)。
|
||||||
|
* 回车/ESC 关闭后 d->ime.buf 已同步(dialog INPUT 关闭时回写 ime_orig),读回即可。 */
|
||||||
|
static int input_cache_limit_gb(void)
|
||||||
|
{
|
||||||
|
char buf[8];
|
||||||
|
snprintf(buf, sizeof buf, "%d", g_st.cache_limit_gb);
|
||||||
|
meow_ime_ctx ime;
|
||||||
|
memset(&ime, 0, sizeof ime);
|
||||||
|
ime.mode = MEOW_IME_NUM;
|
||||||
|
ime.maxlen = sizeof buf;
|
||||||
|
ncm_strlcpy(ime.buf, buf, sizeof ime.buf);
|
||||||
|
|
||||||
|
meow_dialog *d = meow_dialog_create(MEOW_DLG_INPUT, "缓存上限 (GB, 1~100)");
|
||||||
|
meow_dialog_set_ime(d, &ime);
|
||||||
|
meow_dialog_show(d);
|
||||||
|
meow_dialog_free(d);
|
||||||
|
|
||||||
|
int gb = atoi(ime.buf);
|
||||||
|
if (gb < 1) gb = 1;
|
||||||
|
if (gb > 100) gb = 100;
|
||||||
|
return gb;
|
||||||
|
}
|
||||||
|
|
||||||
int settings_is_active(void) { return g_sc.active; }
|
int settings_is_active(void) { return g_sc.active; }
|
||||||
|
|
||||||
int settings_on_tap(int x, int y)
|
int settings_on_tap(int x, int y)
|
||||||
@@ -91,38 +149,60 @@ int settings_on_tap(int x, int y)
|
|||||||
int row = row_at(x, y);
|
int row = row_at(x, y);
|
||||||
if (row < 0) return 0;
|
if (row < 0) return 0;
|
||||||
|
|
||||||
if (row == 0) { /* 音质 */
|
if (row == 0) { /* 音质:Top 档选择,单曲不可用时 core 逐档降级 */
|
||||||
|
static const char *lbls[5] = { "Hi-Res", "Lossless", "Exhigh", "Higher", "Standard" };
|
||||||
|
static const int lvls[5] = { NCM_LEVEL_HIRES, NCM_LEVEL_LOSSLESS,
|
||||||
|
NCM_LEVEL_EXHIGH, NCM_LEVEL_HIGHER, NCM_LEVEL_STANDARD };
|
||||||
meow_sheet *s = meow_sheet_create("Music Quality", 0);
|
meow_sheet *s = meow_sheet_create("Music Quality", 0);
|
||||||
meow_sheet_add_item(s, "Hi-Res");
|
for (int i = 0; i < 5; i++) meow_sheet_add_item(s, lbls[i]);
|
||||||
meow_sheet_add_item(s, "Higher");
|
|
||||||
meow_sheet_add_item(s, "Standard");
|
|
||||||
int op = meow_sheet_open(s);
|
int op = meow_sheet_open(s);
|
||||||
meow_sheet_free(s);
|
meow_sheet_free(s);
|
||||||
if (op >= 0) {
|
if (op >= 0 && op < 5) {
|
||||||
g_st.level = (op == 0) ? NCM_LEVEL_HIRES : (op == 1) ? NCM_LEVEL_HIGHER : NCM_LEVEL_STANDARD;
|
g_st.level = lvls[op];
|
||||||
player_manager_set_quality(g_st.level, g_st.cache_limit_gb);
|
player_manager_set_quality(g_st.level, g_st.cache_limit_gb);
|
||||||
appstate_save_settings();
|
appstate_save_settings();
|
||||||
meow_ui_invalidate(r_cont.x, r_cont.y, r_cont.w, r_cont.h);
|
meow_ui_invalidate(r_cont.x, r_cont.y, r_cont.w, r_cont.h);
|
||||||
}
|
}
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
if (row == 1) { /* 缓存上限 */
|
if (row == 1) { /* 缓存上限:预设 + 自定义(数字输入 1~100) */
|
||||||
static const int gbs[] = { 1, 2, 5, 10, 20, 30, 50, 100 };
|
static const int gbs[] = { 1, 2, 5, 10, 20, 30, 50, 100 };
|
||||||
static const char *labels[] = { "1 GB", "2 GB", "5 GB", "10 GB",
|
static const char *labels[] = { "1 GB", "2 GB", "5 GB", "10 GB",
|
||||||
"20 GB", "30 GB", "50 GB", "100 GB" };
|
"20 GB", "30 GB", "50 GB", "100 GB" };
|
||||||
meow_sheet *s = meow_sheet_create("Cache Limit", 0);
|
meow_sheet *s = meow_sheet_create("Cache Limit", 0);
|
||||||
for (int i = 0; i < 8; i++) meow_sheet_add_item(s, labels[i]);
|
for (int i = 0; i < 8; i++) meow_sheet_add_item(s, labels[i]);
|
||||||
|
meow_sheet_add_item(s, "自定义...");
|
||||||
int op = meow_sheet_open(s);
|
int op = meow_sheet_open(s);
|
||||||
meow_sheet_free(s);
|
meow_sheet_free(s);
|
||||||
if (op >= 0 && op < 8) {
|
if (op == 8) g_st.cache_limit_gb = input_cache_limit_gb();
|
||||||
g_st.cache_limit_gb = gbs[op];
|
else if (op >= 0 && op < 8) g_st.cache_limit_gb = gbs[op];
|
||||||
|
if (op >= 0) {
|
||||||
player_manager_set_quality(g_st.level, g_st.cache_limit_gb);
|
player_manager_set_quality(g_st.level, g_st.cache_limit_gb);
|
||||||
appstate_save_settings();
|
appstate_save_settings(); /* 内含手动调低上限后的 LRU 淘汰 */
|
||||||
meow_ui_invalidate(r_cont.x, r_cont.y, r_cont.w, r_cont.h);
|
meow_ui_invalidate(r_cont.x, r_cont.y, r_cont.w, r_cont.h);
|
||||||
}
|
}
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
if (row == 2) { /* 退出登录 */
|
if (row == 2) { /* 清理缓存 */
|
||||||
|
meow_dialog *d = meow_dialog_create(MEOW_DLG_CONFIRM, "Clear Cache?");
|
||||||
|
char msg[160];
|
||||||
|
char usetxt[24];
|
||||||
|
fmt_bytes(g_cache_used, usetxt, sizeof usetxt);
|
||||||
|
snprintf(msg, sizeof msg, "将删除全部缓存 (%s)。\n已播放的歌曲需重新下载。", usetxt);
|
||||||
|
const char *msgs[1];
|
||||||
|
msgs[0] = msg;
|
||||||
|
meow_dialog_set_items(d, msgs, 1, -1);
|
||||||
|
int yes = meow_dialog_confirm(d);
|
||||||
|
meow_dialog_free(d);
|
||||||
|
if (yes) {
|
||||||
|
ncm_cache_clear(&g_st.ctx.cache);
|
||||||
|
refresh_cache_used();
|
||||||
|
ncm_snack("缓存已清理");
|
||||||
|
meow_ui_invalidate(r_cont.x, r_cont.y, r_cont.w, r_cont.h);
|
||||||
|
}
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
if (row == 3) { /* 退出登录 */
|
||||||
meow_dialog *d = meow_dialog_create(MEOW_DLG_CONFIRM, "Sign out?");
|
meow_dialog *d = meow_dialog_create(MEOW_DLG_CONFIRM, "Sign out?");
|
||||||
const char *msgs[1];
|
const char *msgs[1];
|
||||||
msgs[0] = "Playback will stop.\nYou'll need to scan a QR code again.";
|
msgs[0] = "Playback will stop.\nYou'll need to scan a QR code again.";
|
||||||
@@ -135,8 +215,8 @@ int settings_on_tap(int x, int y)
|
|||||||
}
|
}
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
if (row == 3) { /* 关于(v1 占位) */
|
if (row == 4) { /* 关于 */
|
||||||
ncm_snack("关于页开发中");
|
ncm_snack("网易云音乐 v0.1 · MeowOS 移植版");
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
return 0;
|
return 0;
|
||||||
@@ -154,6 +234,7 @@ void settings_open(void)
|
|||||||
|
|
||||||
void settings_reload(void)
|
void settings_reload(void)
|
||||||
{
|
{
|
||||||
|
refresh_cache_used();
|
||||||
const meow_theme *th = meow_theme_get();
|
const meow_theme *th = meow_theme_get();
|
||||||
meow_ui_set_page_bg(th->bg);
|
meow_ui_set_page_bg(th->bg);
|
||||||
meow_ui_paint_full();
|
meow_ui_paint_full();
|
||||||
|
|||||||
@@ -0,0 +1,201 @@
|
|||||||
|
/* bench/cache_evict_sim.c — 缓存池 LRU 淘汰 / 清空 离线验证(0 网络,仅本地文件系统)。
|
||||||
|
*
|
||||||
|
* 直接驱动 core 的真实函数 ncm_cache_init / ncm_cache_set_limit_gb / ncm_cache_evict /
|
||||||
|
* ncm_cache_total_bytes / ncm_cache_clear / ncm_cache_lookup,验证三期第二批接进生产路径的
|
||||||
|
* 淘汰语义(REPORT §15):
|
||||||
|
* ① 超上限按 mtime 升序删最旧,直到 <= 上限;
|
||||||
|
* ② 未超上限无操作;
|
||||||
|
* ③ ncm_cache_lookup 刷新 mtime(LRU:被访问的最近优先存活);
|
||||||
|
* ④ ncm_cache_set_limit_gb 越界钳制(1~100GB);
|
||||||
|
* ⑤ ncm_cache_clear 删除 song_* 全部(mp3/封面/残留 .tmp)。
|
||||||
|
* 文件用 ftruncate 生成稀疏文件(逻辑大小可控,磁盘零消耗)。
|
||||||
|
*
|
||||||
|
* 编译(宿主,链接 core/ncm_cache.c + 本文件):
|
||||||
|
* $CC_HOST -O2 -DJSMN_HEADER -I. -Icore -c bench/cache_evict_sim.c -o /tmp/ce.o
|
||||||
|
* $CC_HOST -O2 -DJSMN_HEADER -I. -Icore -c core/ncm_cache.c -o /tmp/ce_cache.o
|
||||||
|
* $CC_HOST -O2 -o cache_evict_sim /tmp/ce.o /tmp/ce_cache.o -lpthread
|
||||||
|
* 运行:./cache_evict_sim (0 网络;PASS/FAIL 退出码)
|
||||||
|
*/
|
||||||
|
#include "../core/ncm_cache.h"
|
||||||
|
#include "../core/ncm_common.h"
|
||||||
|
#include "../core/ncm_store.h"
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <stdlib.h>
|
||||||
|
#include <string.h>
|
||||||
|
#include <unistd.h>
|
||||||
|
#include <fcntl.h>
|
||||||
|
#include <errno.h>
|
||||||
|
#include <dirent.h>
|
||||||
|
#include <sys/stat.h>
|
||||||
|
#include <sys/time.h>
|
||||||
|
|
||||||
|
/* ---- ncm_cache.c 依赖的符号(核心库其他 TU 提供;此处最小内联/桩,避免链 mbedtls) ---- */
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
int ncm_store_mkdir_p(const char *path)
|
||||||
|
{
|
||||||
|
if (!path || !*path) return -1;
|
||||||
|
char tmp[512];
|
||||||
|
size_t len = strlen(path);
|
||||||
|
if (len >= sizeof tmp) return -1;
|
||||||
|
memcpy(tmp, path, len + 1);
|
||||||
|
if (tmp[len - 1] == '/') tmp[len - 1] = '\0';
|
||||||
|
for (size_t i = 1; i <= strlen(tmp); i++) {
|
||||||
|
if (tmp[i] == '/' || tmp[i] == '\0') {
|
||||||
|
char ch = tmp[i];
|
||||||
|
tmp[i] = '\0';
|
||||||
|
if (tmp[0] && mkdir(tmp, 0755) != 0 && errno != EEXIST) return -1;
|
||||||
|
tmp[i] = ch;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
int ncm_store_read_cfg_int(const char *dir, const char *key, int def) { (void)dir; (void)key; return def; }
|
||||||
|
|
||||||
|
/* ---- 工具 ---- */
|
||||||
|
static char g_dir[512];
|
||||||
|
static ncm_cache_t g_c;
|
||||||
|
static int g_fail;
|
||||||
|
|
||||||
|
#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)
|
||||||
|
|
||||||
|
/* 按真实生产命名建缓存文件:路径经 ncm_cache_path(与 app/play/cover 同约定,ext 带前导点)。
|
||||||
|
* 结果文件名 = song_<id>.<ext>(ext 带点时即双点,如 song_a..mp3 —— 与 core 现状一致)。 */
|
||||||
|
static void make_cached(const char *id, const char *ext, unsigned long long size, long mtime)
|
||||||
|
{
|
||||||
|
char p[600];
|
||||||
|
if (ncm_cache_path(&g_c, id, ext, p, sizeof p) != 0) { printf(" path err %s\n", id); g_fail = 1; return; }
|
||||||
|
int fd = open(p, O_WRONLY | O_CREAT, 0644);
|
||||||
|
if (fd < 0) { printf(" open %s failed\n", id); g_fail = 1; return; }
|
||||||
|
if (ftruncate(fd, (off_t)size) != 0) printf(" truncate %s err\n", id);
|
||||||
|
close(fd);
|
||||||
|
struct timespec ts[2];
|
||||||
|
ts[0].tv_sec = mtime; ts[0].tv_nsec = 0;
|
||||||
|
ts[1].tv_sec = mtime; ts[1].tv_nsec = 0;
|
||||||
|
utimensat(AT_FDCWD, p, ts, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
static int cached_exists(const char *id, const char *ext)
|
||||||
|
{
|
||||||
|
char p[600];
|
||||||
|
if (ncm_cache_path(&g_c, id, ext, p, sizeof p) != 0) return 0;
|
||||||
|
struct stat st;
|
||||||
|
return stat(p, &st) == 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
static unsigned long long mb(unsigned long long n) { return n * 1024ULL * 1024ULL; }
|
||||||
|
|
||||||
|
static void cleanup(void)
|
||||||
|
{
|
||||||
|
/* 递归删 g_dir 下 song_*(恢复干净池) */
|
||||||
|
DIR *d = opendir(g_dir);
|
||||||
|
if (d) {
|
||||||
|
struct dirent *e;
|
||||||
|
while ((e = readdir(d))) {
|
||||||
|
if (strncmp(e->d_name, "song_", 5) != 0) continue;
|
||||||
|
char p[800];
|
||||||
|
snprintf(p, sizeof p, "%s/%s", g_dir, e->d_name);
|
||||||
|
unlink(p);
|
||||||
|
}
|
||||||
|
closedir(d);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
int main(void)
|
||||||
|
{
|
||||||
|
snprintf(g_dir, sizeof g_dir, "/tmp/ncm_cache_sim_%d", (int)getpid());
|
||||||
|
cleanup();
|
||||||
|
ncm_cache_init(&g_c, g_dir);
|
||||||
|
ncm_cache_set_limit_gb(&g_c, 1); /* 1GB 最小上限(淘汰只在 total > limit 时触发) */
|
||||||
|
|
||||||
|
/* ① 超上限删最旧。A=700MB(最旧), B=300MB, C=400MB(最新) → 删 A 后 700MB<=1GB 停。 */
|
||||||
|
printf("== case 1: evict oldest over-limit ==\n");
|
||||||
|
cleanup();
|
||||||
|
make_cached("a", ".mp3", mb(700), 1000);
|
||||||
|
make_cached("b", ".mp3", mb(300), 1100);
|
||||||
|
make_cached("c", ".mp3", mb(400), 1200);
|
||||||
|
CHECK(ncm_cache_total_bytes(&g_c) == mb(1400), "total=%llu (expect 1400MB)",
|
||||||
|
(unsigned long long)ncm_cache_total_bytes(&g_c));
|
||||||
|
ncm_cache_evict(&g_c);
|
||||||
|
CHECK(!cached_exists("a", ".mp3"), "oldest A evicted");
|
||||||
|
CHECK(cached_exists("b", ".mp3") && cached_exists("c", ".mp3"), "B/C kept");
|
||||||
|
CHECK(ncm_cache_total_bytes(&g_c) == mb(700), "total=%llu after evict (expect 700MB)",
|
||||||
|
(unsigned long long)ncm_cache_total_bytes(&g_c));
|
||||||
|
|
||||||
|
/* ② 4×500MB 超 1GB:删 2 个最旧到 <=1GB。 */
|
||||||
|
printf("== case 2: evict two oldest ==\n");
|
||||||
|
cleanup();
|
||||||
|
make_cached("a", ".mp3", mb(500), 1000);
|
||||||
|
make_cached("b", ".mp3", mb(500), 1100);
|
||||||
|
make_cached("c", ".mp3", mb(500), 1200);
|
||||||
|
make_cached("d", ".mp3", mb(500), 1300);
|
||||||
|
ncm_cache_evict(&g_c);
|
||||||
|
CHECK(!cached_exists("a", ".mp3") && !cached_exists("b", ".mp3"), "A/B (two oldest) evicted");
|
||||||
|
CHECK(cached_exists("c", ".mp3") && cached_exists("d", ".mp3"), "C/D kept");
|
||||||
|
CHECK(ncm_cache_total_bytes(&g_c) == mb(1000), "total=%llu (expect 1000MB)",
|
||||||
|
(unsigned long long)ncm_cache_total_bytes(&g_c));
|
||||||
|
|
||||||
|
/* ③ 未超上限:无删除。 */
|
||||||
|
printf("== case 3: under limit no-op ==\n");
|
||||||
|
cleanup();
|
||||||
|
make_cached("a", ".mp3", mb(300), 1000);
|
||||||
|
make_cached("b", ".mp3", mb(200), 1100);
|
||||||
|
ncm_cache_evict(&g_c);
|
||||||
|
CHECK(cached_exists("a", ".mp3") && cached_exists("b", ".mp3"), "all kept under limit");
|
||||||
|
|
||||||
|
/* ④ lookup 刷新 mtime(LRU):A 被访问 → 变最新,淘汰先删 B/C。 */
|
||||||
|
printf("== case 4: lookup refreshes LRU mtime ==\n");
|
||||||
|
cleanup();
|
||||||
|
make_cached("a", ".mp3", mb(600), 1000);
|
||||||
|
make_cached("b", ".mp3", mb(600), 1100);
|
||||||
|
make_cached("c", ".mp3", mb(600), 1200);
|
||||||
|
char path[600];
|
||||||
|
CHECK(ncm_cache_lookup(&g_c, "a", ".mp3", path, sizeof path) == NCM_OK, "lookup A hit");
|
||||||
|
ncm_cache_evict(&g_c); /* A mtime 已被刷新为最新 → 删 B、C */
|
||||||
|
CHECK(cached_exists("a", ".mp3"), "A (recently accessed) survives");
|
||||||
|
CHECK(!cached_exists("b", ".mp3") && !cached_exists("c", ".mp3"), "B/C evicted");
|
||||||
|
|
||||||
|
/* ⑤ set_limit_gb 钳制。 */
|
||||||
|
printf("== case 5: limit clamping ==\n");
|
||||||
|
ncm_cache_set_limit_gb(&g_c, 0);
|
||||||
|
CHECK(ncm_cache_limit_gb(&g_c) == 1, "limit(0)->%d (expect 1)", ncm_cache_limit_gb(&g_c));
|
||||||
|
ncm_cache_set_limit_gb(&g_c, 200);
|
||||||
|
CHECK(ncm_cache_limit_gb(&g_c) == 100, "limit(200)->%d (expect 100)", ncm_cache_limit_gb(&g_c));
|
||||||
|
|
||||||
|
/* ⑥ clear 删 song_* 全部(mp3 + 封面 + 残留 .tmp)。 */
|
||||||
|
printf("== case 6: clear removes all (mp3+cover+tmp) ==\n");
|
||||||
|
cleanup();
|
||||||
|
make_cached("x", ".mp3", mb(10), 1000);
|
||||||
|
make_cached("y", ".mp3", mb(10), 1001);
|
||||||
|
make_cached("cov123", ".jpg", mb(1), 1002);
|
||||||
|
/* 残留 .tmp:取 mp3 生产路径后追加 .tmp(ncm_cache_open 的临时名)。 */
|
||||||
|
{
|
||||||
|
char tp[600];
|
||||||
|
ncm_cache_path(&g_c, "z", ".mp3", tp, sizeof tp);
|
||||||
|
strncat(tp, ".tmp", sizeof tp - strlen(tp) - 1);
|
||||||
|
int fd = open(tp, O_WRONLY | O_CREAT, 0644);
|
||||||
|
if (ftruncate(fd, (off_t)mb(5)) != 0) printf(" truncate tmp err\n");
|
||||||
|
close(fd);
|
||||||
|
}
|
||||||
|
CHECK(ncm_cache_clear(&g_c) == NCM_OK, "clear rc=0");
|
||||||
|
CHECK(!cached_exists("x", ".mp3") && !cached_exists("y", ".mp3") &&
|
||||||
|
!cached_exists("cov123", ".jpg") && ncm_cache_total_bytes(&g_c) == 0,
|
||||||
|
"all song_* (mp3+cover+tmp) removed; total=0");
|
||||||
|
CHECK(ncm_cache_total_bytes(&g_c) == 0, "total=%llu after clear (expect 0)",
|
||||||
|
(unsigned long long)ncm_cache_total_bytes(&g_c));
|
||||||
|
|
||||||
|
cleanup();
|
||||||
|
printf("%s\n", g_fail ? "RESULT: FAIL" : "RESULT: PASS");
|
||||||
|
return g_fail ? 1 : 0;
|
||||||
|
}
|
||||||
@@ -200,3 +200,19 @@ uint64_t ncm_cache_total_bytes(const ncm_cache_t *c) {
|
|||||||
closedir(d);
|
closedir(d);
|
||||||
return total;
|
return total;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 清空缓存池:删除 song_* 全部文件(mp3 与封面同池,含残留 .tmp)。返回 0=成功。 */
|
||||||
|
int ncm_cache_clear(const ncm_cache_t *c) {
|
||||||
|
if (!c) return NCM_ERR_ARG;
|
||||||
|
DIR *d = opendir(c->dir);
|
||||||
|
if (!d) return NCM_ERR_IO;
|
||||||
|
struct dirent *e;
|
||||||
|
while ((e = readdir(d))) {
|
||||||
|
if (strncmp(e->d_name, "song_", 5) != 0) continue;
|
||||||
|
char p[NCM_CACHE_DIR_MAX + NAME_MAX + 8];
|
||||||
|
snprintf(p, sizeof p, "%s/%s", c->dir, e->d_name);
|
||||||
|
unlink(p);
|
||||||
|
}
|
||||||
|
closedir(d);
|
||||||
|
return NCM_OK;
|
||||||
|
}
|
||||||
@@ -56,6 +56,9 @@ uint64_t ncm_cache_total_bytes(const ncm_cache_t *c);
|
|||||||
/* 立即执行 LRU 淘汰(超上限则删最旧,直到 <= 上限)。 */
|
/* 立即执行 LRU 淘汰(超上限则删最旧,直到 <= 上限)。 */
|
||||||
int ncm_cache_evict(const ncm_cache_t *c);
|
int ncm_cache_evict(const ncm_cache_t *c);
|
||||||
|
|
||||||
|
/* 清空缓存池:删除 song_* 全部文件(mp3 与封面同池,含残留 .tmp)。 */
|
||||||
|
int ncm_cache_clear(const ncm_cache_t *c);
|
||||||
|
|
||||||
#ifdef __cplusplus
|
#ifdef __cplusplus
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Reference in new issue
Block a user