diff --git a/src/Core.Tests/AudioFileScannerTests.cs b/src/Core.Tests/AudioFileScannerTests.cs
new file mode 100644
index 0000000..7ebabf4
--- /dev/null
+++ b/src/Core.Tests/AudioFileScannerTests.cs
@@ -0,0 +1,59 @@
+namespace OCES.Resonance.Core.Tests;
+
+///
+/// AudioFileScanner 单元测试
+///
+public class AudioFileScannerTests
+{
+ [Theory]
+ [InlineData("test.wav", true)]
+ [InlineData("test.WAV", true)]
+ [InlineData("test.mp3", true)]
+ [InlineData("test.txt", false)]
+ [InlineData("", false)]
+ [InlineData("test.xlsx", false)]
+ public void IsSupportedAudioFile_ShouldWork(string input, bool expected)
+ {
+ AudioFileScanner scanner = new();
+
+ bool result = scanner.IsSupportedAudioFile(input);
+
+ Assert.Equal(expected, result);
+ }
+
+ [Fact]
+ public void ScanDirectory_ShouldWork()
+ {
+ string tempDir = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString());
+ Directory.CreateDirectory(tempDir);
+
+ try
+ {
+ // 构造真实文件
+ File.WriteAllText(Path.Combine(tempDir, "a.wav"), "");
+ File.WriteAllText(Path.Combine(tempDir, "b.MP3"), "");
+ File.WriteAllText(Path.Combine(tempDir, "c.txt"), "");
+ File.WriteAllText(Path.Combine(tempDir, "d.xlsx"), "");
+ File.WriteAllText(Path.Combine(tempDir, "e.WAV"), "");
+
+ AudioFileScanner scanner = new();
+
+ List result = scanner.ScanDirectory(tempDir).ToList();
+
+ Assert.Equal(3, result.Count);
+ }
+ finally
+ {
+ Directory.Delete(tempDir, true);
+ }
+ }
+
+ [Fact]
+ public void ScanDirectory_ShouldThrow_WhenDirectoryDoesNotExist()
+ {
+ AudioFileScanner scanner = new();
+ string nonExistentPath = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString());
+
+ Assert.Throws(() => scanner.ScanDirectory(nonExistentPath));
+ }
+}
diff --git a/src/Core.Tests/AudioLibraryServiceTests.cs b/src/Core.Tests/AudioLibraryServiceTests.cs
new file mode 100644
index 0000000..39cf91b
--- /dev/null
+++ b/src/Core.Tests/AudioLibraryServiceTests.cs
@@ -0,0 +1,133 @@
+using FluentAssertions;
+
+namespace OCES.Resonance.Core.Tests;
+
+///
+/// AudioLibraryService 集成测试
+///
+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);
+ }
+}
diff --git a/src/Core.Tests/AudioMetadataReaderTests.cs b/src/Core.Tests/AudioMetadataReaderTests.cs
new file mode 100644
index 0000000..c1b90f8
--- /dev/null
+++ b/src/Core.Tests/AudioMetadataReaderTests.cs
@@ -0,0 +1,130 @@
+using FluentAssertions;
+
+namespace OCES.Resonance.Core.Tests;
+
+///
+/// AudioMetadataReader 单元测试
+///
+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 act = () => this._reader.ReadMetadata(nonExistentFile);
+ act.Should().Throw();
+ }
+
+ [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);
+ }
+}
diff --git a/src/Core.Tests/Core.Tests.csproj b/src/Core.Tests/Core.Tests.csproj
new file mode 100644
index 0000000..d409452
--- /dev/null
+++ b/src/Core.Tests/Core.Tests.csproj
@@ -0,0 +1,27 @@
+
+
+
+ net10.0
+ enable
+ enable
+ false
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/Core.Tests/DatabaseTests.cs b/src/Core.Tests/DatabaseTests.cs
new file mode 100644
index 0000000..7ecce99
--- /dev/null
+++ b/src/Core.Tests/DatabaseTests.cs
@@ -0,0 +1,177 @@
+using System.Data.SQLite;
+using FluentAssertions;
+
+namespace OCES.Resonance.Core.Tests;
+
+///
+/// Database 单元测试
+///
+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();
+ }
+
+ [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
+ {
+ 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),
+ };
+ }
+}
diff --git a/src/Core/AudioFileMeta.cs b/src/Core/AudioFileMeta.cs
index 83352bb..4175ab5 100644
--- a/src/Core/AudioFileMeta.cs
+++ b/src/Core/AudioFileMeta.cs
@@ -5,6 +5,7 @@
///
public class AudioFileMeta
{
+
#region 必备条目
/// 主键ID,音频文件的唯一数字标识符
public required int Id { get; set; }
@@ -29,9 +30,6 @@ public class AudioFileMeta
/// 时长,音频文件的播放长度(通常以秒为单位)
public required double Duration { get; set; }
-
- /// 时长,以采样数记录的时长
- public required uint TotalSamples { get; set; }
/// 位深度,音频采样位深(如 16、24、32 bit)
public required int BitDepth { get; set; }
@@ -40,7 +38,7 @@ public class AudioFileMeta
public required int Channels { get; set; }
/// 采样率,如 44100、48000、96000 Hz
- public required int SampleRate { get; set; }
+ public required double SampleRate { get; set; }
/// 文件类型,音频格式(WAV、MP3、AIFF、FLAC等)
public required string Type { get; set; }
@@ -129,9 +127,13 @@ public class AudioFileMeta
/// CD标题,原始CD专辑名称
public string? CdTitle { get; set; }
+
+ public int? DiscNumber { get; set; }
/// 曲目标题,音乐或音轨的标题
public string? TrackTitle { get; set; }
+
+ public int? TrackNumber { get; set; }
/// 剧集,所属剧集或系列编号
public string? Episode { get; set; }
@@ -153,7 +155,7 @@ public class AudioFileMeta
public DateTime? ReleaseDate { get; set; }
- public string? TrackYear { get; set; }
+ public int? TrackYear { get; set; }
/// 是否已编辑,布尔值(0=否,1=是)
public bool? IsEdited { get; set; }
@@ -164,7 +166,7 @@ public class AudioFileMeta
/// 位置,录音地点或存储位置
public string? Location { get; set; }
- /// 分组,用于组织管理的分组标识
+ /// Content group description Used if the sound belongs to a larger category of sounds/music. For example, classical music is often sorted in different musical sections (e.g. "Piano Concerto").
public string? Group { get; set; }
/// 标记点,音频内的关键时间点标记
diff --git a/src/Core/AudioFileMetaValidator.cs b/src/Core/AudioFileMetaValidator.cs
index 45f5c44..6285db1 100644
--- a/src/Core/AudioFileMetaValidator.cs
+++ b/src/Core/AudioFileMetaValidator.cs
@@ -11,9 +11,9 @@ public class AudioFileMetaValidator
/// 音频元数据对象
/// 验证错误列表
/// 是否验证通过
- public bool Validate(AudioFileMeta meta, out List errors)
+ public bool Validate(AudioFileMeta? meta, out List errors)
{
- errors = new List();
+ errors = [];
if (meta == null)
{
diff --git a/src/Core/AudioFileScanner.cs b/src/Core/AudioFileScanner.cs
index 4982f31..3a2f9ab 100644
--- a/src/Core/AudioFileScanner.cs
+++ b/src/Core/AudioFileScanner.cs
@@ -5,10 +5,10 @@ namespace OCES.Resonance.Core;
///
public class AudioFileScanner
{
- private static readonly string[] SupportedExtensions =
- {
- ".wav", ".mp3", ".flac", ".aiff", ".aif", ".m4a", ".ogg", ".wma", ".bwf", ".wav64"
- };
+ static readonly string[] SupportedExtensions =
+ [
+ ".wav", ".mp3", ".flac", ".aiff", ".aif", ".m4a", ".ogg", ".wma", ".bwf", ".wav64",
+ ];
///
/// 扫描指定目录,返回所有音频文件路径
@@ -23,12 +23,11 @@ public class AudioFileScanner
throw new DirectoryNotFoundException($"目录不存在:{directoryPath}");
}
- var searchOption = recursive ? SearchOption.AllDirectories : SearchOption.TopDirectoryOnly;
-
+ SearchOption searchOption = recursive ? SearchOption.AllDirectories : SearchOption.TopDirectoryOnly;
+
return SupportedExtensions
.SelectMany(ext => Directory.EnumerateFiles(directoryPath, $"*{ext}", searchOption))
- .Where(IsSupportedAudioFile)
- .Distinct(StringComparer.OrdinalIgnoreCase);
+ .Where(IsSupportedAudioFile);
}
///
diff --git a/src/Core/AudioLibraryService.cs b/src/Core/AudioLibraryService.cs
index f9144a0..5dfbaf3 100644
--- a/src/Core/AudioLibraryService.cs
+++ b/src/Core/AudioLibraryService.cs
@@ -1,3 +1,5 @@
+using System.Security.Cryptography;
+
namespace OCES.Resonance.Core;
///
@@ -5,14 +7,8 @@ namespace OCES.Resonance.Core;
///
public class AudioLibraryService
{
- private readonly AudioFileScanner _scanner;
- private readonly AudioMetadataReader _metadataReader;
-
- public AudioLibraryService()
- {
- _scanner = new AudioFileScanner();
- _metadataReader = new AudioMetadataReader();
- }
+ readonly AudioFileScanner _scanner = new();
+ readonly AudioMetadataReader _metadataReader = new();
///
/// 扫描目录并将结果添加到数据库
@@ -32,14 +28,14 @@ public class AudioLibraryService
SuccessCount = 0,
SkipCount = 0,
ErrorCount = 0,
- Errors = new List()
+ Errors = [],
};
// 初始化数据库
Database.InitializeDatabase();
// 扫描所有音频文件
- var audioFiles = _scanner.ScanDirectory(directoryPath).ToList();
+ List audioFiles = this._scanner.ScanDirectory(directoryPath).ToList();
result.TotalFiles = audioFiles.Count;
if (audioFiles.Count == 0)
@@ -47,25 +43,18 @@ public class AudioLibraryService
return result;
}
- var metadataList = new List();
+ List metadataList = [];
// 读取每个文件的元数据
- foreach (var filePath in audioFiles)
+ foreach (string filePath in audioFiles)
{
try
{
// 计算 MD5 用于查重
- var md5 = CalculateMd5ForFile(filePath);
-
- // 检查是否已存在
- if (skipExisting && Database.EntryExists(md5, filePath))
- {
- result.SkipCount++;
- continue;
- }
+ string md5 = CalculateMd5ForFile(filePath);
// 读取元数据
- var metadata = _metadataReader.ReadMetadata(filePath);
+ AudioFileMeta metadata = this._metadataReader.ReadMetadata(filePath);
metadataList.Add(metadata);
result.SuccessCount++;
}
@@ -77,7 +66,8 @@ public class AudioLibraryService
}
// 批量入库
- if (metadataList.Count > 0)
+ if (metadataList.Count <= 0)
+ return await Task.FromResult(result);
{
try
{
@@ -98,10 +88,10 @@ public class AudioLibraryService
///
private static string CalculateMd5ForFile(string filePath)
{
- using var md5 = System.Security.Cryptography.MD5.Create();
- using var stream = File.OpenRead(filePath);
- var hash = md5.ComputeHash(stream);
- return BitConverter.ToString(hash).Replace("-", "").ToLowerInvariant();
+ using MD5 md5 = MD5.Create();
+ using FileStream stream = File.OpenRead(filePath);
+ byte[] hash = md5.ComputeHash(stream);
+ return Convert.ToHexString(hash);
}
}
@@ -123,7 +113,7 @@ public class ScanResult
public int ErrorCount { get; set; }
/// 错误详情列表
- public List Errors { get; set; } = new();
+ public List Errors { get; set; } = [];
///
/// 获取人类可读的统计报告
diff --git a/src/Core/AudioMetadataReader.cs b/src/Core/AudioMetadataReader.cs
index fcdb4c8..f56fdaf 100644
--- a/src/Core/AudioMetadataReader.cs
+++ b/src/Core/AudioMetadataReader.cs
@@ -11,62 +11,36 @@ public class AudioMetadataReader
///
/// 从音频文件读取元数据
///
- /// 音频文件路径
+ /// 音频文件路径
/// 音频元数据对象
- public AudioFileMeta ReadMetadata(string filePath)
+ public AudioFileMeta ReadMetadata(FileInfo audioFile)
{
- if (!File.Exists(filePath))
+ if (!audioFile.Exists)
{
- throw new FileNotFoundException($"文件不存在:{filePath}");
+ throw new FileNotFoundException($"文件不存在:{audioFile}");
}
- var track = new Track(filePath);
- var fileInfo = new FileInfo(filePath);
-
- // 获取音频技术参数
- var durationSeconds = track.DurationMs / 1000.0;
- var sampleRate = track.SampleRate;
- var channels = track.ChannelsArrangement?.NbChannels ?? 2;
- var bitDepth = GetBitDepth(track);
- var totalSamples = (uint)(durationSeconds * sampleRate);
-
- // 提取路径信息
- var fileName = Path.GetFileName(filePath);
- var folder = Path.GetDirectoryName(filePath) ?? string.Empty;
- var directory = Path.GetDirectoryName(filePath) ?? string.Empty;
- var folderName = new DirectoryInfo(folder).Name;
-
- // 生成唯一标识
- var md5 = CalculateMd5(filePath);
- var uniqueId = GenerateUuid();
-
- // 获取文件修改时间
- var lastWriteTime = fileInfo.LastWriteTime;
- var createTime = fileInfo.CreationTime;
-
- // 获取文件扩展名作为类型
- var fileType = Path.GetExtension(filePath).TrimStart('.').ToUpperInvariant();
+ Track track = new(audioFile.FullName);
// 映射 ATL 元数据到 AudioFileMeta
- var meta = new AudioFileMeta
+ AudioFileMeta meta = new()
{
// 必备条目
Id = 0, // 自增 ID,由数据库生成
- UniqueId = uniqueId,
- Md5 = md5,
- Path = Path.GetFullPath(filePath),
- Filename = fileName,
- Folder = folderName,
- Directory = directory,
- Duration = durationSeconds,
- TotalSamples = totalSamples,
- BitDepth = bitDepth,
- Channels = channels,
- SampleRate = (int)sampleRate,
- Type = fileType,
+ UniqueId = GenerateUuid(),
+ Md5 = CalculateMd5(audioFile.FullName),
+ Path = audioFile.FullName,
+ Filename = audioFile.Name,
+ Folder = audioFile.Directory?.Name ?? string.Empty,
+ Directory = audioFile.DirectoryName ?? string.Empty,
+ Duration = track.DurationMs / 1000,
+ BitDepth = track.BitDepth,
+ Channels = track.ChannelsArrangement.NbChannels,
+ SampleRate = track.SampleRate,
+ Type = audioFile.Extension.TrimStart('.').ToUpperInvariant(),
DateAdded = DateTime.Now,
- OriginalModificationDate = lastWriteTime,
- OriginationTime = createTime,
+ OriginalModificationDate = audioFile.LastWriteTime,
+ OriginationTime = audioFile.CreationTime,
// 可选条目
Bpm = track.BPM > 0 ? track.BPM : null,
@@ -74,8 +48,17 @@ public class AudioMetadataReader
Genre = track.Genre,
Artist = track.Artist,
Composer = track.Composer,
+ Comments = track.Comment,
Publisher = track.Publisher,
Copyright = track.Copyright,
+ CdTitle = track.Album,
+ DiscNumber = track.DiscNumber,
+ TrackTitle = track.Title,
+ ReleaseDate = track.OriginalReleaseDate,
+ TrackYear = track.Year,
+ TrackNumber = track.TrackNumber,
+ Group = track.Group,
+
// BWF 特定字段 (通过附加数据获取)
CodingHistory = GetBwfField(track, "CodingHistory"),
@@ -105,69 +88,26 @@ public class AudioMetadataReader
return null;
}
- ///
- /// 获取自定义字段值
- ///
- private static string? GetCustomField(Track track, int index)
- {
- try
- {
- string fieldName = $"User{index}";
- if (track.AdditionalFields != null && track.AdditionalFields.TryGetValue(fieldName, out string? field))
- {
- return field;
- }
- }
- catch
- {
- // 忽略自定义字段读取错误
- }
- return null;
- }
-
- ///
- /// 获取音频位深度
- ///
- private static int GetBitDepth(Track track)
- {
- // ATL 可能通过不同方式提供位深信息
- if (track.BitDepth > 0)
- {
- return track.BitDepth;
- }
-
- // 根据格式推断默认位深
- var extension = Path.GetExtension(track.Path).ToLowerInvariant();
- return extension switch
- {
- ".wav" or ".bwf" or ".wav64" => 24, // 现代 WAV 通常 24bit
- ".flac" => 24,
- ".aiff" or ".aif" => 16,
- ".mp3" => 16, // MP3 实际没有位深概念,默认 16
- _ => 16
- };
- }
-
///
/// 计算文件 MD5 值
///
/// 文件路径
/// MD5 哈希值(十六进制字符串)
- private static string CalculateMd5(string filePath)
+ static string CalculateMd5(string filePath)
{
- using var md5 = MD5.Create();
- using var stream = File.OpenRead(filePath);
+ using MD5 md5 = MD5.Create();
+ using FileStream stream = File.OpenRead(filePath);
- var hash = md5.ComputeHash(stream);
- return BitConverter.ToString(hash).Replace("-", "").ToLowerInvariant();
+ byte[] hash = md5.ComputeHash(stream);
+ return Convert.ToHexStringLower(hash);
}
///
/// 生成 UUID
///
/// UUID 字符串
- private static string GenerateUuid()
+ static string GenerateUuid()
{
- return Guid.NewGuid().ToString("N"); // 无连字符的 UUID
+ return Guid.NewGuid().ToString();
}
}
diff --git a/src/Core/Database.cs b/src/Core/Database.cs
index 461620b..d1c00e2 100644
--- a/src/Core/Database.cs
+++ b/src/Core/Database.cs
@@ -15,7 +15,7 @@ public static class Database
/// 数据库连接
public static IDbConnection GetConnection(string dbName = "default")
{
- var connectionString = $"Data Source={dbName}.db;Version=3;";
+ string connectionString = $"Data Source={dbName}.db;Version=3;";
return new SQLiteConnection(connectionString);
}
@@ -28,7 +28,7 @@ public static class Database
connection.Open();
const string sql = @"
- CREATE TABLE IF NOT EXISTS audio_files (
+ CREATE TABLE IF NOT EXISTS sounds (
id INTEGER PRIMARY KEY AUTOINCREMENT,
unique_id TEXT NOT NULL UNIQUE,
short_id TEXT,
@@ -83,7 +83,7 @@ public static class Database
is_edited INTEGER,
is_split INTEGER,
location TEXT,
- group TEXT,
+ [group] TEXT,
markers TEXT,
comments TEXT,
notes TEXT,
@@ -99,7 +99,7 @@ public static class Database
user6 TEXT,
user7 TEXT,
user8 TEXT,
-
+
INDEX idx_md5 (md5),
INDEX idx_path (path),
INDEX idx_unique_id (unique_id)
@@ -131,7 +131,7 @@ public static class Database
artist, composer, designer, recordist, publisher, manufacturer,
originator, originator_ref, project_name, library, cd_title,
track_title, episode, scene, take, tape, cue_number, sync_point,
- release_date, track_year, is_edited, is_split, location, group,
+ release_date, track_year, is_edited, is_split, location, [group],
markers, comments, notes, copyright, coding_history, microphone,
mic_perspective, user1, user2, user3, user4, user5, user6, user7, user8
) VALUES (
@@ -182,7 +182,7 @@ public static class Database
artist, composer, designer, recordist, publisher, manufacturer,
originator, originator_ref, project_name, library, cd_title,
track_title, episode, scene, take, tape, cue_number, sync_point,
- release_date, track_year, is_edited, is_split, location, group,
+ release_date, track_year, is_edited, is_split, location, [group],
markers, comments, notes, copyright, coding_history, microphone,
mic_perspective, user1, user2, user3, user4, user5, user6, user7, user8
) VALUES (
diff --git a/src/Resonance.sln b/src/Resonance.sln
index d559589..b91825e 100644
--- a/src/Resonance.sln
+++ b/src/Resonance.sln
@@ -2,15 +2,44 @@
Microsoft Visual Studio Solution File, Format Version 12.00
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Core", "Core\Core.csproj", "{855F941D-5DA0-479A-BB79-5C7CE72CD34B}"
EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Core.Tests", "Core.Tests\Core.Tests.csproj", "{24F92458-FB39-44BE-A32F-41275183AF1B}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
+ Debug|x64 = Debug|x64
+ Debug|x86 = Debug|x86
Release|Any CPU = Release|Any CPU
+ Release|x64 = Release|x64
+ Release|x86 = Release|x86
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{855F941D-5DA0-479A-BB79-5C7CE72CD34B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{855F941D-5DA0-479A-BB79-5C7CE72CD34B}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {855F941D-5DA0-479A-BB79-5C7CE72CD34B}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {855F941D-5DA0-479A-BB79-5C7CE72CD34B}.Debug|x64.Build.0 = Debug|Any CPU
+ {855F941D-5DA0-479A-BB79-5C7CE72CD34B}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {855F941D-5DA0-479A-BB79-5C7CE72CD34B}.Debug|x86.Build.0 = Debug|Any CPU
{855F941D-5DA0-479A-BB79-5C7CE72CD34B}.Release|Any CPU.ActiveCfg = Release|Any CPU
{855F941D-5DA0-479A-BB79-5C7CE72CD34B}.Release|Any CPU.Build.0 = Release|Any CPU
+ {855F941D-5DA0-479A-BB79-5C7CE72CD34B}.Release|x64.ActiveCfg = Release|Any CPU
+ {855F941D-5DA0-479A-BB79-5C7CE72CD34B}.Release|x64.Build.0 = Release|Any CPU
+ {855F941D-5DA0-479A-BB79-5C7CE72CD34B}.Release|x86.ActiveCfg = Release|Any CPU
+ {855F941D-5DA0-479A-BB79-5C7CE72CD34B}.Release|x86.Build.0 = Release|Any CPU
+ {24F92458-FB39-44BE-A32F-41275183AF1B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {24F92458-FB39-44BE-A32F-41275183AF1B}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {24F92458-FB39-44BE-A32F-41275183AF1B}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {24F92458-FB39-44BE-A32F-41275183AF1B}.Debug|x64.Build.0 = Debug|Any CPU
+ {24F92458-FB39-44BE-A32F-41275183AF1B}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {24F92458-FB39-44BE-A32F-41275183AF1B}.Debug|x86.Build.0 = Debug|Any CPU
+ {24F92458-FB39-44BE-A32F-41275183AF1B}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {24F92458-FB39-44BE-A32F-41275183AF1B}.Release|Any CPU.Build.0 = Release|Any CPU
+ {24F92458-FB39-44BE-A32F-41275183AF1B}.Release|x64.ActiveCfg = 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.Build.0 = Release|Any CPU
+ EndGlobalSection
+ GlobalSection(SolutionProperties) = preSolution
+ HideSolutionNode = FALSE
EndGlobalSection
EndGlobal
diff --git a/src/Resonance.sln.DotSettings.user b/src/Resonance.sln.DotSettings.user
new file mode 100644
index 0000000..de367d6
--- /dev/null
+++ b/src/Resonance.sln.DotSettings.user
@@ -0,0 +1,11 @@
+
+ ForceIncluded
+ <AssemblyExplorer>
+ <Assembly Path="/Users/oliver/.nuget/packages/fluentassertions/8.9.0/lib/net6.0/FluentAssertions.dll" />
+</AssemblyExplorer>
+ <SessionState ContinuousTestingMode="0" IsActive="True" Name="IsSupportedAudioFile_ShouldWork" xmlns="urn:schemas-jetbrains-com:jetbrains-ut-session">
+ <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.AudioMetadataReaderTests.ReadMetadata_文件不存在_抛出FileNotFoundException</TestId>
+ </TestAncestor>
+</SessionState>
\ No newline at end of file