feat(ncm): 缓存实际档判定/文件名解析 + 查找档位下探(M6 core)

- ncm_cache_actual_level:请求档 + 容器/码率/位深 + 响应生效档 → 实际档(只降不升),
  作为写盘名/播放显示/已缓存列表的唯一事实源
- ncm_cache_level_from_path:文件名档位段解析(song_<id>_<档位>,含 sky_c51)
- ncm_cache_lookup:请求档精确 miss 后按实际档逐档下探(hires 命中已存 exhigh/lossless),
  只降不升,低档请求不命中高档实体
- ncm_cache_scan:actual_level 改用统一判定;同曲同实际档的旧名+新名去重展示
This commit is contained in:
AskaEth committed 2026-11-04 21:11:00 +08:00
1 parent 132622cbc1
commit 30ffe469c8
2 files changed
+167 -35

No files matched your search

+139 -30
View File
@@ -148,10 +148,12 @@ int ncm_cache_path(const ncm_cache_t *c, const char *id, const char *ext,
return (n >= 0 && (size_t)n < out_sz) ? NCM_OK : NCM_ERR_FULL;
}
int ncm_cache_lookup(const ncm_cache_t *c, const char *id, const char *ext,
/* 单键精确查找(含 §19 历史双点名 `song_<key>..<ext>` → 单点名的迁移重命名)。
* 命中:out=正式单点名路径并刷新 mtime(LRU),返回 NCM_OK;未命中 NCM_ERR_NOENT。 */
static int cache_lookup_exact(const ncm_cache_t *c, const char *key, const char *ext,
char *out, size_t out_sz) {
char path[NCM_CACHE_DIR_MAX + 32];
if (ncm_cache_path(c, id, ext, path, sizeof path) != 0) return NCM_ERR_FULL;
if (ncm_cache_path(c, key, ext, path, sizeof path) != 0) return NCM_ERR_FULL;
struct stat st;
if (stat(path, &st) != 0 || !S_ISREG(st.st_mode)) {
/* §19 温和兼容:历史双点名 `song_<id>..<ext>`(旧版 ext 带前导点所致)。
@@ -160,27 +162,23 @@ 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";
const char *base = cache_base_dir(c, id);
int n = snprintf(path, sizeof path, "%s/song_%s..%s", base, id, ee);
const char *base = cache_base_dir(c, key);
int n = snprintf(path, sizeof path, "%s/song_%s..%s", base, key, ee);
if (n < 0 || (size_t)n >= sizeof path) return NCM_ERR_FULL;
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);
n = snprintf(path, sizeof path, "%s/song_%s..%s", c->dir, key, 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) {
if (ncm_cache_path(c, key, ee, fixed, sizeof fixed) == NCM_OK) {
struct stat fst;
if (stat(fixed, &fst) == 0) unlink(path); /* 目标已(在/被并发建):删旧双点 */
else rename(path, fixed);
if (stat(fixed, &st) == 0 && S_ISREG(st.st_mode)) {
ncm_strlcpy(path, fixed, sizeof path);
} else {
ncm_strlcpy(path, fixed, sizeof path); /* 迁移失败(权限等):回到固定路径 */
return NCM_ERR_NOENT;
}
if (stat(fixed, &st) != 0 || !S_ISREG(st.st_mode)) return NCM_ERR_NOENT;
}
}
utime(path, NULL); /* 刷新最后访问(按 mtime LRU) */
@@ -188,6 +186,51 @@ int ncm_cache_lookup(const ncm_cache_t *c, const char *id, const char *ext,
return NCM_OK;
}
/* 档位下探表(高→低;DOLBY 与降级链/UI 同口径不在表内)。缓存文件按**实际档**命名(M6),
* 故高档请求可下探命中已存的低档实体:hires 请求命中 song_<id>_exhigh.mp3(降级产物不重下)。 */
static const int k_level_ladder[] = {
NCM_LEVEL_SURROUND, NCM_LEVEL_SKY, NCM_LEVEL_JYMASTER, NCM_LEVEL_JYEFFECT,
NCM_LEVEL_HIRES, NCM_LEVEL_LOSSLESS, NCM_LEVEL_EXHIGH, NCM_LEVEL_HIGHER,
NCM_LEVEL_STANDARD,
};
#define LEVEL_LADDER_N ((int)(sizeof k_level_ladder / sizeof k_level_ladder[0]))
/* 解析歌曲缓存键 `<数字id>_<档位键>` → 档位枚举(id 回填)。非歌曲键/无档位 → -1。 */
static int split_level_key(const char *key, char *id, size_t id_sz) {
const char *p = key;
size_t n = 0;
if (!key || !key[0]) return -1;
while (p[n] >= '0' && p[n] <= '9' && n + 1 < id_sz) { id[n] = p[n]; n++; }
if (n == 0 || p[n] != '_') return -1;
id[n] = 0;
return ncm_api_level_from_key(p + n + 1);
}
int ncm_cache_lookup(const ncm_cache_t *c, const char *id, const char *ext,
char *out, size_t out_sz) {
int rc;
if (!c || !id || !out) return NCM_ERR_ARG;
rc = cache_lookup_exact(c, id, ext, out, out_sz);
if (rc == NCM_OK || rc == NCM_ERR_FULL) return rc;
if (id_is_cover(id)) return NCM_ERR_NOENT; /* 封面键无档位语义 */
char sid[24];
int lv = split_level_key(id, sid, sizeof sid);
if (lv < 0 || lv > NCM_LEVEL_SURROUND) return NCM_ERR_NOENT;
int idx = -1;
for (int i = 0; i < LEVEL_LADDER_N; i++)
if (k_level_ladder[i] == lv) { idx = i; break; }
if (idx < 0) return NCM_ERR_NOENT;
for (int i = idx + 1; i < LEVEL_LADDER_N; i++) { /* 只降不升 */
char k2[48];
int n = snprintf(k2, sizeof k2, "%s_%s", sid, ncm_api_level_key(k_level_ladder[i]));
if (n < 0 || (size_t)n >= sizeof k2) continue;
rc = cache_lookup_exact(c, k2, ext, out, out_sz);
if (rc == NCM_OK) return NCM_OK;
if (rc == NCM_ERR_FULL) return rc;
}
return NCM_ERR_NOENT;
}
struct ncm_cache_writer {
char final_path[NCM_CACHE_DIR_MAX + 64];
char tmp_path[NCM_CACHE_DIR_MAX + 80];
@@ -465,10 +508,11 @@ int ncm_cache_clear(const ncm_cache_t *c) {
}
/* ---- 「已缓存列表」:扫描/删除(键=歌曲ID+档位,NCM_CACHE_DESIGN §2.5)----
* 文件名 song_<id>_<请求档位>.<实际格式>;**档位段只是请求档**,实际档由文件头探测得出
* (真机 song_<id>_hires.mp3 = 无损请求被降级链落到 exhigh mp3)。 */
* 文件名 song_<id>_<档位>.<实际格式>:M6 起**档位段=实际档**(响应档),如无损请求被
* 降级链落到 exhigh mp3 → song_<id>_exhigh.mp3。旧版缓存名的档位段仍是请求档
* (真机 song_<id>_hires.mp3),读取/删除/LRU 一律按文件名原样兼容,不强制迁移。 */
/* 解析条目名 → id/请求档位键/实际扩展名。song_ 后必须是数字 id(排除 song_cov* 封面)。
/* 解析条目名 → id/文件名档位键/实际扩展名。song_ 后必须是数字 id(排除 song_cov* 封面)。
* 返回 0=是歌曲条目。 */
static int parse_item_name(const char *nm, ncm_cache_item_t *it)
{
@@ -493,23 +537,70 @@ static int parse_item_name(const char *nm, ncm_cache_item_t *it)
return 0;
}
/* 请求档 + 实际容器参数 → 实际档(只降不升:显示「已降级至 X」语义)。 */
static int actual_level_of(int req, const ncm_audio_info_t *ai)
/* 容器探测结果 → 该容器可承载的档位上界(-1=无法判定,由响应档/请求档决定)。 */
static int container_level_cap(const ncm_audio_info_t *ai)
{
int cap;
if (!ai || !ai->container[0]) return req; /* 探测失败:保留请求档 */
if (!ai || !ai->container[0]) return -1;
if (strcmp(ai->container, "flac") == 0) {
if (ai->bits_per_sample <= 16) cap = NCM_LEVEL_LOSSLESS;
else if (ai->sample_rate > 96000) cap = NCM_LEVEL_JYMASTER; /* 192k/24 */
else if (ai->sample_rate > 48000) cap = NCM_LEVEL_JYEFFECT; /* 96k/24 */
else cap = NCM_LEVEL_HIRES; /* 48k/24 */
} else if (strcmp(ai->container, "mp3") == 0) {
cap = (ai->bitrate_kbps > 128) ? NCM_LEVEL_EXHIGH : NCM_LEVEL_STANDARD;
} else {
return req;
if (ai->bits_per_sample <= 0 || ai->sample_rate <= 0) return -1; /* 仅知容器 */
if (ai->bits_per_sample <= 16) return NCM_LEVEL_LOSSLESS;
if (ai->sample_rate > 96000) return NCM_LEVEL_JYMASTER; /* 192k/24 */
if (ai->sample_rate > 48000) return NCM_LEVEL_JYEFFECT; /* 96k/24 */
return NCM_LEVEL_HIRES; /* 48k/24 */
}
if (req < 0) return cap;
return (req < cap) ? req : cap;
if (strcmp(ai->container, "mp3") == 0) {
if (ai->bitrate_kbps > 128) return NCM_LEVEL_EXHIGH;
if (ai->bitrate_kbps > 0) return NCM_LEVEL_STANDARD;
return -1; /* 码率未知:交 resp_level 钳顶 */
}
return -1;
}
int ncm_cache_actual_level(int req, const ncm_audio_info_t *ai, int resp_level)
{
int cap = container_level_cap(ai);
int v;
if (resp_level < 0) resp_level = req;
if (ai && strcmp(ai->container, "mp3") == 0) {
if (cap < 0) cap = NCM_LEVEL_EXHIGH; /* mp3 永不高于极高 */
if (resp_level >= 0 && resp_level < cap) cap = resp_level;
} else if (cap < 0) {
cap = resp_level; /* 头参数未知:信响应/降级链生效档 */
} else if (resp_level >= 0 && resp_level < cap) {
cap = resp_level;
}
if (req < 0) v = (cap < 0) ? -1 : cap;
else if (cap < 0) v = req;
else v = (req < cap) ? req : cap; /* 只降不升 */
return v;
}
int ncm_cache_level_from_path(const char *path)
{
const char *base, *dot, *us = NULL, *us2 = NULL;
char key[20];
size_t n;
int lv;
if (!path || !path[0]) return -1;
base = strrchr(path, '/');
base = base ? base + 1 : path;
dot = strrchr(base, '.');
if (!dot) dot = base + strlen(base);
for (const char *p = base; p < dot; p++)
if (*p == '_') { us2 = us; us = p; }
if (!us || us + 1 >= dot) return -1;
n = (size_t)(dot - us - 1);
if (n >= sizeof key) return -1;
memcpy(key, us + 1, n);
key[n] = 0;
lv = ncm_api_level_from_key(key);
if (lv >= 0 || !us2 || us2 + 1 >= us - 1) return lv;
/* 档位键本身带下划线(sky_c51):再带上前一段试一次 */
n = (size_t)(dot - us2 - 1);
if (n >= sizeof key) return -1;
memcpy(key, us2 + 1, n);
key[n] = 0;
return ncm_api_level_from_key(key);
}
static int cmp_item_mtime_desc(const void *a, const void *b)
@@ -559,8 +650,11 @@ int ncm_cache_scan(const ncm_cache_t *c, ncm_cache_item_t *out, int max)
slot = oldest;
}
memset(&ai, 0, sizeof ai);
it.actual_level = actual_level_of(ncm_api_level_from_key(it.level),
(ncm_audio_probe_file(p, &ai) == NCM_OK) ? &ai : NULL);
/* 文件名档位 + 文件头实测 → 实际档(新命名两者一致;旧请求档命名由探测纠正,
* 如 song_<id>_hires.mp3 实体是 320k mp3 → actual=exhigh)。 */
it.actual_level = ncm_cache_actual_level(ncm_api_level_from_key(it.level),
(ncm_audio_probe_file(p, &ai) == NCM_OK) ? &ai : NULL,
-1);
if (ncm_tag_read_file(p, &tag) == NCM_OK) { /* 文件内 tag 优先(外部播放器同源) */
ncm_strlcpy(it.name, tag.title, sizeof it.name);
ncm_strlcpy(it.artist, tag.artist, sizeof it.artist);
@@ -570,6 +664,21 @@ int ncm_cache_scan(const ncm_cache_t *c, ncm_cache_item_t *out, int max)
}
closedir(d);
qsort(out, (size_t)n, sizeof *out, cmp_item_mtime_desc);
/* 同曲同实际档去重展示:旧命名(请求档)与新命名(实际档)可能同时存在
* (如 song_<id>_hires.mp3 与 song_<id>_exhigh.mp3 都是 320k mp3),只留最近访问的一条。 */
{
int w = 0;
for (int i = 0; i < n; i++) {
int dup = 0;
for (int j = 0; j < w; j++) {
if (strcmp(out[j].id, out[i].id) == 0 &&
out[j].actual_level == out[i].actual_level &&
strcmp(out[j].ext, out[i].ext) == 0) { dup = 1; break; }
}
if (!dup) { if (w != i) out[w] = out[i]; w++; }
}
n = w;
}
return n;
}
+28 -5
View File
@@ -11,6 +11,9 @@
#include <stddef.h>
#include <stdint.h>
#include "ncm_api.h" /* NCM_LEVEL_*(实际档位判定/文件名档位解析) */
#include "ncm_tag.h" /* ncm_audio_info_t(容器/位深/码率探测结果) */
#ifdef __cplusplus
extern "C" {
#endif
@@ -47,10 +50,28 @@ int ncm_cache_path(const ncm_cache_t *c, const char *id, const char *ext,
char *out, size_t out_sz);
/* 查找已缓存文件。命中:写 out 路径并刷新 mtime,返回 NCM_OK。未命中:NCM_ERR_NOENT。
* §19:ext 前导点被忽略(新旧调用方同路径);单点 miss 时回退历史双点名 `song_<id>..<ext>`。 */
* §19:ext 前导点被忽略(新旧调用方同路径);单点 miss 时回退历史双点名 `song_<id>..<ext>`。
*
* 档位下探(M6):键形如 `<歌曲id>_<档位键>` 时,请求档精确 miss 后**只降不升**逐档下探
* (hires → lossless → exhigh → higher → standard)。缓存文件已按**实际档**命名,故
* hires 请求能命中已存的 song_<id>_exhigh.mp3(降级产物不重下);低档请求不会命中高档实体。 */
int ncm_cache_lookup(const ncm_cache_t *c, const char *id, const char *ext,
char *out, size_t out_sz);
/* 实际档位判定(**单一事实源**:写缓存名 / 已缓存列表显示 / 命中档位推断共用)。
* req 请求档 NCM_LEVEL_*(<0=未知)
* ai 容器探测结果(可为 NULL;flac 需 bits/sample_rate 才按头判档)
* resp_level 响应/降级链生效档(<0=未知;flac 响应头参数未知时以它为准)
* 返回 min(req, 容器可承载档, resp_level)——只降不升:
* mp3 码率 ≤128k → standard,>128k → exhigh(mp3 永不高于 exhigh);
* flac 16bit/44.1k → lossless,24bit/48k → hires,96k → jyeffect,192k → jymaster。 */
int ncm_cache_actual_level(int req, const ncm_audio_info_t *ai, int resp_level);
/* 从缓存文件路径解析**文件名档位段**(song_<id>_<档位>.<ext> 的档位;取扩展名前最后一个
* '_' 之后的部分)。新命名=实际档,旧命名=请求档(可能存在降级,调用方需再按容器钳顶)。
* 返回 NCM_LEVEL_*;无档位段/未知档位 → -1。 */
int ncm_cache_level_from_path(const char *path);
/* 流式写入句柄:open 建 temp;write 累积(write(2) 直写无 stdio 缓冲,§21 边下边播
* 读端即时可见);commit 落盘+刷新LRU+触发淘汰;abort 丢弃。 */
typedef struct ncm_cache_writer ncm_cache_writer_t;
@@ -88,12 +109,13 @@ uint64_t ncm_cache_total_bytes(const ncm_cache_t *c);
int ncm_cache_evict(const ncm_cache_t *c);
/* 缓存条目(ncm_cache_scan 输出,供「已缓存列表」展示/点播/删除)。
* 命名约定 song_<id>_<请求档位>.<实际格式>;**档位段是请求档,不是实际档**——真机存在
* song_171650_hires.mp3(无损请求被降级链落到 exhigh mp3)。故 scan 用 ncm_audio_probe_file
* 读实际容器参数,actual_level 给**实际档**(-1=无法判定);播放/删除仍用 id+level+ext 定位。 */
* 命名约定 song_<id>_<档位>.<实际格式>(M6 起档位段=**实际档**;旧版缓存名的档位段仍是请求档,
* 如真机 song_171650_hires.mp3 = 无损请求被降级链落到 exhigh mp3,读可用、不强制迁移)。
* 故 level 只作「文件名档位键」用于定位删除/点播;actual_level 由 ncm_audio_probe_file 读实际
* 容器参数后按 ncm_cache_actual_level 判定(新命名下与文件名一致;-1=无法判定)。 */
typedef struct ncm_cache_item {
char id[24]; /* 歌曲 id(数字串) */
char level[20]; /* 文件名里的请求档位键(""=旧无档位键条目) */
char level[20]; /* 文件名档位键(新命名=实际档;旧命名=请求档;""=旧无档位键条目) */
char ext[8]; /* 实际容器扩展名(mp3/flac,去点) */
int actual_level; /* 实际档 NCM_LEVEL_*(按容器/位深/码率推断;-1=未知) */
uint64_t size; /* 文件字节数(含嵌入 tag) */
@@ -104,6 +126,7 @@ typedef struct ncm_cache_item {
/* 扫描歌曲缓存池(songs/,跳过 .tmp/.part 半成品与非 song_<数字 id> 条目),按 mtime 降序
* (最近播放/写入在前)写入 out(最多 max 条;池内更多时保留最近的 max 条)。返回条目数或负错误码。
* 同一 (id, actual_level, ext) 只保留最近访问的一条(旧请求档名 + 新实际档名并存时去重展示)。
* 每条会读文件头(tag + 音频参数探测),属磁盘 IO——**必须在 worker 线程调用**。 */
int ncm_cache_scan(const ncm_cache_t *c, ncm_cache_item_t *out, int max);