feat: AudioMetadataReader
This commit is contained in:
Regular → Executable
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
-191
@@ -1,191 +0,0 @@
|
|||||||
让我整理一下这些字段的含义:
|
|
||||||
|
|
||||||
1. id - 主键ID
|
|
||||||
2. path - 原始导入路径
|
|
||||||
3. filename - 带扩展名的文件名
|
|
||||||
4. description - 文件自带描述
|
|
||||||
5. length - 音频长度(时长)
|
|
||||||
6. bit_depth - 位深度(如16bit, 24bit)
|
|
||||||
7. channels - 声道数(单声道/立体声/环绕声)
|
|
||||||
8. date_added - 导入日期
|
|
||||||
9. sample_rate - 采样率(如44.1kHz, 48kHz)
|
|
||||||
10. type - 文件类型(如WAV, MP3, AIFF等)
|
|
||||||
11. cd_title - CD标题
|
|
||||||
12. artist - 艺术家
|
|
||||||
13. coding_history - 编码历史
|
|
||||||
14. original_date - 原始日期
|
|
||||||
15. originator_ref - 来源引用
|
|
||||||
16. originator - 来源/创作者
|
|
||||||
17. timecode - 时间码
|
|
||||||
18. subcategory - 子分类
|
|
||||||
19. category - 分类
|
|
||||||
20. comments - 注释
|
|
||||||
21. episode - 剧集/集数
|
|
||||||
22. frame_rate - 帧率
|
|
||||||
23. group - 分组
|
|
||||||
24. library - 音频库
|
|
||||||
25. location - 位置/地点
|
|
||||||
26. project_name - 项目名称
|
|
||||||
27. rating - 评分
|
|
||||||
28. tape - 磁带
|
|
||||||
29. scene - 场景
|
|
||||||
30. sync_point - 同步点
|
|
||||||
31. take - 录音条次
|
|
||||||
32. track_title - 曲目标题
|
|
||||||
33. composer - 作曲家
|
|
||||||
34. designer - 设计师
|
|
||||||
35. publisher - 出版商
|
|
||||||
36. folder - 文件夹
|
|
||||||
37. genre - 流派/风格
|
|
||||||
38. keywords - 关键词
|
|
||||||
39. copyright - 版权
|
|
||||||
40. recordist - 录音师
|
|
||||||
41. manufacturer - 制造商
|
|
||||||
42. notes - 备注
|
|
||||||
43. microphone - 麦克风
|
|
||||||
44. directory - 目录
|
|
||||||
45. origination_time - 创建时间
|
|
||||||
46. unique_id - 唯一标识符
|
|
||||||
47. bpm - 每分钟节拍数
|
|
||||||
48. mood - 情绪/氛围
|
|
||||||
49. short_id - 短ID
|
|
||||||
50. cue_number - 提示号
|
|
||||||
51. style - 风格
|
|
||||||
52. markers - 标记点
|
|
||||||
53. is_edited - 是否已编辑
|
|
||||||
54. is_split - 是否已分割
|
|
||||||
55-62. user1-user8 - 用户自定义字段
|
|
||||||
|
|
||||||
我会为用户提供一个详细的分析报告。
|
|
||||||
|
|
||||||
我来为您分析这个音频管理软件的表结构。根据字段名称和数据类型,这是一个专业的**音频资产管理系统**的数据库结构,类似于 SoundMiner、BaseHead 等专业音频库管理软件。
|
|
||||||
|
|
||||||
## 字段含义分析
|
|
||||||
|
|
||||||
### 📌 基础标识字段
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
| 字段名 | 类型 | 含义说明 |
|
|
||||||
| ----------- | ------- | ---------------------------------------------- |
|
|
||||||
| `id` | integer | **主键ID**,音频文件的唯一数字标识符 |
|
|
||||||
| `unique_id` | text | **全局唯一标识符**,通常为UUID,用于跨系统识别 |
|
|
||||||
| `short_id` | text | **短ID**,便于人工识别或显示的简短标识 |
|
|
||||||
|
|
||||||
### 📁 文件存储字段
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
| 字段名 | 类型 | 含义说明 |
|
|
||||||
| ----------- | ---- | ---------------------------------------------------- |
|
|
||||||
| `path` | text | **原始导入路径**,音频文件被导入时的完整文件路径 |
|
|
||||||
| `filename` | text | **文件名**,包含扩展名的音频文件名称(如 sound.wav) |
|
|
||||||
| `folder` | text | **文件夹**,文件所在的文件夹名称 |
|
|
||||||
| `directory` | text | **目录**,文件的存储目录路径 |
|
|
||||||
|
|
||||||
### 🎵 音频技术参数
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
| 字段名 | 类型 | 含义说明 |
|
|
||||||
| ------------- | ------- | -------------------------------------------------------- |
|
|
||||||
| `length` | real | **时长**,音频文件的播放长度(通常以秒为单位) |
|
|
||||||
| `bit_depth` | integer | **位深度**,音频采样位深(如 16、24、32 bit) |
|
|
||||||
| `channels` | integer | **声道数**,如 1=单声道、2=立体声、6=5.1环绕声 |
|
|
||||||
| `sample_rate` | integer | **采样率**,如 44100、48000、96000 Hz |
|
|
||||||
| `type` | text | **文件类型**,音频格式(WAV、MP3、AIFF、FLAC等) |
|
|
||||||
| `bpm` | real | **节拍速度**,每分钟节拍数,用于音乐类音频 |
|
|
||||||
| `frame_rate` | text | **帧率**,视频同步用的时间基准(如 24fps、25fps、30fps) |
|
|
||||||
| `timecode` | integer | **时间码**,用于音视频同步的时间戳信息 |
|
|
||||||
|
|
||||||
### 📝 元数据描述字段
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
| 字段名 | 类型 | 含义说明 |
|
|
||||||
| ------------- | ------------ | ---------------------------------------------------- |
|
|
||||||
| `description` | text | **描述**,音频内容的文字说明 |
|
|
||||||
| `category` | text | **分类**,音频的大类(如:音效、音乐、对白、环境音) |
|
|
||||||
| `subcategory` | text | **子分类**,更细分的类别(如:交通→汽车→引擎) |
|
|
||||||
| `genre` | text | **流派**,音乐风格分类(如:摇滚、古典、电子) |
|
|
||||||
| `style` | text | **风格**,更具体的风格描述 |
|
|
||||||
| `mood` | text | **情绪**,音频传达的情感氛围(如:紧张、欢快、悲伤) |
|
|
||||||
| `keywords` | text | **关键词**,用于搜索的标签,多个关键词通常用逗号分隔 |
|
|
||||||
| `rating` | decimal(5.3) | **评分**,音频质量或推荐度评分(如 0-5星) |
|
|
||||||
|
|
||||||
### 👤 创作者信息字段
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
| 字段名 | 类型 | 含义说明 |
|
|
||||||
| ---------------- | ---- | -------------------------------- |
|
|
||||||
| `artist` | text | **艺术家**,表演者或演奏者名称 |
|
|
||||||
| `composer` | text | **作曲家**,音乐创作者 |
|
|
||||||
| `designer` | text | **设计师**,音效设计师名称 |
|
|
||||||
| `recordist` | text | **录音师**,现场录音人员 |
|
|
||||||
| `publisher` | text | **出版商**,发行方或版权持有方 |
|
|
||||||
| `manufacturer` | text | **制造商**,音频库或设备的制作方 |
|
|
||||||
| `originator` | text | **来源方**,原始创建者或来源机构 |
|
|
||||||
| `originator_ref` | text | **来源引用**,原始来源的参考编号 |
|
|
||||||
|
|
||||||
### 🎬 项目制作字段
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
| 字段名 | 类型 | 含义说明 |
|
|
||||||
| -------------- | ------- | -------------------------------------- |
|
|
||||||
| `project_name` | text | **项目名称**,所属制作项目 |
|
|
||||||
| `library` | text | **音频库**,所属音效库或音乐库的名称 |
|
|
||||||
| `cd_title` | text | **CD标题**,原始CD专辑名称 |
|
|
||||||
| `track_title` | text | **曲目标题**,音乐或音轨的标题 |
|
|
||||||
| `episode` | text | **剧集**,所属剧集或系列编号 |
|
|
||||||
| `scene` | text | **场景**,对应的影视场景标识 |
|
|
||||||
| `take` | text | **条次**,录音的版本号或拍摄条次 |
|
|
||||||
| `tape` | text | **磁带**,原始磁带编号(历史遗留字段) |
|
|
||||||
| `cue_number` | integer | **提示号**,播放列表或乐谱中的编号 |
|
|
||||||
| `sync_point` | integer | **同步点**,用于音视频同步的参考点 |
|
|
||||||
|
|
||||||
### 📋 管理与状态字段
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
| 字段名 | 类型 | 含义说明 |
|
|
||||||
| ------------------ | ------- | -------------------------------------------------- |
|
|
||||||
| `date_added` | date | **导入日期**,音频入库的时间 |
|
|
||||||
| `original_date` | text | **原始日期**,音频创建或录制的日期 |
|
|
||||||
| `origination_time` | text | **创建时间**,音频文件的原始创建时间 |
|
|
||||||
| `is_edited` | integer | **是否已编辑**,布尔值(0=否,1=是) |
|
|
||||||
| `is_split` | integer | **是否已分割**,布尔值,标记是否从长文件中分割出来 |
|
|
||||||
| `location` | text | **位置**,录音地点或存储位置 |
|
|
||||||
| `group` | text | **分组**,用于组织管理的分组标识 |
|
|
||||||
| `markers` | text | **标记点**,音频内的关键时间点标记 |
|
|
||||||
|
|
||||||
### 📄 备注与版权字段
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
| 字段名 | 类型 | 含义说明 |
|
|
||||||
| ---------------- | ---- | -------------------------------------- |
|
|
||||||
| `comments` | text | **注释**,用户或系统添加的备注 |
|
|
||||||
| `notes` | text | **备注**,额外的说明信息 |
|
|
||||||
| `copyright` | text | **版权**,版权声明信息 |
|
|
||||||
| `coding_history` | text | **编码历史**,BWF格式的编码历史记录 |
|
|
||||||
| `microphone` | text | **麦克风**,录音使用的麦克风型号或设置 |
|
|
||||||
|
|
||||||
### 🔧 用户自定义字段
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
| 字段名 | 类型 | 含义说明 |
|
|
||||||
| ----------------- | ---- | ------------------------------------------------------------ |
|
|
||||||
| `user1` ~ `user8` | text | **用户自定义字段**,共8个预留字段,供用户根据实际需求自定义用途 |
|
|
||||||
|
|
||||||
## 总结
|
|
||||||
|
|
||||||
这是一个**专业级音频资产管理系统**的完整表结构,主要特点:
|
|
||||||
|
|
||||||
1. **符合广播级标准**:包含 BWF(Broadcast Wave Format)标准字段如 `coding_history`、`originator`、`timecode` 等
|
|
||||||
2. **支持影视后期制作**:有 `scene`、`take`、`episode`、`sync_point` 等影视制作相关字段
|
|
||||||
3. **音乐管理功能完善**:包含 `bpm`、`mood`、`genre`、`composer` 等音乐元数据
|
|
||||||
4. **灵活扩展性**:预留了8个用户自定义字段
|
|
||||||
5. **版权管理支持**:包含 `copyright`、`publisher`、`manufacturer` 等版权相关字段
|
|
||||||
+14
@@ -0,0 +1,14 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<OutputType>Exe</OutputType>
|
||||||
|
<TargetFramework>net10.0</TargetFramework>
|
||||||
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
|
<Nullable>enable</Nullable>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="z440.atl.core" Version="7.13.0" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
</Project>
|
||||||
Executable
+86
@@ -0,0 +1,86 @@
|
|||||||
|
using ATL;
|
||||||
|
|
||||||
|
namespace OCES.Resonance.AtlFieldExtractor;
|
||||||
|
|
||||||
|
class DumpCodingHistory
|
||||||
|
{
|
||||||
|
// static void Main(string[] args)
|
||||||
|
// {
|
||||||
|
// string sourceDir = args.Length > 0
|
||||||
|
// ? args[0]
|
||||||
|
// : Path.GetFullPath(Path.Combine(AppContext.BaseDirectory, "../../../../../../data/source"));
|
||||||
|
//
|
||||||
|
// Console.WriteLine($"扫描目录: {sourceDir}");
|
||||||
|
//
|
||||||
|
// if (!Directory.Exists(sourceDir))
|
||||||
|
// {
|
||||||
|
// Console.WriteLine($"目录不存在: {sourceDir}");
|
||||||
|
// return;
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// var wavFiles = Directory.GetFiles(sourceDir, "*.wav", SearchOption.AllDirectories);
|
||||||
|
// Console.WriteLine($"找到 {wavFiles.Length} 个 WAV 文件");
|
||||||
|
//
|
||||||
|
// foreach (var wavFile in wavFiles)
|
||||||
|
// {
|
||||||
|
// try
|
||||||
|
// {
|
||||||
|
// DumpFile(wavFile);
|
||||||
|
// }
|
||||||
|
// catch (Exception ex)
|
||||||
|
// {
|
||||||
|
// Console.WriteLine($"处理文件失败: {wavFile}");
|
||||||
|
// Console.WriteLine($"错误: {ex.Message}");
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// Console.WriteLine("处理完成!");
|
||||||
|
// }
|
||||||
|
|
||||||
|
static void DumpFile(string wavFilePath)
|
||||||
|
{
|
||||||
|
Console.WriteLine($"处理: {wavFilePath}");
|
||||||
|
|
||||||
|
var track = new Track(wavFilePath);
|
||||||
|
|
||||||
|
if (track.AdditionalFields == null ||
|
||||||
|
!track.AdditionalFields.TryGetValue("bext.codingHistory", out var codingHistory))
|
||||||
|
{
|
||||||
|
Console.WriteLine(" -> 无 bext.codingHistory 字段");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Console.WriteLine($" -> 字符串长度: {codingHistory.Length}");
|
||||||
|
|
||||||
|
// 用 Latin-1 编码转回字节(Latin-1 把 Unicode 0x00-0xFF 直接映射为 byte 0x00-0xFF)
|
||||||
|
var bytes = System.Text.Encoding.Latin1.GetBytes(codingHistory);
|
||||||
|
var outputPath = Path.Combine(
|
||||||
|
Path.GetDirectoryName(wavFilePath)!,
|
||||||
|
Path.GetFileNameWithoutExtension(wavFilePath) + ".codingHistory.bin"
|
||||||
|
);
|
||||||
|
|
||||||
|
File.WriteAllBytes(outputPath, bytes);
|
||||||
|
Console.WriteLine($" -> 已保存: {outputPath}");
|
||||||
|
Console.WriteLine($" -> 字节数: {bytes.Length}");
|
||||||
|
|
||||||
|
// 打印前 64 个字节的 hex
|
||||||
|
var hexPreview = Math.Min(64, bytes.Length);
|
||||||
|
Console.Write(" -> Hex 前 64 字节: ");
|
||||||
|
for (int i = 0; i < hexPreview; i++)
|
||||||
|
{
|
||||||
|
Console.Write($"{bytes[i]:X2} ");
|
||||||
|
if ((i + 1) % 16 == 0) Console.Write(" ");
|
||||||
|
}
|
||||||
|
Console.WriteLine();
|
||||||
|
|
||||||
|
// 打印前 64 个字节的 ASCII(不可打印字符用 . 替代)
|
||||||
|
Console.Write(" -> ASCII 前 64 字节: ");
|
||||||
|
for (int i = 0; i < hexPreview; i++)
|
||||||
|
{
|
||||||
|
var c = (char)bytes[i];
|
||||||
|
Console.Write(c >= 32 && c < 127 ? c : '.');
|
||||||
|
if ((i + 1) % 16 == 0) Console.Write(" ");
|
||||||
|
}
|
||||||
|
Console.WriteLine();
|
||||||
|
}
|
||||||
|
}
|
||||||
Executable
+148
@@ -0,0 +1,148 @@
|
|||||||
|
using ATL;
|
||||||
|
|
||||||
|
namespace OCES.Resonance.AtlFieldExtractor;
|
||||||
|
|
||||||
|
class Program
|
||||||
|
{
|
||||||
|
static void Main(string[] args)
|
||||||
|
{
|
||||||
|
// 默认扫描路径:项目的 data/source 目录
|
||||||
|
string sourceDir = args.Length > 0
|
||||||
|
? args[0]
|
||||||
|
: Path.GetFullPath(Path.Combine(AppContext.BaseDirectory, "../../../../../../data/source"));
|
||||||
|
|
||||||
|
Console.WriteLine($"扫描目录: {sourceDir}");
|
||||||
|
|
||||||
|
if (!Directory.Exists(sourceDir))
|
||||||
|
{
|
||||||
|
Console.WriteLine($"目录不存在: {sourceDir}");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 查找所有 WAV 文件
|
||||||
|
var wavFiles = Directory.GetFiles(sourceDir, "*.wav", SearchOption.AllDirectories);
|
||||||
|
|
||||||
|
Console.WriteLine($"找到 {wavFiles.Length} 个 WAV 文件");
|
||||||
|
|
||||||
|
foreach (var wavFile in wavFiles)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
ProcessFile(wavFile);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Console.WriteLine($"处理文件失败: {wavFile}");
|
||||||
|
Console.WriteLine($"错误: {ex.Message}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Console.WriteLine("处理完成!");
|
||||||
|
}
|
||||||
|
|
||||||
|
static void ProcessFile(string wavFilePath)
|
||||||
|
{
|
||||||
|
Console.WriteLine($"处理: {wavFilePath}");
|
||||||
|
|
||||||
|
var track = new Track(wavFilePath);
|
||||||
|
var outputPath = Path.ChangeExtension(wavFilePath, ".atl.txt");
|
||||||
|
|
||||||
|
using var writer = new StreamWriter(outputPath);
|
||||||
|
writer.WriteLine("=== ATL Track 基础属性 ===");
|
||||||
|
writer.WriteLine();
|
||||||
|
|
||||||
|
// 基础技术信息
|
||||||
|
writer.WriteLine($"DurationMs: {track.DurationMs}");
|
||||||
|
writer.WriteLine($"Duration: {track.Duration}");
|
||||||
|
writer.WriteLine($"SampleRate: {track.SampleRate}");
|
||||||
|
writer.WriteLine($"BitDepth: {track.BitDepth}");
|
||||||
|
writer.WriteLine($"Channels: {track.ChannelsArrangement.NbChannels}");
|
||||||
|
writer.WriteLine($"ChannelsArrangement: {track.ChannelsArrangement}");
|
||||||
|
|
||||||
|
writer.WriteLine();
|
||||||
|
writer.WriteLine("=== ATL 元数据字段 ===");
|
||||||
|
writer.WriteLine();
|
||||||
|
|
||||||
|
// 通用元数据
|
||||||
|
writer.WriteLine($"Title: {track.Title}");
|
||||||
|
writer.WriteLine($"Artist: {track.Artist}");
|
||||||
|
writer.WriteLine($"Album: {track.Album}");
|
||||||
|
writer.WriteLine($"AlbumArtist: {track.AlbumArtist}");
|
||||||
|
writer.WriteLine($"Composer: {track.Composer}");
|
||||||
|
writer.WriteLine($"Comment: {track.Comment}");
|
||||||
|
writer.WriteLine($"Genre: {track.Genre}");
|
||||||
|
writer.WriteLine($"Year: {track.Year}");
|
||||||
|
writer.WriteLine($"TrackNumber: {track.TrackNumber}");
|
||||||
|
writer.WriteLine($"DiscNumber: {track.DiscNumber}");
|
||||||
|
writer.WriteLine($"Publisher: {track.Publisher}");
|
||||||
|
writer.WriteLine($"Copyright: {track.Copyright}");
|
||||||
|
writer.WriteLine($"Description: {track.Description}");
|
||||||
|
writer.WriteLine($"OriginalReleaseDate: {track.OriginalReleaseDate}");
|
||||||
|
writer.WriteLine($"Date: {track.Date}");
|
||||||
|
writer.WriteLine($"BPM: {track.BPM}");
|
||||||
|
writer.WriteLine($"Group: {track.Group}");
|
||||||
|
writer.WriteLine($"SeriesTitle: {track.SeriesTitle}");
|
||||||
|
writer.WriteLine($"SeriesPart: {track.SeriesPart}");
|
||||||
|
|
||||||
|
writer.WriteLine();
|
||||||
|
writer.WriteLine("=== ATL AdditionalFields (额外字段) ===");
|
||||||
|
writer.WriteLine();
|
||||||
|
|
||||||
|
// 打印所有附加字段
|
||||||
|
if (track.AdditionalFields != null && track.AdditionalFields.Count > 0)
|
||||||
|
{
|
||||||
|
foreach (var field in track.AdditionalFields.OrderBy(f => f.Key))
|
||||||
|
{
|
||||||
|
writer.WriteLine($"[{field.Key}] = {field.Value}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
writer.WriteLine("(无附加字段)");
|
||||||
|
}
|
||||||
|
|
||||||
|
writer.WriteLine();
|
||||||
|
writer.WriteLine("=== ATL AdditionalImages (封面/图片) ===");
|
||||||
|
writer.WriteLine();
|
||||||
|
|
||||||
|
// 打印图片信息
|
||||||
|
if (track.EmbeddedPictures != null && track.EmbeddedPictures.Count > 0)
|
||||||
|
{
|
||||||
|
foreach (var pic in track.EmbeddedPictures)
|
||||||
|
{
|
||||||
|
writer.WriteLine($"PictureType: {pic.PicType}");
|
||||||
|
writer.WriteLine($"Description: {pic.Description}");
|
||||||
|
writer.WriteLine($"MimeType: {pic.MimeType}");
|
||||||
|
writer.WriteLine($"Data Length: {pic.PictureData?.Length ?? 0} bytes");
|
||||||
|
writer.WriteLine();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
writer.WriteLine("(无嵌入图片)");
|
||||||
|
}
|
||||||
|
|
||||||
|
writer.WriteLine();
|
||||||
|
writer.WriteLine("=== ATL Chapters (章节信息) ===");
|
||||||
|
writer.WriteLine();
|
||||||
|
|
||||||
|
// 打印章节信息
|
||||||
|
if (track.Chapters != null && track.Chapters.Count > 0)
|
||||||
|
{
|
||||||
|
foreach (var chapter in track.Chapters)
|
||||||
|
{
|
||||||
|
writer.WriteLine($"Title: {chapter.Title}");
|
||||||
|
writer.WriteLine($"StartTime: {chapter.StartTime}");
|
||||||
|
writer.WriteLine($"EndTime: {chapter.EndTime}");
|
||||||
|
writer.WriteLine($"Sub-Title: {chapter.Subtitle}");
|
||||||
|
writer.WriteLine();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
writer.WriteLine("(无章节信息)");
|
||||||
|
}
|
||||||
|
|
||||||
|
Console.WriteLine($" -> 已保存: {outputPath}");
|
||||||
|
}
|
||||||
|
}
|
||||||
Regular → Executable
@@ -1,133 +0,0 @@
|
|||||||
using FluentAssertions;
|
|
||||||
|
|
||||||
namespace OCES.Resonance.Core.Tests;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// AudioLibraryService 集成测试
|
|
||||||
/// </summary>
|
|
||||||
public class AudioLibraryServiceTests : IDisposable
|
|
||||||
{
|
|
||||||
readonly AudioLibraryService _service;
|
|
||||||
readonly string _testDirectory;
|
|
||||||
|
|
||||||
public AudioLibraryServiceTests()
|
|
||||||
{
|
|
||||||
_service = new AudioLibraryService();
|
|
||||||
_testDirectory = Path.Combine(Path.GetTempPath(), $"LibraryServiceTest_{Guid.NewGuid()}");
|
|
||||||
Directory.CreateDirectory(_testDirectory);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void Dispose()
|
|
||||||
{
|
|
||||||
if (Directory.Exists(_testDirectory))
|
|
||||||
{
|
|
||||||
Directory.Delete(_testDirectory, true);
|
|
||||||
}
|
|
||||||
// 清理测试数据库
|
|
||||||
var dbPath = "default.db";
|
|
||||||
if (File.Exists(dbPath))
|
|
||||||
{
|
|
||||||
File.Delete(dbPath);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public async Task ScanAndImportToLibrary_空目录_返回零总数()
|
|
||||||
{
|
|
||||||
// Act
|
|
||||||
var result = await _service.ScanAndImportToLibrary(_testDirectory);
|
|
||||||
|
|
||||||
// Assert
|
|
||||||
result.TotalFiles.Should().Be(0);
|
|
||||||
result.SuccessCount.Should().Be(0);
|
|
||||||
result.ErrorCount.Should().Be(0);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public async Task ScanAndImportToLibrary_包含音频文件_成功导入()
|
|
||||||
{
|
|
||||||
// Arrange
|
|
||||||
var wavFile = Path.Combine(_testDirectory, "test.wav");
|
|
||||||
File.WriteAllText(wavFile, "fake wav content");
|
|
||||||
|
|
||||||
// Act
|
|
||||||
var result = await _service.ScanAndImportToLibrary(_testDirectory);
|
|
||||||
|
|
||||||
// Assert
|
|
||||||
result.TotalFiles.Should().Be(1);
|
|
||||||
result.SuccessCount.Should().Be(1);
|
|
||||||
result.ErrorCount.Should().Be(0);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public async Task ScanAndImportToLibrary_多个文件_正确统计()
|
|
||||||
{
|
|
||||||
// Arrange
|
|
||||||
File.WriteAllText(Path.Combine(_testDirectory, "audio1.wav"), "fake");
|
|
||||||
File.WriteAllText(Path.Combine(_testDirectory, "audio2.mp3"), "fake");
|
|
||||||
File.WriteAllText(Path.Combine(_testDirectory, "audio3.flac"), "fake");
|
|
||||||
File.WriteAllText(Path.Combine(_testDirectory, "document.txt"), "not audio");
|
|
||||||
|
|
||||||
// Act
|
|
||||||
var result = await _service.ScanAndImportToLibrary(_testDirectory);
|
|
||||||
|
|
||||||
// Assert
|
|
||||||
result.TotalFiles.Should().Be(3);
|
|
||||||
result.SuccessCount.Should().Be(3);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public async Task ScanAndImportToLibrary_递归扫描子目录()
|
|
||||||
{
|
|
||||||
// Arrange
|
|
||||||
var subDir = Path.Combine(_testDirectory, "subdir");
|
|
||||||
Directory.CreateDirectory(subDir);
|
|
||||||
File.WriteAllText(Path.Combine(_testDirectory, "root.wav"), "fake");
|
|
||||||
File.WriteAllText(Path.Combine(subDir, "sub.wav"), "fake");
|
|
||||||
|
|
||||||
// Act
|
|
||||||
var result = await _service.ScanAndImportToLibrary(_testDirectory);
|
|
||||||
|
|
||||||
// Assert
|
|
||||||
result.TotalFiles.Should().Be(2);
|
|
||||||
result.SuccessCount.Should().Be(2);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void ScanResult_GetSummary_返回正确格式()
|
|
||||||
{
|
|
||||||
// Arrange
|
|
||||||
var result = new ScanResult
|
|
||||||
{
|
|
||||||
TotalFiles = 10,
|
|
||||||
SuccessCount = 8,
|
|
||||||
SkipCount = 1,
|
|
||||||
ErrorCount = 1,
|
|
||||||
};
|
|
||||||
|
|
||||||
// Act
|
|
||||||
var summary = result.GetSummary();
|
|
||||||
|
|
||||||
// Assert
|
|
||||||
summary.Should().Contain("10");
|
|
||||||
summary.Should().Contain("8");
|
|
||||||
summary.Should().Contain("1");
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public async Task ScanAndImportToLibrary_包含不支持格式_只导入音频()
|
|
||||||
{
|
|
||||||
// Arrange
|
|
||||||
File.WriteAllText(Path.Combine(_testDirectory, "audio.wav"), "fake");
|
|
||||||
File.WriteAllText(Path.Combine(_testDirectory, "image.jpg"), "fake");
|
|
||||||
File.WriteAllText(Path.Combine(_testDirectory, "doc.pdf"), "fake");
|
|
||||||
File.WriteAllText(Path.Combine(_testDirectory, "text.txt"), "fake");
|
|
||||||
|
|
||||||
// Act
|
|
||||||
var result = await _service.ScanAndImportToLibrary(_testDirectory);
|
|
||||||
|
|
||||||
// Assert
|
|
||||||
result.TotalFiles.Should().Be(1);
|
|
||||||
result.SuccessCount.Should().Be(1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,273 @@
|
|||||||
|
using System.Globalization;
|
||||||
|
using OCES.Resonance.Core;
|
||||||
|
|
||||||
|
namespace Core.Tests;
|
||||||
|
|
||||||
|
public class AudioMetadataReaderTest
|
||||||
|
{
|
||||||
|
private static readonly string _dataSourceRoot = Path.GetFullPath(
|
||||||
|
Path.Combine(AppContext.BaseDirectory, "../../../../../../data/source"));
|
||||||
|
|
||||||
|
private static string[] GetWavFiles(string vendor)
|
||||||
|
{
|
||||||
|
string dir = Path.Combine(_dataSourceRoot, vendor);
|
||||||
|
return Directory.Exists(dir)
|
||||||
|
? Directory.GetFiles(dir, "*.wav", SearchOption.AllDirectories)
|
||||||
|
: [];
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string? GetAtlTxtPath(string wavPath)
|
||||||
|
{
|
||||||
|
string atlPath = Path.ChangeExtension(wavPath, ".atl.txt");
|
||||||
|
return File.Exists(atlPath) ? atlPath : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 解析 .atl.txt 文件,返回基础属性、元数据字段和额外字段的字典
|
||||||
|
/// </summary>
|
||||||
|
private static (Dictionary<string, string> basic, Dictionary<string, string> meta, Dictionary<string, string> extra)
|
||||||
|
ParseAtlTxt(string atlPath)
|
||||||
|
{
|
||||||
|
var basic = new Dictionary<string, string>();
|
||||||
|
var meta = new Dictionary<string, string>();
|
||||||
|
var extra = new Dictionary<string, string>();
|
||||||
|
|
||||||
|
string[] lines = File.ReadAllLines(atlPath);
|
||||||
|
string section = "";
|
||||||
|
|
||||||
|
foreach (string line in lines)
|
||||||
|
{
|
||||||
|
if (line.StartsWith("=== ATL Track 基础属性 ==="))
|
||||||
|
{
|
||||||
|
section = "basic";
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (line.StartsWith("=== ATL 元数据字段 ==="))
|
||||||
|
{
|
||||||
|
section = "meta";
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (line.StartsWith("=== ATL AdditionalFields"))
|
||||||
|
{
|
||||||
|
section = "extra";
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (line.StartsWith("=== ATL AdditionalImages") || line.StartsWith("=== ATL Chapters"))
|
||||||
|
{
|
||||||
|
section = "";
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (section == "extra" && line.StartsWith("["))
|
||||||
|
{
|
||||||
|
int eqIdx = line.IndexOf("] = ");
|
||||||
|
if (eqIdx > 0)
|
||||||
|
{
|
||||||
|
string key = line[1..eqIdx];
|
||||||
|
string value = line[(eqIdx + 4)..];
|
||||||
|
extra[key] = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if ((section == "basic" || section == "meta") && line.Contains(": "))
|
||||||
|
{
|
||||||
|
int colonIdx = line.IndexOf(": ");
|
||||||
|
if (colonIdx > 0)
|
||||||
|
{
|
||||||
|
string key = line[..colonIdx].Trim();
|
||||||
|
string value = line[(colonIdx + 2)..].Trim();
|
||||||
|
if (section == "basic")
|
||||||
|
basic[key] = value;
|
||||||
|
else
|
||||||
|
meta[key] = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (basic, meta, extra);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ReadMetadata_ShouldThrow_FileNotFoundException()
|
||||||
|
{
|
||||||
|
AudioMetadataReader reader = new();
|
||||||
|
FileInfo nonExistentFile = new(Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString()));
|
||||||
|
|
||||||
|
Assert.Throws<FileNotFoundException>(() => reader.ReadMetadata(nonExistentFile));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Boom 目录的 WAV 文件应包含完整的基础技术参数和富元数据
|
||||||
|
/// </summary>
|
||||||
|
[Fact]
|
||||||
|
public void ReadMetadata_ShouldPopulateAllFields_ForBoomFiles()
|
||||||
|
{
|
||||||
|
string[] wavFiles = GetWavFiles("Boom");
|
||||||
|
if (wavFiles.Length == 0) return;
|
||||||
|
|
||||||
|
AudioMetadataReader reader = new();
|
||||||
|
|
||||||
|
foreach (string wavPath in wavFiles)
|
||||||
|
{
|
||||||
|
FileInfo file = new(wavPath);
|
||||||
|
AudioFileMeta meta = reader.ReadMetadata(file);
|
||||||
|
string? atlPath = GetAtlTxtPath(wavPath);
|
||||||
|
|
||||||
|
// 必备条目:基础技术参数
|
||||||
|
Assert.NotEqual(0.0, meta.Duration);
|
||||||
|
Assert.True(meta.BitDepth > 0);
|
||||||
|
Assert.True(meta.Channels > 0);
|
||||||
|
Assert.True(meta.SampleRate > 0);
|
||||||
|
Assert.Equal("WAV", meta.Type);
|
||||||
|
Assert.Equal(wavPath, meta.Path);
|
||||||
|
Assert.Equal(file.Name, meta.Filename);
|
||||||
|
Assert.Equal(file.Directory?.Name, meta.Folder);
|
||||||
|
Assert.Equal(file.DirectoryName, meta.Directory);
|
||||||
|
Assert.NotEmpty(meta.UniqueId);
|
||||||
|
Assert.NotEmpty(meta.Md5);
|
||||||
|
Assert.Equal(36, meta.UniqueId.Length); // UUID 格式
|
||||||
|
Assert.Equal(32, meta.Md5.Length); // MD5 十六进制格式
|
||||||
|
Assert.NotNull(meta.ChannelLayout);
|
||||||
|
|
||||||
|
// 文件系统时间
|
||||||
|
Assert.Equal(file.LastWriteTime, meta.LastWriteTime);
|
||||||
|
Assert.Equal(file.CreationTime, meta.CreationTime);
|
||||||
|
|
||||||
|
if (atlPath == null)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
var (basic, metaFields, extra) = ParseAtlTxt(atlPath);
|
||||||
|
|
||||||
|
// 基础技术参数与 ATL 一致
|
||||||
|
Assert.Equal(int.Parse(basic["DurationMs"]) / 1000.0, meta.Duration);
|
||||||
|
Assert.Equal(int.Parse(basic["BitDepth"]), meta.BitDepth);
|
||||||
|
Assert.Equal(int.Parse(basic["Channels"]), meta.Channels);
|
||||||
|
Assert.Equal(int.Parse(basic["SampleRate"]), (int)meta.SampleRate);
|
||||||
|
Assert.Equal(basic["ChannelsArrangement"], meta.ChannelLayout);
|
||||||
|
|
||||||
|
// 标准元数据字段
|
||||||
|
Assert.Equal(metaFields["Title"], meta.TrackTitle);
|
||||||
|
Assert.Equal(metaFields["Artist"], meta.Artist);
|
||||||
|
Assert.Equal(metaFields["Album"], meta.CdTitle);
|
||||||
|
Assert.Equal(metaFields["Composer"], meta.Composer);
|
||||||
|
Assert.Equal(metaFields["Comment"], meta.Description);
|
||||||
|
Assert.Equal(metaFields["Publisher"], meta.Publisher);
|
||||||
|
Assert.Equal(metaFields["Copyright"], meta.Copyright);
|
||||||
|
Assert.Equal(metaFields["Group"], meta.Group);
|
||||||
|
|
||||||
|
// Genre: ATL 规范字段可能与 iXML 不一致,仅做非空校验
|
||||||
|
if (!string.IsNullOrEmpty(metaFields["Genre"]))
|
||||||
|
Assert.Equal(metaFields["Genre"], meta.Genre);
|
||||||
|
|
||||||
|
// TrackNumber
|
||||||
|
if (int.TryParse(metaFields["TrackNumber"], out int trackNum) && trackNum > 0)
|
||||||
|
Assert.Equal(trackNum, meta.TrackNumber);
|
||||||
|
|
||||||
|
// DiscNumber
|
||||||
|
if (int.TryParse(metaFields["DiscNumber"], out int discNum) && discNum > 0)
|
||||||
|
Assert.Equal(discNum, meta.DiscNumber);
|
||||||
|
|
||||||
|
// Year
|
||||||
|
if (int.TryParse(metaFields["Year"], out int year) && year > 0)
|
||||||
|
Assert.Equal(year, meta.TrackYear);
|
||||||
|
|
||||||
|
// BPM
|
||||||
|
if (int.TryParse(metaFields["BPM"], out int bpm) && bpm > 0)
|
||||||
|
Assert.Equal((double)bpm, meta.Bpm);
|
||||||
|
|
||||||
|
// iXML / USER 自定义字段(仅 Boom 有)
|
||||||
|
if (extra.TryGetValue("ixml.ASWG.category", out string? cat) || extra.TryGetValue("ixml.USER.CATEGORY", out cat))
|
||||||
|
Assert.Equal(cat, meta.Category);
|
||||||
|
if (extra.TryGetValue("ixml.ASWG.catId", out string? catId) || extra.TryGetValue("ixml.USER.CATID", out catId))
|
||||||
|
Assert.Equal(catId, meta.CatId);
|
||||||
|
if (extra.TryGetValue("ixml.ASWG.library", out string? lib) || extra.TryGetValue("ixml.USER.LIBRARY", out lib))
|
||||||
|
Assert.Equal(lib, meta.Library);
|
||||||
|
if (extra.TryGetValue("ixml.ASWG.notes", out string? notes) || extra.TryGetValue("ixml.USER.NOTES", out notes))
|
||||||
|
Assert.Equal(notes, meta.Notes);
|
||||||
|
if (extra.TryGetValue("ixml.USER.FXNAME", out string? fxName))
|
||||||
|
Assert.Equal(fxName, meta.FxName);
|
||||||
|
if (extra.TryGetValue("ixml.USER.KEYWORDS", out string? keywords))
|
||||||
|
Assert.Equal(keywords, meta.Keywords);
|
||||||
|
if (extra.TryGetValue("ixml.USER.MANUFACTURER", out string? manufacturer))
|
||||||
|
Assert.Equal(manufacturer, meta.Manufacturer);
|
||||||
|
if (extra.TryGetValue("USER.MICPERSPECTIVE", out string? micPersp))
|
||||||
|
Assert.Equal(micPersp, meta.MicPerspective);
|
||||||
|
if (extra.TryGetValue("USER.SUBCATEGORY", out string? subcat) || extra.TryGetValue("ASWG.subCategory", out subcat))
|
||||||
|
Assert.Equal(subcat, meta.Subcategory);
|
||||||
|
if (extra.TryGetValue("USER.RATING", out string? ratingRaw))
|
||||||
|
{
|
||||||
|
if (double.TryParse(ratingRaw, NumberStyles.Float, CultureInfo.InvariantCulture, out double ratingVal))
|
||||||
|
Assert.Equal((uint)Math.Round(ratingVal), meta.Rating);
|
||||||
|
}
|
||||||
|
|
||||||
|
// BWF bext 字段
|
||||||
|
if (extra.TryGetValue("bext.coding_history", out string? codingHistory))
|
||||||
|
Assert.Equal(codingHistory, meta.BwfCodingHistory);
|
||||||
|
if (extra.TryGetValue("bext.originator", out string? originator))
|
||||||
|
Assert.Equal(originator, meta.BwfOriginator);
|
||||||
|
if (extra.TryGetValue("bext.originatorRef", out string? originatorRef))
|
||||||
|
Assert.Equal(originatorRef, meta.BwfOriginatorRef);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Boom 目录的 WAV 文件应包含嵌入的封面图片
|
||||||
|
/// </summary>
|
||||||
|
[Fact]
|
||||||
|
public void ReadMetadata_ShouldContainArtwork_ForBoomFiles()
|
||||||
|
{
|
||||||
|
string[] wavFiles = GetWavFiles("Boom");
|
||||||
|
if (wavFiles.Length == 0) return;
|
||||||
|
|
||||||
|
AudioMetadataReader reader = new();
|
||||||
|
|
||||||
|
foreach (string wavPath in wavFiles)
|
||||||
|
{
|
||||||
|
FileInfo file = new(wavPath);
|
||||||
|
AudioFileMeta meta = reader.ReadMetadata(file);
|
||||||
|
|
||||||
|
Assert.NotNull(meta.Artwork);
|
||||||
|
Assert.NotEmpty(meta.Artwork);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Sound Idea 目录的 WAV 文件元数据最少:仅基础技术参数,
|
||||||
|
/// 无 iXML、BWF 或嵌入图片
|
||||||
|
/// </summary>
|
||||||
|
[Fact]
|
||||||
|
public void ReadMetadata_ShouldHaveNoOptionalMetadata_ForSoundIdeaFiles()
|
||||||
|
{
|
||||||
|
string[] wavFiles = GetWavFiles("Sound Idea");
|
||||||
|
if (wavFiles.Length == 0) return;
|
||||||
|
|
||||||
|
AudioMetadataReader reader = new();
|
||||||
|
|
||||||
|
foreach (string wavPath in wavFiles)
|
||||||
|
{
|
||||||
|
FileInfo file = new(wavPath);
|
||||||
|
AudioFileMeta meta = reader.ReadMetadata(file);
|
||||||
|
|
||||||
|
// 必备条目正确
|
||||||
|
Assert.NotEqual(0.0, meta.Duration);
|
||||||
|
Assert.True(meta.BitDepth > 0);
|
||||||
|
Assert.True(meta.Channels > 0);
|
||||||
|
Assert.True(meta.SampleRate > 0);
|
||||||
|
Assert.Equal("WAV", meta.Type);
|
||||||
|
Assert.Equal(wavPath, meta.Path);
|
||||||
|
Assert.Equal(file.Name, meta.Filename);
|
||||||
|
Assert.NotEmpty(meta.Md5);
|
||||||
|
|
||||||
|
// 可选字段为空或默认值(Sound Idea 无附加字段)
|
||||||
|
Assert.Null(meta.Category);
|
||||||
|
Assert.Null(meta.Subcategory);
|
||||||
|
Assert.Null(meta.CatId);
|
||||||
|
Assert.Null(meta.Library);
|
||||||
|
Assert.Null(meta.FxName);
|
||||||
|
Assert.Null(meta.Keywords);
|
||||||
|
Assert.Null(meta.BwfCodingHistory);
|
||||||
|
Assert.Null(meta.BwfOriginator);
|
||||||
|
Assert.Null(meta.BwfOriginatorRef);
|
||||||
|
Assert.Null(meta.Artwork);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,130 +0,0 @@
|
|||||||
using FluentAssertions;
|
|
||||||
|
|
||||||
namespace OCES.Resonance.Core.Tests;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// AudioMetadataReader 单元测试
|
|
||||||
/// </summary>
|
|
||||||
public class AudioMetadataReaderTests
|
|
||||||
{
|
|
||||||
readonly AudioMetadataReader _reader;
|
|
||||||
readonly string _testDirectory;
|
|
||||||
|
|
||||||
public AudioMetadataReaderTests()
|
|
||||||
{
|
|
||||||
this._reader = new AudioMetadataReader();
|
|
||||||
this._testDirectory = Path.Combine(Path.GetTempPath(), $"MetadataReaderTest_{Guid.NewGuid()}");
|
|
||||||
Directory.CreateDirectory(this._testDirectory);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void ReadMetadata_文件不存在_抛出FileNotFoundException()
|
|
||||||
{
|
|
||||||
// Arrange
|
|
||||||
string nonExistentFile = Path.Combine(this._testDirectory, "non_existent.wav");
|
|
||||||
|
|
||||||
// Act & Assert
|
|
||||||
Func<AudioFileMeta> act = () => this._reader.ReadMetadata(nonExistentFile);
|
|
||||||
act.Should().Throw<FileNotFoundException>();
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void ReadMetadata_空文件_仍然返回元数据()
|
|
||||||
{
|
|
||||||
// Arrange
|
|
||||||
string emptyFile = Path.Combine(this._testDirectory, "empty.wav");
|
|
||||||
File.WriteAllText(emptyFile, "");
|
|
||||||
|
|
||||||
// Act
|
|
||||||
AudioFileMeta result = this._reader.ReadMetadata(emptyFile);
|
|
||||||
|
|
||||||
// Assert
|
|
||||||
result.Should().NotBeNull();
|
|
||||||
result.Filename.Should().Be("empty.wav");
|
|
||||||
result.Path.Should().Be(Path.GetFullPath(emptyFile));
|
|
||||||
result.Md5.Should().NotBeNullOrEmpty();
|
|
||||||
result.UniqueId.Should().NotBeNullOrEmpty();
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void ReadMetadata_生成唯一ID_每次调用都不同()
|
|
||||||
{
|
|
||||||
// Arrange
|
|
||||||
string testFile = Path.Combine(this._testDirectory, "test.wav");
|
|
||||||
File.WriteAllText(testFile, "fake audio content");
|
|
||||||
|
|
||||||
// Act
|
|
||||||
AudioFileMeta result1 = this._reader.ReadMetadata(testFile);
|
|
||||||
AudioFileMeta result2 = this._reader.ReadMetadata(testFile);
|
|
||||||
|
|
||||||
// Assert
|
|
||||||
result1.UniqueId.Should().NotBe(result2.UniqueId);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void ReadMetadata_相同文件_MD5相同()
|
|
||||||
{
|
|
||||||
// Arrange
|
|
||||||
var testFile = Path.Combine(this._testDirectory, "test.wav");
|
|
||||||
File.WriteAllText(testFile, "fake audio content");
|
|
||||||
|
|
||||||
// Act
|
|
||||||
var result1 = this._reader.ReadMetadata(testFile);
|
|
||||||
var result2 = this._reader.ReadMetadata(testFile);
|
|
||||||
|
|
||||||
// Assert
|
|
||||||
result1.Md5.Should().Be(result2.Md5);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Theory]
|
|
||||||
[InlineData(".wav", "WAV")]
|
|
||||||
[InlineData(".mp3", "MP3")]
|
|
||||||
[InlineData(".flac", "FLAC")]
|
|
||||||
[InlineData(".aiff", "AIFF")]
|
|
||||||
public void ReadMetadata_不同格式_正确设置Type字段(string extension, string expectedType)
|
|
||||||
{
|
|
||||||
// Arrange
|
|
||||||
string testFile = Path.Combine(this._testDirectory, $"test{extension}");
|
|
||||||
File.WriteAllText(testFile, "fake audio content");
|
|
||||||
|
|
||||||
// Act
|
|
||||||
AudioFileMeta result = this._reader.ReadMetadata(testFile);
|
|
||||||
|
|
||||||
// Assert
|
|
||||||
result.Type.Should().Be(expectedType);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void ReadMetadata_提取文件夹信息()
|
|
||||||
{
|
|
||||||
// Arrange
|
|
||||||
string subDir = Path.Combine(this._testDirectory, "SubFolder");
|
|
||||||
Directory.CreateDirectory(subDir);
|
|
||||||
string testFile = Path.Combine(subDir, "test.wav");
|
|
||||||
File.WriteAllText(testFile, "fake audio content");
|
|
||||||
|
|
||||||
// Act
|
|
||||||
AudioFileMeta result = this._reader.ReadMetadata(testFile);
|
|
||||||
|
|
||||||
// Assert
|
|
||||||
result.Folder.Should().Be("SubFolder");
|
|
||||||
result.Directory.Should().Be(subDir);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void ReadMetadata_设置DateAdded为当前时间()
|
|
||||||
{
|
|
||||||
// Arrange
|
|
||||||
string testFile = Path.Combine(this._testDirectory, "test.wav");
|
|
||||||
File.WriteAllText(testFile, "fake audio content");
|
|
||||||
DateTime beforeTime = DateTime.Now;
|
|
||||||
|
|
||||||
// Act
|
|
||||||
AudioFileMeta result = this._reader.ReadMetadata(testFile);
|
|
||||||
DateTime afterTime = DateTime.Now;
|
|
||||||
|
|
||||||
// Assert
|
|
||||||
result.DateAdded.Should().BeOnOrAfter(beforeTime);
|
|
||||||
result.DateAdded.Should().BeOnOrBefore(afterTime);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Regular → Executable
+1
@@ -10,6 +10,7 @@
|
|||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="coverlet.collector" Version="6.0.4" />
|
<PackageReference Include="coverlet.collector" Version="6.0.4" />
|
||||||
<PackageReference Include="FluentAssertions" Version="8.9.0" />
|
<PackageReference Include="FluentAssertions" Version="8.9.0" />
|
||||||
|
<PackageReference Include="JetBrains.Annotations" Version="2025.2.4" />
|
||||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.14.1" />
|
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.14.1" />
|
||||||
<PackageReference Include="Moq" Version="4.20.72" />
|
<PackageReference Include="Moq" Version="4.20.72" />
|
||||||
<PackageReference Include="xunit" Version="2.9.3" />
|
<PackageReference Include="xunit" Version="2.9.3" />
|
||||||
|
|||||||
@@ -1,177 +0,0 @@
|
|||||||
using System.Data.SQLite;
|
|
||||||
using FluentAssertions;
|
|
||||||
|
|
||||||
namespace OCES.Resonance.Core.Tests;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Database 单元测试
|
|
||||||
/// </summary>
|
|
||||||
public class DatabaseTests : IDisposable
|
|
||||||
{
|
|
||||||
readonly string _testDbName;
|
|
||||||
readonly string _testDbPath;
|
|
||||||
|
|
||||||
public DatabaseTests()
|
|
||||||
{
|
|
||||||
_testDbName = $"test_{Guid.NewGuid():N}";
|
|
||||||
_testDbPath = $"{_testDbName}.db";
|
|
||||||
}
|
|
||||||
|
|
||||||
public void Dispose()
|
|
||||||
{
|
|
||||||
if (File.Exists(_testDbPath))
|
|
||||||
{
|
|
||||||
File.Delete(_testDbPath);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void GetConnection_返回有效连接()
|
|
||||||
{
|
|
||||||
// Act
|
|
||||||
using var connection = Database.GetConnection(_testDbName);
|
|
||||||
|
|
||||||
// Assert
|
|
||||||
connection.Should().NotBeNull();
|
|
||||||
connection.Should().BeOfType<SQLiteConnection>();
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void InitializeDatabase_创建表结构()
|
|
||||||
{
|
|
||||||
// Act
|
|
||||||
Database.InitializeDatabase();
|
|
||||||
|
|
||||||
// Assert - 验证表已创建(通过尝试查询)
|
|
||||||
using var connection = Database.GetConnection();
|
|
||||||
connection.Open();
|
|
||||||
var command = connection.CreateCommand();
|
|
||||||
command.CommandText = "SELECT COUNT(*) FROM audio_files;";
|
|
||||||
var count = command.ExecuteScalar();
|
|
||||||
count.Should().NotBeNull();
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void AddEntry_添加有效记录_返回True()
|
|
||||||
{
|
|
||||||
// Arrange
|
|
||||||
Database.InitializeDatabase();
|
|
||||||
var meta = CreateTestMeta();
|
|
||||||
|
|
||||||
// Act
|
|
||||||
var result = Database.AddEntry(meta);
|
|
||||||
|
|
||||||
// Assert
|
|
||||||
result.Should().BeTrue();
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void AddEntry_添加记录后_可以查询到()
|
|
||||||
{
|
|
||||||
// Arrange
|
|
||||||
Database.InitializeDatabase();
|
|
||||||
var meta = CreateTestMeta();
|
|
||||||
|
|
||||||
// Act
|
|
||||||
Database.AddEntry(meta);
|
|
||||||
|
|
||||||
// Assert
|
|
||||||
using var connection = Database.GetConnection();
|
|
||||||
connection.Open();
|
|
||||||
var command = connection.CreateCommand();
|
|
||||||
command.CommandText = "SELECT filename FROM audio_files WHERE unique_id = @id;";
|
|
||||||
var param = command.CreateParameter();
|
|
||||||
param.ParameterName = "@id";
|
|
||||||
param.Value = meta.UniqueId;
|
|
||||||
command.Parameters.Add(param);
|
|
||||||
var filename = command.ExecuteScalar() as string;
|
|
||||||
filename.Should().Be(meta.Filename);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void AddEntries_批量添加_返回正确数量()
|
|
||||||
{
|
|
||||||
// Arrange
|
|
||||||
Database.InitializeDatabase();
|
|
||||||
var entries = new List<AudioFileMeta>
|
|
||||||
{
|
|
||||||
CreateTestMeta("file1.wav"),
|
|
||||||
CreateTestMeta("file2.wav"),
|
|
||||||
CreateTestMeta("file3.wav"),
|
|
||||||
};
|
|
||||||
|
|
||||||
// Act
|
|
||||||
var count = Database.AddEntries(entries);
|
|
||||||
|
|
||||||
// Assert
|
|
||||||
count.Should().Be(3);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void EntryExists_记录存在_返回True()
|
|
||||||
{
|
|
||||||
// Arrange
|
|
||||||
Database.InitializeDatabase();
|
|
||||||
var meta = CreateTestMeta();
|
|
||||||
Database.AddEntry(meta);
|
|
||||||
|
|
||||||
// Act
|
|
||||||
var exists = Database.EntryExists(meta.Md5, meta.Path);
|
|
||||||
|
|
||||||
// Assert
|
|
||||||
exists.Should().BeTrue();
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void EntryExists_记录不存在_返回False()
|
|
||||||
{
|
|
||||||
// Arrange
|
|
||||||
Database.InitializeDatabase();
|
|
||||||
|
|
||||||
// Act
|
|
||||||
var exists = Database.EntryExists("non_existent_md5", "/non/existent/path.wav");
|
|
||||||
|
|
||||||
// Assert
|
|
||||||
exists.Should().BeFalse();
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void AddEntry_重复UniqueId_第二次添加失败()
|
|
||||||
{
|
|
||||||
// Arrange
|
|
||||||
Database.InitializeDatabase();
|
|
||||||
var meta1 = CreateTestMeta();
|
|
||||||
var meta2 = CreateTestMeta();
|
|
||||||
meta2.UniqueId = meta1.UniqueId; // 使用相同的 UniqueId
|
|
||||||
Database.AddEntry(meta1);
|
|
||||||
|
|
||||||
// Act
|
|
||||||
var result = Database.AddEntry(meta2);
|
|
||||||
|
|
||||||
// Assert
|
|
||||||
result.Should().BeFalse();
|
|
||||||
}
|
|
||||||
|
|
||||||
static AudioFileMeta CreateTestMeta(string filename = "test.wav")
|
|
||||||
{
|
|
||||||
return new AudioFileMeta
|
|
||||||
{
|
|
||||||
Id = 0, // 自增 ID,由数据库生成
|
|
||||||
UniqueId = Guid.NewGuid().ToString("N"),
|
|
||||||
Md5 = Guid.NewGuid().ToString("N"),
|
|
||||||
Path = $"/test/path/{filename}",
|
|
||||||
Filename = filename,
|
|
||||||
Folder = "test",
|
|
||||||
Directory = "/test/path",
|
|
||||||
Duration = 10.5,
|
|
||||||
TotalSamples = 441000,
|
|
||||||
BitDepth = 24,
|
|
||||||
Channels = 2,
|
|
||||||
SampleRate = 44100,
|
|
||||||
Type = "WAV",
|
|
||||||
DateAdded = DateTime.Now,
|
|
||||||
OriginalModificationDate = DateTime.Now.AddDays(-1),
|
|
||||||
OriginationTime = DateTime.Now.AddDays(-1),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Regular → Executable
+12
-7
@@ -47,13 +47,16 @@ public class AudioFileMeta
|
|||||||
public required DateTime DateAdded { get; set; }
|
public required DateTime DateAdded { get; set; }
|
||||||
|
|
||||||
/// <summary>修改日期,音频创建或录制的日期</summary>
|
/// <summary>修改日期,音频创建或录制的日期</summary>
|
||||||
public required DateTime OriginalModificationDate { get; set; }
|
public required DateTime LastWriteTime { get; set; }
|
||||||
|
|
||||||
/// <summary>创建时间,音频文件的原始创建时间</summary>
|
/// <summary>创建时间,音频文件的原始创建时间</summary>
|
||||||
public required DateTime OriginationTime { get; set; }
|
public required DateTime CreationTime { get; set; }
|
||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
|
/// <summary>音效名称,不含文件前缀的简短效果名(如 "Air Pressure Release Wet Short 01")</summary>
|
||||||
|
public string? FxName { get; set; }
|
||||||
|
|
||||||
///<summary>声道配置</summary>
|
///<summary>声道配置</summary>
|
||||||
public string? ChannelLayout { get; set; }
|
public string? ChannelLayout { get; set; }
|
||||||
|
|
||||||
@@ -122,9 +125,10 @@ public class AudioFileMeta
|
|||||||
/// <summary>来源引用,原始来源的参考编号</summary>
|
/// <summary>来源引用,原始来源的参考编号</summary>
|
||||||
public string? BwfOriginatorRef { get; set; }
|
public string? BwfOriginatorRef { get; set; }
|
||||||
|
|
||||||
public DateTime? BwfDate { get; set; }
|
/// <summary>编码历史,BWF格式的编码历史记录</summary>
|
||||||
|
public string? BwfCodingHistory { get; set; }
|
||||||
|
|
||||||
public string? BwfDescription { get; set; }
|
public byte[]? BwfUmid { get; set; }
|
||||||
|
|
||||||
/// <summary>项目名称,所属制作项目</summary>
|
/// <summary>项目名称,所属制作项目</summary>
|
||||||
public string? ProjectName { get; set; }
|
public string? ProjectName { get; set; }
|
||||||
@@ -188,13 +192,14 @@ public class AudioFileMeta
|
|||||||
/// <summary>版权,版权声明信息</summary>
|
/// <summary>版权,版权声明信息</summary>
|
||||||
public string? Copyright { get; set; }
|
public string? Copyright { get; set; }
|
||||||
|
|
||||||
/// <summary>编码历史,BWF格式的编码历史记录</summary>
|
|
||||||
public string? CodingHistory { get; set; }
|
|
||||||
|
|
||||||
/// <summary>麦克风,录音使用的麦克风型号或设置</summary>
|
/// <summary>麦克风,录音使用的麦克风型号或设置</summary>
|
||||||
public string? Microphone { get; set; }
|
public string? Microphone { get; set; }
|
||||||
|
|
||||||
public string? MicPerspective { get; set; }
|
public string? MicPerspective { get; set; }
|
||||||
|
|
||||||
|
public byte[]? Artwork { get; set; }
|
||||||
|
|
||||||
|
public byte[]? Waveform { get; set; }
|
||||||
|
|
||||||
// 用户自定义字段(共8个)
|
// 用户自定义字段(共8个)
|
||||||
|
|
||||||
|
|||||||
Regular → Executable
+2
-2
@@ -38,8 +38,8 @@ public class AudioFileMetaValidator
|
|||||||
|
|
||||||
// 验证日期字段
|
// 验证日期字段
|
||||||
ValidateRequiredField(meta.DateAdded, "DateAdded", errors);
|
ValidateRequiredField(meta.DateAdded, "DateAdded", errors);
|
||||||
ValidateRequiredField(meta.OriginalModificationDate, "OriginalModificationDate", errors);
|
ValidateRequiredField(meta.LastWriteTime, "LastWriteTime", errors);
|
||||||
ValidateRequiredField(meta.OriginationTime, "OriginationTime", errors);
|
ValidateRequiredField(meta.CreationTime, "CreationTime", errors);
|
||||||
|
|
||||||
// 验证文件路径是否存在
|
// 验证文件路径是否存在
|
||||||
if (!File.Exists(meta.Path))
|
if (!File.Exists(meta.Path))
|
||||||
|
|||||||
Regular → Executable
Regular → Executable
+64
-36
@@ -1,3 +1,4 @@
|
|||||||
|
using System.Globalization;
|
||||||
using System.Security.Cryptography;
|
using System.Security.Cryptography;
|
||||||
using ATL;
|
using ATL;
|
||||||
|
|
||||||
@@ -27,7 +28,7 @@ public class AudioMetadataReader
|
|||||||
{
|
{
|
||||||
// 必备条目
|
// 必备条目
|
||||||
Id = 0, // 自增 ID,由数据库生成
|
Id = 0, // 自增 ID,由数据库生成
|
||||||
UniqueId = GenerateUuid(),
|
UniqueId = Guid.NewGuid().ToString(),
|
||||||
Md5 = CalculateMd5(audioFile.FullName),
|
Md5 = CalculateMd5(audioFile.FullName),
|
||||||
Path = audioFile.FullName,
|
Path = audioFile.FullName,
|
||||||
Filename = audioFile.Name,
|
Filename = audioFile.Name,
|
||||||
@@ -39,52 +40,88 @@ public class AudioMetadataReader
|
|||||||
SampleRate = track.SampleRate,
|
SampleRate = track.SampleRate,
|
||||||
Type = audioFile.Extension.TrimStart('.').ToUpperInvariant(),
|
Type = audioFile.Extension.TrimStart('.').ToUpperInvariant(),
|
||||||
DateAdded = DateTime.Now,
|
DateAdded = DateTime.Now,
|
||||||
OriginalModificationDate = audioFile.LastWriteTime,
|
LastWriteTime = audioFile.LastWriteTime,
|
||||||
OriginationTime = audioFile.CreationTime,
|
CreationTime = audioFile.CreationTime,
|
||||||
|
|
||||||
// 可选条目
|
// 可选条目
|
||||||
Bpm = track.BPM > 0 ? track.BPM : null,
|
Artist = track.Artist,
|
||||||
Description = track.Description,
|
Artwork = track.EmbeddedPictures?.FirstOrDefault()?.PictureData,
|
||||||
Genre = track.Genre,
|
Bpm = track.BPM > 0 ? track.BPM : null,
|
||||||
Artist = track.Artist,
|
Category = GetField(track, "ixml.ASWG.category", "ixml.USER.CATEGORY"),
|
||||||
Composer = track.Composer,
|
CatId = GetField(track, "ixml.ASWG.catId", "ixml.USER.CATID"),
|
||||||
Comments = track.Comment,
|
CdTitle = track.Album,
|
||||||
Publisher = track.Publisher,
|
ChannelLayout = track.ChannelsArrangement?.ToString() ?? string.Empty,
|
||||||
Copyright = track.Copyright,
|
Composer = track.Composer,
|
||||||
CdTitle = track.Album,
|
Copyright = track.Copyright,
|
||||||
DiscNumber = track.DiscNumber,
|
Description = track.Comment,
|
||||||
TrackTitle = track.Title,
|
DiscNumber = track.DiscNumber,
|
||||||
ReleaseDate = track.OriginalReleaseDate,
|
FxName = GetField(track, "ixml.USER.FXNAME"),
|
||||||
TrackYear = track.Year,
|
Genre = track.Genre,
|
||||||
TrackNumber = track.TrackNumber,
|
Group = track.Group,
|
||||||
Group = track.Group,
|
Keywords = GetField(track, "ixml.USER.KEYWORDS"),
|
||||||
|
Library = GetField(track, "ixml.ASWG.library", "ixml.USER.LIBRARY"),
|
||||||
|
Manufacturer = GetField(track, "ixml.USER.MANUFACTURER"),
|
||||||
|
MicPerspective = GetField(track, "USER.MICPERSPECTIVE"),
|
||||||
|
Notes = GetField(track, "ixml.ASWG.notes", "ixml.USER.NOTES"),
|
||||||
|
Publisher = track.Publisher,
|
||||||
|
Rating = ParseRating(GetField(track, "USER.RATING")),
|
||||||
|
ReleaseDate = track.OriginalReleaseDate,
|
||||||
|
Subcategory = GetField(track, "USER.SUBCATEGORY", "ASWG.subCategory"),
|
||||||
|
TrackNumber = track.TrackNumber,
|
||||||
|
TrackTitle = track.Title,
|
||||||
|
TrackYear = track.Year,
|
||||||
|
|
||||||
|
|
||||||
// BWF 特定字段 (通过附加数据获取)
|
// BWF 特定字段 (通过附加数据获取)
|
||||||
CodingHistory = GetBwfField(track, "CodingHistory"),
|
BwfCodingHistory = GetField(track, "bext.coding_history"),
|
||||||
BwfOriginator = GetBwfField(track, "BwfOriginator"),
|
BwfOriginator = GetField(track, "bext.originator"),
|
||||||
BwfOriginatorRef = GetBwfField(track, "BwfOriginatorRef"),
|
BwfOriginatorRef = GetField(track, "bext.originatorRef"),
|
||||||
};
|
};
|
||||||
|
|
||||||
return meta;
|
return meta;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 获取 BWF 字段值
|
/// 从 AdditionalFields 中按优先级获取字段值。
|
||||||
|
/// 返回第一个存在且非空的 value,全部未命中返回 null。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private static string? GetBwfField(Track track, string fieldName)
|
static string? GetField(Track track, params string[] fieldNames)
|
||||||
{
|
{
|
||||||
try
|
if (track.AdditionalFields is null) return null;
|
||||||
|
|
||||||
|
foreach (string name in fieldNames)
|
||||||
{
|
{
|
||||||
if (track.AdditionalFields != null && track.AdditionalFields.TryGetValue(fieldName, out string? field))
|
if (track.AdditionalFields.TryGetValue(name, out string? field)
|
||||||
|
&& !string.IsNullOrEmpty(field))
|
||||||
{
|
{
|
||||||
return field;
|
return field;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
catch
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 将 ATL AdditionalFields 中的 Rating 字符串解析为 uint?
|
||||||
|
/// 支持整数("3")和浮点("0.000000")两种格式
|
||||||
|
/// 输入为空或无法解析时返回 null
|
||||||
|
/// </summary>
|
||||||
|
static uint? ParseRating(string? rawValue)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(rawValue))
|
||||||
|
return null;
|
||||||
|
|
||||||
|
if (double.TryParse(rawValue,
|
||||||
|
NumberStyles.Float | NumberStyles.AllowThousands,
|
||||||
|
CultureInfo.InvariantCulture,
|
||||||
|
out double value))
|
||||||
{
|
{
|
||||||
// 忽略 BWF 字段读取错误
|
if (value is < 0 or > uint.MaxValue)
|
||||||
|
return null;
|
||||||
|
|
||||||
|
return (uint)Math.Round(value);
|
||||||
}
|
}
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -101,13 +138,4 @@ public class AudioMetadataReader
|
|||||||
byte[] hash = md5.ComputeHash(stream);
|
byte[] hash = md5.ComputeHash(stream);
|
||||||
return Convert.ToHexStringLower(hash);
|
return Convert.ToHexStringLower(hash);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 生成 UUID
|
|
||||||
/// </summary>
|
|
||||||
/// <returns>UUID 字符串</returns>
|
|
||||||
static string GenerateUuid()
|
|
||||||
{
|
|
||||||
return Guid.NewGuid().ToString();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
Regular → Executable
+1
-1
@@ -9,7 +9,7 @@
|
|||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="Dapper" Version="2.1.72" />
|
<PackageReference Include="Dapper" Version="2.1.72" />
|
||||||
<PackageReference Include="System.Data.SQLite" Version="2.0.3" />
|
<PackageReference Include="System.Data.SQLite" Version="2.0.3" />
|
||||||
<PackageReference Include="z440.atl.core" Version="7.12.0" />
|
<PackageReference Include="z440.atl.core" Version="7.13.0" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
</Project>
|
</Project>
|
||||||
|
|||||||
Regular → Executable
+5
-5
@@ -6,7 +6,7 @@ namespace OCES.Resonance.Core;
|
|||||||
|
|
||||||
public static class Database
|
public static class Database
|
||||||
{
|
{
|
||||||
static readonly string DefaultConnectionString = "Data Source=default.db;Version=3;";
|
static readonly string DefaultConnectionString = "Data Source=default.rdb;Version=3;";
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 获取数据库连接
|
/// 获取数据库连接
|
||||||
@@ -15,7 +15,7 @@ public static class Database
|
|||||||
/// <returns>数据库连接</returns>
|
/// <returns>数据库连接</returns>
|
||||||
public static IDbConnection GetConnection(string dbName = "default")
|
public static IDbConnection GetConnection(string dbName = "default")
|
||||||
{
|
{
|
||||||
string connectionString = $"Data Source={dbName}.db;Version=3;";
|
string connectionString = $"Data Source={dbName}.rdb;Version=3;";
|
||||||
return new SQLiteConnection(connectionString);
|
return new SQLiteConnection(connectionString);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -28,7 +28,7 @@ public static class Database
|
|||||||
connection.Open();
|
connection.Open();
|
||||||
|
|
||||||
const string sql = @"
|
const string sql = @"
|
||||||
CREATE TABLE IF NOT EXISTS sounds (
|
CREATE TABLE IF NOT EXISTS audio_files (
|
||||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
unique_id TEXT NOT NULL UNIQUE,
|
unique_id TEXT NOT NULL UNIQUE,
|
||||||
short_id TEXT,
|
short_id TEXT,
|
||||||
@@ -144,7 +144,7 @@ public static class Database
|
|||||||
@BwfOriginator, @BwfOriginatorRef, @ProjectName, @Library, @CdTitle,
|
@BwfOriginator, @BwfOriginatorRef, @ProjectName, @Library, @CdTitle,
|
||||||
@TrackTitle, @Episode, @Scene, @Take, @Tape, @CueNumber, @SyncPoint,
|
@TrackTitle, @Episode, @Scene, @Take, @Tape, @CueNumber, @SyncPoint,
|
||||||
@ReleaseDate, @TrackYear, @IsEdited, @IsSplit, @Location, @Group,
|
@ReleaseDate, @TrackYear, @IsEdited, @IsSplit, @Location, @Group,
|
||||||
@Markers, @Comments, @Notes, @Copyright, @CodingHistory, @Microphone,
|
@Markers, @Comments, @Notes, @Copyright, @BwfCodingHistory, @Microphone,
|
||||||
@MicPerspective, @User1, @User2, @User3, @User4, @User5, @User6, @User7, @User8
|
@MicPerspective, @User1, @User2, @User3, @User4, @User5, @User6, @User7, @User8
|
||||||
);
|
);
|
||||||
";
|
";
|
||||||
@@ -195,7 +195,7 @@ public static class Database
|
|||||||
@BwfOriginator, @BwfOriginatorRef, @ProjectName, @Library, @CdTitle,
|
@BwfOriginator, @BwfOriginatorRef, @ProjectName, @Library, @CdTitle,
|
||||||
@TrackTitle, @Episode, @Scene, @Take, @Tape, @CueNumber, @SyncPoint,
|
@TrackTitle, @Episode, @Scene, @Take, @Tape, @CueNumber, @SyncPoint,
|
||||||
@ReleaseDate, @TrackYear, @IsEdited, @IsSplit, @Location, @Group,
|
@ReleaseDate, @TrackYear, @IsEdited, @IsSplit, @Location, @Group,
|
||||||
@Markers, @Comments, @Notes, @Copyright, @CodingHistory, @Microphone,
|
@Markers, @Comments, @Notes, @Copyright, @BwfCodingHistory, @Microphone,
|
||||||
@MicPerspective, @User1, @User2, @User3, @User4, @User5, @User6, @User7, @User8
|
@MicPerspective, @User1, @User2, @User3, @User4, @User5, @User6, @User7, @User8
|
||||||
);
|
);
|
||||||
";
|
";
|
||||||
|
|||||||
Regular → Executable
+14
@@ -4,6 +4,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Core", "Core\Core.csproj",
|
|||||||
EndProject
|
EndProject
|
||||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Core.Tests", "Core.Tests\Core.Tests.csproj", "{24F92458-FB39-44BE-A32F-41275183AF1B}"
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Core.Tests", "Core.Tests\Core.Tests.csproj", "{24F92458-FB39-44BE-A32F-41275183AF1B}"
|
||||||
EndProject
|
EndProject
|
||||||
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AtlFieldExtractor", "AtlFieldExtractor\AtlFieldExtractor.csproj", "{17699013-3F4A-408F-84CE-FD4178B1699D}"
|
||||||
|
EndProject
|
||||||
Global
|
Global
|
||||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||||
Debug|Any CPU = Debug|Any CPU
|
Debug|Any CPU = Debug|Any CPU
|
||||||
@@ -38,6 +40,18 @@ Global
|
|||||||
{24F92458-FB39-44BE-A32F-41275183AF1B}.Release|x64.Build.0 = Release|Any CPU
|
{24F92458-FB39-44BE-A32F-41275183AF1B}.Release|x64.Build.0 = Release|Any CPU
|
||||||
{24F92458-FB39-44BE-A32F-41275183AF1B}.Release|x86.ActiveCfg = Release|Any CPU
|
{24F92458-FB39-44BE-A32F-41275183AF1B}.Release|x86.ActiveCfg = Release|Any CPU
|
||||||
{24F92458-FB39-44BE-A32F-41275183AF1B}.Release|x86.Build.0 = Release|Any CPU
|
{24F92458-FB39-44BE-A32F-41275183AF1B}.Release|x86.Build.0 = Release|Any CPU
|
||||||
|
{17699013-3F4A-408F-84CE-FD4178B1699D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
|
{17699013-3F4A-408F-84CE-FD4178B1699D}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{17699013-3F4A-408F-84CE-FD4178B1699D}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||||
|
{17699013-3F4A-408F-84CE-FD4178B1699D}.Debug|x64.Build.0 = Debug|Any CPU
|
||||||
|
{17699013-3F4A-408F-84CE-FD4178B1699D}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||||
|
{17699013-3F4A-408F-84CE-FD4178B1699D}.Debug|x86.Build.0 = Debug|Any CPU
|
||||||
|
{17699013-3F4A-408F-84CE-FD4178B1699D}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
|
{17699013-3F4A-408F-84CE-FD4178B1699D}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
{17699013-3F4A-408F-84CE-FD4178B1699D}.Release|x64.ActiveCfg = Release|Any CPU
|
||||||
|
{17699013-3F4A-408F-84CE-FD4178B1699D}.Release|x64.Build.0 = Release|Any CPU
|
||||||
|
{17699013-3F4A-408F-84CE-FD4178B1699D}.Release|x86.ActiveCfg = Release|Any CPU
|
||||||
|
{17699013-3F4A-408F-84CE-FD4178B1699D}.Release|x86.Build.0 = Release|Any CPU
|
||||||
EndGlobalSection
|
EndGlobalSection
|
||||||
GlobalSection(SolutionProperties) = preSolution
|
GlobalSection(SolutionProperties) = preSolution
|
||||||
HideSolutionNode = FALSE
|
HideSolutionNode = FALSE
|
||||||
|
|||||||
Regular → Executable
Regular → Executable
+6
@@ -1,11 +1,17 @@
|
|||||||
<wpf:ResourceDictionary xml:space="preserve" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:s="clr-namespace:System;assembly=mscorlib" xmlns:ss="urn:shemas-jetbrains-com:settings-storage-xaml" xmlns:wpf="http://schemas.microsoft.com/winfx/2006/xaml/presentation">
|
<wpf:ResourceDictionary xml:space="preserve" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:s="clr-namespace:System;assembly=mscorlib" xmlns:ss="urn:shemas-jetbrains-com:settings-storage-xaml" xmlns:wpf="http://schemas.microsoft.com/winfx/2006/xaml/presentation">
|
||||||
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AFormat_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003F_002E_002E_003F_002E_002E_003FLibrary_003FApplication_0020Support_003FJetBrains_003FRider2026_002E1_003Fresharper_002Dhost_003FSourcesCache_003Fe2d95a1d7b987bbb7785b2ad7e2e3e29075ba79cdf7619dd26c10d266c5_003FFormat_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AFormat_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003F_002E_002E_003F_002E_002E_003FLibrary_003FApplication_0020Support_003FJetBrains_003FRider2026_002E1_003Fresharper_002Dhost_003FSourcesCache_003Fe2d95a1d7b987bbb7785b2ad7e2e3e29075ba79cdf7619dd26c10d266c5_003FFormat_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
||||||
|
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003ATrack_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003F_002E_002E_003F_002E_002E_003FLibrary_003FApplication_0020Support_003FJetBrains_003FRider2026_002E1_003Fresharper_002Dhost_003FSourcesCache_003F872a20b9878a835418db772627eab53c29e04a65284443b02d35345255649a3a_003FTrack_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
||||||
<s:String x:Key="/Default/Environment/AssemblyExplorer/XmlDocument/@EntryValue"><AssemblyExplorer>
|
<s:String x:Key="/Default/Environment/AssemblyExplorer/XmlDocument/@EntryValue"><AssemblyExplorer>
|
||||||
<Assembly Path="/Users/oliver/.nuget/packages/fluentassertions/8.9.0/lib/net6.0/FluentAssertions.dll" />
|
<Assembly Path="/Users/oliver/.nuget/packages/fluentassertions/8.9.0/lib/net6.0/FluentAssertions.dll" />
|
||||||
</AssemblyExplorer></s:String>
|
</AssemblyExplorer></s:String>
|
||||||
|
<s:String x:Key="/Default/Environment/UnitTesting/CreateUnitTestDialog/TestProjectMapping/=855F941D_002D5DA0_002D479A_002DBB79_002D5C7CE72CD34B/@EntryIndexedValue">24F92458-FB39-44BE-A32F-41275183AF1B</s:String>
|
||||||
|
<s:String x:Key="/Default/Environment/UnitTesting/CreateUnitTestDialog/TestTemplateMapping/=xUnit/@EntryIndexedValue">33ff99d8-b1ad-4ed5-a4fd-376b97c2c841</s:String>
|
||||||
<s:String x:Key="/Default/Environment/UnitTesting/UnitTestSessionStore/Sessions/=5d87cd84_002Da775_002D4e8b_002D859b_002D8460b06eac04/@EntryIndexedValue"><SessionState ContinuousTestingMode="0" IsActive="True" Name="IsSupportedAudioFile_ShouldWork" xmlns="urn:schemas-jetbrains-com:jetbrains-ut-session">
|
<s:String x:Key="/Default/Environment/UnitTesting/UnitTestSessionStore/Sessions/=5d87cd84_002Da775_002D4e8b_002D859b_002D8460b06eac04/@EntryIndexedValue"><SessionState ContinuousTestingMode="0" IsActive="True" Name="IsSupportedAudioFile_ShouldWork" xmlns="urn:schemas-jetbrains-com:jetbrains-ut-session">
|
||||||
<TestAncestor>
|
<TestAncestor>
|
||||||
<TestId>xUnit::24F92458-FB39-44BE-A32F-41275183AF1B::net10.0::OCES.Resonance.Core.Tests.AudioFileScannerTests</TestId>
|
<TestId>xUnit::24F92458-FB39-44BE-A32F-41275183AF1B::net10.0::OCES.Resonance.Core.Tests.AudioFileScannerTests</TestId>
|
||||||
<TestId>xUnit::24F92458-FB39-44BE-A32F-41275183AF1B::net10.0::OCES.Resonance.Core.Tests.AudioMetadataReaderTests.ReadMetadata_文件不存在_抛出FileNotFoundException</TestId>
|
<TestId>xUnit::24F92458-FB39-44BE-A32F-41275183AF1B::net10.0::OCES.Resonance.Core.Tests.AudioMetadataReaderTests.ReadMetadata_文件不存在_抛出FileNotFoundException</TestId>
|
||||||
|
<TestId>xUnit::24F92458-FB39-44BE-A32F-41275183AF1B::net10.0::Core.Tests.AudioMetadataReaderTest.ReadMetadata_ShoudThrow_FileNotFoundException</TestId>
|
||||||
|
<TestId>xUnit::24F92458-FB39-44BE-A32F-41275183AF1B::net10.0::Core.Tests.AudioMetadataReaderTest.ReadMetadata_ShouldThrow_FileNotFoundException</TestId>
|
||||||
|
<TestId>xUnit::24F92458-FB39-44BE-A32F-41275183AF1B::net10.0::Core.Tests.AudioMetadataReaderTest</TestId>
|
||||||
</TestAncestor>
|
</TestAncestor>
|
||||||
</SessionState></s:String></wpf:ResourceDictionary>
|
</SessionState></s:String></wpf:ResourceDictionary>
|
||||||
Reference in New Issue
Block a user