WIP: 单元测试
Reviewing AudioMetadataReader.cs
This commit is contained in:
@@ -5,6 +5,7 @@
|
||||
/// </summary>
|
||||
public class AudioFileMeta
|
||||
{
|
||||
|
||||
#region 必备条目
|
||||
/// <summary>主键ID,音频文件的唯一数字标识符</summary>
|
||||
public required int Id { get; set; }
|
||||
@@ -29,9 +30,6 @@ public class AudioFileMeta
|
||||
|
||||
/// <summary>时长,音频文件的播放长度(通常以秒为单位)</summary>
|
||||
public required double Duration { get; set; }
|
||||
|
||||
/// <summary>时长,以采样数记录的时长</summary>
|
||||
public required uint TotalSamples { get; set; }
|
||||
|
||||
/// <summary>位深度,音频采样位深(如 16、24、32 bit)</summary>
|
||||
public required int BitDepth { get; set; }
|
||||
@@ -40,7 +38,7 @@ public class AudioFileMeta
|
||||
public required int Channels { get; set; }
|
||||
|
||||
/// <summary>采样率,如 44100、48000、96000 Hz</summary>
|
||||
public required int SampleRate { get; set; }
|
||||
public required double SampleRate { get; set; }
|
||||
|
||||
/// <summary>文件类型,音频格式(WAV、MP3、AIFF、FLAC等)</summary>
|
||||
public required string Type { get; set; }
|
||||
@@ -129,9 +127,13 @@ public class AudioFileMeta
|
||||
|
||||
/// <summary>CD标题,原始CD专辑名称</summary>
|
||||
public string? CdTitle { get; set; }
|
||||
|
||||
public int? DiscNumber { get; set; }
|
||||
|
||||
/// <summary>曲目标题,音乐或音轨的标题</summary>
|
||||
public string? TrackTitle { get; set; }
|
||||
|
||||
public int? TrackNumber { get; set; }
|
||||
|
||||
/// <summary>剧集,所属剧集或系列编号</summary>
|
||||
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; }
|
||||
|
||||
/// <summary>是否已编辑,布尔值(0=否,1=是)</summary>
|
||||
public bool? IsEdited { get; set; }
|
||||
@@ -164,7 +166,7 @@ public class AudioFileMeta
|
||||
/// <summary>位置,录音地点或存储位置</summary>
|
||||
public string? Location { get; set; }
|
||||
|
||||
/// <summary>分组,用于组织管理的分组标识</summary>
|
||||
/// <summary>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").</summary>
|
||||
public string? Group { get; set; }
|
||||
|
||||
/// <summary>标记点,音频内的关键时间点标记</summary>
|
||||
|
||||
@@ -11,9 +11,9 @@ public class AudioFileMetaValidator
|
||||
/// <param name="meta">音频元数据对象</param>
|
||||
/// <param name="errors">验证错误列表</param>
|
||||
/// <returns>是否验证通过</returns>
|
||||
public bool Validate(AudioFileMeta meta, out List<string> errors)
|
||||
public bool Validate(AudioFileMeta? meta, out List<string> errors)
|
||||
{
|
||||
errors = new List<string>();
|
||||
errors = [];
|
||||
|
||||
if (meta == null)
|
||||
{
|
||||
|
||||
@@ -5,10 +5,10 @@ namespace OCES.Resonance.Core;
|
||||
/// </summary>
|
||||
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",
|
||||
];
|
||||
|
||||
/// <summary>
|
||||
/// 扫描指定目录,返回所有音频文件路径
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
using System.Security.Cryptography;
|
||||
|
||||
namespace OCES.Resonance.Core;
|
||||
|
||||
/// <summary>
|
||||
@@ -5,14 +7,8 @@ namespace OCES.Resonance.Core;
|
||||
/// </summary>
|
||||
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();
|
||||
|
||||
/// <summary>
|
||||
/// 扫描目录并将结果添加到数据库
|
||||
@@ -32,14 +28,14 @@ public class AudioLibraryService
|
||||
SuccessCount = 0,
|
||||
SkipCount = 0,
|
||||
ErrorCount = 0,
|
||||
Errors = new List<string>()
|
||||
Errors = [],
|
||||
};
|
||||
|
||||
// 初始化数据库
|
||||
Database.InitializeDatabase();
|
||||
|
||||
// 扫描所有音频文件
|
||||
var audioFiles = _scanner.ScanDirectory(directoryPath).ToList();
|
||||
List<string> 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<AudioFileMeta>();
|
||||
List<AudioFileMeta> 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
|
||||
/// </summary>
|
||||
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; }
|
||||
|
||||
/// <summary>错误详情列表</summary>
|
||||
public List<string> Errors { get; set; } = new();
|
||||
public List<string> Errors { get; set; } = [];
|
||||
|
||||
/// <summary>
|
||||
/// 获取人类可读的统计报告
|
||||
|
||||
@@ -11,62 +11,36 @@ public class AudioMetadataReader
|
||||
/// <summary>
|
||||
/// 从音频文件读取元数据
|
||||
/// </summary>
|
||||
/// <param name="filePath">音频文件路径</param>
|
||||
/// <param name="audioFile">音频文件路径</param>
|
||||
/// <returns>音频元数据对象</returns>
|
||||
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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取自定义字段值
|
||||
/// </summary>
|
||||
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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取音频位深度
|
||||
/// </summary>
|
||||
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
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 计算文件 MD5 值
|
||||
/// </summary>
|
||||
/// <param name="filePath">文件路径</param>
|
||||
/// <returns>MD5 哈希值(十六进制字符串)</returns>
|
||||
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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 生成 UUID
|
||||
/// </summary>
|
||||
/// <returns>UUID 字符串</returns>
|
||||
private static string GenerateUuid()
|
||||
static string GenerateUuid()
|
||||
{
|
||||
return Guid.NewGuid().ToString("N"); // 无连字符的 UUID
|
||||
return Guid.NewGuid().ToString();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,7 +15,7 @@ public static class Database
|
||||
/// <returns>数据库连接</returns>
|
||||
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 (
|
||||
|
||||
Reference in New Issue
Block a user