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
+61
-43
@@ -3001,34 +3001,44 @@ cb8187868f71a9fa155075d0624711d6 ncm # mipsel 静态 App(text 101711
|
||||
|
||||
> 真机已实锤三种封面失败形态(详见下「真机三分型」):①404 死链(CDN 源站无对象,`X-Error-Info: Origin`,
|
||||
> 182B JSON 错误体,311/§33.3 已做 4xx 不重试);②**403 防盗链**(p3.music.126.net 返 150B text/html,
|
||||
> 缺 Referer/UA 被拦嫌疑大);③**200 但解码失败**(rc=0 status=200 仍 fetch failed——响应体疑为 webp,
|
||||
> stb_image 不支持)。
|
||||
> 缺 Referer/UA 被拦嫌疑大);③**200 但解码失败**(rc=0 status=200 仍 fetch failed——响应体**非 webp,
|
||||
> 实为源站死对象返 200+JSON 错误体**,stb_image 解不了,本批矩阵实锤)。
|
||||
|
||||
### 34.1 真机三分型结论(本批未能在真机复测——设备 10.78.193.50 全程不可达,见 34.6 诚实出入)
|
||||
### 34.1 真机三分型结论(本批已在设备端跑成 CDN 行为矩阵,见 34.6 明细)
|
||||
|
||||
本批无法执行**设备端 CDN 行为矩阵**(设备 sleep/WiFi 休眠,`No route to host`),故三种分型的鉴定依据:
|
||||
①404 由 §30.5/§33.3 已实测锚定;②403 与③200-webp 由本批任务书给定、代码静态分析 + 宿主 mock 链
|
||||
验证(stb_image v2.18 无 webp 解码器,`meow_img_load_file` 对 webp 必返 NULL → 旧逻辑当成
|
||||
「fetch failed」盲指数退避重试)。**UA/Referer/param 三参数取值**:对齐 CNMPlayer 客户端参考实现
|
||||
(`Referer: https://music.163.com/` + `User-Agent: Mozilla/5.0 CNMPlayer/0.1`,见 CNMPlayer `src/app/mod.rs
|
||||
App::new` 的 `default_headers`);`?param=300y300` 为网易云图片 CDN 标准缩略参数(ncm-api-rs 亦以其为
|
||||
已知格式)。真机矩阵留 34.6 后续确认。
|
||||
设备 10.78.193.50 本批初段 `No route to host`(sleep/WiFi 休眠),后恢复;用 BusyBox `wget`(无 curl)
|
||||
单次只读对日志抓出的真实封面 URL 跑了 header/param 变体矩阵,**实锤结论**(详见 34.6 原始结果):
|
||||
|
||||
- **裸请求(无 Referer)→ 403**:CDN 防盗链按 Referer 拦截——**② 403 防盗链坐实**,修法 = 加 Referer。
|
||||
- **+Referer 仍不返真图**:全变体只看到 **403 / 404 / 200+JSON 错误体(43B,`content-type:
|
||||
application/json`,magic `{"code`)**,从未见 JPEG/PNG/BMP/webp。**③「200 解码失败」真因 = 源站死对象
|
||||
返 200+JSON 错误体**(非 webp)——stbi 解不了 → 旧逻辑当成「fetch failed」盲指数退避 8 次(真机日志
|
||||
`rc=0 status=200` + `retry` 6 连)。故修法重心从「webp」改为「200 且 body 非已知可解码图 → 确定性不重试」。
|
||||
- **UA 分表实测**:`Mozilla/5.0 CNMPlayer/0.1`(CNMPlayer 口径)同 Referer 也返 **403**(CDN 拦 bot 串);
|
||||
旧 cover.c 的**桌面 Chrome UA** 设备实测返 **200**。故封面 UA 用**桌面浏览器**(非 CNMPlayer UA),
|
||||
Referer 仍对齐 CNMPlayer。**参数修正:本批 UA 由初提交的 CNMPlayer 口径改为桌面 Chrome**(见 34.2)。
|
||||
- **`?param=300y300`**:在死对象上→404(对死链无效);对存活 URL 推测可返 300×300 JPEG(网易云标准缩略
|
||||
参数),但本批测试 URL 全为死链未能直接证实——保留 `NCM_COVER_USE_PARAM=1`,真机存活封面再确认。
|
||||
|
||||
### 34.2 修法(`app/logic/cover.c` + `app/ncm_app_internal.h`)
|
||||
|
||||
- **请求头对齐 CNMPlayer**:`req.extra_hdrs = "Referer: https://music.163.com/\r\nUser-Agent: Mozilla/5.0 CNMPlayer/0.1"`
|
||||
(常量宏 `NCM_COVER_REFERER`/`NCM_COVER_UA`,改 UA 只动一处)。修 403 防盗链(②)。
|
||||
- **webp 先判不解码**:`download_cover` 成功后、`meow_img_load_file` 前,检查 `resp.content_type`
|
||||
含 webp 或临时文件魔数 `RIFF....WEBP`(`file_is_webp`,读前 12 字节一次小 fopen)→ 回 `*out_webp=1`
|
||||
返回 NULL;worker 判 webp 单独打日志 `cover: webp unsupported url=.. (no decode)`,立即清目标走兜底
|
||||
(播放页保留歌单封面 `g_st.cover_url`),**不盲目指数重试**(修 ③)。
|
||||
- **封面 URL 追加尺寸参数**:`cover_norm_url`(`cover_request` 入口处规范化)对不含 `param=` 的 URL 追加
|
||||
`?param=300y300`(已有 `?` 用 `&`;溢出 512 缓冲则放弃追加保原 URL)。`NCM_COVER_USE_PARAM=1`;
|
||||
`NCM_COVER_PARAM` 宏放共享头 `ncm_app_internal.h`,与 `bench/cover_chain_sim.c` 的 URL 期望哈希同源,
|
||||
避免字符串漂移。省内存(300×300 ≈20KB vs 全尺寸 1.34MB)+ CDN 出 jpeg(避 webp)+ 命中率高。
|
||||
- **分诊日志强化**:403 单独打 `cover: fetch fail url=.. rc=.. status=403 (anti-hotlink/forbidden, no retry)`;
|
||||
404 及其它确定性 4xx `(4xx, no retry)`;webp `(no decode)`;瞬时错误才指数退避。确定性 4xx + webp
|
||||
一律立即清目标不重试(已有 4xx 逻辑,本批补 403 区分日志 + webp 分支)。
|
||||
- **请求头**:`Referer: https://music.163.com/`(防盗链,解 ②)+ **桌面浏览器 UA**(真机矩阵:CNMPlayer UA
|
||||
被拦 403,桌面 Chrome UA 返 200——解 UA 分表)。常量 `NCM_COVER_REFERER`/`NCM_COVER_UA` 一处可改。
|
||||
- **200 体先判类型(kind)后解码**:`download_cover` 成功后、`meow_img_load_file` 前回填 `*out_kind`:
|
||||
- `1`=WebP(content-type 含 webp 或魔数 `RIFF....WEBP`,stb 不支持)→ 不解码;
|
||||
- `2`=非图(content-type 非 `image/` 前缀,或魔数非 JPEG/PNG/BMP/GIF——**源站死对象 200+JSON 错误体**
|
||||
即此)→ 不解码;
|
||||
- `0`=可解码图(JPEG `FFD8FF`/PNG `89504E47`/BMP `424D`/GIF `474946`) → `meow_img_load_file` 正常解码。
|
||||
`body_is_decodable_img` 读临时文件前 8 字节魔数 + content-type 判定。
|
||||
- **worker 分诊**:kind=1 打 `cover: webp unsupported url=.. (no decode)`;kind=2 打
|
||||
`cover: not an image (origin error body/200) url=.. (no retry)`;403 单独打
|
||||
`(anti-hotlink/forbidden, no retry)`;404/其它确定性 4xx `(4xx, no retry)`;**kind=1/2 与确定性 4xx 一律
|
||||
立即清目标走兜底(保留歌单封面),不盲目指数重试**(根修 ③:死对象 200+JSON 体不再空转 8 次);
|
||||
仅瞬时错误(超时/网络/TLS/5xx)指数退避。
|
||||
- **封面 URL 追加尺寸参数**:`cover_norm_url`(`cover_request` 入口)对不含 `param=` 的 URL 追加
|
||||
`?param=300y300`(已有 `?` 用 `&`;溢出 512 保原 URL)。`NCM_COVER_USE_PARAM=1`;`NCM_COVER_PARAM` 放
|
||||
共享头 `ncm_app_internal.h`,与 sim 的 URL 期望哈希同源。省内存(300×300≈20KB vs 全尺寸 1.34MB)
|
||||
+ 存活 URL 出 jpeg。
|
||||
|
||||
### 34.3 worker 队列评估(304 建议:单槽→有界 FIFO+优先级)
|
||||
|
||||
@@ -3038,11 +3048,11 @@ App::new` 的 `default_headers`);`?param=300y300` 为网易云图片 CDN 标
|
||||
优先级会为**不会显示的过期封面**白付解码 CPU + 驻留内存(设备 RAM 仅 56MB),净负收益。304 原始
|
||||
关切(4xx 空转重试占 worker)已由本批「确定性 4xx/webp 不重试 + 瞬时退避」根治。留后续共识,不迁移。
|
||||
|
||||
### 34.4 验证记录(宿主 + qemu;0 新增真实请求——矩阵测试因设备不可达未执行)
|
||||
### 34.4 验证记录(宿主 + qemu;0 新增真实 CDN 业务请求——设备矩阵测试已执行且为单次只读)
|
||||
|
||||
| 项 | 方式 | 结果 |
|
||||
|---|---|---|
|
||||
| cover 链(含新 webp/403) | `cover_chain_sim`(驱动真实 cover.c + ncm_cache,HTTP 本地 stub;T1-T7 存量 + 新增 T8 webp 魔数 / T9 403) | **T1-T9 ALL PASS**(T8:webp 单次下载不重试+旧封面兜底+日志 `webp unsupported`;T9:403 单次下载不重试+日志含 403) |
|
||||
| cover 链(含 webp/403/非图) | `cover_chain_sim`(驱动真实 cover.c + ncm_cache,HTTP 本地 stub;T1-T7 存量 + 新增 T8 webp 魔数 / T9 403 / T10 200+JSON 非图) | **T1-T10 ALL PASS**(T8:webp 单次下载不重试+旧封面兜底+日志 `webp unsupported`;T9:403 单次下载不重试+日志含 403;T10:JSON 错误体单次下载不重试+日志 `not an image`) |
|
||||
| 缓存 LRU/隔离 | `cache_evict_sim` | **PASS** |
|
||||
| 播放状态机 | `player_flow_sim` P1-P12 | **PASS** |
|
||||
| 大歌单滑窗 | `window_sim` | **PASS** |
|
||||
@@ -3051,40 +3061,48 @@ App::new` 的 `default_headers`);`?param=300y300` 为网易云图片 CDN 标
|
||||
| 降级链 | `degrade_sim`(含真机场景) | **ALL PASS** |
|
||||
| 内存瘦身 | `mem_sim`(MEM_SIM=1) | **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 1019834 / data 1648 / bss 1362500`(bss 与 §33 持平,封面改动未涨静态内存) |
|
||||
| 构建 | `build_ncm.sh`(自有代码 -Wall -Wextra) | **0 warning**;`text 1020306 / data 1648 / bss 1362500`(bss 与 §33 持平,封面改动未涨静态内存) |
|
||||
| 宿主构建 | `build_ncm_host.sh`(自有代码) | 0 warning(仅 SDK `meow_ui_vol.c` 存量 `system()` 未用结果告警,非本批) |
|
||||
| UI 渲染 | `build_ncm_host.sh` + `NCM_PAGE=home/player/playlist MEOW_UI_DUMP` | exit=0,dump 正常 |
|
||||
|
||||
### 34.5 md5(本批产物)
|
||||
|
||||
```
|
||||
a7c22bd4ee3bcd02fb24046d27a33242 ncm # 新(未部署,留主会话上卡)
|
||||
f870b49b11ad8c0bb67cd47d2f6c2162 ncm_host # 宿主调试二进制(重链)
|
||||
06896b54f31ddf55980cbf921a00d67a ncm # 新(未部署,留主会话上卡)
|
||||
2f293d46b535519c2a5b1cb97f0eba9a ncm_host # 宿主调试二进制(重链)
|
||||
97bb2bdb44a2f446877787d9261b4f02 test/test_ncm_core_host # 宿主 core 测试(重编,内容未变)
|
||||
```
|
||||
|
||||
改动文件(本批,仅这些):
|
||||
```
|
||||
修改:
|
||||
app/logic/cover.c # 头对齐 CNMPlayer(Referer+UA) + webp 魔数/content-type 先判 + URL 尺寸参数 + 403/webp 分诊日志 + 兜底不盲重试(34.2)
|
||||
app/logic/cover.c # 头 Referer(CNMPlayer口径)+桌面UA(矩阵实证CNMPlayer UA被拦403) + 200体类型先判(webp/非图/可解码) + URL 尺寸参数 + 403/webp/非图 分诊日志 + 兜底不盲重试(34.2)
|
||||
app/ncm_app_internal.h # NCM_COVER_USE_PARAM / NCM_COVER_PARAM 共享宏(cover.c 与 sim 同源)
|
||||
bench/cover_chain_sim.c # 修 sim 对齐现行 cover.c(ncm_http_download/meow_img_load_file/downscale 桩 + url_hash 参数规则)+ 新增 T8 webp / T9 403 用例
|
||||
bench/cover_chain_sim.c # 修 sim 对齐现行 cover.c(ncm_http_download/meow_img_load_file/downscale 桩 + url_hash 参数规则)+ 新增 T8 webp / T9 403 / T10 200+JSON 非图 用例
|
||||
ncm # mipsel 产物(重链)
|
||||
ncm_host # 宿主产物(重链)
|
||||
```
|
||||
(未动 sdk/、meow_launcherd/、其它 App、devdocs/;0 新增真实 CDN 请求;未部署——留主会话统一上卡。)
|
||||
(未动 sdk/、meow_launcherd/、其它 App、devdocs/;0 新增真实 CDN 业务请求——设备矩阵为单次只读;未部署——留主会话统一上卡。)
|
||||
|
||||
### 34.6 诚实出入 / 遗留
|
||||
|
||||
- **设备端 CDN 矩阵未执行**:设备 10.78.193.50 本批全程 `No route to host`(sleep/WiFi 休眠,宿主机
|
||||
10.78.193.150 同网段 ARP 无应答,多次重试 + 后台 ping 监视 600s 均未恢复)。故③「200 解码失败是否
|
||||
webp」未能在真机用 curl 矩阵直接证实(由 stb_image 无 webp 解码器 + magic/content-type 先判兜住);
|
||||
UA 三变体(桌面浏览器 / iPhone NeteaseMusic 9.0.90·5038 / CNMPlayer 口径)与 `?param=300y300` 的
|
||||
实际 CDN 出图效果未实测。**建议真机恢复后补跑**:取日志失败 URL,`curl` 单次比对 裸请求 /
|
||||
`+Referer` / `+UA 变体` / `?param=300y300`,记 http code + content-type + 魔数(RIFF=webp / FFD8=jpeg),
|
||||
据实微调 `NCM_COVER_UA` 与 `NCM_COVER_USE_PARAM`(宏已就位,改一处即生效——UA 在 cover.c 宏、
|
||||
param 在 ncm_app_internal.h 宏)。
|
||||
- **`?param=300y300` 潜在风险**:个别封面若仅存在全尺寸对象,param 版可能 404——已由确定性 4xx
|
||||
不重试兜底(回退歌单封面),不劣于基线;但缓存键随之变化(旧全尺寸 `cov<rawhash>.jpg` 成为孤儿,
|
||||
LRU 正常淘汰)。
|
||||
- **`http→443` 潜性缺陷不修**(承 §33.3/§30.8 结论;现状封面 URL 均为 https,不受影响)。
|
||||
- **设备端 CDN 矩阵已执行(结果见 34.1)**:设备恢复后用 BusyBox `wget`(无 curl/xxd,`od -An -tx1` 取魔数)
|
||||
对日志抓出的真实封面 URL(p1/p3/p4.music.126.net)跑了 裸/`+Referer`/`+UA`/`?param` 变体。**限制**:
|
||||
①设备端 wget 的 UA/Referer 组合仅有限几组(BusyBox wget 不擅多 UA 变体交互,`CNMPlayer/0.1` 带空格
|
||||
的 UA 曾致 `eval` 引号坏、个别变体 ERR);②**所有测试封面 URL 均为死链**(从未返真图),故 `?param=300y300`
|
||||
转唯对存活 URL 的效果、iPhone NeteaseMusic 9.0.90/5038 UA 变体未获存活样本证实——**建议存活封面补测**:
|
||||
`param` 后缀与 iPhone UA 两变体在真机播放时观察 `cover: fetch fail/ok` 日志即可(按已就位宏 `NCM_COVER_UA`
|
||||
/`NCM_COVER_USE_PARAM` 一处改)。
|
||||
- **UA 选用与 CNMPlayer 口径不同**:CNMPlayer 客户端用 `Mozilla/5.0 CNMPlayer/0.1`,但本批矩阵在设备端实测
|
||||
该 UA 同 Referer 也返 **403**(CDN 拦 bot 串),旧 cover.c 的桌面 Chrome UA 返 200——故封面 UA 定为
|
||||
**桌面浏览器**。若后续真机存活封面用 CNMPlayer UA 亦可,仅改 `NCM_COVER_UA` 宏。
|
||||
- **「200 解码失败」根因修正**:原任务书怀疑 webp;矩阵实锤**源站死对象返 200+JSON 错误体**(43B `{"code`,
|
||||
`application/json`),非 webp。故本批以「200 且 body 非已知可解码图(含 JSON 错误体/未知)→ 确定性不重试」
|
||||
为核心修法(kind=2),webp(kind=1)保留为附加分支。三者(403 / 404 / 200+非图)修法不同且都有明确日志。
|
||||
- **`?param=300y300` 潜在风险**:个别封面若仅存在全尺寸对象,param 版可能 404——已由确定性 4xx/非图
|
||||
不重试兜底(回退歌单封面),不劣于基线;但缓存键随之变化(旧全尺寸 `cov<rawhash>.jpg` 成为孤儿,LRU 淘汰)。
|
||||
- **`http→443` 潜性缺陷不修**(承 §33.3/§30.8 结论)。注意设备日志中确出现 `http://p1.music.126.net/...`
|
||||
明文封面 URL(非全 https),本批未改明文路径(语义错误但 443 上 TLS 可用);若曲库明文 http 图变多,
|
||||
另立批加明文 80 路径。
|
||||
- **真机封面加载成功率提升未实测**(本批未部署——留主会话上卡后播放几首观察 `cover:` 日志,预期 404/403/
|
||||
非图幂等快速跳下一张+歌单封面兜底,不再 8 次空转)。
|
||||
@@ -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)",
|
||||
|
||||
@@ -118,6 +118,7 @@ static int g_http_calls;
|
||||
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 unsigned long djb2(const char *s)
|
||||
@@ -135,6 +136,9 @@ int ncm_http_request(const ncm_http_req_t *req, char *buf, size_t buf_sz, ncm_ht
|
||||
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);
|
||||
/* 真实 cover.c 会做可解码图魔数判定(REPORT §34:200 但非 JPEG/PNG/GIF 判非图不重试),
|
||||
* 故 mock body 加 JPEG 魔数 FFD8FF,与 url_hash() 期望同规则。 */
|
||||
if (n >= 3) { buf[0]=(char)0xFF; buf[1]=(char)0xD8; buf[2]=(char)0xFF; }
|
||||
memset(resp, 0, sizeof *resp);
|
||||
resp->status = 200;
|
||||
resp->body = buf;
|
||||
@@ -157,7 +161,12 @@ int ncm_http_download(const ncm_http_req_t *req, ncm_http_write_cb cb, void *ctx
|
||||
size_t n = sizeof buf;
|
||||
unsigned long h = djb2(req->url);
|
||||
for (size_t i = 0; i < n; i++) buf[i] = (unsigned char)((h >> ((i % 8) * 8)) ^ (unsigned char)i);
|
||||
/* webp 注入:前 12 字节置 RIFF....WEBP 魔数(stb 不支持 → cover.c 判 webp=1 不解码) */
|
||||
/* JPEG 魔数(cover.c 可解码图判定通过);webp/json 注入优先覆盖各自魔数 */
|
||||
buf[0]=0xFF; buf[1]=0xD8; buf[2]=0xFF;
|
||||
if (g_json_next) {
|
||||
g_json_next = 0;
|
||||
buf[0]='{'; buf[1]='"'; buf[2]='c'; buf[3]='o'; buf[4]='d'; buf[5]='e';
|
||||
}
|
||||
if (g_webp_next) {
|
||||
g_webp_next = 0;
|
||||
buf[0]='R';buf[1]='I';buf[2]='F';buf[3]='F';
|
||||
@@ -193,6 +202,7 @@ static unsigned long url_hash(const char *url)
|
||||
unsigned char tmp[4096];
|
||||
unsigned long h = djb2(n);
|
||||
for (size_t i = 0; i < sizeof tmp; i++) tmp[i] = (unsigned char)((h >> ((i % 8) * 8)) ^ (unsigned char)i);
|
||||
tmp[0]=0xFF; tmp[1]=0xD8; tmp[2]=0xFF; /* 与 stub JPEG 魔数一致 */
|
||||
return fnv1a(tmp, sizeof tmp);
|
||||
}
|
||||
|
||||
@@ -242,6 +252,7 @@ static unsigned long curjpg_hash(void)
|
||||
#define URL_D "http://p1.music.126.net/albumD/www.jpg"
|
||||
#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 错误体注入用(未缓存) */
|
||||
|
||||
int main(void)
|
||||
{
|
||||
@@ -317,6 +328,15 @@ int main(void)
|
||||
CHECK(strstr(g_last_log, "403") != NULL, "log: '403' seen");
|
||||
g_status_next = 0;
|
||||
|
||||
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);
|
||||
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("== T7: null/empty + reset ==\n");
|
||||
int b7 = g_http_calls;
|
||||
cover_request(NULL);
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Reference in new issue
Block a user