feat(ncm): 风控加固——接口响应短 TTL 缓存 + 游客 MUSIC_A + track/all 死代码
三(按 NCMAPI_NOTES §2.1/§4/§6):
① 接口级响应缓存(对齐参考 apicache 2 分钟,防风控高频 ip 错误):
- 新增 core/ncm_apicache.{c,h}——内存 4 槽 LRU(单槽≤128KB,TTL 120s,
键=md5(path+\x01+json))+ 可选盘缓存 <data_dir>/ncm_api_cache/.json
(8B 墙钟头+body,超 TTL miss 并删,30s 节流清过期)。自包含模块
(仅 libc+pthread+clock_gettime),可独立链接无符号冲突。
- ncm_api.c:api_post 拆「请求+解析」两段,新增 api_post_cached;四个只读
接口接线:user_playlist/playlist_detail/song_detail/cloudsearch。
song_url/lyric/vip 不入缓存。
- 手动刷新绕过:ncm_api_cache_refresh_path(path) 一次性标记;home/歌单
topbar ↻ 接入;playlist_open/search 不标记(走缓存)。
- 盘层挂载 appstate_init;登出 appstate_logout 全清(防串账号)。
② 游客 MUSIC_A:ncm_req.c weapi+eapi 无 MUSIC_U 且非 login 路径补
MUSIC_A=anonymous_token(对齐参考 cookie 伪装)。
③ track/all 死代码清理:移除 ncm_api_playlist_track_all(真实服务器 8 形态全
404,无调用点);ncm_api_parse_track_all 保留(离线重放/单测),注释标注
「已验证不存在,勿用于真实请求」。
验证:新 bench/apicache_sim.c(0 网络)①TTL 内不重复请求/②过期重拉/③手动
刷新绕过/④cloudsearch 按键区分/⑤清空重拉/⑥盘缓存写入+clear 全 PASS;stream/
player_flow/window/search/cache_evict(1-9)/pagination/playlist_race sim 全
PASS;test_ncm_core host+mipsel(qemu) PASS;build_ncm.sh -Wall -Wextra 0
warning(text 1026914/data 1664/bss 1363460);0 高频真实请求。新 ncm md5
a69c93538920eba9d42a2ebb42f8fdca(留主会话上卡)。
This commit is contained in:
1 parent
9d5301f931
commit
ea645ce2a6
17 files changed
+717
-74
No files matched your search
@@ -3187,3 +3187,64 @@ d102c64f7fab73e981a789c5960e6bd9 ncm_host # 宿主调试二进
|
|||||||
防止同 incident 二次兜底(新图也死时不再回 song/detail)。
|
防止同 incident 二次兜底(新图也死时不再回 song/detail)。
|
||||||
- **歌单封面兜底语义未变**:兜底链两级都失败后仍清目标、保留当前显示封面(歌单封面在进歌单时已由
|
- **歌单封面兜底语义未变**:兜底链两级都失败后仍清目标、保留当前显示封面(歌单封面在进歌单时已由
|
||||||
`cover_request(pcur->cover, NULL)` 取到)。未新增对 `g_st.cover_url` 的主动重取。
|
`cover_request(pcur->cover, NULL)` 取到)。未新增对 `g_st.cover_url` 的主动重取。
|
||||||
|
|
||||||
|
## 36. 风控加固批:接口级响应短 TTL 缓存 + 游客 MUSIC_A + track/all 死代码(2026-09-02)
|
||||||
|
|
||||||
|
> 依据:NCMAPI_NOTES(319 权威接口地图)§2.1/§4/§6。仅改 `patch_v2/ncm_app/`。
|
||||||
|
> 三项:①位只读接口响应缓存(对齐参考 apicache 2 分钟);②游客 `MUSIC_A=anonymous_token`;③清理死代码。
|
||||||
|
|
||||||
|
### 36.1 ① 接口级响应短 TTL 缓存(user_playlist / playlist_detail / song_detail / cloudsearch)
|
||||||
|
|
||||||
|
- 新增 **`core/ncm_apicache.{c,h}`**(自包含:仅 libc+pthread+clock_gettime,不依赖 ncm_store/ncm_crypto/ncm_common 函数,可独立链接)。
|
||||||
|
- **内存**:4 槽 LRU,单槽 ≤128KB,TTL 默认 120s;键 = md5(`path` + `\x01` + 参数 json)。超 TTL 过期、超容量逐 LRU 最旧。
|
||||||
|
- **盘(可选)**:`<data_dir>/ncm_api_cache/<key>.json`,文件头 8B 墙钟毫秒 + body;超 TTL 视为 miss(并删除);写入按 30s 节流清过期文件,磁盘占用有界。
|
||||||
|
- 失效:登出/换账号 `ncm_apicache_clear` 全清;盘超 TTL 自然过期。
|
||||||
|
- **`core/ncm_api.c`**:`api_post` 拆为「请求(ncm_http)+解析」两段;新增 `api_post_cached`(先查缓存、miss/过期/手动刷新→真网络、成功后写缓存)。四个只读接口路由到 `api_post_cached`:`user_playlist`(weapi) / `playlist_detail`(eapi,v6) / `song_detail`(eapi,v3) / `cloudsearch`(weapi)。`song_url`/`lyric`/`vip` **不入缓存**(逐曲/需新鲜/低频)。
|
||||||
|
- **手动刷新绕过**:`ncm_api_cache_refresh_path(path)` 标记某路径下一次调用绕过缓存(一次性消费)。接入 home topbar ↻(`/api/user/playlist`) 与歌单 topbar ↻(`/api/v6/playlist/detail`);`playlist_open`/`search` 不标记(走缓存)。
|
||||||
|
- 盘层挂载:`appstate_init` → `ncm_apicache_init(g_st.data_dir)`;`appstate_logout` → `ncm_apicache_clear()`。
|
||||||
|
|
||||||
|
### 36.2 ② 游客 MUSIC_A(未登录 cookie 伪装,NCMAPI_NOTES §4)
|
||||||
|
|
||||||
|
- `core/ncm_req.c`:weapi 分支(无 `MUSIC_U` 且非 login 路径)补 `MUSIC_A=anonymous_token` 进会话 cookie;eapi 分支同样(无 `MUSIC_U` 且非 login 路径)追加到 eapi Cookie 头。与参考「常驻 cookie 伪装」对齐,降低未登录需登录接口被服务端判定异常(401/400 需验证)概率。
|
||||||
|
|
||||||
|
### 36.3 ③ track/all 死代码清理(NCMAPI_NOTES §1.1 实锤 404)
|
||||||
|
|
||||||
|
- `core/ncm_api.c`/`.h`:**移除**网络封装 `ncm_api_playlist_track_all`(真实服务器无 `/api/playlist/track/all`,8 形态全 404;无任何调用点)。
|
||||||
|
- `ncm_api_parse_track_all`(离线解析器)**保留**供 `offline_track_all`/`probe_track_all`/`test_ncm_core` 离线重放/单测(0 请求),注释标注「已验证端点不存在,仅离线单测用,勿用于真实请求」。
|
||||||
|
|
||||||
|
### 36.4 验证(宿主 + qemu;新缓存用例 0 真实网络请求)
|
||||||
|
|
||||||
|
| 项 | 结果 |
|
||||||
|
|---|---|
|
||||||
|
| 新增 `bench/apicache_sim.c`(真实 core + ncm_http 计数桩,0 网络) | **ALL PASS**:TTL 内不重复请求 / 过期重拉 / 手动刷新绕过 / cloudsearch 按键区分 / 清空重拉 / 盘缓存写入+clear |
|
||||||
|
| `stream_sim` / `player_flow_sim` / `window_sim` / `search_sim` | ALL PASS |
|
||||||
|
| `cache_evict_sim`(1-9)/ `pagination_sim` / `playlist_race_sim` | PASS |
|
||||||
|
| `test_ncm_core_host` + `qemu test_ncm_core_mipsel` | host + mipsel **PASS** |
|
||||||
|
| `build_ncm.sh`(-Wall -Wextra) | **0 warning**;`text 1026914 / data 1664 / bss 1363460` |
|
||||||
|
| `build_ncm_host.sh` | 0 warning(仅 SDK `meow_ui_vol.c` 存量 `system()` 告警,非本批) |
|
||||||
|
| UI 渲染 `NCM_PAGE=home MEOW_UI_DUMP` | exit=0,dump 正常 |
|
||||||
|
|
||||||
|
改动文件(本批,仅这些;未动 sdk/、devdocs/、其它 App):
|
||||||
|
```
|
||||||
|
修改:
|
||||||
|
core/ncm_api.c # api_post 拆分 + api_post_cached + 四接口接线 + 刷新路径 + 移除 track_all 网络版 + 解析器注释
|
||||||
|
core/ncm_api.h # 移除 ncm_api_playlist_track_all 声明 + parse_track_all 注释 + ncm_api_cache_refresh_path
|
||||||
|
core/ncm_req.c # weapi/eapi 游客 MUSIC_A 注入
|
||||||
|
core/ncm.h # 伞头加 ncm_apicache.h
|
||||||
|
app/logic/appstate.c # init/logout 接 ncm_apicache
|
||||||
|
app/ui/home.c # 手动刷新标记 /api/user/playlist
|
||||||
|
app/ui/playlist.c # 手动刷新标记 /api/v6/playlist/detail
|
||||||
|
build_core.sh / build_ncm.sh / build_ncm_sims.sh # 加 ncm_apicache.c;window/search sim 链 ncm_apicache.o
|
||||||
|
ncm / ncm_host / test/test_ncm_core_host # 产物重链
|
||||||
|
新增:
|
||||||
|
core/ncm_apicache.c / core/ncm_apicache.h # 接口级响应短 TTL 缓存(内存+盘)
|
||||||
|
bench/apicache_sim.c # 缓存用例(0 网络)
|
||||||
|
REPORT.md # 本批记录(§36)
|
||||||
|
```
|
||||||
|
|
||||||
|
### 36.5 遗留 / 诚实出入
|
||||||
|
|
||||||
|
- **缓存 TTL=120s 的内存槽上限 4×128KB**:大歌单 `playlist_detail`(trackIds >128KB)不入内存、仅盘缓存(内存瘦身口径 REPORT §32);命中读盘+重解析,避免把上千首 trackIds 常驻内存。
|
||||||
|
- **手刷只标详情接口**:歌单手动刷新只绕过 `v6/playlist/detail`(列表结构),`song_detail` 批 2 分钟内走缓存(改动歌曲为新键自然 miss,元数据 ≤2min 旧可接受);home 刷新绕过 `user/playlist`。
|
||||||
|
- **eapi 分支未注入 MUSIC_U**(登录态 eapi 播放接口仍走原有的非-cookie 鉴权路径)——本次仅按任务补游客 MUSIC_A;MUSIC_U 头注入留后续(超范围)。
|
||||||
|
- **盘缓存读路径未在真机/宿主整链实测**(内存命中优先,盘仅在内存 miss/大体时兜底)——代码审查 + 文件存在性断言验证;真机可观察 `ncm_api_cache/` 落盘与热更后命中。
|
||||||
@@ -87,6 +87,10 @@ int appstate_init(void)
|
|||||||
else
|
else
|
||||||
ncm_ctx_default_cache_dir(g_st.data_dir, g_st.cache_dir, sizeof g_st.cache_dir);
|
ncm_ctx_default_cache_dir(g_st.data_dir, g_st.cache_dir, sizeof g_st.cache_dir);
|
||||||
|
|
||||||
|
/* 风控加固 §1:接口级 JSON 响应短 TTL 缓存的盘层挂到 <data_dir>/ncm_api_cache/。
|
||||||
|
* (内存层由 core 初始化默认启用;盘层跨进程/热更保留,启动清一次过期盘文件。) */
|
||||||
|
ncm_apicache_init(g_st.data_dir);
|
||||||
|
|
||||||
appstate_load_settings();
|
appstate_load_settings();
|
||||||
g_st.logged_in = (g_st.ctx.cookie[0] != 0);
|
g_st.logged_in = (g_st.ctx.cookie[0] != 0);
|
||||||
g_st.active_playlist = -1;
|
g_st.active_playlist = -1;
|
||||||
@@ -156,6 +160,9 @@ void appstate_set_logged_in(const char *uid, const char *nickname)
|
|||||||
void appstate_logout(void)
|
void appstate_logout(void)
|
||||||
{
|
{
|
||||||
ncm_ctx_clear_cookie(&g_st.ctx);
|
ncm_ctx_clear_cookie(&g_st.ctx);
|
||||||
|
/* 风控加固 §1:登出/换账号清接口级响应缓存(防上一账号缓存的 user/playlist/cloudsearch
|
||||||
|
* 被下一账号复用;盘缓存目录一并清)。 */
|
||||||
|
ncm_apicache_clear();
|
||||||
g_st.logged_in = 0;
|
g_st.logged_in = 0;
|
||||||
g_st.uid[0] = 0;
|
g_st.uid[0] = 0;
|
||||||
g_st.nickname[0] = 0;
|
g_st.nickname[0] = 0;
|
||||||
|
|||||||
@@ -257,7 +257,12 @@ int home_on_tap(int x, int y)
|
|||||||
if (g_tb) {
|
if (g_tb) {
|
||||||
int ah = meow_topappbar_hit(g_tb, x, y);
|
int ah = meow_topappbar_hit(g_tb, x, y);
|
||||||
if (ah == MEOW_TOPBAR_BACK) { home_drag_reset(); return 1; }
|
if (ah == MEOW_TOPBAR_BACK) { home_drag_reset(); return 1; }
|
||||||
if (ah == 0) { home_drag_reset(); home_reload(); return 1; } /* 刷新 */
|
if (ah == 0) { /* 刷新 */
|
||||||
|
home_drag_reset();
|
||||||
|
ncm_api_cache_refresh_path("/api/user/playlist"); /* 手动刷新绕过接口缓存 */
|
||||||
|
home_reload();
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
if (ah == 1) { home_drag_reset(); settings_open(); return 1; } /* 设置 */
|
if (ah == 1) { home_drag_reset(); settings_open(); return 1; } /* 设置 */
|
||||||
if (ah >= 2) { home_drag_reset(); search_open(); return 1; } /* 搜索(最右) */
|
if (ah >= 2) { home_drag_reset(); search_open(); return 1; } /* 搜索(最右) */
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -427,7 +427,12 @@ int playlist_on_tap(int x, int y)
|
|||||||
if (g_tb) {
|
if (g_tb) {
|
||||||
int ah = meow_topappbar_hit(g_tb, x, y);
|
int ah = meow_topappbar_hit(g_tb, x, y);
|
||||||
if (ah == MEOW_TOPBAR_BACK) { pl_drag_reset(); meow_view_page_pop(); return 1; }
|
if (ah == MEOW_TOPBAR_BACK) { pl_drag_reset(); meow_view_page_pop(); return 1; }
|
||||||
if (ah >= 0) { pl_drag_reset(); playlist_refresh(); return 1; }
|
if (ah >= 0) { /* topbar ↻ 手动刷新:绕过歌单详情接口缓存 */
|
||||||
|
pl_drag_reset();
|
||||||
|
ncm_api_cache_refresh_path("/api/v6/playlist/detail");
|
||||||
|
playlist_refresh();
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if (x >= r_plall.x && x < r_plall.x + r_plall.w &&
|
if (x >= r_plall.x && x < r_plall.x + r_plall.w &&
|
||||||
y >= r_plall.y && y < r_plall.y + r_plall.h) { pl_drag_reset(); player_play_song(0); return 1; }
|
y >= r_plall.y && y < r_plall.y + r_plall.h) { pl_drag_reset(); player_play_song(0); return 1; }
|
||||||
|
|||||||
@@ -0,0 +1,132 @@
|
|||||||
|
/* bench/apicache_sim.c — 接口级响应短 TTL 缓存验证(风控加固 §1,0 真实 NCM 请求)。
|
||||||
|
*
|
||||||
|
* 用**真实** core(ncm_api.c / ncm_req.c / ncm_crypto.c / ncm_json.c / ncm_common.c /
|
||||||
|
* ncm_apicache.c),仅把网络层 ncm_http_request 换成计数桩(canned JSON 响应),验证:
|
||||||
|
* ① TTL 内重复调用不重复请求(缓存命中);
|
||||||
|
* ② TTL 过期后重拉(expired re-fetch);
|
||||||
|
* ③ 手动刷新(ncm_api_cache_refresh_path)绕过缓存;
|
||||||
|
* ④ cloudsearch 缓存按键(关键词/分页)区分,不同关键词各拉一次;
|
||||||
|
* ⑤ 清空(登出)后重拉;
|
||||||
|
* ⑥ 盘缓存写入/清除(文件存在性)。
|
||||||
|
* 风控纪律:0 次真实 NCM 请求(网络层计数桩);sleep 仅用于 TTL 过期(短 TTL=1s)。
|
||||||
|
*
|
||||||
|
* 编译(宿主),在 ncm_app 目录:见 build_ncm_sims.sh 新增 apicache_sim 段。
|
||||||
|
* 运行:./apicache_sim
|
||||||
|
*/
|
||||||
|
#include "ncm.h"
|
||||||
|
#include "ncm_http.h"
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <string.h>
|
||||||
|
#include <stdlib.h>
|
||||||
|
#include <unistd.h>
|
||||||
|
#include <sys/stat.h>
|
||||||
|
|
||||||
|
static int g_http_calls = 0; /* ncm_http_request 桩计数 = 真实网络调用数 */
|
||||||
|
|
||||||
|
/* 桩:不联网,按 url 返回 canned JSON,并计数。 */
|
||||||
|
static const char *BODY_USER_PL =
|
||||||
|
"{\"code\":200,\"playlist\":[{\"id\":\"1\",\"name\":\"P1\",\"coverImgUrl\":\"http://x/c.jpg\",\"trackCount\":3,\"subscribed\":false}]}";
|
||||||
|
static const char *BODY_CLOUD_SEARCH =
|
||||||
|
"{\"code\":200,\"result\":{\"songCount\":1,\"songs\":[{\"id\":\"100\",\"name\":\"S\",\"dt\":200000,\"fee\":0,"
|
||||||
|
"\"ar\":[{\"name\":\"A1\"}],\"al\":{\"name\":\"AL1\",\"picUrl\":\"http://x/1.jpg\"}}]}}";
|
||||||
|
|
||||||
|
int ncm_http_request(const ncm_http_req_t *req, char *buf, size_t buf_sz, ncm_http_resp_t *resp) {
|
||||||
|
(void)req; (void)buf_sz;
|
||||||
|
g_http_calls++;
|
||||||
|
const char *body = (strstr(req->url, "cloudsearch") != NULL) ? BODY_CLOUD_SEARCH : BODY_USER_PL;
|
||||||
|
size_t n = strlen(body);
|
||||||
|
memcpy(buf, body, n);
|
||||||
|
memset(resp, 0, sizeof *resp);
|
||||||
|
resp->status = 200;
|
||||||
|
resp->body = buf;
|
||||||
|
resp->body_len = n;
|
||||||
|
resp->content_length = n;
|
||||||
|
return NCM_OK;
|
||||||
|
}
|
||||||
|
void ncm_http_set_verify_default(int verify, const char *ca_path) { (void)verify; (void)ca_path; }
|
||||||
|
|
||||||
|
static int g_fail = 0;
|
||||||
|
#define CHECK(cond, fmt, ...) do { \
|
||||||
|
if (cond) printf(" PASS " fmt "\n", ##__VA_ARGS__); \
|
||||||
|
else { printf(" FAIL " fmt "\n", ##__VA_ARGS__); g_fail = 1; } \
|
||||||
|
} while (0)
|
||||||
|
|
||||||
|
static const char *g_device = "AA11BB22CC33DD44EE55FF6600112233445566778899AABB00";
|
||||||
|
|
||||||
|
int main(void) {
|
||||||
|
ncm_apicache_init(NULL); /* 内存缓存;盘层本轮单独测(临时目录) */
|
||||||
|
ncm_apicache_ttl_set(NCM_APICACHE_TTL_S);
|
||||||
|
|
||||||
|
ncm_playlist_t pls[8]; int pn = 0;
|
||||||
|
|
||||||
|
/* ① TTL 内不重复请求(user/playlist) */
|
||||||
|
int c0 = g_http_calls;
|
||||||
|
int rc = ncm_api_user_playlist(g_device, "", "0", 30, 0, pls, 8, &pn);
|
||||||
|
CHECK(rc == NCM_OK && pn == 1, "user_playlist rc=%d n=%d (first = real)", rc, pn);
|
||||||
|
rc = ncm_api_user_playlist(g_device, "", "0", 30, 0, pls, 8, &pn);
|
||||||
|
CHECK(g_http_calls == c0 + 1, "TTL 内不重复请求: calls=%d (期望 %d)", g_http_calls, c0 + 1);
|
||||||
|
CHECK(rc == NCM_OK && pn == 1, "cached user_playlist rc=%d n=%d", rc, pn);
|
||||||
|
|
||||||
|
/* ② TTL 过期后重拉(短 TTL=1s;sleep 2s 确保严格超 1000ms 触发过期) */
|
||||||
|
ncm_apicache_ttl_set(1);
|
||||||
|
sleep(2); /* 等上一条(默认 120s 存的)过期 */
|
||||||
|
c0 = g_http_calls;
|
||||||
|
rc = ncm_api_user_playlist(g_device, "", "0", 30, 0, pls, 8, &pn);
|
||||||
|
CHECK(g_http_calls == c0 + 1, "过期重拉: calls=%d (期望 +1)", g_http_calls);
|
||||||
|
CHECK(rc == NCM_OK && pn == 1, "refetch after expiry rc=%d n=%d", rc, pn);
|
||||||
|
/* 刚存了 1s TTL 的新条目,立即再取应命中 */
|
||||||
|
int c1 = g_http_calls;
|
||||||
|
rc = ncm_api_user_playlist(g_device, "", "0", 30, 0, pls, 8, &pn);
|
||||||
|
CHECK(g_http_calls == c1, "重拉后 TTL 内命中: calls=%d (期望不增)", g_http_calls);
|
||||||
|
ncm_apicache_ttl_set(NCM_APICACHE_TTL_S); /* 还原默认 TTL */
|
||||||
|
|
||||||
|
/* ③ 手动刷新绕过缓存(一次性消费) */
|
||||||
|
c0 = g_http_calls;
|
||||||
|
ncm_api_cache_refresh_path("/api/user/playlist");
|
||||||
|
rc = ncm_api_user_playlist(g_device, "", "0", 30, 0, pls, 8, &pn);
|
||||||
|
CHECK(g_http_calls == c0 + 1, "手动刷新绕过: calls=%d (期望 +1)", g_http_calls);
|
||||||
|
CHECK(rc == NCM_OK && pn == 1, "refresh bypass rc=%d n=%d", rc, pn);
|
||||||
|
c1 = g_http_calls;
|
||||||
|
rc = ncm_api_user_playlist(g_device, "", "0", 30, 0, pls, 8, &pn);
|
||||||
|
CHECK(g_http_calls == c1, "刷新后(路径已消费)回缓存: calls=%d", g_http_calls);
|
||||||
|
|
||||||
|
/* ④ cloudsearch 缓存:同关键词命中、不同关键词各自请求 */
|
||||||
|
ncm_song_t songs[8]; int sn = 0; int64_t total = -1;
|
||||||
|
c0 = g_http_calls;
|
||||||
|
rc = ncm_api_cloudsearch_song(g_device, "", "abc", 0, 30, songs, 8, &sn, &total);
|
||||||
|
CHECK(rc == NCM_OK && sn == 1, "cloudsearch abc rc=%d n=%d", rc, sn);
|
||||||
|
int c2 = g_http_calls;
|
||||||
|
rc = ncm_api_cloudsearch_song(g_device, "", "abc", 0, 30, songs, 8, &sn, &total);
|
||||||
|
CHECK(g_http_calls == c2, "cloudsearch 同关键词命中: calls=%d", g_http_calls);
|
||||||
|
rc = ncm_api_cloudsearch_song(g_device, "", "def", 0, 30, songs, 8, &sn, &total);
|
||||||
|
CHECK(g_http_calls == c2 + 1, "cloudsearch 不同关键词各拉一次: calls=%d (期望 %d)", g_http_calls, c2 + 1);
|
||||||
|
CHECK(rc == NCM_OK && sn == 1, "cloudsearch def rc=%d n=%d", rc, sn);
|
||||||
|
|
||||||
|
/* ⑤ 清空(登出/换账号)后重拉 */
|
||||||
|
c0 = g_http_calls;
|
||||||
|
ncm_apicache_clear();
|
||||||
|
rc = ncm_api_user_playlist(g_device, "", "0", 30, 0, pls, 8, &pn);
|
||||||
|
CHECK(g_http_calls == c0 + 1, "清空后重拉: calls=%d (期望 +1)", g_http_calls);
|
||||||
|
|
||||||
|
/* ⑥ 盘缓存写入/清除(临时目录,默认 TTL) */
|
||||||
|
char tmpdir[128];
|
||||||
|
snprintf(tmpdir, sizeof tmpdir, "/tmp/apicache_sim_%ld", (long)getpid());
|
||||||
|
mkdir(tmpdir, 0777);
|
||||||
|
ncm_apicache_init(tmpdir);
|
||||||
|
ncm_apicache_ttl_set(NCM_APICACHE_TTL_S);
|
||||||
|
ncm_apicache_put("0123456789abcdef0123456789abcdef", "hello-disk", 10);
|
||||||
|
char dp[512];
|
||||||
|
snprintf(dp, sizeof dp, "%s/ncm_api_cache/0123456789abcdef0123456789abcdef.json", tmpdir);
|
||||||
|
struct stat st;
|
||||||
|
CHECK(stat(dp, &st) == 0, "盘缓存写入文件存在: %s", dp);
|
||||||
|
/* 命中盘缓存(内存若也存了会先命中内存;这里单独验证 clear 后文件被删) */
|
||||||
|
ncm_apicache_clear();
|
||||||
|
CHECK(stat(dp, &st) != 0, "盘缓存 clear 后文件删除");
|
||||||
|
/* 清掉临时目录(内容已由 clear 删除,rmdir) */
|
||||||
|
char cmd[160];
|
||||||
|
snprintf(cmd, sizeof cmd, "rm -rf \"%s\"", tmpdir);
|
||||||
|
if (system(cmd) != 0) { /* 忽略 */ }
|
||||||
|
|
||||||
|
printf("\n%s\n", g_fail ? "RESULT: FAIL" : "RESULT: PASS");
|
||||||
|
return g_fail ? 1 : 0;
|
||||||
|
}
|
||||||
@@ -7,7 +7,7 @@ INC_CORE="-Icore"
|
|||||||
INC_VENDOR="-I$VENDOR/mbedtls/include -I$VENDOR/jsmn -I$VENDOR/qrcodegen/c"
|
INC_VENDOR="-I$VENDOR/mbedtls/include -I$VENDOR/jsmn -I$VENDOR/qrcodegen/c"
|
||||||
CORE="core/ncm_common.c core/ncm_json.c core/ncm_store.c core/ncm_http.c \
|
CORE="core/ncm_common.c core/ncm_json.c core/ncm_store.c core/ncm_http.c \
|
||||||
core/ncm_crypto.c core/ncm_req.c core/ncm_login.c core/ncm_api.c \
|
core/ncm_crypto.c core/ncm_req.c core/ncm_login.c core/ncm_api.c \
|
||||||
core/ncm_cache.c core/ncm_ctx.c"
|
core/ncm_cache.c core/ncm_ctx.c core/ncm_apicache.c"
|
||||||
VENDOR_C="vendor/qrcodegen/c/qrcodegen.c" # qrcodegen 单独编译
|
VENDOR_C="vendor/qrcodegen/c/qrcodegen.c" # qrcodegen 单独编译
|
||||||
JSMN_IMPL="core/jsmn_impl.c" # jsmn 唯一实现 TU
|
JSMN_IMPL="core/jsmn_impl.c" # jsmn 唯一实现 TU
|
||||||
|
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ STRICT="-Wall -Wextra -Os -DJSMN_HEADER"
|
|||||||
|
|
||||||
CORE="core/ncm_common.c core/ncm_json.c core/ncm_store.c core/ncm_http.c \
|
CORE="core/ncm_common.c core/ncm_json.c core/ncm_store.c core/ncm_http.c \
|
||||||
core/ncm_crypto.c core/ncm_req.c core/ncm_login.c core/ncm_api.c \
|
core/ncm_crypto.c core/ncm_req.c core/ncm_login.c core/ncm_api.c \
|
||||||
core/ncm_cache.c core/ncm_ctx.c"
|
core/ncm_cache.c core/ncm_ctx.c core/ncm_apicache.c"
|
||||||
APP="app/app.c app/ui/login.c app/ui/home.c app/ui/playlist.c app/ui/player.c \
|
APP="app/app.c app/ui/login.c app/ui/home.c app/ui/playlist.c app/ui/player.c \
|
||||||
app/ui/settings.c app/ui/search.c app/ui/ncm_colors.c app/ui/chrome.c \
|
app/ui/settings.c app/ui/search.c app/ui/ncm_colors.c app/ui/chrome.c \
|
||||||
app/logic/appstate.c app/logic/lyric.c app/logic/songmeta.c \
|
app/logic/appstate.c app/logic/lyric.c app/logic/songmeta.c \
|
||||||
|
|||||||
@@ -59,7 +59,8 @@ gcc -O2 -ffunction-sections -DJSMN_HEADER -I. -I../sdk -I../meow_launcherd \
|
|||||||
-Icore -Ivendor/jsmn -Ivendor/mbedtls/include -c app/logic/appstate.c -o /tmp/ws_app.o
|
-Icore -Ivendor/jsmn -Ivendor/mbedtls/include -c app/logic/appstate.c -o /tmp/ws_app.o
|
||||||
gcc -O2 -ffunction-sections -DJSMN_HEADER -I. -I../sdk -I../meow_launcherd \
|
gcc -O2 -ffunction-sections -DJSMN_HEADER -I. -I../sdk -I../meow_launcherd \
|
||||||
-Icore -Ivendor/jsmn -Ivendor/mbedtls/include -c app/logic/songmeta.c -o /tmp/ws_meta.o
|
-Icore -Ivendor/jsmn -Ivendor/mbedtls/include -c app/logic/songmeta.c -o /tmp/ws_meta.o
|
||||||
gcc -O2 -Wl,--gc-sections -o window_sim /tmp/ws.o /tmp/ws_app.o /tmp/ws_meta.o -lpthread
|
gcc -O2 -Wl,--gc-sections -o window_sim /tmp/ws.o /tmp/ws_app.o /tmp/ws_meta.o \
|
||||||
|
out/core-host/ncm_apicache.o -lpthread # appstate.c 调 ncm_apicache_init/clear(自包含模块)
|
||||||
run_sim window_sim
|
run_sim window_sim
|
||||||
|
|
||||||
echo "==> build search_sim"
|
echo "==> build search_sim"
|
||||||
@@ -67,7 +68,8 @@ gcc -O2 -ffunction-sections -DJSMN_HEADER -I. -I../sdk -I../meow_launcherd -Icor
|
|||||||
-c bench/search_sim.c -o /tmp/sh.o
|
-c bench/search_sim.c -o /tmp/sh.o
|
||||||
gcc -O2 -ffunction-sections -DJSMN_HEADER -I. -I../sdk -I../meow_launcherd \
|
gcc -O2 -ffunction-sections -DJSMN_HEADER -I. -I../sdk -I../meow_launcherd \
|
||||||
-Icore -Ivendor/jsmn -c app/logic/appstate.c -o /tmp/sh_app.o
|
-Icore -Ivendor/jsmn -c app/logic/appstate.c -o /tmp/sh_app.o
|
||||||
gcc -O2 -Wl,--gc-sections -o search_sim /tmp/sh.o /tmp/sh_app.o -lpthread
|
gcc -O2 -Wl,--gc-sections -o search_sim /tmp/sh.o /tmp/sh_app.o \
|
||||||
|
out/core-host/ncm_apicache.o -lpthread # appstate.c 调 ncm_apicache_init/clear
|
||||||
run_sim search_sim
|
run_sim search_sim
|
||||||
|
|
||||||
echo "==> build cache_evict_sim"
|
echo "==> build cache_evict_sim"
|
||||||
@@ -76,4 +78,17 @@ gcc -O2 -DJSMN_HEADER -I. -Icore -Ivendor/mbedtls/include -c core/ncm_cache.c -o
|
|||||||
gcc -O2 -o cache_evict_sim /tmp/ce.o /tmp/ce_cache.o -lpthread
|
gcc -O2 -o cache_evict_sim /tmp/ce.o /tmp/ce_cache.o -lpthread
|
||||||
run_sim cache_evict_sim
|
run_sim cache_evict_sim
|
||||||
|
|
||||||
|
echo "==> build apicache_sim (风控加固 §1 接口级响应缓存;0 真实网络——ncm_http 计数桩)"
|
||||||
|
gcc -O2 -Wall -Wextra -DJSMN_HEADER -I. -Icore -I../sdk -I../meow_launcherd \
|
||||||
|
-Ivendor/jsmn -Ivendor/mbedtls/include \
|
||||||
|
-c bench/apicache_sim.c -o /tmp/ac.o
|
||||||
|
# 真实 core(api/req/crypto/json/common/apicache)+ 网络层计数桩(apicache_sim 已含 ncm_http_request 桩,
|
||||||
|
# 故不链 ncm_http.o)+ jsmn + 宿主 mbedtls;核心对象从 out/core-host/*.o 取(含 ncm_apicache.o)。
|
||||||
|
gcc -O2 -o apicache_sim /tmp/ac.o \
|
||||||
|
$(ls out/core-host/ncm_api.o out/core-host/ncm_req.o out/core-host/ncm_crypto.o \
|
||||||
|
out/core-host/ncm_json.o out/core-host/ncm_common.o out/core-host/ncm_apicache.o) \
|
||||||
|
out/core-host/jsmn_impl.o \
|
||||||
|
-Lout/mbedtls/host -lmbedtls -lmbedx509 -lmbedcrypto -lpthread -lm
|
||||||
|
run_sim apicache_sim
|
||||||
|
|
||||||
echo "===== all sims done ====="
|
echo "===== all sims done ====="
|
||||||
@@ -15,6 +15,7 @@
|
|||||||
#include "ncm_login.h"
|
#include "ncm_login.h"
|
||||||
#include "ncm_api.h"
|
#include "ncm_api.h"
|
||||||
#include "ncm_cache.h"
|
#include "ncm_cache.h"
|
||||||
|
#include "ncm_apicache.h"
|
||||||
|
|
||||||
#define NCM_CTX_CA_PATH_MAX 256
|
#define NCM_CTX_CA_PATH_MAX 256
|
||||||
|
|
||||||
|
|||||||
+102
-57
@@ -1,5 +1,6 @@
|
|||||||
/* ncm_api.c — 歌单/播放数据接口实现(响应/token 走堆,避免大缓冲驻栈) */
|
/* ncm_api.c — 歌单/播放数据接口实现(响应/token 走堆,避免大缓冲驻栈) */
|
||||||
#include "ncm_api.h"
|
#include "ncm_api.h"
|
||||||
|
#include "ncm_apicache.h"
|
||||||
#include "ncm_common.h"
|
#include "ncm_common.h"
|
||||||
#include "ncm_http.h"
|
#include "ncm_http.h"
|
||||||
#include "ncm_json.h"
|
#include "ncm_json.h"
|
||||||
@@ -11,7 +12,7 @@
|
|||||||
|
|
||||||
/* 响应缓冲上界(内存瘦身,OOM 修复):多数接口响应远小于 1MB,按需给尺寸避免每次调用
|
/* 响应缓冲上界(内存瘦身,OOM 修复):多数接口响应远小于 1MB,按需给尺寸避免每次调用
|
||||||
* 都 malloc 1MB 堆峰值(播放中 song_url/lyric/song_detail 并发,几 × 1MB 是可砍的瞬态)。
|
* 都 malloc 1MB 堆峰值(播放中 song_url/lyric/song_detail 并发,几 × 1MB 是可砍的瞬态)。
|
||||||
* 仅 playlist detail / track_all(全量 trackIds,最多 NCM_TRACKID_MAX 条)保留传统 1MB。 */
|
* 仅 playlist detail(全量 trackIds,最多 NCM_TRACKID_MAX 条)保留传统 2MB。 */
|
||||||
#define RESP_SMALL (64 * 1024) /* song_url:url+字段,实际 ~1KB */
|
#define RESP_SMALL (64 * 1024) /* song_url:url+字段,实际 ~1KB */
|
||||||
#define RESP_MED (256 * 1024) /* lyric/song_detail/playlist/cloudsearch:实际 ≤100KB */
|
#define RESP_MED (256 * 1024) /* lyric/song_detail/playlist/cloudsearch:实际 ≤100KB */
|
||||||
|
|
||||||
@@ -62,6 +63,34 @@ typedef struct api_json {
|
|||||||
|
|
||||||
static void api_free(api_json_t *c);
|
static void api_free(api_json_t *c);
|
||||||
|
|
||||||
|
/* 解析 c->rbuf/c->resp.body 为 JSON + 业务码判定(api_fetch 与缓存命中共用):
|
||||||
|
* 前置:c->rbuf 指向响应体缓冲、c->resp.body/body_len 已就位。 */
|
||||||
|
static int api_parse_body(api_json_t *c) {
|
||||||
|
jsmn_parser p;
|
||||||
|
jsmn_init(&p);
|
||||||
|
int n = jsmn_parse(&p, c->resp.body, c->resp.body_len, NULL, 0);
|
||||||
|
if (n <= 0) { free(c->rbuf); c->rbuf = NULL; return NCM_ERR_JSON; }
|
||||||
|
c->toks = (jsmntok_t *)malloc(sizeof(jsmntok_t) * (size_t)n);
|
||||||
|
c->parent = (int *)malloc(sizeof(int) * (size_t)n);
|
||||||
|
if (!c->toks || !c->parent) {
|
||||||
|
free(c->rbuf); free(c->toks); free(c->parent);
|
||||||
|
c->rbuf = NULL; c->toks = NULL; c->parent = NULL;
|
||||||
|
return NCM_ERR_OOM;
|
||||||
|
}
|
||||||
|
int rc = ncm_json_parse(&c->j, c->resp.body, c->resp.body_len, c->toks, n, c->parent);
|
||||||
|
if (rc < 0) { api_free(c); return NCM_ERR_JSON; }
|
||||||
|
c->ntoks = n;
|
||||||
|
|
||||||
|
long code;
|
||||||
|
if (ncm_json_path_int(&c->j, "code", &code) == NCM_OK && code != 200) {
|
||||||
|
int ec = NCM_ERR_HTTP;
|
||||||
|
if (code == 301 || code == -460 || code == 2000 || code == 201) ec = NCM_ERR_AUTH;
|
||||||
|
api_free(c);
|
||||||
|
return ec;
|
||||||
|
}
|
||||||
|
return NCM_OK;
|
||||||
|
}
|
||||||
|
|
||||||
static int api_post(const char *device_id, const char *cookie, const char *path,
|
static int api_post(const char *device_id, const char *cookie, const char *path,
|
||||||
const char *json, int kind, size_t resp_cap, api_json_t *c) {
|
const char *json, int kind, size_t resp_cap, api_json_t *c) {
|
||||||
char body[NCM_REQ_BODY_MAX], hdrs[NCM_REQ_HDRS_MAX], url[NCM_REQ_URL_MAX];
|
char body[NCM_REQ_BODY_MAX], hdrs[NCM_REQ_HDRS_MAX], url[NCM_REQ_URL_MAX];
|
||||||
@@ -78,30 +107,66 @@ static int api_post(const char *device_id, const char *cookie, const char *path,
|
|||||||
if (!c->rbuf) return NCM_ERR_OOM;
|
if (!c->rbuf) return NCM_ERR_OOM;
|
||||||
rc = ncm_http_request(&req, c->rbuf, resp_cap, &c->resp);
|
rc = ncm_http_request(&req, c->rbuf, resp_cap, &c->resp);
|
||||||
if (rc != NCM_OK) { free(c->rbuf); c->rbuf = NULL; return rc; }
|
if (rc != NCM_OK) { free(c->rbuf); c->rbuf = NULL; return rc; }
|
||||||
|
return api_parse_body(c);
|
||||||
|
}
|
||||||
|
|
||||||
jsmn_parser p;
|
/* ---- 接口级响应短 TTL 缓存(风控加固 §1)----
|
||||||
jsmn_init(&p);
|
* 目标接口:user_playlist / playlist_detail / song_detail / cloudsearch(只读幂等)。
|
||||||
int n = jsmn_parse(&p, c->resp.body, c->resp.body_len, NULL, 0);
|
* 其余(song_url/lyric/vip)不经此路径——逐曲、需新鲜、或低频。
|
||||||
if (n <= 0) { free(c->rbuf); c->rbuf = NULL; return NCM_ERR_JSON; }
|
* 手动刷新语义:ncm_api_cache_refresh_path 标记的路径下一次调用绕过缓存(不读不写)。
|
||||||
c->toks = (jsmntok_t *)malloc(sizeof(jsmntok_t) * (size_t)n);
|
* 键 = md5(path + \x01 + json),内存+可选盘都由 ncm_apicache 管。 */
|
||||||
c->parent = (int *)malloc(sizeof(int) * (size_t)n);
|
static char g_refresh_path[64]; /* 待绕过的路径(一次性消费) */
|
||||||
if (!c->toks || !c->parent) {
|
|
||||||
free(c->rbuf); free(c->toks); free(c->parent);
|
|
||||||
c->rbuf = NULL; c->toks = NULL; c->parent = NULL;
|
|
||||||
return NCM_ERR_OOM;
|
|
||||||
}
|
|
||||||
rc = ncm_json_parse(&c->j, c->resp.body, c->resp.body_len, c->toks, n, c->parent);
|
|
||||||
if (rc < 0) { api_free(c); return NCM_ERR_JSON; }
|
|
||||||
c->ntoks = n;
|
|
||||||
|
|
||||||
long code;
|
void ncm_api_cache_refresh_path(const char *path) {
|
||||||
if (ncm_json_path_int(&c->j, "code", &code) == NCM_OK && code != 200) {
|
if (!path) return;
|
||||||
int ec = NCM_ERR_HTTP;
|
ncm_strlcpy(g_refresh_path, path, sizeof g_refresh_path);
|
||||||
if (code == 301 || code == -460 || code == 2000 || code == 201) ec = NCM_ERR_AUTH;
|
}
|
||||||
api_free(c);
|
|
||||||
return ec;
|
/* 命中「手动刷新」目标路径则消费并返回 1(此调用绕过缓存)。 */
|
||||||
|
static int api_refresh_match(const char *path) {
|
||||||
|
if (!g_refresh_path[0]) return 0;
|
||||||
|
if (ncm_streq(g_refresh_path, path)) { g_refresh_path[0] = 0; return 1; }
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 缓存键(内存+盘):md5(path + '\x01' + json)。堆拼避免大 json 驻栈。 */
|
||||||
|
static void api_cache_key(const char *path, const char *json, char *out, size_t out_sz) {
|
||||||
|
size_t pl = strlen(path), jl = strlen(json);
|
||||||
|
char *buf = (char *)malloc(pl + jl + 2);
|
||||||
|
if (!buf) { out[0] = 0; return; }
|
||||||
|
memcpy(buf, path, pl);
|
||||||
|
buf[pl] = '\x01';
|
||||||
|
memcpy(buf + pl + 1, json, jl + 1);
|
||||||
|
ncm_md5_hex(buf, pl + jl + 1, out); /* 32 hex + NUL */
|
||||||
|
free(buf);
|
||||||
|
if (out_sz) out[out_sz - 1] = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 带缓存的 POST:先查内存/盘缓存(TTL 内命中则直接解析回填);miss/过期/手动刷新 → 真网络,
|
||||||
|
* 成功且非手动刷新 → 写缓存。 */
|
||||||
|
static int api_post_cached(const char *device_id, const char *cookie, const char *path,
|
||||||
|
const char *json, int kind, size_t resp_cap, api_json_t *c) {
|
||||||
|
int refresh = api_refresh_match(path);
|
||||||
|
char key[NCM_APICACHE_KEY_MAX];
|
||||||
|
api_cache_key(path, json, key, sizeof key);
|
||||||
|
|
||||||
|
if (!refresh && key[0]) {
|
||||||
|
char *buf = (char *)malloc(resp_cap);
|
||||||
|
if (buf) {
|
||||||
|
size_t len = 0;
|
||||||
|
if (ncm_apicache_get(key, buf, resp_cap, &len) == NCM_OK) {
|
||||||
|
c->rbuf = buf;
|
||||||
|
c->resp.body = buf; /* 缓存的 body 从缓冲头开始 */
|
||||||
|
c->resp.body_len = len;
|
||||||
|
return api_parse_body(c);
|
||||||
|
}
|
||||||
|
free(buf); /* miss/过期 → 走真网络 */
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return NCM_OK;
|
int rc = api_post(device_id, cookie, path, json, kind, resp_cap, c);
|
||||||
|
if (rc == NCM_OK && key[0] && !refresh)
|
||||||
|
ncm_apicache_put(key, c->resp.body, c->resp.body_len);
|
||||||
|
return rc;
|
||||||
}
|
}
|
||||||
|
|
||||||
static void api_free(api_json_t *c) {
|
static void api_free(api_json_t *c) {
|
||||||
@@ -131,7 +196,7 @@ static int obj_bool(const ncm_json_t *j, int tok, const char *key) {
|
|||||||
return (strcmp(s, "true") == 0);
|
return (strcmp(s, "true") == 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 解析一处「歌曲对象数组」(song_detail / cloudsearch / playlist_track_all 共用)。
|
/* 解析一处「歌曲对象数组」(song_detail / cloudsearch / track_all 离线解析 共用)。
|
||||||
* 元素字段:id/name/ar[0].name/al.name/al.picUrl/dt/fee。id 可为数字或字符串(均以文本拷贝,
|
* 元素字段:id/name/ar[0].name/al.name/al.picUrl/dt/fee。id 可为数字或字符串(均以文本拷贝,
|
||||||
* 兼容 >2^31 的歌曲 id)。返回 NCM_OK,*out_n=写入数。 */
|
* 兼容 >2^31 的歌曲 id)。返回 NCM_OK,*out_n=写入数。 */
|
||||||
static int parse_songs_array(const ncm_json_t *j, int arr, ncm_song_t *out, int max, int *out_n)
|
static int parse_songs_array(const ncm_json_t *j, int arr, ncm_song_t *out, int max, int *out_n)
|
||||||
@@ -173,8 +238,8 @@ int ncm_api_user_playlist(const char *device_id, const char *cookie, const char
|
|||||||
uid, limit > 0 ? limit : 30, offset > 0 ? offset : 0);
|
uid, limit > 0 ? limit : 30, offset > 0 ? offset : 0);
|
||||||
|
|
||||||
api_json_t c; memset(&c, 0, sizeof c);
|
api_json_t c; memset(&c, 0, sizeof c);
|
||||||
int rc = api_post(device_id, cookie, "/api/user/playlist", json, NCM_CRYPT_WEAPI,
|
int rc = api_post_cached(device_id, cookie, "/api/user/playlist", json, NCM_CRYPT_WEAPI,
|
||||||
RESP_MED, &c);
|
RESP_MED, &c);
|
||||||
if (rc != NCM_OK) return rc;
|
if (rc != NCM_OK) return rc;
|
||||||
|
|
||||||
int arr = ncm_json_path(&c.j, "playlist");
|
int arr = ncm_json_path(&c.j, "playlist");
|
||||||
@@ -209,8 +274,8 @@ int ncm_api_playlist_detail(const char *device_id, const char *cookie, const cha
|
|||||||
snprintf(json, sizeof json, "{\"id\":\"%s\",\"n\":1,\"s\":8}", playlist_id);
|
snprintf(json, sizeof json, "{\"id\":\"%s\",\"n\":1,\"s\":8}", playlist_id);
|
||||||
|
|
||||||
api_json_t c; memset(&c, 0, sizeof c);
|
api_json_t c; memset(&c, 0, sizeof c);
|
||||||
int rc = api_post(device_id, cookie, "/api/v6/playlist/detail", json, NCM_CRYPT_EAPI,
|
int rc = api_post_cached(device_id, cookie, "/api/v6/playlist/detail", json, NCM_CRYPT_EAPI,
|
||||||
2 * 1024 * 1024, &c);
|
2 * 1024 * 1024, &c);
|
||||||
if (rc != NCM_OK) return rc;
|
if (rc != NCM_OK) return rc;
|
||||||
|
|
||||||
int pl = ncm_json_path(&c.j, "playlist");
|
int pl = ncm_json_path(&c.j, "playlist");
|
||||||
@@ -253,8 +318,8 @@ int ncm_api_song_detail(const char *device_id, const char *cookie, const int64_t
|
|||||||
snprintf(json, sizeof json, "{\"c\":\"%s\"}", clist);
|
snprintf(json, sizeof json, "{\"c\":\"%s\"}", clist);
|
||||||
|
|
||||||
api_json_t c; memset(&c, 0, sizeof c);
|
api_json_t c; memset(&c, 0, sizeof c);
|
||||||
int rc = api_post(device_id, cookie, "/api/v3/song/detail", json, NCM_CRYPT_EAPI,
|
int rc = api_post_cached(device_id, cookie, "/api/v3/song/detail", json, NCM_CRYPT_EAPI,
|
||||||
RESP_MED, &c);
|
RESP_MED, &c);
|
||||||
if (rc != NCM_OK) return rc;
|
if (rc != NCM_OK) return rc;
|
||||||
|
|
||||||
int arr = ncm_json_path(&c.j, "songs");
|
int arr = ncm_json_path(&c.j, "songs");
|
||||||
@@ -532,8 +597,8 @@ static int cloudsearch_post(const char *device_id, const char *cookie, const cha
|
|||||||
esc[ei] = 0;
|
esc[ei] = 0;
|
||||||
snprintf(json, sizeof json, "{\"s\":\"%s\",\"type\":%d,\"offset\":%d,\"limit\":%d,\"total\":true}",
|
snprintf(json, sizeof json, "{\"s\":\"%s\",\"type\":%d,\"offset\":%d,\"limit\":%d,\"total\":true}",
|
||||||
esc, type, offset > 0 ? offset : 0, limit > 0 ? limit : 30);
|
esc, type, offset > 0 ? offset : 0, limit > 0 ? limit : 30);
|
||||||
return api_post(device_id, cookie, "/api/cloudsearch/pc", json, NCM_CRYPT_WEAPI,
|
return api_post_cached(device_id, cookie, "/api/cloudsearch/pc", json, NCM_CRYPT_WEAPI,
|
||||||
RESP_MED, c);
|
RESP_MED, c);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 解析一处「歌单对象数组」(cloudsearch type=1000)。id 可为数字/字符串(均文本拷贝)。 */
|
/* 解析一处「歌单对象数组」(cloudsearch type=1000)。id 可为数字/字符串(均文本拷贝)。 */
|
||||||
@@ -609,9 +674,13 @@ int ncm_api_cloudsearch(const char *device_id, const char *cookie, const char *k
|
|||||||
return ncm_api_cloudsearch_song(device_id, cookie, keyword, 0, max, out, max, out_n, NULL);
|
return ncm_api_cloudsearch_song(device_id, cookie, keyword, 0, max, out, max, out_n, NULL);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 从一段 track/all 响应 JSON 串解析歌曲数组 + total(离线样本/单元测试用,不打网络)。
|
/* 从一段「track/all 形态」响应 JSON 串解析歌曲数组 + total(离线样本/单测用,不打网络)。
|
||||||
* 数组取「songs → playlist.tracks → data.songs」首个命中;total 取「total → count →
|
* 数组取「songs → playlist.tracks → data.songs」首个命中;total 取「total → count →
|
||||||
* playlist.trackCount → musicCount」,全缺则 0。 */
|
* playlist.trackCount → musicCount」,全缺则 0。
|
||||||
|
* ⚠ 接口核实:`/api/playlist/track/all` 在网易云真实服务器**不存在**(NCMAPI_NOTES §1.1,
|
||||||
|
* 8 形态全 404)——真实取全量曲目走 v6/playlist/detail(trackIds) + v3/song/detail 分批。
|
||||||
|
* 本函数**保留仅为离线单测/样本回放**,勿用于任何真实网络请求(原网络封装
|
||||||
|
* ncm_api_playlist_track_all 已随端点核实为不存在而移除)。 */
|
||||||
int ncm_api_parse_track_all(const char *json, size_t len, ncm_song_t *out, int max,
|
int ncm_api_parse_track_all(const char *json, size_t len, ncm_song_t *out, int max,
|
||||||
int *out_n, int64_t *total) {
|
int *out_n, int64_t *total) {
|
||||||
if (out_n) *out_n = 0;
|
if (out_n) *out_n = 0;
|
||||||
@@ -646,27 +715,3 @@ int ncm_api_parse_track_all(const char *json, size_t len, ncm_song_t *out, int m
|
|||||||
free(toks); free(parent);
|
free(toks); free(parent);
|
||||||
return rc;
|
return rc;
|
||||||
}
|
}
|
||||||
|
|
||||||
int ncm_api_playlist_track_all(const char *device_id, const char *cookie, const char *playlist_id,
|
|
||||||
int offset, int limit, ncm_song_t *out, int max, int *out_n,
|
|
||||||
int64_t *total) {
|
|
||||||
if (out_n) *out_n = 0;
|
|
||||||
if (limit <= 0) limit = 50;
|
|
||||||
if (limit > max) limit = max;
|
|
||||||
if (offset < 0) offset = 0;
|
|
||||||
|
|
||||||
char json[256];
|
|
||||||
snprintf(json, sizeof json, "{\"id\":\"%s\",\"offset\":%d,\"limit\":%d,\"n\":%d,\"s\":8}",
|
|
||||||
playlist_id, offset, limit, limit);
|
|
||||||
|
|
||||||
api_json_t c; memset(&c, 0, sizeof c);
|
|
||||||
int rc = api_post(device_id, cookie, "/api/playlist/track/all", json, NCM_CRYPT_EAPI,
|
|
||||||
NCM_RESP_MAX, &c);
|
|
||||||
if (rc != NCM_OK) return rc;
|
|
||||||
|
|
||||||
int64_t tot = 0;
|
|
||||||
rc = ncm_api_parse_track_all(c.resp.body, c.resp.body_len, out, max, out_n, &tot);
|
|
||||||
if (rc == NCM_OK && total) *total = tot;
|
|
||||||
api_free(&c);
|
|
||||||
return rc;
|
|
||||||
}
|
|
||||||
@@ -158,20 +158,22 @@ int ncm_api_cloudsearch_playlist(const char *device_id, const char *cookie, cons
|
|||||||
int ncm_api_cloudsearch(const char *device_id, const char *cookie, const char *keyword,
|
int ncm_api_cloudsearch(const char *device_id, const char *cookie, const char *keyword,
|
||||||
ncm_song_t *out, int max, int *out_n);
|
ncm_song_t *out, int max, int *out_n);
|
||||||
|
|
||||||
/* 歌单曲目服务端分页(eapi /api/playlist/track/all,REPORT §11 二期)。
|
/* 离线解析:从一段「track/all 形态」响应 JSON 串解析歌曲数组(含 al.picUrl)+ total。
|
||||||
* 相对 /v6/playlist/detail 的「全量 trackIds(5191 首 ≈900KB)」改进:本端点按
|
* 供「宿主离线样本重放」与单元测试用(bench/offline_track_all.c / bench/probe_track_all.c /
|
||||||
* offset/limit 直接返回该页的歌曲对象(含 al.picUrl,无需再 song_detail 二次请求)。
|
* test_ncm_core.c),不打网络。
|
||||||
* 首屏 offset=0 limit=50;滚动到底继续 offset+=limit。out 填 ncm_song_t。
|
* ⚠ 接口核实:`/api/playlist/track/all` 在网易云真实服务器**不存在**(NCMAPI_NOTES §1.1,
|
||||||
* *total 返回歌单总曲数(0=未知;调用方可退化为 playlist.trackCount)。 */
|
* 8 形态全 404)——真实取全量曲目走 v6/playlist/detail(trackIds) + v3/song/detail 分批。
|
||||||
int ncm_api_playlist_track_all(const char *device_id, const char *cookie, const char *playlist_id,
|
* 本函数**保留仅为离线单测/样本回放**,勿用于任何真实网络请求(原网络封装
|
||||||
int offset, int limit, ncm_song_t *out, int max, int *out_n,
|
* ncm_api_playlist_track_all 已随端点核实为不存在而移除)。 */
|
||||||
int64_t *total);
|
|
||||||
|
|
||||||
/* 离线解析:从一段 track/all 响应 JSON 串解析歌曲数组(含 al.picUrl)+ total。
|
|
||||||
* 网络版内部同一解析器;供「宿主离线样本重放」与单元测试用,不打网络。 */
|
|
||||||
int ncm_api_parse_track_all(const char *json, size_t len, ncm_song_t *out, int max,
|
int ncm_api_parse_track_all(const char *json, size_t len, ncm_song_t *out, int max,
|
||||||
int *out_n, int64_t *total);
|
int *out_n, int64_t *total);
|
||||||
|
|
||||||
|
/* 手动刷新语义(风控加固 §1 缓存):标记某个 API 路径的**下一次**调用绕过响应缓存
|
||||||
|
* (不读不写;用完即消费)。用于「手动刷新/下拉刷新」——home topbar ↻(user/playlist)、
|
||||||
|
* 歌单 topbar ↻(v6/playlist/detail)。cloudsearch 无手动刷新概念(换关键词即新意图),
|
||||||
|
* 不标记。非目标路径不消费(可并发各标记各的)。 */
|
||||||
|
void ncm_api_cache_refresh_path(const char *path);
|
||||||
|
|
||||||
#ifdef __cplusplus
|
#ifdef __cplusplus
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
|
|||||||
@@ -0,0 +1,297 @@
|
|||||||
|
/* ncm_apicache.c — 接口级 JSON 响应短 TTL 缓存实现(见 ncm_apicache.h)
|
||||||
|
*
|
||||||
|
* 自包含:只用 libc + pthread + dirent/stat + clock_gettime,不依赖 ncm_store/ncm_crypto/
|
||||||
|
* ncm_common 的任何**函数**(独立单测,可独立链接到任何 sim,无符号冲突);仅取
|
||||||
|
* ncm_common.h 的错误码枚举(NCM_OK/NCM_ERR_*,编译期常量,不产生链接引用)。
|
||||||
|
* 时间:内存条目以单调钟(CLOCK_MONOTONIC)判 TTL;盘条目以文件头墙钟毫秒(CLOCK_REALTIME)
|
||||||
|
* 判 TTL(需跨进程/重启存活)。
|
||||||
|
*/
|
||||||
|
#include "ncm_apicache.h"
|
||||||
|
#include "ncm_common.h" /* 仅错误码枚举;不调用其函数,故无链接依赖 */
|
||||||
|
|
||||||
|
#include <pthread.h>
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <stdlib.h>
|
||||||
|
#include <string.h>
|
||||||
|
#include <time.h>
|
||||||
|
#include <unistd.h>
|
||||||
|
#include <errno.h>
|
||||||
|
#include <sys/stat.h>
|
||||||
|
#include <sys/types.h>
|
||||||
|
#include <dirent.h>
|
||||||
|
|
||||||
|
/* 内存表:固定槽数 LRU(每槽一个响应体)。槽数×单槽上限共同约束常驻内存:
|
||||||
|
* 4 × 128KB = 512KB 峰值,低于现有瞬态缓冲预算(REPORT §32 内存瘦身口径)。 */
|
||||||
|
#define MAX_ENTRIES 4
|
||||||
|
#define BODY_MAX (128 * 1024)
|
||||||
|
#define DISK_DIR_MAX 512
|
||||||
|
#define DISK_SUBDIR "ncm_api_cache"
|
||||||
|
#define DISK_PATH_MAX 1024 /* 目录+文件名拼接缓冲(≥ dir 512 + / + name 255 + nul,免 -Wformat-truncation) */
|
||||||
|
#define DISK_PURGE_MS (30 * 1000) /* 盘过期清理节流(30s) */
|
||||||
|
|
||||||
|
typedef struct {
|
||||||
|
char key[NCM_APICACHE_KEY_MAX]; /* md5 hex + NUL;空串=空槽 */
|
||||||
|
char *body; /* 堆上响应体快照 */
|
||||||
|
size_t body_len;
|
||||||
|
int64_t mono_ms; /* 存槽单调钟(TTL 判定) */
|
||||||
|
int64_t last_ms; /* 最近命中(LRU 淘汰) */
|
||||||
|
} entry_t;
|
||||||
|
|
||||||
|
static entry_t g_e[MAX_ENTRIES];
|
||||||
|
static pthread_mutex_t g_mu = PTHREAD_MUTEX_INITIALIZER; /* 内存表锁 */
|
||||||
|
static pthread_mutex_t g_disk_mu = PTHREAD_MUTEX_INITIALIZER; /* 盘缓存锁(串行 I/O) */
|
||||||
|
static char g_disk_dir[DISK_DIR_MAX];
|
||||||
|
static int g_disk_on = 0;
|
||||||
|
static int g_ttl_s = NCM_APICACHE_TTL_S; /* 默认 120s */
|
||||||
|
static int64_t g_last_purge_ms = 0; /* 盘过期清理节流(单调钟)*/
|
||||||
|
|
||||||
|
static int64_t clock_ms(clockid_t id) {
|
||||||
|
struct timespec ts;
|
||||||
|
if (clock_gettime(id, &ts) != 0) return 0;
|
||||||
|
return (int64_t)ts.tv_sec * 1000 + ts.tv_nsec / 1000000;
|
||||||
|
}
|
||||||
|
static int64_t now_mono(void) { return clock_ms(CLOCK_MONOTONIC); }
|
||||||
|
static int64_t now_wall(void) { return clock_ms(CLOCK_REALTIME); }
|
||||||
|
|
||||||
|
void ncm_apicache_ttl_set(int seconds)
|
||||||
|
{
|
||||||
|
pthread_mutex_lock(&g_mu);
|
||||||
|
g_ttl_s = (seconds < 0) ? NCM_APICACHE_TTL_S : seconds;
|
||||||
|
pthread_mutex_unlock(&g_mu);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void slot_clear_locked(int i)
|
||||||
|
{
|
||||||
|
free(g_e[i].body);
|
||||||
|
g_e[i].body = NULL;
|
||||||
|
g_e[i].body_len = 0;
|
||||||
|
g_e[i].mono_ms = 0;
|
||||||
|
g_e[i].last_ms = 0;
|
||||||
|
g_e[i].key[0] = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
int ncm_apicache_mem_count(void)
|
||||||
|
{
|
||||||
|
pthread_mutex_lock(&g_mu);
|
||||||
|
int c = 0;
|
||||||
|
for (int i = 0; i < MAX_ENTRIES; i++) if (g_e[i].body) c++;
|
||||||
|
pthread_mutex_unlock(&g_mu);
|
||||||
|
return c;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---------- 盘缓存(可选) ---------- */
|
||||||
|
|
||||||
|
static void disk_path(const char *key, char *out, size_t out_sz)
|
||||||
|
{
|
||||||
|
snprintf(out, out_sz, "%s/%s/%s.json", g_disk_dir, DISK_SUBDIR, key);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* mkdir data_dir/ncm_api_cache:目录已存在(EEXIST)静默成功。 */
|
||||||
|
static void ensure_disk_dir(void)
|
||||||
|
{
|
||||||
|
char p[DISK_PATH_MAX];
|
||||||
|
snprintf(p, sizeof p, "%s/%s", g_disk_dir, DISK_SUBDIR);
|
||||||
|
if (mkdir(p, 0777) != 0 && errno != EEXIST) { /* 静默:后续写失败自然返回 */ }
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 删除盘缓存目录内所有文件(清空)。 */
|
||||||
|
static void disk_purge_all(void)
|
||||||
|
{
|
||||||
|
char d[DISK_DIR_MAX + 32];
|
||||||
|
snprintf(d, sizeof d, "%s/%s", g_disk_dir, DISK_SUBDIR);
|
||||||
|
DIR *dir = opendir(d);
|
||||||
|
if (!dir) return;
|
||||||
|
struct dirent *de;
|
||||||
|
while ((de = readdir(dir)) != NULL) {
|
||||||
|
if (de->d_name[0] == '.') continue;
|
||||||
|
char p[DISK_PATH_MAX];
|
||||||
|
snprintf(p, sizeof p, "%s/%s", d, de->d_name);
|
||||||
|
unlink(p);
|
||||||
|
}
|
||||||
|
closedir(dir);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 删除过期盘文件(以文件 mtime 判 TTL,写入即刷新)。用于启动 + 写入节流清理,磁盘有界。 */
|
||||||
|
static void disk_purge_expired(void)
|
||||||
|
{
|
||||||
|
char d[DISK_DIR_MAX + 32];
|
||||||
|
snprintf(d, sizeof d, "%s/%s", g_disk_dir, DISK_SUBDIR);
|
||||||
|
DIR *dir = opendir(d);
|
||||||
|
if (!dir) return;
|
||||||
|
int64_t now = now_wall();
|
||||||
|
int64_t ttl = (int64_t)g_ttl_s * 1000;
|
||||||
|
struct dirent *de;
|
||||||
|
while ((de = readdir(dir)) != NULL) {
|
||||||
|
if (de->d_name[0] == '.') continue;
|
||||||
|
size_t nl = strlen(de->d_name);
|
||||||
|
if (nl < 5 || strcmp(de->d_name + nl - 5, ".json") != 0) continue;
|
||||||
|
char p[DISK_PATH_MAX];
|
||||||
|
snprintf(p, sizeof p, "%s/%s", d, de->d_name);
|
||||||
|
struct stat st;
|
||||||
|
if (stat(p, &st) != 0) { unlink(p); continue; }
|
||||||
|
if (ttl > 0 && now - (int64_t)st.st_mtime * 1000 > ttl) unlink(p);
|
||||||
|
}
|
||||||
|
closedir(dir);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 盘 get:读 8B 墙钟头 + body。过期 → 删文件、miss。调用方已持 g_disk_mu。 */
|
||||||
|
static int disk_get_locked(const char *key, char *buf, size_t buf_sz, size_t *len)
|
||||||
|
{
|
||||||
|
char p[DISK_PATH_MAX];
|
||||||
|
disk_path(key, p, sizeof p);
|
||||||
|
FILE *f = fopen(p, "rb");
|
||||||
|
if (!f) return NCM_ERR_NOENT;
|
||||||
|
int64_t ts = 0;
|
||||||
|
if (fread(&ts, 1, sizeof ts, f) != sizeof ts) { fclose(f); unlink(p); return NCM_ERR_IO; }
|
||||||
|
int64_t now = now_wall();
|
||||||
|
int64_t ttl = (int64_t)g_ttl_s * 1000;
|
||||||
|
if (ttl > 0 && now - ts > ttl) { fclose(f); unlink(p); return NCM_ERR_NOENT; }
|
||||||
|
if (fseek(f, 0, SEEK_END) != 0) { fclose(f); return NCM_ERR_IO; }
|
||||||
|
long fl = ftell(f);
|
||||||
|
if (fl <= (long)sizeof ts) { fclose(f); unlink(p); return NCM_ERR_IO; }
|
||||||
|
if (fseek(f, (long)sizeof ts, SEEK_SET) != 0) { fclose(f); return NCM_ERR_IO; }
|
||||||
|
size_t blen = (size_t)(fl - (long)sizeof ts);
|
||||||
|
if (blen > buf_sz) { fclose(f); return NCM_ERR_FULL; }
|
||||||
|
size_t rd = fread(buf, 1, blen, f);
|
||||||
|
fclose(f);
|
||||||
|
if (rd != blen) return NCM_ERR_IO;
|
||||||
|
if (len) *len = rd;
|
||||||
|
return NCM_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void disk_put_locked(const char *key, const char *body, size_t len)
|
||||||
|
{
|
||||||
|
ensure_disk_dir();
|
||||||
|
int64_t now = now_mono();
|
||||||
|
if (now - g_last_purge_ms > DISK_PURGE_MS) { g_last_purge_ms = now; disk_purge_expired(); }
|
||||||
|
char p[DISK_PATH_MAX];
|
||||||
|
disk_path(key, p, sizeof p);
|
||||||
|
FILE *f = fopen(p, "wb");
|
||||||
|
if (!f) return;
|
||||||
|
int64_t ts = now_wall();
|
||||||
|
fwrite(&ts, 1, sizeof ts, f);
|
||||||
|
fwrite(body, 1, len, f);
|
||||||
|
fclose(f);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void disk_put(const char *key, const char *body, size_t len)
|
||||||
|
{
|
||||||
|
if (!g_disk_on) return;
|
||||||
|
pthread_mutex_lock(&g_disk_mu);
|
||||||
|
disk_put_locked(key, body, len);
|
||||||
|
pthread_mutex_unlock(&g_disk_mu);
|
||||||
|
}
|
||||||
|
|
||||||
|
static int disk_get(const char *key, char *buf, size_t buf_sz, size_t *len)
|
||||||
|
{
|
||||||
|
if (!g_disk_on) return NCM_ERR_NOENT;
|
||||||
|
pthread_mutex_lock(&g_disk_mu);
|
||||||
|
int rc = disk_get_locked(key, buf, buf_sz, len);
|
||||||
|
pthread_mutex_unlock(&g_disk_mu);
|
||||||
|
return rc;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---------- 内存表 ---------- */
|
||||||
|
|
||||||
|
int ncm_apicache_get(const char *key, char *buf, size_t buf_sz, size_t *len)
|
||||||
|
{
|
||||||
|
if (!key || !buf || buf_sz == 0) return NCM_ERR_ARG;
|
||||||
|
if (len) *len = 0;
|
||||||
|
int64_t now = now_mono();
|
||||||
|
int64_t ttl = (int64_t)g_ttl_s * 1000;
|
||||||
|
|
||||||
|
pthread_mutex_lock(&g_mu);
|
||||||
|
int idx = -1;
|
||||||
|
for (int i = 0; i < MAX_ENTRIES; i++)
|
||||||
|
if (g_e[i].body && strcmp(g_e[i].key, key) == 0) { idx = i; break; }
|
||||||
|
if (idx >= 0) {
|
||||||
|
if (ttl > 0 && now - g_e[idx].mono_ms > ttl) {
|
||||||
|
slot_clear_locked(idx); /* 过期 → 槽位作废,落盘兜底 */
|
||||||
|
} else {
|
||||||
|
size_t n = g_e[idx].body_len;
|
||||||
|
if (n > buf_sz) { pthread_mutex_unlock(&g_mu); return NCM_ERR_FULL; }
|
||||||
|
memcpy(buf, g_e[idx].body, n);
|
||||||
|
g_e[idx].last_ms = now; /* LRU touch */
|
||||||
|
if (len) *len = n;
|
||||||
|
pthread_mutex_unlock(&g_mu);
|
||||||
|
return NCM_OK;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pthread_mutex_unlock(&g_mu);
|
||||||
|
|
||||||
|
/* 内存 miss → 盘缓存兜底(命中回填内存)。 */
|
||||||
|
if (g_disk_on) {
|
||||||
|
int rc = disk_get(key, buf, buf_sz, len);
|
||||||
|
if (rc == NCM_OK) ncm_apicache_put(key, buf, *len);
|
||||||
|
return rc;
|
||||||
|
}
|
||||||
|
return NCM_ERR_NOENT;
|
||||||
|
}
|
||||||
|
|
||||||
|
void ncm_apicache_put(const char *key, const char *body, size_t len)
|
||||||
|
{
|
||||||
|
if (!key || !body || len == 0) return;
|
||||||
|
int64_t now = now_mono();
|
||||||
|
|
||||||
|
disk_put(key, body, len); /* 盘层(范围外,避免持内存锁期间做 I/O) */
|
||||||
|
|
||||||
|
pthread_mutex_lock(&g_mu);
|
||||||
|
int idx = -1;
|
||||||
|
for (int i = 0; i < MAX_ENTRIES; i++)
|
||||||
|
if (g_e[i].body && strcmp(g_e[i].key, key) == 0) { idx = i; break; }
|
||||||
|
if (idx < 0) {
|
||||||
|
for (int i = 0; i < MAX_ENTRIES; i++) if (!g_e[i].body) { idx = i; break; }
|
||||||
|
if (idx < 0) { /* 满 → 逐 LRU 最旧 */
|
||||||
|
int64_t mn = g_e[0].last_ms; idx = 0;
|
||||||
|
for (int i = 1; i < MAX_ENTRIES; i++)
|
||||||
|
if (g_e[i].last_ms < mn) { mn = g_e[i].last_ms; idx = i; }
|
||||||
|
slot_clear_locked(idx);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (len > (size_t)BODY_MAX) {
|
||||||
|
/* 超大响应(大歌单 trackIds >128KB):不入内存(内存瘦身),仅盘缓存(若开启)。
|
||||||
|
* 本槽若曾存本 key 旧值,一并作废,改由盘兜底。 */
|
||||||
|
slot_clear_locked(idx);
|
||||||
|
pthread_mutex_unlock(&g_mu);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
char *nb = (char *)malloc(len);
|
||||||
|
if (!nb) { pthread_mutex_unlock(&g_mu); return; }
|
||||||
|
memcpy(nb, body, len);
|
||||||
|
slot_clear_locked(idx);
|
||||||
|
strncpy(g_e[idx].key, key, sizeof g_e[idx].key - 1);
|
||||||
|
g_e[idx].key[sizeof g_e[idx].key - 1] = 0;
|
||||||
|
g_e[idx].body = nb;
|
||||||
|
g_e[idx].body_len = len;
|
||||||
|
g_e[idx].mono_ms = now;
|
||||||
|
g_e[idx].last_ms = now;
|
||||||
|
pthread_mutex_unlock(&g_mu);
|
||||||
|
}
|
||||||
|
|
||||||
|
void ncm_apicache_clear(void)
|
||||||
|
{
|
||||||
|
pthread_mutex_lock(&g_mu);
|
||||||
|
for (int i = 0; i < MAX_ENTRIES; i++) slot_clear_locked(i);
|
||||||
|
pthread_mutex_unlock(&g_mu);
|
||||||
|
if (g_disk_on) {
|
||||||
|
pthread_mutex_lock(&g_disk_mu);
|
||||||
|
disk_purge_all();
|
||||||
|
pthread_mutex_unlock(&g_disk_mu);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void ncm_apicache_init(const char *data_dir)
|
||||||
|
{
|
||||||
|
if (data_dir && data_dir[0]) {
|
||||||
|
strncpy(g_disk_dir, data_dir, sizeof g_disk_dir - 1);
|
||||||
|
g_disk_dir[sizeof g_disk_dir - 1] = 0;
|
||||||
|
pthread_mutex_lock(&g_disk_mu);
|
||||||
|
g_disk_on = 1;
|
||||||
|
ensure_disk_dir();
|
||||||
|
disk_purge_expired(); /* 启动清一次过期盘文件 */
|
||||||
|
pthread_mutex_unlock(&g_disk_mu);
|
||||||
|
} else {
|
||||||
|
g_disk_dir[0] = 0;
|
||||||
|
g_disk_on = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,61 @@
|
|||||||
|
/* ncm_apicache.h — 接口级 JSON 响应短 TTL 缓存(风控加固 §1)
|
||||||
|
*
|
||||||
|
* 对齐参考 NeteaseCloudMusicApi 的 express 层 apicache「2 分钟全局缓存」,防高频拉取
|
||||||
|
* 触发网易服务器「ip 高频错误 / 503」。目标接口:user_playlist / playlist_detail /
|
||||||
|
* song_detail / cloudsearch(只读、幂等)。song_url / lyric / vip 等**不入缓存**
|
||||||
|
* (逐曲/低频/需新鲜)。
|
||||||
|
*
|
||||||
|
* 两级:
|
||||||
|
* - 内存:固定 4 槽 LRU(每槽 ≤128KB,TTL 120s)。键=接口+参数 md5(32hex)。
|
||||||
|
* 超 TTL 过期;超容量逐最旧;登录/换账号可 ncm_apicache_clear 全清。
|
||||||
|
* - 盘(可选):<data_dir>/ncm_api_cache/<md5>.json。文件头 8B 墙钟毫秒 + body。
|
||||||
|
* 超 TTL 视为 miss(并删除);写入时按 30s 节流清过期文件,磁盘占用有界。
|
||||||
|
* 盘缓存跨进程/热更保留,是本机防高频的持久层(songmeta 同类,接口级 JSON 新加)。
|
||||||
|
*
|
||||||
|
* 失效纪律:手动刷新/下拉刷新经 ncm_api_cache_refresh_path 标记 → 该路径下一次调用
|
||||||
|
* 绕过缓存(不读不写);登出/换账号经 ncm_apicache_clear 全清(防串账号)。盘缓存超 TTL
|
||||||
|
* 自然过期,刷新不写盘。
|
||||||
|
*
|
||||||
|
* 模块自包含(仅 libc + pthread),不依赖 ncm_store/ncm_crypto,便于独立单测。
|
||||||
|
*/
|
||||||
|
#ifndef NCM_APICACHE_H
|
||||||
|
#define NCM_APICACHE_H
|
||||||
|
|
||||||
|
#include <stddef.h>
|
||||||
|
#include <stdint.h>
|
||||||
|
|
||||||
|
#ifdef __cplusplus
|
||||||
|
extern "C" {
|
||||||
|
#endif
|
||||||
|
|
||||||
|
/* 默认 TTL(秒):对接参考 apicache 2 分钟。 */
|
||||||
|
#define NCM_APICACHE_TTL_S 120
|
||||||
|
|
||||||
|
/* 内存表键最大长度(md5 hex 32 + NUL)。 */
|
||||||
|
#define NCM_APICACHE_KEY_MAX 33
|
||||||
|
|
||||||
|
/* 初始化:设数据目录用于盘缓存 <data_dir>/ncm_api_cache/。dir 为 NULL 或空 → 仅内存缓存
|
||||||
|
* (盘缓存关闭;test_ncm_core/离线单测用)。非空则在启动时清一次过期盘文件。 */
|
||||||
|
void ncm_apicache_init(const char *data_dir);
|
||||||
|
|
||||||
|
/* 取一份新鲜响应。key=接口+参数 hash(调用方管)。命中:*buf 填响应体、*len 填长度、返 NCM_OK;
|
||||||
|
* 未命中/过期 → NCM_ERR_NOENT(过期项删除,盘文件留给后续清理)。buf_sz 不足 → NCM_ERR_FULL。 */
|
||||||
|
int ncm_apicache_get(const char *key, char *buf, size_t buf_sz, size_t *len);
|
||||||
|
|
||||||
|
/* 存一份响应体到内存(≤BODY_MAX 才入内存;更大仅入盘)+ 盘(若开启)。
|
||||||
|
* body==NULL/len==0 忽略。 */
|
||||||
|
void ncm_apicache_put(const char *key, const char *body, size_t len);
|
||||||
|
|
||||||
|
/* 清空(内存全槽 + 盘缓存目录所有文件)。登出/换账号调用,防串账号数据。 */
|
||||||
|
void ncm_apicache_clear(void);
|
||||||
|
|
||||||
|
/* 覆盖 TTL(秒;<0 还原默认)。仅测试/内部用,运行期可留默认。 */
|
||||||
|
void ncm_apicache_ttl_set(int seconds);
|
||||||
|
|
||||||
|
/* 当前内存条目数(诊断/测试)。 */
|
||||||
|
int ncm_apicache_mem_count(void);
|
||||||
|
|
||||||
|
#ifdef __cplusplus
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
#endif /* NCM_APICACHE_H */
|
||||||
@@ -231,6 +231,10 @@ int ncm_req_build(const char *device_id, const char *cookie, const char *path,
|
|||||||
unsigned char r[4]; ncm_random_bytes(r, 4);
|
unsigned char r[4]; ncm_random_bytes(r, 4);
|
||||||
ncm_hex_encode(r, 4, nmtid, 1); /* 大写 */
|
ncm_hex_encode(r, 4, nmtid, 1); /* 大写 */
|
||||||
ck_set_default(&m, "NMTID", nmtid);
|
ck_set_default(&m, "NMTID", nmtid);
|
||||||
|
/* 游客 cookie 伪装(NCMAPI_NOTES §4):无 MUSIC_U 时补 MUSIC_A=anonymous_token,
|
||||||
|
* 对齐参考「常驻 cookie 伪装」;需登录接口未登录会 301 而非 400(reduce 风控误判)。
|
||||||
|
* 不覆盖已存在的 MUSIC_A(若会话 cookie 自带),也不在 login 路径注入。 */
|
||||||
|
if (ck_find(&m, "MUSIC_U") < 0) ck_set_default(&m, "MUSIC_A", "anonymous_token");
|
||||||
}
|
}
|
||||||
char cookie_full[4096];
|
char cookie_full[4096];
|
||||||
ck_to_string(&m, cookie_full, sizeof cookie_full);
|
ck_to_string(&m, cookie_full, sizeof cookie_full);
|
||||||
@@ -281,6 +285,14 @@ int ncm_req_build(const char *device_id, const char *cookie, const char *path,
|
|||||||
|
|
||||||
char cookie_hdr[2048];
|
char cookie_hdr[2048];
|
||||||
hm_to_cookie(&h, cookie_hdr, sizeof cookie_hdr);
|
hm_to_cookie(&h, cookie_hdr, sizeof cookie_hdr);
|
||||||
|
/* 游客 cookie 伪装(NCMAPI_NOTES §4):无 MUSIC_U 时补 MUSIC_A=anonymous_token
|
||||||
|
* 到 eapi Cookie 头(weapi 分支同样注入);login 路径不注入。仅检测存在性
|
||||||
|
* (MUSIC_U 值可能很长,无需拷出)。 */
|
||||||
|
if (!strstr(path, "login")) {
|
||||||
|
char musicu[8];
|
||||||
|
if (ncm_req_cookie_get(cookie, "MUSIC_U", musicu, sizeof musicu) != NCM_OK)
|
||||||
|
ncm_strlcat(cookie_hdr, "; MUSIC_A=anonymous_token", sizeof cookie_hdr);
|
||||||
|
}
|
||||||
|
|
||||||
int n = snprintf(hdrs, hdrs_sz,
|
int n = snprintf(hdrs, hdrs_sz,
|
||||||
"User-Agent: %s\r\nContent-Type: application/x-www-form-urlencoded\r\nCookie: %s",
|
"User-Agent: %s\r\nContent-Type: application/x-www-form-urlencoded\r\nCookie: %s",
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Reference in new issue
Block a user