fix(ncm): 封面矩阵实锤修正 —— 200解码失败=死对象返JSON错误体(非webp)/UA改桌面浏览器(CNMPlayer UA被拦403)/非图k不定性不重试
真机(设备恢复)CDN 行为矩阵结论并落地: - 裸请求(无Referer)→403: 防盗链坐实, 封面请求头加 Referer https://music.163.com/。 - +Referer 仍不返真图; 全部变体只见 403/404/200+JSON错误体(43B {"code, application/json)。 ③「200解码失败」真因=源站死对象返 200+JSON错误体, 非webp; stbi解不了→旧逻辑盲指数退避8次。 → download_cover 回填 *out_kind(0可解码/1 webp/2非图), worker 对 kind=1/2 与确定性 4xx 一律 立即清目标走兜底(保留歌单封面)不重试; kind=2 打 'not an image (origin error body/200)'。 - UA 分表: CNMPlayer口径 UA(Mozilla/5.0 CNMPlayer/0.1)同Referer也返403(CDN拦bot串); 桌面 Chrome UA(旧cover.c)返200。→ NCM_COVER_UA 改回桌面浏览器 UA。 - ?param=300y300 在死对象上→404(对死链无效); 存活URL未获样本, 保留 USE_PARAM=1(标准缩略)。 - cover_chain_sim 增 T10(200+JSON→非图不重试) + stub 内容加 JPEG 魔数(过 body_is_decodable_img); T1-T10 ALL PASS。mipsel 0 warning(text 1020306/bss 1362500 持平)。 新 ncm md5 06896b54f31ddf55980cbf921a00d67a (未部署, 留主会话上卡)
This commit is contained in:
1 parent
aeb9176101
commit
9d93f1b1ef
5 files changed
+138
-56
No files matched your search
@@ -33,7 +33,11 @@
|
||||
* 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"
|
||||
/* UA 分表(真机矩阵实测结论):本批在设备端逐 UA 试,`Mozilla/5.0 CNMPlayer/0.1`(CNMPlayer 口径)
|
||||
* 对封面 CDN 同 Referer 也返 403(CDN 拦 bot 串);桌面浏览器 UA(旧 cover.c 一直用、设备实测返 200)
|
||||
* 被 CDN 接受。故封面走**桌面浏览器 UA**,Referer 对齐 CNMPlayer(防盗链,防裸请求 403)。 */
|
||||
#define NCM_COVER_UA "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"
|
||||
/* ?param=WxH 缩略后缀宏 NCM_COVER_PARAM / NCM_COVER_USE_PARAM 见 ncm_app_internal.h(与
|
||||
* bench/cover_chain_sim.c 同源)。 */
|
||||
|
||||
@@ -107,8 +111,7 @@ static int str_ci_contains(const char *hay, const char *needle)
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* 文件是否 WebP:魔数 RIFF....WEBP(stb_image 不支持 webp,解码必 NULL → 显示「200 但解码
|
||||
* 失败」三分型③,先决判定不盲重试)。读文件前 12 字节即可,代价一次小 fopen。 */
|
||||
/* 文件是否 WebP:魔数 RIFF....WEBP(stb_image 不支持 webp,解码必 NULL)。读文件前 12 字节。 */
|
||||
static int file_is_webp(const char *path)
|
||||
{
|
||||
if (!path) return 0;
|
||||
@@ -122,17 +125,43 @@ static int file_is_webp(const char *path)
|
||||
b[8]=='W'&&b[9]=='E'&&b[10]=='B'&&b[11]=='P';
|
||||
}
|
||||
|
||||
/* body 是否为 stb 可解码的静态图:JPEG(FFD8FF)/PNG(89504E47)/BMP(424D)/GIF(474946)。
|
||||
* 真机矩阵(REPORT §34.1)实锤:源站死对象对封面 URL 返 **200 + JSON 错误体**(43B `{"code..`,
|
||||
* content-type application/json),旧逻辑 stbi 解码 NULL → 当成「fetch failed」盲指数重试 8 次(真机
|
||||
* 日志 `rc=0 status=200` + `retry`)。故 200 且 body 非**已知可解码图**(含 JSON 错误体/未知格式)
|
||||
* 均判确定性失败不重试(重试救不了死对象)。content-type 明确非 image/ 前缀(如 application/json)
|
||||
* 也直接判非图。 */
|
||||
static int body_is_decodable_img(const char *path, const ncm_http_resp_t *resp)
|
||||
{
|
||||
if (resp && resp->has_content_type && resp->content_type[0]) {
|
||||
if (strncmp(resp->content_type, "image/", 6) != 0) return 0; /* 如 application/json */
|
||||
}
|
||||
if (!path) return 0;
|
||||
unsigned char b[8];
|
||||
FILE *f = fopen(path, "rb");
|
||||
if (!f) return 0;
|
||||
size_t n = fread(b, 1, sizeof b, f);
|
||||
fclose(f);
|
||||
if (n < 4) return 0;
|
||||
if (b[0]==0xFF && b[1]==0xD8 && b[2]==0xFF) return 1; /* JPEG */
|
||||
if (b[0]==0x89 && b[1]=='P' && b[2]=='N' && b[3]=='G') return 1; /* PNG */
|
||||
if (b[0]=='B' && b[1]=='M') return 1; /* BMP */
|
||||
if (b[0]=='G' && b[1]=='I' && b[2]=='F' && b[3]=='8') return 1; /* GIF */
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* 下载 url 到 cache 写者 w(流式,写进其临时文件),成功后从临时文件解码并返回 img。
|
||||
* 失败返回 NULL(不 commit,调用方 abort)。*out_len=body 字节数;*out_rc 与 *out_status
|
||||
* 回填下载 rc 与 HTTP status(封面 4xx 加固判定:确定性 4xx 不重试,REPORT §NEW 任务3);
|
||||
* *out_webp=1 表示 200 但 body 是 WebP(stb 不支持,先判不解码——三分型③)。 */
|
||||
* *out_kind 回填 200 体类型:0=可解码图、1=WebP(stb 不支持)、2=非图(源站死对象 200+JSON 错误体/
|
||||
* 未知格式)——1/2 均不解码,确定性不重试(REPORT §34.1 真机矩阵:200 解码失败实为死对象回 JSON 体)。 */
|
||||
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_webp)
|
||||
int *out_rc, int *out_status, int *out_kind)
|
||||
{
|
||||
ncm_http_req_t req;
|
||||
memset(&req, 0, sizeof req);
|
||||
/* 头对齐 CNMPlayer(REPORT §NEW 任务2):Referer 防盗链 + UA 分表。 */
|
||||
char hdrs[192];
|
||||
/* 头:Referer 防盗链(CNMPlayer 口径)+ 桌面浏览器 UA(真机矩阵:CNMPlayer UA 被拦 403)。 */
|
||||
char hdrs[256];
|
||||
snprintf(hdrs, sizeof hdrs, "Referer: %s\r\nUser-Agent: %s", NCM_COVER_REFERER, NCM_COVER_UA);
|
||||
req.url = url;
|
||||
req.method = "GET";
|
||||
@@ -151,9 +180,13 @@ static meow_img *download_cover(const char *url, ncm_cache_writer_t *w, size_t *
|
||||
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 (out_kind) *out_kind = 0;
|
||||
if ((resp.has_content_type && str_ci_contains(resp.content_type, "webp")) || file_is_webp(tmp)) {
|
||||
if (out_webp) *out_webp = 1;
|
||||
if (out_kind) *out_kind = 1; /* webp(stb 不支持) */
|
||||
return NULL;
|
||||
}
|
||||
if (!body_is_decodable_img(tmp, &resp)) {
|
||||
if (out_kind) *out_kind = 2; /* 非图(源站死对象 200+JSON 错误体/未知格式) */
|
||||
return NULL;
|
||||
}
|
||||
return meow_img_load_file(tmp);
|
||||
@@ -221,7 +254,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, dwebp = 0; /* 下载 rc/HTTP status/webp 判回(分诊用) */
|
||||
int drc = NCM_ERR_NET, dstatus = 0, dkind = 0; /* 下载 rc/HTTP status/200 体类型(分诊用) */
|
||||
|
||||
/* ① 缓存命中直接解码(免下载、离线可看;LRU:lookup 刷新 mtime) */
|
||||
char cpath[512];
|
||||
@@ -237,7 +270,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, &dwebp);
|
||||
img = download_cover(url, w, &blen, &drc, &dstatus, &dkind);
|
||||
if (img) {
|
||||
pthread_mutex_lock(&g_cm);
|
||||
save_cover_raw(ncm_cache_writer_tmp_path(w), blen);
|
||||
@@ -263,7 +296,7 @@ static void *cover_worker(void *arg)
|
||||
* - 仅瞬时错误(超时/网络/TLS/5xx)指数退避重试。
|
||||
* 日志一律打 rc/status(排查资产:先前只见 "fetch failed" 不知 HTTP 层返了什么);
|
||||
* 403 单独日志(防盗链特征,与 404 死链区分——三者修法不同)。 */
|
||||
if (dwebp) {
|
||||
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);
|
||||
pthread_mutex_lock(&g_cm);
|
||||
@@ -272,6 +305,17 @@ static void *cover_worker(void *arg)
|
||||
backoff_ms = 0; fails = 0;
|
||||
continue;
|
||||
}
|
||||
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);
|
||||
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);
|
||||
if (dstatus == 403) {
|
||||
meow_log(MEOW_LOG_WARN, "cover: fetch fail url=%.64s rc=%d status=403 (anti-hotlink/forbidden, no retry)",
|
||||
|
||||
Reference in new issue
Block a user