- 搜索历史:进页自动弹键盘;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 请求。
752 lines
29 KiB
C
752 lines
29 KiB
C
/* 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();
|
||
}
|