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

+104 -22
View File
@@ -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<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) {
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_<id>.<ext>。§19 正名:ext 剥离至多一个前导点——旧版调用方传 ".mp3"
* 会生成双点文件名 `song_<id>..mp3`(历史存量),新版一律单点。 */
/* 路径:song_<id>.<ext>,落在 songs/(歌曲)或 covers/(封面键 cov*)。§19 正名:
* ext 剥离至多一个前导点——旧版调用方传 ".mp3" 会生成双点文件名 `song_<id>..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;
}
+14 -6
View File
@@ -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]; /* <dir>/songs:音频 song_*(含 .tmp/.part),LRU 池 */
char covers_dir[NCM_CACHE_DIR_MAX];/* <dir>/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