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

+88 -5
View File
@@ -20,6 +20,9 @@
* gcc -O2 -Wl,--gc-sections -o player_flow_sim /tmp/pf.o /tmp/pf_pm.o -lpthread -lm
* 运行:./player_flow_sim (0 网络;PASS/FAIL 退出码)
* §19:P6 覆盖缓存键=歌曲ID+档位 + flac type → .flac 落盘扩展名。
* §20:P7 覆盖 VIP 钳顶(非会员用户档 SURROUND → 请求档 EXHIGH,缓存键仍用户档 sky_c51;
* SVIP 直达)+ eff_level 回填;P8 覆盖 fee 门禁(fee=1 非会员 NCM_ERR_VIP 零请求 /
* 缓存命中绕过 / CVIP 放行 / fee=8 放行)。
*/
#include "../app/ncm_app_internal.h"
#include "../app/play/decoder.h"
@@ -46,7 +49,8 @@ size_t ncm_strlcpy(char *dst, const char *src, size_t sz)
void meow_log(meow_log_level lvl, const char *fmt, ...) { (void)lvl; (void)fmt; }
void meow_audio_on_preempt(meow_audio_preempt_fn cb, void *ctx) { (void)cb; (void)ctx; }
/* ---- appstate stub:窗内全命中,id=1000+idx ---- */
/* ---- appstate stub:窗内全命中,id=1000+idx;fee 可注入(§20 P8) ---- */
static int g_fee = 0;
int appstate_song_copy(int gidx, ncm_song_t *out)
{
if (gidx < 0 || gidx >= g_st.track_ids_count) return -1;
@@ -54,6 +58,7 @@ int appstate_song_copy(int gidx, ncm_song_t *out)
snprintf(out->id, sizeof out->id, "%d", 1000 + gidx);
snprintf(out->name, sizeof out->name, "Song%d", gidx);
out->duration_ms = 180000;
out->fee = g_fee;
return 0;
}
int appstate_song_fetch(int gidx, ncm_song_t *out) { return appstate_song_copy(gidx, out) == 0 ? NCM_OK : NCM_ERR_ARG; }
@@ -68,6 +73,20 @@ static int g_downloads; /* http_download 调用数(song_url 请求放
static const char *g_url_type = "mp3"; /* §19:song_url 注入的 type(mp3/flac) */
static char g_last_key[80]; /* ncm_cache_open 收到的键(档位键断言用) */
static char g_last_ext[8]; /* ncm_cache_open 收到的扩展名 */
static int g_url_calls; /* §20:song_url_auto 调用数(fee 门禁不得发请求) */
static int g_url_req_level = -1;/* §20:song_url_auto 收到的档位(VIP 钳顶断言用) */
static int g_vip = NCM_VIP_NONE;/* §20:account stub 注入的会员态 */
/* §20:account stub(真实实现在 app/logic/account.c,不链入本 sim) */
int account_vip(void) { return g_vip; }
int account_level_cap(void)
{
switch (account_vip()) {
case NCM_VIP_SVIP: return NCM_LEVEL_SURROUND;
case NCM_VIP_CVIP: return NCM_LEVEL_HIRES;
default: return NCM_LEVEL_EXHIGH;
}
}
int ncm_cache_lookup(const ncm_cache_t *c, const char *id, const char *ext, char *out, size_t out_sz)
{
@@ -89,14 +108,19 @@ int ncm_cache_abort(ncm_cache_writer_t *w) { free(w); return NCM_OK; }
int ncm_cache_evict(const ncm_cache_t *c) { (void)c; return NCM_OK; }
void ncm_cache_set_limit_gb(ncm_cache_t *c, int gb) { (void)c; (void)gb; }
int ncm_api_song_url_auto(const char *dev, const char *ck, const char *id, int level, ncm_play_url_t *out)
int ncm_api_song_url_auto(const char *dev, const char *ck, const char *id, int level,
ncm_play_url_t *out, int *out_level)
{
(void)dev; (void)ck; (void)id; (void)level;
(void)dev; (void)ck; (void)id;
g_url_calls++;
g_url_req_level = level;
if (out_level) *out_level = -1;
if (g_url_rc != NCM_OK) return g_url_rc;
memset(out, 0, sizeof *out);
snprintf(out->url, sizeof out->url, "http://stub/%s.%s", id, g_url_type);
snprintf(out->type, sizeof out->type, "%s", g_url_type);
out->size = 4 * 1024 * 1024;
if (out_level) *out_level = level;
return NCM_OK;
}
@@ -146,15 +170,19 @@ int ncm_dec_seek_ms(ncm_decoder_t *d, int64_t ms, int64_t fb, int64_t dur)
void ncm_dec_close(ncm_decoder_t *d) { free(d); }
const char *ncm_dec_format(const ncm_decoder_t *d) { (void)d; return "stub"; }
/* §19:song_local_file 的缓存键用 ncm_api_level_str(g_level);ext 判定用 ncm_streq */
/* §19/§20:song_local_file 的缓存键用 ncm_api_level_key(g_level);ext 判定用 ncm_streq */
int ncm_streq(const char *a, const char *b)
{
if (!a || !b) return 0;
return strcmp(a, b) == 0;
}
const char *ncm_api_level_str(int level)
const char *ncm_api_level_key(int level)
{
switch (level) {
case NCM_LEVEL_SURROUND: return "sky_c51";
case NCM_LEVEL_SKY: return "sky";
case NCM_LEVEL_JYMASTER: return "jymaster";
case NCM_LEVEL_JYEFFECT: return "jyeffect";
case NCM_LEVEL_HIRES: return "hires";
case NCM_LEVEL_LOSSLESS: return "lossless";
case NCM_LEVEL_EXHIGH: return "exhigh";
@@ -305,6 +333,61 @@ int main(void)
player_manager_set_quality(NCM_LEVEL_STANDARD, 10); /* 还原,避免影响后续(无) */
g_eof = 1;
printf("== P7: VIP cap(§20 超权限档不硬拒,钳顶后降级;下首恢复用户档)==\n");
usleep(300 * 1000); /* 等 P6 曲收尾 */
g_eof = 0; g_reads_left = -1; g_cache_ok = 0; g_dl_delay_ms = 0; g_fee = 0;
player_manager_set_quality(NCM_LEVEL_SURROUND, 10); /* 用户档=沉浸环绕声(SVIP 档) */
g_vip = NCM_VIP_NONE; g_url_calls = 0; g_url_req_level = -1;
player_manager_play(0);
CHECK(wait_state(NCM_PC_PLAYING, 3000), "non-vip song playing (降级后照常播)");
CHECK(g_url_req_level == NCM_LEVEL_EXHIGH,
"req level=%d (expect EXHIGH=%d:非会员钳顶,不浪费高档请求)",
g_url_req_level, NCM_LEVEL_EXHIGH);
CHECK(player_manager_eff_level() == NCM_LEVEL_EXHIGH,
"eff_level=%d (expect EXHIGH,UI 标「已降级至 极高 HQ」)", player_manager_eff_level());
CHECK(strcmp(g_last_key, "1000_sky_c51") == 0,
"cache key=%s (expect 1000_sky_c51 = id+用户档,降级产物归用户档键)", g_last_key);
/* SVIP:同一用户档不钳顶,直达 SURROUND */
g_vip = NCM_VIP_SVIP; g_cache_ok = 0; g_url_calls = 0; g_url_req_level = -1;
player_manager_play(1);
CHECK(wait_state(NCM_PC_PLAYING, 3000), "svip song playing");
CHECK(g_url_req_level == NCM_LEVEL_SURROUND,
"req level=%d (expect SURROUND=%d:SVIP 不钳顶)", g_url_req_level, NCM_LEVEL_SURROUND);
CHECK(player_manager_eff_level() == NCM_LEVEL_SURROUND,
"eff_level=%d (expect SURROUND)", player_manager_eff_level());
g_eof = 1;
printf("== P8: fee 门禁(§20:fee∈{1,4} 且非会员 → NCM_ERR_VIP,不发 song_url)==\n");
usleep(300 * 1000);
g_eof = 0; g_reads_left = -1; g_cache_ok = 0; g_url_rc = NCM_OK;
g_fee = 1; g_vip = NCM_VIP_NONE; g_url_calls = 0;
player_manager_play(2);
CHECK(wait_err(NCM_ERR_VIP, 2000), "fee=1 + 非会员 -> err=NCM_ERR_VIP (rc=%d)",
player_manager_error());
CHECK(g_url_calls == 0, "song_url 零请求(省一次请求,防风控;calls=%d)", g_url_calls);
/* 已缓存的 VIP 曲:门禁不拦(缓存优先,NCM_CACHE_DESIGN §2.4) */
g_cache_ok = 1;
player_manager_play(2);
CHECK(wait_state(NCM_PC_PLAYING, 3000), "cached VIP song plays (缓存命中绕过门禁)");
g_eof = 1; /* 先让它自然收尾(run_song 在跑时同 idx play 不重入) */
wait_state(NCM_PC_STOPPED, 3000);
/* CVIP:fee=1 放行,正常发请求 */
g_eof = 0; g_reads_left = -1;
g_cache_ok = 0; g_vip = NCM_VIP_CVIP; g_url_calls = 0;
player_manager_play(2);
CHECK(wait_state(NCM_PC_PLAYING, 3000), "CVIP plays fee=1 song");
CHECK(g_url_calls == 1, "CVIP song_url 正常请求 (calls=%d)", g_url_calls);
g_eof = 1;
wait_state(NCM_PC_STOPPED, 3000);
/* fee=8(免费听低音质)非会员也放行 */
g_eof = 0; g_reads_left = -1;
g_cache_ok = 0; g_vip = NCM_VIP_NONE; g_fee = 8; g_url_calls = 0;
player_manager_play(2);
CHECK(wait_state(NCM_PC_PLAYING, 3000), "fee=8 non-vip plays (免费听档放行)");
CHECK(g_url_calls == 1, "fee=8 song_url requested (calls=%d)", g_url_calls);
g_fee = 0; g_eof = 1;
player_manager_set_quality(NCM_LEVEL_STANDARD, 10);
usleep(200 * 1000);
player_manager_stop_thread();
printf(g_fail ? "== PLAYER FLOW SIM: FAIL ==\n" : "== PLAYER FLOW SIM: ALL PASS ==\n");