Files
MeowOS-For-R3P2/patch_v2/ncm_app/app/logic/appstate.c
T
AskaEth ea645ce2a6 feat(ncm): 风控加固——接口响应短 TTL 缓存 + 游客 MUSIC_A + track/all 死代码
三(按 NCMAPI_NOTES §2.1/§4/§6):
① 接口级响应缓存(对齐参考 apicache 2 分钟,防风控高频 ip 错误):
   - 新增 core/ncm_apicache.{c,h}——内存 4 槽 LRU(单槽≤128KB,TTL 120s,
     键=md5(path+\x01+json))+ 可选盘缓存 <data_dir>/ncm_api_cache/.json
     (8B 墙钟头+body,超 TTL miss 并删,30s 节流清过期)。自包含模块
     (仅 libc+pthread+clock_gettime),可独立链接无符号冲突。
   - ncm_api.c:api_post 拆「请求+解析」两段,新增 api_post_cached;四个只读
     接口接线:user_playlist/playlist_detail/song_detail/cloudsearch。
     song_url/lyric/vip 不入缓存。
   - 手动刷新绕过:ncm_api_cache_refresh_path(path) 一次性标记;home/歌单
     topbar ↻ 接入;playlist_open/search 不标记(走缓存)。
   - 盘层挂载 appstate_init;登出 appstate_logout 全清(防串账号)。
② 游客 MUSIC_A:ncm_req.c weapi+eapi 无 MUSIC_U 且非 login 路径补
   MUSIC_A=anonymous_token(对齐参考 cookie 伪装)。
③ track/all 死代码清理:移除 ncm_api_playlist_track_all(真实服务器 8 形态全
   404,无调用点);ncm_api_parse_track_all 保留(离线重放/单测),注释标注
   「已验证不存在,勿用于真实请求」。

验证:新 bench/apicache_sim.c(0 网络)①TTL 内不重复请求/②过期重拉/③手动
刷新绕过/④cloudsearch 按键区分/⑤清空重拉/⑥盘缓存写入+clear 全 PASS;stream/
player_flow/window/search/cache_evict(1-9)/pagination/playlist_race sim 全
PASS;test_ncm_core host+mipsel(qemu) PASS;build_ncm.sh -Wall -Wextra 0
warning(text 1026914/data 1664/bss 1363460);0 高频真实请求。新 ncm md5
a69c93538920eba9d42a2ebb42f8fdca(留主会话上卡)。
2026-09-02 12:48:34 +08:00

964 lines
41 KiB
C
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/* logic/appstate.c — 应用全局状态:ctx/登录/歌单/歌曲/设置。仅依赖核心库 + SDK meow_config_*。 */
#include "../ncm_app_internal.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#define NCM_DEFAULT_DATA "/usr/data/mnt/sd_0/MeowData/dev.player.ncm/data"
/* 滑动窗口互斥(REPORT §17):窗口读写(滑窗/物化/拷贝)全走此锁,见下方窗口节注释。 */
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;
}
/* 当前窗口归属歌单 id(§NEW bug B:新进歌单显示上一个歌单内容)。窗口数据(song_ids/songs/
* track_ids_count)属于哪个歌单由 id 唯一标识(对搜索歌单同样成立——不同搜索结果 = 不同 id)。
* worker 首屏发布窗口后调 appstate_playlist_window_set 标记;playlist_refresh 在起 worker 前用
* appstate_playlist_window_is 判「目标歌单是否=当前窗口归属」,不一致 → 同步 appstate_playlist_reset()
* 清掉上一歌单残留,防 pl_apply(1) 把新歌单标题绑到上一歌单数据(「开 B 显示 A」)。 */
static pthread_mutex_t g_winpl_mu = PTHREAD_MUTEX_INITIALIZER;
static char g_win_plid[32];
void appstate_playlist_window_set(const char *plid)
{
pthread_mutex_lock(&g_winpl_mu);
if (plid && plid[0]) ncm_strlcpy(g_win_plid, plid, sizeof g_win_plid);
else g_win_plid[0] = 0;
pthread_mutex_unlock(&g_winpl_mu);
}
int appstate_playlist_window_is(const char *plid)
{
pthread_mutex_lock(&g_winpl_mu);
int same = (plid && plid[0] && g_win_plid[0] && strcmp(g_win_plid, plid) == 0);
pthread_mutex_unlock(&g_winpl_mu);
return same;
}
/* 清窗口/索引(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);
pthread_mutex_lock(&g_winpl_mu); /* 窗口已清 → 归属歌单一并清空(下一 worker 重新标记) */
g_win_plid[0] = 0;
pthread_mutex_unlock(&g_winpl_mu);
}
static void appstate_srch_hist_init(void); /* §23:读入本地搜索历史(appstate_init 首调) */
int appstate_init(void)
{
const char *dd = getenv("MEOW_DATA_DIR");
if (!dd || !dd[0]) dd = NCM_DEFAULT_DATA;
strncpy(g_st.data_dir, dd, sizeof g_st.data_dir - 1);
g_st.data_dir[sizeof g_st.data_dir - 1] = 0;
ncm_ctx_init(&g_st.ctx, g_st.data_dir); /* device_id + cookie 载入 + cache */
if (g_st.ctx.cache.dir[0])
ncm_strlcpy(g_st.cache_dir, g_st.ctx.cache.dir, sizeof g_st.cache_dir);
else
ncm_ctx_default_cache_dir(g_st.data_dir, g_st.cache_dir, sizeof g_st.cache_dir);
/* 风控加固 §1:接口级 JSON 响应短 TTL 缓存的盘层挂到 <data_dir>/ncm_api_cache/。
* (内存层由 core 初始化默认启用;盘层跨进程/热更保留,启动清一次过期盘文件。) */
ncm_apicache_init(g_st.data_dir);
appstate_load_settings();
g_st.logged_in = (g_st.ctx.cookie[0] != 0);
g_st.active_playlist = -1;
g_st.playing_song = -1;
g_st.qr_size = 0;
g_st.login_state = NCM_LOGIN_PENDING;
/* uid/nickname 从配置恢复(登录时落盘);uid 缺失时不在此阻塞校验——由 home_reload 拉歌单时
* appstate_ensure_uid 触发 login_status_check(启动快,避免首屏长时间网络阻塞)。 */
meow_config_get("ncm.uid", g_st.uid, sizeof g_st.uid);
meow_config_get("ncm.nickname", g_st.nickname, sizeof g_st.nickname);
account_load(); /* §20:恢复会员态缓存(account.json;无文件则 NCM_VIP_NONE) */
g_st.queue_src = NCM_QUEUE_PLAYLIST; /* §23:默认播放队列=歌单窗口 */
g_st.search_tab = 0;
appstate_srch_hist_init(); /* §23:读取本地搜索历史 */
return 0;
}
void appstate_load_settings(void)
{
g_st.cache_limit_gb = meow_config_int("ncm.cache_limit_gb", 10);
if (g_st.cache_limit_gb < 1) g_st.cache_limit_gb = 1;
if (g_st.cache_limit_gb > 100) g_st.cache_limit_gb = 100;
/* §20 默认档 = 极高 HQ(EXHIGH,用户定案 NCM_CACHE_DESIGN §3;旧默认 Hi-Res 改掉)。
* 已持久化的合法旧值(含 HIRES 等)原样保留;越界值回落默认档。 */
g_st.level = meow_config_int("ncm.level", NCM_LEVEL_EXHIGH);
if (g_st.level < NCM_LEVEL_STANDARD || g_st.level > NCM_LEVEL_UI_MAX)
g_st.level = NCM_LEVEL_EXHIGH;
/* §22 歌词页背景模式:default(模糊置底) / power_save(封面+黑叠加层);越界回落 default */
g_st.player_bg_mode = meow_config_int("ncm.player_bg_mode", NCM_BG_DEFAULT);
if (g_st.player_bg_mode != NCM_BG_DEFAULT && g_st.player_bg_mode != NCM_BG_POWER_SAVE)
g_st.player_bg_mode = NCM_BG_DEFAULT;
ncm_cache_set_limit_gb(&g_st.ctx.cache, g_st.cache_limit_gb);
g_st.ctx.default_level = g_st.level;
}
void appstate_save_settings(void)
{
char b[16];
snprintf(b, sizeof b, "%d", g_st.cache_limit_gb);
meow_config_set("ncm.cache_limit_gb", b);
snprintf(b, sizeof b, "%d", g_st.level);
meow_config_set("ncm.level", b);
snprintf(b, sizeof b, "%d", g_st.player_bg_mode);
meow_config_set("ncm.player_bg_mode", b);
ncm_cache_set_limit_gb(&g_st.ctx.cache, g_st.cache_limit_gb);
g_st.ctx.default_level = g_st.level;
/* 手动调低上限时立即按 LRU 淘汰(超上限删最旧);未超限则为无操作 */
ncm_cache_evict(&g_st.ctx.cache);
}
void appstate_set_logged_in(const char *uid, const char *nickname)
{
if (uid && uid[0]) { ncm_strlcpy(g_st.uid, uid, sizeof g_st.uid); meow_config_set("ncm.uid", uid); }
if (nickname && nickname[0]) {
ncm_strlcpy(g_st.nickname, nickname, sizeof g_st.nickname);
meow_config_set("ncm.nickname", nickname);
}
g_st.logged_in = 1;
}
void appstate_logout(void)
{
ncm_ctx_clear_cookie(&g_st.ctx);
/* 风控加固 §1:登出/换账号清接口级响应缓存(防上一账号缓存的 user/playlist/cloudsearch
* 被下一账号复用;盘缓存目录一并清)。 */
ncm_apicache_clear();
g_st.logged_in = 0;
g_st.uid[0] = 0;
g_st.nickname[0] = 0;
g_st.vip = NCM_VIP_NONE; /* §20:登出清会员态(下一账号不继承) */
g_st.vip_expire_ms = 0;
g_st.vip_checked_at = 0;
{
char ap[600];
snprintf(ap, sizeof ap, "%s/account.json", g_st.data_dir);
remove(ap);
}
g_st.active_playlist = -1;
g_st.playing_song = -1;
pthread_mutex_lock(&g_song_mu);
memset(g_st.song_valid, 0, sizeof g_st.song_valid);
g_st.window_base = 0;
g_st.song_count = 0;
g_st.track_ids_count = 0;
pthread_mutex_unlock(&g_song_mu);
meow_config_remove("ncm.uid");
meow_config_remove("ncm.nickname");
cover_cache_reset();
}
/* 若 uid 缺失(cookie 在但没存 uid),用 login_status_check 校验并取回。返回 0=成功。 */
int appstate_ensure_uid(void)
{
if (!g_st.logged_in || !g_st.ctx.cookie[0]) return NCM_ERR_AUTH;
if (g_st.uid[0]) return NCM_OK;
char uid[32], nick[128];
int rc = ncm_login_status_check(g_st.ctx.device_id, g_st.ctx.cookie, uid, sizeof uid,
nick, sizeof nick);
if (rc == NCM_OK) {
appstate_set_logged_in(uid, nick);
return NCM_OK;
}
/* 会话失效:清 cookie → 未登录 */
if (rc == NCM_ERR_AUTH) {
ncm_ctx_clear_cookie(&g_st.ctx);
g_st.logged_in = 0;
}
return rc;
}
/* 歌单列表 staging 发布锁(§NEW bug A):appstate_load_playlists 的「网络成功后锁内原子发布
* (拷回 g_st.playlists[] + 更新 playlist_count)」与 UI 线程 home_src2/home_draw 无锁逐行绑定
* 之间的发布点。持久化(playlists.bin)同单写者(home worker)在发布后读,无相互竞态。 */
static pthread_mutex_t g_pls_mu = PTHREAD_MUTEX_INITIALIZER;
int appstate_load_playlists(void)
{
if (!g_st.logged_in) return NCM_ERR_AUTH;
int rc = appstate_ensure_uid();
if (rc != NCM_OK) return rc;
/* §NEW bug A(返回首页各行 2~3 秒空白):旧实现在**UI 可见数组**就地 memset→网络回填,与
* home_src2 无锁逐行绑定竞态——刷新期间 playlist_count 保留旧值(cache),UI 读到被清零的行
* (无标题/曲目数 0),仅下标 0 因先读幸存。照搬本文件 appstate_load_playlist_songs_impl 的
* s_build[] staging 范式:网络结果落 staging(s_pls[]),成功后锁内**一次性拷回** g_st.playlists[]
* + 更新 playlist_count(原子发布——UI 全程读到旧或新完整快照,绝不读到清零态);失败保留旧
* 快照(不改 playlists/playlist_count,断网可离线浏览旧列表)。静态数组:单写者(home worker
* 单飞),免 96KB 栈。 */
static ncm_playlist_t s_pls[NCM_PLAYLIST_MAX];
int n = 0;
int rc2 = ncm_api_user_playlist(g_st.ctx.device_id, g_st.ctx.cookie, g_st.uid,
NCM_PLAYLIST_MAX, 0, s_pls, NCM_PLAYLIST_MAX, &n);
if (rc2 == NCM_OK) {
int cnt = (n > NCM_PLAYLIST_MAX) ? NCM_PLAYLIST_MAX : n;
pthread_mutex_lock(&g_pls_mu);
if (cnt > 0) memcpy(g_st.playlists, s_pls, (size_t)cnt * sizeof(ncm_playlist_t));
g_st.playlist_count = cnt; /* count 在拷回后更新:读侧绝不见「新 count + 未拷回行」 */
pthread_mutex_unlock(&g_pls_mu);
appstate_playlists_cache_save(); /* §26:网络成功 → 覆写盘缓存(stale-while-revalidate) */
}
meow_log(MEOW_LOG_INFO, "home: playlists rc=%d count=%d uid=%s", rc2,
(rc2 == NCM_OK) ? n : g_st.playlist_count, g_st.uid);
return rc2;
}
/* ---- 歌单列表磁盘缓存(§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[] 为槽位有效位图。
* 写侧(滑窗/物化)与读侧(song_copy)全走 g_song_mu;网络/盘 IO 在锁外做,
* 锁内只做内存搬移,UI 行回调(song_copy)不被网络阻塞。
* 单写者假设:ensure/page 仅由 playlist worker(及兼容入口)驱动,同一时刻最多一个。
* ============================================================ */
static int slot_valid_locked(int gidx)
{
int s = gidx - g_st.window_base;
return s >= 0 && s < NCM_SONG_WINDOW && g_st.song_valid[s];
}
static void win_recount_locked(void)
{
int n = 0;
for (int i = 0; i < NCM_SONG_WINDOW; i++)
if (g_st.song_valid[i]) n++;
g_st.song_count = n;
}
/* 锁内滑窗:重叠区 songs+valid 一起 memmove 保留,非重叠端清零(出窗槽位失效)。
* 出窗歌曲的元数据此前已落盘(songmeta),丢弃无数据损失。 */
static void win_slide_locked(int new_base)
{
int old_base = g_st.window_base;
if (new_base == old_base) return;
int ov_lo = (old_base > new_base) ? old_base : new_base;
int ov_hi = (old_base + NCM_SONG_WINDOW < new_base + NCM_SONG_WINDOW)
? old_base + NCM_SONG_WINDOW : new_base + NCM_SONG_WINDOW;
if (ov_lo < ov_hi) {
int src = ov_lo - old_base, dst = ov_lo - new_base, n = ov_hi - ov_lo;
memmove(&g_st.songs[dst], &g_st.songs[src], (size_t)n * sizeof(ncm_song_t));
memmove(&g_st.song_valid[dst], &g_st.song_valid[src], (size_t)n);
if (dst > 0) memset(g_st.song_valid, 0, (size_t)dst);
if (dst + n < NCM_SONG_WINDOW)
memset(&g_st.song_valid[dst + n], 0, (size_t)(NCM_SONG_WINDOW - dst - n));
} else {
memset(g_st.song_valid, 0, sizeof g_st.song_valid);
}
g_st.window_base = new_base;
win_recount_locked();
}
int appstate_range_loaded(int lo, int hi)
{
if (lo < 0) lo = 0;
if (hi > g_st.track_ids_count) hi = g_st.track_ids_count;
if (lo >= hi) return 1;
pthread_mutex_lock(&g_song_mu);
int ok = 1;
for (int i = lo; i < hi; i++)
if (!slot_valid_locked(i)) { ok = 0; break; }
pthread_mutex_unlock(&g_song_mu);
return ok;
}
int appstate_song_copy(int gidx, ncm_song_t *out)
{
if (!out) return -1;
pthread_mutex_lock(&g_song_mu);
int s = gidx - g_st.window_base;
int ok = (s >= 0 && s < NCM_SONG_WINDOW && g_st.song_valid[s]);
if (ok) *out = g_st.songs[s];
pthread_mutex_unlock(&g_song_mu);
return ok ? 0 : -1;
}
int appstate_song_ensure_range(int lo, int hi)
{
if (lo < 0) lo = 0;
if (hi > g_st.track_ids_count) hi = g_st.track_ids_count;
if (lo >= hi) return NCM_OK;
/* 目标基址:请求区间居中(前后留余量,滚动反向时不立即滑窗),钳制到合法范围。 */
int base;
if (hi - lo <= NCM_SONG_WINDOW) {
base = (lo + hi) / 2 - NCM_SONG_WINDOW / 2;
if (base > lo) base = lo;
if (base + NCM_SONG_WINDOW < hi) base = hi - NCM_SONG_WINDOW;
int maxb = g_st.track_ids_count - NCM_SONG_WINDOW;
if (maxb < 0) maxb = 0;
if (base > maxb) base = maxb;
if (base < 0) base = 0;
} else {
base = lo; /* 请求区间超窗(不应发生):覆盖前缀 */
}
pthread_mutex_lock(&g_song_mu);
int covered = 1;
for (int i = lo; i < hi; i++)
if (!slot_valid_locked(i)) { covered = 0; break; }
if (!covered && base != g_st.window_base) win_slide_locked(base);
else base = g_st.window_base; /* 已覆盖:维持原基址 */
pthread_mutex_unlock(&g_song_mu);
if (covered) return NCM_OK;
/* 逐段补齐 [lo,hi) 内 invalid 槽:先试盘缓存(songmeta),miss 攒批 ≤NCM_SONG_BATCH
* 走 song_detail;成功写窗 + 落盘。网络/盘 IO 均在锁外,锁内仅写槽。 */
static ncm_song_t s_fetch[NCM_SONG_BATCH]; /* 单写者(playlist worker),免 60KB 栈 */
int 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);
if (va) { a++; continue; }
int b = a + 1;
while (b < hi && b - a < NCM_SONG_BATCH) {
pthread_mutex_lock(&g_song_mu);
int vb = slot_valid_locked(b);
pthread_mutex_unlock(&g_song_mu);
if (vb) break;
b++;
}
/* 批网格对齐预取:把 [a,b) 扩到 song_detail 批边界(§17.4——逐行滚动时若按原始
* 小区间拉取,song_detail 调用次数会放大到 ~歌单长度/滚动步长;对齐到 100 首
* 网格后每次补齐整批,总请求数 ≈ 歌单长度/NCM_SONG_BATCH)。 */
int a0 = (a / NCM_SONG_BATCH) * NCM_SONG_BATCH;
int b0 = ((b + NCM_SONG_BATCH - 1) / NCM_SONG_BATCH) * NCM_SONG_BATCH;
int wend2 = g_st.window_base + NCM_SONG_WINDOW;
if (wend2 > g_st.track_ids_count) wend2 = g_st.track_ids_count;
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 记录待批拉。
* 索引读取在锁内(与「索引重建」写 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)sid64);
ncm_song_t ms;
if (songmeta_load(g_st.cache_dir, sid, &ms) == NCM_OK) {
pthread_mutex_lock(&g_song_mu);
int s = i - g_st.window_base;
if (s >= 0 && s < NCM_SONG_WINDOW) {
g_st.songs[s] = ms;
g_st.song_valid[s] = 1;
}
pthread_mutex_unlock(&g_song_mu);
} else {
ids[m] = sid64;
gslot[m] = i;
m++;
}
}
if (m > 0) {
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) {
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]);
}
/* §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;
}
}
a = (b0 > a) ? b0 : b; /* 推进到本批网格末尾:失败批不在同一次调用内被重复重发 */
}
pthread_mutex_lock(&g_song_mu);
win_recount_locked();
pthread_mutex_unlock(&g_song_mu);
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 (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);
if (songmeta_load(g_st.cache_dir, sid, out) == NCM_OK) return NCM_OK; /* 盘缓存 */
int bn = 0;
int rc = ncm_api_song_detail(g_st.ctx.device_id, g_st.ctx.cookie, &id, 1, out, 1, &bn);
if (rc != NCM_OK) return rc;
if (bn < 1) return NCM_ERR_NOTFOUND;
songmeta_save(g_st.cache_dir, sid, out);
return NCM_OK;
}
void appstate_adopt_songs(int n)
{
pthread_mutex_lock(&g_song_mu);
if (n < 0) n = 0;
if (n > NCM_SONG_WINDOW) n = NCM_SONG_WINDOW;
memset(g_st.song_valid, 0, sizeof g_st.song_valid);
for (int i = 0; i < n; i++) {
g_st.song_valid[i] = 1;
g_st.song_ids[i] = strtoll(g_st.songs[i].id, NULL, 10);
}
g_st.window_base = 0;
g_st.song_count = n;
g_st.track_ids_count = n;
pthread_mutex_unlock(&g_song_mu);
}
/* 核心加载(无代次校验):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)
{
(void)is_search;
if (limit <= 0 || limit > NCM_SONG_BATCH) limit = NCM_SONG_BATCH;
if (offset < 0) offset = 0;
/* REPORT §13.1:/api/playlist/track/all 服务端分页端点真机全 404(NCM 无此路由),弃用。
* §14 的分页(append-only 到 NCM_SONG_MAX=1000 截断)在 §17 升级为滑动窗口:
* detail(n=1)仍取【全量】trackIds 入 song_ids[](≤ NCM_TRACKID_MAX,支撑 6000+ 首),
* 歌曲对象只物化视口附近的 NCM_SONG_WINDOW 首(offset 为「要确保的区间」起点)。 */
if (offset == 0) {
/* 重头:清窗口 + 重建 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);
g_st.window_base = 0;
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, 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;
pthread_mutex_unlock(&g_song_mu);
} else if (g_st.track_ids_count == 0) {
/* 防御:offset>0 但没有索引(不应发生;主路径先走 offset=0)。 */
return NCM_ERR_ARG;
}
if (offset >= g_st.track_ids_count) return NCM_OK; /* 已到尾部 */
int hi = offset + limit;
if (hi > g_st.track_ids_count) hi = g_st.track_ids_count;
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)
{
/* 兼容入口:载索引 + 物化首个窗口。 */
int rc = appstate_load_playlist_songs_page(idx, 0, NCM_SONG_BATCH);
if (rc != NCM_OK) return rc;
int hi = (g_st.track_ids_count < NCM_SONG_WINDOW) ? g_st.track_ids_count : NCM_SONG_WINDOW;
return appstate_song_ensure_range(0, hi);
}
void appstate_apply_playlist_cover(int idx)
{
if (idx == NCM_PL_SEARCH) {
ncm_strlcpy(g_st.cover_url, g_st.search_pl_open.cover, sizeof g_st.cover_url);
return;
}
if (idx < 0 || idx >= g_st.playlist_count) return;
ncm_strlcpy(g_st.cover_url, g_st.playlists[idx].cover, sizeof g_st.cover_url);
}
/* 设置播放页当前歌曲下标。cover_url 非空时一并更新 g_st.cover_url(歌单封面兜底值);
* §18 起调用方传 NULL——兜底值由 appstate_apply_playlist_cover 维护,勿用单曲 al_pic 污染。 */
void appstate_set_playing_song(int idx, const char *cover_url)
{
g_st.playing_song = idx;
if (cover_url && cover_url[0])
ncm_strlcpy(g_st.cover_url, cover_url, sizeof g_st.cover_url);
}
int appstate_current_playing(void)
{
if (g_st.playing_song < 0) return -1;
int cnt = appstate_queue_count(); /* §23:搜索队列时按搜索条数界(不误判歌单亮点) */
if (g_st.playing_song >= cnt) return -1;
return g_st.playing_song;
}
/* ---- 真机联调 autoplay hook(A2 音频迁移验收用,非正式能力)----
* 触发文件 /tmp/ncm_autoplay 存在且 home 歌单已载 → 后台线程找「已缓存」曲目并播放:
* - 内容为纯数字 → 从该歌单下标起扫最多 AUTOPLAY_SCAN_PL 个歌单首页;
* - 内容为其它文本 → 按歌单名子串匹配**单个**歌单(如 "Docu";避开超大歌单的
* 全量 trackIds 解析,真机联调省时省内存);
* - 空文件 → 从 0 起扫。
* 风控纪律:**绝不发 song_url 下载**——无缓存命中只记日志退出;metadata 请求 = 每歌单
* 1 detail + 1 song_detail(≤ 2×AUTOPLAY_SCAN_PL 次)。 */
#define AUTOPLAY_TRIGGER "/tmp/ncm_autoplay"
#define AUTOPLAY_SCAN_PL 3
/* 真机崩溃复现扩展:autoplay 在后台线程起播后,把「打开播放页」动作交给 UI 线程
* (meow_view_page_push/view_enter 非线程安全,UI 栈只能 UI 线程驱动)。worker 写
* g_autoplay_* 标志,app_on_tick 每 tick 用 autoplay_ui_consume() 取走并调 player_play_song,
* 从而完整复现用户「点曲 → 播放页渲染(封面/歌词/动画/进度)」路径。仅 /tmp/ncm_autoplay 存在时激活。 */
static volatile int g_autoplay_goto_player = 0;
static int g_autoplay_play_idx = -1;
int autoplay_ui_consume(void) /* UI 线程取走「已缓存起播」标志,返回曲目 idx(无则 -1) */
{
if (!g_autoplay_goto_player) return -1;
g_autoplay_goto_player = 0;
int i = g_autoplay_play_idx;
g_autoplay_play_idx = -1;
return i;
}
/* 播放单个歌单首页中第一首已缓存曲目。命中播放返回 1,否则 0。 */
static int autoplay_try_playlist(int pl)
{
int rc = appstate_load_playlist_songs_page(pl, 0, NCM_SONG_BATCH);
if (rc != NCM_OK) {
meow_log(MEOW_LOG_WARN, "autoplay: playlist[%d] load rc=%d", pl, rc);
return 0;
}
char cpath[600];
for (int i = 0; i < g_st.track_ids_count; i++) { /* 滑窗模型:锁内拷贝窗内有效槽 */
ncm_song_t s;
if (appstate_song_copy(i, &s) != 0) break; /* 出窗即停(首页之后未物化) */
/* §19 缓存键 = id+档位(§20 起用 level_key:sky 与 sky_c51 可区分):
* 先试当前档位键(flac/mp3),再回退旧无档位键(存量 mp3) */
char key[64];
snprintf(key, sizeof key, "%s_%s", s.id, ncm_api_level_key(g_st.level));
if (ncm_cache_lookup(&g_st.ctx.cache, key, "flac", cpath, sizeof cpath) == NCM_OK ||
ncm_cache_lookup(&g_st.ctx.cache, key, "mp3", cpath, sizeof cpath) == NCM_OK ||
ncm_cache_lookup(&g_st.ctx.cache, s.id, "mp3", cpath, sizeof cpath) == NCM_OK) {
g_st.active_playlist = pl;
meow_log(MEOW_LOG_INFO, "autoplay: playlist[%d] song[%d] id=%s cached -> play",
pl, i, s.id);
/* 交给 UI 线程打开播放页(模拟用户点曲播放),避免后台线程调 UI 栈 */
g_autoplay_play_idx = i;
g_autoplay_goto_player = 1;
return 1;
}
}
meow_log(MEOW_LOG_INFO, "autoplay: playlist[%d] first window %d/%d songs, no cached track",
pl, g_st.song_count, g_st.track_ids_count);
return 0;
}
static void *autoplay_worker(void *arg)
{
const char *sel = (const char *)arg; /* NULL=从 0 起扫;数字=起始下标;其它=歌单名子串 */
if (sel && sel[0] && !(sel[0] >= '0' && sel[0] <= '9')) {
for (int pl = 0; pl < g_st.playlist_count && !g_app_quit; pl++) {
if (strstr(g_st.playlists[pl].name, sel)) {
meow_log(MEOW_LOG_INFO, "autoplay: name '%s' -> playlist[%d] '%s' (%d tracks)",
sel, pl, g_st.playlists[pl].name, g_st.playlists[pl].track_count);
if (autoplay_try_playlist(pl)) { free((void *)sel); return NULL; }
break;
}
}
meow_log(MEOW_LOG_WARN, "autoplay: no playlist name match / no cached track ('%s')", sel);
free((void *)sel);
return NULL;
}
int start = (sel && sel[0]) ? atoi(sel) : 0;
free((void *)sel);
if (start < 0) start = 0;
int end = start + AUTOPLAY_SCAN_PL;
if (end > g_st.playlist_count) end = g_st.playlist_count;
for (int pl = start; pl < end && !g_app_quit; pl++)
if (autoplay_try_playlist(pl)) return NULL;
meow_log(MEOW_LOG_WARN, "autoplay: no cached track found (no download, risk control)");
return NULL;
}
void autoplay_maybe(void)
{
static int s_fired = 0;
if (s_fired || g_app_quit) return;
if (access(AUTOPLAY_TRIGGER, F_OK) != 0) return;
if (!g_st.logged_in || g_st.playlist_count <= 0) return; /* 等 home 歌单载完 */
s_fired = 1;
char buf[64] = "";
FILE *f = fopen(AUTOPLAY_TRIGGER, "r"); /* 一次性:读完即删 */
if (f) {
if (fgets(buf, sizeof buf, f)) {
size_t n = strlen(buf);
while (n && (buf[n - 1] == '\n' || buf[n - 1] == '\r' || buf[n - 1] == ' ')) buf[--n] = 0;
}
fclose(f);
}
unlink(AUTOPLAY_TRIGGER);
meow_log(MEOW_LOG_INFO, "autoplay: triggered, sel='%s'", buf);
char *sel = strdup(buf);
pthread_t t;
if (ncm_thread_create(&t, autoplay_worker, sel) == 0) pthread_detach(t);
else free(sel);
}
/* ============================================================
* 搜索(REPORT §23):独立结果数组(不覆写歌单窗口)+ 播放队列来源 + 本地历史。
* 结果数组读写经 g_search_mu;queue_src 决定超时/起播读数组还是窗口。
* ============================================================ */
void appstate_set_queue(int src) { g_st.queue_src = src; }
int appstate_get_queue(void) { return g_st.queue_src; }
int appstate_queue_count(void)
{
int c;
pthread_mutex_lock(&g_search_mu);
c = (g_st.queue_src == NCM_QUEUE_SEARCH) ? g_st.search_song_count : g_st.track_ids_count;
pthread_mutex_unlock(&g_search_mu);
return c < 0 ? 0 : c;
}
int appstate_queue_song(int idx, ncm_song_t *out)
{
if (!out) return NCM_ERR_ARG;
if (g_st.queue_src == NCM_QUEUE_SEARCH) {
pthread_mutex_lock(&g_search_mu);
int ok = (idx >= 0 && idx < g_st.search_song_count);
if (ok) *out = g_st.search_songs[idx];
pthread_mutex_unlock(&g_search_mu);
return ok ? NCM_OK : NCM_ERR_ARG;
}
if (appstate_song_copy(idx, out) == 0) return NCM_OK;
return appstate_song_fetch(idx, out);
}
int appstate_search_count(int tab)
{
int c;
pthread_mutex_lock(&g_search_mu);
c = (tab == 0) ? g_st.search_song_count : g_st.search_pl_count;
pthread_mutex_unlock(&g_search_mu);
return c;
}
int appstate_search_offset(int tab)
{
int o;
pthread_mutex_lock(&g_search_mu);
o = (tab == 0) ? g_st.search_song_offset : g_st.search_pl_offset;
pthread_mutex_unlock(&g_search_mu);
return o;
}
int appstate_search_has_more(int tab)
{
int m;
pthread_mutex_lock(&g_search_mu);
m = (tab == 0) ? g_st.search_song_more : g_st.search_pl_more;
pthread_mutex_unlock(&g_search_mu);
return m;
}
int appstate_search_song(int i, ncm_song_t *out)
{
if (!out) return NCM_ERR_ARG;
pthread_mutex_lock(&g_search_mu);
int ok = (i >= 0 && i < g_st.search_song_count);
if (ok) *out = g_st.search_songs[i];
pthread_mutex_unlock(&g_search_mu);
return ok ? NCM_OK : NCM_ERR_ARG;
}
int appstate_search_pl(int i, ncm_playlist_t *out)
{
if (!out) return NCM_ERR_ARG;
pthread_mutex_lock(&g_search_mu);
int ok = (i >= 0 && i < g_st.search_pl_count);
if (ok) *out = g_st.search_pls[i];
pthread_mutex_unlock(&g_search_mu);
return ok ? NCM_OK : NCM_ERR_ARG;
}
void appstate_search_clear(int tab)
{
pthread_mutex_lock(&g_search_mu);
if (tab == 0) {
g_st.search_song_count = 0; g_st.search_song_offset = 0;
g_st.search_song_total = -1; g_st.search_song_more = 1;
memset(g_st.search_songs, 0, sizeof g_st.search_songs);
} else {
g_st.search_pl_count = 0; g_st.search_pl_offset = 0;
g_st.search_pl_total = -1; g_st.search_pl_more = 1;
memset(g_st.search_pls, 0, sizeof g_st.search_pls);
}
pthread_mutex_unlock(&g_search_mu);
}
/* 追加单曲结果(worker 锁内):写 g_st.search_songs[count..),回填 offset/total/has_more。 */
void appstate_search_append_songs(int n, const ncm_song_t *s, int64_t total)
{
if (n <= 0 || !s) return;
pthread_mutex_lock(&g_search_mu);
int room = NCM_SEARCH_SONG_MAX - g_st.search_song_count;
if (n > room) n = room;
if (n > 0) memcpy(&g_st.search_songs[g_st.search_song_count], s, (size_t)n * sizeof(ncm_song_t));
g_st.search_song_count += n;
g_st.search_song_offset = g_st.search_song_count;
g_st.search_song_total = (total >= 0) ? (int)total : g_st.search_song_total;
int t = g_st.search_song_total;
g_st.search_song_more = (t >= 0) ? (g_st.search_song_count < t) : (n >= NCM_SEARCH_PAGE);
pthread_mutex_unlock(&g_search_mu);
}
void appstate_search_append_pls(int n, const ncm_playlist_t *p, int64_t total)
{
if (n <= 0 || !p) return;
pthread_mutex_lock(&g_search_mu);
int room = NCM_SEARCH_PL_MAX - g_st.search_pl_count;
if (n > room) n = room;
if (n > 0) memcpy(&g_st.search_pls[g_st.search_pl_count], p, (size_t)n * sizeof(ncm_playlist_t));
g_st.search_pl_count += n;
g_st.search_pl_offset = g_st.search_pl_count;
g_st.search_pl_total = (total >= 0) ? (int)total : g_st.search_pl_total;
int t = g_st.search_pl_total;
g_st.search_pl_more = (t >= 0) ? (g_st.search_pl_count < t) : (n >= NCM_SEARCH_PAGE);
pthread_mutex_unlock(&g_search_mu);
}
/* ---- 搜索历史(≤NCM_SEARCH_HIST_MAX,最新在前;单键 meow_config 持久化,条目以 \n 连接)---- */
#define NCM_SEARCH_HIST_KEY "ncm.search_history"
static char g_hist[NCM_SEARCH_HIST_MAX][NCM_SEARCH_HIST_LEN];
static int g_hist_count;
static int g_hist_loaded;
static void srch_hist_load(void)
{
char buf[2048];
if (meow_config_get(NCM_SEARCH_HIST_KEY, buf, sizeof buf) != MEOW_CFG_OK) { g_hist_count = 0; return; }
g_hist_count = 0;
char *p = buf;
while (p && *p && g_hist_count < NCM_SEARCH_HIST_MAX) {
char *nl = strchr(p, '\n');
size_t len = nl ? (size_t)(nl - p) : strlen(p);
if (len >= NCM_SEARCH_HIST_LEN) len = NCM_SEARCH_HIST_LEN - 1;
if (len > 0) { memcpy(g_hist[g_hist_count], p, len); g_hist[g_hist_count][len] = 0; g_hist_count++; }
if (!nl) break;
p = nl + 1;
}
}
static void srch_hist_save(void)
{
char buf[2048];
size_t bl = 0;
buf[0] = 0;
for (int i = 0; i < g_hist_count; i++) {
int n = snprintf(buf + bl, sizeof buf - bl, "%s%s", i ? "\n" : "", g_hist[i]);
if (n < 0) break;
bl += (size_t)n;
if (bl >= sizeof buf - 1) break;
}
meow_config_set(NCM_SEARCH_HIST_KEY, buf);
}
static void appstate_srch_hist_init(void)
{
if (!g_hist_loaded) { srch_hist_load(); g_hist_loaded = 1; }
}
int appstate_search_history_count(void)
{
appstate_srch_hist_init();
return g_hist_count;
}
int appstate_search_history_get(int i, char *out, size_t sz)
{
appstate_srch_hist_init();
if (i < 0 || i >= g_hist_count) return 0;
ncm_strlcpy(out, g_hist[i], sz);
return 1;
}
void appstate_search_history_add(const char *kw)
{
if (!kw || !kw[0]) return;
appstate_srch_hist_init();
char tmp[NCM_SEARCH_HIST_MAX][NCM_SEARCH_HIST_LEN];
int m = 0;
ncm_strlcpy(tmp[m], kw, sizeof tmp[m]);
m++;
for (int i = 0; i < g_hist_count && m < NCM_SEARCH_HIST_MAX; i++)
if (strcmp(g_hist[i], kw) != 0) { ncm_strlcpy(tmp[m], g_hist[i], sizeof tmp[m]); m++; }
g_hist_count = m;
memcpy(g_hist, tmp, sizeof g_hist);
srch_hist_save();
}
void appstate_search_history_remove(int i)
{
appstate_srch_hist_init();
if (i < 0 || i >= g_hist_count) return;
for (int j = i; j < g_hist_count - 1; j++)
ncm_strlcpy(g_hist[j], g_hist[j + 1], sizeof g_hist[j]);
g_hist_count--;
srch_hist_save();
}
void appstate_search_history_clear(void)
{
g_hist_count = 0;
g_hist_loaded = 1;
srch_hist_save();
}