fix(sdk+launcher): 图标不渲染真根因(agent-219) — meow_ui_img.c scale_buf cover 采样把乘写成除(÷缩放比→采到源图左上角透明区,全丢); 改乘+居中裁剪偏移修正; 宿主旧测试用纯色图掩盖了该bug。+ main_menu App退出/结构变化重绘时 fb_pan_buf0_force 无条件pan回buffer0(防App翻页留buffer1致菜单残帧)
meow_ui.a→3e0ee8f7 launcher→9d0efc2f hello→9609ea6e files→90f5cf07 ncm→be8be221(均已上卡); 真机截图证据 .tmp/meowbug/fix1.png+fix_b2_after.png
This commit is contained in:
1 parent
05a19a9ea3
commit
ab4da09e75
45 files changed
+1926
-147
No files matched your search
@@ -1 +1 @@
|
||||
{"name":"网易云音乐","exec":"/usr/data/mnt/sd_0/MeowAPPs/dev.player.ncm/ncm","package":"dev.player.ncm","version":"0.1","version_code":1,"framework":1,"immersive":1,"fonts":"gbk","author":"MeowOS"}
|
||||
{"name":"网易云音乐","exec":"/usr/data/mnt/sd_0/MeowAPPs/dev.player.ncm/ncm","package":"dev.player.ncm","version":"0.1","version_code":1,"framework":1,"immersive":1,"fonts":"common","author":"MeowOS"}
|
||||
@@ -117,35 +117,53 @@ int appstate_load_playlists(void)
|
||||
return rc;
|
||||
}
|
||||
|
||||
int appstate_load_playlist_songs(int idx)
|
||||
int appstate_load_playlist_songs_page(int idx, int offset, int limit)
|
||||
{
|
||||
if (idx < 0 || idx >= g_st.playlist_count) return NCM_ERR_ARG;
|
||||
/* 不在此处写 g_st.active_playlist:调用方(playlist_open/UI)已在 UI 线程设定;
|
||||
* 若后台 worker 在此改写,会覆盖用户已切换到的新歌单,造成“打开 B 却显示 A”。 */
|
||||
const ncm_playlist_t *pl = &g_st.playlists[idx];
|
||||
if (limit <= 0 || limit > NCM_SONG_BATCH) limit = NCM_SONG_BATCH;
|
||||
if (offset < 0) offset = 0;
|
||||
|
||||
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; }
|
||||
/* 二期:改走 /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; }
|
||||
|
||||
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;
|
||||
if (offset == 0) g_st.song_count = 0; /* 重头:清空旧歌单(含此前打开的另一歌单) */
|
||||
|
||||
/* 追加到 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;
|
||||
appstate_apply_playlist_cover(idx);
|
||||
return NCM_OK;
|
||||
}
|
||||
|
||||
int appstate_load_playlist_songs(int idx)
|
||||
{
|
||||
/* 兼容入口:整批载入(内部按页循环到 NCM_SONG_MAX 或歌单尾部)。 */
|
||||
int rc, loaded = 0;
|
||||
do {
|
||||
rc = appstate_load_playlist_songs_page(idx, loaded, NCM_PLAYLIST_PAGE);
|
||||
if (rc != NCM_OK) return rc;
|
||||
int before = loaded;
|
||||
loaded = g_st.song_count;
|
||||
if (loaded - before < NCM_PLAYLIST_PAGE) break; /* 尾部:已载完 */
|
||||
if (loaded >= NCM_SONG_MAX) break;
|
||||
} while (1);
|
||||
return NCM_OK;
|
||||
}
|
||||
|
||||
void appstate_apply_playlist_cover(int idx)
|
||||
{
|
||||
if (idx < 0 || idx >= g_st.playlist_count) return;
|
||||
|
||||
@@ -16,6 +16,22 @@ static char s_txt[NCM_LYRIC_LINES][LRC_MAX_TEXT];
|
||||
static char s_tl[NCM_LYRIC_LINES][LRC_MAX_TEXT];
|
||||
static int s_n;
|
||||
|
||||
/* 复制一行歌词正文 n 字节到 dst,把「字面反斜杠 n/r」序列(服务器用 \n 把多行文本塞进一行,
|
||||
* 字面 \n 渲染成豆腐块/凌乱)替换为空格;顺带容忍 \r\n。有界 ≤max。 */
|
||||
static void copy_lrc_text(char *dst, const char *src, size_t n, size_t max)
|
||||
{
|
||||
size_t oi = 0, si = 0;
|
||||
while (si < n && oi < max - 1) {
|
||||
if (src[si] == '\\' && si + 1 < n && (src[si + 1] == 'n' || src[si + 1] == 'r')) {
|
||||
dst[oi++] = ' ';
|
||||
si += 2;
|
||||
} else {
|
||||
dst[oi++] = src[si++];
|
||||
}
|
||||
}
|
||||
dst[oi] = 0;
|
||||
}
|
||||
|
||||
static void lrc_parse(const char *lrc, int is_trans)
|
||||
{
|
||||
if (!lrc) return;
|
||||
@@ -55,14 +71,14 @@ static void lrc_parse(const char *lrc, int is_trans)
|
||||
/* 翻译行:按 first_ms 匹配到已有原文行 */
|
||||
for (int i = 0; i < s_n; i++) {
|
||||
if (s_t[i] == first_ms) {
|
||||
memcpy(s_tl[i], p, len); s_tl[i][len] = 0;
|
||||
copy_lrc_text(s_tl[i], p, len, LRC_MAX_TEXT);
|
||||
break;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (s_n < NCM_LYRIC_LINES) {
|
||||
s_t[s_n] = first_ms;
|
||||
memcpy(s_txt[s_n], p, len); s_txt[s_n][len] = 0;
|
||||
copy_lrc_text(s_txt[s_n], p, len, LRC_MAX_TEXT);
|
||||
s_tl[s_n][0] = 0;
|
||||
s_n++;
|
||||
}
|
||||
|
||||
@@ -43,6 +43,7 @@
|
||||
#define NCM_PLAYLIST_MAX 120 /* 我的歌单上限 */
|
||||
#define NCM_SONG_MAX 256 /* 单个歌单曲目上限(大歌单截断前 256 条) */
|
||||
#define NCM_SONG_BATCH 64 /* song_detail 分批每批条数 */
|
||||
#define NCM_PLAYLIST_PAGE 50 /* 歌单曲目服务端分页每页条数(/api/playlist/track/all) */
|
||||
#define NCM_LYRIC_LINES 600 /* 歌词行上限 */
|
||||
#define NCM_COVER_MAX 288*1024 /* 封面下载限幅 ~288KB */
|
||||
#define NCM_SEARCH_MAX 32 /* 搜索结果上限(v1 只显示前 32 条,无大缓冲) */
|
||||
@@ -246,7 +247,10 @@ void appstate_set_logged_in(const char *uid, const char *nickname);
|
||||
void appstate_logout(void);
|
||||
int appstate_ensure_uid(void); /* 若 uid 缺失则 login_status_check 取回;0=成功 */
|
||||
int appstate_load_playlists(void); /* 拉我的歌单 */
|
||||
int appstate_load_playlist_songs(int idx); /* 拉歌单详情+曲目详情 */
|
||||
int appstate_load_playlist_songs(int idx); /* 兼容:整批载入(内部走分页循环) */
|
||||
int appstate_load_playlist_songs_page(int idx, int offset, int limit);
|
||||
/* 服务端分页载入第 offset 页(offset=0 重头填,
|
||||
* offset>0 追加到 g_st.songs[offset..]);写 g_st.song_count */
|
||||
void appstate_apply_playlist_cover(int idx);
|
||||
void appstate_set_playing_song(int idx, const char *cover_url);
|
||||
int appstate_current_playing(void); /* 返回当前播放歌曲下标(UI) */
|
||||
@@ -258,6 +262,7 @@ const char *lyric_line_text(int i);
|
||||
const char *lyric_line_tl(int i);
|
||||
int lyric_count(void);
|
||||
void lyric_clear(void);
|
||||
int lyric_state(void); /* 0=空闲/未拉 1=加载中 2=已拉完(可能 0 行=无词) */
|
||||
|
||||
/* cover */
|
||||
void cover_request(const char *url); /* 提交目标封面 URL(后台线程下载+解码) */
|
||||
|
||||
@@ -280,21 +280,89 @@ static void *player_thread(void *arg)
|
||||
return NULL;
|
||||
}
|
||||
|
||||
/* 为当前曲目拉歌词并解析(UI 线程调):buf 有界,解析后存 lyric 模块。 */
|
||||
/* ---- 歌词后台加载(二期):UI 不阻塞 —— 持久 worker 监视当前播放歌曲,首播/切歌/自动顺延
|
||||
* 时在独立线程拉取并解析,UI 经 lyric_state() 判「加载中 / 无词」。 ----
|
||||
* 现存 race(§10.9 FM4,UI 读 lyrics 与 worker 写)为良性:数组定长、越界已防护,
|
||||
* 最坏显示一帧旧/空行,下一 tick 自愈;与 g_st.songs 写读竞态同接受。 */
|
||||
static pthread_mutex_t g_lyrmu = PTHREAD_MUTEX_INITIALIZER;
|
||||
static pthread_t g_lyr_thr;
|
||||
static int g_lyr_thr_on;
|
||||
static volatile int g_lyr_state; /* 0=空闲/未拉 1=加载中 2=已拉完 */
|
||||
static char g_lyr_song_id[32]; /* 歌词归属歌曲 id */
|
||||
|
||||
static void *lyric_worker(void *arg)
|
||||
{
|
||||
(void)arg;
|
||||
char last_id[32] = "";
|
||||
while (!g_app_quit) {
|
||||
usleep(300 * 1000);
|
||||
int idx;
|
||||
pthread_mutex_lock(&g_play.mu); idx = g_play.cur_song; pthread_mutex_unlock(&g_play.mu);
|
||||
if (idx < 0 || idx >= g_st.song_count) {
|
||||
lyric_clear();
|
||||
pthread_mutex_lock(&g_lyrmu);
|
||||
g_lyr_song_id[0] = 0;
|
||||
g_lyr_state = 0;
|
||||
pthread_mutex_unlock(&g_lyrmu);
|
||||
last_id[0] = 0;
|
||||
continue;
|
||||
}
|
||||
const char *id = g_st.songs[idx].id;
|
||||
if (!id[0]) continue;
|
||||
if (strcmp(id, last_id) == 0) continue; /* 同一曲已拉/在拉 */
|
||||
|
||||
ncm_strlcpy(last_id, id, sizeof last_id);
|
||||
lyric_clear();
|
||||
pthread_mutex_lock(&g_lyrmu);
|
||||
g_lyr_state = 1; /* 加载中 */
|
||||
pthread_mutex_unlock(&g_lyrmu);
|
||||
|
||||
char *lrc = (char *)malloc(64 * 1024);
|
||||
char *tl = (char *)malloc(32 * 1024);
|
||||
if (!lrc || !tl) {
|
||||
free(lrc); free(tl);
|
||||
pthread_mutex_lock(&g_lyrmu); g_lyr_state = 0; pthread_mutex_unlock(&g_lyrmu);
|
||||
continue;
|
||||
}
|
||||
int rc = ncm_api_lyric(g_st.ctx.device_id, g_st.ctx.cookie, id, lrc, 64 * 1024, tl, 32 * 1024);
|
||||
if (rc == NCM_OK) lyric_parse(lrc, tl);
|
||||
free(lrc); free(tl);
|
||||
|
||||
pthread_mutex_lock(&g_lyrmu);
|
||||
ncm_strlcpy(g_lyr_song_id, id, sizeof g_lyr_song_id);
|
||||
g_lyr_state = 2; /* 拉完(可能 0 行=无词) */
|
||||
pthread_mutex_unlock(&g_lyrmu);
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
|
||||
static void lyric_ensure_worker(void)
|
||||
{
|
||||
pthread_mutex_lock(&g_lyrmu);
|
||||
if (!g_lyr_thr_on) {
|
||||
g_lyr_thr_on = 1;
|
||||
if (pthread_create(&g_lyr_thr, NULL, lyric_worker, NULL) == 0) pthread_detach(g_lyr_thr);
|
||||
else g_lyr_thr_on = 0;
|
||||
}
|
||||
pthread_mutex_unlock(&g_lyrmu);
|
||||
}
|
||||
|
||||
/* 为当前曲目「异步」拉歌词:确保后台 worker 在跑即返回(UI 不阻塞网络/解析)。
|
||||
* 实际拉取+解析在 worker 线程(约 0.3s 内开始),结束置 lyric_state()=2。 */
|
||||
int player_manager_load_lyric(void)
|
||||
{
|
||||
lyric_clear();
|
||||
int idx = player_manager_playing_song();
|
||||
if (idx < 0 || idx >= g_st.song_count) return NCM_ERR_ARG;
|
||||
const ncm_song_t *s = &g_st.songs[idx];
|
||||
char *lrc = (char *)malloc(64 * 1024);
|
||||
char *tl = (char *)malloc(32 * 1024);
|
||||
if (!lrc || !tl) { free(lrc); free(tl); return NCM_ERR_OOM; }
|
||||
int rc = ncm_api_lyric(g_st.ctx.device_id, g_st.ctx.cookie, s->id, lrc, 64 * 1024, tl, 32 * 1024);
|
||||
if (rc == NCM_OK) lyric_parse(lrc, tl);
|
||||
free(lrc);
|
||||
free(tl);
|
||||
return rc;
|
||||
lyric_ensure_worker();
|
||||
return NCM_OK;
|
||||
}
|
||||
|
||||
/* 歌词加载状态(UI 判「loading / no lyric」)。 */
|
||||
int lyric_state(void)
|
||||
{
|
||||
int s;
|
||||
pthread_mutex_lock(&g_lyrmu);
|
||||
s = g_lyr_state;
|
||||
pthread_mutex_unlock(&g_lyrmu);
|
||||
return s;
|
||||
}
|
||||
|
||||
int player_manager_start(void)
|
||||
|
||||
@@ -231,7 +231,12 @@ static void plr_draw_dyn(void *ctx)
|
||||
int line_h = (g_landscape) ? 24 : 26;
|
||||
int center_y = g_lyr.y + g_lyr.h / 2;
|
||||
int lcx = g_lyr.x + g_lyr.w / 2;
|
||||
if (n == 0) { meow_ui_text_center(lcx, center_y - 10, "no lyric", 20, th->onSurfaceVariant); }
|
||||
if (n == 0) {
|
||||
/* 二期:歌词后台加载,状态区分「加载中 / 真的无词」 */
|
||||
meow_ui_text_center(lcx, center_y - 10,
|
||||
lyric_state() == 2 ? "no lyric" : "loading lyric...",
|
||||
20, th->onSurfaceVariant);
|
||||
}
|
||||
else {
|
||||
int64_t ps = player_manager_pos_ms();
|
||||
int cur2 = lyric_line_at(ps);
|
||||
@@ -249,9 +254,10 @@ static void plr_draw_dyn(void *ctx)
|
||||
}
|
||||
}
|
||||
|
||||
/* 页脚提示 */
|
||||
/* 页脚提示(用「・」U+30FB 而非 U+00B7「·」:common 字库按 GB2312 解码 A1A4 得 U+30FB,
|
||||
* 不含 U+00B7 → 后者在设备上是豆腐块。REPORT §11 二期。) */
|
||||
char foot[64];
|
||||
snprintf(foot, sizeof foot, "左右滑切歌 · 点顶部切横屏");
|
||||
snprintf(foot, sizeof foot, "左右滑切歌 ・ 点顶部切横屏");
|
||||
meow_ui_text_center(r_cont.x + r_cont.w / 2, r_cont.y + r_cont.h - APP_FOOTER_H,
|
||||
foot, 20, th->onSurfaceVariant);
|
||||
}
|
||||
@@ -297,9 +303,13 @@ void player_on_tap(int x, int y)
|
||||
void player_play_song(int idx)
|
||||
{
|
||||
if (idx < 0 || idx >= g_st.song_count) return;
|
||||
/* 二期:黑胶封面用真实专辑图(song_detail 的 al.picUrl,经 ncm_cache 缓存/限幅),
|
||||
* 无 al.picUrl(如无版权占位)时回落歌单封面。 */
|
||||
const ncm_song_t *s = &g_st.songs[idx];
|
||||
const char *cover = (s && s->al_pic[0]) ? s->al_pic : g_st.cover_url;
|
||||
player_manager_play(idx);
|
||||
appstate_set_playing_song(idx, g_st.cover_url);
|
||||
cover_request(g_st.cover_url);
|
||||
appstate_set_playing_song(idx, cover);
|
||||
cover_request(cover);
|
||||
meow_view_page_push(ACT_PLAYER);
|
||||
}
|
||||
|
||||
@@ -328,6 +338,11 @@ void player_tick(uint64_t now)
|
||||
|
||||
if (sc != g_last_song) {
|
||||
g_last_song = sc;
|
||||
/* 切歌(next/prev/自动顺延)→ 请求新曲专辑封面(黑胶盘换图) */
|
||||
if (sc >= 0) {
|
||||
const ncm_song_t *s2 = &g_st.songs[sc];
|
||||
cover_request(s2->al_pic[0] ? s2->al_pic : g_st.cover_url);
|
||||
}
|
||||
meow_ui_invalidate(g_dyn_r.x, g_dyn_r.y, g_dyn_r.w, g_dyn_r.h); /* 歌名/歌手变 */
|
||||
}
|
||||
if (cover_changed()) {
|
||||
|
||||
@@ -33,6 +33,8 @@ static volatile int g_plgen;
|
||||
static int g_placked;
|
||||
static volatile int g_plloading;
|
||||
static volatile int g_plloaded_idx;
|
||||
static volatile int g_ploffset; /* 本页起始下标(0=首屏重头;>0=触底追加) */
|
||||
static volatile int g_pl_more; /* 1=尚有下一页可加载(页满且未到 NCM_SONG_MAX) */
|
||||
|
||||
static void pl_drag_reset(void)
|
||||
{
|
||||
@@ -171,12 +173,12 @@ static void pl_draw_footer(void *ctx)
|
||||
ncm_draw_footer(r_cont, foot);
|
||||
}
|
||||
|
||||
static void pl_apply(void)
|
||||
/* apply:reset_scroll=1 时列表回顶并清选中(首屏/刷新);0=保持当前滚动(触底追加,防跳顶)。 */
|
||||
static void pl_apply(int reset_scroll)
|
||||
{
|
||||
if (g_list) {
|
||||
meow_listview_set_count(g_list, g_st.song_count);
|
||||
meow_listview_set_scroll(g_list, 0);
|
||||
meow_listview_set_sel(g_list, -1);
|
||||
if (reset_scroll) { meow_listview_set_scroll(g_list, 0); meow_listview_set_sel(g_list, -1); }
|
||||
}
|
||||
if (g_tb && g_st.active_playlist >= 0 && g_st.active_playlist < g_st.playlist_count) {
|
||||
snprintf(g_title, sizeof g_title, "%s", g_st.playlists[g_st.active_playlist].name);
|
||||
@@ -191,12 +193,17 @@ static void *playlist_worker(void *arg)
|
||||
{
|
||||
(void)arg;
|
||||
int my_idx = g_st.active_playlist;
|
||||
int off = g_ploffset;
|
||||
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);
|
||||
else rc = appstate_load_playlist_songs_page(my_idx, off, NCM_PLAYLIST_PAGE);
|
||||
pthread_mutex_lock(&g_plmu);
|
||||
g_load_err = rc;
|
||||
/* 页满(==NCM_PLAYLIST_PAGE)且未到记忆上限 → 还有下一页;否则到屏尾 */
|
||||
int loaded = (rc == NCM_OK) ? (g_st.song_count - off) : 0;
|
||||
g_pl_more = (rc == NCM_OK) && (loaded >= NCM_PLAYLIST_PAGE) &&
|
||||
(g_st.song_count < NCM_SONG_MAX);
|
||||
g_plloading = 0;
|
||||
g_plloaded_idx = my_idx;
|
||||
g_plgen++;
|
||||
@@ -204,18 +211,39 @@ static void *playlist_worker(void *arg)
|
||||
return NULL;
|
||||
}
|
||||
|
||||
void playlist_refresh(void)
|
||||
/* 触底加载下一页:仅当仍有下一页、未在载、且未到 NCM_SONG_MAX 时发。 */
|
||||
static void pl_maybe_load_more(void)
|
||||
{
|
||||
if (g_host_mock) { g_load_err = 0; pl_apply(); return; }
|
||||
if (g_plthr_on) return;
|
||||
g_load_err = 0;
|
||||
g_st.song_count = 0;
|
||||
if (g_plthr_on || g_load_err != 0 || g_host_mock) return;
|
||||
if (!g_pl_more) return;
|
||||
if (g_st.song_count >= NCM_SONG_MAX) return;
|
||||
/* 视口底接近列表底(最后一行即将可见即预取) */
|
||||
int list_px = g_st.song_count * APP_ROW_H;
|
||||
int max_scroll = list_px - r_list.h;
|
||||
if (max_scroll < 0) max_scroll = 0;
|
||||
if (meow_listview_scroll(g_list) < max_scroll - APP_ROW_H * 2) return;
|
||||
g_ploffset = g_st.song_count;
|
||||
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();
|
||||
}
|
||||
|
||||
void playlist_refresh(void)
|
||||
{
|
||||
if (g_host_mock) { g_load_err = 0; g_pl_more = 0; pl_apply(1); return; }
|
||||
if (g_plthr_on) return;
|
||||
g_load_err = 0;
|
||||
g_st.song_count = 0;
|
||||
g_ploffset = 0;
|
||||
g_pl_more = 0;
|
||||
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(1);
|
||||
}
|
||||
|
||||
void playlist_tick(uint64_t now)
|
||||
@@ -227,8 +255,11 @@ void playlist_tick(uint64_t now)
|
||||
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();
|
||||
if (g_load_err != 0) {
|
||||
if (g_ploffset == 0) g_st.song_count = 0; /* 首屏失败 → 整页空态 */
|
||||
else { ncm_snack("加载失败"); g_load_err = 0; } /* 追加失败 → 保留已载歌曲 */
|
||||
}
|
||||
pl_apply(g_ploffset == 0); /* 首屏回顶;触底追加保持滚动 */
|
||||
}
|
||||
|
||||
void playlist_open(int idx)
|
||||
@@ -294,8 +325,10 @@ void playlist_touch_move(int x, int y)
|
||||
if (dy == 0) return;
|
||||
int before = meow_listview_scroll(g_list);
|
||||
meow_listview_scroll_by(g_list, -dy);
|
||||
if (meow_listview_scroll(g_list) != before)
|
||||
if (meow_listview_scroll(g_list) != before) {
|
||||
meow_ui_invalidate(r_list.x, r_list.y, r_list.w, r_list.h);
|
||||
pl_maybe_load_more();
|
||||
}
|
||||
}
|
||||
|
||||
void playlist_swipe(int dir, int dist)
|
||||
@@ -305,8 +338,10 @@ void playlist_swipe(int dir, int dist)
|
||||
int del = (dir == MEOW_SWIPE_UP) ? dist : -dist;
|
||||
int before = meow_listview_scroll(g_list);
|
||||
meow_listview_scroll_by(g_list, del);
|
||||
if (meow_listview_scroll(g_list) != before)
|
||||
if (meow_listview_scroll(g_list) != before) {
|
||||
meow_ui_invalidate(r_list.x, r_list.y, r_list.w, r_list.h);
|
||||
pl_maybe_load_more();
|
||||
}
|
||||
}
|
||||
|
||||
static void pl_page_create(meow_view *v, void *ctx)
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
/* bench/offline_track_all.c — 二期大歌单分页 离线样本重放(不打网络)。
|
||||
*
|
||||
* 用与 App 完全相同的 core 解析函数 ncm_api_parse_track_all 解析一段合成的
|
||||
* /api/playlist/track/all 响应(songs[] 全量对象含 al.picUrl + total)。纯解析,
|
||||
* 验证:① 数组路径 songs 命中;② id >2^31 不溢出;③ 新字段 al.picUrl 解析。
|
||||
* 风控纪律:验证期 0 次真实 NCM 请求(离线样本重放)。
|
||||
* 编译(宿主):$CC_HOST -O2 -DJSMN_HEADER -Icore -I../vendor/jsmn \
|
||||
* offline_track_all.c ../out/core-host/*.o \
|
||||
* -L../out/mbedtls/host -lmbedtls -lmbedx509 -lmbedcrypto -lz -lpthread -lm -o offline_track_all
|
||||
*/
|
||||
#include "ncm.h"
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
|
||||
static int g_fail = 0;
|
||||
#define CHECK(cond, fmt, ...) do { \
|
||||
if (cond) printf(" PASS " fmt "\n", ##__VA_ARGS__); \
|
||||
else { printf(" FAIL " fmt "\n", ##__VA_ARGS__); g_fail = 1; } \
|
||||
} while (0)
|
||||
|
||||
int main(void) {
|
||||
/* 合成真实形态 track/all 响应:songs[](全 song 对象,含 al.picUrl)+ total。
|
||||
* id 用 >2^31 数字(2746301825),验 int64 不溢出;al.picUrl 验专辑封面。 */
|
||||
const char *json =
|
||||
"{\"code\":200,\"songs\":["
|
||||
"{\"id\":2746301825,\"name\":\"Chrono Rewind\",\"dt\":253000,\"fee\":1,"
|
||||
" \"ar\":[{\"name\":\"CVRCH\"}],"
|
||||
" \"al\":{\"name\":\"Rewrite\",\"picUrl\":\"http://p1.music.126.net/ab/cover1.jpg\"}},"
|
||||
"{\"id\":2048962657,\"name\":\"Verve\",\"dt\":210000,\"fee\":0,"
|
||||
" \"ar\":[{\"name\":\"Ryan Taubert\"}],"
|
||||
" \"al\":{\"name\":\"Rebuild\",\"picUrl\":\"https://p2.music.126.net/cd/cover2.jpg\"}}"
|
||||
"],\"total\":5191}";
|
||||
|
||||
ncm_song_t songs[8];
|
||||
int n = 0;
|
||||
int64_t total = 0;
|
||||
int rc = ncm_api_parse_track_all(json, strlen(json), songs, 8, &n, &total);
|
||||
CHECK(rc == NCM_OK, "parse_track_all rc=%d", rc);
|
||||
CHECK(n == 2, "parsed 2 songs (n=%d)", n);
|
||||
CHECK(total == 5191, "total=%lld", (long long)total);
|
||||
CHECK(strcmp(songs[0].name, "Chrono Rewind") == 0, "song0 name='%s'", songs[0].name);
|
||||
CHECK(strcmp(songs[0].art_name, "CVRCH") == 0, "song0 artist='%s'", songs[0].art_name);
|
||||
CHECK(strcmp(songs[0].album_name, "Rewrite") == 0, "song0 album='%s'", songs[0].album_name);
|
||||
CHECK(strcmp(songs[0].al_pic, "http://p1.music.126.net/ab/cover1.jpg") == 0,
|
||||
"song0 al_pic='%s'", songs[0].al_pic);
|
||||
CHECK(strcmp(songs[1].id, "2048962657") == 0, "song1 id='%s'(numeric->text)", songs[1].id);
|
||||
CHECK(strcmp(songs[1].al_pic, "https://p2.music.126.net/cd/cover2.jpg") == 0,
|
||||
"song1 al_pic='%s'", songs[1].al_pic);
|
||||
CHECK(songs[0].duration_ms == 253000 && songs[0].fee == 1, "song0 dt=%ld fee=%d",
|
||||
songs[0].duration_ms, songs[0].fee);
|
||||
|
||||
/* 数组路径容错:缺 songs 但嵌套 playlist.tracks 也应命中(防御真实端点变体)。 */
|
||||
const char *json2 =
|
||||
"{\"code\":200,\"playlist\":{\"trackCount\":3,\"tracks\":["
|
||||
"{\"id\":1,\"name\":\"A\",\"dt\":1000,\"fee\":0,\"ar\":[{\"name\":\"X\"}],"
|
||||
"\"al\":{\"name\":\"AL\",\"picUrl\":\"http://x/y.jpg\"}}]}}";
|
||||
int n2 = 0; int64_t t2 = 0;
|
||||
rc = ncm_api_parse_track_all(json2, strlen(json2), songs, 8, &n2, &t2);
|
||||
CHECK(rc == NCM_OK && n2 == 1, "parse variant playlist.tracks rc=%d n=%d", rc, n2);
|
||||
CHECK(t2 == 3, "variant total(trackCount)=%lld", (long long)t2);
|
||||
|
||||
printf("%s\n", g_fail ? "RESULT: FAIL" : "RESULT: PASS");
|
||||
return g_fail ? 1 : 0;
|
||||
}
|
||||
@@ -100,6 +100,38 @@ static int obj_bool(const ncm_json_t *j, int tok, const char *key) {
|
||||
return (strcmp(s, "true") == 0);
|
||||
}
|
||||
|
||||
/* 解析一处「歌曲对象数组」(song_detail / cloudsearch / playlist_track_all 共用)。
|
||||
* 元素字段:id/name/ar[0].name/al.name/al.picUrl/dt/fee。id 可为数字或字符串(均以文本拷贝,
|
||||
* 兼容 >2^31 的歌曲 id)。返回 NCM_OK,*out_n=写入数。 */
|
||||
static int parse_songs_array(const ncm_json_t *j, int arr, ncm_song_t *out, int max, int *out_n)
|
||||
{
|
||||
*out_n = 0;
|
||||
if (arr < 0 || ncm_json_type(j, arr) != JSMN_ARRAY) return NCM_ERR_NOTFOUND;
|
||||
int cnt = ncm_json_arr_len(j, arr);
|
||||
if (cnt > max) cnt = max;
|
||||
memset(out, 0, sizeof(*out) * (size_t)(cnt > 0 ? cnt : 0));
|
||||
for (int i = 0; i < cnt; i++) {
|
||||
int e = ncm_json_arr_get(j, arr, i);
|
||||
if (e < 0) break;
|
||||
obj_str(j, e, "id", out[i].id, sizeof out[i].id);
|
||||
obj_str(j, e, "name", out[i].name, sizeof out[i].name);
|
||||
out[i].duration_ms = obj_int(j, e, "dt");
|
||||
out[i].fee = (int)obj_int(j, e, "fee");
|
||||
int ar = ncm_json_obj_get(j, e, "ar");
|
||||
if (ar >= 0 && ncm_json_type(j, ar) == JSMN_ARRAY) {
|
||||
int a0 = ncm_json_arr_get(j, ar, 0);
|
||||
if (a0 >= 0) obj_str(j, a0, "name", out[i].art_name, sizeof out[i].art_name);
|
||||
}
|
||||
int al = ncm_json_obj_get(j, e, "al");
|
||||
if (al >= 0) {
|
||||
obj_str(j, al, "name", out[i].album_name, sizeof out[i].album_name);
|
||||
obj_str(j, al, "picUrl", out[i].al_pic, sizeof out[i].al_pic); /* 专辑封面(黑胶) */
|
||||
}
|
||||
}
|
||||
*out_n = cnt;
|
||||
return NCM_OK;
|
||||
}
|
||||
|
||||
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) {
|
||||
if (!out_n) return NCM_ERR_ARG;
|
||||
@@ -195,28 +227,9 @@ int ncm_api_song_detail(const char *device_id, const char *cookie, const int64_t
|
||||
if (rc != NCM_OK) return rc;
|
||||
|
||||
int arr = ncm_json_path(&c.j, "songs");
|
||||
if (arr < 0 || ncm_json_type(&c.j, arr) != JSMN_ARRAY) { api_free(&c); return NCM_ERR_NOTFOUND; }
|
||||
int cnt = ncm_json_arr_len(&c.j, arr);
|
||||
if (cnt > max) cnt = max;
|
||||
memset(out, 0, sizeof(*out) * (size_t)(cnt > 0 ? cnt : 0));
|
||||
for (int i = 0; i < cnt; i++) {
|
||||
int e = ncm_json_arr_get(&c.j, arr, i);
|
||||
if (e < 0) break;
|
||||
obj_str(&c.j, e, "id", out[i].id, sizeof out[i].id);
|
||||
obj_str(&c.j, e, "name", out[i].name, sizeof out[i].name);
|
||||
out[i].duration_ms = obj_int(&c.j, e, "dt");
|
||||
out[i].fee = (int)obj_int(&c.j, e, "fee");
|
||||
int ar = ncm_json_obj_get(&c.j, e, "ar");
|
||||
if (ar >= 0 && ncm_json_type(&c.j, ar) == JSMN_ARRAY) {
|
||||
int a0 = ncm_json_arr_get(&c.j, ar, 0);
|
||||
if (a0 >= 0) obj_str(&c.j, a0, "name", out[i].art_name, sizeof out[i].art_name);
|
||||
}
|
||||
int al = ncm_json_obj_get(&c.j, e, "al");
|
||||
if (al >= 0) obj_str(&c.j, al, "name", out[i].album_name, sizeof out[i].album_name);
|
||||
}
|
||||
*out_n = cnt;
|
||||
int rc2 = parse_songs_array(&c.j, arr, out, max, out_n);
|
||||
api_free(&c);
|
||||
return NCM_OK;
|
||||
return rc2;
|
||||
}
|
||||
|
||||
int ncm_api_song_url(const char *device_id, const char *cookie, const char *id,
|
||||
@@ -305,30 +318,69 @@ int ncm_api_cloudsearch(const char *device_id, const char *cookie, const char *k
|
||||
if (rc != NCM_OK) return rc;
|
||||
|
||||
int arr = ncm_json_path(&c.j, "result.songs");
|
||||
if (arr < 0 || ncm_json_type(&c.j, arr) != JSMN_ARRAY) { api_free(&c); return NCM_ERR_NOTFOUND; }
|
||||
int cnt = ncm_json_arr_len(&c.j, arr);
|
||||
if (cnt > max) cnt = max;
|
||||
memset(out, 0, sizeof(*out) * (size_t)(cnt > 0 ? cnt : 0));
|
||||
for (int i = 0; i < cnt; i++) {
|
||||
int e = ncm_json_arr_get(&c.j, arr, i);
|
||||
if (e < 0) break;
|
||||
obj_str(&c.j, e, "name", out[i].name, sizeof out[i].name);
|
||||
out[i].duration_ms = obj_int(&c.j, e, "dt");
|
||||
out[i].fee = (int)obj_int(&c.j, e, "fee");
|
||||
/* id 用 i64:网易云歌曲 id 可 >2^31 */
|
||||
int vt = ncm_json_obj_get(&c.j, e, "id");
|
||||
int rc2;
|
||||
int64_t idv = ncm_json_get_i64_at(&c.j, vt, &rc2);
|
||||
snprintf(out[i].id, sizeof out[i].id, "%lld", (long long)idv);
|
||||
int ar = ncm_json_obj_get(&c.j, e, "ar");
|
||||
if (ar >= 0 && ncm_json_type(&c.j, ar) == JSMN_ARRAY) {
|
||||
int a0 = ncm_json_arr_get(&c.j, ar, 0);
|
||||
if (a0 >= 0) obj_str(&c.j, a0, "name", out[i].art_name, sizeof out[i].art_name);
|
||||
}
|
||||
int al = ncm_json_obj_get(&c.j, e, "al");
|
||||
if (al >= 0) obj_str(&c.j, al, "name", out[i].album_name, sizeof out[i].album_name);
|
||||
}
|
||||
*out_n = cnt;
|
||||
int rc2 = parse_songs_array(&c.j, arr, out, max, out_n);
|
||||
api_free(&c);
|
||||
return NCM_OK;
|
||||
return rc2;
|
||||
}
|
||||
|
||||
/* 从一段 track/all 响应 JSON 串解析歌曲数组 + total(离线样本/单元测试用,不打网络)。
|
||||
* 数组取「songs → playlist.tracks → data.songs」首个命中;total 取「total → count →
|
||||
* playlist.trackCount → musicCount」,全缺则 0。 */
|
||||
int ncm_api_parse_track_all(const char *json, size_t len, ncm_song_t *out, int max,
|
||||
int *out_n, int64_t *total) {
|
||||
if (out_n) *out_n = 0;
|
||||
if (total) *total = 0;
|
||||
if (!json || len == 0 || !out || max <= 0) return NCM_ERR_ARG;
|
||||
|
||||
jsmn_parser p; jsmn_init(&p);
|
||||
int n = jsmn_parse(&p, json, len, NULL, 0);
|
||||
if (n <= 0) return NCM_ERR_JSON;
|
||||
jsmntok_t *toks = (jsmntok_t *)malloc(sizeof(jsmntok_t) * (size_t)n);
|
||||
int *parent = (int *)malloc(sizeof(int) * (size_t)n);
|
||||
if (!toks || !parent) { free(toks); free(parent); return NCM_ERR_OOM; }
|
||||
ncm_json_t j;
|
||||
int rc = ncm_json_parse(&j, json, len, toks, n, parent);
|
||||
if (rc < 0) { free(toks); free(parent); return NCM_ERR_JSON; }
|
||||
|
||||
/* 定位歌曲数组(多路径容错)。 */
|
||||
int arr = ncm_json_path(&j, "songs");
|
||||
if (arr < 0) arr = ncm_json_path(&j, "playlist.tracks");
|
||||
if (arr < 0) arr = ncm_json_path(&j, "data.songs");
|
||||
if (arr < 0 || ncm_json_type(&j, arr) != JSMN_ARRAY) { free(toks); free(parent); return NCM_ERR_NOTFOUND; }
|
||||
|
||||
long v = 0;
|
||||
if (ncm_json_path_int(&j, "total", &v) != NCM_OK && ncm_json_path_int(&j, "count", &v) != NCM_OK &&
|
||||
ncm_json_path_int(&j, "playlist.trackCount", &v) != NCM_OK && ncm_json_path_int(&j, "musicCount", &v) != NCM_OK)
|
||||
v = 0;
|
||||
if (total) *total = (int64_t)v;
|
||||
|
||||
int on = 0;
|
||||
rc = parse_songs_array(&j, arr, out, max, &on);
|
||||
if (out_n) *out_n = on;
|
||||
free(toks); free(parent);
|
||||
return rc;
|
||||
}
|
||||
|
||||
int ncm_api_playlist_track_all(const char *device_id, const char *cookie, const char *playlist_id,
|
||||
int offset, int limit, ncm_song_t *out, int max, int *out_n,
|
||||
int64_t *total) {
|
||||
if (out_n) *out_n = 0;
|
||||
if (limit <= 0) limit = 50;
|
||||
if (limit > max) limit = max;
|
||||
if (offset < 0) offset = 0;
|
||||
|
||||
char json[256];
|
||||
snprintf(json, sizeof json, "{\"id\":\"%s\",\"offset\":%d,\"limit\":%d,\"n\":%d,\"s\":8}",
|
||||
playlist_id, offset, limit, limit);
|
||||
|
||||
api_json_t c; memset(&c, 0, sizeof c);
|
||||
int rc = api_post(device_id, cookie, "/api/playlist/track/all", json, NCM_CRYPT_EAPI,
|
||||
NCM_RESP_MAX, &c);
|
||||
if (rc != NCM_OK) return rc;
|
||||
|
||||
int64_t tot = 0;
|
||||
rc = ncm_api_parse_track_all(c.resp.body, c.resp.body_len, out, max, out_n, &tot);
|
||||
if (rc == NCM_OK && total) *total = tot;
|
||||
api_free(&c);
|
||||
return rc;
|
||||
}
|
||||
@@ -38,6 +38,7 @@ typedef struct ncm_song {
|
||||
char name[256];
|
||||
char art_name[256]; /* 首个歌手名 */
|
||||
char album_name[256];
|
||||
char al_pic[512]; /* 专辑封面 picUrl(song_detail/cloudsearch/playlist_track_all 解析;播放页黑胶封面) */
|
||||
long duration_ms;
|
||||
int fee;
|
||||
} ncm_song_t;
|
||||
@@ -83,6 +84,20 @@ int ncm_api_lyric(const char *device_id, const char *cookie, const char *id,
|
||||
int ncm_api_cloudsearch(const char *device_id, const char *cookie, const char *keyword,
|
||||
ncm_song_t *out, int max, int *out_n);
|
||||
|
||||
/* 歌单曲目服务端分页(eapi /api/playlist/track/all,REPORT §11 二期)。
|
||||
* 相对 /v6/playlist/detail 的「全量 trackIds(5191 首 ≈900KB)」改进:本端点按
|
||||
* offset/limit 直接返回该页的歌曲对象(含 al.picUrl,无需再 song_detail 二次请求)。
|
||||
* 首屏 offset=0 limit=50;滚动到底继续 offset+=limit。out 填 ncm_song_t。
|
||||
* *total 返回歌单总曲数(0=未知;调用方可退化为 playlist.trackCount)。 */
|
||||
int ncm_api_playlist_track_all(const char *device_id, const char *cookie, const char *playlist_id,
|
||||
int offset, int limit, ncm_song_t *out, int max, int *out_n,
|
||||
int64_t *total);
|
||||
|
||||
/* 离线解析:从一段 track/all 响应 JSON 串解析歌曲数组(含 al.picUrl)+ total。
|
||||
* 网络版内部同一解析器;供「宿主离线样本重放」与单元测试用,不打网络。 */
|
||||
int ncm_api_parse_track_all(const char *json, size_t len, ncm_song_t *out, int max,
|
||||
int *out_n, int64_t *total);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
Binary file not shown.
Binary file not shown.
@@ -222,6 +222,36 @@ static void test_network(void) {
|
||||
if (!net_ok) printf(" SKIP network live checks (host unreachable or TLS CA missing)\n");
|
||||
}
|
||||
|
||||
/* ---------- 二期:/api/playlist/track/all 服务端分页 离线解析 ---------- */
|
||||
static void test_track_all_parse(void) {
|
||||
printf("== track_all 离线解析(二期)==\n");
|
||||
/* 合成一个真实形态的 track/all 响应:songs[](全 song 对象,含 al.picUrl)+ total。
|
||||
* id 为 >2^31 数字,验证 int64 不溢出;al.picUrl 验证专辑封面字段。 */
|
||||
const char *json =
|
||||
"{\"code\":200,\"songs\":["
|
||||
"{\"id\":2746301825,\"name\":\"Chrono Rewind\",\"dt\":253000,\"fee\":1,"
|
||||
" \"ar\":[{\"name\":\"CVRCH\"}],"
|
||||
" \"al\":{\"name\":\"Rewrite\",\"picUrl\":\"http://p1.music.126.net/ab/cover1.jpg\"}},"
|
||||
"{\"id\":2048962657,\"name\":\"Verve\",\"dt\":210000,\"fee\":0,"
|
||||
" \"ar\":[{\"name\":\"Ryan Taubert\"}],"
|
||||
" \"al\":{\"name\":\"Rebuild\",\"picUrl\":\"https://p2.music.126.net/cd/cover2.jpg\"}}"
|
||||
"],\"total\":5191}";
|
||||
ncm_song_t songs[8];
|
||||
int n = 0;
|
||||
int64_t total = 0;
|
||||
int rc = ncm_api_parse_track_all(json, strlen(json), songs, 8, &n, &total);
|
||||
CHECKRC(rc, "parse_track_all rc");
|
||||
CHECK(n == 2, "parsed 2 songs (n=%d)", n);
|
||||
CHECK(total == 5191, "total=%lld", (long long)total);
|
||||
CHECK(strcmp(songs[0].name, "Chrono Rewind") == 0, "song0 name");
|
||||
CHECK(strcmp(songs[0].art_name, "CVRCH") == 0, "song0 artist");
|
||||
CHECK(strcmp(songs[0].album_name, "Rewrite") == 0, "song0 album");
|
||||
CHECK(strcmp(songs[0].al_pic, "http://p1.music.126.net/ab/cover1.jpg") == 0, "song0 al_pic");
|
||||
CHECK(strcmp(songs[1].id, "2048962657") == 0, "song1 id(numeric->text)");
|
||||
CHECK(strcmp(songs[1].al_pic, "https://p2.music.126.net/cd/cover2.jpg") == 0, "song1 al_pic");
|
||||
CHECK(songs[0].duration_ms == 253000 && songs[0].fee == 1, "song0 dt/fee");
|
||||
}
|
||||
|
||||
int main(int argc, char **argv) {
|
||||
const char *data_dir = (argc > 1) ? argv[1] : "/tmp/ncm_test";
|
||||
printf("ncm_core self-test, data_dir=%s\n", data_dir);
|
||||
@@ -236,6 +266,7 @@ int main(int argc, char **argv) {
|
||||
test_cache();
|
||||
test_qr();
|
||||
test_network();
|
||||
test_track_all_parse();
|
||||
|
||||
printf("\n%s\n", g_fail ? "== RESULT: FAIL ==" : "== RESULT: PASS ==");
|
||||
return g_fail ? 1 : 0;
|
||||
|
||||
Binary file not shown.
Reference in new issue
Block a user