fix(ncm §33): 流媒专项批六项——流式SIGSEGV防御日志/档位保真(无损级必须flac+显示档按实际)/封面4xx不重试/收藏显隐/缓存命名迁移/topbar位图图标

- §2① ncm_api.c level_attempt: 无损级(hires/lossless及以上)命中必须 type=flac,返回 mp3 视为该档不可用续降
  (修「hires 档静默给 mp3 被当命中」)
- §2② player_manager.c eff_level_for_resp: 显示档按响应实际格式/码率——mp3 绝不显示 Hi-Res(≤128k→标准, 否则极高),
  flac 采信链 eff; br 未知钳 ≤EXHIGH
- §1 流式 SIGSEGV(《十面埋伏》sig=11) 审计+UBSAN 探针未复现 → 落地防御日志(dl start/done/interrupt + play done fin/exp/fail/underrun)
- §3 cover.c: download_cover 回填 rc/status; 确定性 4xx(404等)不重试立即跳下一张, 仅瞬时错误退避(rc=%d status=%d 日志)
- §4 playlist.c: pl_show_fav 我创建(is_collected==0)隐藏收藏钮, 我收藏保留, 搜索歌单保守显示(评论槽并入无空洞)
- §5 ncm_cache.c 双点→单点迁移 + player_manager song_local_file 旧键→当前档位键(不重下)
- §6 home.c: topbar 动作 ⚙↻⌕ 迁移 sb_icons 位图(meow_topappbar_set_item_icon, 修 4k 字库豆腐)
- 测试: test_ncm_core 新 test_grade_flac_gate + test_cache 双点迁移断言; degrade_sim/player_flow_sim 桩 flac 化
- 验证: stream/player_flow/window/search/cache_evict/degrade sim 全 PASS + test_ncm_core host+mipsel(qemu) + mem_sim + build_ncm.sh 0 warning
- 新 ncm md5 cb818786 (text 1017110/bss 1362500), 未部署留主会话; 0 真实 NCM 业务请求
This commit is contained in:
AskaEth committed 2026-09-02 04:20:46 +08:00
1 parent 68ce46a5d8
commit 44a790dc1d
13 files changed
+425 -24

No files matched your search

+188
View File
@@ -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_<id>_<档位>.<ext>` + 存量双点迁移
**根因**:新键 `audio_cache_key=id_level` 已按用户定案生成 `song_<id>_<level>.<ext>`;但①卡上存量
`song_<id>..mp3`(旧版 ext 带前导点生成的双点畸形)/ 旧无档位键 `song_<id>.mp3` 仍被「温和兼容
只读不迁」,永远留存量;②`ncm_cache_lookup` 双点回退命中后不转正。
**修法**:
- `core/ncm_cache.c ncm_cache_lookup`:双点回退命中后**迁移重命名**为单点名(同 id+ext,
目标已存在则删旧双点),之后本条目走单点主路径,双点存量自然消亡。
- `app/play/player_manager.c song_local_file`(`hit==2` 旧键路径):改名到**当前档位键**
`song_<id>_<g_level>.<ext>`(`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_<id>_<level>.<ext>` 无双点;
⑥home topbar 三动作显示位图图标(非豆腐)。
+22 -4
View File
@@ -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) {
+55 -4
View File
@@ -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_<id>[.]mp3,含历史双点名):
* 命中即播,不主动改名/删除,由 LRU 自然淘汰。 */
/* §19 前存量:无档位后缀的 song_<id>[.]mp3(含历史双点名 song_<id>..mp3)。
* §NEW 任务5:命中即**迁移重命名**到当前档位键 song_<id>_<level>.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_<id>.mp3 / 历史双点 song_<id>..mp3)
* 迁移重命名到当前档位键 song_<id>_<g_level>.<ext>(**不重下**,仅改名)。
* 目标已存在(该档已有正式条目)则删旧;迁移失败回退旧路径照常播。 */
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 会被外层当「自然播完」
+5
View File
@@ -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));
+31 -9
View File
@@ -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) {
+6 -1
View File
@@ -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;
+10
View File
@@ -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");
+7 -1
View File
@@ -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 只在
* <lossless(standard/higher/exhigh)接受(mp3 码率上限即极高,不可能达无损档)。 */
if (lv >= NCM_LEVEL_LOSSLESS && ncm_streq(p.type, "mp3")) return 0;
*out = p;
if (out_level) {
int rl = level_from_str(p.level);
+16 -2
View File
@@ -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_<id>..<ext>`(旧版 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);
Binary file not shown.
Binary file not shown.
+85 -3
View File
@@ -9,10 +9,14 @@
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/stat.h>
#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_<id>..mp3(旧版 ext 带前导点所致)lookup 可命中 */
/* §19/§NEW 任务5:历史双点名 song_<id>..mp3(旧版 ext 带前导点所致)lookup 命中并
* **迁移重命名**到单点名 song_<id>.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(<lossless 的最高 mp3 档)并拿 mp3 */
g_grade_mode = 0;
int 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_EXHIGH && ncm_streq(u.type, "mp3"),
"mp3-only repo: hires->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;
Binary file not shown.