diff --git a/patch_v2/ncm_app/bench/degrade_sim.c b/patch_v2/ncm_app/bench/degrade_sim.c index fe789a5..8f71aee 100644 --- a/patch_v2/ncm_app/bench/degrade_sim.c +++ b/patch_v2/ncm_app/bench/degrade_sim.c @@ -66,6 +66,34 @@ static int total_calls(void) return n; } +/* ---- §20.3 账户/fee 感知降级链(auto_av_f)期望模型 ---- + * cap(vip):NONE→EXHIGH / CVIP→HIRES / SVIP→SURROUND(与 account.c account_level_cap 同口径)。 + * start = clamp(desired, STANDARD, cap);只试 gate(lv)<=vip 的档;落在 lv<=avail 的最近可用档。 */ +static int cap_for_vip(int vip) { + switch (vip) { + case NCM_VIP_SVIP: return NCM_LEVEL_SURROUND; + case NCM_VIP_CVIP: return NCM_LEVEL_HIRES; + default: return NCM_LEVEL_EXHIGH; + } +} +static int expect_land(int vip, int desired, int avail) { + int cap = cap_for_vip(vip); + int start = desired > cap ? cap : desired; + if (start < NCM_LEVEL_STANDARD) start = NCM_LEVEL_STANDARD; + for (int lv = start; lv >= NCM_LEVEL_STANDARD; lv--) + if (ncm_api_level_gate(lv) <= vip && lv <= avail) return lv; + return -1; +} +static int expect_tried(int vip, int desired, int best) { + int cap = cap_for_vip(vip); + int start = desired > cap ? cap : desired; + if (start < NCM_LEVEL_STANDARD) start = NCM_LEVEL_STANDARD; + int n = 0; + for (int lv = start; lv >= NCM_LEVEL_STANDARD; lv--) + if (ncm_api_level_gate(lv) <= vip && (best < 0 || lv >= best)) n++; + return n; +} + int main(void) { ncm_play_url_t u; @@ -160,6 +188,63 @@ int main(void) "req surround, resp level=sky (歧义) -> eff=surround (got %d)", eff); g_resp_level = NULL; + printf("== 7) auto_av_f fee 门禁(§20.4:fee∈{1,4} 且非 CVIP → 零请求 NCM_ERR_VIP)==\n"); + reset_calls(); g_avail_from = NCM_LEVEL_SURROUND; g_type = "mp3"; + CHECK(ncm_api_song_url_auto_av_f("d", "c", "1", NCM_VIP_NONE, 1, NCM_LEVEL_HIRES, &u, &eff, fake_try) + == NCM_ERR_VIP && total_calls() == 0, + "fee=1 + NONE -> NCM_ERR_VIP (calls=%d)", total_calls()); + CHECK(ncm_api_song_url_auto_av_f("d", "c", "1", NCM_VIP_NONE, 4, NCM_LEVEL_HIRES, &u, &eff, fake_try) + == NCM_ERR_VIP && total_calls() == 0, + "fee=4 + NONE -> NCM_ERR_VIP (calls=%d)", total_calls()); + CHECK(ncm_api_song_url_auto_av_f("d", "c", "1", NCM_VIP_CVIP, 1, NCM_LEVEL_HIRES, &u, &eff, fake_try) + == NCM_OK, "fee=1 + CVIP -> 放行"); + CHECK(ncm_api_song_url_auto_av_f("d", "c", "1", NCM_VIP_NONE, 8, NCM_LEVEL_HIRES, &u, &eff, fake_try) + == NCM_OK, "fee=8 + NONE -> 放行(免费听低音质)"); + CHECK(ncm_api_song_url_auto_av_f("d", "c", "1", NCM_VIP_NONE, 0, NCM_LEVEL_HIRES, &u, &eff, fake_try) + == NCM_OK, "fee=0 + NONE -> 放行"); + + printf("== 8) auto_av_f 全矩阵: account_vip 3 x desired 9 x avail 9(钳顶+门槛+最近可用档)==\n"); + int av_bad = 0, av_cases = 0; + for (int vip = NCM_VIP_NONE; vip <= NCM_VIP_SVIP; vip++) { + for (int desired = NCM_LEVEL_STANDARD; desired <= NCM_LEVEL_SURROUND; desired++) { + for (int avail = NCM_LEVEL_STANDARD; avail <= NCM_LEVEL_SURROUND; avail++) { + reset_calls(); + g_avail_from = avail; g_type = "mp3"; + int rc = ncm_api_song_url_auto_av_f("dev", "ck", "1", vip, 0, desired, &u, &eff, fake_try); + int want = expect_land(vip, desired, avail); + int ok = (want >= 0 && rc == NCM_OK && eff == want && u.url[0] && + total_calls() == expect_tried(vip, desired, want)); + av_cases++; + if (!ok) { + av_bad++; + printf(" FAIL vip=%d desired=%d(%s) avail=%d -> rc=%d eff=%d calls=%d (want %d/%d)\n", + vip, desired, ncm_api_level_str(desired), avail, rc, eff, total_calls(), + want, expect_tried(vip, desired, want)); + g_fail = 1; + } + } + } + } + CHECK(av_bad == 0, "auto_av matrix %d cases all pass (vip 3 x desired 9 x avail 9)", av_cases); + + printf("== 9) 真机场景复现:CVIP + 设置Hi-Res + 歌曲「有SQ无Hi-Res」→ 落无损SQ ==\n"); + reset_calls(); + g_avail_from = NCM_LEVEL_LOSSLESS; /* 歌曲只提供到 SQ(及更低)→ 无 Hi-Res */ + g_type = "flac"; /* SQ 以 flac 下发 */ + int rc9 = ncm_api_song_url_auto_av_f("d", "c", "10", NCM_VIP_CVIP, 0, NCM_LEVEL_HIRES, + &u, &eff, fake_try); + CHECK(rc9 == NCM_OK && eff == NCM_LEVEL_LOSSLESS && strcmp(u.type, "flac") == 0, + "CVIP Hi-Res、有SQ无HiRes -> 落无损SQ (rc=%d eff=%d(%s) type=%s)", + rc9, eff, ncm_api_level_str(eff), u.type); + /* 对照:若账户VIP态未就绪(NONE),钳顶把Hi-Res压到极高,SQ被跳过(旧app clamp即此路径) */ + reset_calls(); + g_avail_from = NCM_LEVEL_LOSSLESS; g_type = "flac"; + int rcn = ncm_api_song_url_auto_av_f("d", "c", "10", NCM_VIP_NONE, 0, NCM_LEVEL_HIRES, + &u, &eff, fake_try); + CHECK(rcn == NCM_OK && eff == NCM_LEVEL_EXHIGH, + "NONE+Hi-Res -> 钳到极高EXHIGH (rc=%d eff=%d) — 旧app clamp正是此路径致SQ被跳过", + rcn, eff); + printf(g_fail ? "== DEGRADE SIM: FAIL ==\n" : "== DEGRADE SIM: ALL PASS ==\n"); return g_fail; } diff --git a/patch_v2/ncm_app/core/ncm_api.c b/patch_v2/ncm_app/core/ncm_api.c index 62f106b..1cefd27 100644 --- a/patch_v2/ncm_app/core/ncm_api.c +++ b/patch_v2/ncm_app/core/ncm_api.c @@ -299,46 +299,95 @@ static int level_from_str(const char *s) { return -1; /* sky/dolby/未知:不采信 */ } +/* 账户权限允许的最高档(§20.3,与 app/logic/account.c account_level_cap 同口径): + * NONE→EXHIGH / CVIP→HIRES / SVIP→SURROUND。core 侧权威实现,供降级链自钳顶。 */ +static int level_cap_for_vip(int vip) +{ + switch (vip) { + case NCM_VIP_SVIP: return NCM_LEVEL_SURROUND; + case NCM_VIP_CVIP: return NCM_LEVEL_HIRES; + default: return NCM_LEVEL_EXHIGH; + } +} + +/* 逐级一次尝试:请求 lv 档,url 非空且可解码(§19:type 权威 mp3/flac;type 缺失按 + * url 扩展名,默认当 mp3 收)→ 回填 out/out_level 返回 1;否则返回 0(该档不可用, + * 继续降级)。out_level 采信服务端静默降档(data[0].level < 请求档且无歧义串); + * sky 族(SKY/SURROUND 同 "sky")无法区分时保守取请求档。 */ +static int level_attempt(const char *device_id, const char *cookie, const char *id, + int lv, ncm_song_url_try_fn try_fn, + ncm_play_url_t *out, int *out_level) +{ + ncm_play_url_t p; + int rc = try_fn(device_id, cookie, id, lv, &p); + if (rc != NCM_OK) return 0; + int ok = ncm_streq(p.type, "mp3") || ncm_streq(p.type, "flac"); + if (!p.type[0]) { + ok = 1; + const char *dot = strrchr(p.url, '.'); + if (dot && ncm_streq(dot, ".flac")) ncm_strlcpy(p.type, "flac", sizeof p.type); + } + if (!(p.url[0] && ok)) return 0; /* url 为 null 或未知格式:继续降级 */ + *out = p; + if (out_level) { + int rl = level_from_str(p.level); + *out_level = (rl >= 0 && rl < lv) ? rl : lv; + } + return 1; +} + /* 降级链(§20 全档表):从 desired 逐档向下;DOLBY(=16) 不在链内(不进 UI/不降级经过)。 - * 单次请求失败(网络/鉴权)与「该档不可用」(url null/未知格式)都继续降级。 */ + * 单次请求失败(网络/鉴权)与「该档不可用」(url null/未知格式)都继续降级。 + * 本函数无账户感知——信任调用方已按会员权限钳顶(app/play/player_manager.c 负责)。 */ int ncm_api_song_url_auto_f(const char *device_id, const char *cookie, const char *id, int desired, ncm_play_url_t *out, int *out_level, ncm_song_url_try_fn try_fn) { if (!try_fn) try_fn = ncm_api_song_url; if (out_level) *out_level = -1; + /* 从 desired 往下降级:surround(sky+c51)->sky->jymaster->jyeffect->hires->lossless + * ->exhigh->higher->standard;DOLBY/越界钳到合法区间。 */ int start = desired; if (start < NCM_LEVEL_STANDARD) start = NCM_LEVEL_STANDARD; if (start > NCM_LEVEL_UI_MAX) start = NCM_LEVEL_UI_MAX; - /* 从 desired 往下降级:surround(sky+c51)->sky->jymaster->jyeffect->hires->lossless - * ->exhigh->higher->standard */ for (int lv = start; lv >= NCM_LEVEL_STANDARD; lv--) { - ncm_play_url_t p; - int rc = try_fn(device_id, cookie, id, lv, &p); - if (rc != NCM_OK) continue; - /* §19:接受 mp3 与 flac(dr_flac 落地后无损/Hi-Res 真实生效)。 - * type 权威;type 缺失(旧端点兼容)时看 url 扩展名,默认当 mp3 收。 */ - int ok = ncm_streq(p.type, "mp3") || ncm_streq(p.type, "flac"); - if (!p.type[0]) { - ok = 1; - const char *dot = strrchr(p.url, '.'); - if (dot && ncm_streq(dot, ".flac")) ncm_strlcpy(p.type, "flac", sizeof p.type); - } - if (p.url[0] && ok) { - *out = p; - if (out_level) { - /* 服务端可能在 url 非空时仍静默降档(响应 data[0].level < 请求档)—— - * 无歧义串(standard..jymaster)且低于请求档时采信响应档; - * sky 族无法区分 SKY/SURROUND,保守取请求档。 */ - int rl = level_from_str(p.level); - *out_level = (rl >= 0 && rl < lv) ? rl : lv; - } - return NCM_OK; - } - /* url 为 null 或未知格式:继续降级 */ + if (level_attempt(device_id, cookie, id, lv, try_fn, out, out_level)) return NCM_OK; } return NCM_ERR_NOTFOUND; } +int ncm_api_song_url_auto_av_f(const char *device_id, const char *cookie, const char *id, + int account_vip, int fee, int desired, + ncm_play_url_t *out, int *out_level, + ncm_song_url_try_fn try_fn) { + if (!try_fn) try_fn = ncm_api_song_url; + if (out_level) *out_level = -1; + /* fee 门禁(§20.4,BIZ_NOTES §3 社区口径):VIP 才能听(1=VIP 曲 / 4=付费专)且 + * 账户不足 CVIP → 不发任何 url 请求,直接 NCM_ERR_VIP(省请求,防风控); + * fee=0/8 放行(8=免费听低音质,播放允许)。 */ + if ((fee == 1 || fee == 4) && account_vip < NCM_VIP_CVIP) + return NCM_ERR_VIP; + /* 账户权限钳顶:desired 超出账户可达档 → 压到上限(免去注定空 url 的高档请求)。 + * 与 player_manager 的钳顶同口径,此处按真实 account_vip 自钳,避免调用方漏钳/ + * 错钳导致 SQ 被跳过。 */ + int cap = level_cap_for_vip(account_vip); + if (desired > cap) desired = cap; + if (desired < NCM_LEVEL_STANDARD) desired = NCM_LEVEL_STANDARD; + /* 逐级降级:只试账户可访问的档(ncm_api_level_gate(lv)<=account_vip),越权档跳过 + * (注定空 url,风控不试);落在「账户可访问且歌曲提供」的最近可用档。 */ + for (int lv = desired; lv >= NCM_LEVEL_STANDARD; lv--) { + if (ncm_api_level_gate(lv) > account_vip) continue; + if (level_attempt(device_id, cookie, id, lv, try_fn, out, out_level)) return NCM_OK; + } + return NCM_ERR_NOTFOUND; +} + +int ncm_api_song_url_auto_av(const char *device_id, const char *cookie, const char *id, + int account_vip, int fee, int desired, + ncm_play_url_t *out, int *out_level) { + return ncm_api_song_url_auto_av_f(device_id, cookie, id, account_vip, fee, desired, + out, out_level, NULL); +} + int ncm_api_song_url_auto(const char *device_id, const char *cookie, const char *id, int desired, ncm_play_url_t *out, int *out_level) { return ncm_api_song_url_auto_f(device_id, cookie, id, desired, out, out_level, NULL); diff --git a/patch_v2/ncm_app/core/ncm_api.h b/patch_v2/ncm_app/core/ncm_api.h index 4a27119..127e465 100644 --- a/patch_v2/ncm_app/core/ncm_api.h +++ b/patch_v2/ncm_app/core/ncm_api.h @@ -113,6 +113,21 @@ int ncm_api_song_url_auto_f(const char *device_id, const char *cookie, const cha int desired, ncm_play_url_t *out, int *out_level, ncm_song_url_try_fn try_fn); +/* 账户/fee 感知的降级链(§20.3 权威形态,修「SQ 被跳过」类降级链 bug): + * 内部先做 fee 门禁(fee∈{1,4} 且 account_vip