feat(ncm): 网易云客户端 v1 UI+播放链路 (agent-205) — os.meow.ncm: 登录页(真实unikey二维码+轮询状态机)/我的歌单(listview)/歌单详情/播放页(封面+seek+滚动歌词+竖横屏切换)/设置(音质默认Hi-res+缓存上限默认10G); 播放线程(缓存LRU+minimp3→ALSA UAPI直开hw:0,0); 媒体会话MSG_META上报+PLAYCTL接收(首真实媒体源)
ncm 1e331964 (text 764KB); 0 warning; 宿主离屏6页截图目检+真实QR可扫+qemu-mipsel干净启动+解码446xRT 真机待联调: ALSA时序/803扫码闭环/媒体会话联动/gbk大字库渲染
This commit is contained in:
1 parent
32543ac616
commit
21519ce2e4
23 files changed
+3255
No files matched your search
@@ -0,0 +1,165 @@
|
||||
/* logic/appstate.c — 应用全局状态:ctx/登录/歌单/歌曲/设置。仅依赖核心库 + SDK meow_config_*。 */
|
||||
#include "../ncm_app_internal.h"
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
#define NCM_DEFAULT_DATA "/usr/data/mnt/sd_0/MeowData/os.meow.ncm/data"
|
||||
|
||||
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])
|
||||
strncpy(g_st.cache_dir, g_st.ctx.cache.dir, sizeof g_st.cache_dir - 1);
|
||||
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);
|
||||
|
||||
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;
|
||||
|
||||
g_st.level = meow_config_int("ncm.level", NCM_LEVEL_HIRES);
|
||||
if (g_st.level < NCM_LEVEL_STANDARD || g_st.level > NCM_LEVEL_HIRES)
|
||||
g_st.level = NCM_LEVEL_HIRES;
|
||||
|
||||
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);
|
||||
ncm_cache_set_limit_gb(&g_st.ctx.cache, g_st.cache_limit_gb);
|
||||
g_st.ctx.default_level = g_st.level;
|
||||
}
|
||||
|
||||
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.active_playlist = -1;
|
||||
g_st.playing_song = -1;
|
||||
g_st.song_count = 0;
|
||||
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;
|
||||
return rc;
|
||||
}
|
||||
|
||||
int appstate_load_playlist_songs(int idx)
|
||||
{
|
||||
if (idx < 0 || idx >= g_st.playlist_count) return NCM_ERR_ARG;
|
||||
g_st.active_playlist = idx;
|
||||
const ncm_playlist_t *pl = &g_st.playlists[idx];
|
||||
|
||||
int idn = 0;
|
||||
int rc = ncm_api_playlist_detail(g_st.ctx.device_id, g_st.ctx.cookie, pl->id,
|
||||
NULL, 0, NULL, 0, g_st.song_ids, NCM_SONG_MAX, &idn);
|
||||
if (rc != NCM_OK) { g_st.song_count = 0; return rc; }
|
||||
|
||||
memset(g_st.songs, 0, sizeof g_st.songs);
|
||||
int outn = 0;
|
||||
for (int off = 0; off < idn; off += NCM_SONG_BATCH) {
|
||||
int batch = idn - off;
|
||||
if (batch > NCM_SONG_BATCH) batch = NCM_SONG_BATCH;
|
||||
int bn = 0;
|
||||
rc = ncm_api_song_detail(g_st.ctx.device_id, g_st.ctx.cookie, &g_st.song_ids[off],
|
||||
batch, &g_st.songs[off], NCM_SONG_MAX - off, &bn);
|
||||
if (rc != NCM_OK) break;
|
||||
outn += bn;
|
||||
if (bn < batch) break;
|
||||
}
|
||||
g_st.song_count = outn;
|
||||
appstate_apply_playlist_cover(idx);
|
||||
return NCM_OK;
|
||||
}
|
||||
|
||||
void appstate_apply_playlist_cover(int idx)
|
||||
{
|
||||
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);
|
||||
}
|
||||
|
||||
/* 设置播放页当前歌曲 + 封面 URL(player 页 UI 读)。idx=-1 表示清除。 */
|
||||
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 || g_st.playing_song >= g_st.song_count) return -1;
|
||||
return g_st.playing_song;
|
||||
}
|
||||
@@ -0,0 +1,141 @@
|
||||
/* logic/cover.c — 封面下载(HTTPS)+ 限幅 + 解码缓存(单张,URL 变更即换;后台线程)。
|
||||
*
|
||||
* 内存纪律:下载限幅 NCM_COVER_MAX(~288KB);解码后的 RGBA 保持单张(换封面先 free 旧再解码)。
|
||||
* 线程模型:一个持久的 cover 工作线程负责「目标 URL → 下载 → 解码 → 换入」;UI 线程只经
|
||||
* cover_request(url)(提交目标)与 cover_get()/cover_changed()(读取/判变)+ cover_ack()(消费)。
|
||||
* 工作线程不碰 meow_ui(绘制在 UI 线程 tick 判 cover_changed 后触发 invalidate)。
|
||||
*/
|
||||
#include "../ncm_app_internal.h"
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
#include <stdlib.h>
|
||||
#include <unistd.h>
|
||||
|
||||
static pthread_mutex_t g_cm = PTHREAD_MUTEX_INITIALIZER;
|
||||
static meow_img *g_cover_img;
|
||||
static char g_cover_url[512]; /* g_cover_img 对应的 URL */
|
||||
static char g_tgt_url[512]; /* 目标 URL(worker 拉取) */
|
||||
static pthread_t g_thr;
|
||||
static int g_thr_on;
|
||||
static int g_quit;
|
||||
static volatile int g_gen; /* 每次换入 +1 */
|
||||
static int g_acked; /* UI 已消费的版本 */
|
||||
static char g_cover_path[512]; /* 封面原始文件路径(供 launcher) */
|
||||
|
||||
static unsigned char g_buf[NCM_COVER_MAX]; /* 单张下载缓冲(常驻) */
|
||||
|
||||
static meow_img *decode_cover(const char *url, size_t *out_len)
|
||||
{
|
||||
if (!url || !url[0]) return NULL;
|
||||
ncm_http_req_t req;
|
||||
memset(&req, 0, sizeof req);
|
||||
req.url = url;
|
||||
req.method = "GET";
|
||||
req.extra_hdrs = "User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) "
|
||||
"AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36";
|
||||
req.verify_ca = NCM_HTTP_VERIFY_DEFAULT;
|
||||
req.timeout_ms = 10000;
|
||||
ncm_http_resp_t resp;
|
||||
int rc = ncm_http_request(&req, (char *)g_buf, sizeof g_buf, &resp);
|
||||
if (rc != NCM_OK || resp.status != 200 || resp.body_len == 0) return NULL;
|
||||
if (out_len) *out_len = resp.body_len;
|
||||
return meow_img_load_mem((const void *)resp.body, (int)resp.body_len);
|
||||
}
|
||||
|
||||
static void save_cover_raw(const char *url, size_t len)
|
||||
{
|
||||
if (!url || len == 0) return;
|
||||
if (g_st.cache_dir[0]) {
|
||||
char path[512];
|
||||
snprintf(path, sizeof path, "%s/cover_current.jpg", g_st.cache_dir);
|
||||
ncm_store_write_file(path, g_buf, len);
|
||||
pthread_mutex_lock(&g_cm);
|
||||
ncm_strlcpy(g_cover_path, path, sizeof g_cover_path);
|
||||
pthread_mutex_unlock(&g_cm);
|
||||
}
|
||||
(void)url;
|
||||
}
|
||||
|
||||
static void *cover_worker(void *arg)
|
||||
{
|
||||
(void)arg;
|
||||
while (!g_quit) {
|
||||
pthread_mutex_lock(&g_cm);
|
||||
if (!g_tgt_url[0]) { pthread_mutex_unlock(&g_cm); usleep(250 * 1000); continue; }
|
||||
char url[512];
|
||||
ncm_strlcpy(url, g_tgt_url, sizeof url);
|
||||
pthread_mutex_unlock(&g_cm);
|
||||
|
||||
size_t blen = 0;
|
||||
meow_img *img = decode_cover(url, &blen);
|
||||
if (!img) { usleep(500 * 1000); continue; }
|
||||
|
||||
pthread_mutex_lock(&g_cm);
|
||||
/* 目标仍是 url 才换入(期间未提交更早) */
|
||||
if (g_tgt_url[0] && strcmp(g_tgt_url, url) == 0) {
|
||||
if (g_cover_img) meow_img_free(g_cover_img);
|
||||
g_cover_img = img;
|
||||
ncm_strlcpy(g_cover_url, url, sizeof g_cover_url);
|
||||
save_cover_raw(url, blen);
|
||||
g_gen++;
|
||||
} else {
|
||||
meow_img_free(img);
|
||||
}
|
||||
pthread_mutex_unlock(&g_cm);
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
|
||||
/* UI 线程:提交目标封面 URL(无网络/无 URL 则忽略)。 */
|
||||
void cover_request(const char *url)
|
||||
{
|
||||
if (!url || !url[0]) return;
|
||||
pthread_mutex_lock(&g_cm);
|
||||
if (g_cover_url[0] && strcmp(g_cover_url, url) == 0) { pthread_mutex_unlock(&g_cm); return; }
|
||||
ncm_strlcpy(g_tgt_url, url, sizeof g_tgt_url);
|
||||
pthread_mutex_unlock(&g_cm);
|
||||
if (!g_thr_on) {
|
||||
g_thr_on = 1;
|
||||
if (pthread_create(&g_thr, NULL, cover_worker, NULL) == 0)
|
||||
pthread_detach(g_thr);
|
||||
else
|
||||
g_thr_on = 0;
|
||||
}
|
||||
}
|
||||
|
||||
/* UI 线程:读取当前封面(不拥有)。 */
|
||||
meow_img *cover_get(void)
|
||||
{
|
||||
meow_img *im;
|
||||
pthread_mutex_lock(&g_cm);
|
||||
im = g_cover_img;
|
||||
pthread_mutex_unlock(&g_cm);
|
||||
return im;
|
||||
}
|
||||
|
||||
/* 是否有新封面待绘制(UI tick 判变)。 */
|
||||
int cover_changed(void) { return g_gen != g_acked; }
|
||||
/* UI 消费一次:标记已见当前版本。 */
|
||||
void cover_ack(void) { g_acked = g_gen; }
|
||||
|
||||
/* 封面原始文件路径(供 launcher 媒体卡片读);NULL=无。 */
|
||||
const char *cover_current_path(void)
|
||||
{
|
||||
const char *p;
|
||||
pthread_mutex_lock(&g_cm);
|
||||
p = g_cover_path[0] ? g_cover_path : NULL;
|
||||
pthread_mutex_unlock(&g_cm);
|
||||
return p;
|
||||
}
|
||||
|
||||
void cover_cache_reset(void)
|
||||
{
|
||||
pthread_mutex_lock(&g_cm);
|
||||
if (g_cover_img) { meow_img_free(g_cover_img); g_cover_img = NULL; }
|
||||
g_cover_url[0] = 0;
|
||||
g_tgt_url[0] = 0;
|
||||
g_cover_path[0] = 0;
|
||||
pthread_mutex_unlock(&g_cm);
|
||||
g_gen++;
|
||||
g_acked = g_gen;
|
||||
}
|
||||
@@ -0,0 +1,139 @@
|
||||
/* logic/lyric.c — LRC 时间轴解析 + 当前行定位。
|
||||
*
|
||||
* 输入:ncm_api_lyric 返回的 lrc(原文)与 tlrc(翻译)。两者均为标准 [mm:ss.xx] 行。
|
||||
* 解析:把所有带时间戳的行按时间升序排序,存 timestamp(ms)/text;翻译按相同时间戳合并到
|
||||
* 对应原文行(存 tl)。无匹配的翻译行忽略。流式、有界:NCM_LYRIC_LINES 行,超限截断。
|
||||
* 纯 C 无 UI,仅依赖标准库。
|
||||
*/
|
||||
#include "../ncm_app_internal.h"
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
#define LRC_MAX_TEXT 192
|
||||
static long s_t[NCM_LYRIC_LINES];
|
||||
static char s_txt[NCM_LYRIC_LINES][LRC_MAX_TEXT];
|
||||
static char s_tl[NCM_LYRIC_LINES][LRC_MAX_TEXT];
|
||||
static int s_n;
|
||||
|
||||
static void lrc_parse(const char *lrc, int is_trans)
|
||||
{
|
||||
if (!lrc) return;
|
||||
const char *p = lrc;
|
||||
while (*p) {
|
||||
/* 找行首 '[' */
|
||||
while (*p && *p != '[') p++;
|
||||
if (!*p) break;
|
||||
/* 解析一个或多个时间标签 */
|
||||
int have = 0;
|
||||
long first_ms = 0;
|
||||
while (*p == '[') {
|
||||
int m = 0, s = 0, cs = 0;
|
||||
if (sscanf(p, "[%d:%d.%d]", &m, &s, &cs) >= 2) {
|
||||
long ms = (long)m * 60000L + (long)s * 1000L + (long)cs * 10L;
|
||||
if (!have) { first_ms = ms; have = 1; }
|
||||
/* 跳到本标签结尾 */
|
||||
const char *q = strchr(p, ']');
|
||||
if (!q) break;
|
||||
p = q + 1;
|
||||
} else {
|
||||
/* 非时间标签([ti:..] 等)——跳过到行尾 */
|
||||
const char *q = strchr(p, ']');
|
||||
if (!q) return;
|
||||
p = q + 1;
|
||||
/* 若行内还有时间标签继续解析(极少),否则整行的正文以第一个文本为准 */
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (have) {
|
||||
/* 正文 = 到行尾(去 \r\n) */
|
||||
const char *e = p;
|
||||
while (*e && *e != '\n' && *e != '\r') e++;
|
||||
size_t len = (size_t)(e - p);
|
||||
if (len > LRC_MAX_TEXT - 1) len = LRC_MAX_TEXT - 1;
|
||||
if (is_trans) {
|
||||
/* 翻译行:按 first_ms 匹配到已有原文行 */
|
||||
for (int i = 0; i < s_n; i++) {
|
||||
if (s_t[i] == first_ms) {
|
||||
memcpy(s_tl[i], p, len); s_tl[i][len] = 0;
|
||||
break;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (s_n < NCM_LYRIC_LINES) {
|
||||
s_t[s_n] = first_ms;
|
||||
memcpy(s_txt[s_n], p, len); s_txt[s_n][len] = 0;
|
||||
s_tl[s_n][0] = 0;
|
||||
s_n++;
|
||||
}
|
||||
}
|
||||
}
|
||||
while (*p && *p != '\n') p++; /* 跳到下一行 */
|
||||
if (*p == '\n') p++;
|
||||
}
|
||||
}
|
||||
|
||||
static int cmp_lines(const void *a, const void *b)
|
||||
{
|
||||
const int *ia = (const int *)a, *ib = (const int *)b;
|
||||
if (s_t[*ia] < s_t[*ib]) return -1;
|
||||
if (s_t[*ia] > s_t[*ib]) return 1;
|
||||
return 0;
|
||||
}
|
||||
|
||||
int lyric_parse(const char *lrc, const char *tlrc)
|
||||
{
|
||||
lyric_clear();
|
||||
lrc_parse(lrc, 0);
|
||||
lrc_parse(tlrc, 1);
|
||||
/* 按时间排序(用索引数组) */
|
||||
int *idx = (int *)malloc(sizeof(int) * (size_t)(s_n ? s_n : 1));
|
||||
if (!idx) return s_n;
|
||||
for (int i = 0; i < s_n; i++) idx[i] = i;
|
||||
qsort(idx, (size_t)s_n, sizeof(int), cmp_lines);
|
||||
long ttmp[NCM_LYRIC_LINES];
|
||||
char txt[NCM_LYRIC_LINES][LRC_MAX_TEXT];
|
||||
char tl[NCM_LYRIC_LINES][LRC_MAX_TEXT];
|
||||
memcpy(ttmp, s_t, sizeof(s_t));
|
||||
memcpy(txt, s_txt, sizeof(s_txt));
|
||||
memcpy(tl, s_tl, sizeof(s_tl));
|
||||
for (int i = 0; i < s_n; i++) {
|
||||
s_t[i] = ttmp[idx[i]];
|
||||
memcpy(s_txt[i], txt[idx[i]], LRC_MAX_TEXT);
|
||||
memcpy(s_tl[i], tl[idx[i]], LRC_MAX_TEXT);
|
||||
}
|
||||
free(idx);
|
||||
return s_n;
|
||||
}
|
||||
|
||||
int lyric_line_at(int64_t ms)
|
||||
{
|
||||
int line = 0;
|
||||
for (int i = 0; i < s_n; i++) {
|
||||
if (s_t[i] <= ms) line = i;
|
||||
else break;
|
||||
}
|
||||
return line;
|
||||
}
|
||||
|
||||
const char *lyric_line_text(int i)
|
||||
{
|
||||
if (i < 0 || i >= s_n) return "";
|
||||
return s_txt[i];
|
||||
}
|
||||
|
||||
const char *lyric_line_tl(int i)
|
||||
{
|
||||
if (i < 0 || i >= s_n) return "";
|
||||
return s_tl[i];
|
||||
}
|
||||
|
||||
int lyric_count(void) { return s_n; }
|
||||
|
||||
void lyric_clear(void)
|
||||
{
|
||||
s_n = 0;
|
||||
memset(s_t, 0, sizeof(s_t));
|
||||
memset(s_txt, 0, sizeof(s_txt));
|
||||
memset(s_tl, 0, sizeof(s_tl));
|
||||
}
|
||||
Reference in new issue
Block a user