From 021f4b1cd16a3998432b24aab607dee6ff86f1c7 Mon Sep 17 00:00:00 2001 From: Oliver Wong Date: Fri, 17 Apr 2026 14:20:03 +0800 Subject: [PATCH] =?UTF-8?q?WIP:=20=E5=8D=95=E5=85=83=E6=B5=8B=E8=AF=95=20R?= =?UTF-8?q?eviewing=20AudioMetadataReader.cs?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/Core.Tests/AudioFileScannerTests.cs | 59 +++++++ src/Core.Tests/AudioLibraryServiceTests.cs | 133 ++++++++++++++++ src/Core.Tests/AudioMetadataReaderTests.cs | 130 +++++++++++++++ src/Core.Tests/Core.Tests.csproj | 27 ++++ src/Core.Tests/DatabaseTests.cs | 177 +++++++++++++++++++++ src/Core/AudioFileMeta.cs | 14 +- src/Core/AudioFileMetaValidator.cs | 4 +- src/Core/AudioFileScanner.cs | 15 +- src/Core/AudioLibraryService.cs | 44 ++--- src/Core/AudioMetadataReader.cs | 130 ++++----------- src/Core/Database.cs | 12 +- src/Resonance.sln | 29 ++++ src/Resonance.sln.DotSettings.user | 11 ++ 13 files changed, 641 insertions(+), 144 deletions(-) create mode 100644 src/Core.Tests/AudioFileScannerTests.cs create mode 100644 src/Core.Tests/AudioLibraryServiceTests.cs create mode 100644 src/Core.Tests/AudioMetadataReaderTests.cs create mode 100644 src/Core.Tests/Core.Tests.csproj create mode 100644 src/Core.Tests/DatabaseTests.cs create mode 100644 src/Resonance.sln.DotSettings.user 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