WIP: 扫描扫描指定目录内的所有音频文件,获取其meta信息,并将其添加至一个名为default的数据库。
This commit is contained in:
@@ -0,0 +1,127 @@
|
|||||||
|
namespace OCES.Resonance.Core;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 音频元数据验证器,验证元数据完整性和格式
|
||||||
|
/// </summary>
|
||||||
|
public class AudioFileMetaValidator
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 验证必填字段是否完整
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="meta">音频元数据对象</param>
|
||||||
|
/// <param name="errors">验证错误列表</param>
|
||||||
|
/// <returns>是否验证通过</returns>
|
||||||
|
public bool Validate(AudioFileMeta meta, out List<string> errors)
|
||||||
|
{
|
||||||
|
errors = new List<string>();
|
||||||
|
|
||||||
|
if (meta == null)
|
||||||
|
{
|
||||||
|
errors.Add("元数据对象为空");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 验证必备字符串字段
|
||||||
|
ValidateRequiredField(meta.UniqueId, "UniqueId", errors);
|
||||||
|
ValidateRequiredField(meta.Md5, "Md5", errors);
|
||||||
|
ValidateRequiredField(meta.Path, "Path", errors);
|
||||||
|
ValidateRequiredField(meta.Filename, "Filename", errors);
|
||||||
|
ValidateRequiredField(meta.Folder, "Folder", errors);
|
||||||
|
ValidateRequiredField(meta.Directory, "Directory", errors);
|
||||||
|
ValidateRequiredField(meta.Type, "Type", errors);
|
||||||
|
|
||||||
|
// 验证数值字段
|
||||||
|
ValidatePositiveValue(meta.Duration, "Duration", errors);
|
||||||
|
ValidatePositiveValue(meta.TotalSamples, "TotalSamples", errors);
|
||||||
|
ValidatePositiveValue(meta.BitDepth, "BitDepth", errors);
|
||||||
|
ValidatePositiveValue(meta.Channels, "Channels", errors);
|
||||||
|
ValidatePositiveValue(meta.SampleRate, "SampleRate", errors);
|
||||||
|
|
||||||
|
// 验证日期字段
|
||||||
|
ValidateRequiredField(meta.DateAdded, "DateAdded", errors);
|
||||||
|
ValidateRequiredField(meta.OriginalModificationDate, "OriginalModificationDate", errors);
|
||||||
|
ValidateRequiredField(meta.OriginationTime, "OriginationTime", errors);
|
||||||
|
|
||||||
|
// 验证文件路径是否存在
|
||||||
|
if (!File.Exists(meta.Path))
|
||||||
|
{
|
||||||
|
errors.Add($"文件不存在:{meta.Path}");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 验证 MD5 格式(32 位十六进制)
|
||||||
|
if (!IsValidMd5(meta.Md5))
|
||||||
|
{
|
||||||
|
errors.Add($"MD5 格式无效:{meta.Md5}");
|
||||||
|
}
|
||||||
|
|
||||||
|
return errors.Count == 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 验证必填字符串字段
|
||||||
|
/// </summary>
|
||||||
|
private static void ValidateRequiredField(string? value, string fieldName, List<string> errors)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(value))
|
||||||
|
{
|
||||||
|
errors.Add($"{fieldName} 不能为空");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 验证必填值类型字段
|
||||||
|
/// </summary>
|
||||||
|
private static void ValidateRequiredField<T>(T value, string fieldName, List<string> errors) where T : struct
|
||||||
|
{
|
||||||
|
if (EqualityComparer<T>.Default.Equals(value, default))
|
||||||
|
{
|
||||||
|
errors.Add($"{fieldName} 不能为空");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 验证正数值
|
||||||
|
/// </summary>
|
||||||
|
private static void ValidatePositiveValue(double value, string fieldName, List<string> errors)
|
||||||
|
{
|
||||||
|
if (value <= 0)
|
||||||
|
{
|
||||||
|
errors.Add($"{fieldName} 必须为正数 (当前值:{value})");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 验证正数整数值
|
||||||
|
/// </summary>
|
||||||
|
private static void ValidatePositiveValue(int value, string fieldName, List<string> errors)
|
||||||
|
{
|
||||||
|
if (value <= 0)
|
||||||
|
{
|
||||||
|
errors.Add($"{fieldName} 必须为正数 (当前值:{value})");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 验证正数无符号整数值
|
||||||
|
/// </summary>
|
||||||
|
private static void ValidatePositiveValue(uint value, string fieldName, List<string> errors)
|
||||||
|
{
|
||||||
|
if (value <= 0)
|
||||||
|
{
|
||||||
|
errors.Add($"{fieldName} 必须为正数 (当前值:{value})");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 验证 MD5 格式是否有效
|
||||||
|
/// </summary>
|
||||||
|
private static bool IsValidMd5(string md5)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(md5) || md5.Length != 32)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return md5.All(c => (c >= '0' && c <= '9') || (c >= 'a' && c <= 'f') || (c >= 'A' && c <= 'F'));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
namespace OCES.Resonance.Core;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 音频文件扫描器,负责递归扫描目录并识别音频文件
|
||||||
|
/// </summary>
|
||||||
|
public class AudioFileScanner
|
||||||
|
{
|
||||||
|
private static readonly string[] SupportedExtensions =
|
||||||
|
{
|
||||||
|
".wav", ".mp3", ".flac", ".aiff", ".aif", ".m4a", ".ogg", ".wma", ".bwf", ".wav64"
|
||||||
|
};
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 扫描指定目录,返回所有音频文件路径
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="directoryPath">要扫描的目录路径</param>
|
||||||
|
/// <param name="recursive">是否递归扫描子目录</param>
|
||||||
|
/// <returns>音频文件路径集合</returns>
|
||||||
|
public IEnumerable<string> ScanDirectory(string directoryPath, bool recursive = true)
|
||||||
|
{
|
||||||
|
if (!Directory.Exists(directoryPath))
|
||||||
|
{
|
||||||
|
throw new DirectoryNotFoundException($"目录不存在:{directoryPath}");
|
||||||
|
}
|
||||||
|
|
||||||
|
var searchOption = recursive ? SearchOption.AllDirectories : SearchOption.TopDirectoryOnly;
|
||||||
|
|
||||||
|
return SupportedExtensions
|
||||||
|
.SelectMany(ext => Directory.EnumerateFiles(directoryPath, $"*{ext}", searchOption))
|
||||||
|
.Where(IsSupportedAudioFile)
|
||||||
|
.Distinct(StringComparer.OrdinalIgnoreCase);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 判断文件是否为支持的音频格式
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="filePath">文件路径</param>
|
||||||
|
/// <returns>是否为支持的音频格式</returns>
|
||||||
|
public bool IsSupportedAudioFile(string filePath)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(filePath))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
var extension = Path.GetExtension(filePath).ToLowerInvariant();
|
||||||
|
return SupportedExtensions.Contains(extension);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,135 @@
|
|||||||
|
namespace OCES.Resonance.Core;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 音频库管理服务,协调扫描、读取和入库的完整流程
|
||||||
|
/// </summary>
|
||||||
|
public class AudioLibraryService
|
||||||
|
{
|
||||||
|
private readonly AudioFileScanner _scanner;
|
||||||
|
private readonly AudioMetadataReader _metadataReader;
|
||||||
|
|
||||||
|
public AudioLibraryService()
|
||||||
|
{
|
||||||
|
_scanner = new AudioFileScanner();
|
||||||
|
_metadataReader = new AudioMetadataReader();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 扫描目录并将结果添加到数据库
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="directoryPath">要扫描的目录路径</param>
|
||||||
|
/// <param name="databaseName">数据库名称(不含扩展名)</param>
|
||||||
|
/// <param name="skipExisting">是否跳过已存在的文件</param>
|
||||||
|
/// <returns>扫描结果统计</returns>
|
||||||
|
public async Task<ScanResult> ScanAndImportToLibrary(
|
||||||
|
string directoryPath,
|
||||||
|
string databaseName = "default",
|
||||||
|
bool skipExisting = true)
|
||||||
|
{
|
||||||
|
var result = new ScanResult
|
||||||
|
{
|
||||||
|
TotalFiles = 0,
|
||||||
|
SuccessCount = 0,
|
||||||
|
SkipCount = 0,
|
||||||
|
ErrorCount = 0,
|
||||||
|
Errors = new List<string>()
|
||||||
|
};
|
||||||
|
|
||||||
|
// 初始化数据库
|
||||||
|
Database.InitializeDatabase();
|
||||||
|
|
||||||
|
// 扫描所有音频文件
|
||||||
|
var audioFiles = _scanner.ScanDirectory(directoryPath).ToList();
|
||||||
|
result.TotalFiles = audioFiles.Count;
|
||||||
|
|
||||||
|
if (audioFiles.Count == 0)
|
||||||
|
{
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
var metadataList = new List<AudioFileMeta>();
|
||||||
|
|
||||||
|
// 读取每个文件的元数据
|
||||||
|
foreach (var filePath in audioFiles)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// 计算 MD5 用于查重
|
||||||
|
var md5 = CalculateMd5ForFile(filePath);
|
||||||
|
|
||||||
|
// 检查是否已存在
|
||||||
|
if (skipExisting && Database.EntryExists(md5, filePath))
|
||||||
|
{
|
||||||
|
result.SkipCount++;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 读取元数据
|
||||||
|
var metadata = _metadataReader.ReadMetadata(filePath);
|
||||||
|
metadataList.Add(metadata);
|
||||||
|
result.SuccessCount++;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
result.ErrorCount++;
|
||||||
|
result.Errors.Add($"读取失败 {filePath}: {ex.Message}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 批量入库
|
||||||
|
if (metadataList.Count > 0)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
Database.AddEntries(metadataList);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
result.Errors.Add($"批量入库失败:{ex.Message}");
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return await Task.FromResult(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 计算文件 MD5(用于查重,不创建完整 AudioFileMeta 对象)
|
||||||
|
/// </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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 扫描结果统计
|
||||||
|
/// </summary>
|
||||||
|
public class ScanResult
|
||||||
|
{
|
||||||
|
/// <summary>扫描到的文件总数</summary>
|
||||||
|
public int TotalFiles { get; set; }
|
||||||
|
|
||||||
|
/// <summary>成功入库的文件数</summary>
|
||||||
|
public int SuccessCount { get; set; }
|
||||||
|
|
||||||
|
/// <summary>跳过的文件数(已存在)</summary>
|
||||||
|
public int SkipCount { get; set; }
|
||||||
|
|
||||||
|
/// <summary>读取失败的文件数</summary>
|
||||||
|
public int ErrorCount { get; set; }
|
||||||
|
|
||||||
|
/// <summary>错误详情列表</summary>
|
||||||
|
public List<string> Errors { get; set; } = new();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 获取人类可读的统计报告
|
||||||
|
/// </summary>
|
||||||
|
public string GetSummary()
|
||||||
|
{
|
||||||
|
return $"扫描完成:共 {TotalFiles} 个文件,成功 {SuccessCount} 个,跳过 {SkipCount} 个,失败 {ErrorCount} 个";
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,173 @@
|
|||||||
|
using System.Security.Cryptography;
|
||||||
|
using ATL;
|
||||||
|
|
||||||
|
namespace OCES.Resonance.Core;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 音频元数据读取器,使用 ATL 库读取音频文件的技术参数和元数据
|
||||||
|
/// </summary>
|
||||||
|
public class AudioMetadataReader
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 从音频文件读取元数据
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="filePath">音频文件路径</param>
|
||||||
|
/// <returns>音频元数据对象</returns>
|
||||||
|
public AudioFileMeta ReadMetadata(string filePath)
|
||||||
|
{
|
||||||
|
if (!File.Exists(filePath))
|
||||||
|
{
|
||||||
|
throw new FileNotFoundException($"文件不存在:{filePath}");
|
||||||
|
}
|
||||||
|
|
||||||
|
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();
|
||||||
|
|
||||||
|
// 映射 ATL 元数据到 AudioFileMeta
|
||||||
|
var meta = new AudioFileMeta
|
||||||
|
{
|
||||||
|
// 必备条目
|
||||||
|
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,
|
||||||
|
DateAdded = DateTime.Now,
|
||||||
|
OriginalModificationDate = lastWriteTime,
|
||||||
|
OriginationTime = createTime,
|
||||||
|
|
||||||
|
// 可选条目
|
||||||
|
Bpm = track.BPM > 0 ? track.BPM : null,
|
||||||
|
Description = track.Description,
|
||||||
|
Genre = track.Genre,
|
||||||
|
Artist = track.Artist,
|
||||||
|
Composer = track.Composer,
|
||||||
|
Publisher = track.Publisher,
|
||||||
|
Copyright = track.Copyright,
|
||||||
|
|
||||||
|
// BWF 特定字段 (通过附加数据获取)
|
||||||
|
CodingHistory = GetBwfField(track, "CodingHistory"),
|
||||||
|
Originator = GetBwfField(track, "Originator"),
|
||||||
|
OriginatorRef = GetBwfField(track, "OriginatorRef"),
|
||||||
|
};
|
||||||
|
|
||||||
|
return meta;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 获取 BWF 字段值
|
||||||
|
/// </summary>
|
||||||
|
private static string? GetBwfField(Track track, string fieldName)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (track.AdditionalFields != null && track.AdditionalFields.TryGetValue(fieldName, out string? field))
|
||||||
|
{
|
||||||
|
return field;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
// 忽略 BWF 字段读取错误
|
||||||
|
}
|
||||||
|
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)
|
||||||
|
{
|
||||||
|
using var md5 = MD5.Create();
|
||||||
|
using var stream = File.OpenRead(filePath);
|
||||||
|
|
||||||
|
var hash = md5.ComputeHash(stream);
|
||||||
|
return BitConverter.ToString(hash).Replace("-", "").ToLowerInvariant();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 生成 UUID
|
||||||
|
/// </summary>
|
||||||
|
/// <returns>UUID 字符串</returns>
|
||||||
|
private static string GenerateUuid()
|
||||||
|
{
|
||||||
|
return Guid.NewGuid().ToString("N"); // 无连字符的 UUID
|
||||||
|
}
|
||||||
|
}
|
||||||
+231
-1
@@ -1,9 +1,239 @@
|
|||||||
|
using System.Data;
|
||||||
|
using System.Data.SQLite;
|
||||||
|
using Dapper;
|
||||||
|
|
||||||
namespace OCES.Resonance.Core;
|
namespace OCES.Resonance.Core;
|
||||||
|
|
||||||
public static class Database
|
public static class Database
|
||||||
{
|
{
|
||||||
internal static bool AddEntry()
|
static readonly string DefaultConnectionString = "Data Source=default.db;Version=3;";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 获取数据库连接
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="dbName">数据库名称(不含扩展名)</param>
|
||||||
|
/// <returns>数据库连接</returns>
|
||||||
|
public static IDbConnection GetConnection(string dbName = "default")
|
||||||
|
{
|
||||||
|
var connectionString = $"Data Source={dbName}.db;Version=3;";
|
||||||
|
return new SQLiteConnection(connectionString);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 初始化数据库表结构
|
||||||
|
/// </summary>
|
||||||
|
public static void InitializeDatabase()
|
||||||
|
{
|
||||||
|
using var connection = GetConnection();
|
||||||
|
connection.Open();
|
||||||
|
|
||||||
|
const string sql = @"
|
||||||
|
CREATE TABLE IF NOT EXISTS audio_files (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
unique_id TEXT NOT NULL UNIQUE,
|
||||||
|
short_id TEXT,
|
||||||
|
md5 TEXT NOT NULL,
|
||||||
|
path TEXT NOT NULL,
|
||||||
|
filename TEXT NOT NULL,
|
||||||
|
folder TEXT NOT NULL,
|
||||||
|
directory TEXT NOT NULL,
|
||||||
|
duration REAL NOT NULL,
|
||||||
|
total_samples INTEGER NOT NULL,
|
||||||
|
bit_depth INTEGER NOT NULL,
|
||||||
|
channels INTEGER NOT NULL,
|
||||||
|
sample_rate INTEGER NOT NULL,
|
||||||
|
type TEXT NOT NULL,
|
||||||
|
date_added TEXT NOT NULL,
|
||||||
|
original_modification_date TEXT NOT NULL,
|
||||||
|
origination_time TEXT NOT NULL,
|
||||||
|
|
||||||
|
bpm REAL,
|
||||||
|
frame_rate TEXT,
|
||||||
|
timecode INTEGER,
|
||||||
|
description TEXT,
|
||||||
|
category TEXT,
|
||||||
|
subcategory TEXT,
|
||||||
|
cat_id TEXT,
|
||||||
|
category_full TEXT,
|
||||||
|
genre TEXT,
|
||||||
|
style TEXT,
|
||||||
|
mood TEXT,
|
||||||
|
keywords TEXT,
|
||||||
|
rating INTEGER,
|
||||||
|
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,
|
||||||
|
track_title TEXT,
|
||||||
|
episode TEXT,
|
||||||
|
scene TEXT,
|
||||||
|
take TEXT,
|
||||||
|
tape TEXT,
|
||||||
|
cue_number INTEGER,
|
||||||
|
sync_point INTEGER,
|
||||||
|
release_date TEXT,
|
||||||
|
track_year TEXT,
|
||||||
|
is_edited INTEGER,
|
||||||
|
is_split INTEGER,
|
||||||
|
location TEXT,
|
||||||
|
group TEXT,
|
||||||
|
markers TEXT,
|
||||||
|
comments TEXT,
|
||||||
|
notes TEXT,
|
||||||
|
copyright TEXT,
|
||||||
|
coding_history TEXT,
|
||||||
|
microphone TEXT,
|
||||||
|
mic_perspective TEXT,
|
||||||
|
user1 TEXT,
|
||||||
|
user2 TEXT,
|
||||||
|
user3 TEXT,
|
||||||
|
user4 TEXT,
|
||||||
|
user5 TEXT,
|
||||||
|
user6 TEXT,
|
||||||
|
user7 TEXT,
|
||||||
|
user8 TEXT,
|
||||||
|
|
||||||
|
INDEX idx_md5 (md5),
|
||||||
|
INDEX idx_path (path),
|
||||||
|
INDEX idx_unique_id (unique_id)
|
||||||
|
);
|
||||||
|
";
|
||||||
|
|
||||||
|
connection.Execute(sql);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 添加单条音频记录
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="meta">音频元数据</param>
|
||||||
|
/// <returns>是否添加成功</returns>
|
||||||
|
public static bool AddEntry(AudioFileMeta meta)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
using var connection = GetConnection();
|
||||||
|
connection.Open();
|
||||||
|
|
||||||
|
const string sql = @"
|
||||||
|
INSERT INTO audio_files (
|
||||||
|
unique_id, short_id, md5, path, filename, folder, directory,
|
||||||
|
duration, total_samples, bit_depth, channels, sample_rate, type,
|
||||||
|
date_added, original_modification_date, origination_time,
|
||||||
|
bpm, frame_rate, timecode, description, category, subcategory,
|
||||||
|
cat_id, category_full, genre, style, mood, keywords, rating,
|
||||||
|
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,
|
||||||
|
markers, comments, notes, copyright, coding_history, microphone,
|
||||||
|
mic_perspective, user1, user2, user3, user4, user5, user6, user7, user8
|
||||||
|
) VALUES (
|
||||||
|
@UniqueId, @ShortId, @Md5, @Path, @Filename, @Folder, @Directory,
|
||||||
|
@Duration, @TotalSamples, @BitDepth, @Channels, @SampleRate, @Type,
|
||||||
|
@DateAdded, @OriginalModificationDate, @OriginationTime,
|
||||||
|
@Bpm, @FrameRate, @Timecode, @Description, @Category, @Subcategory,
|
||||||
|
@CatId, @CategoryFull, @Genre, @Style, @Mood, @Keywords, @Rating,
|
||||||
|
@Artist, @Composer, @Designer, @Recordist, @Publisher, @Manufacturer,
|
||||||
|
@Originator, @OriginatorRef, @ProjectName, @Library, @CdTitle,
|
||||||
|
@TrackTitle, @Episode, @Scene, @Take, @Tape, @CueNumber, @SyncPoint,
|
||||||
|
@ReleaseDate, @TrackYear, @IsEdited, @IsSplit, @Location, @Group,
|
||||||
|
@Markers, @Comments, @Notes, @Copyright, @CodingHistory, @Microphone,
|
||||||
|
@MicPerspective, @User1, @User2, @User3, @User4, @User5, @User6, @User7, @User8
|
||||||
|
);
|
||||||
|
";
|
||||||
|
|
||||||
|
connection.Execute(sql, meta);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
catch (Exception)
|
||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 批量添加音频记录
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="entries">音频元数据列表</param>
|
||||||
|
/// <returns>成功添加的记录数</returns>
|
||||||
|
public static int AddEntries(IEnumerable<AudioFileMeta> entries)
|
||||||
|
{
|
||||||
|
var count = 0;
|
||||||
|
using var connection = GetConnection();
|
||||||
|
connection.Open();
|
||||||
|
|
||||||
|
using var transaction = connection.BeginTransaction();
|
||||||
|
try
|
||||||
|
{
|
||||||
|
const string sql = @"
|
||||||
|
INSERT INTO audio_files (
|
||||||
|
unique_id, short_id, md5, path, filename, folder, directory,
|
||||||
|
duration, total_samples, bit_depth, channels, sample_rate, type,
|
||||||
|
date_added, original_modification_date, origination_time,
|
||||||
|
bpm, frame_rate, timecode, description, category, subcategory,
|
||||||
|
cat_id, category_full, genre, style, mood, keywords, rating,
|
||||||
|
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,
|
||||||
|
markers, comments, notes, copyright, coding_history, microphone,
|
||||||
|
mic_perspective, user1, user2, user3, user4, user5, user6, user7, user8
|
||||||
|
) VALUES (
|
||||||
|
@UniqueId, @ShortId, @Md5, @Path, @Filename, @Folder, @Directory,
|
||||||
|
@Duration, @TotalSamples, @BitDepth, @Channels, @SampleRate, @Type,
|
||||||
|
@DateAdded, @OriginalModificationDate, @OriginationTime,
|
||||||
|
@Bpm, @FrameRate, @Timecode, @Description, @Category, @Subcategory,
|
||||||
|
@CatId, @CategoryFull, @Genre, @Style, @Mood, @Keywords, @Rating,
|
||||||
|
@Artist, @Composer, @Designer, @Recordist, @Publisher, @Manufacturer,
|
||||||
|
@Originator, @OriginatorRef, @ProjectName, @Library, @CdTitle,
|
||||||
|
@TrackTitle, @Episode, @Scene, @Take, @Tape, @CueNumber, @SyncPoint,
|
||||||
|
@ReleaseDate, @TrackYear, @IsEdited, @IsSplit, @Location, @Group,
|
||||||
|
@Markers, @Comments, @Notes, @Copyright, @CodingHistory, @Microphone,
|
||||||
|
@MicPerspective, @User1, @User2, @User3, @User4, @User5, @User6, @User7, @User8
|
||||||
|
);
|
||||||
|
";
|
||||||
|
|
||||||
|
foreach (var entry in entries)
|
||||||
|
{
|
||||||
|
connection.Execute(sql, entry, transaction);
|
||||||
|
count++;
|
||||||
|
}
|
||||||
|
|
||||||
|
transaction.Commit();
|
||||||
|
}
|
||||||
|
catch (Exception)
|
||||||
|
{
|
||||||
|
transaction.Rollback();
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
|
||||||
|
return count;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 检查记录是否已存在
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="md5">文件 MD5 值</param>
|
||||||
|
/// <param name="filePath">文件路径</param>
|
||||||
|
/// <returns>是否存在</returns>
|
||||||
|
public static bool EntryExists(string md5, string filePath)
|
||||||
|
{
|
||||||
|
using var connection = GetConnection();
|
||||||
|
connection.Open();
|
||||||
|
|
||||||
|
const string sql = @"
|
||||||
|
SELECT COUNT(*) FROM audio_files
|
||||||
|
WHERE md5 = @Md5 OR path = @Path;
|
||||||
|
";
|
||||||
|
|
||||||
|
var count = connection.ExecuteScalar<int>(sql, new { Md5 = md5, Path = filePath });
|
||||||
|
return count > 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user