fix(ncm): 封面链分诊与修复 —— CNMPlayer 头(Referer+UA 解403)/webp 魔数内容型先判不解码走兜底/URL 追加 param=300y300 缩略图/403 与 webp 单独分诊日志不盲重试

- cover.c: 请求头对齐 CNMPlayer(Referer https://music.163.com/ + UA Mozilla/5.0 CNMPlayer/0.1);
  下载后先判 content-type 含 webp 或临时文件魔数 RIFF....WEBP(stb_image 不支持),回 webp=1 不解码,
  worker 打 'webp unsupported' 立即清目标走兜底(保留歌单封面),不再当 fetch failed 盲目指数重试;
  403 单独日志(anti-hotlink/forbidden, no retry),确定性 4xx 不重试(沿用 311)。
- cover_request 入口 cover_norm_url 追加 ?param=300y300(已有 param= 尊重原样, 溢出保原 URL),
  缓存键/去重/下载 URL 同参版(省内存+出 jpeg)。
- ncm_app_internal.h: NCM_COVER_USE_PARAM/NCM_COVER_PARAM 共享宏(cover.c 与 sim 同源)。
- bench/cover_chain_sim.c: 补 ncm_http_download/meow_img_load_file/meow_img_downscale 桩对齐现行
  cover.c(旧 ncm_http_request 桩已过时), url_hash 同参数规则; 新增 T8 webp/T9 403 用例。
- 验证: cover_chain_sim T1-T9 ALL PASS + stream/player_flow/window/search/cache_evict/degrade/mem
  sim 全 PASS + core host+mipsel(qemu) PASS + build_ncm.sh 0 warning(text 1019834/bss 1362500 持平)
  + ncm_host UI dump exit=0。0 新增真实 CDN 请求; 设备端 CDN 矩阵因设备不可达未执行(见 REPORT 34.6)。

新 ncm md5 a7c22bd4ee3bcd02fb24046d27a33242 (未部署, 留主会话上卡)
This commit is contained in:
AskaEth committed 2026-09-02 10:37:24 +08:00
1 parent c7ef1ebb04
commit aeb9176101
6 files changed
+308 -20

No files matched your search

+108 -15
View File
@@ -24,9 +24,19 @@
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <ctype.h>
#include <unistd.h>
#include <sys/stat.h>
/* 封面 CDN 请求头(REPORT §NEW 任务2,对齐 CNMPlayer):网易云图片 CDN 靠 Referer 防盗链,
* 缺失被拦返回 403(p3.music.126.net 150B text/html,REPORT §NEW 三分型②);UA 分表口径与
* CNMPlayer 客户端一致(Mozilla/5.0 CNMPlayer/0.1 + Referer https://music.163.com/)。两者
* 均为常量:真机矩阵实测需换 UA 时只改 NCM_COVER_UA 一处,不涉逻辑(见 REPORT §NEW 矩阵结论)。 */
#define NCM_COVER_REFERER "https://music.163.com/"
#define NCM_COVER_UA "Mozilla/5.0 CNMPlayer/0.1"
/* ?param=WxH 缩略后缀宏 NCM_COVER_PARAM / NCM_COVER_USE_PARAM 见 ncm_app_internal.h(与
* bench/cover_chain_sim.c 同源)。 */
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 */
@@ -65,18 +75,68 @@ static int cover_sink_write(void *ctx, const unsigned char *data, size_t len)
return 0;
}
/* 封面 URL 规范化:追加 NCM_COVER_PARAM 尺寸后缀(REPORT §NEW 任务2)。已含 param= 的
* URL 原样保留(尊重调用方/宿主食之已有尺寸);否则追加(已有其它查询用 &,无则用 ?)。
* 溢出(512 缓冲放不下)则放弃追加——宁可用全尺寸 URL 也不截断出坏 URL。 */
static void cover_norm_url(const char *src, char *out, size_t out_sz)
{
ncm_strlcpy(out, src, out_sz);
#if NCM_COVER_USE_PARAM
if (out[0] && strstr(out, "param=")) return; /* 已指定尺寸,不叠 */
size_t l = strlen(out);
size_t add = strlen("?param=") + strlen(NCM_COVER_PARAM);
if (!out[0] || l + add + 1 >= out_sz) return; /* 放不下:保原 URL */
strcat(out, strchr(out, '?') ? "&param=" : "?param=");
strcat(out, NCM_COVER_PARAM);
#endif
}
/* 大小写不敏感的 posix 子串检测(内容 webp 判定用;不依赖 strcasestr/_GNU_SOURCE 桩)。 */
static int str_ci_contains(const char *hay, const char *needle)
{
if (!hay || !needle) return 0;
size_t n = strlen(needle);
if (!n) return 1;
for (; *hay; hay++) {
if (tolower((unsigned char)hay[0]) == tolower((unsigned char)needle[0])) {
const char *a = hay, *b = needle;
while (*b && tolower((unsigned char)*a) == tolower((unsigned char)*b)) { a++; b++; }
if (!*b) return 1;
}
}
return 0;
}
/* 文件是否 WebP:魔数 RIFF....WEBP(stb_image 不支持 webp,解码必 NULL → 显示「200 但解码
* 失败」三分型③,先决判定不盲重试)。读文件前 12 字节即可,代价一次小 fopen。 */
static int file_is_webp(const char *path)
{
if (!path) return 0;
unsigned char b[12];
FILE *f = fopen(path, "rb");
if (!f) return 0;
size_t n = fread(b, 1, sizeof b, f);
fclose(f);
if (n < 12) return 0;
return b[0]=='R'&&b[1]=='I'&&b[2]=='F'&&b[3]=='F' &&
b[8]=='W'&&b[9]=='E'&&b[10]=='B'&&b[11]=='P';
}
/* 下载 url 到 cache 写者 w(流式,写进其临时文件),成功后从临时文件解码并返回 img。
* 失败返回 NULL(不 commit,调用方 abort)。*out_len=body 字节数;*out_rc 与 *out_status
* 回填下载 rc 与 HTTP status(封面 4xx 加固判定:确定性 4xx 不重试,REPORT §NEW 任务3)。 */
* 回填下载 rc 与 HTTP status(封面 4xx 加固判定:确定性 4xx 不重试,REPORT §NEW 任务3);
* *out_webp=1 表示 200 但 body 是 WebP(stb 不支持,先判不解码——三分型③)。 */
static meow_img *download_cover(const char *url, ncm_cache_writer_t *w, size_t *out_len,
int *out_rc, int *out_status)
int *out_rc, int *out_status, int *out_webp)
{
ncm_http_req_t req;
memset(&req, 0, sizeof req);
/* 头对齐 CNMPlayer(REPORT §NEW 任务2):Referer 防盗链 + UA 分表。 */
char hdrs[192];
snprintf(hdrs, sizeof hdrs, "Referer: %s\r\nUser-Agent: %s", NCM_COVER_REFERER, NCM_COVER_UA);
req.url = url;
req.method = "GET";
req.extra_hdrs = "User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) "
"AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36";
req.extra_hdrs = hdrs;
req.verify_ca = NCM_HTTP_VERIFY_DEFAULT;
req.timeout_ms = 15000;
ncm_http_resp_t resp;
@@ -89,6 +149,13 @@ static meow_img *download_cover(const char *url, ncm_cache_writer_t *w, size_t *
/* 从写者临时文件解码(未 commit,文件仍在 .tmp;解码到内存 RGBA,与落盘无关) */
const char *tmp = ncm_cache_writer_tmp_path(w);
if (!tmp) return NULL;
/* WebP 先判:content-type 含 webp 或文件魔数 RIFF....WEBP → 不解码(stb 不支持),
* 回 webp=1 走兜底;避免 stbi_load 空转返回 NULL 后当成「fetch failed」盲指数退重试。 */
if (out_webp) *out_webp = 0;
if ((resp.has_content_type && str_ci_contains(resp.content_type, "webp")) || file_is_webp(tmp)) {
if (out_webp) *out_webp = 1;
return NULL;
}
return meow_img_load_file(tmp);
}
@@ -154,7 +221,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 判定用) */
int drc = NCM_ERR_NET, dstatus = 0, dwebp = 0; /* 下载 rc/HTTP status/webp 判回(分诊用) */
/* ① 缓存命中直接解码(免下载、离线可看;LRU:lookup 刷新 mtime) */
char cpath[512];
@@ -170,7 +237,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, &drc, &dstatus);
img = download_cover(url, w, &blen, &drc, &dstatus, &dwebp);
if (img) {
pthread_mutex_lock(&g_cm);
save_cover_raw(ncm_cache_writer_tmp_path(w), blen);
@@ -189,12 +256,33 @@ static void *cover_worker(void *arg)
/* 内存瘦身:全尺寸封面解码后降采样到 ≤480px 驻留(省 ~7MB anon,OOM 修复)。 */
if (img) img = cover_cap(img);
if (!img) {
/* 失败分诊(REPORT §NEW 任务3):确定性 4xx(404 死链等)**不重试**,立即清目标
* 跳下一张(重试也白费);仅瞬时错误(超时/网络/TLS/5xx)指数退避重试。
* 日志打 rc/status(排查资产:先前只见 "fetch failed" 不知 HTTP 层返了什么)。 */
/* 失败分诊(REPORT §NEW 任务3):
* - webp:200 但 stb 不支持解码(三分型③),**非瞬时、非重试可愈** → 立即清目标
* 走兜底(播放页保留歌单封面 g_st.cover_url,不盲目指数重试空转);
* - 确定性 4xx(404 死链 / **403 防盗链**)**不重试**,立即清目标跳下一张;
* - 仅瞬时错误(超时/网络/TLS/5xx)指数退避重试。
* 日志一律打 rc/status(排查资产:先前只见 "fetch failed" 不知 HTTP 层返了什么);
* 403 单独日志(防盗链特征,与 404 死链区分——三者修法不同)。 */
if (dwebp) {
meow_log(MEOW_LOG_WARN, "cover: webp unsupported url=%.64s rc=%d status=%d (no decode)",
url, drc, dstatus);
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;
}
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 (dstatus == 403) {
meow_log(MEOW_LOG_WARN, "cover: fetch fail url=%.64s rc=%d status=403 (anti-hotlink/forbidden, no retry)",
url, drc);
} else if (deterministic) {
meow_log(MEOW_LOG_WARN, "cover: fetch fail url=%.64s rc=%d status=%d (4xx, no retry)",
url, drc, dstatus);
} else {
meow_log(MEOW_LOG_WARN, "cover: fetch fail url=%.64s rc=%d status=%d",
url, drc, dstatus);
}
if (deterministic) {
pthread_mutex_lock(&g_cm);
if (strcmp(g_tgt_url, url) == 0) g_tgt_url[0] = 0; /* 立即跳下一张 */
@@ -238,16 +326,21 @@ static void *cover_worker(void *arg)
return NULL;
}
/* UI 线程:提交目标封面 URL(无网络/无 URL 则忽略)。 */
/* UI 线程:提交目标封面 URL(无网络/无 URL 则忽略)。
* 先 cover_norm_url 规范化(追加 ?param=300y300 缩略后缀,REPORT §NEW 任务2),使
* g_tgt_url/g_cover_url/缓存键/下载 URL 走同一参数版本(小图省内存、命中高)。 */
void cover_request(const char *url)
{
if (!url || !url[0]) return;
if (g_host_mock) return; /* 宿主 mock = 0 网络纪律(mock URL 不得发真实 HTTPS) */
char norm[512];
cover_norm_url(url, norm, sizeof norm);
const char *u = norm[0] ? norm : url; /* 规范化溢出保护:退回原 URL */
pthread_mutex_lock(&g_cm);
/* 去重:已在展示 或 已在拉取途中的同一 URL 不重复提交。 */
if ((g_cover_url[0] && strcmp(g_cover_url, url) == 0) ||
(g_tgt_url[0] && strcmp(g_tgt_url, url) == 0)) { pthread_mutex_unlock(&g_cm); return; }
ncm_strlcpy(g_tgt_url, url, sizeof g_tgt_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);
pthread_mutex_unlock(&g_cm);
if (!g_thr_on) {
g_thr_on = 1;