feat(ncm §20): 音质体系全量——八档UI/VIP检测缓存/逐级降级下首恢复/fee门禁/默认极高HQ

- core: 9档枚举(顺序=降级链, DOLBY映射保留不进UI/链) + level_key(sky/sky_c51)
  + level_gate(CVIP/SVIP门槛) + song_url surround档附加immerseType=c51
  + song_url_auto全档表降级(out_level回填/响应level静默降档采信/try_fn可注入)
  + vip_info(weapi v2优先v1兜底) + parse_vip_info离线可测 + NCM_ERR_VIP(-15)
- app/logic/account.c(新): account.json会员态缓存(uid/nick/vip/双expire/checked_at)
  刷新=登录后/超1天/到期后各一次不轮询; account_vip本地到期降档视图
- player_manager: 用户档按会员钳顶后逐级降级(不硬拒,缓存键仍用户档),
  fee∈{1,4}且非会员→NCM_ERR_VIP零请求(缓存命中优先), eff_level回填
- UI: 设置页八档(规格+门槛+未解锁标注,允许选定), 播放页时间行中央
  生效档/已降级至xx小字, VIP专享文案; 默认档Hi-Res→极高HQ(存量设置保留)
- 验证(0真实请求): degrade_sim 90例矩阵全PASS; player_flow_sim P1-P8全PASS;
  test_ncm_core vip解析9例宿主+qemu全PASS; 窗口/分页/缓存/封面/audiod sim全PASS;
  mipsel -Wall -Wextra 0 warning; 6页离屏rc=0。未部署真机(留主会话)
This commit is contained in:
MeowOS committed 2026-09-01 15:55:42 +08:00
1 parent ab4dfb4f9c
commit bdd9e898f4
23 files changed
+1026 -70

No files matched your search

+43 -22
View File
@@ -1,8 +1,10 @@
/* ui/settings.c — 设置页(ACT_SETTINGS)。
*
* 行项:音质(Hi-res 默认,单曲不可用时 core ncm_api_song_url_auto 逐档降级)+ 缓存上限
* (默认 10GB,上限 100GB,支持预设与「自定义」数字输入)+ 清理缓存(显示当前占用)+
* 退出登录 + 关于/版本。设置持久化到 MeowData/dev.player.ncm/data(meow_config_*,与 core
* 行项:音质(§20 八档:标准/极高 HQ/无损 SQ/Hi-Res/高清臻音/臻音全景声/超清母带/沉浸环绕声,
* 默认极高 HQ;每档标注规格+会员门槛 CVIP/SVIP;超权限档允许选定,播放时钳顶+逐级降级,
* 见 player_manager.c song_local_file)+ 缓存上限(默认 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 行,无需列表滚动)。
@@ -24,16 +26,12 @@ static uint64_t g_cache_used; /* 缓存占用字节(进设置页时
#define SETTINGS_ROWS 5
static const char *level_name(int lv)
{
switch (lv) {
case NCM_LEVEL_HIRES: return "Hi-Res";
case NCM_LEVEL_LOSSLESS:return "无损";
case NCM_LEVEL_EXHIGH: return "极高";
case NCM_LEVEL_HIGHER: return "较高";
default: return "标准";
}
}
/* 八档音质 UI 表(用户手机 App 口径为权威,NCM_CACHE_DESIGN §3):显示顺序高→低。
* HIGHER(较高)与 DOLBY(杜比全景声)不进 UI 清单(映射保留,见 ncm_api.h)。 */
static const int g_ui_levels[8] = {
NCM_LEVEL_SURROUND, NCM_LEVEL_JYMASTER, NCM_LEVEL_SKY, NCM_LEVEL_JYEFFECT,
NCM_LEVEL_HIRES, NCM_LEVEL_LOSSLESS, NCM_LEVEL_EXHIGH, NCM_LEVEL_STANDARD,
};
/* 缓存占用字节 → 人类可读(整数运算,不用浮点/软浮点)。buf ≥ 24。 */
static void fmt_bytes(uint64_t b, char *buf, size_t sz)
@@ -80,7 +78,19 @@ static void settings_draw(void *ctx)
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));
{
/* 当前档 + 会员门槛标注;超权限(未解锁)时提示将降级(不硬拒,§20) */
int gate = ncm_api_level_gate(g_st.level);
const char *gate_s = (gate == NCM_VIP_SVIP) ? "SVIP" : (gate == NCM_VIP_CVIP) ? "CVIP" : "";
int locked = (account_vip() < gate);
if (gate_s[0])
snprintf(rows[0].sub, sizeof rows[0].sub, "当前: %s (%s) · 需 %s%s",
ncm_level_name(g_st.level), ncm_level_spec(g_st.level), gate_s,
locked ? ",将降级" : "");
else
snprintf(rows[0].sub, sizeof rows[0].sub, "当前: %s (%s)",
ncm_level_name(g_st.level), ncm_level_spec(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 : "退出");
@@ -98,7 +108,7 @@ static void settings_draw(void *ctx)
/* 品牌红页脚小字。 */
int ny = y + SETTINGS_ROWS * (g_row_h + 8) + 4;
meow_ui_text(x, ny, "Hi-Res/无损需黑胶会员,不支持时自动降级", 20, th->onSurfaceVariant);
meow_ui_text(x, ny, "无损/Hi-Res 需 CVIP ・ 臻音/母带需 SVIP,不支持时自动降级", 20, th->onSurfaceVariant);
meow_ui_text(x, ny + 24, "网易云音乐 · MeowOS 移植版", 20, pa->red);
}
@@ -149,16 +159,27 @@ int settings_on_tap(int x, int y)
int row = row_at(x, y);
if (row < 0) return 0;
if (row == 0) { /* 音质:Top 档选择,单曲不可用时 core 逐档降级 */
static const char *lbls[5] = { "Hi-Res", "无损", "极高", "较高", "标准" };
static const int lvls[5] = { NCM_LEVEL_HIRES, NCM_LEVEL_LOSSLESS,
NCM_LEVEL_EXHIGH, NCM_LEVEL_HIGHER, NCM_LEVEL_STANDARD };
if (row == 0) { /* 音质:八档(§20;超权限档可选不硬拒,播放时钳顶+逐级降级) */
meow_sheet *s = meow_sheet_create("选择音质", 0);
for (int i = 0; i < 5; i++) meow_sheet_add_item(s, lbls[i]);
int vip = account_vip();
for (int i = 0; i < 8; i++) {
int lv = g_ui_levels[i];
int gate = ncm_api_level_gate(lv);
char lbl[96];
/* 每档标注 规格+会员门槛;未解锁档标注「需 CVIP/SVIP」(sheet 无置灰能力,
* 以文字标注代替——允许选定,实际播放逐级降级到可用档) */
const char *gate_s = (gate == NCM_VIP_SVIP) ? "SVIP" : (gate == NCM_VIP_CVIP) ? "CVIP" : NULL;
if (gate_s)
snprintf(lbl, sizeof lbl, "%s (%s) · %s%s", ncm_level_name(lv),
ncm_level_spec(lv), gate_s, (vip < gate) ? "·未解锁" : "");
else
snprintf(lbl, sizeof lbl, "%s (%s)", ncm_level_name(lv), ncm_level_spec(lv));
meow_sheet_add_item(s, lbl);
}
int op = meow_sheet_open(s);
meow_sheet_free(s);
if (op >= 0 && op < 5) {
g_st.level = lvls[op];
if (op >= 0 && op < 8) {
g_st.level = g_ui_levels[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);