feat(ncm): 下载落盘名用响应实际档 + 命中按文件名标实际档(M6 播放链路)

- song_download_attempt:URL/类型确定后算实际档,.part/open/commit 统一用实际档键
  (hires 请求被降级为 320k mp3 → song_<id>_exhigh.mp3),eff_level=实际档
- song_local_file:命中路径由 ncm_cache_level_from_path 取文件名档位并按容器钳顶
  (mp3 永不高于极高),旧 hires.mp3 正确显示「极高」
- 移除旧「stale-degraded 改名+强制重下」与「无档位键迁移」:老卡文件读可用、不迁移
- player_flow_sim 适配:补实际档/路径档位桩与 M5 链接桩;P7/P12 期望键改实际档,
  P13 改为旧命名命中即播、不重下不迁移
This commit is contained in:
AskaEth committed 2026-11-04 21:11:00 +08:00
1 parent 30ffe469c8
commit 68833cf642
2 files changed
+143 -100

No files matched your search

+45 -78
View File
@@ -499,7 +499,9 @@ static void song_gov_cleanup(song_src_t *ss)
meow_cachegov_drop_finished(ss->gov_final_path); meow_cachegov_drop_finished(ss->gov_final_path);
} }
/* 缓存键(用户定案 NCM_CACHE_DESIGN §2.5):歌曲ID + 音质档位 → "song_<id>_<level>.<ext>"。 /* 请求缓存键(用户定案 NCM_CACHE_DESIGN §2.5):歌曲ID + 用户音质档位 → "song_<id>_<level>.<ext>"。
* 只用于**查找**(命中下探由 ncm_cache_lookup 处理);**写入**的正式名用响应实际档
* (M6:hires 请求被降级链落到 exhigh mp3 → 落盘为 song_<id>_exhigh.mp3,名实相符)。
* 同歌同档命中不重复下载;换档=新条目,旧档留给 LRU 自然淘汰。 * 同歌同档命中不重复下载;换档=新条目,旧档留给 LRU 自然淘汰。
* §20:键后缀用 ncm_api_level_key(sky 与 sky_c51 可区分)。 */ * §20:键后缀用 ncm_api_level_key(sky 与 sky_c51 可区分)。 */
static void audio_cache_key(char *out, size_t sz, const char *id, int level) static void audio_cache_key(char *out, size_t sz, const char *id, int level)
@@ -507,19 +509,17 @@ static void audio_cache_key(char *out, size_t sz, const char *id, int level)
snprintf(out, sz, "%s_%s", id, ncm_api_level_key(level)); snprintf(out, sz, "%s_%s", id, ncm_api_level_key(level));
} }
/* §档位保真(REPORT §NEW 任务2②):显示档以响应实际格式/码率为准—— /* 响应 → 实际档(写盘文件名 / 播放页显示 / 已缓存列表三处同源,core ncm_cache_actual_level):
* - type=flac:采信降级链 eff(无损档链已保证 flac,br/采样由解码后定)。 * 容器 type 权威;mp3 按响应 br(≤128k→标准,否则极高,永不高于极高);flac 取降级链
* - type=mp3:绝不显示 Hi-Res 及以上(Hi-Res 需 >16bit/高采样,mp3 达不到)。按响应 br * 生效档 eff(头参数下载后才能测,落盘名不夸大)。只降不升(min(请求档, 实际档))。
* 参与判定:≤128k→标准;否则→极高 HQ(≤320k,仅降级链中间档「较高 192k」不进 UI,不上屏)。 * 返回实际档(g_play.eff_level 用)。 */
* - br 未知:钳 chain_eff 到 ≤EXHIGH(保守不夸大)。 static int actual_level_for_resp(const ncm_play_url_t *u, const char *ext, int chain_eff, int want)
* 返回 UI 显示用档位(g_play.eff_level)。 */
static int eff_level_for_resp(const ncm_play_url_t *u, int chain_eff, int want)
{ {
if (u && ncm_streq(u->type, "flac")) return (chain_eff >= 0) ? chain_eff : want; ncm_audio_info_t ai;
int br = u ? u->br : 0; memset(&ai, 0, sizeof ai);
if (br > 0) return (br <= 128000) ? NCM_LEVEL_STANDARD : NCM_LEVEL_EXHIGH; /* 按实际码率 */ ncm_strlcpy(ai.container, ext ? ext : "", sizeof ai.container);
int e = (chain_eff >= 0) ? chain_eff : want; if (u && u->br > 0 && ncm_streq(ai.container, "mp3")) ai.bitrate_kbps = u->br / 1000;
return (e < NCM_LEVEL_EXHIGH) ? e : NCM_LEVEL_EXHIGH; /* 未知:钳到 ≤EXHIGH */ return ncm_cache_actual_level(want, &ai, chain_eff);
} }
/* 同曲重播护栏(§25 切音质后重播同一曲,替代 §18 的 still_current):仅 cur/state 不够—— /* 同曲重播护栏(§25 切音质后重播同一曲,替代 §18 的 still_current):仅 cur/state 不够——
@@ -540,7 +540,7 @@ static int still_intent(int idx, int my_seq)
* 返回:NCM_OK=src 就绪(下载在后台继续);DL_RETRYABLE=预缓冲前网络失败(调用方整首重试); * 返回:NCM_OK=src 就绪(下载在后台继续);DL_RETRYABLE=预缓冲前网络失败(调用方整首重试);
* DL_ABORTED=切歌/退出(静默放弃,不重试);其它负数=硬错误(不重试)。 * DL_ABORTED=切歌/退出(静默放弃,不重试);其它负数=硬错误(不重试)。
* *real_err 回填预缓冲失败的真实错误码。失败时 ss->w/src/dl_thr 已清理干净。 */ * *real_err 回填预缓冲失败的真实错误码。失败时 ss->w/src/dl_thr 已清理干净。 */
static int song_download_attempt(const ncm_song_t *s, int idx, int seq, const char *key, int want, static int song_download_attempt(const ncm_song_t *s, int idx, int seq, int want,
song_src_t *ss, char *path, size_t psz, int64_t *bytes, song_src_t *ss, char *path, size_t psz, int64_t *bytes,
int *real_err) int *real_err)
{ {
@@ -553,15 +553,21 @@ static int song_download_attempt(const ncm_song_t *s, int idx, int seq, const ch
account_vip(), s->fee, want, &u, &eff); account_vip(), s->fee, want, &u, &eff);
if (rc != NCM_OK) return rc; if (rc != NCM_OK) return rc;
if (!u.url[0]) return NCM_ERR_NOTFOUND; if (!u.url[0]) return NCM_ERR_NOTFOUND;
pthread_mutex_lock(&g_play.mu);
g_play.eff_level = eff_level_for_resp(&u, eff, want); /* §2②:显示档按实际格式/码率 */
g_play.dl_total = (u.size > 0) ? u.size : 0; /* 缓冲进度% 分母(0=未知只转菊花) */
pthread_mutex_unlock(&g_play.mu);
/* 落盘扩展名:type 权威;type 缺失时看 url 扩展名,默认 mp3 */ /* 落盘扩展名:type 权威;type 缺失时看 url 扩展名,默认 mp3 */
const char *ext = "mp3"; const char *ext = "mp3";
if (ncm_streq(u.type, "flac")) ext = "flac"; if (ncm_streq(u.type, "flac")) ext = "flac";
else if (!u.type[0] && strstr(u.url, ".flac")) ext = "flac"; else if (!u.type[0] && strstr(u.url, ".flac")) ext = "flac";
ncm_strlcpy(ss->ext, ext, sizeof ss->ext); ncm_strlcpy(ss->ext, ext, sizeof ss->ext);
/* M6:写盘名用**响应实际档**(不是请求档)。hires 请求被降级链落到 exhigh mp3
* → song_<id>_exhigh.mp3,文件名与内容相符;同档再播按实际档键直接命中。
* .part 续传也用同一实际档键(同名才可续传/commit)。 */
int actual = actual_level_for_resp(&u, ext, eff, want);
char akey[64];
audio_cache_key(akey, sizeof akey, id, actual);
pthread_mutex_lock(&g_play.mu);
g_play.eff_level = actual; /* 显示档 = 实际档(与文件名/列表同源) */
g_play.dl_total = (u.size > 0) ? u.size : 0; /* 缓冲进度% 分母(0=未知只转菊花) */
pthread_mutex_unlock(&g_play.mu);
/* 文件内 tag 字段(下载完整后由 dl_thread 写入;空字段不写)。 */ /* 文件内 tag 字段(下载完整后由 dl_thread 写入;空字段不写)。 */
memset(&ss->tag, 0, sizeof ss->tag); memset(&ss->tag, 0, sizeof ss->tag);
ncm_strlcpy(ss->tag.title, s->name, sizeof ss->tag.title); ncm_strlcpy(ss->tag.title, s->name, sizeof ss->tag.title);
@@ -574,17 +580,17 @@ static int song_download_attempt(const ncm_song_t *s, int idx, int seq, const ch
/* 断点续传起点(§5.2):.part 残体存在且本次 URL 的 size > 残体大小 → 续传: /* 断点续传起点(§5.2):.part 残体存在且本次 URL 的 size > 残体大小 → 续传:
* 追加打开 .part(O_APPEND,保留已写字节),written 从 part_size 起算(读端秒开播)。 */ * 追加打开 .part(O_APPEND,保留已写字节),written 从 part_size 起算(读端秒开播)。 */
uint64_t part_size = 0; uint64_t part_size = 0;
int have_part = (ncm_cache_part_size(&g_st.ctx.cache, key, ext, &part_size) == NCM_OK) && int have_part = (ncm_cache_part_size(&g_st.ctx.cache, akey, ext, &part_size) == NCM_OK) &&
part_size > 0; part_size > 0;
int resuming = 0; int resuming = 0;
if (have_part && (long)part_size < u.size) { if (have_part && (long)part_size < u.size) {
rc = ncm_cache_open_resume(&g_st.ctx.cache, key, ext, &ss->w); rc = ncm_cache_open_resume(&g_st.ctx.cache, akey, ext, &ss->w);
if (rc == NCM_OK) { ss->resume_base = part_size; resuming = 1; } if (rc == NCM_OK) { ss->resume_base = part_size; resuming = 1; }
else have_part = 0; /* 残体打开失败:转整首 */ else have_part = 0; /* 残体打开失败:转整首 */
} }
if (!resuming) { if (!resuming) {
if (have_part) ncm_cache_part_remove(&g_st.ctx.cache, key, ext); /* 残体过期(size 不符):删 */ if (have_part) ncm_cache_part_remove(&g_st.ctx.cache, akey, ext); /* 残体过期(size 不符):删 */
rc = ncm_cache_open(&g_st.ctx.cache, key, ext, &ss->w); rc = ncm_cache_open(&g_st.ctx.cache, akey, ext, &ss->w);
if (rc != NCM_OK) return rc; if (rc != NCM_OK) return rc;
} }
if (stream_src_open_stream_at(&ss->src, ncm_cache_writer_tmp_path(ss->w), if (stream_src_open_stream_at(&ss->src, ncm_cache_writer_tmp_path(ss->w),
@@ -596,8 +602,8 @@ static int song_download_attempt(const ncm_song_t *s, int idx, int seq, const ch
ss->total = (u.size > 0) ? u.size : 0; ss->total = (u.size > 0) ? u.size : 0;
/* §NEW 任务1(SIGSEGV 定位):下载链路诊断日志——起播即留档,断流/崩溃后从 log 回溯。 */ /* §NEW 任务1(SIGSEGV 定位):下载链路诊断日志——起播即留档,断流/崩溃后从 log 回溯。 */
meow_log(MEOW_LOG_INFO, meow_log(MEOW_LOG_INFO,
"play: dl start id=%s lv=%s eff=%s size=%lld resume=%s url=%.56s", "play: dl start id=%s req=%s actual=%s size=%lld resume=%s url=%.56s",
id, ncm_api_level_key(want), ncm_api_level_key(eff >= 0 ? eff : want), id, ncm_api_level_key(want), ncm_api_level_key(actual),
(long long)u.size, resuming ? "yes" : "no", u.url); (long long)u.size, resuming ? "yes" : "no", u.url);
/* 上限:Hi-Res flac 单曲可达数百 MB,flac 上限 1GB;mp3 维持 64MB。 /* 上限:Hi-Res flac 单曲可达数百 MB,flac 上限 1GB;mp3 维持 64MB。
* size 可信时按 size 限幅(防超额),未知按格式默认上限(续传 body ≤ size 亦安全)。 */ * size 可信时按 size 限幅(防超额),未知按格式默认上限(续传 body ≤ size 亦安全)。 */
@@ -665,7 +671,7 @@ static int song_local_file(const ncm_song_t *s, int idx, int seq, song_src_t *ss
char key[64]; char key[64];
audio_cache_key(key, sizeof key, id, g_level); audio_cache_key(key, sizeof key, id, g_level);
/* 已缓存列表点播(NCM_QUEUE_CACHE):用条目**真实键**(id+文件名请求档)与实际扩展名直接 /* 已缓存列表点播(NCM_QUEUE_CACHE):用条目**真实键**(id+文件名档位)与实际扩展名直接
* 命中,不走用户当前档键——否则别的档位/降级产物条目会 miss 而被重下(与「点缓存就该放缓存」 * 命中,不走用户当前档键——否则别的档位/降级产物条目会 miss 而被重下(与「点缓存就该放缓存」
* 相悖,也会白耗流量)。条目已被删/被 LRU 淘汰 → NOENT(不重下,UI 报错)。 */ * 相悖,也会白耗流量)。条目已被删/被 LRU 淘汰 → NOENT(不重下,UI 报错)。 */
if (appstate_get_queue() == NCM_QUEUE_CACHE) { if (appstate_get_queue() == NCM_QUEUE_CACHE) {
@@ -690,75 +696,36 @@ static int song_local_file(const ncm_song_t *s, int idx, int seq, song_src_t *ss
return NCM_ERR_NOENT; return NCM_ERR_NOENT;
} }
/* 缓存命中 → 静态 stream_src(新键两扩展名都试;档位=用户设定档,降级产物也归该档键) */ /* 缓存命中 → 静态 stream_src。查找顺序(档位下探在 ncm_cache_lookup 内):
* ① 请求档精确(flac 优先于 mp3)
* ② 请求档以下逐档下探——缓存名已按**实际档**命名,hires 请求命中已存的
* song_<id>_exhigh.mp3(降级产物不重下)
* ③ 历史无档位键 song_<id>.mp3 / 双点名(老卡存量:读可用,**不强制迁移**) */
int hit = 0; int hit = 0;
if (ncm_cache_lookup(&g_st.ctx.cache, key, "flac", path, psz) == NCM_OK || 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) { ncm_cache_lookup(&g_st.ctx.cache, key, "mp3", path, psz) == NCM_OK ||
ncm_cache_lookup(&g_st.ctx.cache, id, "mp3", path, psz) == NCM_OK) {
hit = 1; hit = 1;
} else if (ncm_cache_lookup(&g_st.ctx.cache, id, "mp3", path, psz) == NCM_OK) {
/* §19 前存量:无档位后缀的 song_<id>[.]mp3(含历史双点名 song_<id>..mp3)。
* §NEW 任务5:命中即**迁移重命名**到当前档位键 song_<id>_<level>.mp3(下方 hit==2
* 分支做 rename,不重下),之后不再回退旧键。 */
hit = 2;
} }
if (hit) { if (hit) {
struct stat st; struct stat st;
if (stat(path, &st) != 0) return NCM_ERR_NOENT; if (stat(path, &st) != 0) return NCM_ERR_NOENT;
if (hit == 2) { /* 生效/显示档:新命名文件名即实际档;旧命名(请求档)向下钳——mp3 实体不可能高于
/* §NEW 任务5:存量旧键(无档位后缀 song_<id>.mp3 / 历史双点 song_<id>..mp3) * 极高,故旧 song_<id>_hires.mp3 标注 EXHIGH;旧无档位键条目按用户档同规则钳。
* 迁移重命名到当前档位键 song_<id>_<g_level>.<ext>(**不重下**,仅改名)。 * 只改显示不改文件名(老卡文件继续可播/可删/可淘汰,免迁移)。 */
* 目标已存在(该档已有正式条目)则删旧;迁移失败回退旧路径照常播。 */
char nk[64], np[512];
audio_cache_key(nk, sizeof nk, id, g_level);
const char *dot = strrchr(path, '.'); const char *dot = strrchr(path, '.');
const char *ext = dot ? dot + 1 : "mp3"; int eff = ncm_cache_level_from_path(path);
if (ncm_cache_path(&g_st.ctx.cache, nk, ext, np, sizeof np) == NCM_OK) { if (eff < 0) eff = g_level;
struct stat nst; if (dot && ncm_streq(dot, ".mp3") && eff > NCM_LEVEL_EXHIGH) eff = NCM_LEVEL_EXHIGH;
if (stat(np, &nst) == 0) unlink(path); /* 目标已在(或并发建成):删旧双点/旧名 */
else rename(path, np);
ncm_strlcpy(path, np, psz); /* 迁移后即用新路径(无论 rename 成败,
* 覆盖到同一目标;失败则下次会再试) */
if (stat(np, &st) != 0) return NCM_ERR_NOENT;
}
}
/* §NEW 任务2:静默降级时代产物修复(用户实锤:hires 档键下存着 mp3,播放命中旧档)。
* 用户档 > EXHIGH(无损/更高)且缓存实体是 .mp3 = flac 门禁前把 exhigh 的 mp3 写进了
* 无损域键(当年降级产物归用户档键)。迁移重命名到 exhigh 键 song_<id>_exhigh.mp3
* (选「极高」档时仍可用;目标已存在则删旧),然后本档视为未命中走下载——
* 311 的 flac 门禁会拿到真无损或正确降级。放在 hit==2 迁移块之后、静态流打开之前。 */
const char *mdot = strrchr(path, '.');
if (g_level > NCM_LEVEL_EXHIGH && mdot && ncm_streq(mdot, ".mp3")) {
char kh[64], ph[512];
audio_cache_key(kh, sizeof kh, id, NCM_LEVEL_EXHIGH);
if (ncm_cache_path(&g_st.ctx.cache, kh, "mp3", ph, sizeof ph) == NCM_OK) {
struct stat hst;
if (stat(ph, &hst) == 0) unlink(path); /* exhigh 目标已在:删旧(降级产物) */
else rename(path, ph);
meow_log(MEOW_LOG_INFO,
"play: stale-degraded id=%s lv=%s entity=mp3 -> exhigh key; redownload",
id, ncm_api_level_key(g_level));
hit = 0; /* 本档不符 → 走下载(flac 门禁取真档) */
}
}
if (hit) {
if (stream_src_open_static(&ss->src, path, (uint64_t)st.st_size) != 0) if (stream_src_open_static(&ss->src, path, (uint64_t)st.st_size) != 0)
return NCM_ERR_IO; return NCM_ERR_IO;
ss->total = (int64_t)st.st_size; ss->total = (int64_t)st.st_size;
*bytes = ss->total; *bytes = ss->total;
/* 生效档推断(缓存键=用户档,降级产物归用户档键,无法精确还原):
* mp3 实体 + 用户档 > 极高 → 必为降级产物,标 EXHIGH;其余按用户档显示。
* (flac 实体视为与用户档一致;hires→lossless 档内降级会高估显示,已知取舍。)
* 注:mp3 + 用户档 > 极高 的组合已被上方迁移块拦截走下载,此处仅存防御。 */
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); pthread_mutex_lock(&g_play.mu);
g_play.eff_level = eff; g_play.eff_level = eff;
pthread_mutex_unlock(&g_play.mu); pthread_mutex_unlock(&g_play.mu);
return NCM_OK; return NCM_OK;
} }
}
/* fee 门禁:VIP 才能听(1=VIP 歌曲 / 4=付费数字专辑)且非会员 → 不请求 url。 /* fee 门禁:VIP 才能听(1=VIP 歌曲 / 4=付费数字专辑)且非会员 → 不请求 url。
* (必须在缓存查找之后:已缓存的过期/下架曲本地可播,NCM_CACHE_DESIGN §2.4。) */ * (必须在缓存查找之后:已缓存的过期/下架曲本地可播,NCM_CACHE_DESIGN §2.4。) */
@@ -781,7 +748,7 @@ static int song_local_file(const ncm_song_t *s, int idx, int seq, song_src_t *ss
if (g_app_quit || !still_intent(idx, seq)) return NCM_ERR_IO; if (g_app_quit || !still_intent(idx, seq)) return NCM_ERR_IO;
} }
int real_err = NCM_ERR_NET; int real_err = NCM_ERR_NET;
int rc = song_download_attempt(s, idx, seq, key, want, ss, path, psz, bytes, &real_err); int rc = song_download_attempt(s, idx, seq, want, ss, path, psz, bytes, &real_err);
if (rc == NCM_OK) return NCM_OK; if (rc == NCM_OK) return NCM_OK;
if (rc == DL_RETRYABLE) { last_rc = real_err; continue; } /* 重试整首 */ if (rc == DL_RETRYABLE) { last_rc = real_err; continue; } /* 重试整首 */
return rc; /* DL_ABORTED / 硬错误:不重试 */ return rc; /* DL_ABORTED / 硬错误:不重试 */
+98 -22
View File
@@ -14,8 +14,8 @@
* P12 切音质后重播同一曲(§25):同 idx 在播时再 play —— 修前被解码主循环忽略 * P12 切音质后重播同一曲(§25):同 idx 在播时再 play —— 修前被解码主循环忽略
* (旧曲继续出声、state 恒 BUFFER/「缓冲中」,收尾被外层当播完顺延下一首); * (旧曲继续出声、state 恒 BUFFER/「缓冲中」,收尾被外层当播完顺延下一首);
* 修后 seq 变化即重启换档,状态回 PLAYING、新档位下载、eff_level 回填。 * 修后 seq 变化即重启换档,状态回 PLAYING、新档位下载、eff_level 回填。
* P13 陈旧 hires.mp3(flac 门禁前静默降级产物,hires 键下存 mp3):song_local_file * P13 旧命名 hires.mp3(卡上存量:hires 键下存 mp3):M6 起按文件名档位+容器钳顶显示
* 迁移重命名到 exhigh 键(song_<id>_exhigh.mp3)+ 本档视为 miss 走下载(§NEW 任务2)。 * EXHIGH 直接播放,不重下、不迁移(老卡文件继续可播/可删/可淘汰)。
* P14 歌单里点「正在播的同曲」→ 只聚焦不重播(任务3 re-entry,同曲 id 判据)。 * P14 歌单里点「正在播的同曲」→ 只聚焦不重播(任务3 re-entry,同曲 id 判据)。
* P15 跨歌单切换后同一队列下标点歌 → 不被同曲护栏吞,起播(Bug2 回归:真机日志 * P15 跨歌单切换后同一队列下标点歌 → 不被同曲护栏吞,起播(Bug2 回归:真机日志
* `play: re-enter current idx=0 state=2 -> no restart`,新歌永不播)。 * `play: re-enter current idx=0 state=2 -> no restart`,新歌永不播)。
@@ -87,12 +87,22 @@ int appstate_queue_song(int idx, ncm_song_t *out) { return appstate_song_copy(id
* sim 恒歌单窗口队列、搜索数组为空。appstate.c 未链入,两符号此处提供桩。 */ * sim 恒歌单窗口队列、搜索数组为空。appstate.c 未链入,两符号此处提供桩。 */
int appstate_get_queue(void) { return NCM_QUEUE_PLAYLIST; } int appstate_get_queue(void) { return NCM_QUEUE_PLAYLIST; }
int appstate_search_song(int i, ncm_song_t *out) { (void)i; (void)out; return NCM_ERR_ARG; } int appstate_search_song(int i, ncm_song_t *out) { (void)i; (void)out; return NCM_ERR_ARG; }
/* M5 已缓存列表 / 文件内 tag:本 sim 不链 appstate.c、ncm_tag.c(缓存列表语义由 M5/M6 的
* core 测试覆盖),仅提供链接桩避免 gc-sections 之外的空洞。 */
int appstate_cache_list_load(void) { return 0; }
int appstate_cache_count(void) { return 0; }
int appstate_cache_item(int i, ncm_cache_item_t *out) { (void)i; (void)out; return NCM_ERR_ARG; }
int appstate_cache_song(int i, ncm_song_t *out) { (void)i; (void)out; return NCM_ERR_ARG; }
int appstate_cache_remove(int i) { (void)i; return NCM_ERR_ARG; }
void appstate_cache_clear(void) { }
int ncm_tag_embed_file(const char *path, const char *ext, const ncm_tag_meta_t *m)
{ (void)path; (void)ext; (void)m; return NCM_ERR_ARG; } /* 降级为裸流(生产路径容忍) */
/* ---- 缓存/网络 stub ---- /* ---- 缓存/网络 stub ----
* g_cache_ok=1 → lookup 命中(给真实存在的 /tmp 文件,免下载); * g_cache_ok=1 → lookup 命中(给真实存在的 /tmp 文件,免下载);
* 否则走下载:song_url(g_url_rc 可注入失败) → http_download(g_dl_delay_ms 模拟慢) → commit。 */ * 否则走下载:song_url(g_url_rc 可注入失败) → http_download(g_dl_delay_ms 模拟慢) → commit。 */
static int g_cache_ok = 1; static int g_cache_ok = 1;
static char g_cache_real[600]; /* g_cache_ok==2:lookup 命中返回此真实路径(P13 陈旧 mp3 迁移用) */ static char g_cache_real[600]; /* g_cache_ok==2:lookup 命中返回此真实路径(P13 旧命名命中用) */
static int g_url_rc = NCM_OK; static int g_url_rc = NCM_OK;
static int g_dl_delay_ms = 0; static int g_dl_delay_ms = 0;
static int g_downloads; /* http_download 调用数(song_url 请求放大检测) */ static int g_downloads; /* http_download 调用数(song_url 请求放大检测) */
@@ -138,7 +148,7 @@ static void pf_ensure_sim_mp3(void)
int ncm_cache_lookup(const ncm_cache_t *c, const char *id, const char *ext, char *out, size_t out_sz) int ncm_cache_lookup(const ncm_cache_t *c, const char *id, const char *ext, char *out, size_t out_sz)
{ {
(void)c; (void)id; (void)c; (void)id;
if (g_cache_ok == 2) { /* P13 真实命中模式:仅 mp3 命中(stale hires 实体是 mp3) */ if (g_cache_ok == 2) { /* P13 真实命中模式:仅 mp3 命中(旧命名 hires.mp3 实体是 mp3) */
if (ext && strcmp(ext, "mp3") == 0 && g_cache_real[0]) { if (ext && strcmp(ext, "mp3") == 0 && g_cache_real[0]) {
snprintf(out, out_sz, "%s", g_cache_real); snprintf(out, out_sz, "%s", g_cache_real);
return NCM_OK; return NCM_OK;
@@ -195,6 +205,59 @@ int ncm_cache_path(const ncm_cache_t *c, const char *id, const char *ext, char *
(c && c->dir[0]) ? c->dir : "/tmp", id ? id : "", e ? e : "bin"); (c && c->dir[0]) ? c->dir : "/tmp", id ? id : "", e ? e : "bin");
return NCM_OK; return NCM_OK;
} }
/* M6:player_manager 用 core 的实际档判定/路径档位解析(本 sim 不链 core/ncm_cache.c,
* 此处按 core 同规则提供桩——命名规则本身由 test/test_ncm_cache_host 全量断言)。 */
int ncm_cache_actual_level(int req, const ncm_audio_info_t *ai, int resp_level)
{
int cap = -1, v;
if (resp_level < 0) resp_level = req;
if (ai && ai->container[0]) {
if (strcmp(ai->container, "flac") == 0) {
if (ai->bits_per_sample <= 0 || ai->sample_rate <= 0) cap = -1;
else if (ai->bits_per_sample <= 16) cap = NCM_LEVEL_LOSSLESS;
else if (ai->sample_rate > 96000) cap = NCM_LEVEL_JYMASTER;
else if (ai->sample_rate > 48000) cap = NCM_LEVEL_JYEFFECT;
else cap = NCM_LEVEL_HIRES;
} else if (strcmp(ai->container, "mp3") == 0) {
if (ai->bitrate_kbps > 128) cap = NCM_LEVEL_EXHIGH;
else if (ai->bitrate_kbps > 0) cap = NCM_LEVEL_STANDARD;
}
}
if (ai && strcmp(ai->container, "mp3") == 0) {
if (cap < 0) cap = NCM_LEVEL_EXHIGH;
if (resp_level >= 0 && resp_level < cap) cap = resp_level;
} else if (cap < 0) {
cap = resp_level;
} else if (resp_level >= 0 && resp_level < cap) {
cap = resp_level;
}
if (req < 0) v = (cap < 0) ? -1 : cap;
else if (cap < 0) v = req;
else v = (req < cap) ? req : cap;
return v;
}
int ncm_cache_level_from_path(const char *path)
{
const char *base, *dot, *us = NULL, *us2 = NULL;
char key[20];
size_t n;
int lv;
if (!path || !path[0]) return -1;
base = strrchr(path, '/'); base = base ? base + 1 : path;
dot = strrchr(base, '.'); if (!dot) dot = base + strlen(base);
for (const char *p = base; p < dot; p++) if (*p == '_') { us2 = us; us = p; }
if (!us || us + 1 >= dot) return -1;
n = (size_t)(dot - us - 1);
if (n >= sizeof key) return -1;
memcpy(key, us + 1, n); key[n] = 0;
lv = ncm_api_level_from_key(key);
if (lv >= 0 || !us2 || us2 + 1 >= us - 1) return lv;
n = (size_t)(dot - us2 - 1);
if (n >= sizeof key) return -1;
memcpy(key, us2 + 1, n); key[n] = 0;
return ncm_api_level_from_key(key);
}
int ncm_api_song_url_auto(const char *dev, const char *ck, const char *id, int level, int ncm_api_song_url_auto(const char *dev, const char *ck, const char *id, int level,
ncm_play_url_t *out, int *out_level) ncm_play_url_t *out, int *out_level)
@@ -310,6 +373,20 @@ const char *ncm_api_level_key(int level)
default: return "standard"; default: return "standard";
} }
} }
int ncm_api_level_from_key(const char *key)
{
if (!key || !key[0]) return -1;
if (!strcmp(key, "sky_c51")) return NCM_LEVEL_SURROUND;
if (!strcmp(key, "sky")) return NCM_LEVEL_SKY;
if (!strcmp(key, "jymaster")) return NCM_LEVEL_JYMASTER;
if (!strcmp(key, "jyeffect")) return NCM_LEVEL_JYEFFECT;
if (!strcmp(key, "hires")) return NCM_LEVEL_HIRES;
if (!strcmp(key, "lossless")) return NCM_LEVEL_LOSSLESS;
if (!strcmp(key, "exhigh")) return NCM_LEVEL_EXHIGH;
if (!strcmp(key, "higher")) return NCM_LEVEL_HIGHER;
if (!strcmp(key, "standard")) return NCM_LEVEL_STANDARD;
return -1;
}
/* ---- 音频输出 stub:g_write_fail 注入写失败 ---- */ /* ---- 音频输出 stub:g_write_fail 注入写失败 ---- */
static int g_write_fail; static int g_write_fail;
@@ -467,8 +544,8 @@ int main(void)
g_url_req_level, NCM_LEVEL_EXHIGH); g_url_req_level, NCM_LEVEL_EXHIGH);
CHECK(player_manager_eff_level() == NCM_LEVEL_EXHIGH, CHECK(player_manager_eff_level() == NCM_LEVEL_EXHIGH,
"eff_level=%d (expect EXHIGH,UI 标「已降级至 极高 HQ」)", player_manager_eff_level()); "eff_level=%d (expect EXHIGH,UI 标「已降级至 极高 HQ」)", player_manager_eff_level());
CHECK(strcmp(g_last_key, "1000_sky_c51") == 0, CHECK(strcmp(g_last_key, "1000_exhigh") == 0,
"cache key=%s (expect 1000_sky_c51 = id+用户档,降级产物归用户档键)", g_last_key); "cache key=%s (expect 1000_exhigh = 实际档键,非用户档 sky_c51)", g_last_key);
/* SVIP:同一用户档不钳顶,直达 SURROUND。 /* SVIP:同一用户档不钳顶,直达 SURROUND。
* §21 时序:commit 移到 song0 曲尾 dispose(边下边播)——先 stop 阻断外层自动顺延 * §21 时序:commit 移到 song0 曲尾 dispose(边下边播)——先 stop 阻断外层自动顺延
* 到 song1 的竞态,再等 commit 落地后才切缓存开关,否则 commit stub 会把 * 到 song1 的竞态,再等 commit 落地后才切缓存开关,否则 commit stub 会把
@@ -500,8 +577,7 @@ int main(void)
player_manager_error()); player_manager_error());
CHECK(g_url_calls == 0, "song_url 零请求(省一次请求,防风控;calls=%d)", g_url_calls); CHECK(g_url_calls == 0, "song_url 零请求(省一次请求,防风控;calls=%d)", g_url_calls);
/* 已缓存的 VIP 曲:门禁不拦(缓存优先,NCM_CACHE_DESIGN §2.4)。 /* 已缓存的 VIP 曲:门禁不拦(缓存优先,NCM_CACHE_DESIGN §2.4)。
* 注:把档位降回 EXHIGH——缓存 stub 的「命中」恒是 mp3;用户档若仍 SURROUND(>EXHIGH), * 档位设 EXHIGH:stub 缓存恒返回 mp3(mp3 实际档不高于极高),与条目标注一致。 */
* 会被 §NEW 任务2 当作静默降级产物迁移/重下,与「绕过门禁」的本意不符(P13 单独测迁移)。 */
player_manager_set_quality(NCM_LEVEL_EXHIGH, 10); player_manager_set_quality(NCM_LEVEL_EXHIGH, 10);
g_cache_ok = 1; g_cache_ok = 1;
player_manager_play(2); player_manager_play(2);
@@ -595,8 +671,8 @@ int main(void)
CHECK(wait_state(NCM_PC_PLAYING, 4000), CHECK(wait_state(NCM_PC_PLAYING, 4000),
"re-tap same idx -> back to PLAYING (expect 换档开播,非卡 缓冲中)"); "re-tap same idx -> back to PLAYING (expect 换档开播,非卡 缓冲中)");
CHECK(g_downloads >= 1, "re-tap started new-quality download (downloads=%d)", g_downloads); CHECK(g_downloads >= 1, "re-tap started new-quality download (downloads=%d)", g_downloads);
CHECK(strcmp(g_last_key, "1000_lossless") == 0, CHECK(strcmp(g_last_key, "1000_exhigh") == 0,
"re-tap cache key=%s (expect 1000_lossless = 新档位)", g_last_key); "re-tap cache key=%s (expect 1000_exhigh = 实际档,非会员钳顶后 320k mp3)", g_last_key);
/* sim 账号非会员 → lossless 被 §20 钳顶到 EXHIGH;关键是 eff_level 已按新意图回填 /* sim 账号非会员 → lossless 被 §20 钳顶到 EXHIGH;关键是 eff_level 已按新意图回填
* (UI 据此显示实际生效档,不再停 BUFFER/「缓冲中」)。 */ * (UI 据此显示实际生效档,不再停 BUFFER/「缓冲中」)。 */
CHECK(player_manager_eff_level() == NCM_LEVEL_EXHIGH, CHECK(player_manager_eff_level() == NCM_LEVEL_EXHIGH,
@@ -604,30 +680,30 @@ int main(void)
g_eof = 1; g_eof = 1;
wait_state(NCM_PC_STOPPED, 3000); wait_state(NCM_PC_STOPPED, 3000);
printf("== P13: 陈旧 hires.mp3(静默降级产物)-> exhigh 迁移 + 本档 miss 重下(§NEW 任务2)==\n"); printf("== P13: 旧命名 hires.mp3(实际 exhigh)-> 命中播放 + 实际档显示,不重下不迁移(M6)==\n");
usleep(300 * 1000); usleep(300 * 1000);
g_eof = 0; g_reads_left = -1; g_url_rc = NCM_OK; g_fee = 0; g_eof = 0; g_reads_left = -1; g_url_rc = NCM_OK; g_fee = 0;
g_part_size = 0; g_http_status = 200; g_416_first = 0; g_dl_delay_ms = 0; g_dl_fail = 0; g_part_size = 0; g_http_status = 200; g_416_first = 0; g_dl_delay_ms = 0; g_dl_fail = 0;
g_vip = NCM_VIP_NONE; g_writes = 0; g_downloads = 0; g_url_calls = 0; g_vip = NCM_VIP_NONE; g_writes = 0; g_downloads = 0; g_url_calls = 0;
g_last_key[0] = 0; g_last_ext[0] = 0; g_url_type = "mp3"; g_last_key[0] = 0; g_last_ext[0] = 0; g_url_type = "mp3";
/* 用户档 = Hi-Res(>EXHIGH),缓存实体是一首 mp3 = flac 门禁前把 exhigh 的 mp3 写进 hires /* 用户档 = Hi-Res(>EXHIGH),卡上存量是旧命名 song_1000_hires.mp3,实体是 320k mp3。
* 键的「静默降级」产物。须被迁移到 exhigh 键(song_1000_exhigh.mp3),本档视为 miss 走下载。 */ * M6:文件名语义改为「实际档」后,此条目按文件名档位+容器钳顶显示 EXHIGH,直接播放;
unlink("/tmp/song_1000_exhigh.mp3"); /* 保证 exhigh 目标不存在(走 rename 而非删旧) */ * 不迁移、不重下(旧文件继续可播/可删/可淘汰)。 */
FILE *hf = fopen("/tmp/pf_hires.mp3", "wb"); FILE *hf = fopen("/tmp/pf_hires.mp3", "wb");
if (hf) { fputs("DEGRADED", hf); fclose(hf); } if (hf) { fputs("DEGRADED", hf); fclose(hf); }
snprintf(g_cache_real, sizeof g_cache_real, "/tmp/pf_hires.mp3"); snprintf(g_cache_real, sizeof g_cache_real, "/tmp/pf_hires.mp3");
g_cache_ok = 2; /* lookup 命中返回 g_cache_real(仅 mp3 键) */ g_cache_ok = 2; /* lookup 命中返回 g_cache_real(仅 mp3 键) */
player_manager_set_quality(NCM_LEVEL_HIRES, 10); player_manager_set_quality(NCM_LEVEL_HIRES, 10);
player_manager_play(0); player_manager_play(0);
CHECK(wait_state(NCM_PC_PLAYING, 3000), "stale hires.mp3 redownloaded -> plays"); CHECK(wait_state(NCM_PC_PLAYING, 3000), "legacy hires.mp3 hit -> plays directly");
CHECK(g_downloads >= 1, "stale mp3 triggered redownload (downloads=%d)", g_downloads); CHECK(g_downloads == 0, "legacy entry hit -> no redownload (downloads=%d)", g_downloads);
CHECK(strcmp(g_last_key, "1000_hires") == 0, CHECK(player_manager_eff_level() == NCM_LEVEL_EXHIGH,
"redownload cache key=%s (expect 1000_hires=用户档键)", g_last_key); "eff_level=%d (expect EXHIGH:mp3 实体不可能高于极高)", player_manager_eff_level());
CHECK(strcmp(g_last_ext, "mp3") == 0, "redownload ext=%s (请求档为 mp3,维持原身份)", g_last_ext);
struct stat mm; struct stat mm;
CHECK(stat("/tmp/song_1000_exhigh.mp3", &mm) == 0, CHECK(stat("/tmp/pf_hires.mp3", &mm) == 0 && mm.st_size == 8,
"exhigh migrated file exists (选「极高」档仍可用)"); "legacy file untouched (no migration)");
CHECK(stat("/tmp/pf_hires.mp3", &mm) != 0, "stale hires.mp3 source gone (已迁走)"); CHECK(stat("/tmp/song_1000_exhigh.mp3", &mm) != 0,
"no renamed copy created (old card files need no migration)");
g_cache_ok = 0; g_cache_real[0] = 0; g_cache_ok = 0; g_cache_real[0] = 0;
g_eof = 1; g_eof = 1;
wait_state(NCM_PC_STOPPED, 3000); wait_state(NCM_PC_STOPPED, 3000);