diff --git a/patch_v2/ncm_app/REPORT.md b/patch_v2/ncm_app/REPORT.md index db49272..8d3fd28 100644 --- a/patch_v2/ncm_app/REPORT.md +++ b/patch_v2/ncm_app/REPORT.md @@ -455,3 +455,100 @@ ed2d9658bc8707a5fd689b386da472f9 app/logic/cover.c e7cef918d2be04f6d4c5a - **`appstate_ensure_uid` 仍同步跑在 UI 线程**,首次(uid 未落盘时)会占 UI 至多 ~6s 硬上限(正常 ~0.7s); 真正的“不阻塞歌单页”可由「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/刷机,未动设备系统/恢复区。** diff --git a/patch_v2/ncm_app/app/app.c b/patch_v2/ncm_app/app/app.c index ed4e824..cab3aff 100644 --- a/patch_v2/ncm_app/app/app.c +++ b/patch_v2/ncm_app/app/app.c @@ -80,6 +80,8 @@ static void app_on_tick(void) uint64_t now = meow_ui_time_ms(); if (login_is_active()) { login_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(); } diff --git a/patch_v2/ncm_app/app/logic/appstate.c b/patch_v2/ncm_app/app/logic/appstate.c index 32d1aa7..45337f2 100644 --- a/patch_v2/ncm_app/app/logic/appstate.c +++ b/patch_v2/ncm_app/app/logic/appstate.c @@ -120,7 +120,8 @@ int appstate_load_playlists(void) int appstate_load_playlist_songs(int idx) { 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]; int idn = 0; diff --git a/patch_v2/ncm_app/app/ncm_app_internal.h b/patch_v2/ncm_app/app/ncm_app_internal.h index 98b9e34..6061cd6 100644 --- a/patch_v2/ncm_app/app/ncm_app_internal.h +++ b/patch_v2/ncm_app/app/ncm_app_internal.h @@ -103,8 +103,8 @@ typedef struct ncm_app_state { int playlist_count; int active_playlist; /* 当前打开歌单的下标;-1=无 */ - /* 当前歌单曲目 */ - long song_ids[NCM_SONG_MAX]; + /* 当前歌单曲目(id 用 int64_t:网易云歌曲 id 可 >2^31,MIPS32 long 溢出) */ + int64_t song_ids[NCM_SONG_MAX]; ncm_song_t songs[NCM_SONG_MAX]; int song_count; @@ -159,7 +159,8 @@ int home_on_tap(int x, int y); void home_swipe(int dir, int dist); void home_touch_down(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); /* 左缘返回:到底则退出 */ /* playlist */ @@ -171,6 +172,7 @@ void playlist_touch_down(int x, int y); void playlist_touch_move(int x, int y); void playlist_open(int idx); /* 打开第 idx 个歌单(下拉进歌单详情) */ void playlist_refresh(void); +void playlist_tick(uint64_t now);/* UI tick:检查歌单 worker 是否完成并刷新 */ /* player */ meow_view *player_view_create(void); diff --git a/patch_v2/ncm_app/app/ui/home.c b/patch_v2/ncm_app/app/ui/home.c index bf5cb86..e6edbd0 100644 --- a/patch_v2/ncm_app/app/ui/home.c +++ b/patch_v2/ncm_app/app/ui/home.c @@ -9,6 +9,7 @@ #include #include #include +#include typedef struct { meow_view *view; int active; } home_ctrl; static home_ctrl g_hc; @@ -18,9 +19,19 @@ static meow_widget *g_list; static ncm_rect_t r_cont, r_list; static int g_empty_id, g_footer_id; 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; +/* 歌单加载后台 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) { g_press_row = -1; @@ -52,6 +63,10 @@ static void home_draw_empty(void *ctx) (void)ctx; if (meow_ui_activity_active() != ACT_HOME) 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) ncm_draw_empty(r_list, "NOT SIGNED IN", "sign in first"); 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); } -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) { meow_listview_set_count(g_list, g_st.playlist_count); meow_listview_set_scroll(g_list, 0); @@ -99,6 +107,54 @@ void home_reload(void) 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_on_tap(int x, int y) diff --git a/patch_v2/ncm_app/app/ui/playlist.c b/patch_v2/ncm_app/app/ui/playlist.c index a59d711..b8f7019 100644 --- a/patch_v2/ncm_app/app/ui/playlist.c +++ b/patch_v2/ncm_app/app/ui/playlist.c @@ -8,6 +8,7 @@ #include #include #include +#include typedef struct { meow_view *view; int active; } playlist_ctrl; static playlist_ctrl g_pc; @@ -17,9 +18,19 @@ static meow_widget *g_list; static ncm_rect_t r_cont, r_list; static int g_empty_id, g_footer_id; 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; +/* 歌单详情加载后台 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) { 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; if (meow_ui_activity_active() != ACT_PLAYLIST) 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) ncm_draw_empty(r_list, "SESSION EXPIRED", "please sign in again"); else if (g_load_err) @@ -66,16 +81,9 @@ static void pl_draw_footer(void *ctx) 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) { meow_listview_set_count(g_list, g_st.song_count); meow_listview_set_scroll(g_list, 0); @@ -90,6 +98,62 @@ void playlist_refresh(void) 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) { if (idx < 0 || idx >= g_st.playlist_count) return; diff --git a/patch_v2/ncm_app/bench/verify_fixes.c b/patch_v2/ncm_app/bench/verify_fixes.c new file mode 100644 index 0000000..0057b7b --- /dev/null +++ b/patch_v2/ncm_app/bench/verify_fixes.c @@ -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 +#include + +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; +} diff --git a/patch_v2/ncm_app/core/ncm_api.c b/patch_v2/ncm_app/core/ncm_api.c index 6877e29..7536aa7 100644 --- a/patch_v2/ncm_app/core/ncm_api.c +++ b/patch_v2/ncm_app/core/ncm_api.c @@ -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, 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; *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]; - 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); 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; 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 vt = ncm_json_obj_get(&c.j, e, "id"); 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; api_free(&c); 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) { if (!out_n) return NCM_ERR_ARG; *out_n = 0; + /* 曲目 id 数组要作为 JSON 字符串嵌进 "c",必须把内部引号转义(\"),否则服务端收到 + * {"c":"[{"id":..}]"}(坏 JSON)直接回 200 Content-Length:0 → 解析失败 → 空列表。 + * 实测:转义后 {"c":"[{\"id\":2048962657}]"} 服务端返回 2146B 正常 songs。 */ char clist[4096] = "[", tmp[64]; 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, "]", sizeof clist); diff --git a/patch_v2/ncm_app/core/ncm_api.h b/patch_v2/ncm_app/core/ncm_api.h index e6d9802..caf516f 100644 --- a/patch_v2/ncm_app/core/ncm_api.h +++ b/patch_v2/ncm_app/core/ncm_api.h @@ -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 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, 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 数组。 */ -int ncm_api_song_detail(const char *device_id, const char *cookie, const long *ids, int n, +/* 批量歌曲详情(eapi /v3/song/detail)。ids 传入曲目 id 数组(int64_t)。 */ +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); /* 播放地址(eapi /song/enhance/player/url/v1)。level 指定档位;url 可能为 null(无版权/VIP)。 */ diff --git a/patch_v2/ncm_app/core/ncm_common.c b/patch_v2/ncm_app/core/ncm_common.c index 49cc00a..1ab8b5c 100644 --- a/patch_v2/ncm_app/core/ncm_common.c +++ b/patch_v2/ncm_app/core/ncm_common.c @@ -42,6 +42,17 @@ int ncm_parse_int(const char *s, long *out) { 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/落盘时间戳等“取当前墙钟时间”的场合。 * 勿用于超时/截止计算:REALTIME 会随 NTP 校时跳变,导致 deadline-now 算错(详见 ncm_http.c 注释)。 */ int64_t ncm_now_ms(void) { diff --git a/patch_v2/ncm_app/core/ncm_common.h b/patch_v2/ncm_app/core/ncm_common.h index 6039e84..f69c579 100644 --- a/patch_v2/ncm_app/core/ncm_common.h +++ b/patch_v2/ncm_app/core/ncm_common.h @@ -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); int ncm_streq(const char *a, const char *b); 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_monotonic_ms(void); /* 单调毫秒(CLOCK_MONOTONIC),仅用于超时/截止/间隔计时 */ void ncm_random_bytes(unsigned char *out, size_t n); /* /dev/urandom,失败则回退时钟扰动 */ diff --git a/patch_v2/ncm_app/core/ncm_json.c b/patch_v2/ncm_app/core/ncm_json.c index bf98335..8f70ea6 100644 --- a/patch_v2/ncm_app/core/ncm_json.c +++ b/patch_v2/ncm_app/core/ncm_json.c @@ -125,6 +125,16 @@ long ncm_json_get_int_at(const ncm_json_t *j, int tok, int *rc) { 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) { char key[256]; diff --git a/patch_v2/ncm_app/core/ncm_json.h b/patch_v2/ncm_app/core/ncm_json.h index dc5fbf5..9bc9b72 100644 --- a/patch_v2/ncm_app/core/ncm_json.h +++ b/patch_v2/ncm_app/core/ncm_json.h @@ -8,6 +8,7 @@ #define NCM_JSON_H #include +#include #include "jsmn.h" #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); /* 取 token 处整数;primitiv/text 转 long,失败返回 0(用 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" 等点+下标路径。 * 返回目标 token 索引,失败 -1。 */ diff --git a/patch_v2/ncm_app/core/ncm_req.c b/patch_v2/ncm_app/core/ncm_req.c index 71dd470..9e63e6f 100644 --- a/patch_v2/ncm_app/core/ncm_req.c +++ b/patch_v2/ncm_app/core/ncm_req.c @@ -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); /* 注入 header 与 e_r 到加密 JSON */ - char full2[3072]; + char full2[4096]; { size_t jl = strlen(json); 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; char cookie_hdr[2048]; diff --git a/patch_v2/ncm_app/ncm b/patch_v2/ncm_app/ncm index 325976f..4ad4e01 100755 Binary files a/patch_v2/ncm_app/ncm and b/patch_v2/ncm_app/ncm differ diff --git a/patch_v2/ncm_app/ncm_host b/patch_v2/ncm_app/ncm_host deleted file mode 100755 index 51e3b19..0000000 Binary files a/patch_v2/ncm_app/ncm_host and /dev/null differ