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:
AskaEth committed 2026-09-02 13:51:36 +08:00
1 parent 986e3814bc
commit de18cef02c
11 files changed
+299 -64

No files matched your search

+5
View File
@@ -87,6 +87,11 @@ int appstate_init(void)
else else
ncm_ctx_default_cache_dir(g_st.data_dir, g_st.cache_dir, sizeof g_st.cache_dir); 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 缓存的盘层挂到 <data_dir>/ncm_api_cache/。 /* 风控加固 §1:接口级 JSON 响应短 TTL 缓存的盘层挂到 <data_dir>/ncm_api_cache/。
* (内存层由 core 初始化默认启用;盘层跨进程/热更保留,启动清一次过期盘文件。) */ * (内存层由 core 初始化默认启用;盘层跨进程/热更保留,启动清一次过期盘文件。) */
ncm_apicache_init(g_st.data_dir); ncm_apicache_init(g_st.data_dir);
+6 -3
View File
@@ -277,13 +277,16 @@ static meow_img *download_cover(const char *url, ncm_cache_writer_t *w, size_t *
/* 封面原始文件路径写入(供 launcher 媒体卡片读;兼容保留)。调用方须已持有 g_cm。 /* 封面原始文件路径写入(供 launcher 媒体卡片读;兼容保留)。调用方须已持有 g_cm。
* 从 cache 写者临时文件把已下载字节复制到 cover_current.jpg(早前版本在此内再 lock 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) static void save_cover_raw(const char *path, size_t len)
{ {
if (!path || len == 0) return; 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]; 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)。 */ /* 文件复制(源=临时文件,目标=cover_current.jpg)。 */
FILE *in = fopen(path, "rb"); FILE *in = fopen(path, "rb");
FILE *out = NULL; FILE *out = NULL;
+37 -15
View File
@@ -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 (stat(np, &st) != 0) return NCM_ERR_NOENT;
} }
} }
if (stream_src_open_static(&ss->src, path, (uint64_t)st.st_size) != 0) /* §NEW 任务2:静默降级时代产物修复(用户实锤:hires 档键下存着 mp3,播放命中旧档)。
return NCM_ERR_IO; * 用户档 > EXHIGH(无损/更高)且缓存实体是 .mp3 = flac 门禁前把 exhigh 的 mp3 写进了
ss->total = (int64_t)st.st_size; * 无损域键(当年降级产物归用户档键)。迁移重命名到 exhigh 键 song_<id>_exhigh.mp3
*bytes = ss->total; * (选「极高」档时仍可用;目标已存在则删旧),然后本档视为未命中走下载——
/* 生效档推断(缓存键=用户档,降级产物归用户档键,无法精确还原): * 311 的 flac 门禁会拿到真无损或正确降级。放在 hit==2 迁移块之后、静态流打开之前。 */
* mp3 实体 + 用户档 > 极高 → 必为降级产物,标 EXHIGH;其余按用户档显示。 const char *mdot = strrchr(path, '.');
* (flac 实体视为与用户档一致;hires→lossless 档内降级会高估显示,已知取舍。) */ if (g_level > NCM_LEVEL_EXHIGH && mdot && ncm_streq(mdot, ".mp3")) {
const char *dot = strrchr(path, '.'); char kh[64], ph[512];
int eff = g_level; audio_cache_key(kh, sizeof kh, id, NCM_LEVEL_EXHIGH);
if (dot && ncm_streq(dot, "mp3") && g_level > NCM_LEVEL_EXHIGH) if (ncm_cache_path(&g_st.ctx.cache, kh, "mp3", ph, sizeof ph) == NCM_OK) {
eff = NCM_LEVEL_EXHIGH; struct stat hst;
pthread_mutex_lock(&g_play.mu); if (stat(ph, &hst) == 0) unlink(path); /* exhigh 目标已在:删旧(降级产物) */
g_play.eff_level = eff; else rename(path, ph);
pthread_mutex_unlock(&g_play.mu); meow_log(MEOW_LOG_INFO,
return NCM_OK; "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。 /* fee 门禁:VIP 才能听(1=VIP 歌曲 / 4=付费数字专辑)且非会员 → 不请求 url。
+69 -12
View File
@@ -8,6 +8,8 @@
* ③ ncm_cache_lookup 刷新 mtime(LRU:被访问的最近优先存活); * ③ ncm_cache_lookup 刷新 mtime(LRU:被访问的最近优先存活);
* ④ ncm_cache_set_limit_gb 越界钳制(1~100GB); * ④ ncm_cache_set_limit_gb 越界钳制(1~100GB);
* ⑤ ncm_cache_clear 删除 song_* 全部(mp3/封面/残留 .tmp)。 * ⑤ ncm_cache_clear 删除 song_* 全部(mp3/封面/残留 .tmp)。
* §NEW 缓存分目录:song_* 落 songs/、封面(song_cov*、cover_current.jpg)落 covers/;
* LRU/7 天回收只扫 songs/;⑩ 覆盖 ncm_cache_init 一次性把根混放文件迁入两子目录。
* 文件用 ftruncate 生成稀疏文件(逻辑大小可控,磁盘零消耗)。 * 文件用 ftruncate 生成稀疏文件(逻辑大小可控,磁盘零消耗)。
* *
* 编译(宿主,链接 core/ncm_cache.c + 本文件): * 编译(宿主,链接 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 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) static void cleanup(void)
{ {
/* 递归删 g_dir 下 song_*(恢复干净池) */ char sub[640];
DIR *d = opendir(g_dir); sweep_dir(g_dir);
if (d) { snprintf(sub, sizeof sub, "%s/songs", g_dir); sweep_dir(sub);
struct dirent *e; snprintf(sub, sizeof sub, "%s/covers", g_dir); sweep_dir(sub);
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);
}
} }
int main(void) int main(void)
@@ -330,6 +339,54 @@ int main(void)
CHECK(stat(lp, &st) == 0, "fresh .part kept (inflight/resumable)"); 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(); cleanup();
printf("%s\n", g_fail ? "RESULT: FAIL" : "RESULT: PASS"); printf("%s\n", g_fail ? "RESULT: FAIL" : "RESULT: PASS");
return g_fail ? 1 : 0; return g_fail ? 1 : 0;
+5 -2
View File
@@ -269,11 +269,14 @@ static unsigned long cur_cover_hash(void)
return h; return h;
} }
/* 读 cover_current.jpg 并哈希(验证 launcher 媒体卡片读的文件同步更新)。 */ /* 读 cover_current.jpg 并哈希(验证 launcher 媒体卡片读的文件同步更新)。
* §NEW 缓存分目录:cover_current.jpg 落 covers/(与歌曲 song_* 分池)。 */
static unsigned long curjpg_hash(void) static unsigned long curjpg_hash(void)
{ {
char path[600]; 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"); FILE *f = fopen(path, "rb");
if (!f) return 0; if (!f) return 0;
unsigned char b[4096]; unsigned char b[4096];
+57 -2
View File
@@ -14,6 +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
* 迁移重命名到 exhigh 键(song_<id>_exhigh.mp3)+ 本档视为 miss 走下载(§NEW 任务2)。
* *
* 编译(宿主,ncm_app 目录下): * 编译(宿主,ncm_app 目录下):
* gcc -O2 -ffunction-sections -DJSMN_HEADER -I. -I../sdk -I../meow_launcherd \ * gcc -O2 -ffunction-sections -DJSMN_HEADER -I. -I../sdk -I../meow_launcherd \
@@ -37,6 +39,7 @@
#include <string.h> #include <string.h>
#include <stdlib.h> #include <stdlib.h>
#include <stdarg.h> #include <stdarg.h>
#include <sys/stat.h>
#include <unistd.h> #include <unistd.h>
ncm_app_state_t g_st; 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 文件,免下载); * 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 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 请求放大检测) */
@@ -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) 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; if (!g_cache_ok) return NCM_ERR_NOENT;
pf_ensure_sim_mp3();
snprintf(out, out_sz, "/tmp/pf_sim.mp3"); snprintf(out, out_sz, "/tmp/pf_sim.mp3");
return NCM_OK; 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)", CHECK(wait_err(NCM_ERR_VIP, 2000), "fee=1 + 非会员 -> err=NCM_ERR_VIP (rc=%d)",
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),
* 会被 §NEW 任务2 当作静默降级产物迁移/重下,与「绕过门禁」的本意不符(P13 单独测迁移)。 */
player_manager_set_quality(NCM_LEVEL_EXHIGH, 10);
g_cache_ok = 1; g_cache_ok = 1;
player_manager_play(2); player_manager_play(2);
CHECK(wait_state(NCM_PC_PLAYING, 3000), "cached VIP song plays (缓存命中绕过门禁)"); CHECK(wait_state(NCM_PC_PLAYING, 3000), "cached VIP song plays (缓存命中绕过门禁)");
@@ -562,6 +589,34 @@ 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");
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); usleep(200 * 1000);
player_manager_stop_thread(); player_manager_stop_thread();
printf(g_fail ? "== PLAYER FLOW SIM: FAIL ==\n" : "== PLAYER FLOW SIM: ALL PASS ==\n"); printf(g_fail ? "== PLAYER FLOW SIM: FAIL ==\n" : "== PLAYER FLOW SIM: ALL PASS ==\n");
+104 -22
View File
@@ -38,6 +38,58 @@ static uint64_t gb_to_bytes(int gb) {
return (uint64_t)gb * 1024ULL * 1024ULL * 1024ULL; return (uint64_t)gb * 1024ULL * 1024ULL * 1024ULL;
} }
/* §NEW 缓存分目录(用户定案):歌曲与封面分池。封面键 id 以 "cov" 开头(cover_cache_id 生成
* cov<hex>),其余视为歌曲键(song_<id>_<level>)。据此路由到 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<hex> / 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<hex> 封面条目 */
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) { int ncm_cache_init(ncm_cache_t *c, const char *dir) {
if (!c) return NCM_ERR_ARG; if (!c) return NCM_ERR_ARG;
memset(c, 0, sizeof *c); 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); size_t n = strlen(c->dir);
while (n > 1 && c->dir[n-1]=='/') c->dir[--n] = 0; while (n > 1 && c->dir[n-1]=='/') c->dir[--n] = 0;
ncm_store_mkdir_p(c->dir); 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_gb = DEFAULT_LIMIT_GB;
c->limit_bytes = (uint64_t)gb_to_bytes(DEFAULT_LIMIT_GB); c->limit_bytes = (uint64_t)gb_to_bytes(DEFAULT_LIMIT_GB);
return NCM_OK; 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) { const char *ncm_cache_dir(const ncm_cache_t *c) {
return c ? c->dir : ""; 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_<id>.<ext>。§19 正名:ext 剥离至多一个前导点——旧版调用方传 ".mp3" /* 路径:song_<id>.<ext>,落在 songs/(歌曲)或 covers/(封面键 cov*)。§19 正名:
* 会生成双点文件名 `song_<id>..mp3`(历史存量),新版一律单点。 */ * ext 剥离至多一个前导点——旧版调用方传 ".mp3" 会生成双点文件名 `song_<id>..mp3`
* (历史存量),新版一律单点。 */
int ncm_cache_path(const ncm_cache_t *c, const char *id, const char *ext, int ncm_cache_path(const ncm_cache_t *c, const char *id, const char *ext,
char *out, size_t out_sz) { char *out, size_t out_sz) {
if (!c || !id || !out) return NCM_ERR_ARG; if (!c || !id || !out) return NCM_ERR_ARG;
if (ext && ext[0] == '.') ext++; 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; 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; const char *e = ext;
if (e && e[0] == '.') e++; if (e && e[0] == '.') e++;
const char *ee = e ? e : "bin"; 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 (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]; char fixed[NCM_CACHE_DIR_MAX + 32];
if (ncm_cache_path(c, id, ee, fixed, sizeof fixed) == NCM_OK) { if (ncm_cache_path(c, id, ee, fixed, sizeof fixed) == NCM_OK) {
struct stat fst; 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) { int ncm_cache_evict(const ncm_cache_t *c) {
if (!c) return NCM_ERR_ARG; 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; if (!d) return NCM_ERR_IO;
entry_t *ents = (entry_t *)malloc(sizeof(entry_t) * MAX_ENTRIES); entry_t *ents = (entry_t *)malloc(sizeof(entry_t) * MAX_ENTRIES);
if (!ents) { closedir(d); return NCM_ERR_OOM; } 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(用户不再点该曲、不续传)惰性删除。 /* §26 顺手续删:超过 7 天的孤儿 .part/.tmp(用户不再点该曲、不续传)惰性删除。
* 在播/刚中断的 .tmp/.part mtime 新鲜(写端持续更新),不会被误删。 */ * 在播/刚中断的 .tmp/.part mtime 新鲜(写端持续更新),不会被误删。 */
char pb[NCM_CACHE_DIR_MAX + NAME_MAX + 8]; 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; struct stat sb;
if (stat(pb, &sb) == 0 && S_ISREG(sb.st_mode) && if (stat(pb, &sb) == 0 && S_ISREG(sb.st_mode) &&
(int64_t)sb.st_mtim.tv_sec < orphan_cutoff) (int64_t)sb.st_mtim.tv_sec < orphan_cutoff)
@@ -203,7 +276,7 @@ int ncm_cache_evict(const ncm_cache_t *c) {
continue; continue;
} }
char p[NCM_CACHE_DIR_MAX + NAME_MAX + 8]; 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; struct stat st;
if (stat(p, &st) != 0 || !S_ISREG(st.st_mode)) continue; if (stat(p, &st) != 0 || !S_ISREG(st.st_mode)) continue;
ncm_strlcpy(ents[n].name, e->d_name, sizeof ents[n].name); 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++) { for (int i = 0; i < n; i++) {
if (total <= c->limit_bytes) break; if (total <= c->limit_bytes) break;
char p[NCM_CACHE_DIR_MAX + NAME_MAX + 8]; 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; 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) { uint64_t ncm_cache_total_bytes(const ncm_cache_t *c) {
if (!c) return 0; 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; if (!d) return 0;
uint64_t total = 0; uint64_t total = 0;
int64_t orphan_cutoff = (int64_t)time(NULL) - (int64_t)NCM_ORPHAN_PART_DAYS * 86400; 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:半成品不计入池占用 */ if (is_partial_name(e->d_name)) { /* 阶段3:半成品不计入池占用 */
/* §26 顺手续删:超过 7 天的孤儿 .part/.tmp 惰性删除。 */ /* §26 顺手续删:超过 7 天的孤儿 .part/.tmp 惰性删除。 */
char pb[NCM_CACHE_DIR_MAX + NAME_MAX + 8]; 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; struct stat sb;
if (stat(pb, &sb) == 0 && S_ISREG(sb.st_mode) && if (stat(pb, &sb) == 0 && S_ISREG(sb.st_mode) &&
(int64_t)sb.st_mtim.tv_sec < orphan_cutoff) (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; continue;
} }
char p[NCM_CACHE_DIR_MAX + NAME_MAX + 8]; 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; struct stat st;
if (stat(p, &st) == 0 && S_ISREG(st.st_mode)) total += (uint64_t)st.st_size; 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; 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) { int ncm_cache_clear(const ncm_cache_t *c) {
if (!c) return NCM_ERR_ARG; if (!c) return NCM_ERR_ARG;
DIR *d = opendir(c->dir); const char *dirs[2];
if (!d) return NCM_ERR_IO; int nd = 0;
struct dirent *e; if (c->songs_dir[0]) dirs[nd++] = c->songs_dir;
while ((e = readdir(d))) { if (c->covers_dir[0]) dirs[nd++] = c->covers_dir;
if (strncmp(e->d_name, "song_", 5) != 0) continue; if (!nd) dirs[nd++] = c->dir; /* 未初始化:退回根(旧布局) */
char p[NCM_CACHE_DIR_MAX + NAME_MAX + 8]; for (int i = 0; i < nd; i++) {
snprintf(p, sizeof p, "%s/%s", c->dir, e->d_name); DIR *d = opendir(dirs[i]);
unlink(p); 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; return NCM_OK;
} }
+14 -6
View File
@@ -18,12 +18,17 @@ extern "C" {
#define NCM_CACHE_DIR_MAX 512 #define NCM_CACHE_DIR_MAX 512
typedef struct ncm_cache { 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]; /* <dir>/songs:音频 song_*(含 .tmp/.part),LRU 池 */
char covers_dir[NCM_CACHE_DIR_MAX];/* <dir>/covers:封面 song_cov* + cover_current.jpg */
uint64_t limit_bytes; /* 上限字节 */ uint64_t limit_bytes; /* 上限字节 */
int limit_gb; int limit_gb;
int legacy_migrated; /* init 一次性迁移搬移的文件数(供调用方打日志) */
} ncm_cache_t; } 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); int ncm_cache_init(ncm_cache_t *c, const char *dir);
/* 设置/获取上限(1~100GB,越界钳制)。 */ /* 设置/获取上限(1~100GB,越界钳制)。 */
@@ -33,8 +38,11 @@ int ncm_cache_limit_gb(const ncm_cache_t *c);
/* 从数据目录读 ncm.cache_limit_gb 配置(对齐 SDK meow_config_int)并应用。 */ /* 从数据目录读 ncm.cache_limit_gb 配置(对齐 SDK meow_config_int)并应用。 */
void ncm_cache_load_setting(ncm_cache_t *c, const char *data_dir); 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_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, int ncm_cache_path(const ncm_cache_t *c, const char *id, const char *ext,
char *out, size_t out_sz); 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。 */ * nwritten==0 → unlink(空残体无价值)。音频路径用;封面/测试仍用 ncm_cache_abort。 */
int ncm_cache_abort_part(ncm_cache_writer_t *w); 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); 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); 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); int ncm_cache_clear(const ncm_cache_t *c);
#ifdef __cplusplus #ifdef __cplusplus
Binary file not shown.
+2 -2
View File
@@ -108,13 +108,13 @@ static void test_cache(void) {
printf("== cache ==\n"); printf("== cache ==\n");
ncm_cache_t c; ncm_cache_t c;
char dir[] = "/tmp/ncm_cache_test"; char dir[] = "/tmp/ncm_cache_test";
ncm_cache_init(&c, dir); /* 清空目录(须在 init 前:init 会建 songs/covers 子目录,rm -rf 会一并删掉) */
/* 清空目录 */
{ {
char cmd[128]; char cmd[128];
snprintf(cmd, sizeof cmd, "rm -rf %s; mkdir -p %s", dir, dir); snprintf(cmd, sizeof cmd, "rm -rf %s; mkdir -p %s", dir, dir);
if (system(cmd) != 0) { /* best-effort */ } if (system(cmd) != 0) { /* best-effort */ }
} }
ncm_cache_init(&c, dir); /* 建根 + songs/ + covers/ + 一次性迁移 */
/* miss */ /* miss */
char path[NCM_CACHE_DIR_MAX + 32]; char path[NCM_CACHE_DIR_MAX + 32];
int mrc = ncm_cache_lookup(&c, "1", "mp3", path, sizeof path); int mrc = ncm_cache_lookup(&c, "1", "mp3", path, sizeof path);
Binary file not shown.