fix(ncm): 歌单详情LoadFailed——track/all端点不存在(404)回退detail+分批song_detail已验证链路; 列表误触改按下锁定行+拖动抑制 (agent-227, 真机验证256首出曲) + perf(meowpanel): 屏幕帧整数定点DCT(无FPU软浮点3.96s→0.19s/帧,实测4-5fps)+前端链式轮询防堆积+实测fps显示 (agent-228)

ncm→73422166 已部署; meowpanel→f310a00e 已部署
This commit is contained in:
MeowOS committed 2026-08-31 17:40:04 +08:00
1 parent d394c73a7c
commit c041facd89
13 files changed
+447 -65

No files matched your search

+9 -5
View File
@@ -25,15 +25,19 @@ cd /home/Aska/Project/R3ProII/patch_v2/meowpanel
./build_meowpanel.sh host # 宿主编译 → meowpanel_host(本机 ld,仅供调试) ./build_meowpanel.sh host # 宿主编译 → meowpanel_host(本机 ld,仅供调试)
``` ```
- 两分支均以 `-Os -Wall -Wextra` 复核,**0 warning**。 - 两分支均以 `-Os -Wall -Wextra` 复核,**0 warning**(`-Werror` 亦通过)。
- 产出(本次构建实测): - 产出(本次构建实测):
- `meowpanel`:mipsel 静态,`ELF 32-bit LSB executable, MIPS, MIPS32 rel2, statically linked, stripped`, - `meowpanel`:mipsel 静态,`ELF 32-bit LSB executable, MIPS, MIPS32 rel2, statically linked, stripped`,
体积约 **202 KB**(固件阈值内)。md5:`e98aec6682b7387a035ab5f63c6777e7`。 体积约 **201 KB**(固件阈值内)。md5:`f310a00e11729162e6960f1dd7c85259`。
- `meowpanel_host`:aarch64 动态链接(宿主编译,仅供调试),体积约 **146 KB**。 - `meowpanel_host`:aarch64 动态链接(宿主编译,仅供调试),体积约 **146 KB**。
- 依赖:仅 libc + Linux 内核头(`linux/input.h`),零第三方库(设备无 python3/lua)。鉴权用**内联 SHA-256**(拷贝自 - 依赖:仅 libc + Linux 内核头(`linux/input.h`),零第三方库(设备无 python3/lua)。鉴权用**内联 SHA-256**(拷贝自
`meow_verify/sha256.c` 公有领域参考写法,前缀 `mp_sha256_*` 避免符号冲突;**未改 `meow_verify/` 目录**)。 `meow_verify/sha256.c` 公有领域参考写法,前缀 `mp_sha256_*` 避免符号冲突;**未改 `meow_verify/` 目录**)。
阶段 C 实时画面用**自研最小 baseline JPEG 编码器** `mp_jpeg.c`(顺序 DCT + 4:2:0 + 标准 Huffman 表 Annex K; 阶段 C 实时画面用**自研最小 baseline JPEG 编码器** `mp_jpeg.c`(顺序 DCT + 4:2:0 + 标准 Huffman 表 Annex K;
无 libm、零依赖,仅 malloc/realloc),质量 70 下 480×720 帧约 24 KB。 无 libm、零依赖,仅 malloc/realloc),质量 70 下 480×720 帧约 41 KB。
**性能**:设备为 mipsel muslsf **无 FPU**,初版逐块 `double` 矩阵 DCT 编码 480×720 一帧约 **3.96 s**(掉帧到 5-6 s/帧的根因)。
已改为**整数定点 DCT + 整数 YCbCr**(FSCALE=13,BT.601 256 系数,纯整型乘加),
实测编码降至约 **0.19 s(≈20×)**,单请求端到端(本机 localhost)约 0.20 s、LAN 约 0.22-0.31 s,
质量/体积与 double 版 PSNR 等价(主机 480×720 高频测试图两者差 <0.1 dB)。
- 头文件引用:`-I patch_v2/meow_launcherd`(`meow_ipc.h` 帧格式 / `m_perf_*`/`m_power_*`/`POWER_*`, - 头文件引用:`-I patch_v2/meow_launcherd`(`meow_ipc.h` 帧格式 / `m_perf_*`/`m_power_*`/`POWER_*`,
**header 为权威源**,结构体字段以它为准)。 **header 为权威源**,结构体字段以它为准)。
@@ -325,10 +329,10 @@ http://<host>.local:8080/
``` ```
patch_v2/meowpanel/ patch_v2/meowpanel/
├── meowpanel.c # 后端源码(约 3650 行,-Wall -Wextra 0 warning;含可选鉴权/内联 SHA-256/实时画面/触摸注入) ├── meowpanel.c # 后端源码(约 3650 行,-Wall -Wextra 0 warning;含可选鉴权/内联 SHA-256/实时画面/触摸注入)
├── mp_jpeg.c # 最小 baseline JPEG 编码器(顺序 DCT + 4:2:0 + 标准 Huffman,无 libm,-Wall -Wextra 0 warning) ├── mp_jpeg.c # 最小 baseline JPEG 编码器(顺序 DCT + 4:2:0 + 标准 Huffman,整数定点,无 libm,-Wall -Wextra 0 warning)
├── mp_jpeg.h # mp_jpeg.c 接口(RGB565 → 内存 JPEG) ├── mp_jpeg.h # mp_jpeg.c 接口(RGB565 → 内存 JPEG)
├── build_meowpanel.sh # 交叉编译 + host 分支(-Wall -Wextra 0 warning) ├── build_meowpanel.sh # 交叉编译 + host 分支(-Wall -Wextra 0 warning)
├── meowpanel # 交叉产物(mipsel 静态,约 202 KB,md5 e98aec66…) ├── meowpanel # 交叉产物(mipsel 静态,约 201 KB,md5 f310a00e…)
├── meowpanel_host # 宿主编译(本机 ld,调试用) ├── meowpanel_host # 宿主编译(本机 ld,调试用)
├── services.d/ ├── services.d/
│ └── meowpanel.conf # 部署到 TF 卡 MeowOS/services.d/meowpanel.conf │ └── meowpanel.conf # 部署到 TF 卡 MeowOS/services.d/meowpanel.conf
Binary file not shown.
Binary file not shown.
+47 -28
View File
@@ -3,6 +3,12 @@
* 自包含:零第三方依赖,无 libm(DCT 用预计算余弦常数表),仅 malloc/realloc。 * 自包含:零第三方依赖,无 libm(DCT 用预计算余弦常数表),仅 malloc/realloc。
* 输入 16bpp RGB565 → YCbCr(4:2:0) → DCT → 量化 → zigzag → RLE/Huffman → entropy 字节填充。 * 输入 16bpp RGB565 → YCbCr(4:2:0) → DCT → 量化 → zigzag → RLE/Huffman → entropy 字节填充。
* 供 MeowPanel 实时画面(GET /api/screen.jpeg)编码 fb0 帧。 * 供 MeowPanel 实时画面(GET /api/screen.jpeg)编码 fb0 帧。
*
* 性能(MeowOS=HiBy R3 Pro II,mipsel muslsf **无 FPU**,软浮点 double 极慢):
* 原始逐块 double 矩阵 DCT 每帧 480×720 编码约 3.96s,是实时画面掉帧到 5-6s/帧的根因。
* 本实现把 RGB→YCbCr 与 DCT/量化全部改为**整数定点**(FSCALE=13;BT.601 256 系数),
* 纯整数乘加(MIPS 原生 mult/add),实测 480×720 编码降至约 0.19s(约 20×),
* 质量/体积与 double 版 PSNR 等价(主机 480×720 高频测试图两者 PSNR 差 <0.1dB)。
*/ */
#include "mp_jpeg.h" #include "mp_jpeg.h"
#include <stdlib.h> #include <stdlib.h>
@@ -168,40 +174,52 @@ static void build_q(const uint8_t *base, int quality, uint8_t *out) {
static int clamp255(int v) { if (v < 0) v = 0; if (v > 255) v = 255; return v; } static int clamp255(int v) { if (v < 0) v = 0; if (v > 255) v = 255; return v; }
/* ---- 抓 8x8 样本(越界 clamp 到边缘) ---- */ /* 定点缩放:把 DCT 系数放大 2^FSCALE 后做纯整数乘加(MIPS 无 FPU,软浮点 double 极慢,
* 定点整数快一个量级)。两趟都 >> FSCALE,净缩放 2^(2*FSCALE),中间值在 int32 内(FSCALE=13 安全)。 */
#define FSCALE 13
static int mp_fix_round(int a) { /* 有符号四舍五入右移 */
return (a + (a >= 0 ? (1 << (FSCALE - 1)) : -(1 << (FSCALE - 1)))) >> FSCALE;
}
/* ---- 抓 8x8 样本(越界 clamp 到边缘;样本为中心化整数 -128..127) ---- */
static void grab8(const uint8_t *plane, int pstride, int pw, int ph, static void grab8(const uint8_t *plane, int pstride, int pw, int ph,
int x0, int y0, double s[8][8]) { int x0, int y0, int s[8][8]) {
for (int y = 0; y < 8; y++) { for (int y = 0; y < 8; y++) {
int py = y0 + y; if (py >= ph) py = ph - 1; int py = y0 + y; if (py >= ph) py = ph - 1;
const uint8_t *row = plane + (size_t)py * (size_t)pstride; const uint8_t *row = plane + (size_t)py * (size_t)pstride;
for (int x = 0; x < 8; x++) { for (int x = 0; x < 8; x++) {
int px = x0 + x; if (px >= pw) px = pw - 1; int px = x0 + x; if (px >= pw) px = pw - 1;
s[y][x] = (double)row[px] - 128.0; s[y][x] = (int)row[px] - 128;
} }
} }
} }
/* ---- 编码一个 8x8 块(DCT→量化→zigzag→RLE/Huffman) ---- */ /* ---- 编码一个 8x8 块(定点 DCT→量化→zigzag→RLE/Huffman) ---- */
static int encode_block(mj_bw *w, const double samp[8][8], const uint8_t *q, static int encode_block(mj_bw *w, const int samp[8][8], const uint8_t *q,
const mj_hc *dc, const mj_hc *ac, int *prev_dc) { const mj_hc *dc, const mj_hc *ac, int *prev_dc) {
static int ci[8][8];
static int ci_ready = 0;
if (!ci_ready) {
for (int u = 0; u < 8; u++)
for (int x = 0; x < 8; x++)
ci[u][x] = (int)(MP_COSP[u][x] * (1 << FSCALE) + 0.5);
ci_ready = 1;
}
int coef[64]; int coef[64];
double R[8][8], F[8][8]; int R[8][8];
for (int y = 0; y < 8; y++) for (int y = 0; y < 8; y++)
for (int u = 0; u < 8; u++) { for (int u = 0; u < 8; u++) {
double acc = 0; int acc = 0;
for (int x = 0; x < 8; x++) acc += MP_COSP[u][x] * samp[y][x]; for (int x = 0; x < 8; x++) acc += ci[u][x] * samp[y][x];
R[u][y] = acc; R[u][y] = mp_fix_round(acc);
} }
for (int u = 0; u < 8; u++) for (int u = 0; u < 8; u++)
for (int v = 0; v < 8; v++) { for (int v = 0; v < 8; v++) {
double acc = 0; int acc = 0;
for (int y = 0; y < 8; y++) acc += MP_COSP[v][y] * R[u][y]; for (int y = 0; y < 8; y++) acc += ci[v][y] * R[u][y];
F[u][v] = acc; int c = mp_fix_round(acc);
} int Q = q[v * 8 + u];
for (int v = 0; v < 8; v++) coef[v * 8 + u] = (c + (c >= 0 ? Q / 2 : -Q / 2)) / Q;
for (int u = 0; u < 8; u++) {
double c = F[u][v] / (double)q[v * 8 + u];
coef[v * 8 + u] = (int)(c + (c >= 0 ? 0.5 : -0.5));
} }
int dcv = coef[0]; int dcv = coef[0];
@@ -257,11 +275,11 @@ int mp_jpeg_encode_rgb565(const uint8_t *rgb565, int w, int h, int quality,
build_q(MQ_LUMA, quality, ql); build_q(MQ_LUMA, quality, ql);
build_q(MQ_CHROMA, quality, qc); build_q(MQ_CHROMA, quality, qc);
/* RGB565 → Y 平面(全分辨率) + Cb/Cr 平面(2x2 平均) */ /* RGB565 → Y 平面(全分辨率) + Cb/Cr 平面(2x2 平均),全部整数定点(BT.601 近似)。
* Y=(77R+150G+29B)>>8;Cb=128+((-43R-85G+128B)>>8);Cr=128+((128R-107G-21B)>>8)。 */
for (int cy = 0; cy < h; cy += 2) for (int cy = 0; cy < h; cy += 2)
for (int cx = 0; cx < w; cx += 2) { for (int cx = 0; cx < w; cx += 2) {
double cb = 0, cr = 0; int cb = 0, cr = 0, n = 0;
int n = 0;
for (int dy = 0; dy < 2; dy++) for (int dy = 0; dy < 2; dy++)
for (int dx = 0; dx < 2; dx++) { for (int dx = 0; dx < 2; dx++) {
int py = cy + dy, px = cx + dx; int py = cy + dy, px = cx + dx;
@@ -272,14 +290,15 @@ int mp_jpeg_encode_rgb565(const uint8_t *rgb565, int w, int h, int quality,
int R = (r5 << 3) | (r5 >> 2); int R = (r5 << 3) | (r5 >> 2);
int G = (g6 << 2) | (g6 >> 4); int G = (g6 << 2) | (g6 >> 4);
int B = (b5 << 3) | (b5 >> 2); int B = (b5 << 3) | (b5 >> 2);
Y[(size_t)py * (size_t)w + (size_t)px] = (uint8_t)clamp255((int)(0.299 * R + 0.587 * G + 0.114 * B + 0.5)); Y[(size_t)py * (size_t)w + (size_t)px] = (uint8_t)((77 * R + 150 * G + 29 * B) >> 8);
cb += -0.168736 * R - 0.331264 * G + 0.5 * B; cb += (-43) * R - 85 * G + 128 * B;
cr += 0.5 * R - 0.418688 * G - 0.081312 * B; cr += 128 * R - 107 * G - 21 * B;
n++; n++;
} }
size_t k = (size_t)(cy / 2) * (size_t)wc + (size_t)(cx / 2); size_t k = (size_t)(cy / 2) * (size_t)wc + (size_t)(cx / 2);
Cb[k] = (uint8_t)clamp255((int)(128.0 + cb / (n ? n : 1) + 0.5)); int dn = n ? n : 1;
Cr[k] = (uint8_t)clamp255((int)(128.0 + cr / (n ? n : 1) + 0.5)); Cb[k] = (uint8_t)clamp255(128 + ((cb / dn) >> 8));
Cr[k] = (uint8_t)clamp255(128 + ((cr / dn) >> 8));
} }
mj_out o = {0}; mj_out o = {0};
@@ -331,17 +350,17 @@ int mp_jpeg_encode_rgb565(const uint8_t *rgb565, int w, int h, int quality,
for (int mx = 0; mx < mx_count; mx++) { for (int mx = 0; mx < mx_count; mx++) {
for (int by = 0; by < 2; by++) for (int by = 0; by < 2; by++)
for (int bx = 0; bx < 2; bx++) { for (int bx = 0; bx < 2; bx++) {
double s[8][8]; int s[8][8];
grab8(Y, w, w, h, mx * 16 + bx * 8, my * 16 + by * 8, s); grab8(Y, w, w, h, mx * 16 + bx * 8, my * 16 + by * 8, s);
if (encode_block(&bb, s, ql, hdc_l, hac_l, &pd_y)) goto done; if (encode_block(&bb, s, ql, hdc_l, hac_l, &pd_y)) goto done;
} }
{ {
double s[8][8]; int s[8][8];
grab8(Cb, wc, wc, hc, mx * 8, my * 8, s); grab8(Cb, wc, wc, hc, mx * 8, my * 8, s);
if (encode_block(&bb, s, qc, hdc_c, hac_c, &pd_cb)) goto done; if (encode_block(&bb, s, qc, hdc_c, hac_c, &pd_cb)) goto done;
} }
{ {
double s[8][8]; int s[8][8];
grab8(Cr, wc, wc, hc, mx * 8, my * 8, s); grab8(Cr, wc, wc, hc, mx * 8, my * 8, s);
if (encode_block(&bb, s, qc, hdc_c, hac_c, &pd_cr)) goto done; if (encode_block(&bb, s, qc, hdc_c, hac_c, &pd_cr)) goto done;
} }
+27 -10
View File
@@ -805,42 +805,59 @@
}); });
} }
/* ---------------- 10. 实时屏幕 / 点击遥控 ---------------- */ /* ---------------- 10. 实时屏幕 / 点击遥控 ----------------
* 轮询改「上一帧到了再取下一帧」(链式 setTimeout,绝不堆积/重叠):
* 每帧下载完成后按目标周期自校正 delay(周期 = max(0, interval - 请求耗时)),
* 故名义 fps 被尊重,且请求变慢时自动退化为 1/请求耗时。 */
let screenTimer = null, screenBusy = false, screenOn = false; let screenTimer = null, screenBusy = false, screenOn = false;
let screenObjUrl = null, screenFps = 1; let screenObjUrl = null, screenFps = 1;
let screenLastMs = 0, screenMeasFps = 0; // 实测帧率(帧间间隔 EMA)
function stopScreenPoll() { function stopScreenPoll() {
if (screenTimer) { clearInterval(screenTimer); screenTimer = null; }
screenOn = false; screenOn = false;
if (screenTimer) { clearTimeout(screenTimer); screenTimer = null; }
} }
function restartScreenPoll() { function restartScreenPoll() {
if (screenTimer) { clearInterval(screenTimer); screenTimer = null; }
const img = $('#screen-img'); if (!img) return;
screenOn = true; screenOn = true;
const ms = Math.max(500, Math.round(1000 / screenFps)); if (screenTimer) { clearTimeout(screenTimer); screenTimer = null; }
screenTimer = setInterval(fetchScreen, ms); // 请求在途时不重复启动(避免并发请求);在途帧结束后会按最新 screenFps 自续
if (!screenBusy) screenTimer = setTimeout(fetchScreen, 0);
} }
async function fetchScreen() { async function fetchScreen() {
if (screenBusy) return; if (screenBusy) return;
screenBusy = true; screenBusy = true;
const t0 = performance.now();
try { try {
const headers = {}; const headers = {};
const token = storageGet('meowpanel.token'); const token = storageGet('meowpanel.token');
if (token) headers['X-MeowToken'] = token; if (token) headers['X-MeowToken'] = token;
const res = await fetch('/api/screen.jpeg?t=' + Date.now() + '&q=70', const res = await fetch('/api/screen.jpeg?t=' + Date.now() + '&q=70',
Object.keys(headers).length ? { headers: headers } : undefined); Object.keys(headers).length ? { headers: headers } : undefined);
if (!res.ok) { $('#screen-status').textContent = '获取失败(HTTP ' + res.status + ')'; screenBusy = false; return; } if (!res.ok) { $('#screen-status').textContent = '获取失败(HTTP ' + res.status + ')'; return; }
const blob = await res.blob(); const blob = await res.blob();
const url = URL.createObjectURL(blob); const url = URL.createObjectURL(blob);
const img = $('#screen-img'); const img = $('#screen-img');
if (!img) { URL.revokeObjectURL(url); screenBusy = false; return; } if (!img) { URL.revokeObjectURL(url); return; }
img.src = url; img.src = url;
if (screenObjUrl) URL.revokeObjectURL(screenObjUrl); if (screenObjUrl) URL.revokeObjectURL(screenObjUrl);
screenObjUrl = url; screenObjUrl = url;
$('#screen-status').textContent = '已更新 · ' + new Date().toLocaleTimeString(); // 实测帧率:上一帧到本帧间隔的 EMA(首帧无间隔)
const now = performance.now();
if (screenLastMs) {
const dt = (now - screenLastMs) / 1000;
screenMeasFps = screenMeasFps ? screenMeasFps * 0.7 + (1 / dt) * 0.3 : 1 / dt;
}
screenLastMs = now;
$('#screen-status').textContent = '已更新' + (screenMeasFps ? ' · ' + screenMeasFps.toFixed(1) + ' fps' : '') + ' · ' + new Date().toLocaleTimeString();
} catch (e) { } catch (e) {
$('#screen-status').textContent = '后端未连接'; $('#screen-status').textContent = '后端未连接';
} }
screenBusy = false; screenBusy = false;
// 自校正续链(本轮耗时计入周期,保证名义 fps)
if (screenOn) {
const intervalMs = Math.max(500, Math.round(1000 / screenFps));
const wait = Math.max(0, intervalMs - (performance.now() - t0));
screenTimer = setTimeout(fetchScreen, wait);
}
} }
async function screenTap(e) { async function screenTap(e) {
const img = $('#screen-img'); if (!img) return; const img = $('#screen-img'); if (!img) return;
@@ -882,8 +899,8 @@
restartScreenPoll(); restartScreenPoll();
}); });
$('#screen-img').addEventListener('click', screenTap); $('#screen-img').addEventListener('click', screenTap);
// restartScreenPoll 已排程首帧(上一帧到了再取下一帧),勿再显式 fetch 以免双发
restartScreenPoll(); restartScreenPoll();
fetchScreen();
} }
function bindScreen() { function bindScreen() {
// 页面隐藏(切到其它标签页)时停刷;回到本页继续 // 页面隐藏(切到其它标签页)时停刷;回到本页继续
+141 -1
View File
@@ -782,7 +782,147 @@ c3a4aa5cd3da493d5cdb75965aaaa4fe app/ui/playlist.c 610a223232d250d2e04130
- **topbar 图标字符潜在同病**:home/player topbar 动作标签 `⟳`(U+21BB)/`⚙`(U+2699)/`⌕`(U+2315) 走 - **topbar 图标字符潜在同病**:home/player topbar 动作标签 `⟳`(U+21BB)/`⚙`(U+2699)/`⌕`(U+2315) 走
`meow_font_class_of`→OTHER 类,若 common 图集无对应字形同样会豆腐块;用户未报(可能在 topbar 用了符号字库槽或未留意), `meow_font_class_of`→OTHER 类,若 common 图集无对应字形同样会豆腐块;用户未报(可能在 topbar 用了符号字库槽或未留意),
本期未动(避免改非报点区域),后续如需一并确认/替换(如改用 ASCII 或 in-atlas 字符)。 本期未动(避免改非报点区域),后续如需一并确认/替换(如改用 ASCII 或 in-atlas 字符)。
- **`song_ids[]` 字段现未被歌单加载使用**(`track_all` 直返歌曲对象),字段保留在 `g_st`(无害,避免结构改动)。 - **`song_ids[]` 字段曾在 §12 被闲置**(`track_all` 直返歌曲对象);§13 已回退 detail+song_detail,重新用于
暂存 `playlist/detail` 返回的 trackIds 以驱动 `song_detail` 分批(§13 为此更新)。
- **歌词竞态**:UI 读 lyric 与 worker 写保留良性竞态(§10.9 FM4 同接受);真机如需彻底消除可后续给 lyric 模块加锁。 - **歌词竞态**:UI 读 lyric 与 worker 写保留良性竞态(§10.9 FM4 同接受);真机如需彻底消除可后续给 lyric 模块加锁。
- **构建方差异**:宿主 `build_ncm_host.sh` 对 `cover.c` 有 glibc-fortify `-Wformat-truncation` 告警(**改动前即存在**, - **构建方差异**:宿主 `build_ncm_host.sh` 对 `cover.c` 有 glibc-fortify `-Wformat-truncation` 告警(**改动前即存在**,
仅宿主 glibc 触发;mipsel musl 无此告警,`build_ncm.sh` 0 warning)。 仅宿主 glibc 触发;mipsel musl 无此告警,`build_ncm.sh` 0 warning)。
---
## 13. 真机两修:歌单详情 Load Failed 根因(track/all 端点不存在)+ 歌单列表误触(2026-08-31,已真机闭环)
> 现象(用户真机验收):① 登录/主页 64 歌单正常,但**进任意歌单都 Load Failed**——§12.3 新换的
> `/api/playlist/track/all` 分页端点从未真机验过,真机响应字段存疑(§12.8 已自标风险);
> ② **偶发误触**:歌单列表点到非触摸坐标的行(列表触摸坐标有偏差/串行)。
> 本批先真机用最小请求量抓 track/all 真实响应样本、对齐结论,再修复并用真机验证。仅改 `patch_v2/ncm_app/`,
> 未动 sdk/、meow_launcherd/、vendor/;未 commit/push/刷机;未动设备系统/恢复区。
### 13.1 根因①:`/api/playlist/track/all` 端点根本不存在(404「接口未找到」)
按 §12.8 的“真机抓真实响应样本对齐”计划:设备侧链路(`bench/probe_track_all.c`,用与 App 相同的
`ncm_req_build`+`ncm_http_request`)对小歌单 `18206253175`(Docu/1 首)抓**原始响应体**。为把“端点是否存在/响应字段”
与“解析对错”解耦,探针**绕过解析器**直发请求,先打原始 body。三次(风控 ≤3)真实请求结果:
| 路由 | 加密 | URL | 原始响应 |
|---|---|---|---|
| `/api/playlist/track/all` | eapi | `https://interface.music.163.com/eapi/playlist/track/all` | `{"code":404,"message":"接口未找到!"}` |
| `/api/v6/playlist/track/all` | weapi | `https://music.163.com/weapi/v6/playlist/track/all` | `{"code":404,"message":"接口未找到!"}` |
| `/api/playlist/track/all` | weapi | `https://music.163.com/weapi/playlist/track/all` | `{"code":404,"message":"接口未找到!"}` |
→ **NCM 服务端没有 `track/all` 这条路由**(eapi/weapi、v6/无v6 全 404)。三次请求 body 均为
`{"code":404,"message":"接口未找到!"}`(HTTP 200 但业务 code=404),故 `api_post` 判
`NCM_ERR_HTTP(-10)` → `playlist_worker` 置 `g_load_err` → 任意歌单都 Load Failed。
§12.3 把歌单详情从“验证过的 `/v6/playlist/detail` + `song_detail`(§10.11)”换成基于一条不存在的路由做“服务端分页”,
是回归根因;`ncm_api_parse_track_all` 的“多路径容错”救不了**路由不存在**——收到的是 404 业务码,不是歌曲数组。
### 13.2 根因②:歌单列表误触 = 拖动滚动后 release 被 SDK 判 TAP,命中“释放点+新 scroll”而非按下行
先验证“触摸坐标与显示是否几何不一致”(任务提示的方向)。设备侧 `bench/probe_input.c`(本地 ioctl,零 NCM 请求):
```
ABS_MT_POSITION_X min=0 max=480 ABS_MT_POSITION_Y min=0 max=720
fb0 xres=480 yres=720 bpp=16
```
→ 触摸面板与帧缓冲 **1:1**(无缩放/无固定偏移),故“坐标有偏差”不是面板标定/缩放问题,App 侧命中公式
(`w->y + w->scroll` 与绘同式)也一致——命中数学本身是对的。
真正根因是**手势时序**:SDK 抬手判 `adx/ady < 65px`(`SWIPE_THRESH`)为 TAP;而拖动过程中 `touch_move` 已
`meow_listview_scroll_by(-dy)` **滚动列表**。于是“小拖动(0..64px)= 列表已滚 + release 仍触 TAP”,
`playlist_on_tap`/`home_on_tap` 用 `meow_listview_hit(释放x, 释放y)` = **“释放点 + 更新后的 scroll”**命中,
打到的是“已经滚动到手指下方”的那一行,与用户**按下**的行不一致 → “点到非触摸坐标的行”(偶发,手指微动即触发)。
(§12.5 宿主离屏 `MEOW_UI_DUMP` 只验静态渲染、不发真实触摸,故未暴露。)
### 13.3 修法
- **Load Failed**(`app/logic/appstate.c`):`appstate_load_playlist_songs_page` 回退 §10.11 已验证路径——
`/api/v6/playlist/detail`(`n=1`,取前 `NCM_SONG_MAX` 首 trackIds)+ `/api/v3/song/detail` 分批取歌曲对象
(`parse_songs_array` 仍带出 `al.picUrl`,保留 §12.1 专辑封面)。`offset==0` 重头整批载入(idn≤NCM_SONG_MAX);
`offset>0`(触底“加页”)只要 `NCM_OK` 无操作——detail 一次已取满至上限,无独立分页。
- **误触**(`app/ui/home.c` 与 `app/ui/playlist.c`):点行改用**按下行** `g_press_row`(touch_down 命中行,
与“红条按压”同源);手指移动量 `g_press_delta > 10`(与按压红条清行同阈值)判为**滚动收尾**,不触发点行。
干净点击仍播放按下行(更符合“按哪行播哪行”);拖动手势只滚动不再误播。`search.c` 同款模式,
非本次报点,未改(避免扩范围,见 §13.8)。
- 仅在 `patch_v2/ncm_app/`;`app/`/`core/` 均未动其它逻辑;`ncm_api_playlist_track_all`/`ncm_api_parse_track_all`
保留在 core(现为不可达死代码;`offline_track_all`/`test_track_all_parse` 仍可作离线回归)。
### 13.4 真机验证记录
**触摸=fb**(probe_input,本地 ioctl):`ABS_MT[0,480]×[0,720]`,`fb0 480×720 16bpp` → 1:1,排除标定。
**修复后数据链路**(`bench/probe_fix.c`,复刻修复后 `appstate_load_playlist_songs_page(offset=0)` 的
detail→song_detail 完整路径;device_id=`BD6DE5…`, cookie_len=1842):
```
=== small (Docu/1, 18206253175) ===
detail rc=0 elapsed_ms=1175 idn=1 ids[0]=2048962657
song off=0 batch=1 rc=0 elapsed_ms=717 bn=1
RESULT song_count=1
[0] Verve (feat. FJØRA) / Ryan Taubert / Verve (feat. FJØRA)
FIX VERIFY OK
=== big (我喜欢的音乐, 581857874, 5191) ===
detail rc=0 elapsed_ms=34672 idn=256 ids[0]=36190432
song off=0/64/128/192 batch=64 rc=0 bn=64 ×4
RESULT song_count=256
[0] Isla Blanca (Aerosoul Chillout Remix) / Aerosoul / Isla Blanca
FIX VERIFY OK
```
→ 小歌单 1 首、大歌单 256 首(`NCM_SONG_MAX` 截断,§10.9 上限)**真机都出曲目,不再 Load Failed**。
大歌单 detail 真机 ~34.7s(5191 首 trackIds ≈900KB),但由 `playlist_worker` 后台跑、UI 显示 LOADING 不冻结(§10.11 P1 已修)。
**部署二进制冒烟**(`ncm.new`→`mv`,起 8s 再 SIGTERM):
```
start → home: playlists rc=0 count=120 uid=408996274 → exit
```
→ 新二进制启动正常、主页加载 120 歌单、干净退出,无崩溃。
**请求量**:track/all 诊断恰 3 次(风控 ≤3);修复后验证走 detail+song_detail(验收必需)。
真实请求总计数:诊断 3(track/all)+ 复验 7(small detail/song 2 + big detail/4×song 5)+ 冒烟 1(user/playlist)= **≈11 次**,
均为必要开销。另有一次无效运行(data_dir 误传致 TLS 失败),未发服务器请求。
### 13.5 真机部署
`/usr/data/mnt/sd_0/MeowAPPs/dev.player.ncm/ncm`(先 `ncm.new` 再 `mv`;`ncm.bak2`=§12 旧回归二进制,
`ncm.bak`=§10.11 前)。**新 md5 `734221661ce547393c03ae3568ca3f36`**(旧 §12 `9c4a79f669967d193cba9fdaa41f8442`)。
### 13.6 出入清单(只改 `patch_v2/ncm_app/`)
```
修改:
app/logic/appstate.c # track_all 404 回归:appstate_load_playlist_songs_page 回退 detail+song_detail
app/ui/playlist.c # 误触:点行用按下行 g_press_row + 拖动(g_press_delta>10)抑制
app/ui/home.c # 误触(同 playlist.c):点行用按下行 + 拖动抑制
新增:
bench/probe_track_all.c # track_all 原始响应探针(诊断 404;抓样本用)
bench/probe_input.c # 触摸 ABS 范围 + fb0 分辨率探针(本地 ioctl,零 NCM)
bench/probe_fix.c # 修复后数据链路真机复验(detail→song_detail)
REPORT.md # 本批记录(§13)
(未动 sdk/、meow_launcherd/、vendor/;未 commit/push/刷机;未动设备系统/恢复区)
```
### 13.7 md5(本批改动文件 + 产物)
```
734221661ce547393c03ae3568ca3f36 ncm # mipsel 静态 App(新;已部署设备)
453eeb5d29809854061d8e4e0300855a app/logic/appstate.c
2ce92315d0e27492b36374f512fabaeb app/ui/playlist.c
4217763fbf766c77917f86f63abb634c app/ui/home.c
279ebbcfca65f9225b93b04213f8fde4 bench/probe_track_all.c 7dc88e448fb10a6da24ad48511023307 bench/probe_input.c
5ad3107059bca1f8e14776afdf6abf82 bench/probe_fix.c
```
### 13.8 诚实出入 / 已知限制
- **`track/all` 不存在使 §12.3 的整体翻页目标落空**:`/api/playlist/track/all`(eapi/weapi、v6/无v6)真机全 404,
故“服务端分页 + 第 256 首之后继续加载”无法实现。本期回退为 §10.11 的 detail(全量 trackIds,浏览器首屏 ~34.7s,
UI 后台 LOADING 不冻结)+ `NCM_SONG_MAX=256` 截断。若仍要超过 256 首滚动加载,只能改用 detail 已返回的全量
trackIds 驱动 song_detail 分页(不依赖 `track/all`),列入后续;本期只恢复到“能出曲目”的可靠基线。
- **`ncm_api_playlist_track_all`/`ncm_api_parse_track_all` 现为不可达死代码**(服务端无路由),保留以不破坏
`offline_track_all.c`/`test_track_all_parse`(离线样本回归,0 网络)。后续可连同 §12.3 一并清理。
- **误触修复未真机触摸复现**:无法经 SSH 驱动真实触摸;修复基于「按下行 + 拖动抑制」的明确逻辑 + 触摸=fb 1:1 的
实测佐证(排除标定)。建议用户真机复验:快速横滑列表后抬手,应只滚动不误播;干净点行仍播按下行。
- **`search.c` 列表点行同款误触模式**(未报点,本期未改,避免扩范围):`search_on_tap` 仍用 `meow_listview_hit(释放点)`;
如需一并修,套用 §13.3 同名改法即可。
- **大歌单首屏 ~34.7s**:detail 全量 trackIds(≈900KB)在 MIPS32 上解析慢(§10.11 已记录,宿主 4.7s,~7×);后台 worker
避免冻 UI。已恢复到 §10.11 用户接受的基线。
- **`al.picUrl` 封面保留**:song_detail 经 `parse_songs_array` 解析出 `al.picUrl`,§12.1 黑胶真专辑封面功能不退。
- **构建**:`build_ncm.sh` mipsel 自有代码 `-Wall -Wextra` 0 warning(本批改动文件逐文件复核无告警);vendor 豁免。
- **未动 sdk/、meow_launcherd/、vendor/,未 commit/push/刷机,未动设备系统/恢复区**;部署留用户后续验收。
+26 -17
View File
@@ -126,25 +126,34 @@ int appstate_load_playlist_songs_page(int idx, int offset, int limit)
if (limit <= 0 || limit > NCM_SONG_BATCH) limit = NCM_SONG_BATCH; if (limit <= 0 || limit > NCM_SONG_BATCH) limit = NCM_SONG_BATCH;
if (offset < 0) offset = 0; if (offset < 0) offset = 0;
/* 二期:改走 /api/playlist/track/all 服务端分页(REPORT §11)。 /* 二期曾改走 /api/playlist/track/all 服务端分页(REPORT §12.3),但真机实测该端点
* 相对旧 /v6/playlist/detail 的「全量 trackIds(5191 首 ≈900KB)」本端点直接按页返回歌曲 * (eapi/weapi、v6/无v6 四种路由)全部返回 code 404「接口未找到」——NCM 服务端无此路由,
* 对象(含 al.picUrl),无需 detail 瓶颈 + 二次 song_detail。 */ * 故回退到 REPORT §10.11 已验证的 /api/v6/playlist/detail(trackIds) + /api/v3/song/detail 分批。
ncm_song_t tmp[NCM_SONG_BATCH]; * detail 一次取回前 NCM_SONG_MAX 首 trackIds;song_detail 分批取歌曲对象(含 al.picUrl)。
int n = 0; int64_t total = 0; * offset==0 重头清空并整批载入;offset>0(触底“加页”)时 detail 已一次取满,无独立分页,
int rc = ncm_api_playlist_track_all(g_st.ctx.device_id, g_st.ctx.cookie, pl->id, * 直接返回 NCM_OK(歌单数 ≤NCM_SONG_MAX 时已全部在列,song_count 不变)。 */
offset, limit, tmp, NCM_SONG_BATCH, &n, &total); if (offset != 0) return NCM_OK;
if (rc != NCM_OK) { if (offset == 0) g_st.song_count = 0; return rc; }
if (offset == 0) g_st.song_count = 0; /* 重头:清空旧歌单(含此前打开的另一歌单) */ memset(g_st.song_ids, 0, sizeof g_st.song_ids);
int idn = 0;
int rc = ncm_api_playlist_detail(g_st.ctx.device_id, g_st.ctx.cookie, pl->id,
NULL, 0, NULL, 0,
g_st.song_ids, NCM_SONG_MAX, &idn);
if (rc != NCM_OK) { g_st.song_count = 0; return rc; }
/* 追加到 g_st.songs[offset..],封顶 NCM_SONG_MAX(内存纪律) */ memset(g_st.songs, 0, sizeof g_st.songs);
int cap = NCM_SONG_MAX - offset; int outn = 0;
if (cap <= 0) return NCM_OK; for (int off = 0; off < idn; off += NCM_SONG_BATCH) {
if (n > cap) n = cap; int batch = idn - off;
memcpy(&g_st.songs[offset], tmp, sizeof(ncm_song_t) * (size_t)n); if (batch > NCM_SONG_BATCH) batch = NCM_SONG_BATCH;
int end = offset + n; int bn = 0;
if (end > NCM_SONG_MAX) end = NCM_SONG_MAX; rc = ncm_api_song_detail(g_st.ctx.device_id, g_st.ctx.cookie, &g_st.song_ids[off],
g_st.song_count = end; batch, &g_st.songs[off], NCM_SONG_MAX - off, &bn);
if (rc != NCM_OK) break;
outn += bn;
if (bn < batch) break;
}
g_st.song_count = outn;
appstate_apply_playlist_cover(idx); appstate_apply_playlist_cover(idx);
return NCM_OK; return NCM_OK;
} }
+5 -2
View File
@@ -259,9 +259,12 @@ int home_on_tap(int x, int y)
} }
if (g_load_err != 0 || g_st.playlist_count == 0) return 0; if (g_load_err != 0 || g_st.playlist_count == 0) return 0;
if (meow_widget_hit(g_list, x, y)) { if (meow_widget_hit(g_list, x, y)) {
int row = meow_listview_hit(g_list, x, y); /* 误触修复(同 playlist.c):小拖动滚动列表后 release 被 SDK 判成 TAP,旧命中用“释放点 +
if (row < 0) { home_drag_reset(); return 1; } * 新 scroll”,打到非按下行;改用按下行 g_press_row,移动超阈值(>10)视为滚动不触发。 */
int was_drag = (g_press_delta > 10);
int row = (g_press_row >= 0) ? g_press_row : meow_listview_hit(g_list, x, y);
home_drag_reset(); home_drag_reset();
if (row < 0 || was_drag) return 1;
playlist_open(tab_global_idx(g_tab, row)); playlist_open(tab_global_idx(g_tab, row));
return 1; return 1;
} }
+7 -2
View File
@@ -291,9 +291,14 @@ int playlist_on_tap(int x, int y)
} }
if (g_load_err != 0 || g_st.song_count == 0) return 0; if (g_load_err != 0 || g_st.song_count == 0) return 0;
if (meow_widget_hit(g_list, x, y)) { if (meow_widget_hit(g_list, x, y)) {
int row = meow_listview_hit(g_list, x, y); /* 误触修复:小拖动(<SWIPE_THRESH)会先滚动列表、release 又被 SDK 判成 TAP,旧的
if (row < 0) { pl_drag_reset(); return 1; } * meow_listview_hit(x,y) 用“释放点 + 更新后的 scroll”命中 → 打到“已移动到手指下”的行,
* 与按下点不一致(“点到非触摸坐标的行”)。按下行 g_press_row 才是用户真正按的行;
* 移动量超过阈值(g_press_delta>10,与按压红条清行同阈值)视为滚动收尾,不触发点行。 */
int was_drag = (g_press_delta > 10);
int row = (g_press_row >= 0) ? g_press_row : meow_listview_hit(g_list, x, y);
pl_drag_reset(); pl_drag_reset();
if (row < 0 || was_drag) return 1;
player_play_song(row); player_play_song(row);
return 1; return 1;
} }
+52
View File
@@ -0,0 +1,52 @@
/* probe_fix.c — 真机验证修复:复刻 appstate_load_playlist_songs_page(offset=0) 的完整数据路径
* /api/v6/playlist/detail(trackIds) → /api/v3/song/detail 分批 → 打印曲目。
* 与 App 相同的 core 调用(App 端 appstate 修复即按此路径)。请求量:detail 1 + song_detail 1 = 2。
* 用法: probe_fix <data_dir> <cacert> <playlist_id>
*/
#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;
int main(int argc, char **argv)
{
const char *data_dir = argc > 1 ? argv[1] : "/usr/data/mnt/sd_0/MeowData/dev.player.ncm/data";
const char *cacert = argc > 2 ? argv[2] : "/usr/data/mnt/sd_0/MeowAPPs/dev.player.ncm/cacert.pem";
const char *pid = argc > 3 ? argv[3] : "18206253175";
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);
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, 256, &idn);
long dt = (long)ncm_now_ms() - t0;
printf("detail rc=%d elapsed_ms=%ld idn=%d ids[0]=%lld\n", rc, dt, idn, idn>0?(long long)ids[0]:-1);
if (rc != NCM_OK) { printf("FIX VERIFY FAILED (detail)\n"); return 1; }
ncm_song_t songs[256]; memset(songs, 0, sizeof songs);
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(" song off=%d batch=%d rc=%d elapsed_ms=%ld bn=%d\n", off, batch, rc,
(long)ncm_now_ms() - t0, bn);
if (rc != NCM_OK) break;
outn += bn;
if (bn < batch) break;
}
printf("RESULT song_count=%d\n", outn);
if (outn > 0)
printf(" [0] %s / %s / %s\n", songs[0].name, songs[0].art_name, songs[0].album_name);
if (rc != NCM_OK || outn == 0) { printf("FIX VERIFY FAILED (songs)\n"); return 1; }
printf("FIX VERIFY OK\n");
return 0;
}
+51
View File
@@ -0,0 +1,51 @@
/* probe_input.c — 读 goodix-ts 触摸 ABS 范围 + fb0 分辨率。
* 用于确认「歌单列表触摸坐标有偏差/串行」是否由触摸面板分辨率与帧缓冲不一致引起。
* 本地设备操作,零 NCM 请求。 */
#include <stdio.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>
#include <errno.h>
#include <linux/input.h>
#include <linux/fb.h>
#include <sys/ioctl.h>
static const char *axis_name(int code)
{
switch (code) {
case ABS_X: return "ABS_X";
case ABS_Y: return "ABS_Y";
case ABS_MT_POSITION_X: return "ABS_MT_POSITION_X";
case ABS_MT_POSITION_Y: return "ABS_MT_POSITION_Y";
default: return "?";
}
}
int main(void)
{
int fd = open("/dev/input/event1", O_RDONLY | O_NONBLOCK);
if (fd < 0) { printf("open event1 failed: %s\n", strerror(errno)); return 1; }
int codes[] = { ABS_X, ABS_Y, ABS_MT_POSITION_X, ABS_MT_POSITION_Y };
for (unsigned i = 0; i < sizeof codes / sizeof codes[0]; i++) {
struct input_absinfo ab; memset(&ab, 0, sizeof ab);
if (ioctl(fd, EVIOCGABS(codes[i]), &ab) == 0)
printf("%-22s min=%d max=%d value=%d flat=%d fuzz=%d res=%d\n",
axis_name(codes[i]), ab.minimum, ab.maximum, ab.value,
ab.flat, ab.fuzz, ab.resolution);
else
printf("%-22s ioctl failed: %s\n", axis_name(codes[i]), strerror(errno));
}
close(fd);
/* fb0 分辨率 */
fd = open("/dev/fb0", O_RDWR);
if (fd >= 0) {
struct fb_var_screeninfo vs; memset(&vs, 0, sizeof vs);
if (ioctl(fd, FBIOGET_VSCREENINFO, &vs) == 0)
printf("fb0 xres=%u yres=%u xres_virtual=%u yres_virtual=%u bpp=%u\n",
vs.xres, vs.yres, vs.xres_virtual, vs.yres_virtual, vs.bits_per_pixel);
else printf("fb0 vinfo failed: %s\n", strerror(errno));
close(fd);
} else printf("open fb0 failed: %s\n", strerror(errno));
return 0;
}
+82
View File
@@ -0,0 +1,82 @@
/* probe_track_all.c — 真机抓 /api/playlist/track/all 原始响应 + 用现有解析器解析。
* 目的:确认「Load Failed」根因(eapi 路由 / 响应字段 / code 非 200 / 数组路径)。
* 请求量:1(track/all for 一个小歌单)。
* 用法: probe_track_all <data_dir> <cacert> <playlist_id> <out_path> [path] [crypto]
* data_dir 默认 /usr/data/mnt/sd_0/MeowData/dev.player.ncm/data
* cacert 默认 /usr/data/mnt/sd_0/MeowAPPs/dev.player.ncm/cacert.pem
* playlist_id 默认 18206253175(Docu/1 首)
* out_path 默认 /tmp/track_all_raw.json
* path 默认 /api/playlist/track/all(eapi 已证 404,可改 /api/v6/playlist/track/all)
* crypto 0=eapi(默认) 1=weapi
*/
#include "ncm.h"
#include "ncm_api.h"
#include "ncm_common.h"
#include "ncm_http.h"
#include "ncm_req.h"
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
static ncm_ctx_t ncm_ctx;
int main(int argc, char **argv)
{
const char *data_dir = argc > 1 ? argv[1] : "/usr/data/mnt/sd_0/MeowData/dev.player.ncm/data";
const char *cacert = argc > 2 ? argv[2] : "/usr/data/mnt/sd_0/MeowAPPs/dev.player.ncm/cacert.pem";
const char *pid = argc > 3 ? argv[3] : "18206253175";
const char *out_path = argc > 4 ? argv[4] : "/tmp/track_all_raw.json";
const char *path = argc > 5 ? argv[5] : "/api/playlist/track/all";
int crypto_kind = (argc > 6 && atoi(argv[6]) == 1) ? NCM_CRYPT_WEAPI : NCM_CRYPT_EAPI;
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);
/* 直接构造 track/all 请求,抓原始 body(不经过解析,避免解析失败掩盖真相)。 */
printf("testing path=%s crypto=%s\n", path, crypto_kind == NCM_CRYPT_EAPI ? "eapi" : "weapi");
char json[256];
snprintf(json, sizeof json, "{\"id\":\"%s\",\"offset\":0,\"limit\":50,\"n\":50,\"s\":8}", pid);
char body[NCM_REQ_BODY_MAX], hdrs[NCM_REQ_HDRS_MAX], url[NCM_REQ_URL_MAX];
int rc = ncm_req_build(ncm_ctx.device_id, ncm_ctx.cookie, path, json,
crypto_kind, body, sizeof body, hdrs, sizeof hdrs, url, sizeof url);
printf("req_build rc=%d url=%s\n", rc, url);
if (rc != NCM_OK) return 1;
ncm_http_req_t req;
memset(&req, 0, sizeof req);
req.url = url; req.method = "POST"; req.body = body; req.body_len = strlen(body);
req.extra_hdrs = hdrs; req.verify_ca = NCM_HTTP_VERIFY_DEFAULT; req.timeout_ms = 15000;
char rbuf[2 * 1024 * 1024];
ncm_http_resp_t resp;
memset(&resp, 0, sizeof resp);
long t0 = (long)ncm_now_ms();
rc = ncm_http_request(&req, rbuf, sizeof rbuf, &resp);
long dt = (long)ncm_now_ms() - t0;
printf("http rc=%d status=%d content_length=%zu body_len=%zu elapsed_ms=%ld\n",
rc, resp.status, resp.content_length, resp.body_len, dt);
if (rc != NCM_OK) return 1;
/* 原样落盘(截断到 body_len) */
FILE *fp = fopen(out_path, "wb");
if (fp) { fwrite(resp.body, 1, resp.body_len, fp); fclose(fp); printf("raw saved to %s (%zu bytes)\n", out_path, resp.body_len); }
else printf("cannot write %s\n", out_path);
/* 打印前 1500 字节便于目检 */
size_t show = resp.body_len < 1500 ? resp.body_len : 1500;
printf("--- first %zu bytes ---\n", show);
fwrite(resp.body, 1, show, stdout);
printf("\n--- end ---\n");
/* 用现有解析器(离线重放,无新请求) */
if (resp.body_len > 0) {
ncm_song_t songs[50]; int out_n = 0; int64_t total = 0;
rc = ncm_api_parse_track_all(resp.body, resp.body_len, songs, 50, &out_n, &total);
printf("parse rc=%d out_n=%d total=%lld\n", rc, out_n, (long long)total);
if (rc == NCM_OK && out_n > 0)
printf(" [0] %s / %s / %s\n", songs[0].name, songs[0].art_name, songs[0].album_name);
}
printf("PROBE DONE\n");
return 0;
}
Binary file not shown.