280 歌单链路三修:开A→开B 竞态防串(5194 串数据) + 92/200 批边界 Load Failed + 歌单列表盘缓存 + 孤儿 .part 回收

ncm_app(REPORT §26, ncm 158ec367 未部署留主会话):
- 打开时快照 plid+代次:appstate_playlist_begin/open_gen/open_idx/reset +
  appstate_load_playlist_songs_ctx(快照 pl 不读 g_st.playlists[],detail 结果落
  staging 锁内原子发布,代次/目标不匹配判 NCM_ERR_STALE 不写全局);playlist_tick
  过期丢弃 + appstate_playlist_reset 清窗口防串。修 5194「我喜欢的音乐」3 首/串数据。
- appstate_song_ensure_range fill-and-continue:bn<m 部分返回/单批网络失败非致命
  (记 NCM_ERR_NOTFOUND/r2),已物化保留+占位行;worker 首屏 track_ids_count>0
  降级非致命(snack)。修 92/200 首歌单点开整页 Load Failed。
- 歌单列表盘缓存 <data_dir>/playlists.bin(magic+ver+ts+数组):home_reload 先缓存
  秒渲 + 后台静默刷新,刷新失败有数据不置错态。修返回首页卡 LOADING(275 遗留)。
- 孤儿 .part/.tmp:evict/total_bytes LRU 扫描顺续删 mtime>7 天(in-flight mtime 新鲜安全)。
验证:新 bench/playlist_race_sim(竞态防串/批边界/非致命/盘缓存/reset)+cache_evict_sim
case9(7 天回收)+window/pagination/search/stream_sim 零回归+test_ncm_core host+mipsel
PASS;build_ncm.sh -Wall -Wextra 0 warning(text 978354/bss 1496484);0 真实请求。
遗留:appstate_logout 未联动删 playlists.bin;g_st.playlists[] 快照拷贝仍有极小交错窗。
This commit is contained in:
MeowOS committed 2026-09-01 21:06:57 +08:00
1 parent d8eeef5c57
commit f0b73e119d
11 files changed
+744 -48

No files matched your search

+173 -31
View File
@@ -13,6 +13,39 @@ static pthread_mutex_t g_song_mu = PTHREAD_MUTEX_INITIALIZER;
/* 搜索结果独立数组互斥(§23):g_st.search_songs/search_pls 读写 + queue_src 读。 */
static pthread_mutex_t g_search_mu = PTHREAD_MUTEX_INITIALIZER;
/* 歌单加载上下文(§26 开A→未载完→开B 竞态防串):open_gen 在每次「打开/刷新歌单」时递增并记
* 目标下标;加载器(appstate_load_playlist_songs_ctx)在 worker 完成时校验代次/目标不匹配即
* 判过期(NCM_ERR_STALE),不把旧歌单的索引/窗口落进全局——防「上一歌单结果串进新打开的」。
* init 时 open_idx=-1(无打开目标),open_gen=0(无代次 → ctx_gen<0 兼容路径不校验)。 */
static pthread_mutex_t g_plopen_mu = PTHREAD_MUTEX_INITIALIZER;
static int g_plopen_gen; /* 打开代次(每次 begin +1) */
static int g_plopen_idx = -1; /* 当前打开目标下标(-2=搜索哨兵;-1=无) */
int appstate_playlist_open_gen(void) { return g_plopen_gen; }
int appstate_playlist_open_idx(void) { return g_plopen_idx; }
int appstate_playlist_begin(int idx)
{
pthread_mutex_lock(&g_plopen_mu);
g_plopen_gen++;
g_plopen_idx = idx;
int g = g_plopen_gen;
pthread_mutex_unlock(&g_plopen_mu);
return g;
}
/* 清窗口/索引(UI 丢弃过期歌单结果时用;播放快照不受影响——播放经 play_song 快照)。 */
void appstate_playlist_reset(void)
{
pthread_mutex_lock(&g_song_mu);
memset(g_st.song_valid, 0, sizeof g_st.song_valid);
memset(g_st.song_ids, 0, sizeof g_st.song_ids);
g_st.window_base = 0;
g_st.song_count = 0;
g_st.track_ids_count = 0;
pthread_mutex_unlock(&g_song_mu);
}
static void appstate_srch_hist_init(void); /* §23:读入本地搜索历史(appstate_init 首调) */
int appstate_init(void)
@@ -152,9 +185,66 @@ int appstate_load_playlists(void)
NCM_PLAYLIST_MAX, 0, g_st.playlists, NCM_PLAYLIST_MAX, &n);
g_st.playlist_count = (rc == NCM_OK) ? n : 0;
meow_log(MEOW_LOG_INFO, "home: playlists rc=%d count=%d uid=%s", rc, g_st.playlist_count, g_st.uid);
if (rc == NCM_OK) appstate_playlists_cache_save(); /* §26:网络成功 → 覆写盘缓存(stale-while-revalidate) */
return rc;
}
/* ---- 歌单列表磁盘缓存(§26,NCM_CACHE_DESIGN §2.2)----
* 快照 <data_dir>/playlists.bin:magic(4) + ver(2) + 时间戳(8) + count(4) + 数组(ncm_playlist_t)。
* 进首页先渲染缓存秒开(断网可离线浏览),后台 worker 拉网络成功后覆写;结构体改版靠 ver 判 miss。
* 刻意不进 ncm_cache LRU 池(那是音频/封面池,被「清理缓存」清空)——歌单列表是浏览数据。 */
#define PLS_CACHE_MAGIC 0x50534C54u /* 'PLST' */
#define PLS_CACHE_VER 1
static int64_t g_pls_cache_ts; /* 上次缓存快照时间(秒) */
int appstate_playlists_cache_save(void)
{
char path[640];
snprintf(path, sizeof path, "%s/playlists.bin", g_st.data_dir);
FILE *f = fopen(path, "wb");
if (!f) return NCM_ERR_IO;
int64_t ts = ncm_now_ms() / 1000;
uint32_t magic = PLS_CACHE_MAGIC;
uint16_t ver = PLS_CACHE_VER;
uint32_t count = (uint32_t)g_st.playlist_count;
if (count > NCM_PLAYLIST_MAX) count = NCM_PLAYLIST_MAX;
int ok = (fwrite(&magic, sizeof magic, 1, f) == 1 &&
fwrite(&ver, sizeof ver, 1, f) == 1 &&
fwrite(&ts, sizeof ts, 1, f) == 1 &&
fwrite(&count, sizeof count, 1, f) == 1 &&
fwrite(g_st.playlists, sizeof(ncm_playlist_t), (size_t)count, f) == (size_t)count);
fclose(f);
if (ok) { g_pls_cache_ts = ts; return NCM_OK; }
unlink(path);
return NCM_ERR_IO;
}
int appstate_playlists_cache_load(int64_t *ts_out)
{
if (ts_out) *ts_out = 0;
if (g_st.logged_in == 0) return NCM_ERR_AUTH; /* 未登录不落缓存 */
char path[640];
snprintf(path, sizeof path, "%s/playlists.bin", g_st.data_dir);
FILE *f = fopen(path, "rb");
if (!f) return NCM_ERR_NOENT;
uint32_t magic; uint16_t ver; int64_t ts; uint32_t count;
int ok = (fread(&magic, sizeof magic, 1, f) == 1 &&
fread(&ver, sizeof ver, 1, f) == 1 &&
fread(&ts, sizeof ts, 1, f) == 1 &&
fread(&count, sizeof count, 1, f) == 1 &&
magic == PLS_CACHE_MAGIC && ver == PLS_CACHE_VER &&
count <= NCM_PLAYLIST_MAX);
if (!ok) { fclose(f); return NCM_ERR_IO; }
size_t rd = fread(g_st.playlists, sizeof(ncm_playlist_t), (size_t)count, f);
fclose(f);
if (rd != (size_t)count) return NCM_ERR_IO;
g_st.playlist_count = (int)count;
g_pls_cache_ts = ts;
if (ts_out) *ts_out = ts;
meow_log(MEOW_LOG_INFO, "home: playlists cache hit count=%d ts=%lld", (int)count, (long long)ts);
return NCM_OK;
}
/* ============================================================
* 滑动窗口(REPORT §17,三期第三批)
* songs[i] ↔ 全局下标 window_base+i;song_valid[] 为槽位有效位图。
@@ -257,7 +347,8 @@ int appstate_song_ensure_range(int lo, int hi)
* 走 song_detail;成功写窗 + 落盘。网络/盘 IO 均在锁外,锁内仅写槽。 */
static ncm_song_t s_fetch[NCM_SONG_BATCH]; /* 单写者(playlist worker),免 60KB 栈 */
int rc = NCM_OK;
for (int a = lo; a < hi && rc == NCM_OK; ) {
int first_err = NCM_OK; /* §26:批内错误非致命(缺歌/单批网络失败),记首个供 UI 提示 */
for (int a = lo; a < hi; ) {
pthread_mutex_lock(&g_song_mu);
int va = slot_valid_locked(a);
pthread_mutex_unlock(&g_song_mu);
@@ -280,17 +371,19 @@ int appstate_song_ensure_range(int lo, int hi)
if (a0 < g_st.window_base) a0 = g_st.window_base;
if (b0 > wend2) b0 = wend2;
if (b0 - a0 > NCM_SONG_BATCH) b0 = a0 + NCM_SONG_BATCH;
/* 本段 [a0,b0):盘命中直接写窗,有效槽跳过,miss 记录待批拉 */
/* 本段 [a0,b0):盘命中直接写窗,有效槽跳过,miss 记录待批拉。
* 索引读取在锁内(与「索引重建」写 song_ids 的 worker/重建路径防竞态)。 */
int64_t ids[NCM_SONG_BATCH];
int gslot[NCM_SONG_BATCH];
int m = 0;
for (int i = a0; i < b0; i++) {
pthread_mutex_lock(&g_song_mu);
int vv = slot_valid_locked(i);
int64_t sid64 = vv ? 0 : g_st.song_ids[i];
pthread_mutex_unlock(&g_song_mu);
if (vv) continue;
char sid[24];
snprintf(sid, sizeof sid, "%lld", (long long)g_st.song_ids[i]);
snprintf(sid, sizeof sid, "%lld", (long long)sid64);
ncm_song_t ms;
if (songmeta_load(g_st.cache_dir, sid, &ms) == NCM_OK) {
pthread_mutex_lock(&g_song_mu);
@@ -301,7 +394,7 @@ int appstate_song_ensure_range(int lo, int hi)
}
pthread_mutex_unlock(&g_song_mu);
} else {
ids[m] = g_st.song_ids[i];
ids[m] = sid64;
gslot[m] = i;
m++;
}
@@ -310,36 +403,45 @@ int appstate_song_ensure_range(int lo, int hi)
int bn = 0;
int r2 = ncm_api_song_detail(g_st.ctx.device_id, g_st.ctx.cookie,
ids, m, s_fetch, NCM_SONG_BATCH, &bn);
if (r2 != NCM_OK) { rc = r2; break; }
if (bn > m) bn = m;
for (int k = 0; k < bn; k++) { /* song_detail 结果与请求 ids 同序 */
pthread_mutex_lock(&g_song_mu);
int s = gslot[k] - g_st.window_base;
if (s >= 0 && s < NCM_SONG_WINDOW) {
g_st.songs[s] = s_fetch[k];
g_st.song_valid[s] = 1;
if (r2 == NCM_OK) {
if (bn > m) bn = m;
for (int k = 0; k < bn; k++) { /* song_detail 结果与请求 ids 同序 */
pthread_mutex_lock(&g_song_mu);
int s = gslot[k] - g_st.window_base;
if (s >= 0 && s < NCM_SONG_WINDOW) {
g_st.songs[s] = s_fetch[k];
g_st.song_valid[s] = 1;
}
pthread_mutex_unlock(&g_song_mu);
char sid[24];
snprintf(sid, sizeof sid, "%lld", (long long)ids[k]);
songmeta_save(g_st.cache_dir, sid, &s_fetch[k]);
}
pthread_mutex_unlock(&g_song_mu);
char sid[24];
snprintf(sid, sizeof sid, "%lld", (long long)ids[k]);
songmeta_save(g_st.cache_dir, sid, &s_fetch[k]);
/* §26:部分返回(bn<m,服务端缺歌/权限变化)非致命——已物化槽保留,缺槽留
* invalid(下次 ensure 再试),不再整段置失败。修【92/200 首真机「加载失败」】:
* 原 NCM_ERR_JSON 会把整页打成 Load Failed(批内一首缺歌即全灭)。 */
if (bn < m && first_err == NCM_OK) first_err = NCM_ERR_NOTFOUND;
} else {
/* 单批网络失败:非致命;跳过本批网格,其余批照常补(首屏尽量多物化)。 */
if (first_err == NCM_OK) first_err = r2;
}
if (bn < m) rc = NCM_ERR_JSON; /* 部分返回:缺槽留 invalid,下次 ensure 再试 */
}
a = b;
a = (b0 > a) ? b0 : b; /* 推进到本批网格末尾:失败批不在同一次调用内被重复重发 */
}
pthread_mutex_lock(&g_song_mu);
win_recount_locked();
pthread_mutex_unlock(&g_song_mu);
return rc;
return (first_err != NCM_OK) ? first_err : rc;
}
int appstate_song_fetch(int gidx, ncm_song_t *out)
{
if (!out) return NCM_ERR_ARG;
if (appstate_song_copy(gidx, out) == 0) return NCM_OK; /* 窗内直接命中 */
if (gidx < 0 || gidx >= g_st.track_ids_count) return NCM_ERR_ARG;
if (gidx < 0) return NCM_ERR_ARG;
pthread_mutex_lock(&g_song_mu);
if (gidx >= g_st.track_ids_count) { pthread_mutex_unlock(&g_song_mu); return NCM_ERR_ARG; }
int64_t id = g_st.song_ids[gidx];
pthread_mutex_unlock(&g_song_mu);
if (id == 0) return NCM_ERR_ARG; /* 无索引(如搜索上下文窗外) */
char sid[24];
snprintf(sid, sizeof sid, "%lld", (long long)id);
@@ -368,13 +470,14 @@ void appstate_adopt_songs(int n)
pthread_mutex_unlock(&g_song_mu);
}
int appstate_load_playlist_songs_page(int idx, int offset, int limit)
/* 核心加载(无代次校验):pl 为打开时的快照副本(已拷贝,之后不读 g_st.playlists[]——
* 防 home_worker 刷新歌单列表 memset 旧数组时把正在加载的歌单 id 污染成空、导致
* detail("") 取回 3 首歌/串数据/加载失败,§26)。offset==0 重头用 staging 缓冲收编
* detail 结果、锁内原子发布索引(防 worker 写 song_ids 与播放线程 song_fetch 读竞态)。 */
static int appstate_load_playlist_songs_impl(const ncm_playlist_t *pl, int is_search,
int offset, int limit)
{
/* 搜索歌单详情(§23):idx==NCM_PL_SEARCH 时读 g_st.search_pl_open(复用本链路)。 */
if (idx != NCM_PL_SEARCH && (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 = (idx == NCM_PL_SEARCH) ? &g_st.search_pl_open : &g_st.playlists[idx];
(void)is_search;
if (limit <= 0 || limit > NCM_SONG_BATCH) limit = NCM_SONG_BATCH;
if (offset < 0) offset = 0;
@@ -384,7 +487,8 @@ int appstate_load_playlist_songs_page(int idx, int offset, int limit)
* 歌曲对象只物化视口附近的 NCM_SONG_WINDOW 首(offset 为「要确保的区间」起点)。 */
if (offset == 0) {
/* 重头:清窗口 + 重建 trackIds 索引(song_count/track_ids_count 无论成败都归零)。 */
/* 重头:清窗口 + 重建 trackIds 索引(song_count/track_ids_count 无论成败都归零)。
* 封面兜底值取自快照 pl->cover(不读 g_st.playlists[],防污染)。 */
pthread_mutex_lock(&g_song_mu);
memset(g_st.song_valid, 0, sizeof g_st.song_valid);
memset(g_st.song_ids, 0, sizeof g_st.song_ids);
@@ -392,13 +496,19 @@ int appstate_load_playlist_songs_page(int idx, int offset, int limit)
g_st.song_count = 0;
g_st.track_ids_count = 0;
pthread_mutex_unlock(&g_song_mu);
ncm_strlcpy(g_st.cover_url, pl->cover, sizeof g_st.cover_url);
/* detail 为网络调用(不能持锁);结果落 staging 缓冲,锁内一次性发布索引(原子)。
* 静态缓冲:本加载函数单写者(playlist worker 单飞),免 128KB 栈。 */
static int64_t s_build[NCM_TRACKID_MAX];
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_TRACKID_MAX, &idn);
NULL, 0, NULL, 0, s_build, NCM_TRACKID_MAX, &idn);
if (rc != NCM_OK) return rc;
if (idn > NCM_TRACKID_MAX) idn = NCM_TRACKID_MAX;
pthread_mutex_lock(&g_song_mu);
if (idn > 0) memcpy(g_st.song_ids, s_build, (size_t)idn * sizeof(int64_t));
g_st.track_ids_count = idn;
appstate_apply_playlist_cover(idx);
pthread_mutex_unlock(&g_song_mu);
} else if (g_st.track_ids_count == 0) {
/* 防御:offset>0 但没有索引(不应发生;主路径先走 offset=0)。 */
return NCM_ERR_ARG;
@@ -410,6 +520,38 @@ int appstate_load_playlist_songs_page(int idx, int offset, int limit)
return appstate_song_ensure_range(offset, hi);
}
int appstate_load_playlist_songs_ctx(const ncm_playlist_t *pl, int is_search,
int ctx_gen, int ctx_idx, int offset, int limit)
{
if (!pl || !pl->id[0]) return NCM_ERR_ARG;
/* §26 打开上下文校验:代次/目标不匹配 → 判过期(不写全局),防「开 A→开 B 的迟到结果串 B」。
* ctx_gen<0 = 兼容调用方(既有单测/autoplay)不校验,永远采纳。 */
if (ctx_gen >= 0 && (ctx_gen != g_plopen_gen || ctx_idx != g_plopen_idx))
return NCM_ERR_STALE;
int rc = appstate_load_playlist_songs_impl(pl, is_search, offset, limit);
/* 加载跨网络(detail 一次 + song_detail 分批),期间用户可能已切到新歌单(开 B):
* 再验一次代次,过期即弃(清窗口,避免旧歌单数据被画在新歌单标题下)。 */
if (ctx_gen >= 0 && (ctx_gen != g_plopen_gen || ctx_idx != g_plopen_idx)) {
appstate_playlist_reset();
return NCM_ERR_STALE;
}
return rc;
}
int appstate_load_playlist_songs_page(int idx, int offset, int limit)
{
/* 搜索歌单详情(§23):idx==NCM_PL_SEARCH 时读 g_st.search_pl_open(复用本链路)。 */
if (idx != NCM_PL_SEARCH && (idx < 0 || idx >= g_st.playlist_count)) return NCM_ERR_ARG;
/* 不在此处写 g_st.active_playlist:调用方(playlist_open/UI)已在 UI 线程设定;
* 若后台 worker 在此改写,会覆盖用户已切换到的新歌单,造成“打开 B 却显示 A”。 */
ncm_playlist_t snap;
int is_search = (idx == NCM_PL_SEARCH);
if (is_search) snap = g_st.search_pl_open;
else snap = g_st.playlists[idx];
/* ctx_gen=-1:兼容入口(单测/autoplay)不做代次校验——由调用方协调并发。 */
return appstate_load_playlist_songs_ctx(&snap, is_search, -1, idx, offset, limit);
}
int appstate_load_playlist_songs(int idx)
{
/* 兼容入口:载索引 + 物化首个窗口。 */
+28
View File
@@ -337,6 +337,8 @@ int appstate_song_ensure_range(int lo, int hi);
* 窗口未覆盖先滑窗(重叠区保留,出窗槽位失效);
* 缺失槽位先读盘缓存(<cache_dir>/meta/<id>.bin),
* miss 攒批 ≤NCM_SONG_BATCH 走 song_detail,成功落盘。
* 批内部分返回/单批网络失败不再整段置失败——已物化槽保留,
* 缺槽留 invalid(下次 ensure 再试),返回仍为 NCM_OK(§26)。
* worker 线程调用(含网络);返回 NCM_OK/负错误码。 */
int appstate_range_loaded(int lo, int hi); /* 1=[lo,hi) 全部在窗内且有效(UI 判是否需 ensure) */
int appstate_song_copy(int gidx, ncm_song_t *out); /* 锁内读窗:0=已物化拷出;-1=窗外/未物化 */
@@ -345,6 +347,32 @@ int appstate_song_fetch(int gidx, ncm_song_t *out); /* 任意全局下标物化
void appstate_adopt_songs(int n); /* 收编外部已填的 songs[0..n)(搜索结果)为窗口 base=0,
* 同步 song_ids/track_ids_count(播放上下文) */
/* ---- 歌单加载上下文(§26 开A→未载完→开B 竞态防串)----
* UI(playlist.c)在每次打开/刷新歌单时调用 appstate_playlist_begin 记录「代次+目标」,
* worker 用同一代次调 appstate_load_playlist_songs_ctx;加载完成时若代次/目标已不匹配,
* 结果判过期丢弃(NCM_ERR_STALE),防「前一次加载的 worker 结果落进新打开的歌单」。 */
int appstate_playlist_open_gen(void); /* 当前打开代次(0=从未打开) */
int appstate_playlist_open_idx(void); /* 当前打开目标下标(-2=搜索歌单哨兵;-1=无) */
int appstate_playlist_begin(int idx); /* 递增代次并记目标;返回新代次(UI 打开/刷新时调) */
void appstate_playlist_reset(void); /* 清窗口/索引(UI 丢弃过期结果、在播安全)。加锁。 */
int appstate_load_playlist_songs_ctx(const ncm_playlist_t *pl, int is_search,
int ctx_gen, int ctx_idx, int offset, int limit);
/* 快照式加载(歌单详情/曲目物化):
* pl = 打开时快照(不读 g_st.playlists[],防
* home_worker 并发 memset 污染 id);
* is_search = 1=搜索歌单(NCM_PL_SEARCH 哨兵);
* ctx_gen/ctx_idx = begin() 返回的代次与目标,加载中/末
* 验不匹配 → NCM_ERR_STALE(不写全局);
* ctx_gen < 0 = 不校验(兼容既有调用方/测试)。
* offset=0 重头(detail 全量 trackIds 重建索引+清窗口);
* offset>0 = 物化 [offset,offset+limit)(滑窗+盘缓存+补载)。 */
/* ---- 歌单列表磁盘缓存(§26,NCM_CACHE_DESIGN §2.2)----
* 快照 <data_dir>/playlists.bin(magic/ver/时间戳 + 数组)。进首页先渲染缓存秒开,
* 后台 worker 拉网络成功后覆写缓存(stale-while-revalidate);断网时列表可离线浏览。 */
int appstate_playlists_cache_load(int64_t *ts_out); /* 0=命中(g_st.playlists/playlist_count 已填) */
int appstate_playlists_cache_save(void); /* 把当前 g_st.playlists/playlist_count 落盘 */
/* ---- 播放队列来源(§23)---- */
void appstate_set_queue(int src); /* NCM_QUEUE_PLAYLIST / NCM_QUEUE_SEARCH */
int appstate_get_queue(void);
+12 -5
View File
@@ -168,7 +168,7 @@ static void home_draw_empty(void *ctx)
{
(void)ctx;
if (meow_ui_activity_active() != ACT_HOME) return;
if (g_load_err == 0 && g_st.playlist_count != 0) return;
if (g_st.playlist_count != 0) return; /* 有数据(缓存/已载)即显示列表,后台刷新失败不置空 */
if (g_home_loading) { ncm_draw_empty(r_list, "LOADING", "正在加载歌单..."); return; }
if (g_st.logged_in == 0) ncm_draw_empty(r_list, "NOT SIGNED IN", "请先登录");
else if (g_load_err == NCM_ERR_AUTH) ncm_draw_empty(r_list, "SESSION EXPIRED", "请重新登录");
@@ -201,7 +201,9 @@ static void home_apply(void)
meow_ui_paint_full();
}
/* 后台 worker:唯一走网络的地方(ensure_uid + user_playlist)。 */
/* 后台 worker:唯一走网络的地方(ensure_uid + user_playlist)。
* §26 stale-while-revalidate:网络成功 → appstate_load_playlists 覆写盘缓存 + 更新内存;
* 失败但有缓存在展示 → 保持缓存列表(g_load_err=0),后台静默不打断(断网可离线浏览)。 */
static void *home_worker(void *arg)
{
(void)arg;
@@ -211,7 +213,7 @@ static void *home_worker(void *arg)
if (rc == NCM_OK)
account_vip_maybe_refresh(0); /* §20:超 1 天/到期后各补查一次(worker 线程,不卡 UI) */
pthread_mutex_lock(&g_home_mu);
g_load_err = rc;
g_load_err = (g_st.playlist_count > 0) ? 0 : rc; /* 有数据(缓存/已载)即不置错误态 */
g_home_loading = 0;
g_home_gen++;
pthread_mutex_unlock(&g_home_mu);
@@ -222,9 +224,14 @@ void home_reload(void)
{
if (g_host_mock) { g_load_err = 0; home_apply(); return; }
if (g_home_thr_on) return;
/* §26:进首页先用磁盘缓存秒渲染(若有),后台 worker 静默刷新;无缓存才显示 LOADING。 */
int64_t ts = 0;
int had_cache = (appstate_playlists_cache_load(&ts) == NCM_OK && g_st.playlist_count > 0);
g_load_err = 0;
g_st.playlist_count = 0;
g_home_loading = 1;
if (!had_cache) {
g_st.playlist_count = 0; /* 无缓存 → LOADING 空态 */
g_home_loading = 1;
}
g_home_thr_on = 1;
if (pthread_create(&g_home_thr, NULL, home_worker, NULL) != 0) {
g_home_thr_on = 0; g_home_loading = 0; g_load_err = NCM_ERR_IO;
+69 -10
View File
@@ -35,10 +35,23 @@ static volatile int g_plgen;
static int g_placked;
static volatile int g_plloading;
static volatile int g_plloaded_idx;
static volatile int g_plloaded_gen; /* worker 启动时代次(§26:过期结果丢弃判据) */
static volatile int g_pl_stale; /* 1=本次结果被判过期(未写全局,需重拉当前目标) */
static volatile int g_ens_lo, g_ens_hi; /* 滑窗补载目标区间(全局下标) */
static volatile int g_ens_first; /* 1=首屏重头(offset=0) */
static volatile int g_ens_err; /* 最近一次滑窗补载错误(0=ok;首屏错误走 g_load_err) */
/* 打开快照(§26 开A→未载完→开B 防串):refresh 在启动 worker 前把「目标歌单」拷进 g_tgt,
* worker 启动时再拷到本地——detail 加载不再依赖 g_st.playlists[](会被回家时 home_worker 的
* appstate_load_playlists 并发 memset 污染)。tgen = appstate_playlist_begin 返回的打开代次。 */
typedef struct {
ncm_playlist_t pl; /* 目标快照(id/name/cover/track_count) */
int is_search; /* 1=搜索歌单(active_playlist==NCM_PL_SEARCH) */
int idx; /* 目标下标(-2=搜索哨兵) */
int gen; /* 打开代次(appstate_playlist_begin 返回) */
} pl_target_t;
static pl_target_t g_tgt;
static void pl_drag_reset(void)
{
g_press_row = -1; g_press_delta = 0; g_dragging = 0; g_drag_last_y = 0;
@@ -218,19 +231,45 @@ static void pl_apply(int reset_scroll)
static void *playlist_worker(void *arg)
{
(void)arg;
int my_idx = g_st.active_playlist;
int first = g_ens_first;
int lo = g_ens_lo, hi = g_ens_hi;
/* 拷贝打开快照 + 任务参数:单飞(g_plthr_on)保证 worker 运行期间 g_tgt 不被改写。 */
ncm_playlist_t tpl;
int tsearch, tidx, tgen, first, lo, hi;
pthread_mutex_lock(&g_plmu);
tpl = g_tgt.pl; tsearch = g_tgt.is_search;
tidx = g_tgt.idx; tgen = g_tgt.gen;
first = g_ens_first; lo = g_ens_lo; hi = g_ens_hi;
pthread_mutex_unlock(&g_plmu);
int cur_active = g_st.active_playlist; /* ensure 路径结束校验用(当前目标) */
int rc;
if (g_host_mock) rc = 0;
else if (my_idx < 0) rc = NCM_ERR_ARG;
else if (first) rc = appstate_load_playlist_songs_page(my_idx, 0, NCM_SONG_BATCH);
else rc = appstate_song_ensure_range(lo, hi);
else if (first) {
/* 首屏:快照式加载(detail 全量 trackIds + 物化首段)。ctx 校验代次/目标,过期自动丢弃。 */
if (tidx < 0) rc = NCM_ERR_ARG;
else rc = appstate_load_playlist_songs_ctx(&tpl, tsearch, tgen, tidx, 0, NCM_SONG_BATCH);
} else {
/* 滑窗补载:物化视口区间(盘缓存优先,批网格补载)。 */
rc = (lo < hi) ? appstate_song_ensure_range(lo, hi) : NCM_OK;
}
pthread_mutex_lock(&g_plmu);
if (first) g_load_err = rc; /* 首屏失败 → 整页空态(点按重试) */
else g_ens_err = rc; /* 补载失败 → snack + 占位行保留(滚动/点按再试) */
if (first) {
if (rc == NCM_ERR_STALE) {
/* 打开 A 未完→已切到 B:本次结果过期(ctx 未写全局),丢弃,tick 对当前目标重拉。 */
g_pl_stale = 1; g_load_err = 0;
} else if (rc != NCM_OK && rc != NCM_ERR_AUTH && g_st.track_ids_count > 0) {
/* 索引成功但曲目批部分失败(缺歌/单批网络):非致命——保留已物化 + snack,
* 不整页 Load Failed(修【92/200 首真机点开即「加载失败」】)。 */
g_ens_err = rc; g_load_err = 0;
} else {
g_load_err = rc; /* 索引失败/鉴权失败 → 整页空态(点按重试) */
}
g_plloaded_idx = tidx;
g_plloaded_gen = tgen;
} else {
g_ens_err = rc; /* 补载失败 → snack + 占位行保留(滚动/点按再试) */
g_plloaded_idx = cur_active;
g_plloaded_gen = tgen;
}
g_plloading = 0;
g_plloaded_idx = my_idx;
g_plgen++;
pthread_mutex_unlock(&g_plmu);
return NULL;
@@ -266,7 +305,19 @@ void playlist_refresh(void)
if (g_plthr_on) return;
g_load_err = 0;
g_ens_err = 0;
/* 打开快照 + 代次(§26):锁内拷目标歌单,detail 加载不依赖 g_st.playlists[](会被
* 回家时 home_worker 并发 memset 污染成空 id → 3 首歌/串数据);同时记录打开代次,
* 过期结果在 worker 完成时判 NCM_ERR_STALE 丢弃(防「开 A→开 B 的迟到结果串 B」)。 */
const ncm_playlist_t *pcur = pl_cur();
if (!pcur || !pcur->id[0]) { g_load_err = NCM_ERR_ARG; pl_apply(1); return; }
pthread_mutex_lock(&g_plmu);
g_tgt.pl = *pcur;
g_tgt.is_search = (g_st.active_playlist == NCM_PL_SEARCH);
g_tgt.idx = g_st.active_playlist;
g_tgt.gen = appstate_playlist_begin(g_tgt.idx);
pthread_mutex_unlock(&g_plmu);
g_ens_first = 1; /* 首屏:重建索引 + 物化首段 */
g_pl_stale = 0;
g_plloading = 1;
g_plthr_on = 1;
if (pthread_create(&g_plthr, NULL, playlist_worker, NULL) != 0) {
@@ -283,7 +334,15 @@ void playlist_tick(uint64_t now)
g_placked = g_plgen;
g_plthr_on = 0;
g_plloading = 0;
if (g_plloaded_idx != g_st.active_playlist) { playlist_refresh(); return; }
/* §26 防串:结果过期(代次不匹配)/已切到别的歌单 → 先清窗口(不把旧歌单数据画在新歌单
* 标题下),再对当前 active_playlist 重新发起。 */
if (g_pl_stale || g_plloaded_idx != g_st.active_playlist ||
g_plloaded_gen != appstate_playlist_open_gen()) {
g_pl_stale = 0;
appstate_playlist_reset();
playlist_refresh();
return;
}
if (g_load_err != 0) {
g_st.track_ids_count = 0; /* 首屏失败 → 整页空态(点按重试) */
pl_apply(1);