diff --git a/patch_v2/ncm_app/REPORT.md b/patch_v2/ncm_app/REPORT.md index 9c20947..6779b7a 100644 --- a/patch_v2/ncm_app/REPORT.md +++ b/patch_v2/ncm_app/REPORT.md @@ -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=&ids=[]` **免登录**直接出 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=&ids=[]` + (`[ ]` 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%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` 的主动重取。 diff --git a/patch_v2/ncm_app/app/logic/cover.c b/patch_v2/ncm_app/app/logic/cover.c index 2a03553..a1ad6d3 100644 --- a/patch_v2/ncm_app/app/logic/cover.c +++ b/patch_v2/ncm_app/app/logic/cover.c @@ -45,6 +45,10 @@ static pthread_mutex_t g_cm = PTHREAD_MUTEX_INITIALIZER; static meow_img *g_cover_img; static char g_cover_url[512]; /* g_cover_img 对应的 URL */ static char g_tgt_url[512]; /* 目标 URL(worker 拉取) */ +static char g_tgt_song[24]; /* 目标对应曲目 id(无=歌单封面,不走 song/detail 兜底) */ +static char g_fb_song[24]; /* 最近一次 song/detail 兜底的曲目 id(冷却判定) */ +static int64_t g_fb_ms; /* 最近一次兜底时刻(单调毫秒;冷却) */ +static int g_fb_done; /* 当前目标已尝试过 song/detail 兜底(防二次) */ static pthread_t g_thr; static int g_thr_on; static int g_quit; @@ -52,6 +56,15 @@ static volatile int g_gen; /* 每次换入 +1 */ static int g_acked; /* UI 已消费的版本 */ static char g_cover_path[512]; /* 封面原始文件路径(供 launcher) */ +/* 死链兜底链(REPORT §NEW 任务1):原 picUrl 确定性失败(404/403/200+非图/webp)时,自动调 + * web 端 `GET music.163.com/api/song/detail/?id=&ids=[]`(免登录,songs[0].album.picUrl/ + * blurPicUrl)重取 picUrl——不同接口指向不同 CDN 对象,可能给出**活链**(本批宿主实测坐实: + * 死链 p4.../yR2VGt7F1I9ElOzvXYuw1g==/10995116862531,而 song/detail album.picUrl 给 + * p2.../6y-UleORITEDbvrOLV0Q8A==/5639395138885805.jpg 活 PNG / blurPicUrl 活 JPEG)。 + * 链:原 picUrl → song/detail 重取 → 歌单封面兜底。只对带曲目 id 的专辑封面触发;单曲目冷却 + * COVER_FB_COOLDOWN_MS 内不重复。风控:仅在原链失败时触发,不新增常规请求。 */ +#define COVER_FB_COOLDOWN_MS (5 * 60 * 1000) /* 同曲目 5 分钟冷却,避免反复撞同一死链接口 */ + /* 封面缓存键:URL 的 djb2 哈希(稳定性由 URL 决定——同一专辑封面 URL 名即可命中)。 * 走 ncm_cache(LRU 口径沿用 ncm_cache,见 REPORT §14.2):键进 ncm_cache_path => 文件 * `song_cov.jpg`,与 mp3(song_.mp3)同池,lookup/commit 刷新 mtime(LRU)。 */ @@ -150,6 +163,76 @@ static int body_is_decodable_img(const char *path, const ncm_http_resp_t *resp) return 0; } +/* Web 端 song/detail 免登录重取专辑封面 picUrl(REPORT §NEW 任务1): + * `GET https://music.163.com/api/song/detail/?id=&ids=[]`,取 songs[0].album.picUrl + * (缺则 blurPicUrl)。返回 0 且 pic[0]!=0 视为成功;否则负错误码。只读一次,带 Referer+桌面 UA。 */ +#define COVER_SD_BUF 16384 +static int cover_song_detail_pic(const char *song_id, char *pic, size_t pic_sz) +{ + if (!song_id || !song_id[0]) return NCM_ERR_ARG; + if (pic_sz) pic[0] = 0; + /* ids=[] 的 '[ ]' 需 URL 编码为 %5B/%5D(宿主 curl 实测:裸 [ ] 报 bad range) */ + char url[352]; + snprintf(url, sizeof url, "https://music.163.com/api/song/detail/?id=%s&ids=%%5B%s%%5D", + song_id, song_id); + char hdrs[256]; + snprintf(hdrs, sizeof hdrs, "Referer: %s\r\nUser-Agent: %s", NCM_COVER_REFERER, NCM_COVER_UA); + ncm_http_req_t req; memset(&req, 0, sizeof req); + req.url = url; req.method = "GET"; req.extra_hdrs = hdrs; + req.verify_ca = NCM_HTTP_VERIFY_DEFAULT; req.timeout_ms = 15000; + char buf[COVER_SD_BUF]; + ncm_http_resp_t resp; + int rc = ncm_http_request(&req, buf, sizeof buf, &resp); + if (rc != NCM_OK || resp.status != 200 || resp.body_len == 0) + return rc != NCM_OK ? rc : NCM_ERR_IO; + jsmntok_t toks[256]; int parent[256]; + ncm_json_t j; memset(&j, 0, sizeof j); + if (ncm_json_parse(&j, resp.body, resp.body_len, toks, 256, parent) < 0) + return NCM_ERR_JSON; + if (ncm_json_path_str(&j, "songs[0].album.picUrl", pic, pic_sz) && pic[0]) return NCM_OK; + if (ncm_json_path_str(&j, "songs[0].album.blurPicUrl", pic, pic_sz) && pic[0]) return NCM_OK; + return NCM_ERR_NOTFOUND; +} + +/* 死链兜底(REPORT §NEW 任务1):歌单封面兜底前,试 song/detail 重取一个新图 URL。 + * 仅当 g_tgt_url 仍 == url(目标未被 UI 换走)、曲目非空、本 incident 尚未兜底、且不在同曲冷却内才触发。 + * 命中且新图 != 旧图 → 换 g_tgt_url 为新图、记冷却、置 g_fb_done,返回 1(worker 重试新图); + * 无新图/冷却中/目标被换走/已兜底 → 返回 0(走歌单封面兜底)。每步打日志写清走到哪一级。 */ +static int cover_try_fallback(const char *url, const char *song_id) +{ + if (!song_id || !song_id[0]) return 0; + int64_t now = ncm_monotonic_ms(); + int cooled = (g_fb_song[0] && strcmp(g_fb_song, song_id) == 0 && + (now - g_fb_ms) < COVER_FB_COOLDOWN_MS); + if (cooled) { + meow_log(MEOW_LOG_WARN, "cover: song-detail fallback cooldown id=%s -> playlist cover", song_id); + return 0; + } + char pic[512] = ""; + if (cover_song_detail_pic(song_id, pic, sizeof pic) != NCM_OK || !pic[0]) { + /* 无活图:仍记冷却(同曲 5 分钟内不再撞同一死链接口),落歌单封面 */ + pthread_mutex_lock(&g_cm); + ncm_strlcpy(g_fb_song, song_id, sizeof g_fb_song); g_fb_ms = now; + pthread_mutex_unlock(&g_cm); + meow_log(MEOW_LOG_WARN, "cover: song-detail id=%s no live pic -> playlist cover", song_id); + return 0; + } + char nurl[512]; cover_norm_url(pic, nurl, sizeof nurl); + pthread_mutex_lock(&g_cm); + if (strcmp(g_tgt_url, url) != 0) { pthread_mutex_unlock(&g_cm); return 0; } /* 目标已被换走 */ + if (nurl[0] && strcmp(nurl, g_tgt_url) != 0) { + ncm_strlcpy(g_tgt_url, nurl, sizeof g_tgt_url); + ncm_strlcpy(g_fb_song, song_id, sizeof g_fb_song); g_fb_ms = now; + g_fb_done = 1; + pthread_mutex_unlock(&g_cm); + meow_log(MEOW_LOG_INFO, "cover: song-detail fallback id=%s -> %s", song_id, pic); + return 1; + } + pthread_mutex_unlock(&g_cm); + meow_log(MEOW_LOG_WARN, "cover: song-detail id=%s yields empty/same pic -> playlist cover", song_id); + return 0; +} + /* 下载 url 到 cache 写者 w(流式,写进其临时文件),成功后从临时文件解码并返回 img。 * 失败返回 NULL(不 commit,调用方 abort)。*out_len=body 字节数;*out_rc 与 *out_status * 回填下载 rc 与 HTTP status(封面 4xx 加固判定:确定性 4xx 不重试,REPORT §NEW 任务3); @@ -246,8 +329,11 @@ static void *cover_worker(void *arg) usleep(250 * 1000); continue; } + char song[24]; char url[512]; ncm_strlcpy(url, g_tgt_url, sizeof url); + ncm_strlcpy(song, g_tgt_song, sizeof song); + int fb_done = g_fb_done; /* 本目标是否已做过 song/detail 兜底(防二次) */ pthread_mutex_unlock(&g_cm); char key[32]; @@ -296,9 +382,10 @@ static void *cover_worker(void *arg) * - 仅瞬时错误(超时/网络/TLS/5xx)指数退避重试。 * 日志一律打 rc/status(排查资产:先前只见 "fetch failed" 不知 HTTP 层返了什么); * 403 单独日志(防盗链特征,与 404 死链区分——三者修法不同)。 */ - if (dkind == 1) { /* webp:200 但 stb 不支持解码,非瞬时非重试可愈 → 立即清目标走兜底 */ + if (dkind == 1) { /* webp:200 但 stb 不支持解码,非瞬时非重试可愈 → 兜底或清目标 */ meow_log(MEOW_LOG_WARN, "cover: webp unsupported url=%.64s rc=%d status=%d (no decode)", url, drc, dstatus); + if (song[0] && !fb_done && cover_try_fallback(url, song)) { backoff_ms = 0; fails = 0; continue; } pthread_mutex_lock(&g_cm); if (strcmp(g_tgt_url, url) == 0) g_tgt_url[0] = 0; /* 立即跳下一张(走兜底) */ pthread_mutex_unlock(&g_cm); @@ -307,9 +394,10 @@ static void *cover_worker(void *arg) } if (dkind == 2) { /* 200 但 body 非可解码图(源站死对象回 JSON 错误体/未知格式): * 真机矩阵(REPORT §34.1)确认为「200 解码失败」主因,老逻辑空转 8 次; - * 确定性失败(重试救不了死对象)→ 立即清目标走兜底 */ + * 确定性失败(重试救不了死对象)→ 兜底或清目标 */ meow_log(MEOW_LOG_WARN, "cover: not an image (origin error body/200) url=%.64s rc=%d status=%d (no retry)", url, drc, dstatus); + if (song[0] && !fb_done && cover_try_fallback(url, song)) { backoff_ms = 0; fails = 0; continue; } pthread_mutex_lock(&g_cm); if (strcmp(g_tgt_url, url) == 0) g_tgt_url[0] = 0; pthread_mutex_unlock(&g_cm); @@ -328,6 +416,7 @@ static void *cover_worker(void *arg) url, drc, dstatus); } if (deterministic) { + if (song[0] && !fb_done && cover_try_fallback(url, song)) { backoff_ms = 0; fails = 0; continue; } pthread_mutex_lock(&g_cm); if (strcmp(g_tgt_url, url) == 0) g_tgt_url[0] = 0; /* 立即跳下一张 */ pthread_mutex_unlock(&g_cm); @@ -370,10 +459,11 @@ static void *cover_worker(void *arg) return NULL; } -/* UI 线程:提交目标封面 URL(无网络/无 URL 则忽略)。 +/* UI 线程:提交目标封面 URL(无网络/无 URL 则忽略)。song_id:曲目 id(专辑封面死链时走 + * song/detail 兜底重取,见 REPORT §NEW 任务1);NULL/空=歌单封面(无兜底)。 * 先 cover_norm_url 规范化(追加 ?param=300y300 缩略后缀,REPORT §NEW 任务2),使 * g_tgt_url/g_cover_url/缓存键/下载 URL 走同一参数版本(小图省内存、命中高)。 */ -void cover_request(const char *url) +void cover_request(const char *url, const char *song_id) { if (!url || !url[0]) return; if (g_host_mock) return; /* 宿主 mock = 0 网络纪律(mock URL 不得发真实 HTTPS) */ @@ -385,6 +475,8 @@ void cover_request(const char *url) if ((g_cover_url[0] && strcmp(g_cover_url, u) == 0) || (g_tgt_url[0] && strcmp(g_tgt_url, u) == 0)) { pthread_mutex_unlock(&g_cm); return; } ncm_strlcpy(g_tgt_url, u, sizeof g_tgt_url); + ncm_strlcpy(g_tgt_song, song_id ? song_id : "", sizeof g_tgt_song); + g_fb_done = 0; /* 新目标:重置「是否已兜底」标记 */ pthread_mutex_unlock(&g_cm); if (!g_thr_on) { g_thr_on = 1; @@ -429,6 +521,10 @@ void cover_cache_reset(void) if (g_cover_img) { meow_img_free(g_cover_img); g_cover_img = NULL; } g_cover_url[0] = 0; g_tgt_url[0] = 0; + g_tgt_song[0] = 0; + g_fb_song[0] = 0; + g_fb_ms = 0; + g_fb_done = 0; g_cover_path[0] = 0; pthread_mutex_unlock(&g_cm); g_gen++; diff --git a/patch_v2/ncm_app/app/ncm_app_internal.h b/patch_v2/ncm_app/app/ncm_app_internal.h index fbd97b7..c60b9c0 100644 --- a/patch_v2/ncm_app/app/ncm_app_internal.h +++ b/patch_v2/ncm_app/app/ncm_app_internal.h @@ -451,7 +451,7 @@ int lyric_state(void); /* 0=空闲/未拉 1=加载中 2=已拉完(可能 long lyric_line_time(int i);/* 第 i 行时间戳(ms;越界 -1)。播放页点行跳转(§22) */ /* cover */ -void cover_request(const char *url); /* 提交目标封面 URL(后台线程下载+解码) */ +void cover_request(const char *url, const char *song_id); /* 提交目标封面 URL(后台线程下载+解码);song_id 曲目封面死链时 song/detail 兜底重取,NULL=歌单封面无兜底 */ meow_img *cover_get(void); /* 当前封面(**持锁返回**;NULL=无;用后必须 cover_put) */ void cover_put(void); /* 配对 cover_get 释放锁(§18 修绘制期 UAF) */ int cover_changed(void); /* 1=有新封面待绘制(UI tick 判变) */ diff --git a/patch_v2/ncm_app/app/ui/player.c b/patch_v2/ncm_app/app/ui/player.c index d547e3c..f92f71c 100644 --- a/patch_v2/ncm_app/app/ui/player.c +++ b/patch_v2/ncm_app/app/ui/player.c @@ -538,7 +538,7 @@ void player_play_song(int idx) ? s.al_pic : g_st.cover_url; player_manager_play(idx); appstate_set_playing_song(idx, NULL); - cover_request(cover); + cover_request(cover, s.id); /* 曲目专辑封面:带 id,死链时 song/detail 兜底(REPORT §NEW 任务1) */ g_ppage = 0; /* 新曲 → 回封面页 */ meow_view_page_push(ACT_PLAYER); } @@ -554,7 +554,7 @@ void player_play_search_song(int i) const char *cover = s.al_pic[0] ? s.al_pic : g_st.cover_url; player_manager_play(i); appstate_set_playing_song(i, NULL); - cover_request(cover); + cover_request(cover, s.id); g_ppage = 0; meow_view_page_push(ACT_PLAYER); } @@ -585,7 +585,7 @@ void player_tick(uint64_t now) ncm_song_t s2; if (sc >= 0 && cur_song_meta(&s2) == 0 && strcmp(s2.id, g_last_meta_id) != 0) { ncm_strlcpy(g_last_meta_id, s2.id, sizeof g_last_meta_id); - cover_request(s2.al_pic[0] ? s2.al_pic : g_st.cover_url); + cover_request(s2.al_pic[0] ? s2.al_pic : g_st.cover_url, s2.id); player_static_invalidate(); player_dyn_invalidate(); } diff --git a/patch_v2/ncm_app/app/ui/playlist.c b/patch_v2/ncm_app/app/ui/playlist.c index eb49c3d..4078960 100644 --- a/patch_v2/ncm_app/app/ui/playlist.c +++ b/patch_v2/ncm_app/app/ui/playlist.c @@ -548,7 +548,7 @@ static void pl_page_enter(meow_view *v, void *ctx) * (旧版只在 playlist_open 里 cover_cache_reset 清掉,无人再请求 → 头块永远占位 ♪)。 */ const ncm_playlist_t *pcur = pl_cur(); if (pcur && pcur->cover[0]) - cover_request(pcur->cover); + cover_request(pcur->cover, NULL); /* 歌单封面:无曲目 id,不走 song/detail 兜底 */ playlist_refresh(); } diff --git a/patch_v2/ncm_app/bench/cover_chain_sim.c b/patch_v2/ncm_app/bench/cover_chain_sim.c index 6b60f68..761839e 100644 --- a/patch_v2/ncm_app/bench/cover_chain_sim.c +++ b/patch_v2/ncm_app/bench/cover_chain_sim.c @@ -49,6 +49,16 @@ size_t ncm_strlcpy(char *dst, const char *src, size_t sz) while (src[n]) n++; return n; } +size_t ncm_strlcat(char *dst, const char *src, size_t sz) +{ + size_t dn = strlen(dst), sn = 0; + while (dn + sn + 1 < sz && src[sn]) { dst[dn + sn] = src[sn]; sn++; } + if (sz) dst[dn + sn] = 0; + return dn + sn; +} +int ncm_streq(const char *a, const char *b) { return a && b && strcmp(a, b) == 0; } +int ncm_parse_int(const char *s, long *out) { char *e; long v = strtol(s, &e, 10); if (e == s || *e) return -1; if (out) *out = v; return 0; } +int64_t ncm_monotonic_ms(void) { static int64_t t = 0; return ++t; } /* 单调递增:冷却时差恒 <5min(T12 冷却判定用) */ static int g_log_warns; static char g_last_log[512]; /* 最近一条日志文本(webp/403 分诊日志断言用) */ @@ -113,13 +123,16 @@ void meow_img_free(meow_img *im) /* §18 前的 cover.c 无 cover_put:weak 空实现让同一 sim 可对照旧代码(新代码强符号覆盖)。 */ __attribute__((weak)) void cover_put(void) { } -/* ---- HTTP stub:内容 = djb2(url) 填充 4KB(每 URL 唯一);可注入失败/延迟/webp/403 ---- */ +/* ---- HTTP stub:内容 = djb2(url) 填充 4KB(每 URL 唯一);可注入 失败/延迟/webp/json/403/死链前缀 ---- */ static int g_http_calls; +static int g_sd_calls; /* song/detail 请求计数(死链兜底专用,REPORT §NEW 任务1) */ static int g_fail_next; /* 剩余注入失败次数 */ static int g_delay_ms; /* 模拟慢网络 */ static int g_webp_next; /* 下一次下载返回 WebP 魔数体(200,测「200 解码失败」三分型③) */ static int g_json_next; /* 下一次下载返回 JSON 错误体(200,实测「200 解码失败」真凶=死对象回 JSON) */ static int g_status_next; /* 下一次下载强制返回该 HTTP status(测 403 防盗链;0=200) */ +static char g_dead_prefix[128] = ""; /* URL 此前缀 → 返 404(死链注入;兜底链测试用) */ +static char g_alt_pic[256] = ""; /* song/detail 响应里给的新 picUrl(兜底链活链/死链用) */ static unsigned long djb2(const char *s) { @@ -133,6 +146,23 @@ int ncm_http_request(const ncm_http_req_t *req, char *buf, size_t buf_sz, ncm_ht g_http_calls++; if (g_fail_next > 0) { g_fail_next--; return NCM_ERR_IO; } if (g_delay_ms > 0) usleep((useconds_t)g_delay_ms * 1000); + /* 死链兜底:song/detail 请求 → 返 JSON(songs[0].album.picUrl = g_alt_pic),供 cover.c + * cover_song_detail_pic 重取新图(REPORT §NEW 任务1)。 */ + if (strstr(req->url, "/api/song/detail/")) { + g_sd_calls++; + int n = snprintf(buf, buf_sz, + "{\"songs\":[{\"album\":{\"picUrl\":\"%s\",\"blurPicUrl\":\"\"}}],\"code\":200}", + g_alt_pic); + if (n < 0 || (size_t)n >= buf_sz) return NCM_ERR_FULL; + memset(resp, 0, sizeof *resp); + resp->status = 200; + resp->body = buf; + resp->body_len = (size_t)n; + resp->content_length = (size_t)n; + resp->has_content_type = 1; + ncm_strlcpy(resp->content_type, "application/json", sizeof resp->content_type); + return NCM_OK; + } size_t n = 4096 < buf_sz ? 4096 : buf_sz; unsigned long h = djb2(req->url); for (size_t i = 0; i < n; i++) buf[i] = (char)((h >> ((i % 8) * 8)) ^ (unsigned char)i); @@ -155,6 +185,12 @@ int ncm_http_download(const ncm_http_req_t *req, ncm_http_write_cb cb, void *ctx { (void)max_bytes; g_http_calls++; + /* 死链注入:URL 前缀命中 → 返 404 无 body(cover.c 判确定性 4xx,走 song/detail 兜底) */ + if (g_dead_prefix[0] && strncmp(req->url, g_dead_prefix, strlen(g_dead_prefix)) == 0) { + memset(resp, 0, sizeof *resp); + resp->status = 404; + return NCM_OK; + } if (g_fail_next > 0) { g_fail_next--; return NCM_ERR_IO; } if (g_delay_ms > 0) usleep((useconds_t)g_delay_ms * 1000); unsigned char buf[4096]; @@ -253,6 +289,9 @@ static unsigned long curjpg_hash(void) #define URL_E "http://p1.music.126.net/albumE/vvv.jpg" /* webp 注入用(未缓存) */ #define URL_F "http://p1.music.126.net/albumF/uuu.jpg" /* 403 注入用(未缓存) */ #define URL_G "http://p1.music.126.net/albumG/ttt.jpg" /* JSON 错误体注入用(未缓存) */ +#define URL_H "http://p1.music.126.net/dead/album/AAA.jpg" /* 死链原图(song/detail 兜底测试用) */ +#define URL_H2 "http://p1.music.126.net/dead/album/BBB.jpg" /* 兜底给到的也死链 alt */ +#define URL_LIVE "http://p1.music.126.net/live/SONG.jpg" /* 兜底给到的活链 alt */ int main(void) { @@ -262,7 +301,7 @@ int main(void) ncm_cache_init(&g_st.ctx.cache, g_st.cache_dir); printf("== T1: first request -> cover appears ==\n"); - cover_request(URL_A); + cover_request(URL_A, NULL); CHECK(wait_cover_hash(url_hash(URL_A), 3000), "cover A shown (hash match)"); CHECK(g_http_calls == 1, "http calls=%d (expect 1)", g_http_calls); CHECK(curjpg_hash() == url_hash(URL_A), "cover_current.jpg == A"); @@ -275,7 +314,7 @@ int main(void) CHECK(g_http_calls == 1, "http calls=%d (still 1)", g_http_calls); printf("== T3: switch song -> cover switches + cover_current.jpg updated ==\n"); - cover_request(URL_B); + cover_request(URL_B, NULL); CHECK(wait_cover_hash(url_hash(URL_B), 3000), "cover switched to B"); CHECK(curjpg_hash() == url_hash(URL_B), "cover_current.jpg == B"); CHECK(g_http_calls == 2, "http calls=%d (expect 2)", g_http_calls); @@ -284,13 +323,13 @@ int main(void) CHECK(!cover_changed(), "no churn after switch"); printf("== T4: re-request same url -> dedup ==\n"); - cover_request(URL_B); + cover_request(URL_B, NULL); usleep(500 * 1000); CHECK(g_http_calls == 2, "http calls=%d (still 2, dedup)", g_http_calls); printf("== T5: flaky network x2 -> backoff retry succeeds ==\n"); g_fail_next = 2; - cover_request(URL_C); + cover_request(URL_C, NULL); CHECK(wait_cover_hash(url_hash(URL_C), 8000), "cover C after retries"); CHECK(g_http_calls >= 5, "http calls=%d (>=5: 2 fails + 1 ok for C)", g_http_calls); CHECK(g_log_warns >= 2, "warn logs=%d (failures logged, 不无声)", g_log_warns); @@ -298,7 +337,7 @@ int main(void) printf("== T6: permanent failure -> give up after %d tries ==\n", 8); g_fail_next = 100; int before = g_http_calls; - cover_request(URL_D); + cover_request(URL_D, NULL); usleep(25 * 1000 * 1000 / 10); /* ~2.5s 不够 8 次退避(0.5+1+2+4*5=23.5s),分段等 */ for (int t = 0; t < 30 && g_http_calls - before < 8; t++) usleep(1000 * 1000); int tries = g_http_calls - before; @@ -312,7 +351,7 @@ int main(void) g_fail_next = 0; int b8 = g_http_calls; g_webp_next = 1; - cover_request(URL_E); + cover_request(URL_E, NULL); usleep(800 * 1000); /* 等 worker 处理 webp 体 → 判 webp → 清目标 → 转怠速 */ CHECK(g_http_calls == b8 + 1, "webp: single download=%d (no retry)", g_http_calls - b8); CHECK(cur_cover_hash() == url_hash(URL_C), "webp: 旧封面 C 保留(兜底,不误换)"); @@ -321,7 +360,7 @@ int main(void) printf("== T9: HTTP 403 anti-hotlink -> single download, no retry, distinct log ==\n"); int b9 = g_http_calls; g_status_next = 403; - cover_request(URL_F); + cover_request(URL_F, NULL); usleep(800 * 1000); CHECK(g_http_calls == b9 + 1, "403: single download=%d (no retry)", g_http_calls - b9); CHECK(cur_cover_hash() == url_hash(URL_C), "403: 旧封面 C 保留(兜底)"); @@ -331,16 +370,41 @@ int main(void) printf("== T10: 200+JSON error body (dead origin) -> not an image, no retry ==\n"); int b10 = g_http_calls; g_json_next = 1; - cover_request(URL_G); + cover_request(URL_G, NULL); usleep(800 * 1000); CHECK(g_http_calls == b10 + 1, "json: single download=%d (no retry)", g_http_calls - b10); CHECK(cur_cover_hash() == url_hash(URL_C), "json: 旧封面 C 保留(兜底)"); CHECK(strstr(g_last_log, "not an image") != NULL, "log: 'not an image' seen"); + printf("== T11: 原链死 -> song/detail 兜底活链 -> 出图 ==\n"); + g_fail_next = 0; g_json_next = 0; g_webp_next = 0; g_status_next = 0; + ncm_strlcpy(g_dead_prefix, "http://p1.music.126.net/dead/", sizeof g_dead_prefix); + ncm_strlcpy(g_alt_pic, URL_LIVE, sizeof g_alt_pic); + int sd11 = g_sd_calls; + cover_request(URL_H, "90001"); + CHECK(wait_cover_hash(url_hash(URL_LIVE), 6000), "cover shown via song/detail live pic"); + CHECK(g_sd_calls == sd11 + 1, "song/detail called once (sd=%d)", g_sd_calls - sd11); + CHECK(cur_cover_hash() == url_hash(URL_LIVE), "cover == song/detail live pic"); + CHECK(strstr(g_last_log, "song-detail fallback id=90001") != NULL, "log: song-detail fallback seen"); + + printf("== T12: 原链死 -> 兜底也死 -> 歌单封面兜底 + 冷却不重试 ==\n"); + ncm_strlcpy(g_alt_pic, URL_H2, sizeof g_alt_pic); /* 兜底也给一个死链 URL */ + int sd12 = g_sd_calls; + cover_request(URL_H, "90002"); + usleep(1500 * 1000); /* 等:URL_H 死 -> song/detail(90002)->URL_H2 -> URL_H2 也 404 -> 清目标 */ + CHECK(g_sd_calls == sd12 + 1, "T12 song/detail once (sd=%d)", g_sd_calls - sd12); + CHECK(cur_cover_hash() == url_hash(URL_LIVE), "T12 兜底也死:保留上个封面(URL_LIVE)"); + int sd13 = g_sd_calls; + cover_request(URL_H, "90002"); /* 同曲 90002 再请求 → 冷却,不再 song/detail */ + usleep(1500 * 1000); + CHECK(g_sd_calls == sd13, "cooldown: no 2nd song/detail (sd=%d)", g_sd_calls - sd13); + CHECK(strstr(g_last_log, "song-detail fallback cooldown") != NULL, "log: cooldown seen"); + g_dead_prefix[0] = 0; g_alt_pic[0] = 0; + printf("== T7: null/empty + reset ==\n"); int b7 = g_http_calls; - cover_request(NULL); - cover_request(""); + cover_request(NULL, NULL); + cover_request("", NULL); CHECK(g_http_calls == b7, "null/empty ignored (calls=%d)", g_http_calls); cover_cache_reset(); CHECK(cur_cover_hash() == 0, "reset -> cover cleared"); diff --git a/patch_v2/ncm_app/ncm b/patch_v2/ncm_app/ncm index 1c16103..91f232c 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 38ac9d5..cbcab02 100755 Binary files a/patch_v2/ncm_app/ncm_host and b/patch_v2/ncm_app/ncm_host differ