feat(ncm): 新写入的缓存文件嵌入歌曲元数据(FLAC/MP3,commit 前流式重写,失败降级裸流)

用户需求:保留元数据分离的同时把歌曲元数据嵌进缓存文件(否则外部播放器读不到任何信息)。
- 接入点 player_manager:下载完整(dl_rc==OK)→ stream_src_finish(读端先自然收尾)→
  ncm_tag_embed_file(.tmp, 实际扩展名, {name/art_name/album_name}) → dispose join 之后才
  commit。tag 重写是同目录 .tag.tmp + rename 换 inode,解码端全程持旧 fd,播放/seek/
  .part 续传字节语义零改动。
- 容器按实际 magic 分支(ncm_tag.c);峰值内存 64KB;失败只记 WARN、commit 裸流,
  播放主链路不受影响(用户红线)。
- 元数据分离体系不动:<cache_dir>/meta/<id>.bin 旁路照旧写读;LRU 只 stat 文件,
  无 size/etag 索引需回填(重写后 stat 自然一致)。
- test_ncm_core 新增 test_cache_tag:writer 写裸流 → 嵌入 ID3v2.3 / VORBIS_COMMENT →
  读回 → ncm_cache_scan(hires 名 + 320k mp3 体 → 实际档 EXHIGH,名称来自文件内 tag)
  → ncm_cache_remove → ncm_cache_clear。
- REPORT §38:设计/内存估算/验证/诚实遗留(本机缺 vendor 与交叉工具链,未跑整链与真机)。
This commit is contained in:
AskaEth committed 2026-11-04 20:49:25 +08:00
1 parent f392ee5c48
commit 39465ea1ef
3 files changed
+234 -1

No files matched your search

+20 -1
View File
@@ -349,6 +349,11 @@ typedef struct {
char url[1024];/* 下载线程用(song_local_file 栈上 u.url 生命周期不够,拷贝) */
size_t maxb; /* 下载限幅 */
int64_t total; /* 标称大小(song_url size;0=未知;mp3 seek 分母) */
/* 缓存文件元数据嵌入(NCM_CACHE_DESIGN §2.5 + M5):下载完整后由下载线程把
* id3/vorbis tag 写进 .tmp(按实际容器分支),再交播放线程 commit。失败只记日志,
* commit 裸流——播放主链路不受影响。 */
ncm_tag_meta_t tag;
char ext[8]; /* 实际容器(song_url type / url 扩展名) */
/* 阶段3 .part 续传(§5.2) */
uint64_t resume_base; /* >0=断点续传起点(.part 大小);Http 带 Range: bytes=<n>- */
int first_body; /* 首个 body 回调未处理(按响应状态切换写姿态) */
@@ -441,8 +446,16 @@ static void *dl_thread(void *arg)
if (!ok_st) rc = NCM_ERR_HTTP;
}
ss->dl_rc = rc;
/* 结束通知:成功 final=全长;失败/中止 final=截断点+failed(读端播到截断点自然结束,F5) */
/* 缓存文件元数据嵌入(M5):下载完整 → 把 id3/vorbis tag 流式写进 .tmp(64KB 分块,
* 同 inode 替换:解码端持有旧 fd 继续播,不受影响)。先 finish 让读端自然收尾,再重写;
* dispose 的 join 保证「重写完成 → commit」顺序。任何失败都只降级为裸流。 */
stream_src_finish(ss->src, rc == NCM_OK ? 0 : rc);
if (rc == NCM_OK && ss->w && !g_app_quit) {
int trc = ncm_tag_embed_file(ncm_cache_writer_tmp_path(ss->w), ss->ext, &ss->tag);
if (trc != NCM_OK)
meow_log(MEOW_LOG_WARN, "play: tag embed skipped rc=%d ext=%s (raw stream kept)",
trc, ss->ext);
}
return NULL;
}
@@ -548,6 +561,12 @@ static int song_download_attempt(const ncm_song_t *s, int idx, int seq, const ch
const char *ext = "mp3";
if (ncm_streq(u.type, "flac")) ext = "flac";
else if (!u.type[0] && strstr(u.url, ".flac")) ext = "flac";
ncm_strlcpy(ss->ext, ext, sizeof ss->ext);
/* 文件内 tag 字段(下载完整后由 dl_thread 写入;空字段不写)。 */
memset(&ss->tag, 0, sizeof ss->tag);
ncm_strlcpy(ss->tag.title, s->name, sizeof ss->tag.title);
ncm_strlcpy(ss->tag.artist, s->art_name, sizeof ss->tag.artist);
ncm_strlcpy(ss->tag.album, s->album_name, sizeof ss->tag.album);
/* 写前检查:池大小超限先按 LRU 淘汰(半成品 .tmp/.part 已被 evict 跳过,无在途误删风险) */
ncm_cache_evict(&g_st.ctx.cache);