feat(metadata): 元数据分阶段探测(128KB → 512KB)+ song_key 改为纯 URL

渐进探测:
- FileProvider.getFile 增加 maxBytes 参数,WebDAV 按需只取头部
- MetadataService 先取 128KB 读标签;title/artist 都为空时再补到 512KB
- 缓存文件按上限区分(metadata_<hash>_128k.part / _512k.part)

song_key:
- 从 md5(url|size) 改为 md5(url)。分阶段探测的大小不同,
  带 size 会让同一首歌产生两个键;URL 在各阶段稳定。
- 代价:同 URL 换内容不自动失效,由清除单曲缓存/forceRefresh 兜底
- 数据库升到 v6,清空 songs / metadata_cache(可再生)

验证(release,真机):
  📦 取头部 128KB(上限 128KB,不再整文件下载)
  🧭 状态机: first_seen → pending(validations=1)
  📊 pending conf=0.92 evidence={...}
  ✅ saved: 两生关 - 谢安琪

测试:新增 song_key 3 个用例,累计 19 个全绿。
This commit is contained in:
2026-10-24 20:28:50 +08:00
parent b8138ad125
commit 18308d9d4e
6 changed files with 66 additions and 36 deletions
+9 -13
View File
@@ -4,19 +4,15 @@ import 'package:crypto/crypto.dart';
/// 歌曲缓存键(song_key)。
///
/// 只使用「远程路径 + 文件大小」这类稳定信息:
/// - 不能用 String.hashCode:语义上不保证跨运行稳定(虽然当前 VM 实现是稳定的),
/// 用它做持久化键属于依赖未定义行为;
/// - 不能用本地临时文件的 mtime:WebDAV 的元数据预览文件会被系统清缓存、
/// 或超过 24h 后重新下载,mtime 每次都变,键跟着变 → 缓存永远 miss。
/// 只用 URL:元数据读取是"分阶段取头部"的,第一次可能只取 128KB、
/// 第二次补到 512KB。如果键里带文件大小,两次会得到不同的键,缓存立刻失效;
/// URL 在这些阶段之间是稳定的。
///
/// 因此这里用 md5(url|size)。文件被替换但大小不变时不会自动失效,
/// 由“清除单曲缓存 / forceRefresh”兜底。
String songKeyFor(String url, int fileSize) {
return md5.convert(utf8.encode('$url|$fileSize')).toString();
}
/// 只依赖 URL 的稳定键,用于临时文件命名、拿不到 stat 的场景。
String songKeyForUrl(String url) {
/// 代价:同一个 URL 换了文件内容时不会自动失效,由"清除单曲缓存 /
/// forceRefresh"兜底。
String songKeyFor(String url) {
return md5.convert(utf8.encode(url)).toString();
}
/// 语义化别名,用于临时文件命名等场景。
String songKeyForUrl(String url) => songKeyFor(url);