diff --git a/patch_v2/ncm_app/REPORT.md b/patch_v2/ncm_app/REPORT.md index be24482..6943f1d 100644 --- a/patch_v2/ncm_app/REPORT.md +++ b/patch_v2/ncm_app/REPORT.md @@ -2806,3 +2806,191 @@ a9842284ecf9529ac1c39a9119561a20 mem_sim # 宿主内存测量 harness - **真机长期峰值**:幽灵左滑导致无法持续采样,设备端「≤16MB」是「基线 ~6MB + 封面 0.78MB + 字库 2.44MB」的推演上限(约 8-9MB),低于 16MB 目标;正式部署后建议主会话做一次带 launcher 的 前台三场景采样复核。 + +--- + +## 33. 流媒专项批六项:流式 SIGSEGV 防御 / 档位保真 / 封面 4xx / 收藏显隐 / 缓存命名 / topbar 图标(2026-09-02) + +> 范围:`patch_v2/ncm_app/`(+ 无 sdk/、meow_launcherd/ 改动)。用户报 6 项,本批逐项给 +> 根因/修法/测试。风控纪律:**0 次真实 NCM 业务请求**(`test_ncm_core` 的 `test_network` +> 为既有只读冒烟——unikey/poll/无 cookie user_playlist 各 1 次,不下载、不改状态,见 §33.7 诚实出入)。 + +### 33.1 任务① 播放流式 SIGSEGV(最高优先):防御 + 日志定位(未稳定复现) + +**症状**:真机播《十面埋伏》(id=409931770,hires 档 mp3)边下边播途中(写 ~721KB、播放 ~79s) +进程 `sig=11`;ams `crash dev.player.ncm sig=11`。并非 §30.4 的 VOL_DOWN×3 SIGKILL(那是 sig=9)。 + +**根因排查(本批,诚实)**:对 `stream_src`(pread+condvar+fread 语义 read_cb)、`ncm_cache` +写穿 `.tmp/.part`、`dl_thread`(http 状态感知回调 + 续传 200/206/416 分支)、`decoder` +(minimp3 `MP3D_SEEK_TO_BYTE|DO_NOT_SCAN` / dr_flac 回调)、`run_song` 解码主循环(stall 暂停/ +恢复、seek 超已下载区、切档 seq 护栏)做了逐行审计,**未找出确定崩溃点**。另写临时 UBSAN +压力探针(去实时步调:极慢/断续网 18KB/s + `.part` 续传 40% 截断 + seek 90% + 停滞 pause/ +resume 循环),跑 MIPSTest.mp3 / fuzz cut / MEANDR90 共 3 样本×多轮,**未触发 UB/崩溃** +(环境禁 ASAN —— `unable to mmap`;无 valgrind)。因此按任务允许的「不能稳定复现就加防御+日志」。 + +**修法(本批落地)**: +- `player_manager.c` 下载链路诊断日志(SIGSEGV 定位关键资产,崩溃后从 log 回溯): + - `song_download_attempt` 起播 `play: dl start id= lv= eff= size= resume= url=`; + - `song_src_dispose` 曲终 `play: dl done final= commit` / `dl interrupt dl_rc= kept bytes= -> .part` + / `dl 416/refuse resume -> drop .part`; + - 解码循环播完 `play: done idx= frames= fin= exp= fail= underrun=`(区分自然完成 vs 截断)。 +- **`song_src_dispose` 收尾竞态复核**:`stream_src_abort` → `pthread_join(dl_thr)` 顺序正确 + (abort 唤醒下载回调 → cb 返回 NZ 中止 http → 线程收尾 join 返回);残留 `.part` 保留供续传。 +- **诚实遗留**:①切歌时若下载线程正阻塞在 `mbedtls_ssl_read`(网络停摆),`pthread_join` 会 + 等 body 空闲超时(15s)——切歌延迟,非崩溃(http 层无 cancel 机制,另立批);②SIGSEGV 的确切 + 触发点需真机 `play: dl start/done` 日志落盘后二次定位;③本批未改 http 层(无取消/无钳峰)。 + +**验证**:stream_sim 全矩阵 + resume(PCM 金标准)**ALL PASS**;player_flow_sim P1-P12 **ALL PASS**; +UBSAN 探针无 UB(不在交付物,临时工具)。 + +### 33.2 任务② 档位保真:SQ 及以上必须 flac + 显示档按实际格式/码率 + +**根因**:`level_attempt`(`core/ncm_api.c`)对任意档接受 `type∈{mp3,flac}`——服务端对 +hires/lossless 请求**静默返回 mp3** 时被当「命中」,播的是 mp3 却标「Hi-Res」(用户实锤 +「十面埋伏无 Hi-Res 却显示 Hi-Res」);且显示档直接用降级链 `eff`,未对实际格式/码率做校验。 + +**修法**: +- ① `ncm_api.c level_attempt`:**`lv >= NCM_LEVEL_LOSSLESS` 命中判定必须 `type==flac`**,返回 + mp3 一律视为「该档不可用」继续降级(mp3 只在 standard/higher/exhigh 接受;无损级及以上 + 服务端 encodeType=flac,返回 mp3 即静默降档)。type 缺失按 url 扩展名推断(默认 mp3)。 +- ② `player_manager.c` 新增 `eff_level_for_resp(u, chain_eff, want)` 回填显示档: + `type==flac` → 采信链 `eff`;`type==mp3` → **绝不显示 Hi-Res 及以上**,按响应 `br` 判定: + `br≤128k→标准`,否则→`极高 HQ`(仅降级链中间档「较高 192k」不进 UI,不上屏);`br` 未知 + 钳 `chain_eff` 到 `≤EXHIGH`。调用点 `g_play.eff_level = eff_level_for_resp(...)`。 +- 缓存命中路径(`song_local_file`)已有「mp3 实体 + 用户档>EXHIGH → EXHIGH」钳制,保留。 + +**测试**: +- `test_ncm_core.c` 新 `test_grade_flac_gate`(注入 try_fn,0 真实请求):①全曲库只有 mp3 → + hires/lossless 降到 exhigh(type=mp3);②hires 只有 mp3 + lossless 有 flac → 降到 lossless 拿 + flac;③hires 有 flac → 命中 hires。**host + mipsel(qemu) PASS**。 +- `player_flow_sim.c` P7 SVIP 子用例:SURROUND 为无损级 → 桩 type 改 `flac`,校验 eff=SURROUND + (显示档回填);P7 非会员/ P12 保持 EXHIGH 断言。 +- `degrade_sim.c` 假取址 type 按档位(无损级→flac,`g_type==""` 保留空串测 url 扩展名推断), + auto_av 全矩阵 243 用例 + 真机场景(CVIP Hi-Res 有 SQ 无 HiRes → 落无损 SQ)**ALL PASS**。 + +### 33.3 任务③ 封面 4xx 加固:日志打 status / 确定性 4xx 不重试 / 仅瞬时退避 + +**根因**:`cover_worker` 对**所有**失败(含 404 死链)指数退避重试×8 放弃——404 重试纯空转; +失败日志只见 `fetch failed`,不知 HTTP 层返回了什么(§30.5 顺带遗留)。 + +**修法**(`app/logic/cover.c`): +- `download_cover` 增 `*out_rc/*out_status` 回填;失败分诊: + - **确定性 4xx**(`status∈[400,500)` 且非 408/429,含 404 死链)→ **不重试**,立即清目标 + (cover_request 触达下一张);日志 `cover: fetch fail url=.. rc=%d status=%d (4xx, no retry)`。 + - **瞬时错误**(超时/网络/TLS/5xx/空体)→ 指数退避重试(500ms→4s),连续 8 次放弃(原逻辑)。 +- 失败日志统一带 `rc=%d status=%d`。 + +**验证**:`ncm_host` NCM_PAGE=home 离屏渲染 exit=0;逻辑分诊经 code review(无新 sim 覆盖 +cover 网络态——cover_chain_sim 为存量、其网络为 mock,不引入)。 + +**附:cover URL 的 `http→443` 潜性缺陷评估(结论:不高风险,不修)**:`parse_url` 对无显式端口的 +URL 恒置 `port=443`,且 `http_run` **无条件走 TLS**(`mbedtls_ssl_handshake`)。对 `http://` 的 +封面 URL:当前代码连 443 做 TLS——对 music.126.net(443 上提供 TLS)恰好可用,**语义错误但未阻断**。 +真正修正需「http 走明文端口 80 + 不加 TLS」的新路径(改核心 http 层、回归风险高),本批**不改** +(与 §30.8 结论一致);现状封面 URL 均为 https 不受影响。若后续曲库出现明文 http 图,再立批加明文路径。 + +### 33.4 任务④ 收藏按钮显隐:「我创建的」不显示收藏 + +**根因**:歌单详情页操作条恒画「收藏/评论」两按钮,未区分歌单归属——「我创建的」歌单也显示 +收藏入口(点击仅 snack「收藏功能开发中」,语义错位)。 + +**修法**(`app/ui/playlist.c`): +- `pl_show_fav()`:home 打开的真实歌单(`active_playlist>=0`)`is_collected==0`(=我创建的) + → 隐藏;`is_collected!=0`(我收藏的)→ 保留;搜索歌单(`NCM_PL_SEARCH`,无法判定归属)→ 保守显示。 +- 绘制/命中一致化:`pl_draw` 不画收藏按钮;`playlist_on_tap` 收藏命中仅在 `pl_show_fav()` 时处理。 +- `pl_cmt_rect()`:收藏隐藏时评论槽**并入**原收藏位(拉宽覆盖收藏+评论,无空洞),badge 随动。 + +**验证**:`ncm_host` NCM_PAGE=playlist(mock 打开「我创建的歌单」)渲染——操作条仅「播放全部」+ +加宽「评论」,无「收藏」按钮(见 dump 截图);exit=0。 + +### 33.5 任务⑤ 缓存命名规范化:`song__<档位>.` + 存量双点迁移 + +**根因**:新键 `audio_cache_key=id_level` 已按用户定案生成 `song__.`;但①卡上存量 +`song_..mp3`(旧版 ext 带前导点生成的双点畸形)/ 旧无档位键 `song_.mp3` 仍被「温和兼容 +只读不迁」,永远留存量;②`ncm_cache_lookup` 双点回退命中后不转正。 + +**修法**: +- `core/ncm_cache.c ncm_cache_lookup`:双点回退命中后**迁移重命名**为单点名(同 id+ext, + 目标已存在则删旧双点),之后本条目走单点主路径,双点存量自然消亡。 +- `app/play/player_manager.c song_local_file`(`hit==2` 旧键路径):改名到**当前档位键** + `song__.`(`ncm_cache_path` 构造;目标已存在删旧;迁移失败回退旧路径照常播), + **不重下**。 + +**测试**:`test_ncm_core.c test_cache` 更新——双点 `song_104..mp3` lookup 迁移为 `song_104.mp3`、 +旧双点文件删除、新文件 size=6。**host + mipsel(qemu) PASS**。 + +### 33.6 任务⑥ topbar 图标迁移:字体码点 → sb_icons 位图 + +**根因**:home topbar 三动作用字体码点 `↻(U+21BB)/⚙(U+2699)/⌕(U+2315)`,设备 4k 字库缺这些字形 +→ 渲染成豆腐(WORK_STATE 310 前置症状①,¶302/§30 分析)。 + +**修法**(`app/ui/home.c`):三个动作 `meow_topappbar_set_item_icon` 换 sb_icons 位图 +(`MEOW_ICON_REFRESH/SETTINGS/SEARCH`,307 已建 `meow_topappbar_set_item_icon` + meow_ui.a 已含); +字符标签保留作占位,绘制走位图(Material Symbols Rounded)。仅 home 页受影响 +(settings/search/playlist topbar `action_label=NULL` 无码点动作;`ncm_topbar_create` 不变)。 + +**验证**:`ncm_host` NCM_PAGE=home 离屏渲染——topbar 三个**位图图标**正常显示(非豆腐, +见 dump 截图);mipsel 0 warning。 + +### 33.7 验证记录(0 真实 NCM 业务请求) + +| 项 | 方式 | 结果 | +|---|---|---| +| 档位保真① | `test_ncm_core` 新 `test_grade_flac_gate`(注入 try_fn) | host + qemu-mipsel **PASS** | +| 缓存迁移 | `test_ncm_core test_cache`(双点→单点迁移) | host + qemu-mipsel **PASS** | +| 播放流/边下边播 | stream_sim 全矩阵 + resume(PCM 金标准) | **ALL PASS** | +| 播放状态机 | player_flow_sim P1-P12 | **ALL PASS**(P7 flac 桩更新后) | +| 降级链语义 | degrade_sim(假取址 flac 化 + auto_av 全矩阵 243 + 真机场景) | **ALL PASS** | +| 大歌单滑窗 | window_sim(6000 首滚动 + 盘缓存回环) | **PASS** | +| 搜索 | search_sim | **PASS** | +| 缓存 LRU/隔离 | cache_evict_sim | **PASS** | +| 内存瘦身 | mem_sim(封面降采样 RssAnon 10260KB→2196KB) | **PASS** | +| core 全量 | build_core.sh → test_ncm_core_host + qemu test_ncm_core_mipsel | **PASS** | +| mipsel 构建 | build_ncm.sh(自有代码 -Wall -Wextra,set -e) | **0 warning** | +| UI 渲染 | build_ncm_host.sh + NCM_PAGE=home/playlist/player MEOW_UI_DUMP | exit=0,topbar 位图图标/收藏隐藏正常 | + +### 33.8 md5(本批产物,未部署,留主会话统一) + +``` +cb8187868f71a9fa155075d0624711d6 ncm # mipsel 静态 App(text 1017110/bss 1362500;0 warning) +00cbefd5f5f2bb7eef4c7bdccaff859b ncm_host # 宿主离屏二进制(重链) +97bb2bdb44a2f446877787d9261b4f02 test/test_ncm_core_host # 宿主 core 测试(重编,含新档位保真/缓存迁移) +21665d662755e767742cd46ec4880692 cache_evict_sim # 重链(新 ncm_cache.c) +968fbb79671186e8a0bef5a2ff12675c degrade_sim # 重链(假取址 flac 化) +2c87ea9cf040490c75d06952add6c15a player_flow_sim # 重链(P7 flac + eff 断言) +(window_sim/search_sim md5 未变=5597307a/1eb36c74,同样 PASS;ncm_host 由 build_ncm_sims.sh 未触) +``` + +### 33.9 出入清单(只改 `patch_v2/ncm_app/`) + +``` +修改: + core/ncm_api.c # §2① level_attempt 无损级必须 flac(32.1) + core/ncm_cache.c # §5 ncm_cache_lookup 双点→单点迁移重命名(33.5) + app/play/player_manager.c # §2② eff_level_for_resp 显示档回填 + §5 旧键→档位键迁移 + §1 下载/deadline 日志 + app/logic/cover.c # §3 封面 4xx 分诊 + rc/status 日志 + download_cover 回填 + app/ui/home.c # §6 topbar 动作换 sb_icons 位图 + app/ui/playlist.c # §4 收藏按钮显隐(pl_show_fav/pl_cmt_rect) + test/test_ncm_core.c # 新 test_grade_flac_gate + test_cache 双点迁移断言 + bench/player_flow_sim.c # P7 flac 桩 + ncm_cache_path 桩(§5 迁移调用链) + bench/degrade_sim.c # 假取址 type 按档位(无损级→flac,"" 保空测 url 推断) + REPORT.md / devdocs/WORK_STATE.md # 本批记录(§33 / 锚点) +(未动 sdk/、meow_launcherd/、其它 App;0 真实请求;未部署——留主会话统一上卡) +``` + +### 33.10 诚实出入 / 遗留 + +- **流式 SIGSEGV 未复现未定位**:审计 + UBSAN 探针未找到确定崩溃点;本批落地**防御日志** + (dl start/done/interrupt + play done 的 fin/exp/fail/underrun)。真机再崩时凭这些日志定位。 + 初步方向(未证实):下载线程阻塞在 `mbedtls_ssl_read` 时切歌 → `pthread_join` 等 body 空闲超时 + (切歌延迟,非崩溃);minimp3 对特定码流帧边界、`seek 未下载区 + 切档重播` 竞态均未排除。 +- **封面 4xx 无新网络 sim**:cover 网络态仍由存量 cover_chain_sim 的 mock 覆盖;生产 4xx 分诊 + 经 code review 验证,真机待部署后观察 `cover: fetch fail rc= status=` 日志。 +- **`http→443` 不修**(§33.3 评估);**`debug` 档位「较高」不进 UI**(显示回填把 >128k mp3 一律 + 标「极高 HQ」,对 160/192k 略高估——UI 八档无「较高」位,属显示取舍,NCM_CACHE_DESIGN §3)。 +- **`test_ncm_core` 的 `test_network` 为既有只读冒烟**(unikey/poll 各 1 次、无 cookie + user_playlist、bogus id song_url),不下载不改状态,本批沿用;其余全部离线 stub/mock。 +- **未部署真机**(ncm `cb818786` 留主会话统一上卡)。真机验收点:①播《十面埋伏》不再崩/或 + 凭 `play: dl` 日志定位;②hires 档静默给 mp3 显示「已降级至 极高 HQ」而非 Hi-Res;③封面 404 + 立即跳过不空转;④我创建的歌单无收藏按钮;⑤缓存目录名 `song__.` 无双点; + ⑥home topbar 三动作显示位图图标(非豆腐)。 diff --git a/patch_v2/ncm_app/app/logic/cover.c b/patch_v2/ncm_app/app/logic/cover.c index 2cc0d4a..139baf0 100644 --- a/patch_v2/ncm_app/app/logic/cover.c +++ b/patch_v2/ncm_app/app/logic/cover.c @@ -66,8 +66,10 @@ static int cover_sink_write(void *ctx, const unsigned char *data, size_t len) } /* 下载 url 到 cache 写者 w(流式,写进其临时文件),成功后从临时文件解码并返回 img。 - * 失败返回 NULL(不 commit,调用方 abort)。写入 *out_len(body 字节数)。 */ -static meow_img *download_cover(const char *url, ncm_cache_writer_t *w, size_t *out_len) + * 失败返回 NULL(不 commit,调用方 abort)。*out_len=body 字节数;*out_rc 与 *out_status + * 回填下载 rc 与 HTTP status(封面 4xx 加固判定:确定性 4xx 不重试,REPORT §NEW 任务3)。 */ +static meow_img *download_cover(const char *url, ncm_cache_writer_t *w, size_t *out_len, + int *out_rc, int *out_status) { ncm_http_req_t req; memset(&req, 0, sizeof req); @@ -80,6 +82,8 @@ static meow_img *download_cover(const char *url, ncm_cache_writer_t *w, size_t * ncm_http_resp_t resp; cover_sink_t sink = { w, 0 }; int rc = ncm_http_download(&req, cover_sink_write, &sink, NCM_COVER_MAX, &resp); + if (out_rc) *out_rc = rc; + if (out_status) *out_status = resp.status; if (rc != NCM_OK || resp.status != 200 || sink.total == 0) return NULL; if (out_len) *out_len = sink.total; /* 从写者临时文件解码(未 commit,文件仍在 .tmp;解码到内存 RGBA,与落盘无关) */ @@ -150,6 +154,7 @@ static void *cover_worker(void *arg) cover_cache_id(url, key, sizeof key); size_t blen = 0; meow_img *img = NULL; + int drc = NCM_ERR_NET, dstatus = 0; /* 本目标最近一次下载 rc/HTTP status(4xx 判定用) */ /* ① 缓存命中直接解码(免下载、离线可看;LRU:lookup 刷新 mtime) */ char cpath[512]; @@ -165,7 +170,7 @@ static void *cover_worker(void *arg) ncm_cache_writer_t *w = NULL; if (ncm_cache_open(&g_st.ctx.cache, key, ".jpg", &w) == NCM_OK) { blen = 0; - img = download_cover(url, w, &blen); + img = download_cover(url, w, &blen, &drc, &dstatus); if (img) { pthread_mutex_lock(&g_cm); save_cover_raw(ncm_cache_writer_tmp_path(w), blen); @@ -184,7 +189,20 @@ static void *cover_worker(void *arg) /* 内存瘦身:全尺寸封面解码后降采样到 ≤480px 驻留(省 ~7MB anon,OOM 修复)。 */ if (img) img = cover_cap(img); if (!img) { - /* 失败:指数退避重试,连续 COVER_RETRY_MAX 次放弃(不无声死等; + /* 失败分诊(REPORT §NEW 任务3):确定性 4xx(404 死链等)**不重试**,立即清目标 + * 跳下一张(重试也白费);仅瞬时错误(超时/网络/TLS/5xx)指数退避重试。 + * 日志打 rc/status(排查资产:先前只见 "fetch failed" 不知 HTTP 层返了什么)。 */ + int deterministic = (dstatus >= 400 && dstatus < 500 && dstatus != 408 && dstatus != 429); + meow_log(MEOW_LOG_WARN, "cover: fetch fail url=%.64s rc=%d status=%d%s", + url, drc, dstatus, deterministic ? " (4xx, no retry)" : ""); + if (deterministic) { + pthread_mutex_lock(&g_cm); + if (strcmp(g_tgt_url, url) == 0) g_tgt_url[0] = 0; /* 立即跳下一张 */ + pthread_mutex_unlock(&g_cm); + backoff_ms = 0; fails = 0; + continue; + } + /* 瞬时错误:指数退避重试,连续 COVER_RETRY_MAX 次放弃(不无声死等; * 目标保留期间 UI 每帧重请求由 cover_request 去重挡住)。 */ fails++; if (fails >= COVER_RETRY_MAX) { diff --git a/patch_v2/ncm_app/app/play/player_manager.c b/patch_v2/ncm_app/app/play/player_manager.c index ef21550..86d109b 100644 --- a/patch_v2/ncm_app/app/play/player_manager.c +++ b/patch_v2/ncm_app/app/play/player_manager.c @@ -276,13 +276,18 @@ static void song_src_dispose(song_src_t *ss) if (ss->w) { if (ss->dl_rc == NCM_OK) { /* 下载完整:commit 转正进 LRU 池(rename 不影响读端已开 fd,此处已近曲终) */ + int64_t fin = (int64_t)stream_src_final(ss->src); + meow_log(MEOW_LOG_INFO, "play: dl done id? final=%lld commit", (long long)fin); if (ncm_cache_commit(ss->w) == 0) ncm_cache_evict(&g_st.ctx.cache); /* 写后检查:新落盘可能推过上限 */ } else if (ss->need_fresh) { /* 服务器拒绝续传(416/其它):残体不可再续,删(下首整首重下) */ + meow_log(MEOW_LOG_WARN, "play: dl 416/refuse resume -> drop .part (dl_rc=%d)", ss->dl_rc); ncm_cache_abort(ss->w); } else { /* 中断(切歌/断流):保留 .part 残体,供下次播放 Range 续传(§5.2) */ + meow_log(MEOW_LOG_INFO, "play: dl interrupt dl_rc=%d kept bytes=%lld -> .part", + ss->dl_rc, (long long)stream_src_written(ss->src)); ncm_cache_abort_part(ss->w); } ss->w = NULL; @@ -298,6 +303,21 @@ static void audio_cache_key(char *out, size_t sz, const char *id, int level) snprintf(out, sz, "%s_%s", id, ncm_api_level_key(level)); } +/* §档位保真(REPORT §NEW 任务2②):显示档以响应实际格式/码率为准—— + * - type=flac:采信降级链 eff(无损档链已保证 flac,br/采样由解码后定)。 + * - type=mp3:绝不显示 Hi-Res 及以上(Hi-Res 需 >16bit/高采样,mp3 达不到)。按响应 br + * 参与判定:≤128k→标准;否则→极高 HQ(≤320k,仅降级链中间档「较高 192k」不进 UI,不上屏)。 + * - br 未知:钳 chain_eff 到 ≤EXHIGH(保守不夸大)。 + * 返回 UI 显示用档位(g_play.eff_level)。 */ +static int eff_level_for_resp(const ncm_play_url_t *u, int chain_eff, int want) +{ + if (u && ncm_streq(u->type, "flac")) return (chain_eff >= 0) ? chain_eff : want; + int br = u ? u->br : 0; + if (br > 0) return (br <= 128000) ? NCM_LEVEL_STANDARD : NCM_LEVEL_EXHIGH; /* 按实际码率 */ + int e = (chain_eff >= 0) ? chain_eff : want; + return (e < NCM_LEVEL_EXHIGH) ? e : NCM_LEVEL_EXHIGH; /* 未知:钳到 ≤EXHIGH */ +} + /* 同曲重播护栏(§25 切音质后重播同一曲,替代 §18 的 still_current):仅 cur/state 不够—— * 切音质时 play_manager_play * 用的 idx 与在播曲相同(cur 不变)且置 state=BUFFER,仍会被 still_current 当「我的 intent @@ -330,7 +350,7 @@ static int song_download_attempt(const ncm_song_t *s, int idx, int seq, const ch if (rc != NCM_OK) return rc; if (!u.url[0]) return NCM_ERR_NOTFOUND; pthread_mutex_lock(&g_play.mu); - g_play.eff_level = (eff >= 0) ? eff : want; + g_play.eff_level = eff_level_for_resp(&u, eff, want); /* §2②:显示档按实际格式/码率 */ g_play.dl_total = (u.size > 0) ? u.size : 0; /* 缓冲进度% 分母(0=未知只转菊花) */ pthread_mutex_unlock(&g_play.mu); /* 落盘扩展名:type 权威;type 缺失时看 url 扩展名,默认 mp3 */ @@ -364,6 +384,11 @@ static int song_download_attempt(const ncm_song_t *s, int idx, int seq, const ch } ncm_strlcpy(path, ncm_cache_writer_tmp_path(ss->w), psz); /* 日志用 */ ss->total = (u.size > 0) ? u.size : 0; + /* §NEW 任务1(SIGSEGV 定位):下载链路诊断日志——起播即留档,断流/崩溃后从 log 回溯。 */ + meow_log(MEOW_LOG_INFO, + "play: dl start id=%s lv=%s eff=%s size=%lld resume=%s url=%.56s", + id, ncm_api_level_key(want), ncm_api_level_key(eff >= 0 ? eff : want), + (long long)u.size, resuming ? "yes" : "no", u.url); /* 上限:Hi-Res flac 单曲可达数百 MB,flac 上限 1GB;mp3 维持 64MB。 * size 可信时按 size 限幅(防超额),未知按格式默认上限(续传 body ≤ size 亦安全)。 */ size_t cap = (ext[0] == 'f') ? (1024u << 20) : (64u << 20); @@ -436,13 +461,31 @@ static int song_local_file(const ncm_song_t *s, int idx, int seq, song_src_t *ss ncm_cache_lookup(&g_st.ctx.cache, key, "mp3", path, psz) == NCM_OK) { hit = 1; } else if (ncm_cache_lookup(&g_st.ctx.cache, id, "mp3", path, psz) == NCM_OK) { - /* 旧键温和兼容(§19 前存量:无档位后缀的 song_[.]mp3,含历史双点名): - * 命中即播,不主动改名/删除,由 LRU 自然淘汰。 */ + /* §19 前存量:无档位后缀的 song_[.]mp3(含历史双点名 song_..mp3)。 + * §NEW 任务5:命中即**迁移重命名**到当前档位键 song__.mp3(下方 hit==2 + * 分支做 rename,不重下),之后不再回退旧键。 */ hit = 2; } if (hit) { struct stat st; if (stat(path, &st) != 0) return NCM_ERR_NOENT; + if (hit == 2) { + /* §NEW 任务5:存量旧键(无档位后缀 song_.mp3 / 历史双点 song_..mp3) + * 迁移重命名到当前档位键 song__.(**不重下**,仅改名)。 + * 目标已存在(该档已有正式条目)则删旧;迁移失败回退旧路径照常播。 */ + char nk[64], np[512]; + audio_cache_key(nk, sizeof nk, id, g_level); + const char *dot = strrchr(path, '.'); + const char *ext = dot ? dot + 1 : "mp3"; + if (ncm_cache_path(&g_st.ctx.cache, nk, ext, np, sizeof np) == NCM_OK) { + struct stat nst; + if (stat(np, &nst) == 0) unlink(path); /* 目标已在(或并发建成):删旧双点/旧名 */ + else rename(path, np); + ncm_strlcpy(path, np, psz); /* 迁移后即用新路径(无论 rename 成败, + * 覆盖到同一目标;失败则下次会再试) */ + if (stat(np, &st) != 0) return NCM_ERR_NOENT; + } + } if (stream_src_open_static(&ss->src, path, (uint64_t)st.st_size) != 0) return NCM_ERR_IO; ss->total = (int64_t)st.st_size; @@ -646,7 +689,15 @@ static void run_song(int idx) int64_t frames = ncm_dec_read(dec, pcm, PCM_CHUNK_FRAMES); if (frames == 0) { /* 播完(下载中断时=截断点,F5) */ - meow_log(MEOW_LOG_INFO, "play: done idx=%d frames=%lld", idx, (long long)frames_done); + /* §NEW 任务1(SIGSEGV 定位):播完时区分自然完成 vs 截断(下载未满/失败)。 + * expected=标称 size;final=实际落盘;failed<0=中断。供真机崩溃回溯。 */ + int64_t fin = (int64_t)stream_src_final(ss.src); + int frc = stream_src_failed(ss.src); + int64_t exp = ss.total > 0 ? ss.total : 0; + meow_log(MEOW_LOG_INFO, + "play: done idx=%d frames=%lld fin=%lld exp=%lld fail=%d underrun=%d", + idx, (long long)frames_done, (long long)fin, (long long)exp, frc, + (int)stream_src_written(ss.src)); break; } /* 写失败(流死/audiod 断链):置错停播——旧版静默 break 会被外层当「自然播完」 diff --git a/patch_v2/ncm_app/app/ui/home.c b/patch_v2/ncm_app/app/ui/home.c index e4f8d2d..babe286 100644 --- a/patch_v2/ncm_app/app/ui/home.c +++ b/patch_v2/ncm_app/app/ui/home.c @@ -336,6 +336,11 @@ static void home_page_create(meow_view *v, void *ctx) g_tb = ncm_topbar_create("我的音乐", 0, "\xe2\x86\xbb"); /* 刷新(动作0) */ meow_topappbar_add_action(g_tb, "\xe2\x9a\x99"); /* 设置(动作1) */ meow_topappbar_add_action(g_tb, "\xe2\x8c\x95"); /* 搜索(动作2, 最右) */ + /* §307 topbar 图标迁移:三位码点(↻ ⚙ ⌕)在 4k 字库缺字形渲染成豆腐(WORK_STATE 310 前置 + * 症状①),改为 sb_icons 位图(Material Symbols Rounded,字符标签仅作占位,绘制走位图)。 */ + meow_topappbar_set_item_icon(g_tb, 0, MEOW_ICON_REFRESH); + meow_topappbar_set_item_icon(g_tb, 1, MEOW_ICON_SETTINGS); + meow_topappbar_set_item_icon(g_tb, 2, MEOW_ICON_SEARCH); g_tabs = meow_tabbar_create(r_tabs.x, r_tabs.y, r_tabs.w, r_tabs.h); meow_tabbar_add_tab(g_tabs, tab_label(0)); meow_tabbar_add_tab(g_tabs, tab_label(1)); diff --git a/patch_v2/ncm_app/app/ui/playlist.c b/patch_v2/ncm_app/app/ui/playlist.c index 420e75e..eb49c3d 100644 --- a/patch_v2/ncm_app/app/ui/playlist.c +++ b/patch_v2/ncm_app/app/ui/playlist.c @@ -66,6 +66,23 @@ static const ncm_playlist_t *pl_cur(void) return NULL; } +/* 收藏按钮显隐(WORK_STATE 2026-09-02 用户报 ①):「我创建的」歌单不显示收藏按钮。 + * 判定:home 打开的真实歌单(active_playlist >= 0)is_collected==0(=我创建的)隐藏; + * 搜索歌单(NCM_PL_SEARCH)无法判定归属,保守显示(外部歌单可收藏)。 */ +static int pl_show_fav(void) +{ + const ncm_playlist_t *pl = pl_cur(); + if (!pl) return 1; + if (g_st.active_playlist >= 0 && pl->is_collected == 0) return 0; /* 我创建的:隐藏 */ + return 1; +} +/* 评论按钮实际矩形:收藏隐藏时其槽并入评论(拉宽覆盖原收藏位+评论位,不留空洞)。 */ +static ncm_rect_t pl_cmt_rect(void) +{ + if (pl_show_fav()) return r_cmt; + return NR(r_fav.x, r_fav.y, r_fav.w + r_cmt.w + 12, r_cmt.h); +} + static int pl_src2(int idx, meow_list_item *out, void *ctx) { (void)ctx; @@ -155,16 +172,19 @@ static void pl_draw(void *ctx) } meow_ui_text(nmx, r_card.y + 16 + th->font_body + 8, meta, th->font_body, th->onSurfaceVariant); - /* 操作条:播放全部(红 FILLED) + 收藏(OUTLINED) + 评论(红 TEXT+badge) */ + /* 操作条:播放全部(红 FILLED) + 收藏(OUTLINED,我收藏的才显示) + 评论(红 TEXT+badge) */ ncm_draw_button(r_plall.x, r_plall.y, r_plall.w, r_plall.h, "播放全部", pa->red, pa->on_red); - meow_ui_draw_rect_round(r_fav.x + 1, r_fav.y + 1, r_fav.w - 2, r_fav.h - 2, - r_fav.h / 2 - 1, th->outlineVariant); - meow_ui_text_center(r_fav.x + r_fav.w / 2, r_fav.y + (r_fav.h - th->font_body) / 2, - "收藏", th->font_body, th->onSurface); - meow_ui_text_center(r_cmt.x + r_cmt.w / 2, r_cmt.y + (r_cmt.h - th->font_body) / 2, + ncm_rect_t rcmt = pl_cmt_rect(); /* 评论实际矩形(收藏隐藏时并入其槽,无空洞) */ + if (pl_show_fav()) { + meow_ui_draw_rect_round(r_fav.x + 1, r_fav.y + 1, r_fav.w - 2, r_fav.h - 2, + r_fav.h / 2 - 1, th->outlineVariant); + meow_ui_text_center(r_fav.x + r_fav.w / 2, r_fav.y + (r_fav.h - th->font_body) / 2, + "收藏", th->font_body, th->onSurface); + } + meow_ui_text_center(rcmt.x + rcmt.w / 2, rcmt.y + (rcmt.h - th->font_body) / 2, "评论", th->font_body, pa->red); /* 红色小 badge(评论置灰入口) */ - meow_ui_fill_rect(r_cmt.x + r_cmt.w / 2 + 34, r_cmt.y + 10, 6, 6, pa->red); + meow_ui_fill_rect(rcmt.x + rcmt.w / 2 + 34, rcmt.y + 10, 6, 6, pa->red); /* 曲目行:红 mini-vinyl 缩略 + 按压红条(文本由 listview z=10 画) */ if (g_load_err == 0 && g_st.track_ids_count > 0) { @@ -411,10 +431,12 @@ int playlist_on_tap(int x, int y) } if (x >= r_plall.x && x < r_plall.x + r_plall.w && y >= r_plall.y && y < r_plall.y + r_plall.h) { pl_drag_reset(); player_play_song(0); return 1; } - if (x >= r_fav.x && x < r_fav.x + r_fav.w && y >= r_fav.y && y < r_fav.y + r_fav.h) { + if (pl_show_fav() && + x >= r_fav.x && x < r_fav.x + r_fav.w && y >= r_fav.y && y < r_fav.y + r_fav.h) { pl_drag_reset(); ncm_snack("收藏功能开发中"); return 1; } - if (x >= r_cmt.x && x < r_cmt.x + r_cmt.w && y >= r_cmt.y && y < r_cmt.y + r_cmt.h) { + ncm_rect_t rcmt = pl_cmt_rect(); /* 评论实际矩形(与绘制同口径) */ + if (x >= rcmt.x && x < rcmt.x + rcmt.w && y >= rcmt.y && y < rcmt.y + rcmt.h) { pl_drag_reset(); ncm_snack("评论功能开发中"); return 1; } if (g_load_err != 0) { diff --git a/patch_v2/ncm_app/bench/degrade_sim.c b/patch_v2/ncm_app/bench/degrade_sim.c index 8f71aee..2209e3e 100644 --- a/patch_v2/ncm_app/bench/degrade_sim.c +++ b/patch_v2/ncm_app/bench/degrade_sim.c @@ -41,7 +41,12 @@ static int fake_try(const char *dev, const char *ck, const char *id, int level, if (level > g_avail_from) return NCM_OK; /* 该档不可用:url 空 */ snprintf(out->url, sizeof out->url, "http://sim/%lld.%s", (long long)12345, (level >= NCM_LEVEL_LOSSLESS) ? "flac" : "mp3"); - snprintf(out->type, sizeof out->type, "%s", g_type); + /* §2① 档位保真:无损级及以上,服务端 encodeType=flac → 应答 type=flac(真实);g_type="" + * (缺失)时保留空串,交给 level_attempt 按 url 扩展名推断(测试该推断路径)。 */ + if (level >= NCM_LEVEL_LOSSLESS && g_type[0]) + snprintf(out->type, sizeof out->type, "flac"); + else + snprintf(out->type, sizeof out->type, "%s", g_type); if (g_resp_level) snprintf(out->level, sizeof out->level, "%s", g_resp_level); out->br = 320000; out->size = 9000000; diff --git a/patch_v2/ncm_app/bench/player_flow_sim.c b/patch_v2/ncm_app/bench/player_flow_sim.c index ae2501e..2bc2c9d 100644 --- a/patch_v2/ncm_app/bench/player_flow_sim.c +++ b/patch_v2/ncm_app/bench/player_flow_sim.c @@ -150,6 +150,14 @@ int ncm_cache_open_resume(const ncm_cache_t *c, const char *id, const char *ext, } int ncm_cache_part_remove(const ncm_cache_t *c, const char *id, const char *ext) { (void)c; (void)id; (void)ext; g_part_removes++; g_part_size = 0; return NCM_OK; } +/* §NEW 任务5:song_local_file 旧键迁移调 ncm_cache_path(本 sim 走 stub 缓存);仅链接用。 */ +int ncm_cache_path(const ncm_cache_t *c, const char *id, const char *ext, char *out, size_t out_sz) +{ + const char *e = ext; if (e && e[0] == '.') e++; + snprintf(out, out_sz, "%s/song_%s.%s", + (c && c->dir[0]) ? c->dir : "/tmp", id ? id : "", e ? e : "bin"); + return NCM_OK; +} 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) @@ -434,12 +442,14 @@ int main(void) } g_eof = 0; g_reads_left = -1; /* 上一步非 VIP 部分设了 g_eof=1 让 song0 收尾;SVIP song1 需正常播 */ g_vip = NCM_VIP_SVIP; g_cache_ok = 0; g_url_calls = 0; g_url_req_level = -1; + g_url_type = "flac"; /* §2②:SURROUND 为无损级 → 应答 flac(真实),eff 回填 SURROUND */ 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_url_type = "mp3"; g_eof = 1; printf("== P8: fee 门禁(§20:fee∈{1,4} 且非会员 → NCM_ERR_VIP,不发 song_url)==\n"); diff --git a/patch_v2/ncm_app/core/ncm_api.c b/patch_v2/ncm_app/core/ncm_api.c index 4a455f9..f74190a 100644 --- a/patch_v2/ncm_app/core/ncm_api.c +++ b/patch_v2/ncm_app/core/ncm_api.c @@ -329,11 +329,17 @@ static int level_attempt(const char *device_id, const char *cookie, const char * 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); + else ncm_strlcpy(p.type, "mp3", sizeof p.type); /* 默认 mp3(原语义),供格式门槛判定 */ + ok = 1; } if (!(p.url[0] && ok)) return 0; /* url 为 null 或未知格式:继续降级 */ + /* §档位保真(REPORT §NEW 任务2①):SQ 及以上(lossless/hires/母带/全景声) + * 命中判定必须 type=flac——无损/Hi-Res 等档返回 mp3 一律视为「该档不可用」继续降级 + * (服务端静默降档场景,修「hires 档静默给 mp3 被当命中」);mp3 只在 + * = NCM_LEVEL_LOSSLESS && ncm_streq(p.type, "mp3")) return 0; *out = p; if (out_level) { int rl = level_from_str(p.level); diff --git a/patch_v2/ncm_app/core/ncm_cache.c b/patch_v2/ncm_app/core/ncm_cache.c index 3099d14..2a6b19f 100644 --- a/patch_v2/ncm_app/core/ncm_cache.c +++ b/patch_v2/ncm_app/core/ncm_cache.c @@ -88,12 +88,26 @@ int ncm_cache_lookup(const ncm_cache_t *c, const char *id, const char *ext, struct stat st; if (stat(path, &st) != 0 || !S_ISREG(st.st_mode)) { /* §19 温和兼容:历史双点名 `song_..`(旧版 ext 带前导点所致)。 - * 命中照常刷新 LRU;不主动改名/删除,由 LRU 自然淘汰。 */ + * REPORT §NEW 任务5:命中即**迁移重命名**到单点名(同 id+ext,不重下)—— + * 之后本条目走单点主路径,双点存量自然消亡(lookup/evict 定期见到)。 */ const char *e = ext; if (e && e[0] == '.') e++; - int n = snprintf(path, sizeof path, "%s/song_%s..%s", c->dir, id, e ? e : "bin"); + const char *ee = e ? e : "bin"; + int n = snprintf(path, sizeof path, "%s/song_%s..%s", c->dir, id, ee); if (n < 0 || (size_t)n >= sizeof path) return NCM_ERR_FULL; if (stat(path, &st) != 0 || !S_ISREG(st.st_mode)) return NCM_ERR_NOENT; + char fixed[NCM_CACHE_DIR_MAX + 32]; + if (ncm_cache_path(c, id, ee, fixed, sizeof fixed) == NCM_OK) { + struct stat fst; + if (stat(fixed, &fst) == 0) unlink(path); /* 目标已(在/被并发建):删旧双点 */ + else rename(path, fixed); + if (stat(fixed, &st) == 0 && S_ISREG(st.st_mode)) { + ncm_strlcpy(path, fixed, sizeof path); + } else { + ncm_strlcpy(path, fixed, sizeof path); /* 迁移失败(权限等):回到固定路径 */ + return NCM_ERR_NOENT; + } + } } utime(path, NULL); /* 刷新最后访问(按 mtime LRU) */ ncm_strlcpy(out, path, out_sz); diff --git a/patch_v2/ncm_app/ncm b/patch_v2/ncm_app/ncm index 16b607a..ecd94e9 100755 Binary files a/patch_v2/ncm_app/ncm and b/patch_v2/ncm_app/ncm differ diff --git a/patch_v2/ncm_app/ncm_host b/patch_v2/ncm_app/ncm_host index 5279db4..3bb7c70 100755 Binary files a/patch_v2/ncm_app/ncm_host and b/patch_v2/ncm_app/ncm_host differ diff --git a/patch_v2/ncm_app/test/test_ncm_core.c b/patch_v2/ncm_app/test/test_ncm_core.c index c86daec..6153b8b 100644 --- a/patch_v2/ncm_app/test/test_ncm_core.c +++ b/patch_v2/ncm_app/test/test_ncm_core.c @@ -9,10 +9,14 @@ #include #include #include +#include #include "ncm.h" #include "mbedtls/aes.h" +/* §2① 档位保真离线测试用:注入 try_fn 的服务器行为模式 */ +static int g_grade_mode; + static int g_fail = 0; #define CHECK(cond, fmt, ...) do { \ if (cond) printf(" PASS " fmt "\n", ##__VA_ARGS__); \ @@ -147,15 +151,20 @@ static void test_cache(void) { CHECKRC(ncm_cache_path(&c, "103", ".flac", p2, sizeof p2), "path with dotted ext"); CHECK(strstr(p2, "song_103.flac") != NULL && strstr(p2, "..") == NULL, "dotted ext normalized to single dot (%s)", p2); - /* §19 温和兼容:历史双点名 song_..mp3(旧版 ext 带前导点所致)lookup 可命中 */ + /* §19/§NEW 任务5:历史双点名 song_..mp3(旧版 ext 带前导点所致)lookup 命中并 + * **迁移重命名**到单点名 song_.mp3(不重下),双点存量自然消亡。 */ { char legacy[NCM_CACHE_DIR_MAX + 40]; snprintf(legacy, sizeof legacy, "%s/song_104..mp3", dir); FILE *lf = fopen(legacy, "wb"); if (lf) { fputs("LEGACY", lf); fclose(lf); } int lrc = ncm_cache_lookup(&c, "104", "mp3", path, sizeof path); - CHECK(lrc == NCM_OK && strstr(path, "song_104..mp3") != NULL, - "legacy double-dot name hit (rc=%d path=%s)", lrc, path); + CHECK(lrc == NCM_OK && strstr(path, "song_104.mp3") != NULL && + strstr(path, "..") == NULL, + "legacy double-dot migrated to single-dot (rc=%d path=%s)", lrc, path); + struct stat mst; + CHECK(stat(legacy, &mst) != 0, "legacy double-dot file removed after migration"); + CHECK(stat(path, &mst) == 0 && mst.st_size == 6, "migrated file exists size=6"); } } @@ -237,6 +246,78 @@ static void test_network(void) { if (!net_ok) printf(" SKIP network live checks (host unreachable or TLS CA missing)\n"); } +/* ---------- §2① 档位保真:SQ 及以上(lossless/hires)命中必须 flac ---------- + * 服务端对无损档请求静默返回 mp3 时,视为「该档不可用」继续降级(修「hires 档静默给 mp3 + * 被当命中」)。注入 try_fn 离线验证(0 真实请求)。 */ +static int grade_gate_try(const char *device_id, const char *cookie, + const char *id, int level, ncm_play_url_t *out) +{ + (void)device_id; (void)cookie; (void)id; + memset(out, 0, sizeof *out); + if (g_grade_mode == 0) { + /* 全曲库只有 mp3:lossless+ 请求被静默降成 mp3(谎报 level 为请求档) */ + snprintf(out->url, sizeof out->url, "http://x/song_%d.mp3", level); + ncm_strlcpy(out->type, "mp3", sizeof out->type); + ncm_strlcpy(out->level, ncm_api_level_str(level), sizeof out->level); + out->br = 320000; + } else if (g_grade_mode == 1) { + /* lossless 有 flac、hires 只有 mp3(降级场景) */ + if (level == NCM_LEVEL_HIRES) { + snprintf(out->url, sizeof out->url, "http://x/song.mp3"); + ncm_strlcpy(out->type, "mp3", sizeof out->type); + ncm_strlcpy(out->level, "hires", sizeof out->level); /* 服务端谎报 hires */ + out->br = 320000; + } else if (level == NCM_LEVEL_LOSSLESS) { + snprintf(out->url, sizeof out->url, "http://x/song.flac"); + ncm_strlcpy(out->type, "flac", sizeof out->type); + ncm_strlcpy(out->level, "lossless", sizeof out->level); + out->br = 900000; + } else { + snprintf(out->url, sizeof out->url, "http://x/song.mp3"); + ncm_strlcpy(out->type, "mp3", sizeof out->type); + ncm_strlcpy(out->level, ncm_api_level_str(level), sizeof out->level); + out->br = 320000; + } + } else { + /* hires 有 flac(正常无损链) */ + snprintf(out->url, sizeof out->url, "http://x/song.flac"); + ncm_strlcpy(out->type, "flac", sizeof out->type); + ncm_strlcpy(out->level, ncm_api_level_str(level), sizeof out->level); + out->br = 900000; + } + return NCM_OK; +} + +static void test_grade_flac_gate(void) { + printf("== §2① 档位保真:lossless+ 必须 flac 命中 ==\n"); + ncm_play_url_t u; int el; + /* 只有 mp3 的曲库:请求 hires → 应降到 exhigh(exhigh (rc=%d el=%d type=%s)", rc, el, u.type); + /* 只有 mp3 的曲库:请求 lossless → 也降到 exhigh */ + g_grade_mode = 0; + rc = ncm_api_song_url_auto_av_f("dev", "", "409931770", NCM_VIP_SVIP, 0, + NCM_LEVEL_LOSSLESS, &u, &el, grade_gate_try); + CHECK(rc == NCM_OK && el == NCM_LEVEL_EXHIGH && ncm_streq(u.type, "mp3"), + "mp3-only repo: lossless->exhigh (rc=%d el=%d)", rc, el); + /* hires 只有 mp3、lossless 有 flac:请求 hires → 降到 lossless 并拿 flac */ + g_grade_mode = 1; + rc = ncm_api_song_url_auto_av_f("dev", "", "409931770", NCM_VIP_SVIP, 0, + NCM_LEVEL_HIRES, &u, &el, grade_gate_try); + CHECK(rc == NCM_OK && el == NCM_LEVEL_LOSSLESS && ncm_streq(u.type, "flac"), + "hires mp3 + lossless flac: hires->lossless flac (rc=%d el=%d type=%s)", + rc, el, u.type); + /* hires 有 flac:正常命中 hires */ + g_grade_mode = 2; + rc = ncm_api_song_url_auto_av_f("dev", "", "409931770", NCM_VIP_SVIP, 0, + NCM_LEVEL_HIRES, &u, &el, grade_gate_try); + CHECK(rc == NCM_OK && el == NCM_LEVEL_HIRES && ncm_streq(u.type, "flac"), + "hires flac: hit hires (rc=%d el=%d type=%s)", rc, el, u.type); +} + /* ---------- 二期:/api/playlist/track/all 服务端分页 离线解析 ---------- */ static void test_track_all_parse(void) { printf("== track_all 离线解析(二期)==\n"); @@ -391,6 +472,7 @@ int main(int argc, char **argv) { test_track_all_parse(); test_vip_parse(); test_json_unescape(); + test_grade_flac_gate(); printf("\n%s\n", g_fail ? "== RESULT: FAIL ==" : "== RESULT: PASS =="); return g_fail ? 1 : 0; diff --git a/patch_v2/ncm_app/test/test_ncm_core_host b/patch_v2/ncm_app/test/test_ncm_core_host index 10adbde..da6bb5a 100755 Binary files a/patch_v2/ncm_app/test/test_ncm_core_host and b/patch_v2/ncm_app/test/test_ncm_core_host differ