feat(ncm): 三期第一批 — 歌单真分页(取消256截断)+专辑封面ncm_cache+歌词后台加载补锁

[agent-239] 仅改 patch_v2/ncm_app/。

1. 歌单分页(REPORT §14.1): track/all 全 404(§13.1) 弃用, 回 detail(n=1)全量 trackIds
   + song_detail 分批(≤NCM_SONG_BATCH=100) 触底逐页加载。appstate_load_playlist_songs_page
   offset=0 重建 song_ids[]/track_ids_count 载第0页, offset>0 追加(失败保留已载);
   playlist.c g_pl_more=(song_count<track_ids_count && <NCM_SONG_MAX), 触底 g_ploffset=song_count
   起下一页(保留滚动), 首屏失败整页空态+点按重试, 追加失败 snack+保留已载。NCM_SONG_MAX
   256→1000(单页渲染上限, bss +~1MB), NCM_SONG_BATCH 64→100, 删 NCM_PLAYLIST_PAGE(track/all 遗留)。

2. 专辑封面(§14.2): cover.c 封面改走 ncm_cache(LRU, 键=djb2(url)→song_cov<h>.jpg 同池),
   缓存命中免下载/未命中下载→open/write/commit 落盘, 仍写 cover_current.jpg 供 launcher;
   顺带修真 bug: save_cover_raw 在已持 g_cm 临界区内再 lock(非递归死锁)→封面永不刷新, 改调用方持锁。
   歌单列表/详情头块封面保持现状。

3. 歌词后台加载(§14.3): lyric_worker 已异步(§11)未改线程模型; 补 lyric.c g_lyric_mu 数据竞争锁,
   写侧 lyric_parse 整段持锁(清空用 lyric_clear_nolock 免二次取锁), 读侧 lyric_count/line_at/
   line_text/line_tl 逐次取锁, 消除 UI/IPC 与 worker 间数据竞争(原 §10.9 FM4 良性竞态)。

验证(0 真实 NCM 请求): bench/pagination_sim.c 离线 stub 驱动真实 appstate 分页函数全 PASS
(130首读全程/5191首封顶1000/空/越界无操作); mipsel build_ncm.sh 0 warning(text 842820/bss 2052068);
宿主 build_ncm_host.sh+MEOW_UI_DUMP home/playlist/player/lands 渲染正常无崩溃; host SDK_SRC 补
meow_pinyin(拼音IME py_predict 漏编致链接断)。ncm md5 b34bc1ed, ncm_host md5 1a2327e0。
未动 sdk/(主会话改 meow_ui_ime.c)/meow_launcherd/meow_amsd/vendor/core; 未 commit 它们。
This commit is contained in:
MeowOS committed 2026-08-31 19:33:10 +08:00
1 parent 5fac5e380d
commit ee104f1b18
10 files changed
+471 -87

No files matched your search

+45 -34
View File
@@ -126,34 +126,44 @@ 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 §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;
/* REPORT §13.1:/api/playlist/track/all 服务端分页端点真机全 404(NCM 无此路由),弃用。
* 回到 §10.11 已验证路径并做**真分页**:detail(n=1)取【全量】trackIds 入 song_ids[],
* song_detail 按 ≤NCM_SONG_BATCH 分批拉歌曲对象(含 al.picUrl),offset 为「下一页」起始;
* 列表触底驱动 offset 前进,song_count 逐页累加(不再一次性拉满,大歌单不冻 UI,
* 也不把「第 N 页之后的歌」挡在 256 之外——NCM_SONG_MAX 上限提升到 1000,见 §14)。 */
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; }
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;
if (offset == 0) {
/* 重头:重建 trackIds 索引 + 清空已物化歌曲(song_count 无论成败都归零)。 */
memset(g_st.song_ids, 0, sizeof g_st.song_ids);
memset(g_st.songs, 0, sizeof g_st.songs);
g_st.song_count = 0;
g_st.track_ids_count = 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) return rc;
g_st.track_ids_count = idn;
/* 首屏只需第 0 页:song_detail 只拉前 limit 首,其余待滚动触底再拉(分页驱动)。 */
} else if (g_st.track_ids_count == 0) {
/* 防御:offset>0 但没有索引(不应发生;主路径先走 offset=0)。 */
return NCM_ERR_ARG;
}
g_st.song_count = outn;
if (offset >= g_st.track_ids_count) return NCM_OK; /* 已到尾部/上限 */
if (offset >= NCM_SONG_MAX) return NCM_OK; /* 内存上限:不再物化更多 */
int can = g_st.track_ids_count - offset;
int room = NCM_SONG_MAX - offset;
if (can > limit) can = limit; /* 每页 ≤ limit(保守 100) */
if (can > room) can = room; /* 不越界 song_ids[]/songs[] */
int bn = 0;
int rc = ncm_api_song_detail(g_st.ctx.device_id, g_st.ctx.cookie, &g_st.song_ids[offset],
can, &g_st.songs[offset], NCM_SONG_MAX - offset, &bn);
if (rc != NCM_OK) return rc; /* 失败:song_count 不变(保留已载页) */
g_st.song_count = offset + bn; /* 成功:累加本页 */
appstate_apply_playlist_cover(idx);
return NCM_OK;
}
@@ -161,15 +171,16 @@ int appstate_load_playlist_songs_page(int idx, int offset, int limit)
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);
int rc = appstate_load_playlist_songs_page(idx, 0, NCM_SONG_BATCH);
if (rc != NCM_OK) return rc;
int off = g_st.song_count;
while (off < g_st.track_ids_count && off < NCM_SONG_MAX) {
rc = appstate_load_playlist_songs_page(idx, off, NCM_SONG_BATCH);
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);
int nxt = g_st.song_count;
if (nxt <= off) break; /* 无进展:防死循环 */
off = nxt;
}
return NCM_OK;
}
+50 -6
View File
@@ -10,6 +10,7 @@
#include <string.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/stat.h>
static pthread_mutex_t g_cm = PTHREAD_MUTEX_INITIALIZER;
static meow_img *g_cover_img;
@@ -42,18 +43,39 @@ static meow_img *decode_cover(const char *url, size_t *out_len)
return meow_img_load_mem((const void *)resp.body, (int)resp.body_len);
}
/* 封面缓存键:URL 的 djb2 哈希(稳定性由 URL 决定——同一专辑封面 URL 名即可命中)。
* 走 ncm_cache(LRU 口径沿用 ncm_cache,见 REPORT §14.2):键进 ncm_cache_path => 文件
* `song_cov<hex>.jpg`,与 mp3(song_<id>.mp3)同池,lookup/commit 刷新 mtime(LRU)。 */
static void cover_cache_id(const char *url, char *out, size_t out_sz)
{
unsigned long h = 5381;
for (const char *p = url; *p; p++) h = ((h << 5) + h) + (unsigned char)*p;
snprintf(out, out_sz, "cov%lx", h);
}
/* 读整文件到 buf(< cap),成功返回 1,写入 *len。 */
static int read_file_into_buf(const char *path, unsigned char *buf, size_t cap, size_t *len)
{
FILE *f = fopen(path, "rb");
if (!f) return 0;
size_t n = fread(buf, 1, cap, f);
fclose(f);
if (n == 0) return 0;
if (len) *len = n;
return 1;
}
/* 写 cover_current.jpg 供 launcher 媒体卡片读(兼容保留)。调用方须已持有 g_cm
* (早前版本在此内再 lock g_cm → 非递归锁自锁死——cover worker 永久卡死、封面永不出现)。 */
static void save_cover_raw(const char *url, size_t len)
{
if (!url || len == 0) return;
if (g_st.cache_dir[0]) {
char path[512];
char path[NCM_CACHE_DIR_MAX + 32]; /* >512:宿主 glibc fortify 对 %s 展开宽证明可放 */
snprintf(path, sizeof path, "%s/cover_current.jpg", g_st.cache_dir);
ncm_store_write_file(path, g_buf, len);
pthread_mutex_lock(&g_cm);
ncm_strlcpy(g_cover_path, path, sizeof g_cover_path);
pthread_mutex_unlock(&g_cm);
}
(void)url;
}
static void *cover_worker(void *arg)
@@ -66,9 +88,31 @@ static void *cover_worker(void *arg)
ncm_strlcpy(url, g_tgt_url, sizeof url);
pthread_mutex_unlock(&g_cm);
char key[32];
cover_cache_id(url, key, sizeof key);
size_t blen = 0;
meow_img *img = decode_cover(url, &blen);
if (!img) { usleep(500 * 1000); continue; }
meow_img *img = NULL;
/* ① 缓存命中直接解码(免下载、离线可看;LRU:lookup 刷新 mtime) */
char cpath[512];
if (g_st.ctx.cache.dir[0] && ncm_cache_lookup(&g_st.ctx.cache, key, ".jpg",
cpath, sizeof cpath) == NCM_OK &&
read_file_into_buf(cpath, g_buf, sizeof g_buf, &blen)) {
img = meow_img_load_mem(g_buf, (int)blen);
}
/* ② 未命中 → 下载解码;成功后落进 ncm_cache(LRU 池,超限由 ncm_cache_evict 淘汰) */
if (!img) {
blen = 0;
img = decode_cover(url, &blen);
if (!img) { usleep(500 * 1000); continue; }
if (g_st.ctx.cache.dir[0]) {
ncm_cache_writer_t *w = NULL;
if (ncm_cache_open(&g_st.ctx.cache, key, ".jpg", &w) == NCM_OK) {
ncm_cache_write(w, g_buf, blen);
ncm_cache_commit(w);
}
}
}
pthread_mutex_lock(&g_cm);
/* 目标仍是 url 才换入(期间未提交更早) */
+56 -26
View File
@@ -10,11 +10,24 @@
#include <stdlib.h>
#include <string.h>
/* 歌词数据由 lyric_worker(player_manager.c,后台线程)写(lyric_parse/lyric_clear)而 UI 线程
* 与 IPC 线程读(lyric_count/lyric_line_at/lyric_line_text/lyric_line_tl)。三期第一批(REPORT §14.3)
* 补一把锁消除**数据竞争**(早前为良性竞态 §10.9 FM4;本节要求禁止数据竞争)。锁粒度=函数级,
* 写侧在 lyric_parse 整段持锁、读侧每次访问持锁,无嵌套(清空用 nolock 版,避免二次取锁自锁死)。 */
#define LRC_MAX_TEXT 192
static long s_t[NCM_LYRIC_LINES];
static char s_txt[NCM_LYRIC_LINES][LRC_MAX_TEXT];
static char s_tl[NCM_LYRIC_LINES][LRC_MAX_TEXT];
static int s_n;
static pthread_mutex_t g_lyric_mu = PTHREAD_MUTEX_INITIALIZER;
static void lyric_clear_nolock(void)
{
s_n = 0;
memset(s_t, 0, sizeof(s_t));
memset(s_txt, 0, sizeof(s_txt));
memset(s_tl, 0, sizeof(s_tl));
}
/* 复制一行歌词正文 n 字节到 dst,把「字面反斜杠 n/r」序列(服务器用 \n 把多行文本塞进一行,
* 字面 \n 渲染成豆腐块/凌乱)替换为空格;顺带把 \r\n / \n\r 折叠为单空格。有界 ≤max。 */
@@ -104,57 +117,74 @@ static int cmp_lines(const void *a, const void *b)
int lyric_parse(const char *lrc, const char *tlrc)
{
lyric_clear();
pthread_mutex_lock(&g_lyric_mu);
lyric_clear_nolock();
int *idx = NULL;
int result = 0;
lrc_parse(lrc, 0);
lrc_parse(tlrc, 1);
/* 按时间排序(用索引数组) */
int *idx = (int *)malloc(sizeof(int) * (size_t)(s_n ? s_n : 1));
if (!idx) return s_n;
for (int i = 0; i < s_n; i++) idx[i] = i;
qsort(idx, (size_t)s_n, sizeof(int), cmp_lines);
long ttmp[NCM_LYRIC_LINES];
char txt[NCM_LYRIC_LINES][LRC_MAX_TEXT];
char tl[NCM_LYRIC_LINES][LRC_MAX_TEXT];
memcpy(ttmp, s_t, sizeof(s_t));
memcpy(txt, s_txt, sizeof(s_txt));
memcpy(tl, s_tl, sizeof(s_tl));
for (int i = 0; i < s_n; i++) {
s_t[i] = ttmp[idx[i]];
memcpy(s_txt[i], txt[idx[i]], LRC_MAX_TEXT);
memcpy(s_tl[i], tl[idx[i]], LRC_MAX_TEXT);
idx = (int *)malloc(sizeof(int) * (size_t)(s_n ? s_n : 1));
if (idx) {
for (int i = 0; i < s_n; i++) idx[i] = i;
qsort(idx, (size_t)s_n, sizeof(int), cmp_lines);
long ttmp[NCM_LYRIC_LINES];
char txt[NCM_LYRIC_LINES][LRC_MAX_TEXT];
char tl[NCM_LYRIC_LINES][LRC_MAX_TEXT];
memcpy(ttmp, s_t, sizeof(s_t));
memcpy(txt, s_txt, sizeof(s_txt));
memcpy(tl, s_tl, sizeof(s_tl));
for (int i = 0; i < s_n; i++) {
s_t[i] = ttmp[idx[i]];
memcpy(s_txt[i], txt[idx[i]], LRC_MAX_TEXT);
memcpy(s_tl[i], tl[idx[i]], LRC_MAX_TEXT);
}
free(idx);
}
free(idx);
return s_n;
result = s_n;
pthread_mutex_unlock(&g_lyric_mu);
return result;
}
int lyric_line_at(int64_t ms)
{
pthread_mutex_lock(&g_lyric_mu);
int line = 0;
for (int i = 0; i < s_n; i++) {
if (s_t[i] <= ms) line = i;
else break;
}
pthread_mutex_unlock(&g_lyric_mu);
return line;
}
const char *lyric_line_text(int i)
{
if (i < 0 || i >= s_n) return "";
return s_txt[i];
pthread_mutex_lock(&g_lyric_mu);
const char *p = (i < 0 || i >= s_n) ? "" : s_txt[i];
pthread_mutex_unlock(&g_lyric_mu);
return p;
}
const char *lyric_line_tl(int i)
{
if (i < 0 || i >= s_n) return "";
return s_tl[i];
pthread_mutex_lock(&g_lyric_mu);
const char *p = (i < 0 || i >= s_n) ? "" : s_tl[i];
pthread_mutex_unlock(&g_lyric_mu);
return p;
}
int lyric_count(void) { return s_n; }
int lyric_count(void)
{
pthread_mutex_lock(&g_lyric_mu);
int n = s_n;
pthread_mutex_unlock(&g_lyric_mu);
return n;
}
void lyric_clear(void)
{
s_n = 0;
memset(s_t, 0, sizeof(s_t));
memset(s_txt, 0, sizeof(s_txt));
memset(s_tl, 0, sizeof(s_tl));
pthread_mutex_lock(&g_lyric_mu);
lyric_clear_nolock();
pthread_mutex_unlock(&g_lyric_mu);
}
+20 -7
View File
@@ -39,11 +39,17 @@
#define APP_OP_W 48 /* 行右侧操作区触达宽 */
#define APP_FOOTER_H 26
/* ---- 数据规模(内存纪律:歌单/歌曲大列表虚拟化显示,数组有界)---- */
/* ---- 数据规模(内存纪律:歌单/歌曲大列表虚拟化显示,数组有界;硬约束 R3P2 ~14MB)---- */
#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) */
/* 单个歌单曲目上限 = 分页驱动下的「单页渲染上限」(REPORT §14,三期第一批):
* detail(/v6/playlist/detail, n=1)取回全量 trackIds 入 song_ids[](≤ NCM_SONG_MAX);
* song_detail 按 NCM_SONG_BATCH 分批拉歌词对象,列表触底自动加载下一页,直到
* song_count == track_ids_count(歌单尾部)或 == NCM_SONG_MAX(内存上限,footer 显示 x/总数)。
* 不再硬截断 256(NCM_SONG_MAX 从 256 提升到 1000,约 1.32KB/首 → 歌曲数组 ~1.37MB,14MB 内安全)。
* 数值权衡见 REPORT §14.8(可再上调,代价是 bss 线性增长:每 +100 首 ≈ +132KB)。 */
#define NCM_SONG_MAX 1000 /* 歌单曲目/歌曲对象上限(append-only 分页加载) */
#define NCM_SONG_BATCH 100 /* song_detail 分批每批条数(≤500 安全窗内取保守 100;
* 同时作「下一页」加载粒度;c 数组缓冲 clist[4096] 装 100 首 ≈2KB */
#define NCM_LYRIC_LINES 600 /* 歌词行上限 */
#define NCM_COVER_MAX 288*1024 /* 封面下载限幅 ~288KB */
#define NCM_SEARCH_MAX 32 /* 搜索结果上限(v1 只显示前 32 条,无大缓冲) */
@@ -106,10 +112,15 @@ typedef struct ncm_app_state {
int playlist_count;
int active_playlist; /* 当前打开歌单的下标;-1=无 */
/* 当前歌单曲目(id 用 int64_t:网易云歌曲 id 可 >2^31,MIPS32 long 溢出) */
/* 当前歌单曲目(id 用 int64_t:网易云歌曲 id 可 >2^31,MIPS32 long 溢出)。
* song_ids[] = detail 返回的全量 trackIds(≤ NCM_SONG_MAX,分页索引);
* songs[] = 已物化的歌曲对象(append-only,与 song_ids[] 前 song_count 首一一对应);
* song_count = 已物化数(列表 count = song_count);track_ids_count = trackIds 总数(≥ song_count,
* 其差值 = 仍有「下一页」待 song_detail 分批拉取)。 */
int64_t song_ids[NCM_SONG_MAX];
ncm_song_t songs[NCM_SONG_MAX];
int song_count;
int track_ids_count; /* detail 取回的 trackIds 总数(≤ NCM_SONG_MAX) */
/* 播放页视图态 */
int playing_song; /* 正在播放歌曲在 songs[] 下标(UI 侧缓存);-1=无 */
@@ -249,8 +260,10 @@ int appstate_ensure_uid(void); /* 若 uid 缺失则 login_status_check 取
int appstate_load_playlists(void); /* 拉我的歌单 */
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 */
/* detail(全量 trackIds)后按页拉 song_detail:
* offset=0 重头(重建 song_ids[]/track_ids_count,清空后载第 0 页);
* offset>0 追加第 offset 页到 g_st.songs[offset..];
* 写 g_st.song_count(=offset+本页实际数)。 */
void appstate_apply_playlist_cover(int idx);
void appstate_set_playing_song(int idx, const char *cover_url);
int appstate_current_playing(void); /* 返回当前播放歌曲下标(UI) */
+31 -13
View File
@@ -34,7 +34,7 @@ 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 volatile int g_pl_more; /* 1=尚有下一页可加载(song_count < track_ids_count 且未到 NCM_SONG_MAX) */
static void pl_drag_reset(void)
{
@@ -157,7 +157,7 @@ static void pl_draw_empty(void *ctx)
if (g_load_err == 0 && g_st.song_count != 0) return;
if (g_plloading) { ncm_draw_empty(r_list, "LOADING", "正在加载曲目..."); return; }
if (g_load_err == NCM_ERR_AUTH) ncm_draw_empty(r_list, "SESSION EXPIRED", "请重新登录");
else if (g_load_err) ncm_draw_empty(r_list, "LOAD FAILED", "返回后重试");
else if (g_load_err) ncm_draw_empty(r_list, "LOAD FAILED", "点按重试");
else ncm_draw_empty(r_list, "NO TRACKS", "这个歌单是空的");
}
@@ -197,12 +197,11 @@ static void *playlist_worker(void *arg)
int rc;
if (g_host_mock) rc = 0;
else if (my_idx < 0) rc = NCM_ERR_ARG;
else rc = appstate_load_playlist_songs_page(my_idx, off, NCM_PLAYLIST_PAGE);
else rc = appstate_load_playlist_songs_page(my_idx, off, NCM_SONG_BATCH);
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) &&
/* 本页后仍有多余 trackIds(tail 未到)且未到内存上限 → 还有下一页;否则到屏尾。 */
g_pl_more = (rc == NCM_OK) && (g_st.song_count < g_st.track_ids_count) &&
(g_st.song_count < NCM_SONG_MAX);
g_plloading = 0;
g_plloaded_idx = my_idx;
@@ -211,10 +210,11 @@ static void *playlist_worker(void *arg)
return NULL;
}
/* 触底加载下一页:仅当仍有下一页、未在载、且未到 NCM_SONG_MAX 时发。 */
/* 触底加载下一页:仅当仍有下一页、未在载、未失败、未到 NCM_SONG_MAX 时发。 */
static void pl_maybe_load_more(void)
{
if (g_plthr_on || g_load_err != 0 || g_host_mock) return;
if (g_plthr_on || g_host_mock) return;
if (g_load_err != 0) return; /* 失败态:走点按重试,不自动循环 */
if (!g_pl_more) return;
if (g_st.song_count >= NCM_SONG_MAX) return;
/* 视口底接近列表底(最后一行即将可见即预取) */
@@ -222,11 +222,11 @@ static void pl_maybe_load_more(void)
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_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;
g_plthr_on = 0; g_plloading = 0; ncm_snack("加载失败");
}
}
@@ -236,6 +236,7 @@ void playlist_refresh(void)
if (g_plthr_on) return;
g_load_err = 0;
g_st.song_count = 0;
g_st.track_ids_count = 0;
g_ploffset = 0;
g_pl_more = 0;
g_plloading = 1;
@@ -256,8 +257,18 @@ void playlist_tick(uint64_t now)
g_plloading = 0;
if (g_plloaded_idx != g_st.active_playlist) { playlist_refresh(); return; }
if (g_load_err != 0) {
if (g_ploffset == 0) g_st.song_count = 0; /* 首屏失败 → 整页空态 */
else { ncm_snack("加载失败"); g_load_err = 0; } /* 追加失败 → 保留已载歌曲 */
if (g_ploffset == 0) {
g_st.song_count = 0; /* 首屏失败 → 整页空态(点按重试) */
pl_apply(1);
} else {
/* 追加失败 → 保留已载歌曲 + 重置 g_pl_more(触底/点击可重试) */
ncm_snack("加载失败");
g_load_err = 0;
g_pl_more = (g_st.song_count < g_st.track_ids_count) &&
(g_st.song_count < NCM_SONG_MAX);
pl_apply(0);
}
return;
}
pl_apply(g_ploffset == 0); /* 首屏回顶;触底追加保持滚动 */
}
@@ -289,7 +300,14 @@ int playlist_on_tap(int x, int y)
if (x >= r_cmt.x && x < r_cmt.x + r_cmt.w && y >= r_cmt.y && y < r_cmt.y + r_cmt.h) {
pl_drag_reset(); ncm_snack("评论功能开发中"); return 1;
}
if (g_load_err != 0 || g_st.song_count == 0) return 0;
if (g_load_err != 0) {
/* 失败态(LOAD FAILED / SESSION EXPIRED / NETWORK ERROR):点按内容区重试。 */
pl_drag_reset();
meow_ui_invalidate(r_list.x, r_list.y, r_list.w, r_list.h);
playlist_refresh();
return 1;
}
if (g_st.song_count == 0) return 0; /* 空歌单(无失败):不重试 */
if (meow_widget_hit(g_list, x, y)) {
/* 误触修复:小拖动(<SWIPE_THRESH)会先滚动列表、release 又被 SDK 判成 TAP,旧的
* meow_listview_hit(x,y) 用“释放点 + 更新后的 scroll”命中 → 打到“已移动到手指下”的行,