feat(ncm): 缓存分目录(songs//covers/)+ 陈旧 mp3 缓存修复
任务1 缓存分目录(用户定案):\n- ncm_cache_init mkdir songs/ 与 covers/ 子目录 + 一次性迁移\n (根混放 song_*/cov*/cover_current.jpg 挪进两子目录,幂等,legacy_migrated 计数)\n- ncm_cache_path 按 id 前缀路由(cov*→covers/,其余→songs/)\n- LRU(evict/total_bytes/7天 .part 回收)只扫 songs/;clear 清 songs/+covers/ 的 song_*\n- cover.c save_cover_raw 落 covers/cover_current.jpg\n\n任务2 陈旧 mp3 缓存修复(真机实锤:hires 档键下存 mp3,播放命中旧档):\n- song_local_file 缓存命中且用户档>EXHIGH(无损+)且实体是 .mp3 = 静默降级产物\n → 迁移重命名到 exhigh 键 song_<id>_exhigh.mp3(选极高仍可用;目标已存在删旧)\n → 本档视为未命中走下载(311 flac 门禁拿真无损/正确降级)\n\n验证:cache_evict_sim case10(目录迁移一次成型)+P1-9;player_flow_sim P13(迁移+miss)\n+P1-12 零回归;stream/window/search/degrade/cover_chain/test_ncm_core(host+qemu) 全 PASS;\nbuild_ncm.sh -Wall -Wextra 0 warning。0 真实请求。新 ncm md5 a90ba8c3 (text1028458/bss1364484)。
This commit is contained in:
1 parent
986e3814bc
commit
de18cef02c
11 files changed
+299
-64
No files matched your search
@@ -8,6 +8,8 @@
|
||||
* ③ ncm_cache_lookup 刷新 mtime(LRU:被访问的最近优先存活);
|
||||
* ④ ncm_cache_set_limit_gb 越界钳制(1~100GB);
|
||||
* ⑤ ncm_cache_clear 删除 song_* 全部(mp3/封面/残留 .tmp)。
|
||||
* §NEW 缓存分目录:song_* 落 songs/、封面(song_cov*、cover_current.jpg)落 covers/;
|
||||
* LRU/7 天回收只扫 songs/;⑩ 覆盖 ncm_cache_init 一次性把根混放文件迁入两子目录。
|
||||
* 文件用 ftruncate 生成稀疏文件(逻辑大小可控,磁盘零消耗)。
|
||||
*
|
||||
* 编译(宿主,链接 core/ncm_cache.c + 本文件):
|
||||
@@ -103,20 +105,27 @@ static int cached_exists(const char *id, const char *ext)
|
||||
|
||||
static unsigned long long mb(unsigned long long n) { return n * 1024ULL * 1024ULL; }
|
||||
|
||||
static void sweep_dir(const char *dir)
|
||||
{
|
||||
DIR *d = opendir(dir);
|
||||
if (!d) return;
|
||||
struct dirent *e;
|
||||
while ((e = readdir(d))) {
|
||||
if (strncmp(e->d_name, "song_", 5) != 0) continue;
|
||||
char p[900];
|
||||
snprintf(p, sizeof p, "%s/%s", dir, e->d_name);
|
||||
unlink(p);
|
||||
}
|
||||
closedir(d);
|
||||
}
|
||||
|
||||
/* 分目录后:song_* 落在 songs/(封面条目 song_cov* 在 covers/),清根 + 两个子目录。 */
|
||||
static void cleanup(void)
|
||||
{
|
||||
/* 递归删 g_dir 下 song_*(恢复干净池) */
|
||||
DIR *d = opendir(g_dir);
|
||||
if (d) {
|
||||
struct dirent *e;
|
||||
while ((e = readdir(d))) {
|
||||
if (strncmp(e->d_name, "song_", 5) != 0) continue;
|
||||
char p[800];
|
||||
snprintf(p, sizeof p, "%s/%s", g_dir, e->d_name);
|
||||
unlink(p);
|
||||
}
|
||||
closedir(d);
|
||||
}
|
||||
char sub[640];
|
||||
sweep_dir(g_dir);
|
||||
snprintf(sub, sizeof sub, "%s/songs", g_dir); sweep_dir(sub);
|
||||
snprintf(sub, sizeof sub, "%s/covers", g_dir); sweep_dir(sub);
|
||||
}
|
||||
|
||||
int main(void)
|
||||
@@ -330,6 +339,54 @@ int main(void)
|
||||
CHECK(stat(lp, &st) == 0, "fresh .part kept (inflight/resumable)");
|
||||
}
|
||||
|
||||
/* ⑩ §NEW 缓存分目录:ncm_cache_init 一次性把根混放的 song_*、cov*、cover_current.jpg 迁入
|
||||
* songs//covers/(幂等),迁移后 lookup/total 照常在 songs/ 工作(覆盖场景 1)。 */
|
||||
printf("== case 10: init legacy migration songs//covers/ one-shot ==\n");
|
||||
cleanup();
|
||||
{
|
||||
char p[900], sp[900], path[900];
|
||||
struct stat st;
|
||||
/* 根放旧布局混血文件:歌曲(含双点畸形/在途 .tmp)+ 封面条目 + cover_current */
|
||||
snprintf(p, sizeof p, "%s/song_legacy..mp3", g_dir);
|
||||
FILE *f = fopen(p, "wb"); if (f) { fputs("OLD1", f); fclose(f); }
|
||||
snprintf(p, sizeof p, "%s/song_legacy2.mp3", g_dir);
|
||||
f = fopen(p, "wb"); if (f) { fputs("OLD2", f); fclose(f); }
|
||||
snprintf(p, sizeof p, "%s/song_legacy3.mp3.tmp", g_dir); /* 在途 .tmp 随迁 */
|
||||
f = fopen(p, "wb"); if (f) { fputs("TMP!", f); fclose(f); }
|
||||
snprintf(p, sizeof p, "%s/song_covDEAD.jpg", g_dir);
|
||||
f = fopen(p, "wb"); if (f) { fputs("COV", f); fclose(f); }
|
||||
snprintf(p, sizeof p, "%s/cover_current.jpg", g_dir);
|
||||
f = fopen(p, "wb"); if (f) { fputs("CUR", f); fclose(f); }
|
||||
|
||||
/* 重 init 触发迁移(memset 复位 + mkdir + 迁移);注意 limit 被复位到默认 10GB。 */
|
||||
ncm_cache_init(&g_c, g_dir);
|
||||
ncm_cache_set_limit_gb(&g_c, 1);
|
||||
CHECK(g_c.legacy_migrated == 5, "legacy_migrated=%d (expect 5)", g_c.legacy_migrated);
|
||||
|
||||
snprintf(sp, sizeof sp, "%s/songs/song_legacy..mp3", g_dir);
|
||||
CHECK(stat(sp, &st) == 0, "double-dot song moved into songs/");
|
||||
snprintf(sp, sizeof sp, "%s/songs/song_legacy2.mp3", g_dir);
|
||||
CHECK(stat(sp, &st) == 0, "song moved into songs/");
|
||||
snprintf(sp, sizeof sp, "%s/songs/song_legacy3.mp3.tmp", g_dir);
|
||||
CHECK(stat(sp, &st) == 0, "in-flight .tmp moved into songs/");
|
||||
snprintf(sp, sizeof sp, "%s/covers/song_covDEAD.jpg", g_dir);
|
||||
CHECK(stat(sp, &st) == 0, "cover cache entry moved into covers/");
|
||||
snprintf(sp, sizeof sp, "%s/covers/cover_current.jpg", g_dir);
|
||||
CHECK(stat(sp, &st) == 0, "cover_current.jpg moved into covers/");
|
||||
snprintf(sp, sizeof sp, "%s/song_legacy2.mp3", g_dir);
|
||||
CHECK(stat(sp, &st) != 0, "root song_* gone after migration");
|
||||
|
||||
/* 双点存量在 songs/ 内被 lookup 修成单点(不重下) */
|
||||
CHECK(ncm_cache_lookup(&g_c, "legacy", "mp3", path, sizeof path) == NCM_OK,
|
||||
"migrated double-dot lookup hit");
|
||||
CHECK(strstr(path, "song_legacy.mp3") != NULL && strstr(path, "..") == NULL,
|
||||
"double-dot fixed to single-dot (%s)", path);
|
||||
/* total 只计 songs/(OLD1 4B + OLD2 4B;.tmp 半成品/封面不在 LRU 池) */
|
||||
CHECK(ncm_cache_total_bytes(&g_c) == 8,
|
||||
"total counts songs only (=%llu, expect 8)",
|
||||
(unsigned long long)ncm_cache_total_bytes(&g_c));
|
||||
}
|
||||
|
||||
cleanup();
|
||||
printf("%s\n", g_fail ? "RESULT: FAIL" : "RESULT: PASS");
|
||||
return g_fail ? 1 : 0;
|
||||
|
||||
@@ -269,11 +269,14 @@ static unsigned long cur_cover_hash(void)
|
||||
return h;
|
||||
}
|
||||
|
||||
/* 读 cover_current.jpg 并哈希(验证 launcher 媒体卡片读的文件同步更新)。 */
|
||||
/* 读 cover_current.jpg 并哈希(验证 launcher 媒体卡片读的文件同步更新)。
|
||||
* §NEW 缓存分目录:cover_current.jpg 落 covers/(与歌曲 song_* 分池)。 */
|
||||
static unsigned long curjpg_hash(void)
|
||||
{
|
||||
char path[600];
|
||||
snprintf(path, sizeof path, "%s/cover_current.jpg", g_st.cache_dir);
|
||||
const char *covdir = g_st.ctx.cache.covers_dir[0] ? g_st.ctx.cache.covers_dir
|
||||
: g_st.cache_dir;
|
||||
snprintf(path, sizeof path, "%s/cover_current.jpg", covdir);
|
||||
FILE *f = fopen(path, "rb");
|
||||
if (!f) return 0;
|
||||
unsigned char b[4096];
|
||||
|
||||
@@ -14,6 +14,8 @@
|
||||
* P12 切音质后重播同一曲(§25):同 idx 在播时再 play —— 修前被解码主循环忽略
|
||||
* (旧曲继续出声、state 恒 BUFFER/「缓冲中」,收尾被外层当播完顺延下一首);
|
||||
* 修后 seq 变化即重启换档,状态回 PLAYING、新档位下载、eff_level 回填。
|
||||
* P13 陈旧 hires.mp3(flac 门禁前静默降级产物,hires 键下存 mp3):song_local_file
|
||||
* 迁移重命名到 exhigh 键(song_<id>_exhigh.mp3)+ 本档视为 miss 走下载(§NEW 任务2)。
|
||||
*
|
||||
* 编译(宿主,ncm_app 目录下):
|
||||
* gcc -O2 -ffunction-sections -DJSMN_HEADER -I. -I../sdk -I../meow_launcherd \
|
||||
@@ -37,6 +39,7 @@
|
||||
#include <string.h>
|
||||
#include <stdlib.h>
|
||||
#include <stdarg.h>
|
||||
#include <sys/stat.h>
|
||||
#include <unistd.h>
|
||||
|
||||
ncm_app_state_t g_st;
|
||||
@@ -77,6 +80,7 @@ int appstate_queue_song(int idx, ncm_song_t *out) { return appstate_song_copy(id
|
||||
* g_cache_ok=1 → lookup 命中(给真实存在的 /tmp 文件,免下载);
|
||||
* 否则走下载:song_url(g_url_rc 可注入失败) → http_download(g_dl_delay_ms 模拟慢) → commit。 */
|
||||
static int g_cache_ok = 1;
|
||||
static char g_cache_real[600]; /* g_cache_ok==2:lookup 命中返回此真实路径(P13 陈旧 mp3 迁移用) */
|
||||
static int g_url_rc = NCM_OK;
|
||||
static int g_dl_delay_ms = 0;
|
||||
static int g_downloads; /* http_download 调用数(song_url 请求放大检测) */
|
||||
@@ -107,10 +111,30 @@ int account_level_cap(void)
|
||||
}
|
||||
}
|
||||
|
||||
/* 兜底保证 /tmp/pf_sim.mp3 存在(fake 缓存命中用;§NEW 任务2 的迁移 rename 会把它挪走到
|
||||
* exhigh 键——下次命中前补回,避免跨用例污染损坏共享命中文件)。 */
|
||||
static void pf_ensure_sim_mp3(void)
|
||||
{
|
||||
struct stat st;
|
||||
if (stat("/tmp/pf_sim.mp3", &st) == 0) return;
|
||||
FILE *f = fopen("/tmp/pf_sim.mp3", "wb");
|
||||
if (!f) return;
|
||||
for (int i = 0; i < 4096; i++) fputc(i & 0xFF, f);
|
||||
fclose(f);
|
||||
}
|
||||
|
||||
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)ext;
|
||||
(void)c; (void)id;
|
||||
if (g_cache_ok == 2) { /* P13 真实命中模式:仅 mp3 命中(stale hires 实体是 mp3) */
|
||||
if (ext && strcmp(ext, "mp3") == 0 && g_cache_real[0]) {
|
||||
snprintf(out, out_sz, "%s", g_cache_real);
|
||||
return NCM_OK;
|
||||
}
|
||||
return NCM_ERR_NOENT;
|
||||
}
|
||||
if (!g_cache_ok) return NCM_ERR_NOENT;
|
||||
pf_ensure_sim_mp3();
|
||||
snprintf(out, out_sz, "/tmp/pf_sim.mp3");
|
||||
return NCM_OK;
|
||||
}
|
||||
@@ -460,7 +484,10 @@ int main(void)
|
||||
CHECK(wait_err(NCM_ERR_VIP, 2000), "fee=1 + 非会员 -> err=NCM_ERR_VIP (rc=%d)",
|
||||
player_manager_error());
|
||||
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),
|
||||
* 会被 §NEW 任务2 当作静默降级产物迁移/重下,与「绕过门禁」的本意不符(P13 单独测迁移)。 */
|
||||
player_manager_set_quality(NCM_LEVEL_EXHIGH, 10);
|
||||
g_cache_ok = 1;
|
||||
player_manager_play(2);
|
||||
CHECK(wait_state(NCM_PC_PLAYING, 3000), "cached VIP song plays (缓存命中绕过门禁)");
|
||||
@@ -562,6 +589,34 @@ int main(void)
|
||||
g_eof = 1;
|
||||
wait_state(NCM_PC_STOPPED, 3000);
|
||||
|
||||
printf("== P13: 陈旧 hires.mp3(静默降级产物)-> exhigh 迁移 + 本档 miss 重下(§NEW 任务2)==\n");
|
||||
usleep(300 * 1000);
|
||||
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_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";
|
||||
/* 用户档 = Hi-Res(>EXHIGH),缓存实体是一首 mp3 = flac 门禁前把 exhigh 的 mp3 写进 hires
|
||||
* 键的「静默降级」产物。须被迁移到 exhigh 键(song_1000_exhigh.mp3),本档视为 miss 走下载。 */
|
||||
unlink("/tmp/song_1000_exhigh.mp3"); /* 保证 exhigh 目标不存在(走 rename 而非删旧) */
|
||||
FILE *hf = fopen("/tmp/pf_hires.mp3", "wb");
|
||||
if (hf) { fputs("DEGRADED", hf); fclose(hf); }
|
||||
snprintf(g_cache_real, sizeof g_cache_real, "/tmp/pf_hires.mp3");
|
||||
g_cache_ok = 2; /* lookup 命中返回 g_cache_real(仅 mp3 键) */
|
||||
player_manager_set_quality(NCM_LEVEL_HIRES, 10);
|
||||
player_manager_play(0);
|
||||
CHECK(wait_state(NCM_PC_PLAYING, 3000), "stale hires.mp3 redownloaded -> plays");
|
||||
CHECK(g_downloads >= 1, "stale mp3 triggered redownload (downloads=%d)", g_downloads);
|
||||
CHECK(strcmp(g_last_key, "1000_hires") == 0,
|
||||
"redownload cache key=%s (expect 1000_hires=用户档键)", g_last_key);
|
||||
CHECK(strcmp(g_last_ext, "mp3") == 0, "redownload ext=%s (请求档为 mp3,维持原身份)", g_last_ext);
|
||||
struct stat mm;
|
||||
CHECK(stat("/tmp/song_1000_exhigh.mp3", &mm) == 0,
|
||||
"exhigh migrated file exists (选「极高」档仍可用)");
|
||||
CHECK(stat("/tmp/pf_hires.mp3", &mm) != 0, "stale hires.mp3 source gone (已迁走)");
|
||||
g_cache_ok = 0; g_cache_real[0] = 0;
|
||||
g_eof = 1;
|
||||
wait_state(NCM_PC_STOPPED, 3000);
|
||||
|
||||
usleep(200 * 1000);
|
||||
player_manager_stop_thread();
|
||||
printf(g_fail ? "== PLAYER FLOW SIM: FAIL ==\n" : "== PLAYER FLOW SIM: ALL PASS ==\n");
|
||||
|
||||
Reference in new issue
Block a user