Files
MeowOS-For-R3P2/patch_v2/ncm_app/app/ui/login.c
T
AskaEth cd7329d141 feat(ncm): MUSIC_U 临期主动续期 — 启动查龄超阈值后台静默刷新/eapi 会话锚点/cookie 合并+冷却
用户定案:失效前用当前 cookie 换新登录态(用户无感);仅刷新失败才退扫码重登。

- core/ncm_req.c|h:eapi Cookie 头补齐会话锚点 MUSIC_U/MUSIC_A(对齐 request.rs,
  补 §36.5 遗留——登录刷新/song_url 会员档依赖);新增 ncm_req_cookie_merge
  (patch 覆盖 base 同名 cookie,MUSIC_A/__remember_me 等不丢)。
- core/ncm_login.c|h:ncm_login_refresh(eapi /api/login/token/refresh 空体,
  code200+set-cookie→merge;code!=200→NCM_ERR_AUTH 退扫码;网络/TLS→负码退避)。
- app/logic/account.c:account.json 增 last_refresh_ts + 拆分 cvip/svip;
  account_cookie_maybe_refresh(年龄阈值 12h 可配 ncm.cookie_refresh_h,单飞/boot,
  失败冷却 30min 跨 boot 持久化,AUTH 置回登录页标志)+ mark_fresh/need_relogin。
- app/ui/home.c:home_worker 先续期再拉歌单;app/ui/login.c:803 落盘后 mark_fresh;
  app/app.c:UI tick 消费回登录标志 → logout+切登录页。
- 新增测试:test_ncm_core 的 cookie_merge + eapi_musicu_hdr;bench/cookie_refresh_sim
  (端点语义,0 网络);bench/account_cookie_sim(临期/未临期/退避/失效/冷却,0 网络)。

验证:host+qemu 核心测试 PASS;build_ncm_sims.sh 全家桶 PASS;build_ncm.sh 0 warning。
真机只读探针 2 次(设备 10.78.193.50,不写回):refresh 走 eapi 域
interface.music.163.com 在当前网络被 TLS reset(alert 80)→ rc=-5 瞬断退避,
session.enc/deviceId md5 前后一致(登录态零损伤)。新 ncm md5 c960244b。
2026-09-02 14:30:04 +08:00

255 lines
9.7 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.
/* ui/login.c — 扫码登录页(ACT_LOGIN,根页)。
*
* 流程:进入即起后台 worker(login_worker):
* unikey(eapi) → 本地二维码矩阵(qrcodegen) → 轮询(801/802/803) → 803 捕获 set-cookie 加密落盘
* → login_status 拿 uid/nickname → 置 g_login_done。
* UI 线程只在 tick 读 g_st.qr 状态与 login_state 重绘并检查 g_login_done → 切入 home。
* 800(过期)自动刷新二维码。UI 不卡:网络全在 worker 线程。
*/
#include "../ncm_app_internal.h"
#include <stdio.h>
#include <string.h>
#include <unistd.h>
static pthread_t g_thread;
static volatile int g_polling; /* worker 运行中 */
static volatile int g_done; /* 登录成功(UI tick 切入 home) */
static volatile int g_err; /* 最后一次错误(网络不可达等) */
static int g_active;
static int g_custom_id; /* 绘制物 */
static ncm_rect_t r_scr; /* 整屏(沉浸式,hero 从 y=0 起) */
/* ---- 绘制 ---- */
static void login_draw(void *ctx)
{
(void)ctx;
if (meow_ui_activity_active() != ACT_LOGIN) return;
const meow_theme *th = meow_theme_get();
const ncm_pal_t *pa = ncm_palette(g_dark);
int W = meow_ui_width(), H = meow_ui_height();
/* 品牌红 hero 带(沉浸,从 y=0 到 264;day=red / night=hero 深红压场)。 */
meow_ui_fill_rect(0, 0, W, 264, pa->hero);
/* 标题「网易云音乐」+ 红点 logo(用字体大小近似排版,host 字库用 ASCII 替文本)。 */
const char *title = "网易云音乐";
int tw = meow_ui_text_width(title, 34);
meow_ui_text_center(W/2, 112, title, 34, pa->on_red);
/* 24px 红点 logo(标题左前) */
ncm_fill_round(W/2 - tw/2 - 36, 112 + 4, 24, 24, 12, pa->red_lit);
meow_ui_text_center(W/2, 160, "扫码登录 / 音樂的力量", 22, pa->on_red_dim);
/* 弱分隔(hero 与内容衔接) */
meow_ui_fill_rect(0, 264, W, 1, th->outline);
/* QR 卡(128,228,224,224):day 纯白 / night surfaceContainer;圆角 16 + 内描边。 */
int qc = 224, qx = (W - qc) / 2, qy = 228;
ncm_fill_round(qx, qy, qc, qc, 16, th->surfaceContainer);
meow_ui_draw_rect_round(qx, qy, qc, qc, 16, th->outlineVariant);
int sz = g_st.qr_size;
if (sz > 0) {
/* 模块尺寸随内容缩放(二维码版本随内容长度变化,尺寸不固定——
* 按容器内边距算模块边长,绝不溢出卡片) */
int m = (qc - 32) / sz;
if (m < 1) m = 1;
int qs = m * sz;
int ox = qx + (qc - qs) / 2, oy = qy + (qc - qs) / 2;
/* 夜间:深灰底 + 浅灰码;白天:白底黑码 */
uint32_t qr_bg = g_dark ? 0x2A2B30 : 0xFFFFFF;
uint32_t qr_fg = g_dark ? 0xC9CAD1 : 0x000000;
meow_ui_fill_rect(ox, oy, qs, qs, qr_bg);
for (int y = 0; y < sz; y++)
for (int x = 0; x < sz; x++)
if (g_st.qr_mods[y*sz + x])
meow_ui_fill_rect(ox + x*m, oy + y*m, m, m, qr_fg);
} else {
meow_ui_text_center(W/2, qy + qc/2 - 10,
g_err ? "网络异常,点按重试" : "正在获取二维码...", 20, th->dim);
}
/* 状态文字:成功/等待用红(day red / night red_lit),失败红字提示。 */
const char *st = login_status_str(g_st.login_state);
if (g_st.login_state == NCM_LOGIN_OK && g_done) st = "登录成功";
uint32_t scol = (g_st.login_state == NCM_LOGIN_OK) ? pa->red : th->dim;
meow_ui_text_center(W/2, 476, st, 20, scol);
/* 底部提示 */
meow_ui_text_center(W/2, H - 56, "由 MeowOS 驱动 / 仅本地解码", 18, th->dim);
}
/* 重绘 login 内容(QR+状态变化时)。 */
static void login_repaint(void)
{
meow_ui_invalidate(r_scr.x, r_scr.y, r_scr.w, r_scr.h);
}
const char *login_status_str(int state)
{
switch (state) {
case NCM_LOGIN_PENDING: return "等待扫码...";
case NCM_LOGIN_SCANNED: return "已扫码,请在手机上确认";
case NCM_LOGIN_OK: return "登录成功";
case NCM_LOGIN_EXPIRED: return "二维码已过期,正在刷新";
default: return "出错了,点按重试";
}
}
static int get_unikey_and_qr(char *unikey, size_t usz)
{
int rc = ncm_login_unikey(g_st.ctx.device_id, unikey, usz);
if (rc != NCM_OK) { meow_log(MEOW_LOG_ERROR, "login: unikey fetch failed rc=%d", rc); return rc; }
meow_log(MEOW_LOG_INFO, "login: unikey ok, qr ready");
ncm_strlcpy(g_st.unikey, unikey, sizeof g_st.unikey);
ncm_login_qr_url(unikey, g_st.qr_url, sizeof g_st.qr_url);
int size = 0;
if (ncm_login_qr_matrix(g_st.qr_url, g_st.qr_mods, sizeof g_st.qr_mods / 177, &size) != NCM_OK)
g_st.qr_size = 0;
else
g_st.qr_size = size;
g_st.login_state = NCM_LOGIN_PENDING;
g_err = 0;
return NCM_OK;
}
static void *login_worker(void *arg)
{
(void)arg;
char unikey[64];
/* 自动重试:unikey 失败(网络未就绪/抖动)时 5s 退避重试,不再要求用户点重试 */
int retry = 0;
while (get_unikey_and_qr(unikey, sizeof unikey) != NCM_OK) {
g_err = 1;
if (g_app_quit) return NULL;
meow_log(MEOW_LOG_WARN, "login: unikey retry in 5s (attempt %d)", ++retry);
usleep(5 * 1000 * 1000);
}
meow_log(MEOW_LOG_INFO, "login: poll worker start");
char cookie[NCM_COOKIE_MAX];
ncm_strlcpy(cookie, g_st.ctx.cookie, sizeof cookie);
while (!g_app_quit) {
ncm_login_poll_t r;
char out_cookie[NCM_COOKIE_MAX];
int rc = ncm_login_poll(g_st.ctx.device_id, unikey, cookie, &r,
out_cookie, sizeof out_cookie);
if (rc != NCM_OK) { meow_log(MEOW_LOG_WARN, "login: poll failed rc=%d", rc); g_err = 1; usleep(2 * 1000 * 1000); continue; }
if (r.state != g_st.login_state)
meow_log(MEOW_LOG_INFO, "login: state -> %d", (int)r.state);
g_st.login_state = r.state;
if (r.state == NCM_LOGIN_OK) {
meow_log(MEOW_LOG_INFO, "login: 803 confirmed, saving cookie");
ncm_strlcpy(cookie, out_cookie, sizeof cookie);
ncm_ctx_set_cookie(&g_st.ctx, cookie);
account_cookie_mark_fresh(); /* §NEW:新登录态 cookie 视为全新(年龄=now,重置单飞/冷却) */
/* 803 后同步取 uid/nickname 并落盘(weapi 已修复:不再挂死、cookie 全量且去重发送,
* 服务端认可会话)。此处在 worker 线程执行(不卡 UI);失败不阻断跳页——home 加载
* 时 appstate_ensure_uid 会再验一次(也设了硬超时上限,不阻塞歌单页)。 */
char uid[32] = "", nick[128] = "";
int src = ncm_login_status_check(g_st.ctx.device_id, cookie,
uid, sizeof uid, nick, sizeof nick);
meow_log(MEOW_LOG_INFO, "login: status_check rc=%d uid=%s nick=%s", src, uid, nick);
appstate_set_logged_in(src == NCM_OK ? uid : "", src == NCM_OK ? nick : "");
g_st.logged_in = 1;
account_vip_maybe_refresh(1); /* §20:登录后查一次会员态(本 worker 线程,不卡 UI) */
g_done = 1;
meow_log(MEOW_LOG_INFO, "login: -> home");
break;
}
if (r.state == NCM_LOGIN_EXPIRED) {
/* 刷新二维码 */
usleep(500 * 1000);
get_unikey_and_qr(unikey, sizeof unikey);
continue;
}
usleep(1500 * 1000);
}
return NULL;
}
int login_start(void)
{
if (g_polling) return 0;
g_polling = 1;
g_done = 0;
g_err = 0;
if (ncm_thread_create(&g_thread, login_worker, NULL) != 0) {
g_polling = 0;
return -1;
}
pthread_detach(g_thread);
return 0;
}
int login_is_active(void) { return g_active; }
void login_on_tap(int x, int y)
{
(void)x; (void)y;
if (g_st.login_state == NCM_LOGIN_OK && g_done) return;
if (g_err) { login_start(); login_repaint(); } /* 网络失败重试 */
}
/* UI tick:重绘 QR/状态变化;登录成功 → 切 home。 */
void login_tick(uint64_t now)
{
(void)now;
chrome_tick();
static int last_state = -1, last_qr = 0;
if (g_st.login_state != last_state || g_st.qr_size != last_qr) {
last_state = g_st.login_state;
last_qr = g_st.qr_size;
login_repaint();
}
if (g_done)
meow_view_page_replace(ACT_HOME);
}
/* ---- 生命周期 ---- */
static void login_page_create(meow_view *v, void *ctx)
{
(void)v; (void)ctx;
r_scr = NR(0, 0, meow_ui_width(), meow_ui_height());
/* 沉浸式:登录页整屏自绘(hero 从 y=0 起,覆盖状态栏区)。只画一次并监听绘制物重绘。 */
g_custom_id = meow_draw_custom_register(1, r_scr.x, r_scr.y, r_scr.w, r_scr.h,
login_draw, NULL);
g_active = 0;
}
static void login_page_enter(meow_view *v, void *ctx)
{
(void)v; (void)ctx;
g_active = 1;
const meow_theme *th = meow_theme_get();
meow_ui_set_page_bg(th->bg);
meow_ui_paint_full();
if (!g_host_mock) login_start(); /* 进入即获取二维码 + 轮询 */
}
static void login_page_leave(meow_view *v, void *ctx)
{
(void)v; (void)ctx;
g_active = 0;
}
static void login_page_destroy(meow_view *v, void *ctx)
{
(void)v; (void)ctx;
if (g_custom_id) { meow_draw_unregister(g_custom_id); g_custom_id = 0; }
}
meow_view *login_view_create(void)
{
static const meow_view_hooks h = {
.on_create = login_page_create,
.on_enter = login_page_enter,
.on_leave = login_page_leave,
.on_destroy = login_page_destroy,
};
return meow_view_create(ACT_LOGIN, MEOW_VIEW_ACTIVITY, &h, NULL);
}