feat(ncm §20): 音质体系全量——八档UI/VIP检测缓存/逐级降级下首恢复/fee门禁/默认极高HQ
- core: 9档枚举(顺序=降级链, DOLBY映射保留不进UI/链) + level_key(sky/sky_c51)
+ level_gate(CVIP/SVIP门槛) + song_url surround档附加immerseType=c51
+ song_url_auto全档表降级(out_level回填/响应level静默降档采信/try_fn可注入)
+ vip_info(weapi v2优先v1兜底) + parse_vip_info离线可测 + NCM_ERR_VIP(-15)
- app/logic/account.c(新): account.json会员态缓存(uid/nick/vip/双expire/checked_at)
刷新=登录后/超1天/到期后各一次不轮询; account_vip本地到期降档视图
- player_manager: 用户档按会员钳顶后逐级降级(不硬拒,缓存键仍用户档),
fee∈{1,4}且非会员→NCM_ERR_VIP零请求(缓存命中优先), eff_level回填
- UI: 设置页八档(规格+门槛+未解锁标注,允许选定), 播放页时间行中央
生效档/已降级至xx小字, VIP专享文案; 默认档Hi-Res→极高HQ(存量设置保留)
- 验证(0真实请求): degrade_sim 90例矩阵全PASS; player_flow_sim P1-P8全PASS;
test_ncm_core vip解析9例宿主+qemu全PASS; 窗口/分页/缓存/封面/audiod sim全PASS;
mipsel -Wall -Wextra 0 warning; 6页离屏rc=0。未部署真机(留主会话)
This commit is contained in:
1 parent
ab4dfb4f9c
commit
bdd9e898f4
23 files changed
+1026
-70
No files matched your search
@@ -0,0 +1,129 @@
|
||||
/* logic/account.c — 账号会员态缓存与刷新策略(§20,NCM_CACHE_DESIGN §2.1 定案)。
|
||||
*
|
||||
* 落盘:<data_dir>/account.json = {uid, nickname, vip, cvip_expire_ms, svip_expire_ms,
|
||||
* checked_at}。开 App 先用缓存渲染(account_load 进 g_st);后台重查时机(风控:不轮询):
|
||||
* ① 登录成功后(login worker,force=1);
|
||||
* ② checked_at 距今 > 1 天;
|
||||
* ③ 会员到期时间已过(可能自动续费,再查一次)。
|
||||
* 每条件每次启动只查一次(单飞标志);查询在调用方 worker 线程上下文执行(UI 零网络)。
|
||||
* account_vip() 是纯本地视图:到期时间已过即按降档后的有效态返回(不联网)。
|
||||
*/
|
||||
#include "../ncm_app_internal.h"
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
|
||||
#define ACCOUNT_FILE "account.json"
|
||||
#define VIP_TTL_SEC (24 * 3600) /* 超 1 天重查 */
|
||||
|
||||
/* 单飞标志(每次启动每条件最多一次请求) */
|
||||
static int s_refreshed_login; /* 登录后已查 */
|
||||
static int s_refreshed_stale; /* 超 1 天已查 */
|
||||
static int s_refreshed_expired; /* 到期后已查 */
|
||||
|
||||
static void account_path(char *out, size_t sz)
|
||||
{
|
||||
snprintf(out, sz, "%s/%s", g_st.data_dir, ACCOUNT_FILE);
|
||||
}
|
||||
|
||||
void account_load(void)
|
||||
{
|
||||
g_st.vip = NCM_VIP_NONE;
|
||||
g_st.vip_expire_ms = 0;
|
||||
g_st.vip_checked_at = 0;
|
||||
|
||||
char path[600], buf[512];
|
||||
account_path(path, sizeof path);
|
||||
int len = ncm_store_read_file(path, buf, sizeof buf);
|
||||
if (len <= 0) return;
|
||||
|
||||
/* 小 JSON 直接 jsmn 解析(栈上 token 数组,≤48 token 足够) */
|
||||
jsmntok_t toks[48];
|
||||
int parent[48];
|
||||
ncm_json_t j;
|
||||
if (ncm_json_parse(&j, buf, (size_t)len, toks, 48, parent) < 0) return;
|
||||
|
||||
long vip = 0, checked = 0;
|
||||
ncm_json_path_int(&j, "vip", &vip);
|
||||
ncm_json_path_int(&j, "checked_at", &checked);
|
||||
int rc = 0;
|
||||
int64_t cvip_exp = ncm_json_get_i64_at(&j, ncm_json_path(&j, "cvip_expire_ms"), &rc);
|
||||
if (rc != NCM_OK) cvip_exp = 0;
|
||||
int64_t svip_exp = ncm_json_get_i64_at(&j, ncm_json_path(&j, "svip_expire_ms"), &rc);
|
||||
if (rc != NCM_OK) svip_exp = 0;
|
||||
|
||||
if (vip < NCM_VIP_NONE || vip > NCM_VIP_SVIP) vip = NCM_VIP_NONE;
|
||||
g_st.vip = (int)vip;
|
||||
g_st.vip_checked_at = checked;
|
||||
/* 有效到期时间 = 当前档对应的到期字段 */
|
||||
g_st.vip_expire_ms = (vip == NCM_VIP_SVIP) ? svip_exp
|
||||
: (vip == NCM_VIP_CVIP) ? cvip_exp : 0;
|
||||
meow_log(MEOW_LOG_INFO, "account: loaded vip=%d expire=%lld checked_at=%ld",
|
||||
g_st.vip, (long long)g_st.vip_expire_ms, (long)g_st.vip_checked_at);
|
||||
}
|
||||
|
||||
static void account_save(int vip, int64_t cvip_exp, int64_t svip_exp, int64_t checked_at)
|
||||
{
|
||||
char path[600], buf[512];
|
||||
account_path(path, sizeof path);
|
||||
snprintf(buf, sizeof buf,
|
||||
"{\"uid\":\"%s\",\"nickname\":\"%s\",\"vip\":%d,"
|
||||
"\"cvip_expire_ms\":%lld,\"svip_expire_ms\":%lld,\"checked_at\":%lld}",
|
||||
g_st.uid, g_st.nickname, vip,
|
||||
(long long)cvip_exp, (long long)svip_exp, (long long)checked_at);
|
||||
if (ncm_store_write_file(path, buf, strlen(buf)) != NCM_OK)
|
||||
meow_log(MEOW_LOG_WARN, "account: save failed");
|
||||
}
|
||||
|
||||
/* 当前有效会员档(本地视图,不联网):到期时间已过则降档。 */
|
||||
int account_vip(void)
|
||||
{
|
||||
int vip = g_st.vip;
|
||||
if (vip > NCM_VIP_NONE && g_st.vip_expire_ms > 0 &&
|
||||
g_st.vip_expire_ms <= ncm_now_ms())
|
||||
vip = NCM_VIP_NONE; /* 已到期:按非会员对待(后台会再查一次确认续费) */
|
||||
return vip;
|
||||
}
|
||||
|
||||
int account_level_cap(void)
|
||||
{
|
||||
switch (account_vip()) {
|
||||
case NCM_VIP_SVIP: return NCM_LEVEL_SURROUND;
|
||||
case NCM_VIP_CVIP: return NCM_LEVEL_HIRES;
|
||||
default: return NCM_LEVEL_EXHIGH;
|
||||
}
|
||||
}
|
||||
|
||||
void account_vip_maybe_refresh(int force)
|
||||
{
|
||||
if (!g_st.logged_in || !g_st.ctx.cookie[0]) return;
|
||||
|
||||
int64_t now_s = ncm_now_ms() / 1000;
|
||||
int expired = (g_st.vip > NCM_VIP_NONE && g_st.vip_expire_ms > 0 &&
|
||||
g_st.vip_expire_ms <= ncm_now_ms());
|
||||
const char *why = NULL;
|
||||
if (force && !s_refreshed_login) why = "login";
|
||||
else if ((g_st.vip_checked_at == 0 || now_s - g_st.vip_checked_at > VIP_TTL_SEC)
|
||||
&& !s_refreshed_stale) why = "stale>1d";
|
||||
else if (expired && !s_refreshed_expired) why = "expired";
|
||||
if (!why) return;
|
||||
|
||||
ncm_vip_info_t info;
|
||||
int rc = ncm_api_vip_info(g_st.ctx.device_id, g_st.ctx.cookie, g_st.uid, &info);
|
||||
/* 无论成败都置单飞标志:本条件本启动只查一次(失败等下次启动/下个条件,防风控) */
|
||||
if (force) s_refreshed_login = 1;
|
||||
if (strcmp(why, "stale>1d") == 0) s_refreshed_stale = 1;
|
||||
if (strcmp(why, "expired") == 0) s_refreshed_expired = 1;
|
||||
if (rc != NCM_OK) {
|
||||
meow_log(MEOW_LOG_WARN, "account: vip refresh (%s) failed rc=%d, keep cached vip=%d",
|
||||
why, rc, g_st.vip);
|
||||
return;
|
||||
}
|
||||
|
||||
g_st.vip = info.vip;
|
||||
g_st.vip_expire_ms = (info.vip == NCM_VIP_SVIP) ? info.svip_expire_ms
|
||||
: (info.vip == NCM_VIP_CVIP) ? info.cvip_expire_ms : 0;
|
||||
g_st.vip_checked_at = now_s;
|
||||
account_save(info.vip, info.cvip_expire_ms, info.svip_expire_ms, now_s);
|
||||
meow_log(MEOW_LOG_INFO, "account: vip refresh (%s) -> vip=%d expire=%lld",
|
||||
why, info.vip, (long long)g_st.vip_expire_ms);
|
||||
}
|
||||
@@ -35,6 +35,8 @@ int appstate_init(void)
|
||||
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) */
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
@@ -44,9 +46,11 @@ void appstate_load_settings(void)
|
||||
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;
|
||||
/* §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;
|
||||
|
||||
ncm_cache_set_limit_gb(&g_st.ctx.cache, g_st.cache_limit_gb);
|
||||
g_st.ctx.default_level = g_st.level;
|
||||
@@ -81,6 +85,14 @@ void appstate_logout(void)
|
||||
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);
|
||||
@@ -435,9 +447,10 @@ static int autoplay_try_playlist(int pl)
|
||||
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+档位:先试当前档位键(flac/mp3),再回退旧无档位键(存量 mp3) */
|
||||
/* §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_str(g_st.level));
|
||||
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) {
|
||||
|
||||
@@ -89,6 +89,7 @@ typedef struct ncm_play_run {
|
||||
int64_t dl_done; /* 本曲已下载字节(缓冲期进度反馈,§18) */
|
||||
int64_t dl_total; /* 本曲标称大小(song_url 返回;0=未知) */
|
||||
int has_track; /* 当前是否有可播曲目(含 url 获取中) */
|
||||
int eff_level; /* 实际生效音质档(§20;≠用户档时播放页标注「已降级至 xx」;-1=未定) */
|
||||
/* 播放曲快照(REPORT §17):run_song 起播时把歌曲对象整体拷入(mu 保护),
|
||||
* 之后播放/歌词/IPC/UI 全读快照而非窗口——窗口滑出当前播放曲也不影响播放。 */
|
||||
ncm_song_t play_song;
|
||||
@@ -115,6 +116,10 @@ typedef struct ncm_app_state {
|
||||
/* 账号 */
|
||||
char uid[32];
|
||||
char nickname[128];
|
||||
/* 会员态(§20,account.json 落盘:data/account.json;刷新=登录后/超 1 天/到期后一次) */
|
||||
int vip; /* NCM_VIP_*(NONE/CVIP/SVIP) */
|
||||
int64_t vip_expire_ms; /* 当前档到期时间(ms;0=无/未知) */
|
||||
int64_t vip_checked_at; /* vip/info 最近获取时间(秒) */
|
||||
|
||||
/* 我的歌单 */
|
||||
ncm_playlist_t playlists[NCM_PLAYLIST_MAX];
|
||||
@@ -290,6 +295,13 @@ int appstate_song_fetch(int gidx, ncm_song_t *out); /* 任意全局下标物化
|
||||
* 不滑窗(播放线程用,避免与浏览视口抢窗口) */
|
||||
void appstate_adopt_songs(int n); /* 收编外部已填的 songs[0..n)(搜索结果)为窗口 base=0,
|
||||
* 同步 song_ids/track_ids_count(播放上下文) */
|
||||
|
||||
/* account(app/logic/account.c,§20 会员态):account.json 落盘 + 刷新策略
|
||||
* (登录后/缓存超 1 天/到期后 各查一次,不轮询——风控)。 */
|
||||
void account_load(void); /* appstate_init 时读 account.json 恢复 vip 态 */
|
||||
int account_vip(void); /* 当前有效会员档(到期自动降档视图,不联网) */
|
||||
int account_level_cap(void); /* 会员态允许的最高音质档(NONE→EXHIGH/CVIP→HIRES/SVIP→SURROUND) */
|
||||
void account_vip_maybe_refresh(int force); /* worker 线程上下文调用(含网络,单次) */
|
||||
void appstate_apply_playlist_cover(int idx);
|
||||
void appstate_set_playing_song(int idx, const char *cover_url);
|
||||
int appstate_current_playing(void); /* 返回当前播放歌曲下标(UI) */
|
||||
@@ -340,6 +352,11 @@ int player_manager_load_lyric(void); /* 为当前曲目拉歌词并
|
||||
int player_manager_error(void);
|
||||
int player_manager_current_song(ncm_song_t *out); /* 播放曲快照锁内拷贝;0=有效 -1=无(REPORT §17) */
|
||||
void player_manager_dl(int64_t *done, int64_t *total); /* 本曲下载进度(字节;total=0 未知,§18) */
|
||||
int player_manager_eff_level(void); /* 实际生效音质档(§20;-1=未定) */
|
||||
|
||||
/* 音质档位显示名/规格(ui/chrome.c,§20 八档 UI 权威口径;settings/player 共用) */
|
||||
const char *ncm_level_name(int lv); /* 短名:标准/极高 HQ/无损 SQ/Hi-Res/高清臻音/臻音全景声/超清母带/沉浸环绕声 */
|
||||
const char *ncm_level_spec(int lv); /* 规格:128kbps / ≤320kbps / 48kHz·16bit / ... */
|
||||
|
||||
/* songmeta(app/logic/songmeta.c):歌曲元数据本地缓存(<cache_dir>/meta/<id>.bin),
|
||||
* 拉过的 song_detail 结果序列化落盘,再次入窗免重复请求(REPORT §17)。 */
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/* play/player_manager.c — 播放线程:URL 获取(Hi-res 逐档降级)→缓存(命中直接播/否则流式落盘)→
|
||||
* minimp3 解码→audio_out 直写。UI 不卡(网络+解码全在此线程)。
|
||||
/* play/player_manager.c — 播放线程:URL 获取(全档表逐级降级)→缓存(命中直接播/否则流式落盘)→
|
||||
* minimp3/dr_flac 解码→audio_out 直写。UI 不卡(网络+解码全在此线程)。
|
||||
*
|
||||
* 模型:一个持久的播放线程。外层循环取「目标曲目」,run_song() 负责一首歌的
|
||||
* 「下载(缓冲) → 解码 → 输出 → 结束/被指令打断」。暂停=冻结(停读停写停推 pos,状态置 PAUSED);
|
||||
@@ -9,6 +9,8 @@
|
||||
* §19:解码经 decoder.c 按 magic 分发(fLaC→dr_flac / 其它→minimp3);rate/ch/bits 按实际流
|
||||
* 给 audio_out(flac ≤16bit→S16,>16bit→S32;audiod 格式归一/DAC 原生率跟随在服务端);
|
||||
* 音频缓存键 = 歌曲ID+音质档位(用户定案 NCM_CACHE_DESIGN §2.5),旧无档位键温和兼容。
|
||||
* §20:音质体系全量——用户档超会员权限不硬拒(钳顶后逐级降级,eff_level 回填供播放页
|
||||
* 「已降级至 xx」标注;下一首回到用户档);fee∈{1,4} 且非会员 → NCM_ERR_VIP 不发请求。
|
||||
*/
|
||||
#include "../ncm_app_internal.h"
|
||||
#include <stdio.h>
|
||||
@@ -44,6 +46,9 @@ int64_t player_manager_pos_ms(void) { int64_t p; pthread_mutex_lock(&g_play.mu);
|
||||
int64_t player_manager_dur_ms(void) { int64_t d; pthread_mutex_lock(&g_play.mu); d = g_play.dur_ms; pthread_mutex_unlock(&g_play.mu); return d; }
|
||||
int player_manager_error(void) { int e; pthread_mutex_lock(&g_play.mu); e = g_play.err; pthread_mutex_unlock(&g_play.mu); return e; }
|
||||
|
||||
/* 实际生效音质档(§20):下载时由 song_url_auto 回填;缓存命中按 ext 推断;-1=未定。 */
|
||||
int player_manager_eff_level(void) { int e; pthread_mutex_lock(&g_play.mu); e = g_play.eff_level; pthread_mutex_unlock(&g_play.mu); return e; }
|
||||
|
||||
/* 本曲下载进度(UI 缓冲反馈,§18)。total=0 表示未知(song_url 未给 size / 缓存命中)。 */
|
||||
void player_manager_dl(int64_t *done, int64_t *total)
|
||||
{
|
||||
@@ -86,6 +91,7 @@ void player_manager_play(int song_idx)
|
||||
g_play.dl_done = 0;
|
||||
g_play.dl_total = 0;
|
||||
g_play.seq++;
|
||||
g_play.eff_level = -1; /* 新播放意图:生效档未定(song_local_file 回填) */
|
||||
pthread_mutex_unlock(&g_play.mu);
|
||||
}
|
||||
|
||||
@@ -120,6 +126,7 @@ void player_manager_next(int dir)
|
||||
g_play.dl_done = 0;
|
||||
g_play.dl_total = 0;
|
||||
g_play.seq++;
|
||||
g_play.eff_level = -1; /* 切歌:生效档随新曲重估(下首恢复用户档,§20) */
|
||||
}
|
||||
pthread_mutex_unlock(&g_play.mu);
|
||||
}
|
||||
@@ -146,16 +153,24 @@ static int download_cb(void *ctx, const unsigned char *data, size_t len)
|
||||
}
|
||||
|
||||
/* 缓存键(用户定案 NCM_CACHE_DESIGN §2.5):歌曲ID + 音质档位 → "song_<id>_<level>.<ext>"。
|
||||
* 同歌同档命中不重复下载;换档=新条目,旧档留给 LRU 自然淘汰。 */
|
||||
* 同歌同档命中不重复下载;换档=新条目,旧档留给 LRU 自然淘汰。
|
||||
* §20:键后缀用 ncm_api_level_key(sky 与 sky_c51 可区分)。 */
|
||||
static void audio_cache_key(char *out, size_t sz, const char *id, int level)
|
||||
{
|
||||
snprintf(out, sz, "%s_%s", id, ncm_api_level_str(level));
|
||||
snprintf(out, sz, "%s_%s", id, ncm_api_level_key(level));
|
||||
}
|
||||
|
||||
/* 得到本地音频路径 + 文件大小。命中缓存直接返回;否则 URL 降级 + 流式落缓存。
|
||||
* 返回 NCM_OK 或负错误码。id 由调用方(run_song)从播放曲快照取,与窗口解耦(§17)。 */
|
||||
static int song_local_file(const char *id, char *path, size_t psz, int64_t *bytes)
|
||||
* 返回 NCM_OK 或负错误码。s 为播放曲快照(run_song 物化,与窗口解耦 §17)。
|
||||
* §20 权限/会员:
|
||||
* - fee 门禁(BIZ_NOTES §3 社区口径):fee∈{1,4}(VIP 才能听/付费专)且当前非会员
|
||||
* → 直接 NCM_ERR_VIP,**不发 song_url 请求**(省一次请求,防风控);fee=0/8 放行
|
||||
* (8=免费听低音质,播放允许)。本地缓存命中优先于门禁(缓存可播不拦)。
|
||||
* - VIP 档上限:用户档超会员权限不硬拒,播放时先钳到会员允许的最高档再逐级降级
|
||||
* (免去注定空 URL 的高档请求);实际生效档回填 g_play.eff_level。 */
|
||||
static int song_local_file(const ncm_song_t *s, char *path, size_t psz, int64_t *bytes)
|
||||
{
|
||||
const char *id = s->id;
|
||||
char key[64];
|
||||
audio_cache_key(key, sizeof key, id, g_level);
|
||||
|
||||
@@ -163,20 +178,51 @@ static int song_local_file(const char *id, char *path, size_t psz, int64_t *byte
|
||||
if (ncm_cache_lookup(&g_st.ctx.cache, key, "flac", path, psz) == NCM_OK ||
|
||||
ncm_cache_lookup(&g_st.ctx.cache, key, "mp3", path, psz) == NCM_OK) {
|
||||
struct stat st;
|
||||
if (stat(path, &st) == 0) { *bytes = (int64_t)st.st_size; return NCM_OK; }
|
||||
if (stat(path, &st) == 0) {
|
||||
*bytes = (int64_t)st.st_size;
|
||||
/* 生效档推断(缓存键=用户档,降级产物归用户档键,无法精确还原):
|
||||
* mp3 实体 + 用户档 > 极高 → 必为降级产物,标 EXHIGH;其余按用户档显示。
|
||||
* (flac 实体视为与用户档一致;hires→lossless 档内降级会高估显示,已知取舍。) */
|
||||
const char *dot = strrchr(path, '.');
|
||||
int eff = g_level;
|
||||
if (dot && ncm_streq(dot, "mp3") && g_level > NCM_LEVEL_EXHIGH)
|
||||
eff = NCM_LEVEL_EXHIGH;
|
||||
pthread_mutex_lock(&g_play.mu);
|
||||
g_play.eff_level = eff;
|
||||
pthread_mutex_unlock(&g_play.mu);
|
||||
return NCM_OK;
|
||||
}
|
||||
}
|
||||
/* 旧键温和兼容(§19 前存量:无档位后缀的 song_<id>[.]mp3,含历史双点名):命中即播,
|
||||
* 不主动改名/删除,由 LRU 自然淘汰。 */
|
||||
if (ncm_cache_lookup(&g_st.ctx.cache, id, "mp3", path, psz) == NCM_OK) {
|
||||
struct stat st;
|
||||
if (stat(path, &st) == 0) { *bytes = (int64_t)st.st_size; return NCM_OK; }
|
||||
if (stat(path, &st) == 0) {
|
||||
*bytes = (int64_t)st.st_size;
|
||||
pthread_mutex_lock(&g_play.mu);
|
||||
g_play.eff_level = (g_level > NCM_LEVEL_EXHIGH) ? NCM_LEVEL_EXHIGH : g_level;
|
||||
pthread_mutex_unlock(&g_play.mu);
|
||||
return NCM_OK;
|
||||
}
|
||||
}
|
||||
|
||||
/* 播放地址:从 g_level 降级,接受 mp3 与 flac(§19:dr_flac 落地后无损/Hi-Res 真实生效) */
|
||||
/* fee 门禁:VIP 才能听(1=VIP 歌曲 / 4=付费数字专辑)且非会员 → 不请求 url。
|
||||
* (必须在缓存查找之后:已缓存的过期/下架曲本地可播,NCM_CACHE_DESIGN §2.4。) */
|
||||
if ((s->fee == 1 || s->fee == 4) && account_vip() < NCM_VIP_CVIP)
|
||||
return NCM_ERR_VIP;
|
||||
|
||||
/* 播放地址:用户档先按会员权限钳顶,再逐档降级(§20 全档表),接受 mp3 与 flac */
|
||||
int want = g_level;
|
||||
int vip_cap = account_level_cap();
|
||||
if (want > vip_cap) want = vip_cap;
|
||||
ncm_play_url_t u;
|
||||
int rc = ncm_api_song_url_auto(g_st.ctx.device_id, g_st.ctx.cookie, id, g_level, &u);
|
||||
int eff = -1;
|
||||
int rc = ncm_api_song_url_auto(g_st.ctx.device_id, g_st.ctx.cookie, id, want, &u, &eff);
|
||||
if (rc != NCM_OK) return rc;
|
||||
if (!u.url[0]) return NCM_ERR_NOTFOUND;
|
||||
pthread_mutex_lock(&g_play.mu);
|
||||
g_play.eff_level = (eff >= 0) ? eff : want;
|
||||
pthread_mutex_unlock(&g_play.mu);
|
||||
/* 落盘扩展名:type 权威;type 缺失时看 url 扩展名,默认 mp3 */
|
||||
const char *ext = "mp3";
|
||||
if (ncm_streq(u.type, "flac")) ext = "flac";
|
||||
@@ -260,11 +306,12 @@ static void run_song(int idx)
|
||||
|
||||
int64_t file_bytes = 0;
|
||||
char path[512];
|
||||
int rc = song_local_file(s.id, path, sizeof path, &file_bytes);
|
||||
int rc = song_local_file(&s, path, sizeof path, &file_bytes);
|
||||
if (rc != NCM_OK) {
|
||||
if (!still_current(idx)) return; /* 下载期间已切歌:静默放弃(缓存已落盘不亏) */
|
||||
const char *m = (rc == NCM_ERR_NOTFOUND) ? "no stream (VIP/copyright)"
|
||||
: (rc == NCM_ERR_AUTH) ? "session expired" : "load failed";
|
||||
const char *m = (rc == NCM_ERR_VIP) ? "VIP only (fee gate)"
|
||||
: (rc == NCM_ERR_NOTFOUND) ? "no stream (VIP/copyright)"
|
||||
: (rc == NCM_ERR_AUTH) ? "session expired" : "load failed";
|
||||
set_err(rc, m);
|
||||
return;
|
||||
}
|
||||
@@ -295,8 +342,9 @@ static void run_song(int idx)
|
||||
g_play.dur_ms = dur;
|
||||
g_play.err = 0;
|
||||
pthread_mutex_unlock(&g_play.mu);
|
||||
meow_log(MEOW_LOG_INFO, "play: start idx=%d id=%s fmt=%s %dHz/%dch/%dbit dur=%lldms",
|
||||
idx, s.id, ncm_dec_format(dec), rate, ch, bits, (long long)dur);
|
||||
meow_log(MEOW_LOG_INFO, "play: start idx=%d id=%s fmt=%s %dHz/%dch/%dbit dur=%lldms level=%s eff=%s",
|
||||
idx, s.id, ncm_dec_format(dec), rate, ch, bits, (long long)dur,
|
||||
ncm_api_level_key(g_level), ncm_api_level_key(g_play.eff_level >= 0 ? g_play.eff_level : g_level));
|
||||
|
||||
/* 单块缓冲:4096 帧 × ≤2ch × 4B(S32 上限)= 32KB,静态(播放线程单实例,省 32KB 栈)。 */
|
||||
static int32_t pcm[PCM_CHUNK_FRAMES * 2];
|
||||
|
||||
@@ -103,3 +103,37 @@ void chrome_tick(void)
|
||||
s_snack_until = 0;
|
||||
}
|
||||
}
|
||||
|
||||
/* ---- §20 音质档位显示名/规格(八档 UI 权威口径,NCM_CACHE_DESIGN §3;settings/player 共用)----
|
||||
* 档位中文名以我方设计文档为准(BIZ_NOTES §2.1:CNMPlayer 标签只做 level 串对齐参考)。 */
|
||||
const char *ncm_level_name(int lv)
|
||||
{
|
||||
switch (lv) {
|
||||
case NCM_LEVEL_SURROUND: return "沉浸环绕声";
|
||||
case NCM_LEVEL_SKY: return "臻音全景声";
|
||||
case NCM_LEVEL_JYMASTER: return "超清母带";
|
||||
case NCM_LEVEL_JYEFFECT: return "高清臻音";
|
||||
case NCM_LEVEL_DOLBY: return "杜比全景声"; /* 映射保留,不进 UI 清单 */
|
||||
case NCM_LEVEL_HIRES: return "Hi-Res";
|
||||
case NCM_LEVEL_LOSSLESS: return "无损 SQ";
|
||||
case NCM_LEVEL_EXHIGH: return "极高 HQ";
|
||||
case NCM_LEVEL_HIGHER: return "较高"; /* 仅降级链中间档,不进 UI */
|
||||
default: return "标准";
|
||||
}
|
||||
}
|
||||
|
||||
const char *ncm_level_spec(int lv)
|
||||
{
|
||||
switch (lv) {
|
||||
case NCM_LEVEL_SURROUND: return "5.1 声道";
|
||||
case NCM_LEVEL_SKY: return "Audio Vivid";
|
||||
case NCM_LEVEL_JYMASTER: return "192kHz/24bit";
|
||||
case NCM_LEVEL_JYEFFECT: return "96kHz/24bit";
|
||||
case NCM_LEVEL_DOLBY: return "Dolby Atmos";
|
||||
case NCM_LEVEL_HIRES: return "192kHz/24bit";
|
||||
case NCM_LEVEL_LOSSLESS: return "48kHz/16bit";
|
||||
case NCM_LEVEL_EXHIGH: return "≤320kbps";
|
||||
case NCM_LEVEL_HIGHER: return "192kbps";
|
||||
default: return "128kbps";
|
||||
}
|
||||
}
|
||||
@@ -208,6 +208,8 @@ static void *home_worker(void *arg)
|
||||
int rc;
|
||||
if (!g_st.logged_in) rc = NCM_ERR_AUTH;
|
||||
else rc = appstate_load_playlists();
|
||||
if (rc == NCM_OK)
|
||||
account_vip_maybe_refresh(0); /* §20:超 1 天/到期后各补查一次(worker 线程,不卡 UI) */
|
||||
pthread_mutex_lock(&g_home_mu);
|
||||
g_load_err = rc;
|
||||
g_home_loading = 0;
|
||||
|
||||
@@ -154,6 +154,7 @@ static void *login_worker(void *arg)
|
||||
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;
|
||||
|
||||
@@ -26,6 +26,7 @@ static int g_custom_disc, g_custom_dyn;
|
||||
static int g_landscape;
|
||||
static int g_last_pos_ms, g_last_state, g_last_line, g_last_song;
|
||||
static int g_last_dl, g_last_err; /* 缓冲进度粒度值 / 错误沿(§18) */
|
||||
static int g_last_eff; /* 生效档沿(§20:降级标注出现/消失刷动态带) */
|
||||
static char g_last_meta_id[24]; /* 已上封面的元数据 id(快照滞后 settle 时纠正封面/歌名) */
|
||||
|
||||
/* 运行期几何(player_compute_geom 填充;各带绘制物/命中共用)。 */
|
||||
@@ -232,6 +233,22 @@ static void plr_draw_dyn(void *ctx)
|
||||
meow_ui_fill_rect(g_bar.x, g_bar.y, g_bar.w, g_bar.h, th->surfaceVariant);
|
||||
meow_ui_fill_rect(g_bar.x, g_bar.y, (int)((int64_t)g_bar.w * frac / 1000), g_bar.h, pa->red);
|
||||
|
||||
/* §20:实际生效音质档(与用户档不同时小字标注「已降级至 xx」;缓冲期未定不显示)。
|
||||
* 位置:时间行中部(左右是时间戳,中央留白),竖/横屏同一条带。 */
|
||||
{
|
||||
int eff = player_manager_eff_level();
|
||||
if (have && eff >= 0 && player_manager_state() != NCM_PC_STOPPED) {
|
||||
char qb[72];
|
||||
if (eff != g_st.level)
|
||||
snprintf(qb, sizeof qb, "已降级至 %s", ncm_level_name(eff));
|
||||
else
|
||||
snprintf(qb, sizeof qb, "%s", ncm_level_name(eff));
|
||||
int qcx = g_landscape ? (g_bar.x + g_bar.w / 2) : (r_cont.x + r_cont.w / 2);
|
||||
uint32_t qc = (eff != g_st.level) ? pa->red : th->onSurfaceVariant;
|
||||
meow_ui_text_center(qcx, g_times_y, qb, 18, qc);
|
||||
}
|
||||
}
|
||||
|
||||
int st = player_manager_state();
|
||||
draw_ctl(g_cprev_x, g_ctl_y, g_cr - 6, 0, th->surfaceContainer, th->onSurface);
|
||||
/* 暂停/停止(出错待重试)都画「播放」三角;播放/缓冲画暂停符 */
|
||||
@@ -248,7 +265,8 @@ static void plr_draw_dyn(void *ctx)
|
||||
int perr = player_manager_error();
|
||||
if (perr != 0) {
|
||||
/* §18:播放错误明示(旧版 err 无 UI 消费者 → 失败无声死等) */
|
||||
const char *em = (perr == NCM_ERR_NOTFOUND) ? "无法播放:无音源(VIP/版权)"
|
||||
const char *em = (perr == NCM_ERR_VIP) ? "VIP 专享,开通会员后可播放"
|
||||
: (perr == NCM_ERR_NOTFOUND) ? "无法播放:无音源(VIP/版权)"
|
||||
: (perr == NCM_ERR_AUTH) ? "登录已过期,请重新登录"
|
||||
: "加载失败,请检查网络";
|
||||
meow_ui_text_center(lcx, center_y - 20, em, 20, pa->red);
|
||||
@@ -423,7 +441,14 @@ void player_tick(uint64_t now)
|
||||
if (perr != g_last_err) {
|
||||
g_last_err = perr;
|
||||
meow_ui_invalidate(g_dyn_r.x, g_dyn_r.y, g_dyn_r.w, g_dyn_r.h);
|
||||
if (perr) ncm_snack("播放失败");
|
||||
if (perr == NCM_ERR_VIP) ncm_snack("VIP 专享歌曲");
|
||||
else if (perr) ncm_snack("播放失败");
|
||||
}
|
||||
/* §20:生效档变化(降级判定完成/切歌重置)刷动态带。 */
|
||||
int eff = player_manager_eff_level();
|
||||
if (eff != g_last_eff) {
|
||||
g_last_eff = eff;
|
||||
meow_ui_invalidate(g_dyn_r.x, g_dyn_r.y, g_dyn_r.w, g_dyn_r.h);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -440,7 +465,7 @@ static void plr_page_create(meow_view *v, void *ctx)
|
||||
g_custom_dyn = meow_draw_custom_register(11, g_dyn_r.x, g_dyn_r.y, g_dyn_r.w, g_dyn_r.h,
|
||||
plr_draw_dyn, NULL);
|
||||
g_last_pos_ms = -1; g_last_state = -1; g_last_line = -1; g_last_song = -1;
|
||||
g_last_dl = -1; g_last_err = 0;
|
||||
g_last_dl = -1; g_last_err = 0; g_last_eff = -2; /* -2: 与 eff=-1(未定) 区分,首 tick 必刷 */
|
||||
g_last_meta_id[0] = 0;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
/* ui/settings.c — 设置页(ACT_SETTINGS)。
|
||||
*
|
||||
* 行项:音质(Hi-res 默认,单曲不可用时 core ncm_api_song_url_auto 逐档降级)+ 缓存上限
|
||||
* (默认 10GB,上限 100GB,支持预设与「自定义」数字输入)+ 清理缓存(显示当前占用)+
|
||||
* 退出登录 + 关于/版本。设置持久化到 MeowData/dev.player.ncm/data(meow_config_*,与 core
|
||||
* 行项:音质(§20 八档:标准/极高 HQ/无损 SQ/Hi-Res/高清臻音/臻音全景声/超清母带/沉浸环绕声,
|
||||
* 默认极高 HQ;每档标注规格+会员门槛 CVIP/SVIP;超权限档允许选定,播放时钳顶+逐级降级,
|
||||
* 见 player_manager.c song_local_file)+ 缓存上限(默认 10GB,上限 100GB,支持预设与
|
||||
* 「自定义」数字输入)+ 清理缓存(显示当前占用)+ 退出登录 + 关于/版本。
|
||||
* 设置持久化到 MeowData/dev.player.ncm/data(meow_config_*,与 core
|
||||
* ncm_store_read_cfg_int 同目录同键,ncm.cache_limit_gb/ncm.level)。超上限触发 ncm_cache LRU
|
||||
* 淘汰(写缓存前后 + 手动调低上限时,见 player_manager.c/cover.c/appstate.c)。
|
||||
* 空态/绘制用 custom 绘制物 + 手动行命中(仅 5 行,无需列表滚动)。
|
||||
@@ -24,16 +26,12 @@ static uint64_t g_cache_used; /* 缓存占用字节(进设置页时
|
||||
|
||||
#define SETTINGS_ROWS 5
|
||||
|
||||
static const char *level_name(int lv)
|
||||
{
|
||||
switch (lv) {
|
||||
case NCM_LEVEL_HIRES: return "Hi-Res";
|
||||
case NCM_LEVEL_LOSSLESS:return "无损";
|
||||
case NCM_LEVEL_EXHIGH: return "极高";
|
||||
case NCM_LEVEL_HIGHER: return "较高";
|
||||
default: return "标准";
|
||||
}
|
||||
}
|
||||
/* 八档音质 UI 表(用户手机 App 口径为权威,NCM_CACHE_DESIGN §3):显示顺序高→低。
|
||||
* HIGHER(较高)与 DOLBY(杜比全景声)不进 UI 清单(映射保留,见 ncm_api.h)。 */
|
||||
static const int g_ui_levels[8] = {
|
||||
NCM_LEVEL_SURROUND, NCM_LEVEL_JYMASTER, NCM_LEVEL_SKY, NCM_LEVEL_JYEFFECT,
|
||||
NCM_LEVEL_HIRES, NCM_LEVEL_LOSSLESS, NCM_LEVEL_EXHIGH, NCM_LEVEL_STANDARD,
|
||||
};
|
||||
|
||||
/* 缓存占用字节 → 人类可读(整数运算,不用浮点/软浮点)。buf ≥ 24。 */
|
||||
static void fmt_bytes(uint64_t b, char *buf, size_t sz)
|
||||
@@ -80,7 +78,19 @@ static void settings_draw(void *ctx)
|
||||
rows[2].title = "清理缓存"; rows[2].red = 0;
|
||||
rows[3].title = "退出登录"; rows[3].red = 1;
|
||||
rows[4].title = "关于网易云"; rows[4].red = 0;
|
||||
snprintf(rows[0].sub, sizeof rows[0].sub, "当前: %s", level_name(g_st.level));
|
||||
{
|
||||
/* 当前档 + 会员门槛标注;超权限(未解锁)时提示将降级(不硬拒,§20) */
|
||||
int gate = ncm_api_level_gate(g_st.level);
|
||||
const char *gate_s = (gate == NCM_VIP_SVIP) ? "SVIP" : (gate == NCM_VIP_CVIP) ? "CVIP" : "";
|
||||
int locked = (account_vip() < gate);
|
||||
if (gate_s[0])
|
||||
snprintf(rows[0].sub, sizeof rows[0].sub, "当前: %s (%s) · 需 %s%s",
|
||||
ncm_level_name(g_st.level), ncm_level_spec(g_st.level), gate_s,
|
||||
locked ? ",将降级" : "");
|
||||
else
|
||||
snprintf(rows[0].sub, sizeof rows[0].sub, "当前: %s (%s)",
|
||||
ncm_level_name(g_st.level), ncm_level_spec(g_st.level));
|
||||
}
|
||||
snprintf(rows[1].sub, sizeof rows[1].sub, "当前: %d GB · 上限 100", g_st.cache_limit_gb);
|
||||
snprintf(rows[2].sub, sizeof rows[2].sub, "占用: %s", usetxt);
|
||||
snprintf(rows[3].sub, sizeof rows[3].sub, "%s", g_st.nickname[0] ? g_st.nickname : "退出");
|
||||
@@ -98,7 +108,7 @@ static void settings_draw(void *ctx)
|
||||
|
||||
/* 品牌红页脚小字。 */
|
||||
int ny = y + SETTINGS_ROWS * (g_row_h + 8) + 4;
|
||||
meow_ui_text(x, ny, "Hi-Res/无损需黑胶会员,不支持时自动降级", 20, th->onSurfaceVariant);
|
||||
meow_ui_text(x, ny, "无损/Hi-Res 需 CVIP ・ 臻音/母带需 SVIP,不支持时自动降级", 20, th->onSurfaceVariant);
|
||||
meow_ui_text(x, ny + 24, "网易云音乐 · MeowOS 移植版", 20, pa->red);
|
||||
}
|
||||
|
||||
@@ -149,16 +159,27 @@ int settings_on_tap(int x, int y)
|
||||
int row = row_at(x, y);
|
||||
if (row < 0) return 0;
|
||||
|
||||
if (row == 0) { /* 音质:Top 档选择,单曲不可用时 core 逐档降级 */
|
||||
static const char *lbls[5] = { "Hi-Res", "无损", "极高", "较高", "标准" };
|
||||
static const int lvls[5] = { NCM_LEVEL_HIRES, NCM_LEVEL_LOSSLESS,
|
||||
NCM_LEVEL_EXHIGH, NCM_LEVEL_HIGHER, NCM_LEVEL_STANDARD };
|
||||
if (row == 0) { /* 音质:八档(§20;超权限档可选不硬拒,播放时钳顶+逐级降级) */
|
||||
meow_sheet *s = meow_sheet_create("选择音质", 0);
|
||||
for (int i = 0; i < 5; i++) meow_sheet_add_item(s, lbls[i]);
|
||||
int vip = account_vip();
|
||||
for (int i = 0; i < 8; i++) {
|
||||
int lv = g_ui_levels[i];
|
||||
int gate = ncm_api_level_gate(lv);
|
||||
char lbl[96];
|
||||
/* 每档标注 规格+会员门槛;未解锁档标注「需 CVIP/SVIP」(sheet 无置灰能力,
|
||||
* 以文字标注代替——允许选定,实际播放逐级降级到可用档) */
|
||||
const char *gate_s = (gate == NCM_VIP_SVIP) ? "SVIP" : (gate == NCM_VIP_CVIP) ? "CVIP" : NULL;
|
||||
if (gate_s)
|
||||
snprintf(lbl, sizeof lbl, "%s (%s) · %s%s", ncm_level_name(lv),
|
||||
ncm_level_spec(lv), gate_s, (vip < gate) ? "·未解锁" : "");
|
||||
else
|
||||
snprintf(lbl, sizeof lbl, "%s (%s)", ncm_level_name(lv), ncm_level_spec(lv));
|
||||
meow_sheet_add_item(s, lbl);
|
||||
}
|
||||
int op = meow_sheet_open(s);
|
||||
meow_sheet_free(s);
|
||||
if (op >= 0 && op < 5) {
|
||||
g_st.level = lvls[op];
|
||||
if (op >= 0 && op < 8) {
|
||||
g_st.level = g_ui_levels[op];
|
||||
player_manager_set_quality(g_st.level, g_st.cache_limit_gb);
|
||||
appstate_save_settings();
|
||||
meow_ui_invalidate(r_cont.x, r_cont.y, r_cont.w, r_cont.h);
|
||||
|
||||
Reference in new issue
Block a user