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:
MeowOS committed 2026-08-31 20:15:31 +08:00
1 parent 1a239c5eab
commit f23720f934
13 files changed
+469 -33

No files matched your search

+120
View File
@@ -1050,3 +1050,123 @@ c73366373b3a372d96b2218e528c6381 build_ncm_host.sh
| `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 总数,驱动「还有下一页」判据 |
---
## 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),供「清理缓存」与测试 |
+2
View File
@@ -57,6 +57,8 @@ void appstate_save_settings(void)
meow_config_set("ncm.level", b);
ncm_cache_set_limit_gb(&g_st.ctx.cache, g_st.cache_limit_gb);
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)
+4
View File
@@ -106,10 +106,14 @@ static void *cover_worker(void *arg)
img = decode_cover(url, &blen);
if (!img) { usleep(500 * 1000); continue; }
if (g_st.ctx.cache.dir[0]) {
/* 写前检查:池大小超限先按 LRU 淘汰(与 mp3 同池;封面约 200KB,先腾空间) */
ncm_cache_evict(&g_st.ctx.cache);
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);
/* 写后检查:新封面可能把池推过上限 → 再按 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 (!u.url[0]) return NCM_ERR_NOTFOUND;
/* 写前检查:池大小超限先按 LRU 淘汰(此线程尚未开 temp,无在途文件被误删风险) */
ncm_cache_evict(&g_st.ctx.cache);
/* 流式下载到缓存 temp → commit(稳妥者:先落盘再播) */
ncm_cache_writer_t *w = NULL;
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);
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)
return NCM_ERR_NOENT;
struct stat st;
+2
View File
@@ -145,6 +145,8 @@ int main(int argc, char **argv)
ncm_strlcpy(g_st.nickname, "Tester", sizeof g_st.nickname);
g_st.level = NCM_LEVEL_HIRES;
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) {
mock_playlists();
static const char *sn[] = { "Song One", "Song Two", "Song Three" };
+3 -1
View File
@@ -235,9 +235,11 @@ static void sr_page_create(meow_view *v, void *ctx)
meow_ui_activity_push(ACT_SEARCH); /* owner=ACT_SEARCH */
g_tb = ncm_topbar_create("搜索", 1, NULL);
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_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,
sr_src2, NULL, MEOW_LIST_SEL_NONE);
meow_listview_set_item_h(g_list, APP_ROW_H);
+113 -32
View File
@@ -1,14 +1,17 @@
/* ui/settings.c — 设置页(ACT_SETTINGS)。
*
* 行项:音质(Hi-res/较高/标准,默认 Hi-res)+ 缓存上限(1~100GB,默认 10GB)+ 退出登录。
* 设置持久化到 MeowData/dev.player.ncm/data(meow_config_*;缓存上限键 ncm.cache_limit_gb 与核心库
* ncm_cache_load_setting 对齐)。音质/上限经 BottomSheet 选择;退出登录经 confirm 弹窗。
* 空态/绘制用 custom 绘制物 + 手动行命中(仅 3 行,无需列表滚动)。
* 行项:音质(Hi-res 默认,单曲不可用时 core ncm_api_song_url_auto 逐档降级)+ 缓存上限
* (默认 10GB,上限 100GB,支持预设与「自定义」数字输入)+ 清理缓存(显示当前占用)+
* 退出登录 + 关于/版本。设置持久化到 MeowData/dev.player.ncm/data(meow_config_*,与 core
* 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 <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <stdint.h>
typedef struct { meow_view *view; int active; } settings_ctrl;
static settings_ctrl g_sc;
@@ -17,6 +20,9 @@ static meow_widget *g_tb;
static ncm_rect_t r_cont;
static int g_custom_id;
static int g_row_h; /* 行高 */
static uint64_t g_cache_used; /* 缓存占用字节(进设置页时算一次,清理后清零) */
#define SETTINGS_ROWS 5
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)
{
(void)ctx;
@@ -40,18 +70,23 @@ static void settings_draw(void *ctx)
int w = r_cont.w - 2 * APP_PAD;
int x = APP_PAD;
/* 行:音质 / 缓存上限 / 退出登录 / 关于(色 = 语义色;退出 = error)。 */
struct { const char *title; char sub[160]; int red; } rows[4];
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");
char usetxt[24];
fmt_bytes(g_cache_used, usetxt, sizeof usetxt);
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);
ncm_fill_round(x, yy, w, g_row_h, 12, th->surfaceContainer);
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);
}
/* 分组说明(占位)+ 品牌红小字。 */
int ny = y + 4 * (g_row_h + 8) + 4;
meow_ui_text(x, ny, "以下为占位,v1 不启用", 20, th->onSurfaceVariant);
/* 品牌红页脚小字。 */
int ny = y + SETTINGS_ROWS * (g_row_h + 8) + 4;
meow_ui_text(x, ny, "解锁完整音质请登录网易云黑胶会员", 20, th->onSurfaceVariant);
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;
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);
if (y >= yy && y <= yy + g_row_h) return i;
}
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_on_tap(int x, int y)
@@ -91,38 +149,60 @@ int settings_on_tap(int x, int y)
int row = row_at(x, y);
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_add_item(s, "Hi-Res");
meow_sheet_add_item(s, "Higher");
meow_sheet_add_item(s, "Standard");
for (int i = 0; i < 5; i++) meow_sheet_add_item(s, lbls[i]);
int op = meow_sheet_open(s);
meow_sheet_free(s);
if (op >= 0) {
g_st.level = (op == 0) ? NCM_LEVEL_HIRES : (op == 1) ? NCM_LEVEL_HIGHER : NCM_LEVEL_STANDARD;
if (op >= 0 && op < 5) {
g_st.level = lvls[op];
player_manager_set_quality(g_st.level, g_st.cache_limit_gb);
appstate_save_settings();
meow_ui_invalidate(r_cont.x, r_cont.y, r_cont.w, r_cont.h);
}
return 1;
}
if (row == 1) { /* 缓存上限 */
if (row == 1) { /* 缓存上限:预设 + 自定义(数字输入 1~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",
"20 GB", "30 GB", "50 GB", "100 GB" };
meow_sheet *s = meow_sheet_create("Cache Limit", 0);
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);
meow_sheet_free(s);
if (op >= 0 && op < 8) {
g_st.cache_limit_gb = gbs[op];
if (op == 8) g_st.cache_limit_gb = input_cache_limit_gb();
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);
appstate_save_settings();
appstate_save_settings(); /* 内含手动调低上限后的 LRU 淘汰 */
meow_ui_invalidate(r_cont.x, r_cont.y, r_cont.w, r_cont.h);
}
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?");
const char *msgs[1];
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;
}
if (row == 3) { /* 关于(v1 占位) */
ncm_snack("关于页开发中");
if (row == 4) { /* 关于 */
ncm_snack("网易云音乐 v0.1 · MeowOS 移植版");
return 1;
}
return 0;
@@ -154,6 +234,7 @@ void settings_open(void)
void settings_reload(void)
{
refresh_cache_used();
const meow_theme *th = meow_theme_get();
meow_ui_set_page_bg(th->bg);
meow_ui_paint_full();
+201
View File
@@ -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;
}
+16
View File
@@ -200,3 +200,19 @@ uint64_t ncm_cache_total_bytes(const ncm_cache_t *c) {
closedir(d);
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;
}
+3
View File
@@ -56,6 +56,9 @@ uint64_t ncm_cache_total_bytes(const ncm_cache_t *c);
/* 立即执行 LRU 淘汰(超上限则删最旧,直到 <= 上限)。 */
int ncm_cache_evict(const ncm_cache_t *c);
/* 清空缓存池:删除 song_* 全部文件(mp3 与封面同池,含残留 .tmp)。 */
int ncm_cache_clear(const ncm_cache_t *c);
#ifdef __cplusplus
}
#endif
Binary file not shown.
Binary file not shown.
Binary file not shown.