Files
MeowOS-For-R3P2/patch_v2/ncm_app/app/logic/appstate.c
T
MeowOS 1e97dbf54a ncm search: 历史+单曲/歌单分类+分页+误触修复+不覆写歌单上下文 (§23)
- 搜索历史:进页自动弹键盘;meow_config 持久化 ≤12 去重最新在前;空态=历史列表(点搜/左滑删/清空)
- 结果分类单曲/歌单页签:独立结果集+独立滚动;歌单行进详情(playlist_open_pl);单曲行播放
- 触底分页:offset 累加 limit=30/页 cloudsearch 同参数;每类各记 offset
- 误触抑制:g_press_row + g_press_delta>10(对齐 home/playlist §13.3)
- 结果改独立数组(不写 g_st.songs[]);播放搜索单曲建搜索队列(queue_src=SEARCH),不弄丢歌单窗口
- core cloudsearch 扩展 type/offset/limit/total + 歌单解析
- bench/search_sim.c 离线回归(历史/独立集/翻页 offset/队列上下文);host searchtap 误触自检

验证:mipsel -Wall -Wextra 0 warning;core host/mipsel PASS;search_sim PASS;
宿主离屏 search(单曲)/searchpl(歌单)/searchhist(空态历史)/searchtap(误触) 均正常;0 真实 NCM 请求。
2026-09-01 19:21:16 +08:00

752 lines
29 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;
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);
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);
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;
}
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;
memset(g_st.playlists, 0, sizeof g_st.playlists);
int n = 0;
rc = ncm_api_user_playlist(g_st.ctx.device_id, g_st.ctx.cookie, g_st.uid,
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);
return rc;
}
/* ============================================================
* 滑动窗口(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;
for (int a = lo; a < hi && rc == NCM_OK; ) {
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 记录待批拉 */
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);
pthread_mutex_unlock(&g_song_mu);
if (vv) continue;
char sid[24];
snprintf(sid, sizeof sid, "%lld", (long long)g_st.song_ids[i]);
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] = g_st.song_ids[i];
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) { 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;
}
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]);
}
if (bn < m) rc = NCM_ERR_JSON; /* 部分返回:缺槽留 invalid,下次 ensure 再试 */
}
a = b;
}
pthread_mutex_lock(&g_song_mu);
win_recount_locked();
pthread_mutex_unlock(&g_song_mu);
return 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;
int64_t id = g_st.song_ids[gidx];
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);
}
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”。 */
const ncm_playlist_t *pl = (idx == NCM_PL_SEARCH) ? &g_st.search_pl_open : &g_st.playlists[idx];
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 无论成败都归零)。 */
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);
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);
if (rc != NCM_OK) return rc;
g_st.track_ids_count = idn;
appstate_apply_playlist_cover(idx);
} 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(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
/* 播放单个歌单首页中第一首已缓存曲目。命中播放返回 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);
player_manager_play(i);
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 (pthread_create(&t, NULL, 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();
}