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:
1 parent
9d93f1b1ef
commit
2bfe1ed796
8 files changed
+261
-20
No files matched your search
@@ -3106,3 +3106,84 @@ cb8187868f71a9fa155075d0624711d6 ncm # mipsel 静态 App(text 101711
|
|||||||
另立批加明文 80 路径。
|
另立批加明文 80 路径。
|
||||||
- **真机封面加载成功率提升未实测**(本批未部署——留主会话上卡后播放几首观察 `cover:` 日志,预期 404/403/
|
- **真机封面加载成功率提升未实测**(本批未部署——留主会话上卡后播放几首观察 `cover:` 日志,预期 404/403/
|
||||||
非图幂等快速跳下一张+歌单封面兜底,不再 8 次空转)。
|
非图幂等快速跳下一张+歌单封面兜底,不再 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` 的主动重取。
|
||||||
@@ -45,6 +45,10 @@ static pthread_mutex_t g_cm = PTHREAD_MUTEX_INITIALIZER;
|
|||||||
static meow_img *g_cover_img;
|
static meow_img *g_cover_img;
|
||||||
static char g_cover_url[512]; /* g_cover_img 对应的 URL */
|
static char g_cover_url[512]; /* g_cover_img 对应的 URL */
|
||||||
static char g_tgt_url[512]; /* 目标 URL(worker 拉取) */
|
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 pthread_t g_thr;
|
||||||
static int g_thr_on;
|
static int g_thr_on;
|
||||||
static int g_quit;
|
static int g_quit;
|
||||||
@@ -52,6 +56,15 @@ static volatile int g_gen; /* 每次换入 +1 */
|
|||||||
static int g_acked; /* UI 已消费的版本 */
|
static int g_acked; /* UI 已消费的版本 */
|
||||||
static char g_cover_path[512]; /* 封面原始文件路径(供 launcher) */
|
static char g_cover_path[512]; /* 封面原始文件路径(供 launcher) */
|
||||||
|
|
||||||
|
/* 死链兜底链(REPORT §NEW 任务1):原 picUrl 确定性失败(404/403/200+非图/webp)时,自动调
|
||||||
|
* web 端 `GET music.163.com/api/song/detail/?id=<id>&ids=[<id>]`(免登录,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 名即可命中)。
|
/* 封面缓存键:URL 的 djb2 哈希(稳定性由 URL 决定——同一专辑封面 URL 名即可命中)。
|
||||||
* 走 ncm_cache(LRU 口径沿用 ncm_cache,见 REPORT §14.2):键进 ncm_cache_path => 文件
|
* 走 ncm_cache(LRU 口径沿用 ncm_cache,见 REPORT §14.2):键进 ncm_cache_path => 文件
|
||||||
* `song_cov<hex>.jpg`,与 mp3(song_<id>.mp3)同池,lookup/commit 刷新 mtime(LRU)。 */
|
* `song_cov<hex>.jpg`,与 mp3(song_<id>.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;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Web 端 song/detail 免登录重取专辑封面 picUrl(REPORT §NEW 任务1):
|
||||||
|
* `GET https://music.163.com/api/song/detail/?id=<id>&ids=[<id>]`,取 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=[<id>] 的 '[ ]' 需 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。
|
/* 下载 url 到 cache 写者 w(流式,写进其临时文件),成功后从临时文件解码并返回 img。
|
||||||
* 失败返回 NULL(不 commit,调用方 abort)。*out_len=body 字节数;*out_rc 与 *out_status
|
* 失败返回 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);
|
||||||
@@ -246,8 +329,11 @@ static void *cover_worker(void *arg)
|
|||||||
usleep(250 * 1000);
|
usleep(250 * 1000);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
char song[24];
|
||||||
char url[512];
|
char url[512];
|
||||||
ncm_strlcpy(url, g_tgt_url, sizeof url);
|
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);
|
pthread_mutex_unlock(&g_cm);
|
||||||
|
|
||||||
char key[32];
|
char key[32];
|
||||||
@@ -296,9 +382,10 @@ static void *cover_worker(void *arg)
|
|||||||
* - 仅瞬时错误(超时/网络/TLS/5xx)指数退避重试。
|
* - 仅瞬时错误(超时/网络/TLS/5xx)指数退避重试。
|
||||||
* 日志一律打 rc/status(排查资产:先前只见 "fetch failed" 不知 HTTP 层返了什么);
|
* 日志一律打 rc/status(排查资产:先前只见 "fetch failed" 不知 HTTP 层返了什么);
|
||||||
* 403 单独日志(防盗链特征,与 404 死链区分——三者修法不同)。 */
|
* 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)",
|
meow_log(MEOW_LOG_WARN, "cover: webp unsupported url=%.64s rc=%d status=%d (no decode)",
|
||||||
url, drc, dstatus);
|
url, drc, dstatus);
|
||||||
|
if (song[0] && !fb_done && cover_try_fallback(url, song)) { backoff_ms = 0; fails = 0; continue; }
|
||||||
pthread_mutex_lock(&g_cm);
|
pthread_mutex_lock(&g_cm);
|
||||||
if (strcmp(g_tgt_url, url) == 0) g_tgt_url[0] = 0; /* 立即跳下一张(走兜底) */
|
if (strcmp(g_tgt_url, url) == 0) g_tgt_url[0] = 0; /* 立即跳下一张(走兜底) */
|
||||||
pthread_mutex_unlock(&g_cm);
|
pthread_mutex_unlock(&g_cm);
|
||||||
@@ -307,9 +394,10 @@ static void *cover_worker(void *arg)
|
|||||||
}
|
}
|
||||||
if (dkind == 2) { /* 200 但 body 非可解码图(源站死对象回 JSON 错误体/未知格式):
|
if (dkind == 2) { /* 200 但 body 非可解码图(源站死对象回 JSON 错误体/未知格式):
|
||||||
* 真机矩阵(REPORT §34.1)确认为「200 解码失败」主因,老逻辑空转 8 次;
|
* 真机矩阵(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)",
|
meow_log(MEOW_LOG_WARN, "cover: not an image (origin error body/200) url=%.64s rc=%d status=%d (no retry)",
|
||||||
url, drc, dstatus);
|
url, drc, dstatus);
|
||||||
|
if (song[0] && !fb_done && cover_try_fallback(url, song)) { backoff_ms = 0; fails = 0; continue; }
|
||||||
pthread_mutex_lock(&g_cm);
|
pthread_mutex_lock(&g_cm);
|
||||||
if (strcmp(g_tgt_url, url) == 0) g_tgt_url[0] = 0;
|
if (strcmp(g_tgt_url, url) == 0) g_tgt_url[0] = 0;
|
||||||
pthread_mutex_unlock(&g_cm);
|
pthread_mutex_unlock(&g_cm);
|
||||||
@@ -328,6 +416,7 @@ static void *cover_worker(void *arg)
|
|||||||
url, drc, dstatus);
|
url, drc, dstatus);
|
||||||
}
|
}
|
||||||
if (deterministic) {
|
if (deterministic) {
|
||||||
|
if (song[0] && !fb_done && cover_try_fallback(url, song)) { backoff_ms = 0; fails = 0; continue; }
|
||||||
pthread_mutex_lock(&g_cm);
|
pthread_mutex_lock(&g_cm);
|
||||||
if (strcmp(g_tgt_url, url) == 0) g_tgt_url[0] = 0; /* 立即跳下一张 */
|
if (strcmp(g_tgt_url, url) == 0) g_tgt_url[0] = 0; /* 立即跳下一张 */
|
||||||
pthread_mutex_unlock(&g_cm);
|
pthread_mutex_unlock(&g_cm);
|
||||||
@@ -370,10 +459,11 @@ static void *cover_worker(void *arg)
|
|||||||
return NULL;
|
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),使
|
* 先 cover_norm_url 规范化(追加 ?param=300y300 缩略后缀,REPORT §NEW 任务2),使
|
||||||
* g_tgt_url/g_cover_url/缓存键/下载 URL 走同一参数版本(小图省内存、命中高)。 */
|
* 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 (!url || !url[0]) return;
|
||||||
if (g_host_mock) return; /* 宿主 mock = 0 网络纪律(mock URL 不得发真实 HTTPS) */
|
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) ||
|
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; }
|
(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_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);
|
pthread_mutex_unlock(&g_cm);
|
||||||
if (!g_thr_on) {
|
if (!g_thr_on) {
|
||||||
g_thr_on = 1;
|
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; }
|
if (g_cover_img) { meow_img_free(g_cover_img); g_cover_img = NULL; }
|
||||||
g_cover_url[0] = 0;
|
g_cover_url[0] = 0;
|
||||||
g_tgt_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;
|
g_cover_path[0] = 0;
|
||||||
pthread_mutex_unlock(&g_cm);
|
pthread_mutex_unlock(&g_cm);
|
||||||
g_gen++;
|
g_gen++;
|
||||||
|
|||||||
@@ -451,7 +451,7 @@ int lyric_state(void); /* 0=空闲/未拉 1=加载中 2=已拉完(可能
|
|||||||
long lyric_line_time(int i);/* 第 i 行时间戳(ms;越界 -1)。播放页点行跳转(§22) */
|
long lyric_line_time(int i);/* 第 i 行时间戳(ms;越界 -1)。播放页点行跳转(§22) */
|
||||||
|
|
||||||
/* cover */
|
/* 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) */
|
meow_img *cover_get(void); /* 当前封面(**持锁返回**;NULL=无;用后必须 cover_put) */
|
||||||
void cover_put(void); /* 配对 cover_get 释放锁(§18 修绘制期 UAF) */
|
void cover_put(void); /* 配对 cover_get 释放锁(§18 修绘制期 UAF) */
|
||||||
int cover_changed(void); /* 1=有新封面待绘制(UI tick 判变) */
|
int cover_changed(void); /* 1=有新封面待绘制(UI tick 判变) */
|
||||||
|
|||||||
@@ -538,7 +538,7 @@ void player_play_song(int idx)
|
|||||||
? s.al_pic : g_st.cover_url;
|
? s.al_pic : g_st.cover_url;
|
||||||
player_manager_play(idx);
|
player_manager_play(idx);
|
||||||
appstate_set_playing_song(idx, NULL);
|
appstate_set_playing_song(idx, NULL);
|
||||||
cover_request(cover);
|
cover_request(cover, s.id); /* 曲目专辑封面:带 id,死链时 song/detail 兜底(REPORT §NEW 任务1) */
|
||||||
g_ppage = 0; /* 新曲 → 回封面页 */
|
g_ppage = 0; /* 新曲 → 回封面页 */
|
||||||
meow_view_page_push(ACT_PLAYER);
|
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;
|
const char *cover = s.al_pic[0] ? s.al_pic : g_st.cover_url;
|
||||||
player_manager_play(i);
|
player_manager_play(i);
|
||||||
appstate_set_playing_song(i, NULL);
|
appstate_set_playing_song(i, NULL);
|
||||||
cover_request(cover);
|
cover_request(cover, s.id);
|
||||||
g_ppage = 0;
|
g_ppage = 0;
|
||||||
meow_view_page_push(ACT_PLAYER);
|
meow_view_page_push(ACT_PLAYER);
|
||||||
}
|
}
|
||||||
@@ -585,7 +585,7 @@ void player_tick(uint64_t now)
|
|||||||
ncm_song_t s2;
|
ncm_song_t s2;
|
||||||
if (sc >= 0 && cur_song_meta(&s2) == 0 && strcmp(s2.id, g_last_meta_id) != 0) {
|
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);
|
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_static_invalidate();
|
||||||
player_dyn_invalidate();
|
player_dyn_invalidate();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -548,7 +548,7 @@ static void pl_page_enter(meow_view *v, void *ctx)
|
|||||||
* (旧版只在 playlist_open 里 cover_cache_reset 清掉,无人再请求 → 头块永远占位 ♪)。 */
|
* (旧版只在 playlist_open 里 cover_cache_reset 清掉,无人再请求 → 头块永远占位 ♪)。 */
|
||||||
const ncm_playlist_t *pcur = pl_cur();
|
const ncm_playlist_t *pcur = pl_cur();
|
||||||
if (pcur && pcur->cover[0])
|
if (pcur && pcur->cover[0])
|
||||||
cover_request(pcur->cover);
|
cover_request(pcur->cover, NULL); /* 歌单封面:无曲目 id,不走 song/detail 兜底 */
|
||||||
playlist_refresh();
|
playlist_refresh();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -49,6 +49,16 @@ size_t ncm_strlcpy(char *dst, const char *src, size_t sz)
|
|||||||
while (src[n]) n++;
|
while (src[n]) n++;
|
||||||
return 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 int g_log_warns;
|
||||||
static char g_last_log[512]; /* 最近一条日志文本(webp/403 分诊日志断言用) */
|
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 可对照旧代码(新代码强符号覆盖)。 */
|
/* §18 前的 cover.c 无 cover_put:weak 空实现让同一 sim 可对照旧代码(新代码强符号覆盖)。 */
|
||||||
__attribute__((weak)) void cover_put(void) { }
|
__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_http_calls;
|
||||||
|
static int g_sd_calls; /* song/detail 请求计数(死链兜底专用,REPORT §NEW 任务1) */
|
||||||
static int g_fail_next; /* 剩余注入失败次数 */
|
static int g_fail_next; /* 剩余注入失败次数 */
|
||||||
static int g_delay_ms; /* 模拟慢网络 */
|
static int g_delay_ms; /* 模拟慢网络 */
|
||||||
static int g_webp_next; /* 下一次下载返回 WebP 魔数体(200,测「200 解码失败」三分型③) */
|
static int g_webp_next; /* 下一次下载返回 WebP 魔数体(200,测「200 解码失败」三分型③) */
|
||||||
static int g_json_next; /* 下一次下载返回 JSON 错误体(200,实测「200 解码失败」真凶=死对象回 JSON) */
|
static int g_json_next; /* 下一次下载返回 JSON 错误体(200,实测「200 解码失败」真凶=死对象回 JSON) */
|
||||||
static int g_status_next; /* 下一次下载强制返回该 HTTP status(测 403 防盗链;0=200) */
|
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)
|
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++;
|
g_http_calls++;
|
||||||
if (g_fail_next > 0) { g_fail_next--; return NCM_ERR_IO; }
|
if (g_fail_next > 0) { g_fail_next--; return NCM_ERR_IO; }
|
||||||
if (g_delay_ms > 0) usleep((useconds_t)g_delay_ms * 1000);
|
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;
|
size_t n = 4096 < buf_sz ? 4096 : buf_sz;
|
||||||
unsigned long h = djb2(req->url);
|
unsigned long h = djb2(req->url);
|
||||||
for (size_t i = 0; i < n; i++) buf[i] = (char)((h >> ((i % 8) * 8)) ^ (unsigned char)i);
|
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;
|
(void)max_bytes;
|
||||||
g_http_calls++;
|
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_fail_next > 0) { g_fail_next--; return NCM_ERR_IO; }
|
||||||
if (g_delay_ms > 0) usleep((useconds_t)g_delay_ms * 1000);
|
if (g_delay_ms > 0) usleep((useconds_t)g_delay_ms * 1000);
|
||||||
unsigned char buf[4096];
|
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_E "http://p1.music.126.net/albumE/vvv.jpg" /* webp 注入用(未缓存) */
|
||||||
#define URL_F "http://p1.music.126.net/albumF/uuu.jpg" /* 403 注入用(未缓存) */
|
#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_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)
|
int main(void)
|
||||||
{
|
{
|
||||||
@@ -262,7 +301,7 @@ int main(void)
|
|||||||
ncm_cache_init(&g_st.ctx.cache, g_st.cache_dir);
|
ncm_cache_init(&g_st.ctx.cache, g_st.cache_dir);
|
||||||
|
|
||||||
printf("== T1: first request -> cover appears ==\n");
|
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(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(g_http_calls == 1, "http calls=%d (expect 1)", g_http_calls);
|
||||||
CHECK(curjpg_hash() == url_hash(URL_A), "cover_current.jpg == A");
|
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);
|
CHECK(g_http_calls == 1, "http calls=%d (still 1)", g_http_calls);
|
||||||
|
|
||||||
printf("== T3: switch song -> cover switches + cover_current.jpg updated ==\n");
|
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(wait_cover_hash(url_hash(URL_B), 3000), "cover switched to B");
|
||||||
CHECK(curjpg_hash() == url_hash(URL_B), "cover_current.jpg == 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);
|
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");
|
CHECK(!cover_changed(), "no churn after switch");
|
||||||
|
|
||||||
printf("== T4: re-request same url -> dedup ==\n");
|
printf("== T4: re-request same url -> dedup ==\n");
|
||||||
cover_request(URL_B);
|
cover_request(URL_B, NULL);
|
||||||
usleep(500 * 1000);
|
usleep(500 * 1000);
|
||||||
CHECK(g_http_calls == 2, "http calls=%d (still 2, dedup)", g_http_calls);
|
CHECK(g_http_calls == 2, "http calls=%d (still 2, dedup)", g_http_calls);
|
||||||
|
|
||||||
printf("== T5: flaky network x2 -> backoff retry succeeds ==\n");
|
printf("== T5: flaky network x2 -> backoff retry succeeds ==\n");
|
||||||
g_fail_next = 2;
|
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(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_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);
|
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);
|
printf("== T6: permanent failure -> give up after %d tries ==\n", 8);
|
||||||
g_fail_next = 100;
|
g_fail_next = 100;
|
||||||
int before = g_http_calls;
|
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),分段等 */
|
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);
|
for (int t = 0; t < 30 && g_http_calls - before < 8; t++) usleep(1000 * 1000);
|
||||||
int tries = g_http_calls - before;
|
int tries = g_http_calls - before;
|
||||||
@@ -312,7 +351,7 @@ int main(void)
|
|||||||
g_fail_next = 0;
|
g_fail_next = 0;
|
||||||
int b8 = g_http_calls;
|
int b8 = g_http_calls;
|
||||||
g_webp_next = 1;
|
g_webp_next = 1;
|
||||||
cover_request(URL_E);
|
cover_request(URL_E, NULL);
|
||||||
usleep(800 * 1000); /* 等 worker 处理 webp 体 → 判 webp → 清目标 → 转怠速 */
|
usleep(800 * 1000); /* 等 worker 处理 webp 体 → 判 webp → 清目标 → 转怠速 */
|
||||||
CHECK(g_http_calls == b8 + 1, "webp: single download=%d (no retry)", g_http_calls - b8);
|
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 保留(兜底,不误换)");
|
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");
|
printf("== T9: HTTP 403 anti-hotlink -> single download, no retry, distinct log ==\n");
|
||||||
int b9 = g_http_calls;
|
int b9 = g_http_calls;
|
||||||
g_status_next = 403;
|
g_status_next = 403;
|
||||||
cover_request(URL_F);
|
cover_request(URL_F, NULL);
|
||||||
usleep(800 * 1000);
|
usleep(800 * 1000);
|
||||||
CHECK(g_http_calls == b9 + 1, "403: single download=%d (no retry)", g_http_calls - b9);
|
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 保留(兜底)");
|
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");
|
printf("== T10: 200+JSON error body (dead origin) -> not an image, no retry ==\n");
|
||||||
int b10 = g_http_calls;
|
int b10 = g_http_calls;
|
||||||
g_json_next = 1;
|
g_json_next = 1;
|
||||||
cover_request(URL_G);
|
cover_request(URL_G, NULL);
|
||||||
usleep(800 * 1000);
|
usleep(800 * 1000);
|
||||||
CHECK(g_http_calls == b10 + 1, "json: single download=%d (no retry)", g_http_calls - b10);
|
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(cur_cover_hash() == url_hash(URL_C), "json: 旧封面 C 保留(兜底)");
|
||||||
CHECK(strstr(g_last_log, "not an image") != NULL, "log: 'not an image' seen");
|
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");
|
printf("== T7: null/empty + reset ==\n");
|
||||||
int b7 = g_http_calls;
|
int b7 = g_http_calls;
|
||||||
cover_request(NULL);
|
cover_request(NULL, NULL);
|
||||||
cover_request("");
|
cover_request("", NULL);
|
||||||
CHECK(g_http_calls == b7, "null/empty ignored (calls=%d)", g_http_calls);
|
CHECK(g_http_calls == b7, "null/empty ignored (calls=%d)", g_http_calls);
|
||||||
cover_cache_reset();
|
cover_cache_reset();
|
||||||
CHECK(cur_cover_hash() == 0, "reset -> cover cleared");
|
CHECK(cur_cover_hash() == 0, "reset -> cover cleared");
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
Reference in new issue
Block a user