feat(ncm): 封面死链兜底链 —— 原 picUrl 失败→web 端 song/detail 重取活链→歌单封面兜底

用户调研: 官方旧版 GET music.163.com/api/song/detail/?id=<id>&ids=[<id>] 免登录出
songs[0].album.picUrl/blurPicUrl。本批宿主实锤: 登录态 eapi 给的死链封面, 该 web 接口给不同活链
(死链 yR2VGt7.../10995116862531 -> album.picUrl p2.../6y-UleORIT.../...jpg 活 PNG 146KB)。

实现 (patch_v2/ncm_app/):
- cover_request(url, song_id): 新增第二参曲目 id; g_tgt_song 记录; 新目标重置 g_fb_done。
  player.c 三处传 s.id(专辑封面), playlist.c 传 NULL(歌单封面无兜底)。
- cover_song_detail_pic(song_id): web song/detail 重取 album.picUrl(缺 blurPicUrl);
  ids=[<id>] 的 [ ] URL 编码 %5B/%5D(裸[ ] curl 报 bad range); 带 Referer+桌面UA; ncm_http_request
  (头进 resp->headers, 体进 16KB 栈缓冲) + ncm_json 解析; 只读一次。
- cover_try_fallback(url, song_id): 仅原链确定性失败时触发; g_tgt_url 仍==url(未被换走) + 曲目非空 +
  未兜底 + 同曲 5 分钟冷却(ncm_monotonic_ms) 才下探; 拿到不同新图->记冷却/置 g_fb_done/换目标返 1
  (worker 重试新图); 无新图/冷却中/已兜底/目标被换走->返 0 走歌单封面兜底; 每步日志写清级别。
- worker: kind=1(webp)/kind=2(非图)/确定性4xx 死链分支清目标前先
  if(song[0]&&!fb_done&&cover_try_fallback(url,song)) continue; 瞬时错误仍指数退避不触发。
- 风控: 只在原链失败触发(不新增常规请求); 单曲目最多一次(g_fb_done)+5分钟冷却。

验证 (宿主+qemu, 0 新增真实 CDN 业务请求):
- cover_chain_sim T1-T12 ALL PASS(新增 T11 原链死->song/detail活链->出图+恰1次+song-detail fallback日志;
  T12 兜底也死->留歌单封面+同曲再请求 song/detail 0次(冷却)+cooldown日志)。
- cache_evict/player_flow/window/search/stream sim 全 PASS; core host+mipsel(qemu) PASS;
  build_ncm.sh 0 warning(text 1022210/bss 1362564,+64B); ncm_host UI dump exit=0。
  新 ncm md5 e1f4b7ade834b18c7910b9c3e64bc858 (未部署, 留主会话上卡)。
This commit is contained in:
AskaEth committed 2026-09-02 11:41:54 +08:00
1 parent 9d93f1b1ef
commit 2bfe1ed796
8 files changed
+261 -20

No files matched your search

+81
View File
@@ -3106,3 +3106,84 @@ cb8187868f71a9fa155075d0624711d6 ncm # mipsel 静态 App(text 101711
另立批加明文 80 路径。
- **真机封面加载成功率提升未实测**(本批未部署——留主会话上卡后播放几首观察 `cover:` 日志,预期 404/403/
非图幂等快速跳下一张+歌单封面兜底,不再 8 次空转)。
---
## 35. 封面死链兜底链:原 picUrl → song/detail 重取 → 歌单封面兜底(2026-09-02)
> 用户调研:官方旧版 `GET music.163.com/api/song/detail/?id=<id>&ids=[<id>]` **免登录**直接出 JSON,
> `songs[0].album.picUrl`(还有 blurPicUrl)。价值:登录态 eapi 给的 picUrl 死链时,这个 web 端接口
> 可能给出**不同活链**(不同接口指向不同 CDN 对象)。本批宿主实锤:死链 `p4.music.126.net/yR2VGt7.../10995116862531`
> 的 song 经 web 接口返回 `album.picUrl = p2.music.126.net/6y-UleORIT.../5639395138885805.jpg`(活 PNG 146KB)
> 与 `blurPicUrl`(活 JPEG 1.34MB 全尺寸)——兜底价值坐实。
### 35.1 兜底链实现(`app/logic/cover.c` + `app/ui/player.c` + `app/ui/playlist.c` + `app/ncm_app_internal.h`)
**链路**:原 picUrl(确定性 404/403/200+非图/webp)→ 【下探】song/detail 重取新 picUrl → 与原 URL 不同则
再拉一次(仍走 Referer+桌面 UA+`?param`+魔数判定)→ 仍失败/无活图 → **歌单封面兜底**(保留当前显示)。
- **`cover_request(url, song_id)`**:新增第二参 `song_id`(曲目专辑封面传 `s.id`;歌单封面传 NULL)。
`g_tgt_song` 记录当前目标曲目;新目标重置 `g_fb_done=0`。player.c 三处传 `s.id`,playlist.c 传 NULL。
- **`cover_song_detail_pic(song_id, pic)`**(新增):`GET music.163.com/api/song/detail/?id=<id>&ids=[<id>]`
(`[ ]` URL 编码为 `%5B %5D`——宿主 curl 实测裸 `[ ]` 报 bad range),带 Referer+桌面 UA(同封面头),
`ncm_http_request`(头进 resp->headers、体进 16KB 栈缓冲)→ `ncm_json` 取 `songs[0].album.picUrl`,
缺则 `blurPicUrl`。只读一次。
- **`cover_try_fallback(url, song_id)`**(新增):仅当 `g_tgt_url` 仍 == url(目标未被 UI 换走)、曲目非空、
尚未兜底、且**同曲 5 分钟冷却**(`g_fb_song`/`g_fb_ms`,`ncm_monotonic_ms`)未命中才触发;拿到新图且
与旧不同 → 记冷却、置 `g_fb_done=1`、换 `g_tgt_url` 为新图,返回 1(worker 重试新图);无新图/冷却中/
目标被换走/已兜底 → 返回 0(走歌单封面兜底)。每步打日志写清级别(`song-detail fallback id=.. -> url` /
`song-detail id=.. no live pic -> playlist cover` / `song-detail fallback cooldown id=.. -> playlist cover`)。
- **worker 接线**:kind=1(webp)/kind=2(非图)/确定性 4xx 四处死链分支,清目标前先
`if (song[0] && !fb_done && cover_try_fallback(url, song)) { continue; }`——命中即重试新图;
否则照旧清目标走歌单封面兜底。瞬时错误(网络/TLS/5xx)仍指数退避,不触发兜底(非死链)。
- **风控**:song/detail 只在原链失败触发(不新增常规请求),单曲目冷却 5 分钟、最多一次(`g_fb_done`)。
### 35.2 验证(宿主 + qemu;0 新增真实 CDN 业务请求——web 接口仅宿主单次只读验证端点,见 35.4)
| 项 | 方式 | 结果 |
|---|---|---|
| 兜底链(新用例) | `cover_chain_sim`:T1-T10 存量 + **T11 原链死→song/detail 活链→出图** + **T12 原链死→兜底也死→歌单封面兜底 + 同曲冷却不重试** | **T1-T12 ALL PASS**(T11:守到 URL_LIVE 出图 + song/detail 恰 1 次 + 日志 `song-detail fallback`;T12:兜底也死留旧封面 + 再请求同曲 song/detail 0 次(冷却)+ 日志 `cooldown`) |
| 缓存 LRU/隔离 | `cache_evict_sim` | **PASS** |
| 播放状态机 | `player_flow_sim` P1-P12 | **PASS** |
| 大歌单滑窗 | `window_sim` | **PASS** |
| 搜索 | `search_sim` | **PASS** |
| 边下边播 | `stream_sim` | **ALL PASS** |
| core | `build_core.sh` → `test_ncm_core_host` + `qemu test_ncm_core_mipsel` | host + mipsel **PASS** |
| 构建 | `build_ncm.sh`(-Wall -Wextra) | **0 warning**;`text 1022210 / data 1648 / bss 1362564`(bss 仅 +64B,兜底曲目/时间戳 statics) |
| 宿主构建 | `build_ncm_host.sh` | 0 warning(仅 SDK `meow_ui_vol.c` 存量 `system()` 告警,非本批) |
| UI 渲染 | `NCM_PAGE=home/player/playlist MEOW_UI_DUMP` | exit=0,dump 正常 |
### 35.3 md5(本批产物)
```
e1f4b7ade834b18c7910b9c3e64bc858 ncm # 新(含兜底链;未部署,留主会话上卡)
d102c64f7fab73e981a789c5960e6bd9 ncm_host # 宿主调试二进制(重链)
97bb2bdb44a2f446877787d9261b4f02 test/test_ncm_core_host # 宿主 core 测试(重编,内容未变)
```
改动文件(本批,仅这些):
```
修改:
app/logic/cover.c # cover_request 增 song_id + g_tgt_song/g_fb_song/g_fb_ms/g_fb_done + cover_song_detail_pic + cover_try_fallback + worker 死链分支先试兜底 + 头/注释
app/ui/player.c # 三处 cover_request 传曲目 s.id(触发专辑封面兜底)
app/ui/playlist.c # cover_request(pcur->cover, NULL)(歌单封面无兜底)
app/ncm_app_internal.h # cover_request 声明加 song_id
bench/cover_chain_sim.c # sim 补 ncm_json/jsmn/ncm_common 桩 + song/detail/dead/alt stub + T11/T12 + 双参 cover_request
ncm / ncm_host # 产物重链
```
(未动 sdk/、meow_launcherd/、其它 App、devdocs/;0 新增真实 CDN 业务请求;未部署——留主会话统一上卡。)
### 35.4 诚实出入 / 遗留
- **web 端点有效性**已宿主单次只读验证(`ids=%5B<id>%5D`,`code:200`,`songs[0].album.picUrl`/`blurPicUrl`
非空且为活图)——但**未在真机跑通整条兜底链**(本批未部署,留主会话上卡后观察 `cover: song-detail fallback`
日志)。真机预期:死链封面切歌时后台先撞原链(1 次 404/403/非图),立即 song/detail 重取(1 次,
免登录、带 Referer/UA),拿到活链二次拉图出封面;活链封面不再有兜底开销。
- **song/detail 响应缓冲 16KB**(栈):单曲 detail ~3.3KB 足够;个别超大响应 >16KB 会 `NCM_ERR_LIMIT`
→ 兜底取不到图回落歌单封面(可接受;未做动态增长,规避设备 RAM 压力)。
- **瞬时 song/detail 失败不重试**:song/detail 网络/TLS 瞬断 → 视为「无活图」回落歌单封面并记冷却
(同曲 5 分钟内不重撞)。保守但避免高频撞接口——重试救不了死链接,瞬断概率低。
- **冷却按曲目 id**(`g_fb_song`):切回同一曲 5 分钟内不重复兜底;换曲立即解锁。兜底后 `g_fb_done=1`
防止同 incident 二次兜底(新图也死时不再回 song/detail)。
- **歌单封面兜底语义未变**:兜底链两级都失败后仍清目标、保留当前显示封面(歌单封面在进歌单时已由
`cover_request(pcur->cover, NULL)` 取到)。未新增对 `g_st.cover_url` 的主动重取。