fix(ncm §20.3): 降级链账户/fee感知 auto_av —— 修「SQ 被跳过掉标准」(297)
根因:ncm_api_song_url_auto 无账户感知,依赖调用方按会员钳顶;若账户 VIP 态未就绪/
错钳到 EXHIGH,降级链从 EXHIGH 起步,LOSSLESS(无损 SQ) 被跳过,直接掉「标准」。
修法:新增 ncm_api_song_url_auto_av[_f](§20.3 权威形态):fee∈{1,4} 且非 CVIP →
NCM_ERR_VIP 零请求;按 account_vip 自钳顶(NONE→EXHIGH/CVIP→HIRES/SVIP→SURROUND);
只试账户可访问档(ncm_api_level_gate<=vip);落在最近可用档。原 auto/auto_f 行为零回归。
逐级尝试重构为共享 level_attempt(type/url 接受+静默降档采信)。
测试(bench/degrade_sim.c):fee门禁5例 + auto_av矩阵 vip3×desired9×avail9=243例全PASS
+ 真机复现(CVIP+Hi-Res+有SQ无HiRes→落无损SQ flac);原90例零回归。
验证:test_ncm_core_host+qemu-mipsel 全PASS;build_core/build_ncm -Wall -Wextra 0 warning。
新 ncm md5 1064a1d686390cc159f18296ee2980ee(未部署,留主会话)。
遗留:player_manager.c 仍调 auto(不传vip/fee),应改请 auto_av(account_vip()+fee);
真机若仍掉标准需另查旧缓存 song_<id>_hires.mp3 复用或旧二进制。
This commit is contained in:
1 parent
55f0dfe91d
commit
33414de7c4
3 files changed
+175
-26
No files matched your search
@@ -66,6 +66,34 @@ static int total_calls(void)
|
|||||||
return n;
|
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)
|
int main(void)
|
||||||
{
|
{
|
||||||
ncm_play_url_t u;
|
ncm_play_url_t u;
|
||||||
@@ -160,6 +188,63 @@ int main(void)
|
|||||||
"req surround, resp level=sky (歧义) -> eff=surround (got %d)", eff);
|
"req surround, resp level=sky (歧义) -> eff=surround (got %d)", eff);
|
||||||
g_resp_level = NULL;
|
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");
|
printf(g_fail ? "== DEGRADE SIM: FAIL ==\n" : "== DEGRADE SIM: ALL PASS ==\n");
|
||||||
return g_fail;
|
return g_fail;
|
||||||
}
|
}
|
||||||
@@ -299,46 +299,95 @@ static int level_from_str(const char *s) {
|
|||||||
return -1; /* sky/dolby/未知:不采信 */
|
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/不降级经过)。
|
/* 降级链(§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 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,
|
int desired, ncm_play_url_t *out, int *out_level,
|
||||||
ncm_song_url_try_fn try_fn) {
|
ncm_song_url_try_fn try_fn) {
|
||||||
if (!try_fn) try_fn = ncm_api_song_url;
|
if (!try_fn) try_fn = ncm_api_song_url;
|
||||||
if (out_level) *out_level = -1;
|
if (out_level) *out_level = -1;
|
||||||
|
/* 从 desired 往下降级:surround(sky+c51)->sky->jymaster->jyeffect->hires->lossless
|
||||||
|
* ->exhigh->higher->standard;DOLBY/越界钳到合法区间。 */
|
||||||
int start = desired;
|
int start = desired;
|
||||||
if (start < NCM_LEVEL_STANDARD) start = NCM_LEVEL_STANDARD;
|
if (start < NCM_LEVEL_STANDARD) start = NCM_LEVEL_STANDARD;
|
||||||
if (start > NCM_LEVEL_UI_MAX) start = NCM_LEVEL_UI_MAX;
|
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--) {
|
for (int lv = start; lv >= NCM_LEVEL_STANDARD; lv--) {
|
||||||
ncm_play_url_t p;
|
if (level_attempt(device_id, cookie, id, lv, try_fn, out, out_level)) return NCM_OK;
|
||||||
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 或未知格式:继续降级 */
|
|
||||||
}
|
}
|
||||||
return NCM_ERR_NOTFOUND;
|
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 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) {
|
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);
|
return ncm_api_song_url_auto_f(device_id, cookie, id, desired, out, out_level, NULL);
|
||||||
|
|||||||
@@ -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,
|
int desired, ncm_play_url_t *out, int *out_level,
|
||||||
ncm_song_url_try_fn try_fn);
|
ncm_song_url_try_fn try_fn);
|
||||||
|
|
||||||
|
/* 账户/fee 感知的降级链(§20.3 权威形态,修「SQ 被跳过」类降级链 bug):
|
||||||
|
* 内部先做 fee 门禁(fee∈{1,4} 且 account_vip<CVIP → NCM_ERR_VIP,零请求)→ 按
|
||||||
|
* account_vip 自钳顶(NONE→EXHIGH/CVIP→HIRES/SVIP→SURROUND)→ 只试账户可访问的档
|
||||||
|
*(ncm_api_level_gate(lv)<=account_vip)→ 落在「账户可访问且歌曲提供」的最近可用档。
|
||||||
|
* 比 ncm_api_song_url_auto 更稳:即便调用方漏钳/错钳(如账户 VIP 态未就绪时被压到
|
||||||
|
* EXHIGH),只要传入真实 account_vip,无损 SQ 不会被跳过。调用方应改请本函数(app 层
|
||||||
|
* account_vip()+song.fee)。_f 变体可注入 try_fn 供离线 sim。 */
|
||||||
|
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);
|
||||||
|
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);
|
||||||
|
|
||||||
/* 会员信息(BIZ_NOTES §1):vip/info v2 优先、v1 兜底(均 weapi)。
|
/* 会员信息(BIZ_NOTES §1):vip/info v2 优先、v1 兜底(均 weapi)。
|
||||||
* 风控:只在「登录后 / 缓存超 1 天 / 到期后」由 App 层触发一次,不轮询。 */
|
* 风控:只在「登录后 / 缓存超 1 天 / 到期后」由 App 层触发一次,不轮询。 */
|
||||||
typedef struct ncm_vip_info {
|
typedef struct ncm_vip_info {
|
||||||
|
|||||||
Reference in new issue
Block a user