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:
1 parent
d394c73a7c
commit
c041facd89
13 files changed
+447
-65
No files matched your search
@@ -25,15 +25,19 @@ cd /home/Aska/Project/R3ProII/patch_v2/meowpanel
|
||||
./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`,
|
||||
体积约 **202 KB**(固件阈值内)。md5:`e98aec6682b7387a035ab5f63c6777e7`。
|
||||
体积约 **201 KB**(固件阈值内)。md5:`f310a00e11729162e6960f1dd7c85259`。
|
||||
- `meowpanel_host`:aarch64 动态链接(宿主编译,仅供调试),体积约 **146 KB**。
|
||||
- 依赖:仅 libc + Linux 内核头(`linux/input.h`),零第三方库(设备无 python3/lua)。鉴权用**内联 SHA-256**(拷贝自
|
||||
`meow_verify/sha256.c` 公有领域参考写法,前缀 `mp_sha256_*` 避免符号冲突;**未改 `meow_verify/` 目录**)。
|
||||
阶段 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_*`,
|
||||
**header 为权威源**,结构体字段以它为准)。
|
||||
|
||||
@@ -325,10 +329,10 @@ http://<host>.local:8080/
|
||||
```
|
||||
patch_v2/meowpanel/
|
||||
├── 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)
|
||||
├── build_meowpanel.sh # 交叉编译 + host 分支(-Wall -Wextra 0 warning)
|
||||
├── meowpanel # 交叉产物(mipsel 静态,约 202 KB,md5 e98aec66…)
|
||||
├── meowpanel # 交叉产物(mipsel 静态,约 201 KB,md5 f310a00e…)
|
||||
├── meowpanel_host # 宿主编译(本机 ld,调试用)
|
||||
├── services.d/
|
||||
│ └── meowpanel.conf # 部署到 TF 卡 MeowOS/services.d/meowpanel.conf
|
||||
|
||||
Binary file not shown.
Binary file not shown.
@@ -3,6 +3,12 @@
|
||||
* 自包含:零第三方依赖,无 libm(DCT 用预计算余弦常数表),仅 malloc/realloc。
|
||||
* 输入 16bpp RGB565 → YCbCr(4:2:0) → DCT → 量化 → zigzag → RLE/Huffman → entropy 字节填充。
|
||||
* 供 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 <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; }
|
||||
|
||||
/* ---- 抓 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,
|
||||
int x0, int y0, double s[8][8]) {
|
||||
int x0, int y0, int s[8][8]) {
|
||||
for (int y = 0; y < 8; y++) {
|
||||
int py = y0 + y; if (py >= ph) py = ph - 1;
|
||||
const uint8_t *row = plane + (size_t)py * (size_t)pstride;
|
||||
for (int x = 0; x < 8; x++) {
|
||||
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) ---- */
|
||||
static int encode_block(mj_bw *w, const double samp[8][8], const uint8_t *q,
|
||||
/* ---- 编码一个 8x8 块(定点 DCT→量化→zigzag→RLE/Huffman) ---- */
|
||||
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) {
|
||||
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];
|
||||
double R[8][8], F[8][8];
|
||||
int R[8][8];
|
||||
for (int y = 0; y < 8; y++)
|
||||
for (int u = 0; u < 8; u++) {
|
||||
double acc = 0;
|
||||
for (int x = 0; x < 8; x++) acc += MP_COSP[u][x] * samp[y][x];
|
||||
R[u][y] = acc;
|
||||
int acc = 0;
|
||||
for (int x = 0; x < 8; x++) acc += ci[u][x] * samp[y][x];
|
||||
R[u][y] = mp_fix_round(acc);
|
||||
}
|
||||
for (int u = 0; u < 8; u++)
|
||||
for (int v = 0; v < 8; v++) {
|
||||
double acc = 0;
|
||||
for (int y = 0; y < 8; y++) acc += MP_COSP[v][y] * R[u][y];
|
||||
F[u][v] = acc;
|
||||
}
|
||||
for (int v = 0; v < 8; v++)
|
||||
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 acc = 0;
|
||||
for (int y = 0; y < 8; y++) acc += ci[v][y] * R[u][y];
|
||||
int c = mp_fix_round(acc);
|
||||
int Q = q[v * 8 + u];
|
||||
coef[v * 8 + u] = (c + (c >= 0 ? Q / 2 : -Q / 2)) / Q;
|
||||
}
|
||||
|
||||
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_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 cx = 0; cx < w; cx += 2) {
|
||||
double cb = 0, cr = 0;
|
||||
int n = 0;
|
||||
int cb = 0, cr = 0, n = 0;
|
||||
for (int dy = 0; dy < 2; dy++)
|
||||
for (int dx = 0; dx < 2; 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 G = (g6 << 2) | (g6 >> 4);
|
||||
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));
|
||||
cb += -0.168736 * R - 0.331264 * G + 0.5 * B;
|
||||
cr += 0.5 * R - 0.418688 * G - 0.081312 * B;
|
||||
Y[(size_t)py * (size_t)w + (size_t)px] = (uint8_t)((77 * R + 150 * G + 29 * B) >> 8);
|
||||
cb += (-43) * R - 85 * G + 128 * B;
|
||||
cr += 128 * R - 107 * G - 21 * B;
|
||||
n++;
|
||||
}
|
||||
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));
|
||||
Cr[k] = (uint8_t)clamp255((int)(128.0 + cr / (n ? n : 1) + 0.5));
|
||||
int dn = n ? n : 1;
|
||||
Cb[k] = (uint8_t)clamp255(128 + ((cb / dn) >> 8));
|
||||
Cr[k] = (uint8_t)clamp255(128 + ((cr / dn) >> 8));
|
||||
}
|
||||
|
||||
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 by = 0; by < 2; by++)
|
||||
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);
|
||||
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);
|
||||
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);
|
||||
if (encode_block(&bb, s, qc, hdc_c, hac_c, &pd_cr)) goto done;
|
||||
}
|
||||
|
||||
@@ -805,42 +805,59 @@
|
||||
});
|
||||
}
|
||||
|
||||
/* ---------------- 10. 实时屏幕 / 点击遥控 ---------------- */
|
||||
/* ---------------- 10. 实时屏幕 / 点击遥控 ----------------
|
||||
* 轮询改「上一帧到了再取下一帧」(链式 setTimeout,绝不堆积/重叠):
|
||||
* 每帧下载完成后按目标周期自校正 delay(周期 = max(0, interval - 请求耗时)),
|
||||
* 故名义 fps 被尊重,且请求变慢时自动退化为 1/请求耗时。 */
|
||||
let screenTimer = null, screenBusy = false, screenOn = false;
|
||||
let screenObjUrl = null, screenFps = 1;
|
||||
let screenLastMs = 0, screenMeasFps = 0; // 实测帧率(帧间间隔 EMA)
|
||||
function stopScreenPoll() {
|
||||
if (screenTimer) { clearInterval(screenTimer); screenTimer = null; }
|
||||
screenOn = false;
|
||||
if (screenTimer) { clearTimeout(screenTimer); screenTimer = null; }
|
||||
}
|
||||
function restartScreenPoll() {
|
||||
if (screenTimer) { clearInterval(screenTimer); screenTimer = null; }
|
||||
const img = $('#screen-img'); if (!img) return;
|
||||
screenOn = true;
|
||||
const ms = Math.max(500, Math.round(1000 / screenFps));
|
||||
screenTimer = setInterval(fetchScreen, ms);
|
||||
if (screenTimer) { clearTimeout(screenTimer); screenTimer = null; }
|
||||
// 请求在途时不重复启动(避免并发请求);在途帧结束后会按最新 screenFps 自续
|
||||
if (!screenBusy) screenTimer = setTimeout(fetchScreen, 0);
|
||||
}
|
||||
async function fetchScreen() {
|
||||
if (screenBusy) return;
|
||||
screenBusy = true;
|
||||
const t0 = performance.now();
|
||||
try {
|
||||
const headers = {};
|
||||
const token = storageGet('meowpanel.token');
|
||||
if (token) headers['X-MeowToken'] = token;
|
||||
const res = await fetch('/api/screen.jpeg?t=' + Date.now() + '&q=70',
|
||||
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 url = URL.createObjectURL(blob);
|
||||
const img = $('#screen-img');
|
||||
if (!img) { URL.revokeObjectURL(url); screenBusy = false; return; }
|
||||
if (!img) { URL.revokeObjectURL(url); return; }
|
||||
img.src = url;
|
||||
if (screenObjUrl) URL.revokeObjectURL(screenObjUrl);
|
||||
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) {
|
||||
$('#screen-status').textContent = '后端未连接';
|
||||
}
|
||||
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) {
|
||||
const img = $('#screen-img'); if (!img) return;
|
||||
@@ -882,8 +899,8 @@
|
||||
restartScreenPoll();
|
||||
});
|
||||
$('#screen-img').addEventListener('click', screenTap);
|
||||
// restartScreenPoll 已排程首帧(上一帧到了再取下一帧),勿再显式 fetch 以免双发
|
||||
restartScreenPoll();
|
||||
fetchScreen();
|
||||
}
|
||||
function bindScreen() {
|
||||
// 页面隐藏(切到其它标签页)时停刷;回到本页继续
|
||||
|
||||
+141
-1
@@ -782,7 +782,147 @@ c3a4aa5cd3da493d5cdb75965aaaa4fe app/ui/playlist.c 610a223232d250d2e04130
|
||||
- **topbar 图标字符潜在同病**:home/player topbar 动作标签 `⟳`(U+21BB)/`⚙`(U+2699)/`⌕`(U+2315) 走
|
||||
`meow_font_class_of`→OTHER 类,若 common 图集无对应字形同样会豆腐块;用户未报(可能在 topbar 用了符号字库槽或未留意),
|
||||
本期未动(避免改非报点区域),后续如需一并确认/替换(如改用 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 模块加锁。
|
||||
- **构建方差异**:宿主 `build_ncm_host.sh` 对 `cover.c` 有 glibc-fortify `-Wformat-truncation` 告警(**改动前即存在**,
|
||||
仅宿主 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/刷机,未动设备系统/恢复区**;部署留用户后续验收。
|
||||
@@ -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 (offset < 0) offset = 0;
|
||||
|
||||
/* 二期:改走 /api/playlist/track/all 服务端分页(REPORT §11)。
|
||||
* 相对旧 /v6/playlist/detail 的「全量 trackIds(5191 首 ≈900KB)」本端点直接按页返回歌曲
|
||||
* 对象(含 al.picUrl),无需 detail 瓶颈 + 二次 song_detail。 */
|
||||
ncm_song_t tmp[NCM_SONG_BATCH];
|
||||
int n = 0; int64_t total = 0;
|
||||
int rc = ncm_api_playlist_track_all(g_st.ctx.device_id, g_st.ctx.cookie, pl->id,
|
||||
offset, limit, tmp, NCM_SONG_BATCH, &n, &total);
|
||||
if (rc != NCM_OK) { if (offset == 0) g_st.song_count = 0; return rc; }
|
||||
/* 二期曾改走 /api/playlist/track/all 服务端分页(REPORT §12.3),但真机实测该端点
|
||||
* (eapi/weapi、v6/无v6 四种路由)全部返回 code 404「接口未找到」——NCM 服务端无此路由,
|
||||
* 故回退到 REPORT §10.11 已验证的 /api/v6/playlist/detail(trackIds) + /api/v3/song/detail 分批。
|
||||
* detail 一次取回前 NCM_SONG_MAX 首 trackIds;song_detail 分批取歌曲对象(含 al.picUrl)。
|
||||
* offset==0 重头清空并整批载入;offset>0(触底“加页”)时 detail 已一次取满,无独立分页,
|
||||
* 直接返回 NCM_OK(歌单数 ≤NCM_SONG_MAX 时已全部在列,song_count 不变)。 */
|
||||
if (offset != 0) return NCM_OK;
|
||||
|
||||
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(内存纪律) */
|
||||
int cap = NCM_SONG_MAX - offset;
|
||||
if (cap <= 0) return NCM_OK;
|
||||
if (n > cap) n = cap;
|
||||
memcpy(&g_st.songs[offset], tmp, sizeof(ncm_song_t) * (size_t)n);
|
||||
int end = offset + n;
|
||||
if (end > NCM_SONG_MAX) end = NCM_SONG_MAX;
|
||||
g_st.song_count = end;
|
||||
memset(g_st.songs, 0, sizeof g_st.songs);
|
||||
int outn = 0;
|
||||
for (int off = 0; off < idn; off += NCM_SONG_BATCH) {
|
||||
int batch = idn - off;
|
||||
if (batch > NCM_SONG_BATCH) batch = NCM_SONG_BATCH;
|
||||
int bn = 0;
|
||||
rc = ncm_api_song_detail(g_st.ctx.device_id, g_st.ctx.cookie, &g_st.song_ids[off],
|
||||
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);
|
||||
return NCM_OK;
|
||||
}
|
||||
|
||||
@@ -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 (meow_widget_hit(g_list, x, y)) {
|
||||
int row = meow_listview_hit(g_list, x, y);
|
||||
if (row < 0) { home_drag_reset(); return 1; }
|
||||
/* 误触修复(同 playlist.c):小拖动滚动列表后 release 被 SDK 判成 TAP,旧命中用“释放点 +
|
||||
* 新 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();
|
||||
if (row < 0 || was_drag) return 1;
|
||||
playlist_open(tab_global_idx(g_tab, row));
|
||||
return 1;
|
||||
}
|
||||
|
||||
@@ -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 (meow_widget_hit(g_list, x, y)) {
|
||||
int row = meow_listview_hit(g_list, x, y);
|
||||
if (row < 0) { pl_drag_reset(); return 1; }
|
||||
/* 误触修复:小拖动(<SWIPE_THRESH)会先滚动列表、release 又被 SDK 判成 TAP,旧的
|
||||
* 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();
|
||||
if (row < 0 || was_drag) return 1;
|
||||
player_play_song(row);
|
||||
return 1;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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.
Reference in new issue
Block a user