diff --git a/patch_v2/ncm_app/app/logic/appstate.c b/patch_v2/ncm_app/app/logic/appstate.c index 6518afe..18ee0de 100644 --- a/patch_v2/ncm_app/app/logic/appstate.c +++ b/patch_v2/ncm_app/app/logic/appstate.c @@ -87,6 +87,11 @@ int appstate_init(void) else ncm_ctx_default_cache_dir(g_st.data_dir, g_st.cache_dir, sizeof g_st.cache_dir); + /* §NEW 缓存分目录一次性迁移:旧的 song_*、cov* 混放根 → songs/ 与 covers/(init 已搬完,此处只披露)。 */ + if (g_st.ctx.cache.legacy_migrated > 0) + meow_log(MEOW_LOG_INFO, "ncm: cache legacy migration moved %d file(s) into songs/ and covers/", + g_st.ctx.cache.legacy_migrated); + /* 风控加固 §1:接口级 JSON 响应短 TTL 缓存的盘层挂到 /ncm_api_cache/。 * (内存层由 core 初始化默认启用;盘层跨进程/热更保留,启动清一次过期盘文件。) */ ncm_apicache_init(g_st.data_dir); diff --git a/patch_v2/ncm_app/app/logic/cover.c b/patch_v2/ncm_app/app/logic/cover.c index a1ad6d3..708a9ef 100644 --- a/patch_v2/ncm_app/app/logic/cover.c +++ b/patch_v2/ncm_app/app/logic/cover.c @@ -277,13 +277,16 @@ static meow_img *download_cover(const char *url, ncm_cache_writer_t *w, size_t * /* 封面原始文件路径写入(供 launcher 媒体卡片读;兼容保留)。调用方须已持有 g_cm。 * 从 cache 写者临时文件把已下载字节复制到 cover_current.jpg(早前版本在此内再 lock g_cm - * → 非递归锁自锁死——cover worker 永久卡死、封面永不出现)。 */ + * → 非递归锁自锁死——cover worker 永久卡死、封面永不出现)。 + * §NEW 缓存分目录:与歌曲分池,cover_current.jpg 落 covers/(封面条目 song_cov* 也在 covers/)。 */ static void save_cover_raw(const char *path, size_t len) { if (!path || len == 0) return; - if (g_st.cache_dir[0]) { + const char *covdir = g_st.ctx.cache.covers_dir[0] ? g_st.ctx.cache.covers_dir + : g_st.cache_dir; + if (covdir[0]) { char dst[NCM_CACHE_DIR_MAX + 32]; - snprintf(dst, sizeof dst, "%s/cover_current.jpg", g_st.cache_dir); + snprintf(dst, sizeof dst, "%s/cover_current.jpg", covdir); /* 文件复制(源=临时文件,目标=cover_current.jpg)。 */ FILE *in = fopen(path, "rb"); FILE *out = NULL; diff --git a/patch_v2/ncm_app/app/play/player_manager.c b/patch_v2/ncm_app/app/play/player_manager.c index 86d109b..238bb7a 100644 --- a/patch_v2/ncm_app/app/play/player_manager.c +++ b/patch_v2/ncm_app/app/play/player_manager.c @@ -486,21 +486,43 @@ static int song_local_file(const ncm_song_t *s, int idx, int seq, song_src_t *ss if (stat(np, &st) != 0) return NCM_ERR_NOENT; } } - if (stream_src_open_static(&ss->src, path, (uint64_t)st.st_size) != 0) - return NCM_ERR_IO; - ss->total = (int64_t)st.st_size; - *bytes = ss->total; - /* 生效档推断(缓存键=用户档,降级产物归用户档键,无法精确还原): - * 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; + /* §NEW 任务2:静默降级时代产物修复(用户实锤:hires 档键下存着 mp3,播放命中旧档)。 + * 用户档 > EXHIGH(无损/更高)且缓存实体是 .mp3 = flac 门禁前把 exhigh 的 mp3 写进了 + * 无损域键(当年降级产物归用户档键)。迁移重命名到 exhigh 键 song__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) + return NCM_ERR_IO; + ss->total = (int64_t)st.st_size; + *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); + g_play.eff_level = eff; + pthread_mutex_unlock(&g_play.mu); + return NCM_OK; + } } /* fee 门禁:VIP 才能听(1=VIP 歌曲 / 4=付费数字专辑)且非会员 → 不请求 url。 diff --git a/patch_v2/ncm_app/bench/cache_evict_sim.c b/patch_v2/ncm_app/bench/cache_evict_sim.c index c5aaec1..6fc7e26 100644 --- a/patch_v2/ncm_app/bench/cache_evict_sim.c +++ b/patch_v2/ncm_app/bench/cache_evict_sim.c @@ -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; diff --git a/patch_v2/ncm_app/bench/cover_chain_sim.c b/patch_v2/ncm_app/bench/cover_chain_sim.c index 761839e..d378caf 100644 --- a/patch_v2/ncm_app/bench/cover_chain_sim.c +++ b/patch_v2/ncm_app/bench/cover_chain_sim.c @@ -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]; diff --git a/patch_v2/ncm_app/bench/player_flow_sim.c b/patch_v2/ncm_app/bench/player_flow_sim.c index 2bc2c9d..64e7ebd 100644 --- a/patch_v2/ncm_app/bench/player_flow_sim.c +++ b/patch_v2/ncm_app/bench/player_flow_sim.c @@ -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__exhigh.mp3)+ 本档视为 miss 走下载(§NEW 任务2)。 * * 编译(宿主,ncm_app 目录下): * gcc -O2 -ffunction-sections -DJSMN_HEADER -I. -I../sdk -I../meow_launcherd \ @@ -37,6 +39,7 @@ #include #include #include +#include #include 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"); diff --git a/patch_v2/ncm_app/core/ncm_cache.c b/patch_v2/ncm_app/core/ncm_cache.c index 2a6b19f..d011700 100644 --- a/patch_v2/ncm_app/core/ncm_cache.c +++ b/patch_v2/ncm_app/core/ncm_cache.c @@ -38,6 +38,58 @@ static uint64_t gb_to_bytes(int gb) { return (uint64_t)gb * 1024ULL * 1024ULL * 1024ULL; } +/* §NEW 缓存分目录(用户定案):歌曲与封面分池。封面键 id 以 "cov" 开头(cover_cache_id 生成 + * cov),其余视为歌曲键(song__)。据此路由到 covers/ 或 songs/。 */ +static int id_is_cover(const char *id) { + return id && strncmp(id, "cov", 3) == 0; +} + +/* 条目目录:封面键 → covers/,歌曲键 → songs/;covers_dir/songs_dir 未初始化时兜底根 dir。 */ +static const char *cache_base_dir(const ncm_cache_t *c, const char *id) { + if (id_is_cover(id)) return (c->covers_dir[0]) ? c->covers_dir : c->dir; + return (c->songs_dir[0]) ? c->songs_dir : c->dir; +} + +/* 迁移分类:封面条目(song_cov / cov* 历史变体 / cover_current.jpg)与歌曲(song_*)。 + * 注意顺序:song_cov* 同时以 "song_" 开头,必须先判封面再判歌曲(避免封面被当歌曲搬错目录)。 */ +static int cache_entry_is_cover(const char *name) { + if (!name) return 0; + if (strncmp(name, "cov", 3) == 0) return 1; /* 无 song_ 前缀的历史变体 */ + if (strncmp(name, "song_cov", 8) == 0) return 1; /* 现行 song_cov 封面条目 */ + if (strcmp(name, "cover_current.jpg") == 0) return 1; + return 0; +} + +/* 一次性迁移(init 调):缓存根旧布局 song_* (mp3/封面/双点畸形) 与 cover_current.jpg 与 + * 根同层混放 → 歌曲挪 songs/、封面挪 covers/(meta/ 与 ncm_api_cache/ 原位不动)。 + * 已存在的目标文件保留(删旧源);rename 失败则保留源(下次再试)。幂等:迁移后根不再有 song_*。 */ +static void cache_legacy_migrate(ncm_cache_t *c) { + c->legacy_migrated = 0; + DIR *d = opendir(c->dir); + if (!d) return; + struct dirent *e; + while ((e = readdir(d))) { + const char *nm = e->d_name; + if (nm[0] == '.') continue; /* . / .. / 隐藏 */ + int is_cov = cache_entry_is_cover(nm); + int is_song = (!is_cov && strncmp(nm, "song_", 5) == 0); + if (!is_cov && !is_song) continue; /* meta/、ncm_api_cache/ 子目录及其它 */ + char src[NCM_CACHE_DIR_MAX + NAME_MAX + 8]; + char dst[NCM_CACHE_DIR_MAX + NAME_MAX + 8]; + snprintf(src, sizeof src, "%s/%s", c->dir, nm); + struct stat st; + if (stat(src, &st) != 0 || !S_ISREG(st.st_mode)) continue; /* 跳过 songs//covers/ 目录自身 */ + const char *base = is_cov ? c->covers_dir : c->songs_dir; + if (!base[0]) continue; + snprintf(dst, sizeof dst, "%s/%s", base, nm); + struct stat dst_st; + if (stat(dst, &dst_st) == 0) unlink(src); /* 目标已在(并发/重复):删旧源 */ + else if (rename(src, dst) == 0) c->legacy_migrated++; + /* rename 失败(跨设备/权限):保留源,下次 init 再试 */ + } + closedir(d); +} + int ncm_cache_init(ncm_cache_t *c, const char *dir) { if (!c) return NCM_ERR_ARG; memset(c, 0, sizeof *c); @@ -46,6 +98,11 @@ int ncm_cache_init(ncm_cache_t *c, const char *dir) { size_t n = strlen(c->dir); while (n > 1 && c->dir[n-1]=='/') c->dir[--n] = 0; ncm_store_mkdir_p(c->dir); + snprintf(c->songs_dir, sizeof c->songs_dir, "%s/songs", c->dir); + snprintf(c->covers_dir, sizeof c->covers_dir, "%s/covers", c->dir); + ncm_store_mkdir_p(c->songs_dir); /* 已存在不报错(mkdir -p 语义) */ + ncm_store_mkdir_p(c->covers_dir); + cache_legacy_migrate(c); c->limit_gb = DEFAULT_LIMIT_GB; c->limit_bytes = (uint64_t)gb_to_bytes(DEFAULT_LIMIT_GB); return NCM_OK; @@ -70,14 +127,22 @@ void ncm_cache_load_setting(ncm_cache_t *c, const char *data_dir) { const char *ncm_cache_dir(const ncm_cache_t *c) { return c ? c->dir : ""; } +const char *ncm_cache_songs_dir(const ncm_cache_t *c) { + return c ? c->songs_dir : ""; +} +const char *ncm_cache_covers_dir(const ncm_cache_t *c) { + return c ? c->covers_dir : ""; +} -/* 路径:song_.。§19 正名:ext 剥离至多一个前导点——旧版调用方传 ".mp3" - * 会生成双点文件名 `song_..mp3`(历史存量),新版一律单点。 */ +/* 路径:song_.,落在 songs/(歌曲)或 covers/(封面键 cov*)。§19 正名: + * ext 剥离至多一个前导点——旧版调用方传 ".mp3" 会生成双点文件名 `song_..mp3` + * (历史存量),新版一律单点。 */ int ncm_cache_path(const ncm_cache_t *c, const char *id, const char *ext, char *out, size_t out_sz) { if (!c || !id || !out) return NCM_ERR_ARG; if (ext && ext[0] == '.') ext++; - int n = snprintf(out, out_sz, "%s/song_%s.%s", c->dir, id, ext ? ext : "bin"); + const char *base = cache_base_dir(c, id); + int n = snprintf(out, out_sz, "%s/song_%s.%s", base, id, ext ? ext : "bin"); return (n >= 0 && (size_t)n < out_sz) ? NCM_OK : NCM_ERR_FULL; } @@ -93,9 +158,16 @@ int ncm_cache_lookup(const ncm_cache_t *c, const char *id, const char *ext, const char *e = ext; if (e && e[0] == '.') e++; const char *ee = e ? e : "bin"; - int n = snprintf(path, sizeof path, "%s/song_%s..%s", c->dir, id, ee); + const char *base = cache_base_dir(c, id); + int n = snprintf(path, sizeof path, "%s/song_%s..%s", base, id, ee); if (n < 0 || (size_t)n >= sizeof path) return NCM_ERR_FULL; - if (stat(path, &st) != 0 || !S_ISREG(st.st_mode)) return NCM_ERR_NOENT; + if (stat(path, &st) != 0 || !S_ISREG(st.st_mode)) { + /* §NEW 分目录兜底:迁移前旧布局双点名可能仍躺在缓存根(init 迁移后应已在子目录; + * 这里双点兼容:子目录先查、根目录再查,保证任何存量双点都被迁移修正)。 */ + n = snprintf(path, sizeof path, "%s/song_%s..%s", c->dir, id, ee); + if (n < 0 || (size_t)n >= sizeof path) return NCM_ERR_FULL; + if (stat(path, &st) != 0 || !S_ISREG(st.st_mode)) return NCM_ERR_NOENT; + } char fixed[NCM_CACHE_DIR_MAX + 32]; if (ncm_cache_path(c, id, ee, fixed, sizeof fixed) == NCM_OK) { struct stat fst; @@ -181,7 +253,8 @@ static int cmp_mtime_asc(const void *a, const void *b) { int ncm_cache_evict(const ncm_cache_t *c) { if (!c) return NCM_ERR_ARG; - DIR *d = opendir(c->dir); + const char *store = (c->songs_dir[0]) ? c->songs_dir : c->dir; /* LRU 池 = songs/ */ + DIR *d = opendir(store); if (!d) return NCM_ERR_IO; entry_t *ents = (entry_t *)malloc(sizeof(entry_t) * MAX_ENTRIES); if (!ents) { closedir(d); return NCM_ERR_OOM; } @@ -195,7 +268,7 @@ int ncm_cache_evict(const ncm_cache_t *c) { /* §26 顺手续删:超过 7 天的孤儿 .part/.tmp(用户不再点该曲、不续传)惰性删除。 * 在播/刚中断的 .tmp/.part mtime 新鲜(写端持续更新),不会被误删。 */ char pb[NCM_CACHE_DIR_MAX + NAME_MAX + 8]; - snprintf(pb, sizeof pb, "%s/%s", c->dir, e->d_name); + snprintf(pb, sizeof pb, "%s/%s", store, e->d_name); struct stat sb; if (stat(pb, &sb) == 0 && S_ISREG(sb.st_mode) && (int64_t)sb.st_mtim.tv_sec < orphan_cutoff) @@ -203,7 +276,7 @@ int ncm_cache_evict(const ncm_cache_t *c) { continue; } char p[NCM_CACHE_DIR_MAX + NAME_MAX + 8]; - snprintf(p, sizeof p, "%s/%s", c->dir, e->d_name); + snprintf(p, sizeof p, "%s/%s", store, e->d_name); struct stat st; if (stat(p, &st) != 0 || !S_ISREG(st.st_mode)) continue; ncm_strlcpy(ents[n].name, e->d_name, sizeof ents[n].name); @@ -220,7 +293,7 @@ int ncm_cache_evict(const ncm_cache_t *c) { for (int i = 0; i < n; i++) { if (total <= c->limit_bytes) break; char p[NCM_CACHE_DIR_MAX + NAME_MAX + 8]; - snprintf(p, sizeof p, "%s/%s", c->dir, ents[i].name); + snprintf(p, sizeof p, "%s/%s", store, ents[i].name); if (unlink(p) == 0 && ents[i].size < total) total -= ents[i].size; } } @@ -333,7 +406,8 @@ int ncm_cache_abort(ncm_cache_writer_t *w) { uint64_t ncm_cache_total_bytes(const ncm_cache_t *c) { if (!c) return 0; - DIR *d = opendir(c->dir); + const char *store = (c->songs_dir[0]) ? c->songs_dir : c->dir; + DIR *d = opendir(store); if (!d) return 0; uint64_t total = 0; int64_t orphan_cutoff = (int64_t)time(NULL) - (int64_t)NCM_ORPHAN_PART_DAYS * 86400; @@ -343,7 +417,7 @@ uint64_t ncm_cache_total_bytes(const ncm_cache_t *c) { if (is_partial_name(e->d_name)) { /* 阶段3:半成品不计入池占用 */ /* §26 顺手续删:超过 7 天的孤儿 .part/.tmp 惰性删除。 */ char pb[NCM_CACHE_DIR_MAX + NAME_MAX + 8]; - snprintf(pb, sizeof pb, "%s/%s", c->dir, e->d_name); + snprintf(pb, sizeof pb, "%s/%s", store, e->d_name); struct stat sb; if (stat(pb, &sb) == 0 && S_ISREG(sb.st_mode) && (int64_t)sb.st_mtim.tv_sec < orphan_cutoff) @@ -351,7 +425,7 @@ uint64_t ncm_cache_total_bytes(const ncm_cache_t *c) { continue; } char p[NCM_CACHE_DIR_MAX + NAME_MAX + 8]; - snprintf(p, sizeof p, "%s/%s", c->dir, e->d_name); + snprintf(p, sizeof p, "%s/%s", store, e->d_name); struct stat st; if (stat(p, &st) == 0 && S_ISREG(st.st_mode)) total += (uint64_t)st.st_size; } @@ -359,18 +433,26 @@ uint64_t ncm_cache_total_bytes(const ncm_cache_t *c) { return total; } -/* 清空缓存池:删除 song_* 全部文件(mp3 与封面同池,含残留 .tmp)。返回 0=成功。 */ +/* 清空缓存池:删除 songs/ 与 covers/ 的 song_* 全部文件(音频 + 封面条目 + 残留 .tmp/.part)。 + * cover_current.jpg 无 song_ 前缀,保留(同上版本不删它;meta/ 与 ncm_api_cache/ 不动)。返回 0=成功。 */ int ncm_cache_clear(const ncm_cache_t *c) { if (!c) return NCM_ERR_ARG; - DIR *d = opendir(c->dir); - if (!d) return NCM_ERR_IO; - struct dirent *e; - while ((e = readdir(d))) { - if (strncmp(e->d_name, "song_", 5) != 0) continue; - char p[NCM_CACHE_DIR_MAX + NAME_MAX + 8]; - snprintf(p, sizeof p, "%s/%s", c->dir, e->d_name); - unlink(p); + const char *dirs[2]; + int nd = 0; + if (c->songs_dir[0]) dirs[nd++] = c->songs_dir; + if (c->covers_dir[0]) dirs[nd++] = c->covers_dir; + if (!nd) dirs[nd++] = c->dir; /* 未初始化:退回根(旧布局) */ + for (int i = 0; i < nd; i++) { + DIR *d = opendir(dirs[i]); + if (!d) continue; + struct dirent *e; + while ((e = readdir(d))) { + if (strncmp(e->d_name, "song_", 5) != 0) continue; + char p[NCM_CACHE_DIR_MAX + NAME_MAX + 8]; + snprintf(p, sizeof p, "%s/%s", dirs[i], e->d_name); + unlink(p); + } + closedir(d); } - closedir(d); return NCM_OK; } diff --git a/patch_v2/ncm_app/core/ncm_cache.h b/patch_v2/ncm_app/core/ncm_cache.h index 88de9b9..7bdde7d 100644 --- a/patch_v2/ncm_app/core/ncm_cache.h +++ b/patch_v2/ncm_app/core/ncm_cache.h @@ -18,12 +18,17 @@ extern "C" { #define NCM_CACHE_DIR_MAX 512 typedef struct ncm_cache { - char dir[NCM_CACHE_DIR_MAX]; + char dir[NCM_CACHE_DIR_MAX]; /* 缓存根(meta/ 与 ncm_api_cache/ 保留于此) */ + char songs_dir[NCM_CACHE_DIR_MAX]; /* /songs:音频 song_*(含 .tmp/.part),LRU 池 */ + char covers_dir[NCM_CACHE_DIR_MAX];/* /covers:封面 song_cov* + cover_current.jpg */ uint64_t limit_bytes; /* 上限字节 */ int limit_gb; + int legacy_migrated; /* init 一次性迁移搬移的文件数(供调用方打日志) */ } ncm_cache_t; -/* 初始化:设置目录 + 默认 10GB 上限,mkdir -p。 */ +/* 初始化:设置目录 + 默认 10GB 上限,mkdir -p 根 + songs/ + covers/ 子目录; + * 并做一次性迁移(把缓存根的存量 song_* 挪进 songs/、cover_current.jpg/song_cov* 挪进 covers/, + * 已迁移的在 g.legacy_migrated 计数,供调用方打日志)。 */ int ncm_cache_init(ncm_cache_t *c, const char *dir); /* 设置/获取上限(1~100GB,越界钳制)。 */ @@ -33,8 +38,11 @@ int ncm_cache_limit_gb(const ncm_cache_t *c); /* 从数据目录读 ncm.cache_limit_gb 配置(对齐 SDK meow_config_int)并应用。 */ void ncm_cache_load_setting(ncm_cache_t *c, const char *data_dir); -/* 缓存目录 / 单文件路径 */ +/* 缓存目录 / 单文件路径。ncm_cache_path 按 id 前缀路由:id 以 "cov" 开头(封面键)→ + * covers/,否则(歌曲键)→ songs/(§NEW 缓存分目录:song_* 与封面分离)。 */ const char *ncm_cache_dir(const ncm_cache_t *c); +const char *ncm_cache_songs_dir(const ncm_cache_t *c); +const char *ncm_cache_covers_dir(const ncm_cache_t *c); int ncm_cache_path(const ncm_cache_t *c, const char *id, const char *ext, char *out, size_t out_sz); @@ -71,13 +79,13 @@ int ncm_cache_open_resume(const ncm_cache_t *c, const char *id, const char *ext * nwritten==0 → unlink(空残体无价值)。音频路径用;封面/测试仍用 ncm_cache_abort。 */ int ncm_cache_abort_part(ncm_cache_writer_t *w); -/* 当前缓存总字节(扫描目录;跳过 .tmp/.part 半成品) */ +/* 当前缓存总字节(扫描 songs/;跳过 .tmp/.part 半成品)。封面 covers/ 不在 LRU 池内。 */ uint64_t ncm_cache_total_bytes(const ncm_cache_t *c); -/* 立即执行 LRU 淘汰(超上限则删最旧,直到 <= 上限;跳过 .tmp/.part 半成品)。 */ +/* 立即执行 LRU 淘汰(扫描 songs/;超上限则删最旧,直到 <= 上限;跳过 .tmp/.part 半成品)。 */ int ncm_cache_evict(const ncm_cache_t *c); -/* 清空缓存池:删除 song_* 全部文件(mp3 与封面同池,含残留 .tmp)。 */ +/* 清空缓存池:删除 songs/ 与 covers/ 的 song_* 全部文件(音频 + 封面条目 + 残留 .tmp/.part)。 */ int ncm_cache_clear(const ncm_cache_t *c); #ifdef __cplusplus diff --git a/patch_v2/ncm_app/ncm b/patch_v2/ncm_app/ncm index e4a1297..ccce84e 100755 Binary files a/patch_v2/ncm_app/ncm and b/patch_v2/ncm_app/ncm differ diff --git a/patch_v2/ncm_app/test/test_ncm_core.c b/patch_v2/ncm_app/test/test_ncm_core.c index 6153b8b..d2f8961 100644 --- a/patch_v2/ncm_app/test/test_ncm_core.c +++ b/patch_v2/ncm_app/test/test_ncm_core.c @@ -108,13 +108,13 @@ static void test_cache(void) { printf("== cache ==\n"); ncm_cache_t c; char dir[] = "/tmp/ncm_cache_test"; - ncm_cache_init(&c, dir); - /* 清空目录 */ + /* 清空目录(须在 init 前:init 会建 songs/covers 子目录,rm -rf 会一并删掉) */ { char cmd[128]; snprintf(cmd, sizeof cmd, "rm -rf %s; mkdir -p %s", dir, dir); if (system(cmd) != 0) { /* best-effort */ } } + ncm_cache_init(&c, dir); /* 建根 + songs/ + covers/ + 一次性迁移 */ /* miss */ char path[NCM_CACHE_DIR_MAX + 32]; int mrc = ncm_cache_lookup(&c, "1", "mp3", path, sizeof path); diff --git a/patch_v2/ncm_app/test/test_ncm_core_host b/patch_v2/ncm_app/test/test_ncm_core_host index d135b44..8a54deb 100755 Binary files a/patch_v2/ncm_app/test/test_ncm_core_host and b/patch_v2/ncm_app/test/test_ncm_core_host differ