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
@@ -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);
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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。
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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];
|
||||||
|
|||||||
@@ -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");
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
@@ -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.
@@ -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.
Reference in new issue
Block a user