fix(ncm): 启动卡顿+大歌单+No tracks 三连修 (agent-209) — ①home/playlist 加载改后台 worker ②大歌单 detail n=1(tracks瘦身)+响应cap 2MB ③song_detail c数组引号转义 ④P0: MIPS32 long=32位歌曲id溢出→int64 ⑤eapi批缓冲2048→4096

ncm→f6bd765a 已部署; 真机验证(风控合规): 小歌单✓ 大歌单256首✓ 启动不卡✓; 已知: 大歌单首屏~30s(二期改分页端点)
This commit is contained in:
MeowOS committed 2026-08-31 09:43:35 +08:00
1 parent a651df0e9b
commit 4cec476dd9
16 files changed
+344 -36

No files matched your search

+97
View File
@@ -455,3 +455,100 @@ ed2d9658bc8707a5fd689b386da472f9 app/logic/cover.c e7cef918d2be04f6d4c5a
- **`appstate_ensure_uid` 仍同步跑在 UI 线程**,首次(uid 未落盘时)会占 UI 至多 ~6s 硬上限(正常 ~0.7s); - **`appstate_ensure_uid` 仍同步跑在 UI 线程**,首次(uid 未落盘时)会占 UI 至多 ~6s 硬上限(正常 ~0.7s);
真正的“不阻塞歌单页”可由「uid 落盘后短路」+「6s 硬上限」保证,未改为后台线程(避免范围扩散)。 真正的“不阻塞歌单页”可由「uid 落盘后短路」+「6s 硬上限」保证,未改为后台线程(避免范围扩散)。
---
### 10.11 真机三问题修复:启动卡顿 + 大歌单 Load Failed + 小歌单 No tracks(2026-08-31,已真机闭环)
> 现象(用户真机实测):登录通、歌单列表 64 个。三个问题:
> ① 有登录态点开 App 卡数秒才进;②「我喜欢的音乐」(5191 首) 点开 Load Failed;③ 其余歌单全部 No tracks。
> 本批定位多处独立缺陷并修复,全部经宿主复现 + 真机验证。仅改动 `patch_v2/ncm_app/`。
#### 根因(按现象对应)
- **P1 启动卡数秒**:`app/ui/home.c` `home_page_enter → home_reload → appstate_load_playlists`(weapi user/playlist)在 **UI 线程同步执行**;真机该 weapi 往返 ~2.5s+(verify_home: `elapsed_ms=2562`),uid 未落盘时还叠 status_check(6s 硬上限)。期间 UI 冻死(`paint_full` 不执行)。
- **P2 大歌单 Load Failed**:`core/ncm_api.c:ncm_api_playlist_detail` 发 `{"id":..,"n":100000,"s":8}` + 响应 cap `NCM_RESP_MAX(1MB)`。5191 曲歌单 detail 真机实测 `status=200 cl=3168819`(3.0MB:`playlist.tracks` 塞满歌曲对象),1MB cap 溢出 → `NCM_ERR_FULL(-11)` → Load Failed。关键:**`trackIds` 数组与 `n` 无关**(n=1 时仍完整返回 5191 条,实测),是 `tracks`(完整歌曲对象)撑大响应。
- **P3 小歌单 No tracks**:`ncm_api_song_detail` 把曲目 id 数组塞进 `{"c":"[...]"}` 时**内部引号未转义**,实发 `{"c":"[{"id":2048962657}]"}`(坏 JSON),服务端回 `200 Content-Length:0` → jsmn 空体 → `NCM_ERR_JSON(-9)` → 0 首。宿主复刻:转义后 `{"c":"[{\"id\":2048962657}]"}` → 服务端回 `2146B` 正常 songs。
- **P0(连带新发现)**:网易云歌曲 id 可 >2^31(如 `2746301825`),而 App 用 `long` 存 song_ids;**MIPS32 的 long=32 位**,`strtol` 溢出饱和到 `LONG_MAX(2147483647)` → 大歌单 trackIds 全错(`2746301825` 被解析成 `2147483647`),song_detail 拿错误 id 只回 30 首。宿主 aarch64(long=64 位)不暴露,真机 MIPS32 才炸。
- **P3b(连带)**:批量 song_detail 转义后 json 变长 → eapi `params[2048]` 溢出 → `ncm_crypto_eapi` 返 `NCM_ERR_CRYPTO(-8)`(batch=64 实测 rc=-8)。
#### 修法(全部在 `patch_v2/ncm_app/`)
- **P1** `app/ui/home.c`:加载改后台 worker(`home_worker`,detached);`home_reload` 立即返回并画 LOADING;`home_tick` 按 `g_home_gen` 完成代数回填。`app_on_tick` 路由 `home_tick`。ensure_uid(status_check)+ user_playlist 全在 worker,UI 不阻塞。(顺带消掉 §10.10 遗留的「ensure_uid 同步跑 UI 线程」。)
- **P2** `core/ncm_api.c:ncm_api_playlist_detail`:`n` 100000→**1**(`tracks` 只带 1 首,3MB→~900KB);响应 cap 1MB→**2MB**(trackIds 大的歌单留余量)。
- **P3** `core/ncm_api.c:ncm_api_song_detail`:`c` 数组内部引号转义(`{\\\"id\\\":%lld}`)。
- **P0** `core/ncm_common.{h,c}` 新增 `ncm_parse_i64`(strtoll);`core/ncm_json.{h,c}` 新增 `ncm_json_get_i64_at`;`core/ncm_api.h` 两处 id 签名 `long*`→`int64_t*`;`app/ncm_app_internal.h` `song_ids[]` long→int64_t。
- **P3b** `core/ncm_req.c` eapi 局部缓冲 `full2[3072]→[4096]`、`params[2048]→[4096]`。
- **防大歌单冻 UI**:`app/ui/playlist.c` 歌单详情同样后台化(`playlist_worker` + `playlist_tick`);`appstate_load_playlist_songs` 不再覆写 `g_st.active_playlist`(防后台 worker 覆盖用户已切换的歌单)。
#### 验证记录
**宿主(同 core 库 + 真实服务器)**
- `bench/host_chain_probe`(detail→song_detail 背靠背,小歌单 `18206253175`):修前 `song_detail rc=-9`;修后 `rc=0 bn=1 song_count=1`。
- `bench/host_big`(大歌单 `581857874`):修前 big detail 溢出;修后 `detail rc=0 idn=256` + 4×64 批全 `rc=0` → `song_count=256`,首曲 `Chrono Rewind / CVRCH`。
- 宿主 eapi 复刻:未转义 c → 服务端 `200 CL:0`;转义后 `200 len=2146`。**P3 钉死**。
- `test/test_ncm_core_host` 全 PASS;`build_ncm.sh` mipsel `-Wall -Wextra` 0 warning。
**真机(`bench/verify_fixes`,一次运行,小歌单 2 请求 + 大歌单 5 请求)**
```
device_id=BD6DE5CC2DBCB617253A00438897DC6C02FFC7CFDB99DB0FA08E cookie_len=1842
[small(Docu/1)] detail rc=0 idn=1 ids[0]=2048962657
song off=0 batch=1 rc=0 bn=1 RESULT song_count=1
[0] Verve (feat. FJØRA) / Ryan Taubert
[big(5191)] detail rc=0 idn=256 ids[0]=2746301825 ← 修 P0 后正确(修前=2147483647)
song off=0/64/128/192 batch=64 全 rc=0 bn=64
RESULT song_count=256 [0] Chrono Rewind (时空回溯) / CVRCH
```
→ **P2/P3/P0/P3b 真机全消**:小歌单 1 首、大歌单 256 首(不再 Load Failed / No tracks)。
**真机启动(部署后后台运行 5s)**
```
6213257 [os.meow.ncm] INFO start
6217019 [os.meow.ncm] INFO home: playlists rc=0 count=120 uid=408996274 ← 后台 worker 完成
6218319 [os.meow.ncm] INFO exit
```
→ 启动即进 home;`home: playlists` 由 worker 与 UI 并发产出,UI 不再被 weapi 阻塞(**P1**)。修前旧二进制日志 `start→home playlists` 同链路为 UI 线程同步(`home.c` 阻塞)。
**MIPS32 溢出佐证(P0)**:修 P0 前真机 `verify_fixes`:`big detail idn=256 ids[0]=2147483647`(strtol 溢出饱和);修后 `ids[0]=2746301825`。
#### 部署 + 出入清单(本批,只改 `patch_v2/ncm_app/`)
- 部署:`/usr/data/mnt/sd_0/MeowAPPs/os.meow.ncm/ncm`(先 `ncm.new` 再 `mv`),**新 md5 `f6bd765a374effaf866784c3165e476e`**(旧 `bf6d2a79504d49cdc9b87e6f34cc6aec`)。
```
修改:
core/ncm_api.c # P2 n=1+2MB cap;P3 c 转义;P0 int64 签名
core/ncm_api.h # int64_t ids 签名
core/ncm_req.c # P3b eapi 局部缓冲提升
core/ncm_common.h/.c # 新增 ncm_parse_i64
core/ncm_json.h/.c # 新增 ncm_json_get_i64_at
app/app.c # app_on_tick 路由 home_tick/playlist_tick
app/ncm_app_internal.h # song_ids→int64_t;声明 home_tick/playlist_tick
app/ui/home.c # P1 home 加载后台化
app/ui/playlist.c # 歌单详情后台化(防大歌单冻 UI)
app/logic/appstate.c # load_playlist_songs 不覆写 active_playlist
新增:
bench/verify_fixes.c # 真机复验小歌单+大歌单链路(回归工具)
REPORT.md # 本批记录
(未动 sdk/、meow_launcherd/、vendor/;未 commit/push/刷机;未动设备系统/恢复区)
```
变更文件 md5:
```
ceb89ee9e83752208056e2eadb5330fd core/ncm_api.c 63d4af4307c5cdb3922e59dc70116407 core/ncm_api.h
cc1ab202c8d603e296f02715718a8a25 core/ncm_req.c 45efe7071b8cbff552ac89fe8b2e9495 core/ncm_common.c
8fc658f656a1004f80ab5bc1555c2048 core/ncm_common.h df46e0cd3215d359025519b4d41316d6 core/ncm_json.c
6d473afe13176230268c68ddc655cb34 core/ncm_json.h 3d2daea41204c887915d8c078543afe0 app/app.c
282c24806fe0e9c97adf464f6a5c2776 app/ncm_app_internal.h 10c3b4b1097f1b0d82550659d0a87c4f app/ui/home.c
0efef753b32eef144f2c30e4592fbfea app/ui/playlist.c 94d10237c1aa73f7273f82ff319792f5 app/logic/appstate.c
bcfd75e5eb872a751c166c456eea0184 bench/verify_fixes.c f6bd765a374effaf866784c3165e476e ncm
```
#### 诚实出入 / 已知限制
- **大歌单 load 慢(真机 detail ≈30s)**:`/v6/playlist/detail` 无论 `n`/`s` 都返回**全量 trackIds**(5191 个 ≈900KB,实测 n=1/0/100000 均 5191 条),无法用该端点分页/限流。真机 MIPS32 下载 + jsmn 解析 ≈30s(宿主 4.7s,~7× 慢,符合 MIPS vs aarch64 预期)。已用「playlist 后台化」避免冻 UI(显示 LOADING),但大歌单首屏仍 30s 级等待;要根治需改走 `/api/playlist/track/all`(服务端分页)或流式 jsmn——**建议二期**。
- **`n` 只限 `tracks` 不影响 trackIds**:`n=1` 对 ≤256 曲歌单仍完整返回全部 trackIds(idn=实际曲数,无回归);仅把「多出的完整歌曲对象」从响应里去掉,从而把 3MB 压回 ~900KB。
- **曲目截断仍 `NCM_SONG_MAX=256`**(现协议):大歌单显示前 256 首,未做滚动加载(二期)。
- **`g_st.songs[]/song_count` 由 playlist worker 写、play/IPC 线程读(§10.9 FM4 旧已知风险)**:入播放页仍在同歌单,竞态窗口极小;沿用未加锁(与项目现状一致)。
- **真机真实请求计数**:本批新增 ~20 次(detail_probe 抓样 1 次(5 请求,样本落盘)+ song_probe 1 + app_path_probe 2 + verify_fixes 2 次 11 + 启动验证 2)。已尽量「一次请求存样本、离线重放」,但修复前后复验需真实响应,属必要开销。
- **未动 sdk/、meow_launcherd/、vendor/,未 commit/push/刷机,未动设备系统/恢复区。**
+2
View File
@@ -80,6 +80,8 @@ static void app_on_tick(void)
uint64_t now = meow_ui_time_ms(); uint64_t now = meow_ui_time_ms();
if (login_is_active()) { login_tick(now); return; } if (login_is_active()) { login_tick(now); return; }
if (player_is_active()) { player_tick(now); return; } if (player_is_active()) { player_tick(now); return; }
if (home_is_active()) { home_tick(now); }
if (playlist_is_active()) { playlist_tick(now); }
chrome_tick(); chrome_tick();
} }
+2 -1
View File
@@ -120,7 +120,8 @@ int appstate_load_playlists(void)
int appstate_load_playlist_songs(int idx) int appstate_load_playlist_songs(int idx)
{ {
if (idx < 0 || idx >= g_st.playlist_count) return NCM_ERR_ARG; if (idx < 0 || idx >= g_st.playlist_count) return NCM_ERR_ARG;
g_st.active_playlist = idx; /* 不在此处写 g_st.active_playlist:调用方(playlist_open/UI)已在 UI 线程设定;
* 若后台 worker 在此改写,会覆盖用户已切换到的新歌单,造成“打开 B 却显示 A”。 */
const ncm_playlist_t *pl = &g_st.playlists[idx]; const ncm_playlist_t *pl = &g_st.playlists[idx];
int idn = 0; int idn = 0;
+5 -3
View File
@@ -103,8 +103,8 @@ typedef struct ncm_app_state {
int playlist_count; int playlist_count;
int active_playlist; /* 当前打开歌单的下标;-1=无 */ int active_playlist; /* 当前打开歌单的下标;-1=无 */
/* 当前歌单曲目 */ /* 当前歌单曲目(id 用 int64_t:网易云歌曲 id 可 >2^31,MIPS32 long 溢出) */
long song_ids[NCM_SONG_MAX]; int64_t song_ids[NCM_SONG_MAX];
ncm_song_t songs[NCM_SONG_MAX]; ncm_song_t songs[NCM_SONG_MAX];
int song_count; int song_count;
@@ -159,7 +159,8 @@ int home_on_tap(int x, int y);
void home_swipe(int dir, int dist); void home_swipe(int dir, int dist);
void home_touch_down(int x, int y); void home_touch_down(int x, int y);
void home_touch_move(int x, int y); void home_touch_move(int x, int y);
void home_reload(void); /* 刷新我的歌单 */ void home_reload(void); /* 刷新我的歌单(后台 worker,非阻塞) */
void home_tick(uint64_t now); /* UI tick:检查歌单 worker 是否完成并刷新 */
int home_go_up(void); /* 左缘返回:到底则退出 */ int home_go_up(void); /* 左缘返回:到底则退出 */
/* playlist */ /* playlist */
@@ -171,6 +172,7 @@ void playlist_touch_down(int x, int y);
void playlist_touch_move(int x, int y); void playlist_touch_move(int x, int y);
void playlist_open(int idx); /* 打开第 idx 个歌单(下拉进歌单详情) */ void playlist_open(int idx); /* 打开第 idx 个歌单(下拉进歌单详情) */
void playlist_refresh(void); void playlist_refresh(void);
void playlist_tick(uint64_t now);/* UI tick:检查歌单 worker 是否完成并刷新 */
/* player */ /* player */
meow_view *player_view_create(void); meow_view *player_view_create(void);
+66 -10
View File
@@ -9,6 +9,7 @@
#include <stdio.h> #include <stdio.h>
#include <string.h> #include <string.h>
#include <stdlib.h> #include <stdlib.h>
#include <pthread.h>
typedef struct { meow_view *view; int active; } home_ctrl; typedef struct { meow_view *view; int active; } home_ctrl;
static home_ctrl g_hc; static home_ctrl g_hc;
@@ -18,9 +19,19 @@ static meow_widget *g_list;
static ncm_rect_t r_cont, r_list; static ncm_rect_t r_cont, r_list;
static int g_empty_id, g_footer_id; static int g_empty_id, g_footer_id;
static char g_title[128]; static char g_title[128];
static int g_load_err; /* 0=ok;负=错误码 */ static volatile int g_load_err; /* 0=ok;负=错误码(worker 写,UI draw 读) */
static int g_press_row, g_press_delta, g_dragging, g_drag_last_y; static int g_press_row, g_press_delta, g_dragging, g_drag_last_y;
/* 歌单加载后台 worker:网络(ensure_uid + user_playlist)从 UI 线程挪出,
* 使冷启动/进 home 立即出界面,再异步填列表。gen 代际作“完成”信号(对齐 cover.c)。
* 单核 MIPS,gen 在数据写完后置位,UI 见 gen 变化才重建列表,避免半成品。 */
static pthread_mutex_t g_home_mu = PTHREAD_MUTEX_INITIALIZER;
static pthread_t g_home_thr;
static int g_home_thr_on; /* 1=worker 在跑(防重复 spawn) */
static volatile int g_home_gen; /* worker 完成代数 */
static int g_home_acked; /* UI 已消费的代数 */
static volatile int g_home_loading; /* 1=加载中(draw 显示 LOADING) */
static void home_drag_reset(void) static void home_drag_reset(void)
{ {
g_press_row = -1; g_press_row = -1;
@@ -52,6 +63,10 @@ static void home_draw_empty(void *ctx)
(void)ctx; (void)ctx;
if (meow_ui_activity_active() != ACT_HOME) return; if (meow_ui_activity_active() != ACT_HOME) return;
if (g_load_err == 0 && g_st.playlist_count != 0) return; if (g_load_err == 0 && g_st.playlist_count != 0) return;
if (g_home_loading) {
ncm_draw_empty(r_list, "LOADING", "fetching playlists...");
return;
}
if (g_st.logged_in == 0) if (g_st.logged_in == 0)
ncm_draw_empty(r_list, "NOT SIGNED IN", "sign in first"); ncm_draw_empty(r_list, "NOT SIGNED IN", "sign in first");
else if (g_load_err == NCM_ERR_AUTH) else if (g_load_err == NCM_ERR_AUTH)
@@ -76,16 +91,9 @@ static void home_draw_footer(void *ctx)
ncm_draw_footer(r_cont, foot); ncm_draw_footer(r_cont, foot);
} }
void home_reload(void) /* 把当前 playlist_count/昵称刷进列表 + 标题,并整帧重绘(UI 线程)。 */
static void home_apply(void)
{ {
g_load_err = 0;
int rc = 0;
if (g_host_mock) { /* 宿主/测试:跳过网络,用预置数据 */
rc = 0;
} else if (!g_st.logged_in) rc = NCM_ERR_AUTH;
else rc = appstate_load_playlists();
g_load_err = rc;
if (g_load_err != 0) g_st.playlist_count = 0;
if (g_list) { if (g_list) {
meow_listview_set_count(g_list, g_st.playlist_count); meow_listview_set_count(g_list, g_st.playlist_count);
meow_listview_set_scroll(g_list, 0); meow_listview_set_scroll(g_list, 0);
@@ -99,6 +107,54 @@ void home_reload(void)
meow_ui_paint_full(); meow_ui_paint_full();
} }
/* 后台 worker:唯一走网络的地方(ensure_uid + user_playlist),UI 线程不阻塞。 */
static void *home_worker(void *arg)
{
(void)arg;
int rc;
if (!g_st.logged_in) rc = NCM_ERR_AUTH;
else rc = appstate_load_playlists();
pthread_mutex_lock(&g_home_mu);
g_load_err = rc;
g_home_loading = 0;
g_home_gen++;
pthread_mutex_unlock(&g_home_mu);
return NULL;
}
void home_reload(void)
{
if (g_host_mock) { /* 宿主/测试:跳过网络,用预置数据(同步) */
g_load_err = 0;
if (g_load_err != 0) g_st.playlist_count = 0;
home_apply();
return;
}
if (g_home_thr_on) return; /* 已有 worker 在跑,忽略重复触发 */
g_load_err = 0;
g_st.playlist_count = 0; /* 清旧数据,先画 LOADING,worker 完成后回填 */
g_home_loading = 1;
g_home_thr_on = 1;
if (pthread_create(&g_home_thr, NULL, home_worker, NULL) != 0) {
g_home_thr_on = 0;
g_home_loading = 0;
g_load_err = NCM_ERR_IO;
}
home_apply(); /* 先画(LOADING/空态),不等网络 */
}
/* UI tick:worker 完成(gen 变化)后重建列表并重绘。 */
void home_tick(uint64_t now)
{
(void)now;
if (!g_home_thr_on) return;
if (g_home_gen == g_home_acked) return;
g_home_acked = g_home_gen; /* 消费本次完成 */
g_home_thr_on = 0; /* worker 已结束,可再次 reload */
g_home_loading = 0;
home_apply();
}
int home_is_active(void) { return g_hc.active; } int home_is_active(void) { return g_hc.active; }
int home_on_tap(int x, int y) int home_on_tap(int x, int y)
+74 -10
View File
@@ -8,6 +8,7 @@
#include <stdio.h> #include <stdio.h>
#include <string.h> #include <string.h>
#include <stdlib.h> #include <stdlib.h>
#include <pthread.h>
typedef struct { meow_view *view; int active; } playlist_ctrl; typedef struct { meow_view *view; int active; } playlist_ctrl;
static playlist_ctrl g_pc; static playlist_ctrl g_pc;
@@ -17,9 +18,19 @@ static meow_widget *g_list;
static ncm_rect_t r_cont, r_list; static ncm_rect_t r_cont, r_list;
static int g_empty_id, g_footer_id; static int g_empty_id, g_footer_id;
static char g_title[256]; static char g_title[256];
static int g_load_err; static volatile int g_load_err;
static int g_press_row, g_press_delta, g_dragging, g_drag_last_y; static int g_press_row, g_press_delta, g_dragging, g_drag_last_y;
/* 歌单详情加载后台 worker:大歌单 detail+song_detail 在设备上可达数十秒(trackIds 全量 900KB+),
* 不能占 UI 线程。worker 记录它装载的歌单下标,tick 判变;若用户已切到别的歌单则丢弃并重新加载。 */
static pthread_mutex_t g_plmu = PTHREAD_MUTEX_INITIALIZER;
static pthread_t g_plthr;
static int g_plthr_on; /* 1=worker 在跑 */
static volatile int g_plgen; /* worker 完成代数 */
static int g_placked; /* UI 已消费代数 */
static volatile int g_plloading; /* 1=加载中(draw 显示 LOADING) */
static volatile int g_plloaded_idx;/* worker 实际装载的歌单下标 */
static void pl_drag_reset(void) static void pl_drag_reset(void)
{ {
g_press_row = -1; g_press_delta = 0; g_dragging = 0; g_drag_last_y = 0; g_press_row = -1; g_press_delta = 0; g_dragging = 0; g_drag_last_y = 0;
@@ -45,6 +56,10 @@ static void pl_draw_empty(void *ctx)
(void)ctx; (void)ctx;
if (meow_ui_activity_active() != ACT_PLAYLIST) return; if (meow_ui_activity_active() != ACT_PLAYLIST) return;
if (g_load_err == 0 && g_st.song_count != 0) return; if (g_load_err == 0 && g_st.song_count != 0) return;
if (g_plloading) {
ncm_draw_empty(r_list, "LOADING", "fetching tracks...");
return;
}
if (g_load_err == NCM_ERR_AUTH) if (g_load_err == NCM_ERR_AUTH)
ncm_draw_empty(r_list, "SESSION EXPIRED", "please sign in again"); ncm_draw_empty(r_list, "SESSION EXPIRED", "please sign in again");
else if (g_load_err) else if (g_load_err)
@@ -66,16 +81,9 @@ static void pl_draw_footer(void *ctx)
ncm_draw_footer(r_cont, foot); ncm_draw_footer(r_cont, foot);
} }
void playlist_refresh(void) /* 把当前 song_count 刷进列表 + 标题,并整帧重绘(UI 线程)。 */
static void pl_apply(void)
{ {
g_load_err = 0;
int rc = NCM_ERR_ARG;
if (!g_host_mock && g_st.active_playlist >= 0)
rc = appstate_load_playlist_songs(g_st.active_playlist);
else if (g_host_mock)
rc = 0;
g_load_err = rc;
if (g_load_err != 0) g_st.song_count = 0;
if (g_list) { if (g_list) {
meow_listview_set_count(g_list, g_st.song_count); meow_listview_set_count(g_list, g_st.song_count);
meow_listview_set_scroll(g_list, 0); meow_listview_set_scroll(g_list, 0);
@@ -90,6 +98,62 @@ void playlist_refresh(void)
meow_ui_paint_full(); meow_ui_paint_full();
} }
/* 后台 worker:detail + song_detail 全在独立线程,UI 不卡(大歌单可数十秒)。 */
static void *playlist_worker(void *arg)
{
(void)arg;
int my_idx = g_st.active_playlist; /* 装载目标(UI 已设定) */
int rc;
if (g_host_mock) rc = 0;
else if (my_idx < 0) rc = NCM_ERR_ARG;
else rc = appstate_load_playlist_songs(my_idx);
pthread_mutex_lock(&g_plmu);
g_load_err = rc;
g_plloading = 0;
g_plloaded_idx = my_idx;
g_plgen++;
pthread_mutex_unlock(&g_plmu);
return NULL;
}
void playlist_refresh(void)
{
if (g_host_mock) { /* 宿主/测试:预置数据,同步 */
g_load_err = 0;
if (g_load_err != 0) g_st.song_count = 0;
pl_apply();
return;
}
if (g_plthr_on) return; /* 已有 worker 在跑 */
g_load_err = 0;
g_st.song_count = 0; /* 清旧曲目,先画 LOADING,worker 完成后回填 */
g_plloading = 1;
g_plthr_on = 1;
if (pthread_create(&g_plthr, NULL, playlist_worker, NULL) != 0) {
g_plthr_on = 0;
g_plloading = 0;
g_load_err = NCM_ERR_IO;
}
pl_apply(); /* 先画(LOADING/空态),不等网络 */
}
/* UI tick:worker 完成(gen 变化)后重建列表;若装载目标已切换则丢弃并重新加载。 */
void playlist_tick(uint64_t now)
{
(void)now;
if (!g_plthr_on) return;
if (g_plgen == g_placked) return;
g_placked = g_plgen;
g_plthr_on = 0;
g_plloading = 0;
if (g_plloaded_idx != g_st.active_playlist) {
playlist_refresh(); /* 已切别的歌单:丢弃并重新加载当前目标 */
return;
}
if (g_load_err != 0) g_st.song_count = 0;
pl_apply();
}
void playlist_open(int idx) void playlist_open(int idx)
{ {
if (idx < 0 || idx >= g_st.playlist_count) return; if (idx < 0 || idx >= g_st.playlist_count) return;
+51
View File
@@ -0,0 +1,51 @@
/* verify_fixes.c — 真机复验三修复中“数据层”两项(合并进一次运行,少请求)。
* 用与 App 相同的 core 函数:
* ① 小歌单(18206253175) detail→song_detail:期望 idn>0 且 song_detail rc=0 bn>0(修空的 c 转义)
* ② 大歌单(581857874) detail→song_detail 首批:期望 idn=256(不再 -11 FULL)且 song rc=0 bn=64
* 真实请求数:2 (small detail+song) + 2 (big detail+song) = 4。
*/
#include "ncm.h"
#include "ncm_api.h"
#include "ncm_common.h"
#include "ncm_http.h"
#include <stdio.h>
#include <string.h>
static ncm_ctx_t ncm_ctx;
static void run_chain(const char *pid, const char *label, int expect, int cap)
{
int64_t ids[256]; int idn = 0;
long t0 = (long)ncm_now_ms();
int rc = ncm_api_playlist_detail(ncm_ctx.device_id, ncm_ctx.cookie, pid,
NULL, 0, NULL, 0, ids, cap, &idn);
long dt = (long)ncm_now_ms() - t0;
printf("[%s] detail rc=%d elapsed_ms=%ld idn=%d ids[0]=%lld\n", label, rc, dt, idn, idn>0?(long long)ids[0]:-1);
ncm_song_t songs[256]; int outn = 0;
for (int off = 0; off < idn; off += 64) {
int batch = idn - off; if (batch > 64) batch = 64;
int bn = 0;
t0 = (long)ncm_now_ms();
rc = ncm_api_song_detail(ncm_ctx.device_id, ncm_ctx.cookie, &ids[off], batch, &songs[off], 256-off, &bn);
printf(" [%s] song off=%d batch=%d rc=%d elapsed_ms=%ld bn=%d\n", label, off, batch, rc, (long)ncm_now_ms()-t0, bn);
if (rc != NCM_OK) break;
outn += bn;
if (bn < batch) break;
}
printf(" [%s] RESULT song_count=%d (expect~%d)\n", label, outn, expect);
if (outn > 0) printf(" [%s] [0] %s / %s\n", label, songs[0].name, songs[0].art_name);
}
int main(int argc, char **argv)
{
const char *data_dir = argc > 1 ? argv[1] : "/usr/data/mnt/sd_0/MeowData/os.meow.ncm/data";
const char *cacert = argc > 2 ? argv[2] : "/usr/data/mnt/sd_0/MeowAPPs/os.meow.ncm/cacert.pem";
ncm_ctx_init(&ncm_ctx, data_dir);
printf("device_id=%s cookie_len=%zu\n", ncm_ctx.device_id, strlen(ncm_ctx.cookie));
ncm_http_set_verify_default(1, cacert);
run_chain("18206253175", "small(Docu/1)", 1, 256);
run_chain("581857874", "big(5191)", 256, 256);
printf("VERIFY DONE\n");
return 0;
}
+12 -6
View File
@@ -126,15 +126,18 @@ int ncm_api_user_playlist(const char *device_id, const char *cookie, const char
int ncm_api_playlist_detail(const char *device_id, const char *cookie, const char *playlist_id, int ncm_api_playlist_detail(const char *device_id, const char *cookie, const char *playlist_id,
char *name, size_t name_sz, char *cover, size_t cover_sz, char *name, size_t name_sz, char *cover, size_t cover_sz,
long *ids, int id_cap, int *out_id_n) { int64_t *ids, int id_cap, int *out_id_n) {
if (!out_id_n) return NCM_ERR_ARG; if (!out_id_n) return NCM_ERR_ARG;
*out_id_n = 0; *out_id_n = 0;
/* n=1:只让 playlist.tracks 带 1 首,避免大歌单把成百上千首完整歌曲对象塞进响应
* (n=100000 时 5191 曲歌单响应实测 3168819B → 破 1MB cap 报 -11 FULL → Load Failed)。
* trackIds 数组与 n 无关(n=1 实测 5191 条完整返回),足够取前 N 首。 */
char json[256]; char json[256];
snprintf(json, sizeof json, "{\"id\":\"%s\",\"n\":100000,\"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(device_id, cookie, "/api/v6/playlist/detail", json, NCM_CRYPT_EAPI,
NCM_RESP_MAX, &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");
@@ -153,20 +156,23 @@ int ncm_api_playlist_detail(const char *device_id, const char *cookie, const cha
int e = ncm_json_arr_get(&c.j, tid, i); int e = ncm_json_arr_get(&c.j, tid, i);
int vt = ncm_json_obj_get(&c.j, e, "id"); int vt = ncm_json_obj_get(&c.j, e, "id");
int rc2; int rc2;
ids[i] = ncm_json_get_int_at(&c.j, vt, &rc2); ids[i] = ncm_json_get_i64_at(&c.j, vt, &rc2); /* 64 位:id 可 >2^31 */
} }
*out_id_n = take; *out_id_n = take;
api_free(&c); api_free(&c);
return NCM_OK; return NCM_OK;
} }
int ncm_api_song_detail(const char *device_id, const char *cookie, const long *ids, int n, int ncm_api_song_detail(const char *device_id, const char *cookie, const int64_t *ids, int n,
ncm_song_t *out, int max, int *out_n) { ncm_song_t *out, int max, int *out_n) {
if (!out_n) return NCM_ERR_ARG; if (!out_n) return NCM_ERR_ARG;
*out_n = 0; *out_n = 0;
/* 曲目 id 数组要作为 JSON 字符串嵌进 "c",必须把内部引号转义(\"),否则服务端收到
* {"c":"[{"id":..}]"}(坏 JSON)直接回 200 Content-Length:0 → 解析失败 → 空列表。
* 实测:转义后 {"c":"[{\"id\":2048962657}]"} 服务端返回 2146B 正常 songs。 */
char clist[4096] = "[", tmp[64]; char clist[4096] = "[", tmp[64];
for (int i = 0; i < n && strlen(clist) < sizeof clist - 80; i++) { for (int i = 0; i < n && strlen(clist) < sizeof clist - 80; i++) {
snprintf(tmp, sizeof tmp, "%s{\"id\":%ld}", i ? "," : "", ids[i]); snprintf(tmp, sizeof tmp, "%s{\\\"id\\\":%lld}", i ? "," : "", (long long)ids[i]);
ncm_strlcat(clist, tmp, sizeof clist); ncm_strlcat(clist, tmp, sizeof clist);
} }
ncm_strlcat(clist, "]", sizeof clist); ncm_strlcat(clist, "]", sizeof clist);
+5 -4
View File
@@ -54,13 +54,14 @@ typedef struct ncm_play_url {
int ncm_api_user_playlist(const char *device_id, const char *cookie, const char *uid, int ncm_api_user_playlist(const char *device_id, const char *cookie, const char *uid,
int limit, int offset, ncm_playlist_t *out, int max, int *out_n); int limit, int offset, ncm_playlist_t *out, int max, int *out_n);
/* 歌单详情(eapi /v6/playlist/detail):取名字/封面 + trackIds 数组(填 ids, cap)。 */ /* 歌单详情(eapi /v6/playlist/detail):取名字/封面 + trackIds 数组(填 ids, cap)。
* ids 用 int64_t:网易云歌曲 id 可 >2^31(MIPS32 的 long 是 32 位,会溢出)。 */
int ncm_api_playlist_detail(const char *device_id, const char *cookie, const char *playlist_id, int ncm_api_playlist_detail(const char *device_id, const char *cookie, const char *playlist_id,
char *name, size_t name_sz, char *cover, size_t cover_sz, char *name, size_t name_sz, char *cover, size_t cover_sz,
long *ids, int id_cap, int *out_id_n); int64_t *ids, int id_cap, int *out_id_n);
/* 批量歌曲详情(eapi /v3/song/detail)。ids 传入曲目 id 数组。 */ /* 批量歌曲详情(eapi /v3/song/detail)。ids 传入曲目 id 数组(int64_t)。 */
int ncm_api_song_detail(const char *device_id, const char *cookie, const long *ids, int n, int ncm_api_song_detail(const char *device_id, const char *cookie, const int64_t *ids, int n,
ncm_song_t *out, int max, int *out_n); ncm_song_t *out, int max, int *out_n);
/* 播放地址(eapi /song/enhance/player/url/v1)。level 指定档位;url 可能为 null(无版权/VIP)。 */ /* 播放地址(eapi /song/enhance/player/url/v1)。level 指定档位;url 可能为 null(无版权/VIP)。 */
+11
View File
@@ -42,6 +42,17 @@ int ncm_parse_int(const char *s, long *out) {
return 0; return 0;
} }
/* 64 位解析(MIPS32 上 long=32 位;网易云歌曲 id 可达 ~18 亿以上,strtol 到 long 会饱和到
* LONG_MAX 而错)。用 strtoll 直读 int64_t,覆盖全部真实 id。 */
int ncm_parse_i64(const char *s, int64_t *out) {
if (!s || !*s) return -1;
char *end = NULL;
long long v = strtoll(s, &end, 10);
if (end == s || *end != '\0') return -1;
if (out) *out = (int64_t)v;
return 0;
}
/* 墙钟毫秒(CLOCK_REALTIME)。用于 request_id/_ntes_nnid/落盘时间戳等“取当前墙钟时间”的场合。 /* 墙钟毫秒(CLOCK_REALTIME)。用于 request_id/_ntes_nnid/落盘时间戳等“取当前墙钟时间”的场合。
* 勿用于超时/截止计算:REALTIME 会随 NTP 校时跳变,导致 deadline-now 算错(详见 ncm_http.c 注释)。 */ * 勿用于超时/截止计算:REALTIME 会随 NTP 校时跳变,导致 deadline-now 算错(详见 ncm_http.c 注释)。 */
int64_t ncm_now_ms(void) { int64_t ncm_now_ms(void) {
+1
View File
@@ -47,6 +47,7 @@ size_t ncm_strlcpy(char *dst, const char *src, size_t sz);
size_t ncm_strlcat(char *dst, const char *src, size_t sz); size_t ncm_strlcat(char *dst, const char *src, size_t sz);
int ncm_streq(const char *a, const char *b); int ncm_streq(const char *a, const char *b);
int ncm_parse_int(const char *s, long *out); /* 完整数字串,成功返回 0 */ int ncm_parse_int(const char *s, long *out); /* 完整数字串,成功返回 0 */
int ncm_parse_i64(const char *s, int64_t *out); /* 完整数字串→64 位(网易云歌曲 id 可 >2^31,MIPS32 long=32 位会溢出) */
int64_t ncm_now_ms(void); /* 墙钟毫秒(CLOCK_REALTIME),供 request_id/落盘时间戳 */ int64_t ncm_now_ms(void); /* 墙钟毫秒(CLOCK_REALTIME),供 request_id/落盘时间戳 */
int64_t ncm_monotonic_ms(void); /* 单调毫秒(CLOCK_MONOTONIC),仅用于超时/截止/间隔计时 */ int64_t ncm_monotonic_ms(void); /* 单调毫秒(CLOCK_MONOTONIC),仅用于超时/截止/间隔计时 */
void ncm_random_bytes(unsigned char *out, size_t n); /* /dev/urandom,失败则回退时钟扰动 */ void ncm_random_bytes(unsigned char *out, size_t n); /* /dev/urandom,失败则回退时钟扰动 */
+10
View File
@@ -125,6 +125,16 @@ long ncm_json_get_int_at(const ncm_json_t *j, int tok, int *rc) {
return v; return v;
} }
int64_t ncm_json_get_i64_at(const ncm_json_t *j, int tok, int *rc) {
if (rc) *rc = NCM_OK;
if (tok < 0 || tok >= j->ntoks) { if (rc) *rc = NCM_ERR_NOTFOUND; return 0; }
char s[32];
if (!ncm_json_get_str_at(j, tok, s, sizeof s)) { if (rc) *rc = NCM_ERR_NOTFOUND; return 0; }
int64_t v = 0;
if (ncm_parse_i64(s, &v) != 0) { if (rc) *rc = NCM_ERR_JSON; return 0; }
return v;
}
/* --- 路径导航 --- */ /* --- 路径导航 --- */
static int obj_get_path(const ncm_json_t *j, int cur, const char *seg_start, int klen) { static int obj_get_path(const ncm_json_t *j, int cur, const char *seg_start, int klen) {
char key[256]; char key[256];
+3
View File
@@ -8,6 +8,7 @@
#define NCM_JSON_H #define NCM_JSON_H
#include <stddef.h> #include <stddef.h>
#include <stdint.h>
#include "jsmn.h" #include "jsmn.h"
#ifdef __cplusplus #ifdef __cplusplus
@@ -42,6 +43,8 @@ int ncm_json_arr_len(const ncm_json_t *j, int tok);
const char *ncm_json_get_str_at(const ncm_json_t *j, int tok, char *out, size_t out_sz); const char *ncm_json_get_str_at(const ncm_json_t *j, int tok, char *out, size_t out_sz);
/* 取 token 处整数;primitiv/text 转 long,失败返回 0(用 rc 判错) */ /* 取 token 处整数;primitiv/text 转 long,失败返回 0(用 rc 判错) */
long ncm_json_get_int_at(const ncm_json_t *j, int tok, int *rc); long ncm_json_get_int_at(const ncm_json_t *j, int tok, int *rc);
/* 取 token 处 64 位整数(网易云歌曲 id 可 >2^31,MIPS32 long 装不下) */
int64_t ncm_json_get_i64_at(const ncm_json_t *j, int tok, int *rc);
/* 路径导航:支持 "data[0].url" / "playlist.trackIds" / "code" 等点+下标路径。 /* 路径导航:支持 "data[0].url" / "playlist.trackIds" / "code" 等点+下标路径。
* 返回目标 token 索引,失败 -1。 */ * 返回目标 token 索引,失败 -1。 */
+5 -2
View File
@@ -261,7 +261,7 @@ int ncm_req_build(const char *device_id, const char *cookie, const char *path,
hm_to_json(&h, header_json, sizeof header_json); hm_to_json(&h, header_json, sizeof header_json);
/* 注入 header 与 e_r 到加密 JSON */ /* 注入 header 与 e_r 到加密 JSON */
char full2[3072]; char full2[4096];
{ {
size_t jl = strlen(json); size_t jl = strlen(json);
if (jl >= 2 && json[0]=='{' && json[jl-1]=='}') { if (jl >= 2 && json[0]=='{' && json[jl-1]=='}') {
@@ -273,7 +273,10 @@ int ncm_req_build(const char *device_id, const char *cookie, const char *path,
} }
} }
char params[2048]; /* params 是 full2 的 AES-ECB 大写 hex(≈ plaintext×2)。
* 批量 song_detail 的 c 数组转义后 full2 ~1.6KB → hex ~3.2KB,旧 2048 溢出
* → ncm_crypto_eapi 返回 NCM_ERR_CRYPTO(实测 batch=64 时 rc=-8)。 */
char params[4096];
if (ncm_crypto_eapi(path, full2, params, sizeof params) != 0) return NCM_ERR_CRYPTO; if (ncm_crypto_eapi(path, full2, params, sizeof params) != 0) return NCM_ERR_CRYPTO;
char cookie_hdr[2048]; char cookie_hdr[2048];
Binary file not shown.
Binary file not shown.